From 712cf1449b90f931041b706ad923f06b3de16640 Mon Sep 17 00:00:00 2001 From: warlock Date: Sun, 26 Jul 2026 18:07:14 +0800 Subject: [PATCH] first commit --- 2.0.md | 228 +++++++++++++++++++ rpm-repo-tester-设计文档.md | 440 ++++++++++++++++++++++++++++++++++++ 设计.md | 201 ++++++++++++++++ 设计文档.md | 27 +++ 4 files changed, 896 insertions(+) create mode 100644 2.0.md create mode 100644 rpm-repo-tester-设计文档.md create mode 100644 设计.md create mode 100644 设计文档.md diff --git a/2.0.md b/2.0.md new file mode 100644 index 0000000..b94b229 --- /dev/null +++ b/2.0.md @@ -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 中每一条 `` 记录,校验物理 RPM 文件的哈希值(SHA256)与 XML 中的记录是否绝对一致^^。 + * **反向** :扫描本地同步的 RPM 缓存目录,发现任何“未在 XML 中注册”的孤儿包,直接报错。 + + 1. **签名校验** :调用 `rpm -K `,验证 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 + ``` + 2. 抓取 DNF 事务日志。若安装成功,立即执行: + **Bash** + + ``` + dnf --installroot=/var/tmp/repoguard/sandbox remove -y + ``` + 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。 diff --git a/rpm-repo-tester-设计文档.md b/rpm-repo-tester-设计文档.md new file mode 100644 index 0000000..14ce539 --- /dev/null +++ b/rpm-repo-tester-设计文档.md @@ -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 ` 装全部二进制子包,再 `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、重跑失败项)。 diff --git a/设计.md b/设计.md new file mode 100644 index 0000000..59c4ee5 --- /dev/null +++ b/设计.md @@ -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=` 进行依赖闭合检查。 + 4. **异常判定** :如果标准输出或标准错误中包含 `unresolved` 或非 0 退出码,则提取具体的断链包和缺失的 `Requires` 依赖项,写入 `TestResult`^^。 + **PY** + +### 3.3 Koji 溯源校验与签名模块 (`KojiVerifier`) + +* **设计目的** :保证发布仓库中的每一个二进制 RPM 均来自于官方安全、受控的 Koji 构建系统,且带有合规的 GPG 签名。 +* **输入** :Koji 配置文件、`koji` 节点 API 信息^^。**YAML** +* **核心逻辑** : + +1. **签名验证** :遍历待测包,执行系统调用 `rpm -K `,检查输出中是否包含 `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 `。记录事务中的依赖加载树,安装成功后,紧接着执行 `dnf remove -y `,验证卸载过程不会破坏系统核心依赖链。 +* **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=` 校验整体升级可行性。 + +## 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** diff --git a/设计文档.md b/设计文档.md new file mode 100644 index 0000000..956babc --- /dev/null +++ b/设计文档.md @@ -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" |