first commit

This commit is contained in:
warlock
2026-07-26 18:07:14 +08:00
commit 712cf1449b
4 changed files with 896 additions and 0 deletions

228
2.0.md Normal file
View File

@@ -0,0 +1,228 @@
1. 产品定位与核心理念
`RepoGuard` 是一个 **免安装、单兵作战、环境无感的 OS 仓库质量校验工具** 。
* **环境无关(Host-Agnostic)** :它可以运行在任何 RPM-based 操作系统(EulerOS, openEuler, CentOS, Rocky, RHEL 等)的物理机、虚拟机或容器内。
* ~~**零侵入性(Zero-Intrusion)** :引入 **DNF `--installroot` 虚拟根目录**技术。所有的动态安装、卸载、升降级测试都在指定的隔离目录下进行,不修改物理机真实的系统文件。可选择选项。~~
* **极简依赖** :采用 Python 3 编写,仅依赖系统自带的 `python3`、`dnf`、`rpm` 命令行,无需复杂的第三方库部署,拷贝即用。
## 2. 软件总体架构
`RepoGuard` 作为一个独立的 CLI 工具,其内部逻辑分为 **控制面** 和 **系统交互面** :
```
+-----------------------------------------------------------------------+
| RepoGuard CLI |
| (解析命令行参数、加载 repoguard.yaml、调度各测试模块) |
+-----------------------------------------------------------------------+
|
+-------------------------+-------------------------+
| |
+---------------------------------+ +---------------------------------+
| 控制面 (Control Plane) | | 系统交互面 (System Plane) |
| | | |
| * 增量/全量算法计算 | | * DNF Installroot 沙箱管理器 |
| * Koji 溯源 API 客户端 | | * 临时 Repo 配置文件动态生成 |
| * 测试结果收集与格式化 (JSON) | | * 原生 rpm/dnf 子进程调用 |
+---------------------------------+ +---------------------------------+
|
v
+-------------------------+
| 物理机/容器/VM 文件系统 |
| |
| /var/tmp/repoguard/ |
| └── sandbox_root/ | <-- 虚拟的 OS Root
| ├── etc/yum.repos | (所有 DNF 事务
| ├── usr/lib/ | 在此发生,
| └── var/lib/rpm/ | 不伤及真机)
+-------------------------+
```
## 3. 核心功能模块设计
程序由 5 个核心技术模块组成,均继承自抽象基类 `BaseModule`。
### 3.1 宿主安全沙箱模块 (`SandboxManager`)
* **设计目的** :由于工具可能在实体物理机上运行,必须防止 `dnf install/remove` 破坏宿主系统。
* **技术实现** :
1. 在用户指定的临时工作区(如 `/var/tmp/repoguard/sandbox`)创建一个空的目录结构。
2. 利用本地主机的 RPM 数据库初始化该目录:
**Bash**
```
rpm --root /var/tmp/repoguard/sandbox --initdb
```
3. 后续所有的 DNF 事务,强制追加 `--installroot=/var/tmp/repoguard/sandbox` 参数。
4. 所有的软件包安装、依赖解析、降级全部在这套虚拟的 `sandbox` 文件系统中发生,对物理机没有任何副作用。
### 3.2 仓库完整性与元数据校验模块 (`MetadataChecker`)
* **设计目的** :在不依赖任何第三方 Python XML 库的前提下,校验元数据与物理文件的单/双向一致性^^。
* **核心逻辑**^^:
1. **元数据爬取** :从 `update_test` 仓库的 URL 下载 `repomd.xml`^^。
2. **流式解析** :使用 Python 标准库 `xml.etree.ElementTree` 解析并提取 `primary.xml.gz`^^。
3. **双向一致性审计** :
* **正向** :遍历 XML 中每一条 `<package>` 记录,校验物理 RPM 文件的哈希值(SHA256)与 XML 中的记录是否绝对一致^^。
* **反向** :扫描本地同步的 RPM 缓存目录,发现任何“未在 XML 中注册”的孤儿包,直接报错。
1. **签名校验** :调用 `rpm -K <rpm_file>`,验证 GPG 签名链合规性^^。
### 3.3 静态依赖闭合校验模块 (`DependencyChecker`)
* **设计目的** :静态分析目标仓库是否存在任何依赖断链、冲突^^。
* **核心逻辑**^^:
1. 将 `base`、`updates`、`update_test` 动态写入沙箱目录下的 `etc/yum.repos.d/repoguard.repo` 中^^。
2. 运行系统命令,指定 `--installroot`:
**Bash**
```
dnf --installroot=/var/tmp/repoguard/sandbox repoclosure --repoid=KOS-2.0-Updates-Test
```
3. 捕获标准输出,若发现 `unresolved` 或未闭合的 `Requires` 依赖,提取包名和具体缺失对象,转化为 JSON 报告^^。
### 3.4 动态生命周期模拟模块 (`LifecycleTester`)
* **设计目的** :测试软件包在真实升级、安装、卸载链路中的表现。
* **核心逻辑**^^:
* **精准安装与卸载** :
1. 针对待测包列表(全量或增量),依次在沙箱内运行:
**Bash**
```
dnf --installroot=/var/tmp/repoguard/sandbox install -y <package_name>
```
2. 抓取 DNF 事务日志。若安装成功,立即执行:
**Bash**
```
dnf --installroot=/var/tmp/repoguard/sandbox remove -y <package_name>
```
3. 验证卸载后,沙箱系统的基础 RPM 数据库未发生崩溃。
* **同源二进制包(SRPM)联升级测试** :
1. 从 `primary.xml` 中将所有子包按源码包(`sourcerpm` 属性)归类。
2. 升级时,将该 `sourcerpm` 下属于同一个仓库的所有已安装二进制子包**作为整体事务**升级,避免因部分子包未升级导致的文件冲突。
### 3.5 关联产品/大版本升级模块 (`UpgradeAnalyzer`)
* **设计目的** :在物理机上模拟用户执行跨大版本的产品升级测试^^。
* **核心逻辑** :
1. 在沙箱(Installroot)中首先模拟安装旧版本操作系统的核心基础包(如 `basesystem`、`filesystem`)。
2. 将 Repo 源切换至新版本的目标测试仓库。
3. 运行 `dnf --installroot=... system-upgrade` 或 `dnf --installroot=... distro-sync`。
4. 校验整个升级事务是否可以无冲突、无依赖断裂地顺利解析。
## 4. 全量(Full)与增量(Incremental)测试机制
针对每周发布更新频繁的问题,程序在数据层设计了“测试集动态裁剪”算法,避免在物理机上进行冗余的重复测试。
### 4.1 增量包识别数学模型
假设上周发布的稳定更新源为 **$U_{last}$**,本周待测试的源为 **$U_{test}$**^^。
对于每一个 RPM 软件包 **$p$**,其唯一标识为四元组:**$(Name, Epoch, Version, Release)$**(即 NEVR)。
待测试的增量包集合 **$P_{pending}$** 的生成算法:
$$
P_{pending} = \{ p \in U_{test} \mid p \notin U_{last} \lor (p.Name \in U_{last}.Names \land p.EVR > U_{last}.EVR(p.Name)) \}
$$
* **新增包** :**$p$** 在上周稳定源中不存在。
* **升级包** :**$p$** 在上周稳定源中存在,但本周测试源中的版本(EVR)更高^^。
### 4.2 两种模式的动作矩阵
* **全量测试(`--mode full`)** :对 **$U_{test}$** 中的所有软件包进行静态、依赖、安装、升降级全链路验证。
* **增量测试(`--mode incremental`)** :
* 元数据校验、依赖闭合校验:对 **$U_{test}$** 运行 **全量静态扫描** (确保新包没有搞坏老包的依赖)^^。
* 安装、卸载、升降级:仅对计算出来的 **$P_{pending}$** **增量包列表**执行动态 DNF 事务测试(大幅减少物理机磁盘 IO 和时间开销)^^。
## 5. 命令行接口(CLI)与配置规范
为了让程序在物理机终端上极为好用,提供标准而强大的 CLI 接口:
**Bash**
```
# 1. 帮助菜单
repoguard --help
# 2. 物理机上安全运行增量测试(指定自定义 installroot 沙箱路径)
repoguard --config ./repoguard.yaml \
--mode incremental \
--installroot /var/tmp/my_safe_sandbox \
--output-dir ./results
# 3. 强制全量测试
repoguard -c ./repoguard.yaml -m full -o ./reports
```
### 5.1 命令行参数设计
* `-c, --config`:指定配置文件路径(默认:`./repoguard.yaml`)。
* `-m, --mode`:运行模式,可选 `full` 或 `incremental`^^。
* `-i, --installroot`:指定安全沙箱目录(默认:`/var/tmp/repoguard/sandbox`)。如果用户确信在一次性丢弃的虚拟机里运行,可以传入 `/` 直接在宿主机系统测试。
* `-o, --output-dir`:测试报告输出目录。
## 6. 输出规范(JSON / HTML)
`RepoGuard` 执行完毕后,会在 `--output-dir` 下生成两个文件:`report.json` 和 `report.html`^^。
### 6.1 `report.json` 结构
包含极其详尽的报错日志,支持 CI 系统和自动化脚本二次解析^^:
**JSON**
```
{
"summary": {
"product": "KOS",
"version": "2.0",
"arch": "loongarch64",
"mode": "incremental",
"start_time": "2026-07-17T17:00:00Z",
"duration_seconds": 321.4,
"status": "FAILED"
},
"modules": {
"metadata_checker": {
"status": "PASSED",
"metrics": { "total_checked": 4510, "unsigned_packages": 0 }
},
"dependency_checker": {
"status": "PASSED",
"metrics": { "unresolved_deps_count": 0 }
},
"lifecycle_tester": {
"status": "FAILED",
"metrics": { "installed": 124, "upgraded": 124, "failed": 1 },
"failures": [
{
"package": "openssl-perf-3.0.7-5.ko.la64",
"action": "upgrade",
"reason": "FileConflict",
"details": "file /usr/bin/openssl_benchmark conflicts between openssl-perf-3.0.7-5.ko.la64 and openssl-tools-3.0.7-1.ko.la64"
}
]
}
}
}
```
### 6.2 `report.html` 可视化设计
* **不依赖网络** :不加载任何外部 CDN 的 CSS 或 JS,所有的样式表和图表(用纯 CSS 绘制)均内嵌在 HTML 文件中,确保在**无网状态下的物理实体机**上双击也能完美排版、查看。
* **功能区** :
1. **看板区** :红/绿状态大卡片,一眼看出是否通过。
2. **筛选器** :支持点击 "Only Show Failed" 过滤,快速定位问题包。
3. **日志折叠** :点击报错的软件包,展开 DNF 执行过程的完整 Stdout/Stderr 日志,方便开发人员直接复制报错进行 Debug。

