替换平面 RPM 仓库的元数据生成工具,保留发布时间、签名与客户端缓存兼容性。
这是本节的多页打印视图。 .
教程
- 1: 搭建 YUM 仓库
- 2: 迁移已有 YUM 仓库
- 3: 搭建 APT 仓库
- 4: 仓库签名
- 5: 服务与发布仓库
- 6: 演练构建 pigsty-infra 仓库
本节介绍仓库创建、签名、发布与更新。尚未安装 SOW 时,先看安装。
创建 RPM 架构视图,过滤软件包,设置版本上限并配置 DNF。
创建 DEB 包池与 by-hash 索引,并配置 APT 客户端。
签署元数据与 RPM 软件包,再配置客户端验签。
发布到 filesystem 目标,并用 Nginx 提供服务。
同时使用两种格式与架构,测试安装,维护滚动频道、稳定发布与月度快照。
使用示例
按顺序执行命令,把大写占位符与 /path/to/ 输入路径换成实际值。/srv 下的示例需要可写目录,也可以换成用户主目录下的路径。
YUM 与 APT 教程分别从新工作区开始。如果要合并使用,在已有 pigsty 仓库中创建第二个 Dist,并在 sow.yml 保留两个 Dist 的定义。
各教程都包含结果检查。命令报错或操作中断时,参见故障处理。
1 - 搭建 YUM 仓库
本教程从零创建一个 Managed RPM 仓库。先安装 SOW、Python 3,并准备适合目标客户端的 RPM 文件。使用当前用户可创建、可写入的空目录 /srv/sow,或换成其他可写路径。
1. 创建工作区
el9 是这个 RPM 包集合的名称,可以按需要更改。它不改变软件包的操作系统兼容性。
2. 配置成员策略
编辑生成的 sow.yml。下面的配置按包名与架构各保留一个版本,并排除调试包与 LLVM JIT 包:
检查配置:
策略先执行 exclude,再执行 limit。SOW 把 noarch 包加入每个启用的架构视图。architectures 中只填写 CPU 架构名。
3. 添加 RPM
把输入路径换成实际 RPM 目录。add 将接受的软件包存入包池,更新成员并构建索引。检查逐包输出:被排除的文件会单独报告,不会使整条命令失败。
公共树如下:
RPM 索引通过相对路径指向 pool/。服务或复制完整的 pigsty/ 目录树,客户端才能下载软件包。
4. HTTP 预览
在另一个终端执行以下命令,并保持运行:
预览只提供仓库根目录中的文件,并且只能从本机访问。长期运行或供其他主机访问时,按对外服务配置。
5. 配置 dnf
在运行预览的 Linux 主机上,把以下内容保存为 /etc/yum.repos.d/pigsty.repo。远程客户端需先发布仓库,再换成其公共地址:
刷新并查询仓库:
本例使用未签名软件包与元数据。按仓库签名启用验签。预览完成后,删除此源配置文件并停止 Python 服务器。
6. 发布或导出
使用 sow publish 发布到 filesystem 或 R2 目标。它按客户端所需的顺序上传软件包与元数据。隔离主机的手工交付步骤也见该教程。
默认 dnf reposync 等工具会拒绝指向根包池的父级相对路径。需要供这些工具使用时,导出一份
独立 RPM 仓库:
目标目录必须不存在或为空。导出默认复制软件包。--hardlink 只用于同一文件系统;导出文件与源包池共享存储,应保持只读。
更新仓库
add 会自动构建更新。配置 limit: 1 后,同包名、同架构的最新版本会替换此前成员。修改配置后执行 sow build -r pigsty。
撤下软件包时,先用 sow rm PACKAGE_NAME -r pigsty -d el9 --check 预览选择结果,再去掉 --check 执行。包名会选中其全部版本与架构;只删除一个包对象时,使用 SHA-256 引用。
已测试客户端见平台与集成。
2 - 迁移已有 YUM 仓库
本指南适用于 RPM 与 repodata/ 放在同一层目录的平面仓库,例如此前用 createrepo_c
维护的仓库。它使用 Plain sow create,不创建 Managed 工作区。发布时间参数是
SOW 0.5.0 新增功能,请使用该版本或更新版本。
1. 准备工作副本
备份当前软件包与元数据,再准备独立工作副本用于迁移。计划重签的 RPM 使用独立复制,不使用硬链接。检查已安装的二进制:
确认版本为 0.5.0 或更新版本。
| 检查项 | 操作 |
|---|---|
| 软件包布局 | Plain 只读取顶层普通 RPM;嵌套目录需要单独规划迁移 |
| 客户端功能 | Plain 生成 primary/filelists/other XML,不生成 SQLite 数据库或模块流 |
| 包信任 | 保留合法上游签名,或明确换成自己的密钥 |
| 索引信任 | 客户端启用 repo_gpgcheck=1 时,单独签署并验证新 repomd.xml |
| 已有客户端 | 保留旧缓存,使用原来的 URL 和 repo ID 验收 |
客户端使用模块过滤时,按需要保留仓库原有的 module_hotfixes=1 设置。Plain 元数据不包含模块流或 profile。
2. 保留发布时间历史
EL7 YUM 可能拒绝最大 data 时间戳早于缓存的新索引。读取当前 repomd.xml 与保留副本中的 <data><timestamp>,取客户端可能见过的最大值。如果在线索引被重置为零,从旧副本或发布记录恢复这个值。使用 data 时间戳,不使用 revision 或文件修改时间。
维护脚本需串行执行更新,并在服务目录之外保存最大发布时间。内容变化时,选择:
切换公开索引前保存所选时间戳。发布失败可以使序列跳号。恢复旧内容或迁移本地目录时,保留这份记录。+1 用于区分发布;EL7 也接受相等时间戳。
内容不变时,检查索引、引用文件校验和与签名后复用时间戳。零时间戳或已倒退的时间戳需要修复。每次传入当前时间都会改变 repomd.xml,此时 create 就无法报告空操作。
3. 构建并签署候选
下面演示一次重建。先准备独立的 /srv/yum.candidate 副本。
将 HISTORICAL_MAX 设为从当前索引、已保存的最大发布时间和可信历史中核实的最大值。
将 SIGN_KEY 设为元数据签名密钥的完整指纹。
示例用 Python 3 选择严格递增的时间:
发布候选目录前,在维护系统中记录 PUBLISH_TIME。核对 gpg --verify 显示的指纹与预期签名密钥是否一致。
--sign-with KEY 签署未签名 RPM,并保留已有签名,包括供应商签名。要把全部包签名换成自己的密钥,在候选副本中增加 --overwrite。这会改变包文件;create 根据签名后的结果生成索引。最后一次 create 完成后,再生成索引的分离签名。
4. 测试候选仓库
让测试客户端通过原来的仓库 URL 访问候选目录。保留该客户端的旧缓存、仓库 ID 与签名设置, 然后刷新索引、下载并安装一个包。这样可以同时检查缓存兼容性、下载路径与签名。
如果迁移的是自动更新脚本,再检查两种情况:
- 输入、选项与时间戳不变时,重复构建报告
noop=true。保留已有的有效索引签名。 - 包内容变化时,脚本增加时间戳,同一个客户端能获取新版软件包。
与 createrepo_c 比较时,检查依赖含义与客户端行为;XML 排版可以不同。
详情见平台与集成。
5. 发布仓库
Plain 会替换多个文件,不提供整个目录的原子切换。发布层需要协调包文件、索引、分离签名以及 缓存的可见顺序。仍可能被缓存客户端请求的旧元数据文件应继续保留;直接运行 Plain 重建,可能 删除目录中不再引用的 SOW 以校验和命名的元数据。
保留原始备份与已保存的最大发布时间。回滚软件包内容时,把它当成更晚的一次发布重新建库、签名。只恢复旧
repomd.xml、却搭配已经改变的 RPM,会重新引入校验和或时间戳问题。
Plain 仓库使用上述检查;sow check 用于 Managed 仓库。命令参数见 sow create,包签名与元数据签名见仓库签名。
3 - 搭建 APT 仓库
本教程从零创建一个 Managed DEB 仓库。先安装 SOW、Python 3,并准备适合目标客户端的 DEB 文件。使用当前用户可创建、可写入的空目录 /srv/sow,或换成其他可写路径。
1. 创建工作区
Dist 名称 trixie 对应客户端配置中的 Suites,可以按需要更改。它不改变软件包的发行版兼容性。
2. 配置成员策略
如果需要过滤或限制版本,编辑生成的 sow.yml:
然后校验:
配置中的 x86_64、aarch64 分别生成 Debian 的 amd64、arm64 索引。
架构无关的 all 包进入两个视图。
3. 添加 DEB
把输入路径换成实际 DEB 目录。add 将接受的软件包保存一次,并构建索引。公共目录树如下:
Packages 中的 Filename 相对于仓库根目录。保留完整目录树,客户端才能找到软件包。SOW 生成 by-hash 索引,供 APT 按内容哈希获取元数据。
4. HTTP 预览
在另一个终端执行以下命令,并保持运行:
预览只提供仓库根目录中的文件,并且只能从本机访问。长期运行或供其他主机访问时,按对外服务配置。
5. 配置 APT
在运行预览的 Linux 主机上,把以下内容保存为 /etc/apt/sources.list.d/pigsty.sources。ARM64 客户端将 Architectures 改为 arm64。远程客户端需先发布仓库,再换成其公共地址:
刷新并查询:
输出中应出现 trixie/main 的软件包来源。Trusted: yes 用于本例的未签名仓库。
需要签名时,按仓库签名生成签名、安装公钥并配置 Signed-By。
预览完成后,删除测试源配置文件并停止 Python 服务器。
6. 发布仓库
使用 sow publish 发布到 filesystem 或 R2 目标。它先上传软件包与索引,再更新 Release 及其签名。隔离主机的手工交付步骤也见该教程。
更新仓库
add 会自动构建更新。配置 limit: 1 后,同包名、同架构的最新版本会替换此前成员。修改配置后执行 sow build -r pigsty。
撤下软件包时,先用 sow rm PACKAGE_NAME -r pigsty -d trixie --check 预览选择结果,再去掉 --check 执行。包名会选中其全部版本与架构;只删除一个包对象时,使用 SHA-256 引用。
已测试客户端见平台与集成。
4 - 仓库签名
仓库签名分为元数据签名与 RPM 软件包签名两层,需要分别配置:
| 路径 | 产物 | 客户端开关 |
|---|---|---|
| RPM 元数据 | repodata/repomd.xml.asc |
repo_gpgcheck=1 |
| APT 元数据 | InRelease 与 Release.gpg |
Signed-By |
| RPM 包体 | RPM 内嵌签名 | gpgcheck=1 |
APT 通过签名的 Release 文件验证包哈希,因此 DEB 不需要单独的包签名。先配置元数据签名;客户端要求 gpgcheck=1 时,再配置 RPM 软件包签名。
本教程使用 YUM 或 APT 教程中的 pigsty 仓库。只执行你已创建的格式与 Dist 所对应的命令。
1. 创建专用密钥
先安装 GPG。下面生成一把无口令的测试密钥,并导出到私有目录。已有带口令密钥时,按下文配置 passphrase 引用。
SOW 从公共仓库目录之外的 /srv/sow-secrets 读取私钥。如果 SOW 服务账户不同于当前用户,给该账户目录访问权限。向客户端分发 repo-signing.pub。
2. 配置元数据签名
在 /srv/sow/sow.yml 中,将以下元数据配置合并到仓库已有的 signing 块。保留包签名策略与 Dist 定义,只加入使用的格式:
校验配置、重新构建并检查:
带口令的密钥在 key 旁增加 passphrase: env://SOW_METADATA_PASSPHRASE,并向 SOW 提供该环境变量。也可以使用口令文件,或通过 agent://FINGERPRINT 调用 GPG 签名。详情见 passphrase 引用。配置只保存引用,不保存秘密值。
3. 可选:查看元数据签名者
前面的 sow check 已验证元数据签名。需要用 GPG 查看签名者时,执行对应 Dist 的命令:
将显示的指纹与分发给客户端的公钥指纹比较。
4. 可选:签署 RPM 包体
新仓库应在导入 RPM 前配置包签名。在已有 signing.rpm 下增加以下配置:
将占位符替换为 $FPR 中的 40 位十六进制指纹。安装 rpm 与 gpg,
并将私钥放在 rpm 使用的 GPG 密钥环中。
fill 会签署新导入的包;如果已有签名能通过配置密钥或 trusted_keys 验证,则保留原签名。
其他模式见 RPM 签名策略。
SOW 在 add 时签署私有副本,输入文件保持不变。build 检查包池中的 RPM 是否满足策略,不会重签它们。
已有 RPM 时,先检查签名再启用 fill。把信任的上游公钥保留在 trusted_keys 中。对于包池中未签名或不受信任的包,准备使用新版本、新文件名且签名正确的包;也可以先配置签名,再把原始包导入新仓库。
配置策略后,导入新包并检查结果:
需要独立查看包签名时,对 pigsty/pool/ 中的包文件执行 rpmkeys --checksig。
5. 启用 dnf 验签
先发布已签名仓库,再通过可信通道把公钥传到客户端:
再打开与你实际签名范围对应的检查:
只签名元数据时,使用 repo_gpgcheck=1 与 gpgcheck=0。启用包验签时,除了自己的公钥,也要安装所信任的上游包签名公钥。随后执行 sudo dnf makecache --refresh 并安装一个包。
6. 启用 APT 验签
发布已签名仓库后,在客户端将公钥安装为独立密钥环:
在 deb822 配置中引用它,且不要设置 Trusted: yes:
执行 sudo apt update 并安装一个包。验签失败时,核对客户端公钥环指纹与 gpg --verify 显示的签名者是否一致。ARM64 客户端将 Architectures 改为 arm64。
Plain 模式 RPM 签名
Plain 模式可以签署 RPM 包体,但不会签署仓库元数据,也不会生成 APT Release:
将 0123456789ABCDEF 换成你的密钥 ID 或指纹,匹配的私钥必须能被 rpm 与 GPG 使用。
命令保留已有签名,包括上游签名。需要全部换成自己的密钥时,在独立工作副本上使用 --overwrite。
密钥格式见 sow create。
已有 YUM 仓库按迁移指南保留发布时间,
并在最后一次 create 完成后签署 repomd.xml。
更换密钥
轮换元数据密钥时,先分发新公钥,再更新密钥引用,执行 build、check 与 publish。引用或解析出的指纹变化后,相关 Dist 会变为 dirty。
轮换 RPM 包密钥时,使用 fill,并在 trusted_keys 中保留旧公钥,直到旧密钥签署的软件包已撤下或替换。在此期间,也保留客户端对应的公钥。已有包对象保持不变。
用用户实际使用的 DNF 或 APT 版本测试最终仓库。自动化覆盖见平台与集成。
5 - 服务与发布仓库
SOW 生成静态仓库文件。本教程把 Managed 仓库发布到独立目录,再用 Nginx 提供服务。先完成第一个工作区。
源目录是 /srv/sow/local,公开副本将位于 /srv/repo-public/local。
两者都包含完整的 pool/ 与 dists/ 目录树。工作区配置与私有状态保留在服务目录之外。
1. 构建并检查
check 成功后继续。
2. 添加 filesystem 目标
创建目标父目录,并给 SOW 操作者写权限:
如果执行 sow publish 的账户不同,使用该账户。endpoint 必须已存在,并使用不含符号链接的物理路径。
在 /srv/sow/sow.yml 添加目标:
SOW 根据 endpoint 与 prefix 创建 /srv/repo-public/local。三个必填布尔值声明:本工作区是该目标的权威来源,也是唯一写入者。其他工具使用独立前缀。
本例通过 file:// 检查发布文件,并声明没有中间缓存。以后增加代理或 CDN 时,把 public_endpoint 改为公共仓库 URL,
把 max_cache_ttl 设为其最长缓存时间,并通过 publish --rebind 应用变更。
SOW 据此检查交付结果,并确定旧文件何时可以删除。
发布仓库:
SOW 先复制包文件与不可变元数据,再更新客户端入口,随后检查结果并记录发布快照。内容不变时重复发布会报告空操作。
3. 配置 Nginx
安装 Nginx,添加以下服务器配置,并把 repo.example.com 换成你的主机名:
检查并重载配置:
上述命令假定 Nginx 已运行。首次安装时,在 nginx -t 成功后,用操作系统的服务管理器启动 Nginx。
客户端地址如下:
| 客户端 | 地址 |
|---|---|
DNF baseurl |
http://repo.example.com/local/dists/el9/$basearch/ |
APT URIs |
http://repo.example.com/local |
APT Suites / Components |
bookworm / main |
完整源配置见 YUM 或 APT 教程。需要验签时,分发公钥并按仓库签名配置。
4. 检查客户端访问
用上表地址配置客户端,刷新索引并安装一个软件包。
这会检查 dists/ 与 pool/ 是否都能访问。两个目录使用相同的访问规则。
发布更新
修改成员或配置后,构建尚未完成的变更,再发布:
发布中断时,重新执行 sow publish public 继续。恢复与目标变更见 sow publish;缓存宽限期后的清理见 sow gc。
交付到隔离主机
SOW 无法连接目标时,按以下步骤复制。创建并检查副本期间,暂停源工作区的写入。
- 在源端执行
sow check。 - 把完整仓库复制到目标的新发布目录。保留硬链接,例如使用
rsync -aH或tar。 - 根据
sow changes 0或归档校验清单检查传输后的文件。 - 将服务器路径切换到新发布目录。
- 切换后,继续让旧索引引用的软件包 URL 和不可变元数据 URL 可访问,直到最长缓存期结束,再清理旧发布。
在此期间,HTTP 服务可将新目录中缺失的路径回退到上一发布目录。
直接覆盖在线目录,可能在包文件到达前暴露新索引。先准备新目录,再整体切换。
提供 Plain 仓库
Plain 模式直接提供传给 sow create 的目录。使用上例 Nginx 隐藏文件规则,
防止访问临时文件。
发布到 R2
Cloudflare R2 使用 provider: r2。配置 endpoint、bucket、凭据引用、公共 URL 与缓存时间,详见目标配置参考。先发布到测试前缀,再从实际包管理器检查。
R2 目标垃圾回收只报告待删除对象,SOW 不执行远端删除。已测试的存储操作见平台与集成。
6 - 演练构建 pigsty-infra 仓库
pgsty/infra-pkg 是 Pigsty Infra 软件包的上游构建源码。
本教程使用已构建的 x86-64 与 ARM64 RPM、DEB,创建包含滚动频道、稳定发布与月度快照的 infra 仓库。先安装 SOW、jq 与 Docker。安装示例使用 pg-exporter;请包含该包,以及客户端镜像中尚未安装的依赖。
1. 把包集中到 ~/repo
为本工作区使用新的 ~/repo 目录。从 infra-pkg 源码目录复制软件包到两个输入目录:
输入需同时包含 x86-64 与 ARM64 包。第 3 步会按包内元数据检查格式与架构。
2. 创建 infra 仓库与两个 Dist
初始化工作区,并创建名为 infra 的仓库:
在 ~/repo/sow.yml 中给两个 Dist 设置 limit: 1:
limit: 1 按“包名 + 原生架构”只保留最新一个版本。因此 rpm 与 deb 就是两个滚动更新的
最新版本频道;它们仍会同时保留 x86-64 与 ARM64 两个架构。
3. 一次性导入并构建
先导入两种格式的软件包,再统一构建索引:
sow check 成功后,列出 SOW 从包内元数据读出的格式与架构:
预期至少出现:
SOW 使用规范化架构名,因此 DEB 的 amd64/arm64 在这里显示为 x86_64/aarch64。
4. 看懂生成的目录
关键结构应当是:
SOW 把所有 Dist 放在 dists/ 下,把软件包放在 pool/ 下。发布或挂载完整的 ~/repo/infra 目录树,让每个 Dist 都能访问其软件包。
5. 用 Nginx 只读服务仓库
使用官方 nginx:alpine 镜像,把仓库根目录只读挂载到 /infra:
Nginx 只看得到 ~/repo/infra,既看不到 sow.yml 与 .sow/,也无法修改仓库。
6. 在 EL9 中只用 infra 安装 RPM
下面的 Rocky Linux 9 容器位于 --internal 网络中。脚本先删除所有预置仓库,再只启用刚创建的
infra,因此成功安装不能依赖公网软件源。
RPM 的 baseurl 必须落到具体架构视图。$basearch 会由 dnf 展开为 x86_64 或 aarch64。
7. 在 Ubuntu 24.04 中只用 infra 安装 DEB
APT 的 URI 指向仓库根目录,Suites 才是 Dist 名 deb:
本例使用未签名仓库。按仓库签名配置后,为 RPM 元数据启用 repo_gpgcheck=1,为 APT 配置 Signed-By。RPM 包也有签名时,再启用 gpgcheck=1。从已签名 APT 源中删除 Trusted: yes。
默认情况下,Docker 容器使用宿主机架构。需要测试另一架构时,给对应的 docker run
增加 --platform linux/amd64 或 --platform linux/arm64;跨架构运行需要 binfmt/QEMU 支持。
8. 日常维护:添加一个新版本
更新仓库时直接执行 add。假设已经拿到新版 pg-exporter 的四个软件包:
因为 latest Dist 配了 limit: 1,新版本胜出后,旧版本会自动退出该 Dist 的成员集合。
旧包文件不会被立即删除,也不会因为以后放宽策略而自动回来。
可以重新运行第 6、7 节的客户端,先 makecache/update,再安装或升级,完成更新验收。
撤下错误软件包时,用 sow ls -r infra -d rpm --json 找到其 SHA-256。先执行 sow rm sha256:DIGEST -r infra -d rpm --check 预览,再去掉 --check 执行。DEB 使用 -d deb。SHA-256 引用选中一个包对象;裸包名会选中其全部版本与架构。未使用的包池文件由 sow gc 单独回收。
9. 两层保留策略:latest 与 stable
rpm、deb 的 limit: 1 适合持续滚动,但不能表达“保留所有正式发布历史”。为此再创建两个 Dist:
新 Dist 的默认 limit 是 0,表示保留所有版本。最终策略是:
| Dist | 格式 | limit |
角色 |
|---|---|---|---|
rpm |
RPM | 1 | RPM latest |
deb |
DEB | 1 | DEB latest |
rpm-stable |
RPM | 0 | 累积所有已晋升 RPM |
deb-stable |
DEB | 0 | 累积所有已晋升 DEB |
stable 从创建后开始累积每次明确晋升的版本。
10. 把 latest 晋升到 stable
把源 Dist 的包池文件加入目标 Dist,即可晋升软件包。SOW 校验并复用已有包对象,不增加第二份文件。导出清单前暂停其他仓库写入任务,晋升完成后再恢复。
先确保源状态干净,并保存本次晋升清单:
把清单中的包池对象加入 stable Dist:
每次 add 应报告 reused。若中途失败,源 Dist 不受影响;修复问题后对同一清单重跑即可。
随着以后重复晋升,rpm/deb 仍只保留最新版本,而 rpm-stable/deb-stable 会逐次累积历史版本。
11. 从 stable 创建 2026-08 快照
客户端可见的月度快照也是两个新的 Dist:
创建快照期间,暂停其他仓库写入任务。先保存 stable 的包清单:
再把清单加入对应快照 Dist:
如需在将来删除快照 Dist 后仍保留这次构建,另外记录当前仓库构建快照:
retain 保存本次构建的元数据,并防止相关包文件被垃圾回收。客户端仍通过快照 Dist 访问。
SOW 不会将这些 Dist 设为只读;创建后保留其成员与生效配置,内容才会固定。
12. 客户端地址总表
同一个 Nginx 与同一份 infra/pool 支撑所有频道:
| 频道 | dnf baseurl |
APT URIs / Suites |
|---|---|---|
| latest | http://infra-nginx/infra/dists/rpm/$basearch/ |
http://infra-nginx/infra / deb |
| stable | http://infra-nginx/infra/dists/rpm-stable/$basearch/ |
http://infra-nginx/infra / deb-stable |
| 2026-08 | http://infra-nginx/infra/dists/rpm-202608/$basearch/ |
http://infra-nginx/infra / deb-202608 |
用 sow dist ls -r infra 查看全部六个 Dist。
滚动 Dist 保留最新软件包,stable Dist 累积已晋升版本,月度 Dist 保留选定快照。所有视图共用同一个包池。
实验结束后可停止临时服务: