1. 内网装软件这件事,绕不开本地源
标题里这套组合,我给不少机房做过:一台装好 openEuler 22.03 的机器当源服务器,用 httpd 把光盘或 ISO 里的软件包目录发布出去,局域网里其他机器把 repo 指向这台服务器,从此装包不再看外网脸色。听着简单,真做起来有几个地方特别容易翻车,比如源目录权限、SELinux 拦截、repo 文件里的 gpgcheck 与 baseurl 组合写错,以及明明 curl 能打开页面,yum 却报 404。这篇文章我打算把整套流程从零走一遍,顺带把每一步为什么这么做、哪一步最容易出错讲透,适合手上有一批内网机器需要统一装包环境的运维、开发、以及刚接手机房的新人。
先说清楚这个方案能解决什么。openEuler 22.03 的官方软件仓库在公网上,内网机器如果完全不通外网,yum install、dnf install全部会失败;有些机房虽然能通外网,但带宽很小或者策略上不允许业务机器直连,每次装个编译工具链都要等半天,甚至因为 DNS 抖动把事务搞坏。把 ISO 镜像或同步下来的一份仓库放到局域网一台机器上,用 http 发布出来,所有机器就近下载,速度是内网千兆甚至万兆的水平,装一个 gcc 基本是秒级完成,而且版本完全受控——镜像不变,仓库就永远不变,不会出现今天装的和下周装的依赖版本不一致这种莫名其妙的问题。
再说明一点边界:这件事本质上是搭一个只读的静态文件服务,不是什么复杂系统。httpd 在这里的角色就是把一个目录映射成 URL,yum/dnf 的角色就是按 URL 去拉repodata/repomd.xml以及后续的 rpm 包。所有排错思路都可以围绕“这个 URL 能不能取到文件”展开,一旦想通这一点,后面九成的报错都能自己定位。我见过不少人把它想复杂了,去折腾缓存、折腾元数据生成,其实只要你手上的源目录结构是完整的,httpd 配一个 DocumentRoot 就够了。
下面按我实际交付的顺序展开:先确认源目录从哪来、结构长什么样,再装 httpd 并做最小必要配置,然后写 repo 文件并验证,最后集中处理那几个高频坑,包括防火墙、SELinux、目录权限和 repo 语法。每一步我都尽量给出可直接复制的命令和判断标准,你照着做,遇到不符合预期的输出,就对照我写的判断逻辑去找问题。
2. 源目录从哪来:光盘、ISO 还是同步仓库
2.1 先看清 openEuler 22.03 的仓库目录结构
在动手配 httpd 之前,必须先把“源”这个东西弄清楚。openEuler 22.03 的安装介质里,仓库结构的核心是repodata目录,它存放repomd.xml、primary.xml.gz、filelists.xml.gz等元数据文件。yum/dnf 的工作方式是先请求repodata/repomd.xml,从中解析出其他元数据的位置,再根据元数据里记录的相对路径去下载具体的 rpm。所以一个目录能不能当源,判断标准非常硬:该目录下必须存在repodata/repomd.xml。只要这个文件在,且 http 能访问到,基本就成了;反过来,目录里堆了一堆 rpm 但没有repodata,yum 是绝不会认的。
openEuler 22.03 的 ISO 挂载之后,通常能看到类似这样的顶层结构:根目录下有Packages(或按架构分的目录)、repodata、EFI、images、isolinux等。不同构建版本细节会有差异,有的把 rpm 全都放在一个平铺的Packages里,有的按模块分目录,但repodata一定在仓库根这一层。你要发布给局域网用的,就是这个“仓库根”,而不是 ISO 挂载点的上一层。我见过有人把 http 的 DocumentRoot 指向/mnt/iso的父目录,结果 baseurl 写成了http://ip/xxx/,yum 报找不到 repomd,其实是多套了一层路径。
如果你手上没有 ISO,而是想同步一份完整仓库,那思路就不一样了。openEuler 的仓库是可以整体拉下来的,但这件事对带宽和磁盘要求比较高,一般在内网机器上做的话,我会选一台有外网出口的机器先同步,再把目录整体搬到源服务器,或者干脆让源服务器临时通外网同步完再断网。这里的关键是目录结构必须完整保留,不要用那种只下载 rpm 的方式,repodata丢了就全白搭。
2.2 挂载 ISO 并确认 repodata 存在
假设你手上有 openEuler 22.03 的 ISO 文件,放在/data/iso/下。第一步是挂载,这里有两个选择:临时挂载和持久挂载。临时挂载适合先验证、后面再改成永久;持久挂载适合长期当源用。我个人的习惯是先临时挂上去看清楚结构,确认无误后再写进fstab,避免一上来就改配置文件把机器搞出别的问题。
# 创建挂载点 mkdir -p /mnt/openeuler-iso # 临时挂载 ISO(只读,loop 设备) mount -o loop,ro /data/iso/openEuler-22.03-LTS-everything-x86_64-dvd.iso /mnt/openeuler-iso # 确认挂载结果 df -h | grep openeuler ls /mnt/openeuler-iso挂载成功后,立刻检查一个关键文件:
ls -l /mnt/openeuler-iso/repodata/repomd.xml如果这个文件存在且非空,说明这个 ISO 是合格的源。如果repodata目录不存在,那你拿到的可能不是 DVD 或 everything 版本,而是某种精简镜像,或者你把挂载点指错了。这一步的判断没有任何模糊空间,repomd.xml在不在,就是源目录能不能用的唯一标准。
需要提醒的是,mount -o loop挂载的 ISO 在重启后会失效。如果希望长期有效,要在/etc/fstab里加一行,写法是:
/data/iso/openEuler-22.03-LTS-everything-x86_64-dvd.iso /mnt/openeuler-iso iso9660 loop,ro 0 0加完用mount -a验证一遍,不报错、df -h能看到挂载点,就说明这一行写对了。这里我踩过的坑是:有的机器上fstab里 loop 挂载和nofail参数配合不好,开机会卡在挂载阶段;如果是不关键的环境,加个nofail更保险,虽然源挂不上导致装包失败,但至少不会让机器起不来。
2.3 直接把仓库目录放到磁盘上,而不是发布挂载点
这一步很多人会略过,但我觉得它值得单独说:不要把 httpd 的 DocumentRoot 直接指向 ISO 挂载点。原因有两个。一是 ISO 是只读的,挂载点本身的权限和 SELinux 上下文可能跟你预期不一样,httpd 访问时容易被拒;二是以后你要往仓库里补几个额外的 rpm,只读挂载根本改不了,只能重新做 ISO,非常别扭。
更稳的做法是把仓库内容复制到本地磁盘的一个目录,比如/var/www/repo/openeuler22.03/,然后把这个目录发布出去。复制的方式有两种,考虑磁盘占用:
| 方式 | 命令思路 | 优点 | 缺点 |
|---|---|---|---|
| 完整复制 | cp -a整个挂载点内容 | 目录结构原样保留,后续可增删 | 占用磁盘等于 ISO 内容大小 |
| 复制关键部分 | 只复制Packages、repodata等 | 省空间 | 容易漏文件,导致 yum 报缺包 |
如果磁盘足够,我强烈建议完整复制。几十 GB 的 everything 镜像占用的空间,在现在这个年代不算什么,但能省掉大量“为什么这个包找不到”的排查时间。用cp -a而不是cp -r,因为-a会保留权限、时间戳、符号链接等属性,虽然对这个场景影响不大,但养成习惯没坏处。
mkdir -p /var/www/repo/openeuler22.03 cp -a /mnt/openeuler-iso/. /var/www/repo/openeuler22.03/ ls /var/www/repo/openeuler22.03/repodata/repomd.xml复制完再检查一次repomd.xml是否存在,这一步是为了确认复制过程没有因为磁盘满、权限中断而漏掉东西。复制几十 GB 的时候建议用df -h盯着点剩余空间,磁盘写满导致复制中断、repodata不完整,是后面 yum 报各种元数据解析错误的经典原因。
3. httpd 的安装与最小配置思路
3.1 装 httpd 之前先确认软件包从哪来
这里有个先有鸡还是先有蛋的问题:你要用 yum 装 httpd,但本地源还没搭好。解决办法很简单,源服务器这台机器在搭建阶段可以临时用外网源,或者直接从 ISO 里用 rpm 安装。openEuler 22.03 默认的 repo 配置在/etc/yum.repos.d/下,如果机器能通外网,直接dnf install -y httpd就行;如果不能通外网,也可以先临时挂载 ISO 配一个本地 file 源,把 httpd 装上,装完再切成 http 源。
# 能通外网的情况 dnf install -y httpd # 查看装出来的版本 rpm -q httpd装完之后确认服务状态和监听端口。openEuler 22.03 上 httpd 的默认配置监听 80,配置文件在/etc/httpd/conf/httpd.conf,默认的 DocumentRoot 是/var/www/html。我们不打算改这个全局 DocumentRoot,而是用更清晰的方式——加一个独立的配置文件,把仓库目录映射出去。这样做的好处是配置之间互不影响,以后要加别的站点或目录也方便。
3.2 为什么用独立 conf 而不是改主配置
/etc/httpd/conf.d/下的.conf文件会被主配置自动 include,这是 httpd 的标准机制。把仓库的配置单独写成一个文件,比如/etc/httpd/conf.d/repo.conf,有几个实际好处。第一,主配置保持干净,出问题时可以直接把整个repo.conf删掉回到初始状态,不用在一大坨配置里找自己改过哪几行。第二,方便迁移,换机器时把这个文件带走就行。第三,便于做目录级的访问控制,比如你只想让局域网某个网段访问,限制写在这个文件里比写主配置更清晰。
配置内容我一般这么写,用Alias把 URL 路径映射到真实目录,比改 DocumentRoot 更精准:
# /etc/httpd/conf.d/repo.conf Alias /openeuler22.03 /var/www/repo/openeuler22.03 <Directory "/var/www/repo/openeuler22.03"> Options Indexes FollowSymLinks AllowOverride None Require all granted </Directory>逐行解释一下为什么这么写。Alias的作用是当请求http://服务器IP/openeuler22.03/...时,实际去读/var/www/repo/openeuler22.03/...。这样 URL 路径短,repo 文件里写 baseurl 也干净。Options Indexes允许目录列表,方便你用浏览器打开 URL 直接看到文件列表,验证起来很直观;FollowSymLinks是为了以后万一目录里用了符号链接不会出问题。AllowOverride None表示不读目录里的.htaccess,仓库是只读的静态文件,没必要开。Require all granted是关键,不放行的话所有请求都是 403。
注意:如果你在 httpd 2.4 之前的语法习惯里写
Order allow,deny之类,在 openEuler 22.03 的 httpd 上会直接报语法错误,因为 2.4 已经改成Require指令。改完配置后必须用httpd -t做语法检查,别直接重启。
配置写完,先别急着启动服务,做三件事:语法检查、SELinux 上下文处理、防火墙放行。前一件是防止配置错,后两件是防止服务起来了但访问不到——这两个坑我在下面单独拆。
# 语法检查 httpd -t # 启动并设置开机自启 systemctl enable --now httpd systemctl status httpdhttpd -t的输出如果是Syntax OK,说明配置本身没问题。启动后用ss -lntp | grep :80确认监听上了,再用本机curl -I http://127.0.0.1/openeuler22.03/repodata/repomd.xml验证文件能不能取到。本机 curl 通了,才轮到考虑其他机器访问的问题;本机都不通,先解决本机。
4. SELinux 与防火墙:两个最容易让服务“看起来启动成功但访问不到”的原因
4.1 SELinux 上下文不匹配的典型报错
openEuler 22.03 默认启用 SELinux,且一般是 enforcing 模式。httpd 能不能读一个目录里的文件,不只看文件权限,还要看文件的 SELinux 上下文标签。你从 ISO 复制出来的东西,上下文是iso9660_t或类似的类型,不是 httpd 期望的httpd_sys_content_t,于是会出现这样的现象:ls -l看权限是-rw-r--r--,本机curl直接 403,/var/log/httpd/error_log里写着Permission denied。很多人第一反应是去改chmod 777,改完发现还是 403,因为根本不是普通权限的问题。
正确的处理方式是给目录打上 httpd 可读的标签:
# 查看当前上下文 ls -Zd /var/www/repo/openeuler22.03 # 递归打上 httpd 可读标签 semanage fcontext -a -t httpd_sys_content_t "/var/www/repo(/.*)?" restorecon -Rv /var/www/reposemanage需要policycoreutils-python-utils这个包提供,如果命令不存在,先装上。restorecon -Rv会按规则重新打标签,输出里能看到每个文件被处理的记录。处理完再用ls -Zd确认标签变成了httpd_sys_content_t。
提示:
/var/www子目录在大部分默认策略里其实已经有对应规则,但如果你把仓库放在别的路径,比如/data/repo,那就必须手动加规则,否则一定 403。我习惯不管放在哪都显式加一条,省得依赖默认策略,换机器时也不会因为默认策略差异翻车。
如果你想确认问题到底是不是 SELinux 引起的,可以临时把 SELinux 设成 permissive 模式验证:setenforce 0,然后再访问。如果访问立刻正常了,那就是上下文问题,按上面的方法处理后再setenforce 1恢复。不要长期把 SELinux 关掉来“解决”问题,这个做法在内部环境里可能没人管你,但它会掩盖真正的问题,而且以后遇到同类现象就没有判断依据了。
4.2 防火墙放行要区分 firewalld 是否启用
openEuler 22.03 默认用 firewalld。判断是否启用不需要猜,直接看服务状态:
systemctl is-active firewalld firewall-cmd --state如果返回running,就要把 80 端口放行到对应的 zone。默认网卡一般落在publiczone 上,所以:
firewall-cmd --permanent --add-service=http firewall-cmd --reload firewall-cmd --list-services--permanent和--reload的顺序是固定的:先写永久规则,再重新加载才生效。直接--add-service不加--permanent只在当前运行时生效,重启机器就没了,这是很多人配完当天能用、第二天失联的原因。放行后从另一台机器用curl -I试,如果本机通、别机不通,先查防火墙,再查httpd是否监听在0.0.0.0而不是只绑了127.0.0.1。
如果内网机器上 firewalld 是被禁用的,那就不用管这一节,但建议先确认一下,不要凭印象。我遇到过一台机器上 firewalld 是 disabled 的,但另装了别的防火墙规则集,端口照样被拦着,最后是靠ss -lntp加对方机器telnet才定位到。判断链路通不通,从服务端看监听、从客户端看可达,两头对,是最快的定位方式。
5. 客户端 repo 文件的写法与验证闭环
5.1 baseurl 到底该写到哪一层
这是最容易写错的一处,必须讲清楚。baseurl指向的是包含repodata的那个目录,也就是说 URL 打开后下面应该能直接看到repodata/。按照我们前面的 Alias 配置,仓库根是/var/www/repo/openeuler22.03,对应 URL 是http://源服务器IP/openeuler22.03/。所以客户端的 repo 文件应该这么写:
# /etc/yum.repos.d/openeuler-local.repo [openeuler-local] name=openEuler 22.03 Local Repo baseurl=http://192.168.10.20/openeuler22.03/ enabled=1 gpgcheck=0这里的gpgcheck=0是有意为之。本地源的可信度来自网络边界,内网自己发布的仓库,逐个校验 GPG 签名反而会因为没有导入对应 key 导致装包失败。如果你们的规范要求开启校验,那就得同时配gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-openEuler并把 key 文件放到对应位置。小环境里我一般先关校验把流程跑通,后面有合规要求再开,这样能把“装不上”和“验签失败”两种问题分开排。
写 baseurl 有几个常见错误,列出来对照:
| 错误写法 | 现象 | 原因 |
|---|---|---|
http://ip/ | 404 或找不到 repomd | 少了 Alias 路径那一层 |
http://ip/openeuler22.03/repodata/ | 找不到 repomd | 多写了一层,源根就是 repodata 的父目录 |
http://ip/openeuler22.03(末尾无斜杠) | 部分场景正常、部分 404 | 拼接相对路径时行为不一致,建议带上斜杠 |
| 用主机名但没配 DNS | 解析失败 | 客户端解析不到主机名,改用 IP 或补 hosts |
一个很实用的验证方法:在客户端直接curl -I http://192.168.10.20/openeuler22.03/repodata/repomd.xml,返回200就说明 URL 层级对;返回403就回去查 SELinux 和目录权限;返回404就回去查 Alias 和目录是否存在。这一步比直接dnf更快定位问题,因为它只验证 URL,不掺和 yum 的缓存和元数据逻辑。
5.2 清缓存、更新元数据并做一次真实安装验证
repo 文件写好后,不要立刻dnf install,先清缓存再生成元数据:
dnf clean all dnf makecache dnf repolist -vdnf repolist -v会打印每个仓库的 id、名称、baseurl、包数量等信息。看到包数量是一个合理的大数字(everything 镜像一般是几千到上万个包),说明元数据拉取成功。如果包数量是 0 或者仓库根本没列出来,就回头看enabled=1是否写对、文件后缀是否.repo、文件里有没有语法错误。
真正的验收标准是装一个包,而且要选一个能在内网源里找到、又不至于影响系统的东西,比如tree、lsof这种小工具:
dnf install -y tree rpm -q tree装成功之后,建议看看它是从哪个源装的,用dnf repoquery --info tree或者装的时候不看-y走一遍确认提示。如果输出里显示的是你的本地源名字,闭环就完成了。这一步别省,因为有些环境里本地源配上了,但默认仓库还在、优先级又乱,实际装包还是从外网拉的,等于没切成内网。我一般会临时把外网 DNS 或网关断掉测一次,能装上就说明真的只走本地源了。
6. 排错实录:几个让人抓狂的现象与定位链路
6.1 403、404、空 repolist 三类报错怎么分
这三类现象表面看都是“装不上”,但根因完全不同,混在一起查会浪费大量时间。我的分法是先定位到 http 层,再定位到 yum 层。
| 现象 | 定位命令 | 常见根因 |
|---|---|---|
| curl 返回 403 | ls -l+ls -Z+tail /var/log/httpd/error_log | SELinux 上下文不对、目录权限不足 |
| curl 返回 404 | ls目标路径 +httpd -S看 Alias 生效情况 | Alias 路径与实际目录不一致、复制漏目录 |
| curl 200 但 repolist 为空 | dnf repolist -v看 baseurl | repo 文件语法错、后缀不对、enabled=0 |
| repolist 有包但 install 报找不到 | dnf search 包名 | 包不在该镜像里、yum 缓存旧数据 |
httpd -S这个命令值得单独提一句,它会打印虚拟主机和 Alias 的解析结果,能直接看到你写的 Alias 到底有没有被加载、映射到了哪个目录。如果repo.conf文件名不以.conf结尾,主配置不会 include,httpd -t也不报错,但就是访问不到——这种情况用httpd -S一眼就能看出 Alias 没生效。
tail /var/log/httpd/error_log是排 403 的核心依据。SELinux 拦截时这里会出现很明确的记录,比如(13)Permission denied: access to /xxx denied,配合audit.log里的AVC记录可以确认是 SELinux 干的。如果没有 AVC 记录、错误日志里也是普通权限拒绝,那就去看目录的属主和属组,httpd进程默认以apache用户运行,目录至少要让它有读和执行权限,路径上每一层目录都不能缺执行位。
6.2 目录权限的坑:路径上每一层都要能被穿透
这个坑我踩过不止一次。文件本身-rw-r--r--没问题,但父目录权限是700且属主不是apache,httpd 照样进不去。对 httpd 来说,要读到/var/www/repo/openeuler22.03/repodata/repomd.xml,从/开始每一级目录都需要有执行(x)权限,因为执行位在目录上表示“可以进入/查找”。所以排查时不能只看最后那个文件,要沿着路径一路看:
namei -l /var/www/repo/openeuler22.03/repodata/repomd.xmlnamei -l会把路径上每一级的权限和属主都列出来,一眼就能看到哪一级卡住了。这个命令在排文件访问问题时比ls逐层看高效得多,基本是必用工具。
修复方式不是无脑 777,而是保证 httpd 用户的可读可穿透。常见做法是把目录属组设成apache,给组读和执行,或者简单点给o+rx:
chmod -R o+rX /var/www/repo/openeuler22.03注意这里用的是大写的X,它的含义是只对目录和本来就有执行位的文件加执行权限,不会把数据文件变成可执行,比chmod -R 755干净。用-R 777在内部源上虽然能通,但会让整个仓库变成谁都能改,长期看是个隐患,没必要。
6.3 火墙之外还有一层:httpd 只监听了本机回环
还有一类问题容易被误判成防火墙:httpd的Listen配置被改成Listen 127.0.0.1:80,导致本机 curl 通、别的机器怎么都连不上。判断方法就是看监听地址:
ss -lntp | grep :80输出里如果是127.0.0.1:80而不是0.0.0.0:80或*:80,那就是绑定了回环。改法是在/etc/httpd/conf/httpd.conf里确认Listen 80没有被改成绑回环的形式,改完重启服务再验证。这类问题在接手别人配过的机器时特别常见,因为前一个人可能为了“安全”只让本机访问,后面没人记得这事。
另外值得留意systemctl status httpd里有没有 warning,比如端口冲突、模块加载失败。有些环境里装了别的 web 服务占着 80,httpd 启动“成功”但实际没抢到端口,ss一下就能看出来,别只看active (running)就以为万事大吉。
7. 长期可用的几个加固与维护习惯
7.1 把仓库目录的访问范围收窄
内网源虽然只在内网,但也不是所有网段都该能访问。如果你们的网络分了业务区和办公区,可以只放行业务网段,用Require ip收窄:
<Directory "/var/www/repo/openeuler22.03"> Options Indexes FollowSymLinks AllowOverride None Require ip 192.168.10.0/24 </Directory>这样其他网段访问会得到 403。改完httpd -t检查、systemctl reload httpd生效。注意reload比restart温和,不会中断已有连接,配置类改动优先用 reload。这个收窄不是必须的,但如果你们对访问范围有要求,加上更稳。
7.2 仓库更新的方式:换目录、保留旧版本回退
本地源用久了会有更新需求,比如你需要补几个新版本包或者换一个新的 ISO。直接覆盖原目录风险比较大,一旦新镜像有问题,所有机器都没法装包,而且旧包被覆盖后也没法回退。我习惯的做法是目录按版本或日期命名,比如/var/www/repo/openeuler22.03-202406和...-202412,Alias 先用软链指向当前版本,要回退就把软链切回去。
ln -sfn /var/www/repo/openeuler22.03-202412 /var/www/repo/current对应的 Alias 指向/var/www/repo/current,客户端的 baseurl 保持不变。切换时记得对软链本身和目标目录都处理好 SELinux 上下文,软链换了之后restorecon再跑一次,避免新目录标签不到位。这个方式的好处是客户端配置完全不用动,服务端切换是原子的,出问题切回来就行。
7.3 客户端缓存与“为什么新包装不上”
本地源更新之后,客户端常见现象是dnf search找不到新包。原因是客户端有本地元数据缓存,需要清掉重新拉:
dnf clean all dnf makecache如果还是找不到,检查repolist -v里显示的包数量有没有变化,没有变化就说明客户端根本没拉到新元数据,可能是浏览器或中间的缓存,也可能是 baseurl 指到了旧目录。我一般会在更新完源之后,随便找一台客户端做一次dnf clean all && dnf makecache验证,确认新包能被搜到,再通知其他机器清缓存。这个动作不大,但能省掉大量“我这边装不上”的来回沟通。
还有一点值得提:如果你们内网有多个源服务器或代理叠加,客户端配置里可能出现多个 repo 指向不同源,这时候优先级和元数据版本差异会导致一些莫名其妙的行为。把客户端yum.repos.d里的内容清干净,只留指向当前有效源的配置,是让环境保持可预测的最简单办法。环境越复杂,配置越要少而确定。