View File

@@ -0,0 +1,440 @@
# RPM 仓库发布测试平台 — 详细产品设计
版本:v0.1 日期:2026-07-26
---
## 1. 背景与目标
多个产品线维护自己的 RPM 仓库,仓库结构统一分为:
- **base 仓库**:单版本,只发布一次(首次 GA)。
- **update 仓库**:多版本,每周增量发布。每次发布同时产出两个仓库:
- `updates`:正式对外发布仓库;
- `updates-test`:先行验证仓库(全量仓库,不是增量),用于在推正式仓库前跑一遍完整回归。
仓库下按架构(x86_64 / aarch64 / loongarch64 / mips64 等,架构集合不固定,需可配置)分别组织。
目标是构建一个**独立运行的、模块化的自动化测试程序**,在每次仓库发布前(base 全量发布 / update 每周增量发布)对仓库做完整性、正确性、可用性验证,产出结构化 + 可视化报告,可跑在物理机或容器中,可接入 CI。
---
## 2. 术语与核心抽象
| 术语 | 定义 |
|---|---|
| **Product(产品)** | 一条产品线,如 "OS-Server",包含 1 个 base + 1 组 update/update-test,按架构展开 |
| **Repo(仓库)** | 某产品在某架构下的一个 RPM 仓库(本地路径或 URL),有 `repo_type ∈ {base, update, update-test}` |
| **SRPM 单元** | 一个源码包(含其版本),是**测试调度的最小单元** |
| **Binary Set** | 某 SRPM 在某架构仓库下对应构建出的全部二进制 RPM(含 debuginfo/devel 子包等) |
| **TestUnit** | `(product, repo, arch, srpm_name, srpm_version)` 五元组,一次测试运行会展开成若干 TestUnit |
| **TestRun** | 一次完整的测试执行(全量或增量),有唯一 run_id,产出一份报告 |
| **Baseline 快照** | 上一次测试运行时仓库的 repodata 快照,用于增量 diff 出"本周变化的 SRPM" |
---
## 3. 总体架构
```
┌───────────────────────────────────────────────────────────────────────┐
│ CLI / 入口 (rpm-repo-tester) │
├───────────────────────────────────────────────────────────────────────┤
│ 配置解析层 ConfigResolver │
│ CLI 参数 > 环境变量 > 配置文件(YAML) > 内置默认值 (deep-merge) │
├───────────────────────────────────────────────────────────────────────┤
│ 产品/仓库模型层 Inventory │
│ Product → Repo(arch, type, url/path) → SRPM 清单 → Binary 清单 │
│ (通过 repodata 解析获得,支持本地路径 / http(s) / rsync 挂载路径) │
├───────────────────────────────────────────────────────────────────────┤
│ 差异计算层 DiffEngine(仅增量模式使用) │
│ baseline snapshot vs 当前 repodata → 变化的 SRPM 集合 │
├───────────────────────────────────────────────────────────────────────┤
│ 调度层 Orchestrator │
│ 生成 TestUnit 列表 → 按 (module × unit) 建执行计划 → 并发调度 │
│ → 环境隔离 (per-arch sandbox: container/chroot) → 超时/重试 │
├───────────────────────────────────────────────────────────────────────┤
│ 测试模块层 Test Modules(插件化,统一接口) │
│ M1 仓库完整性 │ M2 元数据检查 │ M3 repodata-package一致性 │
│ M4 安装/卸载 │ M5 升级/降级 │ M6 签名校验 │
│ M7 Koji哈希比对 │ M8 依赖检查 │ M9 关联产品版本比对 │ M10 产品升级测试 │
├───────────────────────────────────────────────────────────────────────┤
│ 执行适配层 Adapters │
│ dnf/yum CLI 封装 │ createrepo_c/repoquery │ rpm/gpg │ koji API 客户端 │
│ 容器/chroot 驱动 (podman/docker, systemd-nspawn 可选) │
├───────────────────────────────────────────────────────────────────────┤
│ 结果存储层 ResultStore │
│ 每个 ModuleResult 落盘 (SQLite + 原始日志文件) │
├───────────────────────────────────────────────────────────────────────┤
│ 报告层 Reporters(可插拔) │
│ JSON(默认)│ HTML(默认,可视化)│ JUnit XML(可选,CI用)│ Markdown │
└───────────────────────────────────────────────────────────────────────┘
```
---
## 4. 配置体系设计
### 4.1 优先级与合并规则
`最终配置 = merge(默认值, 配置文件, 环境变量, CLI参数)`,后者覆盖前者,且只覆盖**显式设置**的字段(CLI 参数默认值为 `None`/`sentinel`,未传值不参与覆盖)。
### 4.2 配置文件示例(YAML)
```yaml
# config.yaml
run:
mode: incremental # full | incremental
concurrency: 8
timeout_per_unit: 900 # 秒
workdir: /var/lib/rpm-repo-tester
products:
- name: os-server
arches: [x86_64, aarch64, loongarch64]
repos:
base:
path_template: "/repos/os-server/{arch}/base"
update:
path_template: "/repos/os-server/{arch}/updates"
update_test:
path_template: "/repos/os-server/{arch}/updates-test"
related_products: # 关联产品版本比对用
- name: os-server-kernel-modules
compare_rules:
- source: kernel
target: kernel-modules
rule: "exact_version_match"
koji:
hub_url: "https://koji.example.com/kojihub"
auth: token # token | ssl_cert | anonymous
token_env: KOJI_TOKEN
modules:
repo_integrity: {enabled: true}
repo_metadata: {enabled: true}
repodata_consistency: {enabled: true}
install_remove: {enabled: true, sandbox: container}
upgrade_downgrade: {enabled: true, sandbox: container, previous_version_source: base}
signature: {enabled: true, trusted_keyring: /etc/pki/rpm-gpg/}
koji_hash: {enabled: true}
dependency: {enabled: true, allow_missing: []}
cross_product_version: {enabled: true}
product_upgrade: {enabled: false} # 默认关,按需开启,成本较高
sandbox:
driver: podman # podman | docker | chroot
image_map:
x86_64: registry.local/rpm-tester/x86_64:latest
aarch64: registry.local/rpm-tester/aarch64:latest
loongarch64: registry.local/rpm-tester/loongarch64:latest
network: none # 强制仅使用受测repo,不联外网
report:
formats: [json, html]
output_dir: ./report
history_dir: ./history # 保存历次 baseline 快照
```
### 4.3 环境变量映射
约定前缀 `RPMTEST_`,用 `__` 表示层级,如:
```
RPMTEST_RUN__MODE=full
RPMTEST_KOJI__TOKEN_ENV=KOJI_TOKEN
RPMTEST_SANDBOX__DRIVER=docker
```
### 4.4 CLI 参数(最高优先级,仅覆盖显式传入项)
```
rpm-repo-tester run \
--config config.yaml \
--mode incremental \
--product os-server \
--arch x86_64,aarch64 \
--only-modules install_remove,signature \
--concurrency 16 \
--output-dir ./report/2026-07-26
```
---
## 5. 仓库/产品数据模型
```python
@dataclass
class Repo:
product: str
arch: str
repo_type: Literal["base", "update", "update-test"]
location: str # 本地路径或 URL
repomd_checksum: str | None = None
@dataclass
class SrpmUnit:
name: str
version: str
release: str
repo: Repo
binaries: list["BinaryPkg"] # 该 srpm 在该 arch/repo 下对应的全部二进制包
@dataclass
class BinaryPkg:
nevra: str
filename: str
size: int
checksum: str
checksum_type: str
signature_keyid: str | None
```
数据来源:解析各 repo 的 `repodata/repomd.xml → primary.xml(.gz) → filelists/other`,反查 rpm 文件的 `sourcerpm` header 做 SRPM→Binary 分组(比 primary.xml 里的 sourcerpm 字段更权威,直接读 rpm header 兜底)。
---
## 6. 全量 / 增量测试策略
### 6.1 全量测试(Full)
- 场景:base 首次发布;update-test 仓库(本身即全量);人工触发的完整回归。
- 覆盖 base + update(latest per srpm) + update-test 的全部 SRPM 单元。
- 因规模大,默认高并发 + 可按 arch/产品拆分成多个子任务并行提交。
### 6.2 增量测试(Incremental)
- 场景:`updates` 每周例行发布。
- 流程:
1. 加载上一次成功 TestRun 保存的 baseline repodata 快照(`history/{product}/{arch}/{repo_type}/repomd-snapshot.json`);
2. 拉取当前 repodata,做 SRPM 级 diff:新增 / 版本变化 / 删除;
3. 只对"新增或版本变化"的 SRPM 单元生成 TestUnit(删除的包只做仓库完整性/元数据层面确认,不需要安装测试);
4. 测试通过后,把当前 repodata 存为新的 baseline。
- **updates 与 updates-test 分别独立处理**:updates-test 虽然是"全量仓库",但语义上是即将发布到 updates 的候选,因此实际执行策略建议:
- `updates-test`:全量测试(因为它承载本周要上线的所有内容且是最终把关点);
- `updates`:增量测试(相对上上周已发布内容的 diff),主要用于确认"最终发布仓库结构本身"没有被搬运/签名/repodata 生成过程破坏(属于 M1/M2/M3/M6/M7 这类"仓库级"检查,不必重复做全量安装测试)。
- 这个策略在配置里可覆盖(例如强制两个仓库都全量)。
### 6.3 关联产品版本对比(跨仓库/跨产品)
单独维护 `related_products` 规则,在所有 SRPM 单元测试完成后作为一个"聚合阶段"执行(不属于单 SRPM 粒度),比较双方仓库中匹配包的版本关系(精确一致 / 语义兼容 / ABI 兼容等规则可扩展)。
---
## 7. 测试模块设计(统一接口)
### 7.1 模块接口
```python
class BaseTestModule(ABC):
name: str
scope: Literal["repo", "srpm_unit", "cross_product"]
def is_applicable(self, ctx: "TestContext") -> bool: ...
def run(self, ctx: "TestContext") -> "ModuleResult": ...
@dataclass
class ModuleResult:
module: str
unit_key: str
status: Literal["pass", "fail", "warn", "skip", "error"]
summary: str
details: dict # 结构化细节,写入 json
evidence_paths: list[str] # 原始日志/输出文件路径
duration_s: float
```
### 7.2 模块清单
| # | 模块 | scope | 说明 | 关键实现 |
|---|---|---|---|---|
| M1 | `repo_integrity` | repo | 检查仓库目录结构完整、所有 rpm 文件可读不损坏(gzip/rpm 文件头校验)、repodata 各文件存在且未截断 | 逐文件 `rpm -qp --nosignature`、gzip 完整性检测 |
| M2 | `repo_metadata` | repo | repomd.xml schema 合法性、`revision` 时间戳合理、repomd.xml 签名验证(若签名)、各 metadata(primary/filelists/other/updateinfo/comps)可被标准工具解析 | `createrepo_c --checkrepo` 或等价校验、xmllint schema 校验 |
| M3 | `repodata_consistency` | repo | repodata 记录的包集合 == 实际目录扫描出的包集合(无多、无少);记录的 checksum/size 与实际文件一致 | 对比 primary.xml 条目 vs 目录 glob + 实际 sha256 |
| M4 | `install_remove` | srpm_unit | 在纯净沙箱里,用 `dnf install <pkg>` 装全部二进制子包,再 `dnf remove` 卸载,校验退出码、文件落地、卸载后无残留 | dnf 原生 + `.repo` 文件指向被测仓库(仅此仓库,`network=none`) |
| M5 | `upgrade_downgrade` | srpm_unit | 对该 SRPM 产出的全部二进制:从"基线版本"(通常是 base 或上一周版本)`dnf upgrade` 到本次版本,再 `dnf downgrade` 回退,检查两个方向是否成功、无依赖冲突、关键配置文件处理(noreplace)正常 | 双仓库同时挂载(旧+新),dnf 原生升级降级命令 |
| M6 | `signature` | srpm_unit(或repo) | `rpm -K` / `rpm --checksig` 校验签名有效、签名 key 在受信 keyring 内、与产品要求的签名策略匹配(如正式仓库不允许无签名包) | rpm 命令 + GPG keyring 管理 |
| M7 | `koji_hash` | srpm_unit | 调 koji API 按 NEVRA 查询官方构建记录的 checksum,和仓库中实际文件 checksum 比对,防篡改/被替换 | koji XML-RPC/REST 客户端,带缓存与限流 |
| M8 | `dependency` | srpm_unit | `dnf repoquery --deplist` / `dnf install --assumeno` 做依赖可解析性检查,检测缺失依赖、循环依赖、多版本冲突 | dnf repoquery + 依赖图分析 |
| M9 | `cross_product_version` | cross_product | 聚合阶段执行,按配置规则比较关联产品间的包版本关系 | 规则引擎,见 §7.3 |
| M10 | `product_upgrade` | cross_product/repo | 产品级别升级路径测试(如整机镜像/系统从上一版本升级到本版本),成本高,默认关闭,按需触发 | 基于容器/虚机镜像 + 升级脚本编排,可能调用外部 image 工具链 |
### 7.3 M9 规则引擎示例
```yaml
compare_rules:
- rule: exact_version_match # kernel 与 kernel-devel 必须版本完全一致
- rule: version_gte # A 版本 >= B 要求的最低依赖版本
- rule: abi_tag_match # 通过包内 provides/requires 里的 ABI tag 匹配
```
---
## 8. 沙箱与执行隔离
- 每个架构一个基础镜像(`image_map`),镜像内预装 `dnf`, `rpm`, `gpg`, 对应架构工具链;跨架构测试通过 QEMU user-mode + binfmt 或原生同架构机器(Runner 标签调度)。
- 每个 TestUnit 执行前:
1. 从基础镜像启动一个一次性容器(`--rm`,只读根 + 可写 overlay);
2. 写入原生 `.repo` 文件到 `/etc/yum.repos.d/`,`baseurl` 指向被测仓库(本地 bind mount 或内网 URL),**关闭其它仓库**,`network=none` 防止误连公网源;
3. 执行 dnf 命令,采集 stdout/stderr/exit code/`rpm -qa` 前后差异;
4. 容器销毁,天然回滚,无需手工清理。
- 并发模型:进程池按 `(arch)` 分组各自持有独立容器池,`concurrency` 控制单机总并发;支持分布式(每 arch 一台 worker 机)通过简单的任务队列(Redis/SQLite 均可)扩展,v1 先支持单机多进程。
---
## 9. 调度与执行流程(Orchestrator)
```
1. 加载配置 → 构建 Inventory(解析各仓库 repodata)
2. mode=full → TestUnits = 全部 SRPM
mode=incr → TestUnits = DiffEngine(baseline, current)
3. 生成执行计划:对每个 TestUnit × 该 scope 下启用的模块 → Task
(repo 级模块 M1/M2/M3 每个 repo 只需执行一次,不随 SRPM 重复)
4. 提交任务池:
- repo 级任务先跑(快、且后续模块可复用其结论,如 M3 失败的包直接跳过 M4/M5 并标记 skip+关联原因)
- srpm 级任务按 arch 分组并发跑
5. 汇总 → cross_product 聚合模块 M9/M10 最后跑
6. 写 ResultStore,更新 baseline 快照
7. 生成报告(JSON + HTML,其他格式按配置追加)
8. 返回进程退出码:全 pass=0,有 fail=非0(便于 CI 判断),warn 不影响退出码(可配置)
```
失败处理:单个 TestUnit 内某模块失败不阻塞其它 TestUnit(互不影响),但同一 unit 内如果 M3(repodata 一致性)失败,M4/M5 会标记为 `skip`(因为包本身可能损坏,装了也没有意义),并在报告里注明依赖关系。
---
## 10. 报告设计
### 10.1 JSON(机器可读,默认输出)
```json
{
"run_id": "20260726-103000",
"mode": "incremental",
"started_at": "...",
"finished_at": "...",
"summary": {
"total_units": 482,
"pass": 470,
"fail": 8,
"warn": 4,
"skip": 0
},
"products": [
{
"name": "os-server",
"arches": ["x86_64", "aarch64"],
"repo_results": [ /* M1/M2/M3 结果 */ ],
"unit_results": [
{
"srpm": "openssl-3.2.1-1",
"arch": "x86_64",
"repo_type": "updates-test",
"modules": [
{"module": "install_remove", "status": "pass", "duration_s": 12.3},
{"module": "signature", "status": "fail", "summary": "key not in trusted keyring", "evidence_paths": ["..."]}
]
}
],
"cross_product": [ /* M9 结果 */ ]
}
]
}
```
### 10.2 HTML(默认输出,人读)
- 首页仪表盘:整体通过率、按产品/仓库/模块的失败分布图(条形/饼图,纯前端渲染,单文件内嵌 JS,不依赖外网 CDN,便于离线环境)。
- 失败列表可筛选(按产品/架构/模块/仓库类型)。
- 点击某条结果 → 详情钻取(命令行、stdout/stderr、diff、签名信息等),日志原文可展开或下载。
- 单文件 HTML(内嵌数据 + 内嵌 JS/CSS),方便邮件/内网直接打开,无需额外静态资源服务器。
### 10.3 可扩展格式
- `JUnit XML`:便于接入 Jenkins/GitLab CI 的测试报告展示。
- `Markdown`:便于贴到 Wiki/IM 群通知摘要。
- Reporter 同样走插件接口:`BaseReporter.render(run_result) -> file`。
---
## 11. 目录结构(项目工程)
```
rpm-repo-tester/
├── rpmtest/
│ ├── cli.py # 入口,argparse/click
│ ├── config/
│ │ ├── schema.py # 配置 schema + 校验(pydantic)
│ │ └── resolver.py # 三层合并
│ ├── inventory/
│ │ ├── repo_scanner.py # 解析 repodata → Repo/SrpmUnit/BinaryPkg
│ │ └── diff_engine.py # baseline diff
│ ├── orchestrator/
│ │ ├── scheduler.py
│ │ └── sandbox/
│ │ ├── container_driver.py # podman/docker
│ │ └── chroot_driver.py
│ ├── modules/
│ │ ├── base.py # BaseTestModule
│ │ ├── repo_integrity.py
│ │ ├── repo_metadata.py
│ │ ├── repodata_consistency.py
│ │ ├── install_remove.py
│ │ ├── upgrade_downgrade.py
│ │ ├── signature.py
│ │ ├── koji_hash.py
│ │ ├── dependency.py
│ │ ├── cross_product_version.py
│ │ └── product_upgrade.py
│ ├── adapters/
│ │ ├── dnf_adapter.py # 封装 dnf/yum 命令调用与解析
│ │ ├── rpm_adapter.py
│ │ ├── koji_client.py
│ │ └── createrepo_adapter.py
│ ├── store/
│ │ ├── models.py # SQLite ORM
│ │ └── result_store.py
│ └── report/
│ ├── base.py
│ ├── json_reporter.py
│ └── html_reporter.py
├── configs/
│ └── config.example.yaml
├── docker/
│ ├── Dockerfile.controller # 主程序镜像
│ └── Dockerfile.sandbox-* # 各架构测试沙箱镜像
├── tests/ # 本项目自身的单元测试
└── pyproject.toml
```
- 模块通过 `entry_points`(`pyproject.toml` 的 `[project.entry-points."rpmtest.modules"]`)注册,新增测试类型无需改动 Orchestrator 代码。
- 沙箱驱动、Reporter 同样走 entry_points 插件化。
---
## 12. 部署形态
| 场景 | 方式 |
|---|---|
| 本地/CI 容器内运行 | `docker run --privileged -v /repos:/repos -v $PWD/report:/report rpm-repo-tester run ...`(需要 `--privileged` 或至少给容器内嵌套容器权限来跑 sandbox;也可换成 rootless podman) |
| 物理机直接运行 | `pip install rpm-repo-tester` 或发行为单一可执行(PyInstaller/zipapp),依赖宿主机已有 `dnf`, `podman/docker`, `createrepo_c` |
| CI 集成 | 退出码 + JUnit XML,Jenkins/GitLab 原生展示;HTML 报告作为 artifact 归档 |
---
## 13. 关键设计取舍说明
1. **为什么用 dnf/yum 原生命令而不是自己解析 RPM 依赖装包**:安装/升级/降级测试的目的是"模拟真实用户",任何自制逻辑都会引入与用户体验不一致的风险;原生命令 + 原生 `.repo` 配置是最贴近生产的方式,出问题也更可信。
2. **为什么 SRPM 而不是 Binary RPM 作为最小单元**:需求里升级降级测试是"源码包下所有二进制"的升级降级,且一个源码包出的多个子包往往要一起验证版本一致性、依赖关系,用 SRPM 归组更符合发布评审的思维方式,也方便报告按"一个变更"呈现。
3. **update-test 全量、updates 增量的差异化策略**:避免每周对 `updates` 重复跑一遍全量安装测试(updates-test 已经全量验证过实际内容一致),`updates` 阶段重点确认"从 test 仓库到正式仓库的搬运/签名/repodata 生成"过程没有引入新问题,可显著降低执行成本;此策略在配置中可关闭(强制两者都全量)。
4. **仓库级模块 vs 包级模块解耦**:M1/M2/M3 是仓库整体的健康检查,先于所有包级测试执行,其结论可以让包级测试提前判断是否需要 `skip`,避免在已知损坏的仓库上浪费大量安装测试时间。
5. **容器沙箱 + `network=none`**:保证测试只使用被测仓库内容,不被公网源"掩盖"仓库自身的缺失依赖问题(这本身也是 M8 依赖检查要抓的典型问题)。
---
## 14. 后续可迭代方向(v2+)
- 分布式任务队列,支持多机 worker(尤其是稀缺架构如 loongarch64/mips 物理机资源有限时)。
- 历史趋势看板(多次 TestRun 的通过率/耗时趋势)。
- 失败自动归因(把常见失败模式聚类,比如"某 key 过期"批量识别)。
- Web UI(在 HTML 报告基础上加一个常驻服务,支持查询历史 run、重跑失败项)。

