Files
storage-labos/ProjectManual.md
T
yuanshuai c91a64fddb
CI / Python 3.12 (push) Waiting to run
CI / Python 3.9 (push) Waiting to run
chore(repo): initialize team collaboration repository
2026-07-27 20:40:12 +08:00

1607 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FlashOps 项目开发手册 (Project Manual)
本文档用于 FlashOps / STORAGE LABOS 项目的开发、测试、部署、运维与交接。内容以
2026-07-27 的实际源码和生产环境为准;目标架构与尚未实现的能力会明确标注,避免把原型能力
误认为真实硬件能力。
> **文档状态**:V1.2 · 已与当前代码、Git 仓库和云端部署边界核对
>
> **生产地址**<https://flashops.imagebrewing.com/>
>
> **团队 Git**<https://git.imagebrewing.com/yuanshuai/storage-labos>
>
> **后端代号**FlashOps
>
> **控制台品牌**STORAGE LABOS
>
> **安全提醒**:本文不记录 SSH 密码、Gitea 管理员密码、私钥或其他明文凭据。
---
## 目录
1. [项目概述](#1-项目概述)
2. [快速开始](#2-快速开始)
3. [系统架构](#3-系统架构)
4. [核心业务流程与状态机](#4-核心业务流程与状态机)
5. [安全门禁与危险动作](#5-安全门禁与危险动作)
6. [数据库与领域模型](#6-数据库与领域模型)
7. [API 接口设计](#7-api-接口设计)
8. [前端控制台](#8-前端控制台)
9. [工作流、Agent 与适配器](#9-工作流agent-与适配器)
10. [证据、失败分析与指标](#10-证据失败分析与指标)
11. [配置与环境变量](#11-配置与环境变量)
12. [腾讯云生产部署与运维](#12-腾讯云生产部署与运维)
13. [测试、质量门与开发规范](#13-测试质量门与开发规范)
14. [已知边界与演进路线](#14-已知边界与演进路线)
15. [故障排查](#15-故障排查)
16. [术语表与 FAQ](#16-术语表与-faq)
17. [变更记录](#17-变更记录)
---
## 1. 项目概述
### 1.1 背景
固件回归测试通常跨越数小时甚至数天,期间可能发生 Agent 崩溃、操作系统蓝屏、测试盘掉盘、
网络中断或整机失联。传统做法依赖工程师通宵值守、手工重启、记录截图和拼装报告,成本高,
而且很难证明“当时到底发生了什么”。
FlashOps 的目标是把一条真实实验室 SOP 变成可执行、可恢复、可审计、可取证的状态机:
- 自动执行固件 A/B 回归与循环测试;
- 在破坏性操作前阻止错盘、系统盘和未授权 DUT;
- 主机失联后通过独立带外通道逐级恢复;
- 从安全检查点继续,而不是盲目重跑非幂等步骤;
- 统一记录控制平面、Agent、OOB 和人工操作事件;
- 生成可回放的 Evidence Bundle
- 用无人值守完成率和人工触碰次数量化节省的值守成本。
### 1.2 主要用户
| 用户 | 主要任务 |
|---|---|
| 固件 / SSD 测试工程师 | 创建回归任务、查看实时进度、分析失败证据 |
| 实验室管理员 | 管理 DUT、测试主机、带外控制器和危险动作授权 |
| 研发工程师 | 对比固件版本、复现失败、读取时间线与环境指纹 |
| 测试负责人 | 查看 UCR、证据完整率、阻断问题和工位利用情况 |
| 运维人员 | 维护控制平面、数据库、证书、日志和备份 |
### 1.3 当前可运行基线
当前版本已经具备一条无需真实硬件即可端到端运行的纵向链路:
```text
创建任务
→ 六项安全预检
→ DUT / 测试主机资源加锁
→ N 次模拟回归循环
→ 注入 Agent 心跳丢失
→ L1 Agent 软恢复失败
→ L2 OS 重启失败
→ L3 带外 Reset 恢复
→ 校验环境指纹并从检查点续跑
→ 生成 Evidence Bundle
→ 控制台展示状态与事件时间线
```
### 1.4 能力状态
| 能力 | 状态 | 说明 |
|---|---|---|
| FastAPI 控制平面 | 已实现 | 任务、预检、状态、人工操作、看板接口 |
| 14 张持久化表 | 已实现 | SQLite 可直接建库和灌种子数据 |
| Run 事件溯源 | 已实现 | 状态变化统一经 `events/recorder.py` 写入 |
| Run 状态转移约束 | 已实现 | 非法转移抛出异常,终态不可继续 |
| 六项安全门禁 | 已实现 | 序列号、授权、系统盘、固件、Host、OOB |
| L1–L5 恢复决策 | 已实现 | 演示链路实际执行到 L3;L4/L5 为策略能力 |
| 检查点续跑 | 模拟实现 | 当前保存循环边界检查点 |
| Evidence Bundle | 最小实现 | `manifest.json` + `events.ndjson` |
| 控制台 | 部分动态 | 任务中心和实时运行接 API,其余主要为产品原型 |
| 生产部署 | 已实现 | 腾讯云 CVM + systemd + Nginx + HTTPS |
| 团队 Git 服务 | 已实现 | Gitea 1.27 + HTTPS + PR/Issue + COS 定时备份 |
| 独立 Host Agent | 部分实现 | 注册、token 摘要、主机探针、心跳及状态老化已实现;Step 租约待实现 |
| 真实 NVMe / OOB 接入 | 未实现 | 尚未连接可牺牲 DUT、PDU 或 KVM |
| 失败指纹聚类执行逻辑 | 未实现 | 数据模型和界面已定义,服务逻辑待接入 |
| 应用级账号与 RBAC | 未实现 | FlashOps 预览站当前匿名开放;接真实硬件前必须恢复认证与审批 |
### 1.5 系统边界
当前版本是“可演示、可开发、可部署的控制平面基线”,不是已经可以对真实盘执行刷写和
Sanitize 的生产硬件平台。以下行为不得通过修改模拟数据来伪装完成:
- 不得宣称 Step 租约、真实命令执行和真实 OOB 已经交付;
- 不得把 `sim://` 的固件、OOB 和证据 URI 当作真实硬件结果;
- 匿名预览环境不得接入真实硬件、客户数据或有效危险命令;接入前必须启用应用级认证、
审批和命令模板白名单;
- 不得直接把生产部署的 SQLite 单机形态扩展为多 worker
- 不得绕过安全门禁向真实设备下发自由文本命令。
### 1.6 技术栈
| 层 | 技术 | 当前用途 |
|---|---|---|
| 后端 | Python 3.9+ / FastAPI | HTTP API 与应用生命周期 |
| ORM | SQLAlchemy 2.x Async | 领域模型、事务和查询 |
| 开发数据库 | SQLite + aiosqlite | 零外部依赖启动 |
| 目标数据库 | PostgreSQL 16 + asyncpg | 多工位生产形态,尚未切换 |
| 服务进程 | Uvicorn | ASGI 运行时 |
| 校验 | Pydantic 2.x | API 请求体校验 |
| 工作流配置 | YAML / JSON | SOP 与步骤定义 |
| 测试 | pytest / pytest-asyncio / httpx | 纯函数与 API 端到端测试 |
| 前端 | 静态 HTML + Design System JS/CSS | 产品控制台原型和部分 API 接入 |
| 生产守护 | systemd | 单 worker 服务、开机自启、故障重启 |
| 入口 | 宝塔 Nginx | HTTPS、反代和安全响应头;FlashOps 预览站匿名开放 |
| Git 协作 | Gitea 1.27 | 仓库、提交历史、分支、Issue、Pull Request 与成员管理 |
| 证书 | Let's Encrypt / Certbot | HTTPS 与自动续期 |
### 1.7 目录结构
```text
STORAGE LABOS/
├── .github/ PR 模板与 CI 工作流模板
├── .gitignore / .gitattributes 忽略规则与跨平台文本规则
├── README.md Git 仓库首页
├── CONTRIBUTING.md 团队 Git 协作入口
├── ProjectManual.md 本手册
├── flashops/ 后端与技术文档
│ ├── Makefile 本地开发统一入口
│ ├── README.md 项目短说明和能力状态
│ ├── docker-compose.yml 目标生产拓扑草案
│ ├── deploy/ 当前腾讯云生产配置
│ │ ├── README.md
│ │ ├── flashops.service
│ │ ├── nginx-http.conf
│ │ ├── nginx-https.conf
│ │ └── certbot-reload-nginx.sh
│ ├── docs/ 架构、协议、状态机、ADR
│ │ ├── architecture.md
│ │ ├── domain-model.md
│ │ ├── state-machine.md
│ │ ├── workflow-spec.md
│ │ ├── agent-protocol.md
│ │ ├── adapter-sdk.md
│ │ └── decisions/
│ ├── services/control-plane/
│ │ ├── requirements.txt
│ │ ├── flashops_control/
│ │ │ ├── api/ HTTP 路由
│ │ │ ├── engine/ 状态转移与恢复决策
│ │ │ ├── events/ 事件写入口
│ │ │ ├── evidence/ 证据包生成
│ │ │ ├── models/ 14 张表
│ │ │ ├── safety/ 六项安全门禁
│ │ │ ├── services/ Run 编排和模拟执行器
│ │ │ ├── cli.py 建库、种子、演示命令
│ │ │ ├── config.py 配置入口
│ │ │ ├── db.py 异步会话与 SQLite 并发保护
│ │ │ └── main.py FastAPI 应用入口
│ │ └── tests/
│ ├── services/host-agent/
│ │ ├── README.md 独立 Agent 运行与配置
│ │ ├── flashops_agent/ 注册、指纹、凭据和心跳客户端
│ │ └── tests/
│ └── var/ 本地数据库、对象和证据(不入库)
└── 前端UI八页面完成/ 控制台静态页面与设计系统
├── Dashboard.dc.html
├── Tasks.dc.html
├── LiveRun.dc.html
├── Evidence.dc.html
├── Failures.dc.html
├── Assets.dc.html
├── Compare.dc.html
├── Workflows.dc.html
├── ConsoleNav.dc.html
├── ds-base.js / support.js
└── _ds/ 设计系统资源
```
### 1.8 服务地址与端口
| 环境 | 地址 / 端口 | 用途 |
|---|---|---|
| 本地开发 | `http://localhost:8000` | API 与控制台同源服务 |
| 本地控制台 | `/console/Dashboard.dc.html` | 控制台入口 |
| 本地 API 文档 | `/docs` | FastAPI OpenAPI UI |
| 生产内部监听 | `127.0.0.1:18080` | Uvicorn,仅服务器本机可访问 |
| 生产公网 | `https://flashops.imagebrewing.com` | Nginx HTTPS 入口 |
| 团队 Git | `https://git.imagebrewing.com` | Gitea 页面、HTTPS clone、PR 与成员管理 |
| 生产 HTTP | `80` | ACME challenge;其他请求跳转 HTTPS |
| 生产 HTTPS | `443` | TLS + 反向代理;FlashOps 预览站无需登录 |
---
## 2. 快速开始
### 2.1 环境准备
- macOS、Linux 或 WSL
- Python 3.9 或更高版本;
- GNU Make
- 本地运行不需要 Node、Docker、PostgreSQL 或 Redis。
### 2.2 首次初始化
```bash
cd "/Users/yuanshuai/Desktop/STORAGE LABOS/flashops"
make setup
```
`make setup` 会执行以下动作:
1. 创建 `flashops/.venv`
2. 安装 `services/control-plane/requirements.txt`
3. 重建本地 `var/flashops.db`
4. 创建 `var/objects/`
5. 灌入模拟工作流、DUT、Host、OOB 和固件数据。
> `make setup` 内含 `db-reset`,会删除本地 `var/` 中的开发数据。不要在生产目录执行。
### 2.3 启动开发服务
```bash
make dev
```
打开:
- 总览:<http://localhost:8000/console/Dashboard.dc.html>
- 任务中心:<http://localhost:8000/console/Tasks.dc.html>
- 实时运行:<http://localhost:8000/console/LiveRun.dc.html>
- API 文档:<http://localhost:8000/docs>
### 2.4 跑一条完整演示
```bash
make demo
```
演示默认运行 8 个循环,并注入一次心跳丢失。完成后会输出 Run ID 和 Evidence Bundle 路径。
也可以在控制台“任务中心”先执行安全预检,再创建任务,然后进入“实时运行”观察事件时间线。
### 2.5 运行测试
```bash
make test
```
当前基线共有 25 项测试,覆盖:
- Run 状态转移;
- L1L5 恢复阶梯选择;
- 系统盘 / 分区保护;
- 六项安全预检;
- 完整模拟 Run、故障恢复、Evidence Bundle
- 暂停、继续、紧急停止与人工触碰审计;
- Agent 注册、token 轮换、Bearer 鉴权和本地凭据权限;
- 心跳状态老化:15 秒降级、30 秒离线。
### 2.6 常用 Make 命令
| 命令 | 作用 |
|---|---|
| `make setup` | 创建虚拟环境、安装依赖、重建开发库 |
| `make setup-py` | 只准备 Python 依赖 |
| `make db-reset` | 删除本地数据库和对象目录后重建 |
| `make seed` | 向空库灌入种子数据;已有工作流时不重复写 |
| `make demo` | CLI 跑完整模拟链路 |
| `make dev` | 启动 API 和同源控制台 |
| `make dev-agent` | 注册 Agent 并发送一次真实进程心跳 |
| `make dev-agent-loop` | 持续运行 Agent 心跳循环 |
| `make test` | 执行完整测试集 |
| `make e2e` | 只跑端到端纵向测试 |
| `make clean` | 清理本地 `var/`、缓存和 `__pycache__` |
---
## 3. 系统架构
### 3.1 五层架构
```text
┌──────────────────────────────────────────────────────────────┐
│ 交互与集成层 静态控制台 · CLI · OpenAPI │
├──────────────────────────────────────────────────────────────┤
│ 控制平面 FastAPI · Safety · Run Service · Event Store │
├──────────────────────────────────────────────────────────────┤
│ 执行平面 当前:内置模拟器 + Agent 心跳;目标:Step 执行 │
├──────────────────────────────────────────────────────────────┤
│ 带外平面 当前:模拟 OOB;目标:KVM / PDU / ATX / AC │
├──────────────────────────────────────────────────────────────┤
│ 证据与智能层 统一事件流 · Evidence Bundle · Failure Signature│
└──────────────────────────────────────────────────────────────┘
```
### 3.2 依赖方向
```text
Browser / CLI ──HTTP──▶ Control Plane ◀──HTTP pull── Host Agent(部分落地)
│ │
├──▶ Database ├──▶ Adapters
├──▶ Object Store └──▶ OOB
└──▶ Event Timeline
```
硬规则:
1. Agent 不导入控制平面代码,只按 `docs/agent-protocol.md` 通信;
2. 控制平面不主动反连客户主机,Agent 采用拉模式;
3. 状态机纯逻辑不依赖 FastAPI、SQLAlchemy、文件系统或真实时钟;
4. API 路由只做校验与服务调用,不承载复杂业务决策;
5. 危险动作必须先过控制平面安全门禁;
6. Run 状态不得绕过 `events/recorder.py` 直接修改;
7. 日志正文和大文件进入对象存储,事件表只保存索引、摘要和 URI。
### 3.3 当前运行架构
```text
Internet
DNSPod: flashops.imagebrewing.com → 62.234.39.66
Nginx :443
├── TLS 1.2 / 1.3
├── 安全响应头
└── proxy_pass http://127.0.0.1:18080
systemd: flashops.service
Uvicorn 单 worker / FastAPI
├── SQLite: var/flashops.db
├── Evidence: var/objects/
└── Console: ../前端UI八页面完成/
```
### 3.4 开发形态与目标形态
| 组件 | 当前开发 / 线上基线 | 目标多工位形态 |
|---|---|---|
| 数据库 | SQLite | PostgreSQL 16 |
| 事务并发 | 应用级串行 SQLite session | 数据库行锁与队列 |
| 进程数 | 1 worker | 多 worker / 多实例 |
| 事件总线 | 进程内任务 | Redis / NATS |
| 对象存储 | 本地目录 | MinIO / S3 / COS |
| 执行器 | Run 仍由内置模拟器执行;独立 Agent 已能注册和心跳 | Agent 领取并执行 Step |
| OOB | 模拟 | JetKVM / PDU / 主板控制器 |
| 认证 | 匿名预览,无应用账号 | OIDC / SSO + RBAC + 审批 |
### 3.5 关键架构决策
`flashops/docs/decisions/` 记录已经接受的 ADR
- ADR-0001Monorepo 与技术栈;
- ADR-0002Run 采用事件溯源;
- ADR-0003:首版自研状态机,不依赖 Temporal / Airflow
- ADR-0004:安全门禁放在控制平面,Agent 保持简单;
- ADR-0005:模拟器是一等产品能力,不是临时测试脚手架。
修改这些边界前,应新增 ADR,而不是只改代码。
---
## 4. 核心业务流程与状态机
### 4.1 创建任务
任务创建时会冻结以下信息:
- 工作流 key、version 和 `spec_hash`
- DUT、Host、固件 A、固件 B
- 循环次数和是否注入故障;
- OS、Kernel、BIOS、Agent、工具链、DUT 序列号和型号;
- 环境指纹哈希;
- 创建人和创建时间。
创建后依次写入 `RUN_CREATED``RUN_QUEUED` 事件,再启动模拟执行任务。
### 4.2 Run 状态机
```text
QUEUED ──▶ PREFLIGHT ──▶ RUNNING ──▶ COMPLETED
│ │ │
│ └──▶ REJECTED │
│ ├──▶ PAUSED ──▶ RUNNING
│ ├──▶ RECOVERING ──▶ RUNNING
│ │ └──▶ FROZEN
└──────────────────────────┴──────────────▶ ABORTED
```
| 当前状态 | 允许转移到 |
|---|---|
| `QUEUED` | `PREFLIGHT`, `ABORTED` |
| `PREFLIGHT` | `RUNNING`, `REJECTED`, `ABORTED` |
| `RUNNING` | `RECOVERING`, `PAUSED`, `FROZEN`, `COMPLETED`, `ABORTED` |
| `RECOVERING` | `RUNNING`, `FROZEN`, `ABORTED` |
| `PAUSED` | `RUNNING`, `ABORTED` |
| `FROZEN` | 无,终态 |
| `COMPLETED` | 无,终态 |
| `ABORTED` | 无,终态 |
| `REJECTED` | 无,终态 |
非法转移由 `engine/states.py::assert_run_transition()` 拒绝。
### 4.3 Step 状态机(目标契约)
```text
PENDING → DISPATCHED → RUNNING → SUCCEEDED
├──→ FAILED
├──→ TIMED_OUT
├──→ SKIPPED
└──→ CANCELLED
```
`run_steps` 表已经定义状态、attempt、租约、输入输出和错误字段,但当前内置模拟器主要在 Run
和循环层执行,完整 Step runtime / Agent 租约尚未接入。
### 4.4 恢复阶梯
| 级别 | 名称 | 条件与动作 |
|---|---|---|
| L1 | `L1_AGENT_SOFT` | Agent 在线时优雅停止或重启执行进程 |
| L2 | `L2_OS_REBOOT` | Host 网络仍在线时请求操作系统重启 |
| L3 | `L3_OOB_RESET` | OOB 在线时触发主板 Reset |
| L4 | `L4_OOB_ATX_POWER` | 模拟 ATX 长按关机并重新开机 |
| L5 | `L5_OOB_AC_CYCLE` | 整机 AC 断电,等待安全间隔后上电 |
| Freeze | `FREEZE` | 停止自动动作,保留现场并等待人工 |
数据完整性失败不走逐级恢复,直接 `FREEZE`,防止自动动作覆盖关键证据。
### 4.5 检查点语义
当前模拟执行器在循环结束时保存:
```json
{
"loop_index": 3,
"next_step_key": "regression-loop",
"dut_fw": "B"
}
```
恢复成功后必须先确认环境指纹和物理状态仍符合预期,再写 `CHECKPOINT_RESUMED`
刷写、Format、Sanitize 等非幂等步骤默认不得因为进程重启自动重复执行。
### 4.6 暂停、继续与紧急停止
- 暂停:仅 `RUNNING → PAUSED`
- 继续:仅 `PAUSED → RUNNING`
- 紧急停止:任何非终态可进入 `ABORTED`
- 每次人工操作先写 `HUMAN_TOUCH`,递增 `human_touches`,并把
`unattended_completion` 设为 `false`
- 紧急停止会释放资源锁并取消内置模拟任务。
### 4.7 SQLite 并发保护
模拟器与人工暂停可能同时写 `run_events`。SQLite 只允许一个 writer;如果两个会话同时读取
`event_seq` 再写入,会发生唯一键冲突。当前 `db.py` 对 SQLite 的完整 `session_scope` 事务按事件
循环串行化,保证事件序号和投影一致。
这项保护只适合单进程 / 单 worker 基线。切换 PostgreSQL 后不使用应用级锁,并需要增加
PostgreSQL 并发集成测试。
---
## 5. 安全门禁与危险动作
### 5.1 默认拒绝原则
安全判断发生在控制平面,并且必须早于命令下发。Agent 只能执行已批准模板和经过 schema
校验的参数,不能接收来自界面的自由文本 shell 命令。
### 5.2 六项预检
| key | 门禁 | 通过条件 |
|---|---|---|
| `serial` | DUT 序列号白名单 | 期望序列号非空且等于现场发现序列号 |
| `destructive` | 破坏性写入授权 | `duts.allow_destructive = true` |
| `system_disk` | 系统盘 / 分区保护 | DUT 路径不命中系统盘及其任一分区 |
| `firmware` | 固件适配型号 | DUT 型号在固件适用列表中,或列表为空 |
| `host` | 测试主机与 Agent | Host 状态为 `ONLINE` |
| `oob` | 带外控制器 | 绑定 OOB 存在且状态为 `ONLINE` |
六项全部通过,`passed` 才为 `true`
### 5.3 系统盘保护
`safety/gates.py::_same_device()` 同时比较完整设备名和分区归属。例如:
- 目标 `/dev/nvme0n1`,系统盘 `/dev/nvme0n1`:拒绝;
- 目标 `/dev/nvme0n1p2`,系统盘 `/dev/nvme0n1`:拒绝;
- 目标 `/dev/nvme1n1`,系统盘 `/dev/nvme0n1`:可继续检查其他门禁。
真实 Agent 接入时还应补充:挂载点、root filesystem、启动标志、Windows Disk Number、BDF
和序列号的现场交叉校验。
### 5.4 危险级别
| 级别 | 含义 |
|---|---|
| `none` | 只读、报告或普通控制动作 |
| `low` | 有状态变化但可逆 |
| `high` | 刷写、Format、Sanitize、断电等,需要审批与审计 |
### 5.5 进入真实实验室前必须补齐
1. 命令模板注册表与 JSON Schema 参数校验;
2. 不可绕过的系统盘识别;
3. 应用账号、RBAC、危险动作审批和双人复核;
4. Agent 身份、短期令牌、请求签名与重放保护;
5. 审批人、模板版本、参数、执行结果和撤销动作审计;
6. 真实 OOB 的安全间隔、最大恢复次数和硬件熔断;
7. 真实 DUT 的可牺牲测试环境,禁止先在业务盘验证。
---
## 6. 数据库与领域模型
### 6.1 当前数据库
- 本地:`flashops/var/flashops.db`
- 生产:`/data/wangzhan/app/storage-labos/flashops/var/flashops.db`
- 驱动:`sqlite+aiosqlite`
- 表由 SQLAlchemy `Base.metadata.create_all()` 创建;
- 应用启动时会确保表和种子数据存在;
- 当前无 Alembic 迁移,修改表结构时需要先补迁移方案。
### 6.2 14 张表
| 领域 | 表 | 作用 |
|---|---|---|
| 资产 | `oob_controllers` | 带外控制器、能力、状态、电源和温度 |
| 资产 | `test_hosts` | 操作系统、Agent、工具链、心跳和 OOB 绑定 |
| 资产 | `duts` | 序列号、型号、BDF、设备路径、危险授权和健康信息 |
| 资产 | `firmware_artifacts` | 版本、哈希、签名、适用型号和文件 URI |
| 工作流 | `workflows` | SOP spec、版本、哈希、危险级别和审批人 |
| 运行 | `runs` | 当前状态投影、环境指纹、进度、检查点和结论 |
| 运行 | `run_steps` | 步骤状态、attempt、租约、输入输出和错误 |
| 事件 | `run_events` | 每个 Run 内单调递增的 append-only 时间线 |
| 恢复 | `recovery_actions` | 触发原因、恢复级别、结果、详情和证据 URI |
| 调度 | `resource_locks` | DUT / Host 独占锁,防止并发踩踏 |
| 分析 | `failure_signatures` | 失败聚类签名和统计 |
| 分析 | `run_failures` | 单次失败实例及其分类 |
| 证据 | `evidence_bundles` | 证据包 URI、manifest、大小和完整率 |
| 审计 | `audit_log` | 操作者、动作、命令模板、参数、审批和结果 |
### 6.3 事件溯源
```text
run_events(顺序真相)
├──投影──▶ runs
├──投影──▶ run_steps
└──关联──▶ recovery_actions / evidence_bundles / failures
```
`runs` 是为了查询效率保存的当前投影,`run_events` 才是完整顺序真相。事件的 `seq` 由控制平面
分配,不能用不同机器的时间戳代替排序。
### 6.4 关键字段
- `runs.spec_hash`:任务使用的工作流不可变版本;
- `runs.env_fingerprint` / `env_fingerprint_hash`A/B 可比性和失败聚类输入;
- `runs.event_seq`:每个 Run 的下一事件序号来源;
- `runs.checkpoint`:安全续跑位置;
- `runs.unattended_completion` / `human_touches`UCR 与人工介入统计;
- `duts.serial` / `device_path` / `allow_destructive`:危险动作三信号;
- `firmware_artifacts.sha256` / `signature`:固件完整性与来源;
- `evidence_bundles.completeness` / `missing_fields`:证据质量;
- `resource_locks` 的资源类型和资源 ID 唯一约束:防止同一资产被重复占用。
### 6.5 数据文件与 Git
以下内容属于运行产物,不进入 Git:
```text
flashops/var/
flashops/.venv/
flashops/.pytest_cache/
*.db
*.db-journal
*.log
.env
.env.local
```
---
## 7. API 接口设计
### 7.1 基础约定
- 前缀:`/api/v1`
- 数据格式:JSON
- 时间:API 输出 ISO 8601 UTC 字符串;
- 开发文档:`/docs`
- 当前应用内部没有用户认证中间件;
- 当前公网预览站未启用 Nginx Basic Auth,页面和普通 API 可匿名访问;
- 匿名预览只能使用模拟资产。进入真实硬件或多用户阶段前必须增加应用级认证、RBAC、
审批和审计上下文。
### 7.2 接口清单
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | `/api/v1/health` | 服务健康检查 |
| POST | `/api/v1/preflight` | 对指定或默认资产执行六项预检 |
| POST | `/api/v1/runs` | 创建 Run 并启动模拟执行 |
| POST | `/api/v1/runs/demo` | 用默认参数创建演示 Run |
| GET | `/api/v1/runs?limit=30` | 查询最近 Runlimit 限制 1100 |
| GET | `/api/v1/runs/{run_id}` | 查询 Run 当前投影和 Evidence Bundle |
| GET | `/api/v1/runs/{run_id}/events?after_seq=0` | 增量查询事件时间线 |
| POST | `/api/v1/runs/{run_id}/pause` | 人工暂停 RUNNING 任务 |
| POST | `/api/v1/runs/{run_id}/resume` | 人工继续 PAUSED 任务 |
| POST | `/api/v1/runs/{run_id}/emergency-stop` | 紧急停止非终态任务 |
| GET | `/api/v1/dashboard/summary` | 汇总任务数、活动数、完成数和 UCR |
| POST | `/api/v1/agent/register` | 首次注册或轮换 Agent token |
| POST | `/api/v1/agent/{agent_id}/heartbeat` | Bearer 心跳、主机状态和探针上报 |
| GET | `/api/v1/agent/hosts` | 查询 Host / Agent 在线投影,不返回 token |
### 7.3 请求模型
#### PreflightBody
```json
{
"dut_id": null,
"host_id": null,
"firmware_id": null,
"discovered_serial": null
}
```
全部为空时使用第一组种子资产。`discovered_serial` 可用于模拟现场序列号不一致。
#### CreateRunBody
```json
{
"name": "固件 A/B 无人值守回归",
"workflow_id": null,
"dut_id": null,
"host_id": null,
"firmware_a_id": null,
"firmware_b_id": null,
"loops": 8,
"inject_failure": true,
"created_by": "console-demo"
}
```
API 当前允许 `loops` 为 1–500;工作流种子 spec 的产品目标上限为 10000,两者尚未统一。
#### HumanActionBody
```json
{
"actor": "engineer-name",
"reason": "检查暂停"
}
```
### 7.4 调用示例
本地:
```bash
curl -sS http://localhost:8000/api/v1/health
curl -sS -X POST http://localhost:8000/api/v1/preflight \
-H 'Content-Type: application/json' \
-d '{}'
curl -sS -X POST http://localhost:8000/api/v1/runs \
-H 'Content-Type: application/json' \
-d '{"loops":8,"inject_failure":true,"created_by":"manual"}'
```
公网预览环境可直接访问:
```bash
curl https://flashops.imagebrewing.com/api/v1/health
```
### 7.5 常见状态码
| 状态码 | 场景 |
|---|---|
| 200 | 查询或动作成功 |
| 201 | Run 或 Agent 注册成功 |
| 400 | 资产或工作流数据无法解析 |
| 401 | Agent 注册口令或 Agent bearer token 无效 |
| 404 | Run / DUT / Host 等资源不存在 |
| 409 | 当前状态不允许暂停、继续或停止 |
| 422 | Pydantic 请求参数校验失败 |
| 503 | 生产环境尚未配置 Agent 注册口令 |
| 500 | 应用未处理异常或数据约束冲突 |
---
## 8. 前端控制台
### 8.1 挂载方式
`main.py` 把后端仓库的同级目录 `前端UI八页面完成/` 挂载到 `/console`
```text
STORAGE LABOS/
├── flashops/
└── 前端UI八页面完成/
```
生产服务器必须保持同样的兄弟目录关系,否则根路径会跳到 `/docs`,控制台路径返回 404。
### 8.2 核心页面
| 页面 | 文件 | 当前状态 |
|---|---|---|
| 总览 | `Dashboard.dc.html` | 产品级静态样例,看板 API 尚未全面接入 |
| 任务中心 | `Tasks.dc.html` | 已接预检与创建 Run API |
| 实时运行 | `LiveRun.dc.html` | 已接 Run、事件、演示、暂停、继续、急停 API |
| 证据浏览器 | `Evidence.dc.html` | 主要为静态样例,真实 Evidence 下载待接 |
| 失败中心 | `Failures.dc.html` | 静态产品原型,聚类服务待实现 |
| 资产中心 | `Assets.dc.html` | 静态产品原型,资产 CRUD 待实现 |
| 版本比较 | `Compare.dc.html` | 静态产品原型,真实 A/B 查询待实现 |
| 工作流模板 | `Workflows.dc.html` | 静态产品原型,编辑与发布 API 待实现 |
`ConsoleNav.dc.html` 提供导航与全局停止交互原型;`Deck.dc.html``_template/` 用于展示稿,
不属于正式业务路由。
### 8.3 前端调用约定
- 页面与 API 同源,不配置独立 API Base URL
- 所有动态请求使用相对路径 `/api/v1/...`
- 前端代码不得保存任何生产密码或 Agent token
- Run ID 应通过 URL 或浏览器状态传递;
- 事件增量读取使用 `after_seq`,不要每次重复拉取全部事件;
- 页面中静态样例必须用“演示 / 示例”标识,避免与真实数据混淆。
### 8.4 下一步前端接入优先级
1. 总览接 `/dashboard/summary` 与最近 Run
2. 证据页接 Run Evidence Bundle 和真实事件;
3. 资产页增加只读 API,再增加受审批保护的写接口;
4. 工作流页面接版本、spec hash、发布和审批;
5. 失败中心接失败签名和人工分类;
6. 版本比较接真实环境指纹与指标;
7. 引入统一的 loading、empty、error、401、409 状态。
---
## 9. 工作流、Agent 与适配器
### 9.1 种子工作流
默认工作流:
```text
key: fw-ab-regression
name: 固件 A/B 电源状态回归
version: 3
danger_level: high
```
步骤:
1. `preflight`:安全预检;
2. `flash-fw-b`:高危、非幂等固件刷写,完成后检查点;
3. `regression-loop`:fio、设备枚举、完整性校验,循环边界检查点;
4. `report`:汇总报告。
每个发布版本保存完整 spec 和稳定 `spec_hash`。同 key 的修改应创建新版本,不覆盖历史版本。
### 9.2 工作流字段原则
- `key`:稳定机器标识;
- `version`:不可变整数版本;
- `danger_level`:工作流整体风险;
- `params`:类型、默认值、范围;
- `steps[].adapter`:调用的能力边界;
- `steps[].template`:命令模板,而不是任意命令文本;
- `idempotent`:是否允许安全重试;
- `checkpoint`:是否允许从此边界恢复;
- `on_fail`retry / fail_run / continue / freeze / recover。
详见 `flashops/docs/workflow-spec.md`
### 9.3 Host Agent 当前实现与目标模型
独立 Agent 已作为 `services/host-agent/flashops_agent` 实现注册、主机指纹发现、
本地 `0600` 凭据文件、Bearer token 和心跳。控制平面只保存 token 的 SHA-256 摘要;
生产注册口令缺失时默认拒绝注册。心跳迟到 15 秒时 Host 自动进入 `DEGRADED`
丢失 30 秒时进入 `OFFLINE`;安全预检会刷新状态后再判断 Host 是否可用。
当前 Run 执行仍由内置模拟器完成。
下一阶段继续按拉模式实现:
```text
Agent 注册 / 心跳
→ 拉取可执行 Step 租约
→ ACK 接手
→ 按模板执行
→ 上报输出摘要与对象 URI
→ 完成 / 失败 / 超时
→ 租约续期或到期回收
```
拉模式用于适配客户内网、NAT 和不允许控制平面反向连接的测试主机。
### 9.4 Adapter 边界
目标适配器包括:
- `nvme_cli`:识别设备、固件下载 / commit、SMART、Format / Sanitize
- `fio`:负载执行、指标和错误提取;
- `shell`:只允许登记模板;
- `oob`Reset、ATX、AC、画面 / 电源状态;
- `builtin`:预检、等待、报告和证据处理。
模拟适配器与真实适配器必须通过同一套契约测试。真实实现不得在接口外增加隐藏行为。
### 9.5 幂等、租约和重放
- 每次 Step dispatch 带唯一 idempotency key
- Agent 重复上报不能重复产生危险动作;
- 非幂等步骤中断后默认 `INCONCLUSIVE` 或人工确认;
- 租约到期只代表执行权回收,不代表命令一定未执行;
- 重派前必须根据模板类型和现场状态决定是否安全;
- 任何重放都必须在事件流中留下原因和原始 dispatch 引用。
---
## 10. 证据、失败分析与指标
### 10.1 Evidence Bundle 当前内容
每个完成 Run 的对象目录:
```text
var/objects/runs/<run_id>/
├── manifest.json
└── events.ndjson
```
`manifest.json` 当前包含:
- schema version
- Run ID
- workflow key / version / spec hash
- environment fingerprint
- checkpoint
- event count
- 文件路径和字节数。
`events.ndjson` 每行一条按 `seq` 排序的事件,包含时间、来源、kind、严重度、消息和 payload。
### 10.2 目标证据包
真实硬件阶段应补充:
- Agent stdout / stderr 和工具原始输出;
- SMART、identify、PCIe、OS、驱动、BIOS 快照;
- fio JSON、校验 hash、失败 LBA 范围;
- 蓝屏 / panic dump、系统日志和屏幕证据;
- OOB 电源状态与画面指纹;
- 固件文件 hash 与签名验证结果;
- 审批、命令模板和参数;
- 文件级 SHA-256、缺失字段和生成器版本。
### 10.3 失败分类
| 分类 | 含义 |
|---|---|
| `DUT_DEFECT` | DUT / 固件自身缺陷 |
| `INFRA_FAILURE` | 主机、网络、供电、OOB 等基础设施问题 |
| `SCRIPT_FAILURE` | 测试脚本、适配器或参数问题 |
| `DATA_INTEGRITY` | 数据不一致,默认冻结现场 |
| `UNKNOWN` | 证据不足或尚未分类 |
基础设施故障必须与 DUT 缺陷分开统计,否则会污染固件质量结论。
### 10.4 失败签名目标
建议由以下稳定要素归一化后计算 hash:
```text
workflow step
+ normalized error codes
+ normalized log templates
+ host state
+ DUT enumeration state
+ data integrity state
+ recovery outcome
+ environment fingerprint
```
原始时间、随机 ID、绝对路径等高基数字段不应直接进入签名。
### 10.5 核心产品指标
| 指标 | 定义 |
|---|---|
| UCR | 无人值守完成 Run / 已完成 Run |
| Human Touches | Run 执行期间人工操作次数 |
| Evidence Completeness | 已收集必需证据字段 / 应收集字段 |
| Recovery Success Rate | 自动恢复成功次数 / 自动恢复尝试次数 |
| Infra False Positive Rate | 被误判为 DUT 缺陷的基础设施故障比例 |
| Reproduction Rate | 可由证据与最小步骤成功复现的失败比例 |
当前 `/dashboard/summary` 已提供总 Run、活动 Run、完成 Run 和 UCR 的最小聚合。
---
## 11. 配置与环境变量
所有应用变量使用 `FLASHOPS_` 前缀。
| 变量 | 开发默认值 | 说明 |
|---|---|---|
| `FLASHOPS_ENV` | `development` | 环境标识 |
| `FLASHOPS_DATABASE_URL` | `sqlite+aiosqlite:///.../var/flashops.db` | SQLAlchemy DSN |
| `FLASHOPS_OBJECT_STORE_URL` | `.../var/objects` | 当前必须为本地路径 |
| `FLASHOPS_BUS_URL` | 空 | 目标 Redis / NATS 地址,当前未启用 |
| `FLASHOPS_AGENT_ENROLLMENT_TOKEN` | 空 | 控制面与 Agent 共享的首次注册口令;生产控制面缺失时拒绝注册 |
| `FLASHOPS_API_HOST` | `0.0.0.0` | 监听地址 |
| `FLASHOPS_API_PORT` | `8000` | 监听端口 |
| `FLASHOPS_CORS_ORIGINS` | `http://localhost:3000` | 逗号分隔 CORS 来源 |
| `FLASHOPS_ENGINE_TICK_S` | `0.5` | 引擎 tick 间隔 |
| `FLASHOPS_TIME_SCALE` | `1.0` | 模拟时间倍率;测试使用更小值 |
### 11.1 时间策略默认值
| 配置 | 默认值 |
|---|---:|
| 心跳间隔 | 5s |
| 心跳降级 | 15s |
| 心跳丢失 | 30s |
| Step 租约 TTL | 60s |
| 租约拉取超时 | 20s |
| Step 默认超时 | 600s |
| 恢复后稳定等待 | 20s |
| AC 断电安全间隔 | 10s |
| 单循环最大恢复 | 3 |
| 单 Run 最大恢复 | 20 |
当前这些时间策略在 `config.py::TimingPolicy` 中定义,还未全部开放为环境变量。
### 11.2 Host Agent 运行变量
| 变量 | 默认值 | 说明 |
|---|---|---|
| `FLASHOPS_AGENT_SERVER` | `http://127.0.0.1:8000` | 控制面基础地址 |
| `FLASHOPS_AGENT_HOST_ID` | 空 | 绑定已有 Test Host ID |
| `FLASHOPS_AGENT_HOST_NAME` | 本机 hostname | 按名称绑定或新建 Test Host |
| `FLASHOPS_AGENT_STATE_PATH` | `~/.flashops/agent-state.json` | 本地 bearer 凭据文件,权限固定为 `0600` |
| `FLASHOPS_AGENT_ENROLLMENT_TOKEN` | 空 | 与控制面一致的首次注册口令 |
| `FLASHOPS_AGENT_BASIC_USER` | 空 | 可选上游认证网关用户名;当前预览站未使用 |
| `FLASHOPS_AGENT_BASIC_PASSWORD` | 空 | 可选上游认证网关密码;当前预览站未使用 |
用户名和密码必须同时配置。Agent 不会把 bearer token 输出到终端;首次注册响应中的原始 token
只写入本机状态文件。重新注册会轮换 token,使旧 token 立即失效。
### 11.3 配置原则
- 凭据不得写进仓库、本文档、systemd unit 或 Nginx 注释;
- 生产密码仅保存在受控凭据文件、密码管理器或服务器哈希文件;
- 修改生产环境变量后必须重启 `flashops.service`
- 数据库和对象目录必须可写,其余应用目录应只读;
- SQLite URL 使用四个斜杠表示绝对路径;
- 多 worker 前必须先切换 PostgreSQL 和外部事件总线。
---
## 12. 腾讯云生产部署与运维
### 12.1 生产资源
| 项目 | 当前值 |
|---|---|
| 云厂商 | 腾讯云 CVM |
| 操作系统 | Ubuntu 24.04 |
| 公网 IP | `62.234.39.66` |
| 域名 | `flashops.imagebrewing.com``git.imagebrewing.com` |
| DNS | DNSPod A 记录,TTL 600 |
| 反向代理 | 宝塔 Nginx |
| 应用服务 | `flashops.service` |
| 应用用户 | `ubuntu` |
| 内部端口 | `127.0.0.1:18080` |
| 应用根目录 | `/data/wangzhan/app/storage-labos` |
| 持久化目录 | `/data/wangzhan/app/storage-labos/flashops/var` |
| Nginx vhost | `/www/server/panel/vhost/nginx/flashops.imagebrewing.com.conf` |
| 证书 | `/etc/letsencrypt/live/flashops.imagebrewing.com/` |
| 发布包备份 | `/data/wangzhan/backups/flashops-release-20260727.tar.gz` |
| Git 服务 | Gitea 1.27.0 Docker 容器,回环端口 `127.0.0.1:13000` |
| Git 实时数据 | `/data/gitea/data`,本地文件系统 |
| Git 云端备份 | `/chucun/wangzhan-production/backups/gitea/`,每日定时 dump + SHA-256 |
初次证书签发于 2026-07-27,初始到期日为 2026-10-25。Certbot 已配置自动续期,并通过
`--dry-run` 验证;续期成功后会执行 `/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh`
> Agent 注册 / 心跳能力已在本地代码与进程联调中通过,但本节所述线上版本尚未发布该增量。
> 发布前需创建 root-only `/etc/flashops/flashops.env` 并配置
> `FLASHOPS_AGENT_ENROLLMENT_TOKEN`;不得把注册口令写进 systemd unit 或仓库。
### 12.2 生产安全边界
- Uvicorn 仅监听回环地址,不直接暴露 18080;
- Nginx 统一处理 TLSFlashOps 当前作为匿名半成品预览开放;
- ACME challenge 路径不要求认证,以支持自动续期;
- systemd 启用 `NoNewPrivileges``PrivateTmp``ProtectSystem=full``ProtectHome`
-`flashops/var` 允许应用写入;
- 当前 SQLite 生产基线固定一个 worker
- 公开预览不得绑定真实 DUT、客户数据、真实 OOB 或有效危险命令;接入前必须先启用应用级
账号、RBAC、审批与命令模板白名单。
### 12.3 服务管理
```bash
sudo systemctl status flashops --no-pager -l
sudo systemctl restart flashops
sudo systemctl stop flashops
sudo systemctl start flashops
sudo systemctl enable flashops
```
健康检查:
```bash
curl -fsS http://127.0.0.1:18080/api/v1/health
curl https://flashops.imagebrewing.com/api/v1/health
```
### 12.4 日志
```bash
# 应用实时日志
sudo journalctl -u flashops -f
# 最近 200 行
sudo journalctl -u flashops -n 200 --no-pager
# Nginx 访问 / 错误日志
sudo tail -f /www/wwwlogs/flashops.imagebrewing.com.log
sudo tail -f /www/wwwlogs/flashops.imagebrewing.com.error.log
```
### 12.5 Nginx 与证书
```bash
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certificates
sudo certbot renew --cert-name flashops.imagebrewing.com \
--dry-run --non-interactive --no-random-sleep-on-renew
```
任何 Nginx 修改都必须先 `nginx -t`,只有成功后才能 reload。
### 12.6 发布流程
标准流程:
1. 本地 `make test`
2. 确认前端和后端兄弟目录结构;
3. 打包时排除 `.venv``var`、缓存和 `__pycache__`
4. 上传到服务器临时目录;
5. 解压到独立 incoming 目录;
6. 安装服务器虚拟环境和依赖;
7. 保留生产 `var/`,不要用本地空目录覆盖;
8. 安装 / 校验 systemd unit
9. 重启服务并轮询内部 health
10. `nginx -t` 后 reload
11. 从服务器和外部网络各验证一次匿名 HTTPS 200 和 API health
12. 保存带 SHA-256 的发布包到 `/data/wangzhan/backups/`
### 12.7 SQLite 备份
推荐使用 SQLite 在线备份 API,避免直接复制正在写入的数据库:
```bash
sudo /data/wangzhan/app/storage-labos/flashops/.venv/bin/python - <<'PY'
import datetime
import sqlite3
from pathlib import Path
source = Path('/data/wangzhan/app/storage-labos/flashops/var/flashops.db')
stamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
target = Path('/data/wangzhan/backups') / f'flashops-db-{stamp}.db'
target.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(source) as src, sqlite3.connect(target) as dst:
src.backup(dst)
print(target)
PY
```
恢复数据库是高影响操作:先停止服务,核对目标备份并额外备份当前数据库,再替换文件、修复
`ubuntu:ubuntu` 所有权、启动服务并检查事件序号连续性。不得在未确认备份内容时直接覆盖。
### 12.8 公网预览访问策略
2026-07-27 起,`flashops.imagebrewing.com` 已取消 Nginx Basic Auth 和 IP 访问限制,匿名用户
可以查看控制台并调用模拟 API。取消前的 Nginx 配置保留在:
```text
/www/server/panel/vhost/nginx/flashops.imagebrewing.com.conf.before-public-20260727
```
这是临时产品演示策略,不是最终安全方案。接入真实硬件或非公开数据前,不能只恢复 Basic Auth;
必须优先实现应用账号、RBAC、危险动作审批和审计上下文。
### 12.9 发布验收清单
- [ ] `flashops.service` 为 active
- [ ] Nginx 为 active 且 `nginx -t` 成功;
- [ ] `getent ahostsv4 flashops.imagebrewing.com` 指向 `62.234.39.66`
- [ ] HTTP 返回 301 到 HTTPS
- [ ] HTTPS 证书域名正确且未过期;
- [ ] 未登录访问控制台最终返回 200
- [ ] `/api/v1/health` 返回 `status=ok`
- [ ] `/api/v1/dashboard/summary` 返回合法 JSON
- [ ] 生产 `var/flashops.db``var/objects` 可写;
- [ ] 应用最近日志无 error
- [ ] Certbot 模拟续期成功;
- [ ] 发布包和数据库备份已记录。
### 12.10 Gitea 团队 Git 服务
| 项目 | 当前值 |
|---|---|
| 页面与 clone 地址 | `https://git.imagebrewing.com` |
| 项目仓库 | `https://git.imagebrewing.com/yuanshuai/storage-labos` |
| 容器 | `gitea` / `gitea/gitea:1.27.0` |
| HTTP 监听 | `127.0.0.1:13000`,不直接暴露公网 |
| Git 传输 | HTTPS;当前不开放独立 SSH 端口 |
| 匿名访问 | 可查看 public 仓库与最近提交 |
| 注册 | 开放申请,但必须由管理员人工批准后才能登录 |
| 实时数据 | `/data/gitea/data`,不得放在 COSFS 上 |
| 备份 | 每日约 03:20 dump 到 `/chucun/wangzhan-production/backups/gitea/` |
Gitea 管理员凭据只保存在本机被 `.gitignore` 排除的 `gitea-admin.env.local`,不写进手册、
仓库或服务器部署文件。团队成员先在网页注册,再由管理员在“站点管理 → 用户账户”中批准,
随后把成员加入仓库协作者。
```bash
# 服务与健康
sudo docker ps --filter name=gitea
curl -fsS http://127.0.0.1:13000/api/healthz
# 日志
sudo docker logs --tail 200 gitea
# 备份定时器与手工备份
sudo systemctl status flashops-gitea-backup.timer --no-pager
sudo systemctl start flashops-gitea-backup.service
```
生产服务器没有启用 Gitea Actions Runner。原因是 Runner 需要执行仓库中的任意代码,直接挂在
同一台生产服务器会放大供应链风险;当前 PR 必须附本地 `make test` 结果。未来 CI 应部署到
隔离机器或一次性 Runner,而不是把 Docker socket 交给生产仓库任务。
---
## 13. 测试、质量门与开发规范
### 13.1 当前测试基线
截至 2026-07-27
- 测试总数:25
- 含 Agent 增量的完整测试集本地连续 20 轮全部通过;
- 腾讯云生产环境依赖下连续 20 轮全部通过;
- 服务器单轮约 12 秒;
- 证书续期 dry-run 通过;
- systemd 重启后 health 恢复通过;
- FlashOps 匿名 HTTPS 200、Gitea HTTPS 200 与两个证书域名验证通过。
### 13.2 测试分层
| 层 | 文件 | 重点 |
|---|---|---|
| 状态机 | `test_state_machine.py` | 合法 / 非法转移与终态 |
| 恢复策略 | `test_recovery.py` | Agent、网络、OOB、完整性触发 |
| 安全 | `test_safety.py` | 序列号、系统盘、分区和门禁组合 |
| 端到端 | `test_e2e_vertical_slice.py` | 创建、恢复、证据、暂停、继续、急停 |
| Agent 安全 | `test_agent_enrollment.py` | 生产缺少注册口令时默认拒绝 |
| Agent 状态 | `test_agent_status.py` | 新鲜、迟到、失联和主动降级心跳 |
| Agent 客户端 | `services/host-agent/tests/test_client.py` | HTTP 契约、401、凭据 `0600` |
### 13.3 提交前检查
```bash
cd "/Users/yuanshuai/Desktop/STORAGE LABOS/flashops"
make test
make demo
```
如果改了部署配置,再检查:
```bash
systemd-analyze verify deploy/flashops.service # 在 Linux 上执行
sudo nginx -t # 在目标服务器执行
```
### 13.4 开发规则
1. 状态变化必须追加事件;
2. 新状态必须更新枚举、转移表、文档和测试;
3. 新事件 kind 只增不改,历史事件已经落库;
4. 安全规则保持纯函数,拒绝发生在下发之前;
5. 危险步骤默认不可自动重试;
6. API 路由不堆业务逻辑;
7. 真实适配器与模拟适配器使用同一契约;
8. 大日志写对象存储,不塞进数据库 JSON;
9. 生产数据和凭据不进 Git
10. 修改数据结构必须同时提供迁移和回滚说明;
11. 文档中的“已实现”必须能由代码或验收记录证明;
12. 生产发布必须保留可核验的归档包和数据库备份。
### 13.5 Git 仓库与团队协作
当前 Git 基线:
| 项目 | 当前值 |
|---|---|
| 本地仓库根 | `STORAGE LABOS/` |
| 默认分支 | `main` |
| 远程 | `https://git.imagebrewing.com/yuanshuai/storage-labos.git` |
| 网页 | `https://git.imagebrewing.com/yuanshuai/storage-labos` |
| 可见性 | public:匿名可读;提交、Issue 和 PR 写操作必须登录 |
| 管理员 | `yuanshuai`;密码只在本机忽略文件中保存 |
| 团队规则入口 | 根目录 `CONTRIBUTING.md` |
不要通过聊天工具反复传 ZIP 继续开发。团队以远程仓库为唯一代码与文档版本源,新成员执行:
```bash
git clone https://git.imagebrewing.com/yuanshuai/storage-labos.git
cd storage-labos/flashops
make setup
make test
```
分支规则:
- `main`:始终保持可测试、可发布,禁止直接开发和强制推送;
- `feat/<short-name>`:功能;
- `fix/<short-name>`:缺陷;
- `docs/<short-name>`:文档;
- `chore/<short-name>`:仓库、依赖、CI 和部署。
日常开发:
```bash
git switch main
git pull --ff-only
git switch -c feat/agent-step-lease
# 修改后先验证
cd flashops
make test
make demo
cd ..
# 只暂存本次相关文件;混合工作区不要直接 git add -A
git status -sb
git add <明确的文件或目录>
git diff --cached --stat
git diff --cached --check
git commit -m "feat(agent): add step lease polling"
git push -u origin "$(git branch --show-current)"
```
采用 Conventional Commits`type(scope): subject`
| 类型 | 用途 | 示例 |
|---|---|---|
| `feat` | 新功能 | `feat(agent): add step lease polling` |
| `fix` | 修复 | `fix(safety): reject stale host heartbeat` |
| `docs` | 文档 | `docs(manual): add gitea operations` |
| `test` | 测试 | `test(agent): cover token rotation` |
| `refactor` | 不改变行为的重构 | `refactor(events): isolate projection writer` |
| `perf` | 性能优化 | `perf(api): reduce timeline query cost` |
| `chore` | 工具、依赖、部署 | `chore(repo): initialize collaboration repository` |
每项工作使用独立分支和 Pull Request。PR 必须说明改动、原因、影响、验证结果、风险和回滚;
至少一名团队成员评审后再合并。优先 Squash merge,合并后删除功能分支。只允许对自己的未合并
分支使用 `--force-with-lease`,严禁对 `main` 强推;已共享的错误提交使用 `git revert` 撤销。
团队成员在 Gitea 注册后需要管理员人工批准。批准后在仓库“设置 → 协作者”中授予写权限;
不要共用管理员账号。离职或设备丢失时立即移除成员、撤销 token 并检查审计记录。
以下内容永不入库:`.env`、密码、token、私钥、证书私钥、Agent state、SQLite、Evidence、日志、
生产备份、`.venv``node_modules` 和未脱敏客户数据。若凭据误入提交,先轮换凭据,再清理历史;
仅删除当前文件不能消除 Git 历史中的泄露。
### 13.6 CI 与 Pull Request 质量门
根目录 `.github/workflows/ci.yml` 已提供 Python 3.9 / 3.12 测试矩阵模板,执行控制平面与
Host Agent 测试并编译全部 Python 模块。它可用于后续 GitHub 镜像或启用 Gitea Actions。
当前生产 Gitea 未配置 Actions Runner,因此这个工作流不会自动执行。这样做是刻意的:把
Runner 和 Docker socket 放在同一台生产服务器,会允许仓库代码获得过大的服务器权限。
现阶段 PR 必须附上本地 `make test` 与相关演示结果;下一阶段把 Runner 放到隔离机器或一次性
执行环境,再把检查设为 `main` 的必需状态。
仍待补充的质量门:PostgreSQL 集成、Ruff / mypy、工作流 schema、依赖漏洞与 secret scan、
Nginx / systemd 静态检查、模拟器长跑、发布包 SBOM 和签名。
---
## 14. 已知边界与演进路线
### 14.1 当前已知边界
| 边界 | 风险 | 处理方向 |
|---|---|---|
| SQLite 单 writer | 多并发会阻塞 | 当前单 worker;后续 PostgreSQL |
| 无 Alembic | 表结构难升级 | 引入版本化 migration |
| 无应用账号 / RBAC | 无细粒度审计与授权 | OIDC/SSO、角色、审批 |
| 匿名公网预览 | 页面和模拟 API 可被任何人调用 | 不接真实资产;真实阶段启用 OIDC/RBAC/审批 |
| Agent 无 Step 租约 | 只能注册和心跳,不能领取真实任务 | 实现 lease、ACK、续租和完成上报 |
| OOB 为模拟 | 无法验证真实断电恢复 | 接可牺牲硬件和独立网络 OOB |
| Step runtime 不完整 | 工作流只是部分执行 | planner/runtime/lease 落地 |
| Evidence 最小 | 不足以支撑真实缺陷结论 | 接工具原始输出、hash、dump 和画面 |
| 失败聚类未实现 | 失败中心为静态原型 | 归一化、签名、合并 / 拆分和复现任务 |
| 多数页面静态 | UI 与真实数据可能不一致 | 按 8.4 顺序接 API |
| 依赖范围较宽 | 重新安装可能拿到不同版本 | 生成并维护 production lock file |
| 无定时 DB 备份 | 数据恢复点不稳定 | systemd timer + 备份校验 + 保留策略 |
### 14.2 建议演进顺序
#### Phase 1:独立 Agent 契约
- 已完成:注册、token 轮换、心跳、状态老化、主机探针和本地凭据;
- 待完成:租约、ACK、续租、结果上报;
- idempotency key 和重放保护;
- 模拟 Agent 与控制平面完全分进程;
- Agent 崩溃、网络断开、控制平面重启测试。
#### Phase 2:真实只读硬件接入
- NVMe identify / SMART / BDF / device path
- Host 环境探针;
- OOB 只读电源和画面状态;
- 不执行刷写、Format、Sanitize 或断电。
#### Phase 3:受控危险动作
- 命令模板白名单;
- 审批、双人复核、短期授权;
- 可牺牲 DUT
- 真实刷写、Reset、ATX、AC 阶梯;
- 完整 Evidence Bundle。
#### Phase 4:多工位与生产化
- PostgreSQL、外部事件总线、对象存储;
- 多工位调度与资源配额;
- SSO / RBAC / 审计;
- HA、监控、告警、备份和灾备;
- 评估 Temporal 是否只替换 runtime。
#### Phase 5:失败智能与版本决策
- 失败签名聚类;
- 环境可比性检查;
- 自动裁剪最小复现工作流;
- A/B 指标和证据门禁;
- AI 只生成带证据引用的摘要,不替代工程师结论。
---
## 15. 故障排查
### 15.1 公网返回 502
```bash
sudo systemctl status flashops --no-pager -l
sudo journalctl -u flashops -n 200 --no-pager
curl -v http://127.0.0.1:18080/api/v1/health
sudo nginx -t
sudo tail -n 100 /www/wwwlogs/flashops.imagebrewing.com.error.log
```
常见原因:服务未启动、虚拟环境缺依赖、端口不一致、数据库目录不可写、systemd 安全策略阻止写入。
### 15.2 公网返回 401 / 403
当前预览站允许匿名访问,首页和普通查询 API 不应由 Nginx 返回 401 / 403。遇到问题时:
- 用无痕窗口访问,排除浏览器缓存的旧认证状态;
- 检查 Nginx 生效配置中是否残留 `auth_basic``allow``deny`
- 区分普通页面与 Agent APIAgent 注册口令或 bearer token 无效仍会返回 401
- 执行 `nginx -t`,确认无误后 reload
- 查看站点 access/error 日志,确认状态码来自 Nginx、应用还是上游安全产品。
### 15.3 HTTPS 证书错误
```bash
getent ahostsv4 flashops.imagebrewing.com
sudo certbot certificates
sudo nginx -T | grep -A8 -B3 flashops.imagebrewing.com
openssl s_client -connect 62.234.39.66:443 \
-servername flashops.imagebrewing.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
```
检查 DNS 是否指向正确 IP、证书路径是否存在、Nginx 是否已经 reload。
### 15.4 控制台 404
确认目录关系:
```text
/data/wangzhan/app/storage-labos/
├── flashops/
└── 前端UI八页面完成/
```
`main.py` 通过 `REPO_ROOT.parent / "前端UI八页面完成"` 查找控制台。
### 15.5 SQLite locked / 事件序号冲突
- 确认 Uvicorn 仍为一个 worker
- 不要从多个进程同时直接操作同一个 SQLite 文件;
- 所有应用事务必须经 `session_scope()`
- 所有 Run 状态写入必须经 `append_event()`
- 检查是否有脚本绕过服务直接写库;
- 若需要多进程并发,迁移 PostgreSQL,不要继续堆 SQLite 锁。
### 15.6 服务不断重启
```bash
sudo systemctl show flashops -p NRestarts -p ExecMainStatus
sudo journalctl -u flashops --since '30 minutes ago' --no-pager
```
重点检查:`PYTHONPATH`、依赖安装、数据库 URL、目录权限、配置拼写和 18080 端口占用。
### 15.7 测试本机通过、服务器失败
比较:
```bash
.venv/bin/python --version
.venv/bin/pip freeze
```
当前 requirements 使用范围约束而非完整 lock,同一天不同环境可能安装不同 FastAPI、Starlette、
httpx 或 SQLAlchemy 版本。应先复现服务器依赖,再决定修代码或锁版本,不能只在本机降级掩盖并发缺陷。
### 15.8 Gitea 无法访问、注册或推送
```bash
sudo docker ps --filter name=^gitea$
sudo docker logs --tail 100 gitea
curl -fsS http://127.0.0.1:13000/api/healthz
getent ahostsv4 git.imagebrewing.com
sudo nginx -t
```
- 公开仓库页面应允许匿名查看和克隆;
- 新成员注册后需要管理员在 Gitea 后台批准,未批准时不能登录;
- 推送必须使用已批准账号,不要把密码或访问令牌写入远程地址;
- 若备份失败,检查 `gitea-backup.service` 日志和 `/chucun` 挂载状态;
- Gitea 实时数据必须保留在 `/data/gitea/data`,不得迁移到 COSFS 挂载目录。
---
## 16. 术语表与 FAQ
### 16.1 术语表
| 术语 | 含义 |
|---|---|
| DUT | Device Under Test,被测 SSD / NVMe 设备 |
| Host | 连接 DUT 并运行测试工具的主机 |
| Agent | 部署在 Host 上,拉取并执行已批准步骤的服务 |
| OOB | Out-of-Band,主机失联后仍可观测 / 控制的带外通道 |
| Run | 一次完整工作流执行实例 |
| Step | Run 中最小可调度执行单元 |
| Workflow | 版本化 SOP 和步骤定义 |
| Checkpoint | 可安全恢复的执行边界 |
| Evidence Bundle | 一次 Run 的证据清单与文件集合 |
| Event Sourcing | 用 append-only 事件作为状态变化真相 |
| Projection | 从事件派生的当前状态读模型 |
| UCR | Unattended Completion Rate,无人值守完成率 |
| Failure Signature | 归一化失败要素生成的聚类标识 |
| Idempotency | 同一请求重复执行不会产生额外副作用 |
| Lease | 控制平面临时授予 Agent 的步骤执行权 |
| BDF | PCIe Bus:Device.Function 地址 |
### 16.2 FAQ
#### 为什么生产还用 SQLite
当前是单工位、单 worker、内部演示基线,SQLite 能降低部署摩擦。它不是多工位最终方案;增加并发前
必须切换 PostgreSQL。
#### 为什么不用 Airflow / Temporal
恢复语义跨越 Agent、OS、OOB 和物理供电,且非幂等步骤默认不重跑。首版用纯函数状态机更容易
精确控制和测试。多工位跨节点调度成为瓶颈时,再评估用 Temporal 替换 runtime。
#### 为什么事件不能按时间戳排序?
Host、OOB 和控制平面时钟可能不同,主机崩溃时还会丢失最后日志。控制平面分配的 `seq` 才能提供
稳定、可重放的总顺序。
#### 为什么数据完整性失败直接冻结?
自动重启、重跑或写盘可能覆盖最关键的现场。宁可暂停等待工程师,也不能为了完成率破坏证据。
#### 为什么 Agent 要用拉模式?
客户测试主机通常位于 NAT、内网或防火墙后面。由 Agent 主动向控制平面发起连接更容易部署,
也减少暴露入站端口。
#### 为什么控制台和后端放在一起?
当前静态控制台由 FastAPI 同源挂载,避免单独的前端构建和 CORS 配置,适合原型与演示。后续如果
切换 Next.js 或独立 SPA,可以保持 `/api/v1` 契约不变。
#### 管理凭据在哪里?
本文档不保存明文密码。Gitea 管理员、SSH 与云平台凭据通过本机受控文件、系统钥匙串或密码管理器
交接;这些文件必须命中 `.gitignore`,不得进入提交历史。
#### 可以直接把 18080 暴露公网吗?
不可以。该端口没有应用级认证和 TLS,必须只绑定 `127.0.0.1`,公网只走 Nginx 443。
---
## 17. 变更记录
### V1.2 — 2026-07-27
- 上线 `git.imagebrewing.com` Gitea 团队 Git 服务和公开项目仓库;
- 增加分支、提交、评审、密钥和本地验证规范;
- 使用 `/chucun/wangzhan-production/backups/gitea/` 保存每日 Gitea 校验备份;
- FlashOps 预览站取消 Nginx Basic Auth 与 IP 限制,普通页面和查询 API 可匿名访问;
- 明确匿名预览不得连接真实硬件、危险动作或非公开数据,正式生产前必须完成 SSO / RBAC。
### V1.1 — 2026-07-27
- 新增独立拉模式 Host Agent
- 新增 Agent 注册、token 轮换、Bearer 心跳和 Host 状态查询 API
- 控制平面只保存 token SHA-256 摘要,Agent 凭据文件权限固定为 `0600`
- 生产环境缺少 Agent 注册口令时默认拒绝注册;
- Agent 保留可选网关 Basic Auth 能力,当前匿名预览环境不启用;
- 新增 Agent 客户端、注册安全、状态老化和进程联调测试;全量测试增至 22 项;
- Step 租约、ACK、事件、完成与产物上传仍为下一阶段。
### V1.0 — 2026-07-27
- 根据实际源码建立首版完整项目手册;
- 记录控制平面、14 张表、状态机、六项门禁和 Evidence Bundle
- 记录 8 个核心控制台页面及真实 / 静态边界;
- 记录腾讯云、DNSPod、systemd、Nginx 和 Let's Encrypt 部署;
- 记录 SQLite 并发写入修复和单 worker 约束;
- 记录本机与服务器连续 20 轮测试结果;
- 给出生产发布、验收、备份、密码轮换和故障排查流程;
- 明确独立 Agent、真实硬件、RBAC、PostgreSQL 和失败聚类为后续工作。
---
## 文档维护要求
以下变化发生时,必须同步更新本文:
- 新增 / 删除 API
- 新增状态、事件种类或恢复级别;
- 修改表结构或迁移策略;
- 修改环境变量;
- 调整域名、端口、服务器路径或服务名;
- 新增真实 Agent、Adapter 或 OOB
- 安全门禁、审批或认证方式变化;
- Evidence Bundle schema 变化;
- 生产发布和回滚流程变化;
- 能力从“模拟 / 原型”变为“真实已验收”。
文档里的完成状态必须由代码、测试或生产验收记录支撑。