Files
storage-labos/flashops/docs/adapter-sdk.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

4.8 KiB
Raw Blame History

Adapter SDK:产品扩张的关键边界

方案 §6.4 的判断是对的——适配器是产品扩张的关键。每接一个客户, 新增的工作量应该收敛到"写一个适配器",而不是改引擎。这一波把这条边界钉死。

实现:services/host-agent/flashops_agent/adapters/

契约(7 个方法,一个都不能少)

class ToolAdapter(Protocol):
    name: str
    version: str

    def discover(self, ctx: AdapterContext) -> Capabilities: ...
    def precheck(self, ctx: AdapterContext) -> CheckResult: ...
    def execute(self, step: StepSpec, ctx: AdapterContext) -> ExecutionHandle: ...
    def collect(self, handle: ExecutionHandle) -> EvidenceArtifact: ...
    def cancel(self, handle: ExecutionHandle) -> None: ...
    def health(self) -> HealthStatus: ...
    def normalize_result(self, raw: RawResult) -> UnifiedResult: ...
方法 职责 什么时候被调
discover 报告本工具在这台主机上能干什么(版本、支持的设备、能力位) Agent 启动 / 资产刷新
precheck 执行前自检:工具在不在、权限够不够、设备在不在、参数合不合法 每个步骤执行前,失败即拒绝执行
execute 启动执行,立即返回句柄,不阻塞 步骤开始
collect 从句柄收集产物:stdout/stderr、结果文件、设备日志 步骤结束 / 中途取证
cancel 终止执行(含进程树),保证不留孤儿进程 紧急停止 / 超时 / 恢复前
health 适配器自身健康(工具还在吗、许可证过期没) 心跳周期
normalize_result 把工具原生输出翻译成统一结果模型 collect 之后

normalize_result 是最重要的一个

它决定了失败签名能不能跨工具聚类。原生输出千奇百怪,统一模型只有一种:

@dataclass
class UnifiedResult:
    outcome: Literal["PASS", "FAIL", "ERROR", "TIMEOUT"]
    error_codes: List[str]        # 归一化后的错误码,如 ["NVME_STATUS_0x2002"]
    log_templates: List[str]      # 日志模板化后的指纹,数字/路径已替换为占位符
    metrics: Dict[str, float]     # iops / latency_p99 / temperature_c ...
    device_state: DeviceState     # 枚举状态、固件版本、SMART 关键项
    integrity: IntegrityState     # OK / MISMATCH / NOT_CHECKED
    artifacts: List[str]          # 产物 URI

适配器的实现者只需要保证一件事:同样的故障,error_codeslog_templates 要稳定。不稳定 → 签名散开 → 聚类失效 → 同一个问题重复提单, 客户第一时间就会发现这个系统在制造噪音。

日志模板化规则在 flashops_agent/adapters/templating.py 数字 → <N>,十六进制 → <HEX>,路径 → <PATH>UUID → <UUID> 时间戳 → <TS>。所以 "nvme0n1: I/O error, sector 12345678 at 2026-07-27T03:14:15" 归一为 "nvme<N>n<N>: I/O error, sector <N> at <TS>"

这一波带的适配器

适配器 状态 说明
ShellAdapter 真实 执行签名命令模板,进程树管理、超时、输出捕获
NvmeCliAdapter ⚠️ 真实骨架 + 模拟回退 命令与解析是真的;无真实设备时走模拟器
FioAdapter ⚠️ 真实骨架 + 模拟回退 解析 fio --output-format=json
SimulatedDutAdapter 模拟 可注入故障的假 DUT,让全链路无硬件可跑
VendorFlashAdapter 占位 客户私有刷写工具——这个必须等拿到真实 SOP 再写

VendorFlashAdapter 故意留空并在导入时抛 NotImplementedError。 猜客户的刷写工具长什么样是纯浪费——报告 Phase 0 的结论是先拿 SOP 再写代码。

写一个新适配器的清单

  1. 继承 BaseAdapter,实现 7 个方法
  2. adapters/__init__.pyREGISTRY 里注册(key 就是工作流 YAML 里的 adapter:
  3. 危险命令必须在控制平面 safety/templates.py 注册签名模板; 适配器里不允许拼接任意命令行——参数只能来自模板 schema 校验过的字典
  4. 写一个 tests/adapters/test_<name>.py:至少覆盖 precheck 失败路径、超时路径、normalize_result 的两个不同故障输出
  5. make test 里的适配器契约测试(test_adapter_contract.py 会对 REGISTRY 里每个适配器自动断言 7 个方法齐全且签名正确)

边界纪律

  • 适配器不知道 Run、Workflow、状态机的存在。它只认 StepSpecAdapterContext
  • 适配器不做重试和恢复决策。挂了就如实报告,重试与恢复是控制平面的事。
  • 适配器不写数据库,产物写本地临时目录,由 Agent 上传。
  • 适配器里不允许出现 if customer == "X" 这种分支。客户差异靠不同适配器 + 工作流参数表达,不靠代码里的 if。