201
设计.md Normal file
View File

@@ -0,0 +1,201 @@
# RepoGuard 自动化仓库测试引擎 —— 详细产品设计文档
## 1. 引言
### 1.1 产品背景
在现代企业级 Linux 操作系统发行版的发布流程中,RPM 软件仓库(包括 `base` 和 `update` 仓库)的质量保障(QA)是发布前的核心环节。由于产品线需要支持多种硬件架构(如 x86_64、aarch64、LoongArch、MIPS 等),且每周都有增量更新发布(包括 `updates` 稳定源和 `update-test` 测试源),传统的人工测试或简易脚本测试已无法满足高吞吐量、多架构并发、高可靠性的发布要求。
### 1.2 产品定位与目标
**RepoGuard** 是一款专为操作系统社区/企业发行版设计的自动化仓库 QA 测试引。其目标是:
**PDF**
* **模块化与可扩展性** :采用插件化设计,测试项之间解耦,支持快速横向扩展。
* **原生仿真度** :完全基于系统原生的 `dnf/yum` 和 `rpm` 命令行工具,100% 模拟最终用户的真实使用场景
* 。**PDF**
* **云原生集成** :完美兼容 Jenkins Pipeline 矩阵流水线,以轻量级容器(Docker)为执行载体,针对每个“产品 + 架构”启动独立沙箱运行。
* **双模支持** :完美支持针对 `update-test` 的全量静态分析,以及针对每周 `updates` 变动的增量动态测试。
## 2. 总体架构与工作流设计
### 2.1 拓扑架构
RepoGuard 作为独立测试程序,部署在沙箱容器内部。Jenkins 负责根据产品和架构调度不同的 Docker 镜像,并将宿主机的安全证书(如 Koji API 证书)和本地缓存挂载至容器中。
```
+-----------------------------+
| Jenkins 流水线控制 |
+-----------------------------+
| (并发调度)
+-------------------------+-------------------------+
| (LoongArch 容器) | (ARM64 容器)
+---------------------------------+ +---------------------------------+
| Docker Sandbox (LoongArch) | | Docker Sandbox (ARM64) |
| +---------------------------+ | | +---------------------------+ |
| | RepoGuard 引擎 | | | | RepoGuard 引擎 | |
| | [Metadata] [Dependency] | | | | [Metadata] [Dependency] | |
| | [Lifecycle] [KojiVerify] | | | | [Lifecycle] [KojiVerify] | |
| +---------------------------+ | | +---------------------------+ |
| | | | | |
| (生成 JSON/HTML) | | (生成 JSON/HTML) |
+---------------------------------+ +---------------------------------+
```
### 2.2 核心执行生命周期
RepoGuard 启动后,其生命周期主要分为 5 个阶段^^:
**PY**
1. **初始化阶段** :解析 `repoguard.yaml` 配置文件,清空并重新初始化容器内的原生 `/etc/yum.repos.d/` 配置^^。**PY**+ 1
2. **静态校验阶段(Meta & Static Check)** :拉取仓库元数据,分析元数据完整性,比对 Koji 编译哈希,验证 RPM 签名^^。**PY**
3. **依赖树闭合分析(Dependency Closure)** :运行 `dnf repoclosure` 进行静态依赖树扫描^^。**PY**
4. **动态执行阶段(Dynamic Actions)** :对增量/全量包进行安装、卸载、同一源码包(SRPM)下所有二进制包的同步升降级测试。
5. **报告生成阶段** :汇总各模块测试结果,在指定工作区输出 `report.json` 和 `report.html`。
## 3. 详细功能模块设计
RepoGuard 内部采用松耦合的模块化设计,每个测试模块均继承自统一的基类 `BaseModule`^^。
**PY**
### 3.1 仓库元数据与完整性校验模块 (`MetadataChecker`)
* **设计目的** :验证远程或本地仓库的索引文件(`repomd.xml`)与物理 RPM 包之间的一致性,防止由于网络传输或仓库构建中断导致的“死链”^^。**PDF**
* **输入** :`target.repositories.update_test.url`^^。**YAML**
* **核心逻辑**^^:**PY**
1. 通过 HTTP/HTTPS 下载目标仓库的 `repomd.xml`。
2. 解析 `repomd.xml` 节点,定位到类型为 `primary` 的元数据项,下载并解压其指向的 `primary.xml.gz`。
3. 解析 `primary.xml`,获取所有注册 RPM 包的相对路径(`location href`)、文件大小、以及 SHA256 哈希。
4. **双向核对(Bi-directional Check)** :
* **正向** :遍历 XML 中注册的每一个 RPM,验证其在本地工作目录中是否存在。如果开启了哈希校验,读取本地 RPM 计算 SHA256,比对是否一致^^。**PY**
* **反向** :扫描下载下来的 RPM 物理目录,核对是否有“未在 XML 中注册”的孤儿 RPM 文件。
### 3.2 静态依赖树校验模块 (`DependencyChecker`)
* **设计目的** :静态分析仓库在当前架构下是否存在无法满足的依赖关系(Broken Dependencies)或循环冲突^^。**PY**+ 1
* **输入** :全局仓库配置(`base`、`updates`、`update_test`)^^。**YAML**
* **核心逻辑**^^:**PY**
1. 调用 `_setup_native_repo()` 方法,将 `repoguard.yaml` 中配置的所有仓库写入容器的 `/etc/yum.repos.d/repoguard_temp.repo`^^。**PY**+ 1
2. 执行 `dnf clean all` 与 `dnf makecache` 强制使系统 DNF 底层库刷新缓存。
3. 执行 `dnf repoclosure --repoid=<update_test_repo_name>` 进行依赖闭合检查。
4. **异常判定** :如果标准输出或标准错误中包含 `unresolved` 或非 0 退出码,则提取具体的断链包和缺失的 `Requires` 依赖项,写入 `TestResult`^^。
**PY**
### 3.3 Koji 溯源校验与签名模块 (`KojiVerifier`)
* **设计目的** :保证发布仓库中的每一个二进制 RPM 均来自于官方安全、受控的 Koji 构建系统,且带有合规的 GPG 签名。
* **输入** :Koji 配置文件、`koji` 节点 API 信息^^。**YAML**
* **核心逻辑** :
1. **签名验证** :遍历待测包,执行系统调用 `rpm -K <rpm_path>`,检查输出中是否包含 `gpg OK` 字段^^。**PY**
2. **Koji 比对** :
* 解析 RPM 的 Header 获得 `Name`、`Version`、`Release`(NVR)。
* 调用 Koji 的 XML-RPC 接口 `getBuild(NVR)` 检索该构建。
* 提取 Koji 侧该构建对应的二进制文件哈希值,与仓库中物理 RPM 文件的实际哈希值进行碰撞比对,确保仓库包未被篡改。
### 3.4 动态生命周期测试模块 (`LifecycleTester`)
* **设计目的** :真实模拟最终用户在终端使用 `dnf` 安装、升级、降级包的行为。
* **测试策略** :
* **安装卸载** :对识别出的待测包,调用 `dnf install -y <pkg_name>`。记录事务中的依赖加载树,安装成功后,紧接着执行 `dnf remove -y <pkg_name>`,验证卸载过程不会破坏系统核心依赖链。
* **SRPM 联动升降级** :
* **痛点** :一个源码包(SRPM)通常会编译出多个子包(如 `libuv`、`libuv-devel`)。如果只升级其中一个,极易引发文件冲突或 ABI 不匹配。
* **解法** :模块会预先根据 `primary.xml` 将所有二进制包按照 **同源 SRPM** 进行归组。升级测试时,必须将该 SRPM 下的所有已安装子包打包放入 DNF 事务,统一执行 `dnf upgrade`,降级时亦然。
### 3.5 跨仓库对比与系统升级模块 (`CrossRepoAnalyzer`)
* **设计目的** :支持关联产品间的版本基线对比,以及大版本间的 `dnf system-upgrade` 原生升级模拟。
* **核心逻辑** :
* **版本对比** :加载关联产品(如高版本 OS 与低版本 OS)的 `repodata`,对同名包进行 Epoch-Version-Release(EVR)比对,发现逆向降级(Downgrade)或不合规的版本跳跃。
* **升级测试** :配置上一代稳定版为 Base 源,新测试源为 Upgrade 源,执行 `dnf system-upgrade download --releasever=<new_version>` 校验整体升级可行性。
## 4. 增量测试与全量测试策略
为了兼顾“测试全面性”与“持续集成(CI)的高效性”,RepoGuard 设计了两种测试模式。
| 维度 | 全量测试模式 (`full`) | 增量测试模式 (`incremental`)^^ **YAML** |
| ---------------------- | ----------------------------------------------------- | --------------------------------------------------------------------- |
| **适用场景** | 每周大版本演进、基线变更、Alpha/Beta 节点。 | 每周例行增量包推送、紧急安全补丁(CVE)推送。 |
| **测试目标** | 仓库内的所有 RPM 软件包。 | 仅测试较上周版本有变动的新包、升级包^^。**YAML** |
| **元数据校验** | 全量扫描。 | 全量扫描(确保新包加入后未破坏旧包结构)^^。**PY** |
| **依赖分析** | 对整个依赖树运行 `dnf repoclosure`^^。 **PY** | 对整个依赖树运行 `dnf repoclosure`^^。 **PY** |
| **安装与升降级** | 随机抽样测试,或对核心系统组件全量测试。 | **精准测试** :仅对提取出的增量包列表执行动态安装与升降级测试。 |
| **执行耗时** | 较长(通常在数小时级别)。 | 极快(通常在 5 ~ 15 分钟内)。 |
### 增量包差异提取算法:
**Pending_List**=**Packages**(**update-test**)**−**Packages**(**updates**)**
*其中,**−** 定义为:同名包在 `update-test` 中 EVR(Epoch-Version-Release)大于 `updates`,或该包属于 `update-test` 独有的全新引入包。*
## 5. 数据配置与输入输出设计
### 5.1 核心配置树设计 (`repoguard.yaml`)
配置文件是 RepoGuard 启动的唯一信任源,其完整结构已在 [Source 2] 中进行了规范定义,支持全局参数调优、多仓库分层配置(`base`、`updates`、`update-test`)以及对各个子测试模块的独立开关^^。
**YAML**
### 5.2 标准化 JSON 输出架构
所有模块执行完毕后,结果会被汇总序列化为 `report.json`^^。该结构包含总体元数据汇总和各个子模块的独立测试矩阵^^:
**PY**+ 1
* **`test_metadata`** :记录产品名、测试架构、测试模式、时间戳和通过率汇总。
* **`results`** :是一个模块级数组,每个元素均包含该模块的执行状态(`passed` 或 `failed`)、耗时指标(`metrics`)和具体的失败日志(`errors`)^^。
**PY**
### 5.3 HTML 可视化报告设计
为了在 Jenkins “Publish HTML Report” 插件中提供极致的可读性,RepoGuard 的 `report.html` 采用 **Self-contained 单网页设计** (CSS 和 JS 均内联引入)^^。
**PDF**
```
+-------------------------------------------------------------------------+
| [RepoGuard QA Dashboard] Product: KOS 2.0 | Arch: loongarch64 |
+-------------------------------------------------------------------------+
| |
| [ Passed: 11 ] [ Failed: 1 ] [ Total: 12 Modules ] |
| ========================================================== |
| |
| [+] Metadata Integrity Validation .............................. PASSED|
| [+] Static Dependency Closure (repoclosure) .................... PASSED|
| [-] Lifecycle Dynamic Package Tester ........................... FAILED|
| |
| --------------------------------------------------------------------- |
| | [Detail of Failures] | |
| | Package: nginx-1.24.0-1.el9 | |
| | Action: upgrade | |
| | Reason: FileConflict | |
| | Message: /etc/nginx/nginx.conf conflicts with nginx-core... | |
| --------------------------------------------------------------------- |
+-------------------------------------------------------------------------+
```
## 6. 非功能性需求与部署设计
### 6.1 高性能并发设计
由于 RPM 升级和卸载测试需要频繁进行网络下载和磁盘 I/O,RepoGuard 引入了基于 `multiprocessing` 的并行测试队列。在执行 `LifecycleTester` 时,会根据配置的 `parallel_jobs`(如开启 4 线程)分配独立的 DNF 事务,大幅缩短总体 CI 时间^^。
**YAML**
### 6.2 强隔离与幂等性保证
* **容器化生存** :每次测试必须在干净的 Docker 容器中拉起。
* **DNF 数据库防污染** :每次执行完模块后,测试引擎会自动调用 `_cleanup_native_repo()` 并执行 `dnf clean all` 还原环境,防止后置测试受到脏数据干扰^^。
**PY**
### 6.3 健壮的异常处理
为了防止由于临时性网络抖动导致的大面积下载失败,RepoGuard 对元数据下载(`_fetch_file`)和 `urllib` 请求包装了 **退避重试(Exponential Backoff Retry)** 机制,默认支持最大 3 次重试^^。
**PY**

27
设计文档.md Normal file
View File

@@ -0,0 +1,27 @@
## RPM 仓库发布测试平台 — 设计说明
## 1. 背景与目标
多个产品线维护自己的 RPM 仓库,仓库结构统一分为:
- **base 仓库**:单版本,只发布一次(首次 GA)。
- **update 仓库**:多版本,每周增量发布。每次发布同时产出两个仓库:
- `updates`:正式对外发布仓库;
- `updates-test`:先行验证仓库(全量仓库,不是增量),用于在推正式仓库前跑一遍完整回归。
- **其他仓库**: 其他可配置仓库。
仓库下按架构(x86_64 / aarch64 / loongarch64 / mips64 等,架构集合不固定,需可配置)分别组织。
目标是构建一个**独立运行的、模块化的自动化测试程序**,在每次仓库发布前(base 全量发布 / update 每周增量发布)对仓库做完整性、正确性、可用性验证,产出结构化 + 可视化报告,可跑在物理机或容器中,可接入 CI。
2. 术语与核心抽象
| 术语 | 定义 |
| ------------------------- | -------------------------------------------------------------------------------------------------- |
| **Product(产品)** | 一条产品线,如 "OS-Server",包含 1 个 base + 1 组 update/update-test,按架构展开 |
| **Repo(仓库)** | 某产品在某架构下的一个 RPM 仓库(本地路径或 URL),有 `repo_type ∈ {base, update, update-test}` |
| **SRPM 单元** | 一个源码包(含其版本),是**测试调度的最小单元** |
| **Binary Set** | 某 SRPM 在某架构仓库下对应构建出的全部二进制 RPM(含 debuginfo/devel 子包等) |
| **TestUnit** | `(product, repo, arch, srpm_name, srpm_version)` 五元组,一次测试运行会展开成若干 TestUnit |
| **TestRun** | 一次完整的测试执行(全量或增量),有唯一 run_id,产出一份报告 |
| **Baseline 快照** | 测试运行时仓库的 repodata 快照,用于增量 diff 出"本周变化的 SRPM" |