Files
hawkbit/huaxu/RUNBOOK.md
lingwei.kong c191bd07ba
Some checks failed
Mark & close stale issues / stale (push) Has been cancelled
Vulnerability Scan / trivy-scan (1.0) (push) Has been cancelled
Vulnerability Scan / trivy-scan (master) (push) Has been cancelled
添加huaxuapi说明
2026-06-26 17:14:13 +08:00

59 KiB
Raw Permalink Blame History

Hawkbit DDI Client — 运行流程文档

目录

  1. 整体架构
  2. 首次注册流程
  3. 守护进程轮询流程
  4. 单次轮询流程
  5. OTA 部署流程
  6. 认证流程
  7. 取消任务流程
  8. 确认任务流程
  9. 断点续传流程
  10. 错误处理流程
  11. 安装钩子定制
  12. 部署与运维

1. 整体架构

┌──────────────────────────────────────────────────────────┐
│                    ddi-client.py                          │
│                                                          │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │  属性检测  │  │  认证模块  │  │  轮询模块  │  │  安装钩子  │ │
│  │ (SN/OS/  │  │ Gateway/ │  │ Poll/    │  │ _install()│ │
│  │  HW/Kernel│  │ Target   │  │ Deploy/  │  │ 可覆盖    │ │
│  │  ...)    │  │ Token    │  │ Download │  │          │ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘ │
│       │             │             │             │        │
│       ▼             ▼             ▼             ▼        │
│  ┌──────────────────────────────────────────────────┐    │
│  │                   HTTP 层                          │    │
│  │       (urllib + SSL context + Range 支持)          │    │
│  └──────────────────────┬───────────────────────────┘    │
└─────────────────────────┼────────────────────────────────┘
                          │
                          ▼
              ┌──────────────────────┐
              │    Hawkbit Server    │
              │   DDI API v1 (9090)  │
              └──────────────────────┘

数据流

属性文件 ──┐
           ├──→ 属性检测 ──→ PUT /configData ──→ Hawkbit 记录 target attributes
SN 检测  ──┘

轮询 ──→ GET /controller/v1/{SN}
      ←── deploymentBase / cancelAction / confirmationBase / configData / 无

部署 ──→ GET /deploymentBase/{id}      ←── chunks[].artifacts[]
下载 ──→ GET /softwaremodules/{id}/...  ←── 二进制流 (Range 支持)
反馈 ──→ POST /feedback                 ←── 200 OK

2. 首次注册流程

触发条件:设备 SN 在 Hawkbit 中不存在(或脚本首次连接后重启)

启动参数:
  ddi-client.py -u http://192.168.77.70:8080 -t DEFAULT \
      --gateway-token @/etc/hawkbit/gateway.key

═══════════════════════════════════════════════════════════════
 Step 1: 读取 Gateway Token
═══════════════════════════════════════════════════════════════
  输入: @/etc/hawkbit/gateway.key
  动作: open("/etc/hawkbit/gateway.key") → read() → strip()
  输出: Authorization: GatewayToken <文件内容>

  备选: --gateway-token <明文key> 直接传入
        --target-token <明文token> 直接传入
        --target-token @/etc/hawkbit/target.token 文件读取

═══════════════════════════════════════════════════════════════
 Step 2: 检测设备 SN
═══════════════════════════════════════════════════════════════
  优先级:
    ① getprop ro.serialno              → Android 序列号
    ② /sys/class/dmi/id/product_serial  → Linux DMI (x86)
    ③ /sys/devices/virtual/dmi/id/...   → Linux DMI (ARM/嵌入式)
    ④ /etc/machine-id                   → systemd 机器 ID
    ⑤ socket.gethostname()              → 兜底

  跳过条件: --controller-id 显式指定
  输出: controllerId = "89aa36c5b6744dc2910479f7ecd6d2c3"

═══════════════════════════════════════════════════════════════
 Step 3: 检测设备属性
═══════════════════════════════════════════════════════════════
  自动检测:
    osType      → /system/build.prop 存在 ? "android" : "linux"
    platform    → platform.machine()                             # x86_64 / aarch64
    kernel      → os.uname().release                            # 6.17.0-35-generic
    hostname    → socket.gethostname()
    hwRevision  → DMI product_name + product_version
    manufacturer→ DMI sys_vendor / Android ro.product.manufacturer

  Android 额外属性:
    androidVersion → ro.build.version.release
    productModel   → ro.product.model
    buildNumber    → ro.build.version.incremental

  用户属性文件 (/etc/hawkbit/device_attrs.json):
    {"dept": "production", "region": "east", "owner": "team-a"}

  版本文件 (/etc/hawkbit/current_version) :
    V1.0.0.0  →  swVersion

═══════════════════════════════════════════════════════════════
 Step 4: 上报属性
═══════════════════════════════════════════════════════════════
  PUT http://host/DEFAULT/controller/v1/{SN}/configData
  Body:
    {
      "mode": "merge",
      "data": {
        "osType": "linux",
        "platform": "x86_64",
        "kernel": "6.17.0-35-generic",
        "hostname": "device-001",
        "hwRevision": "VMware Virtual Platform",
        "manufacturer": "VMware, Inc.",
        "swVersion": "V1.0.0.0",
        "dept": "production",
        "region": "east"
      }
    }

  mode 说明:
    "merge"   → 只更新传入的 key保留已有属性 (推荐)
    "replace" → 完全替换
    "remove"  → 删除传入的 key

═══════════════════════════════════════════════════════════════
 Step 5: 首次轮询(自动注册)
═══════════════════════════════════════════════════════════════
  GET http://host/DEFAULT/controller/v1/{SN}
  Hawkbit 行为:
    findOrRegisterTargetIfItDoesNotExist(SN)
    → 不存在 → INSERT INTO target (controller_id, ip_address, ...)
    → 存在   → UPDATE last_target_query

  响应:
    无 action:
      {"config": {"polling": {"sleep": "00:05:00"}}, "_links": {}}
      → sleep 5分钟

    有 deployment:
      "_links": {
        "deploymentBase": {"href": "http://.../deploymentBase/1?c=..."},
        "configData":     {"href": "http://.../configData"}
      }
      → 进入 OTA 部署流程

    有 cancel:
      "_links": {"cancelAction": {"href": "http://.../cancelAction/..."}}
      → 进入取消流程

    有 confirmation:
      "_links": {"confirmationBase": {"href": "http://.../confirmationBase"}}
      → 进入确认流程

3. 守护进程轮询流程

启动命令:
  ddi-client.py -u http://192.168.77.70:8080 -t DEFAULT \
      --gateway-token my-key -i 60

┌─────────────────────────────────────────────────┐
│              守护模式 (daemon=True)                │
├─────────────────────────────────────────────────┤
│                                                  │
│  ┌──────────┐                                    │
│  │ 启动/打印 │  ╔═══════════════════╗              │
│  │ banner   │  ║ Server:  host     ║              │
│  └────┬─────┘  ║ Tenant:  DEFAULT  ║              │
│       │        ║ Device:  <SN>     ║              │
│       ▼        ║ Auth:    Gateway  ║              │
│  ┌──────────┐  ╚═══════════════════╝              │
│  │ 上报属性  │  PUT /configData                    │
│  │(首次)    │  (仅一次, 成功即跳过)                  │
│  └────┬─────┘                                     │
│       │                                           │
│       ▼                                           │
│  ┌──────────┐                                     │
│  │   轮询    │  GET /controller/v1/{SN}            │
│  │  Poll    │                                     │
│  └────┬─────┘                                     │
│       │                                           │
│   ┌───┴───┬───────────┬───────────┐               │
│   ▼       ▼           ▼           ▼               │
│ deploy  cancel    confirm     无任务               │
│   │       │         │           │                 │
│   ▼       ▼         ▼           │                 │
│ OTA流程  取消流程   确认流程      │                 │
│   │       │         │           │                 │
│   └───┴───┴─────────┴───────────┘                 │
│                   │                               │
│                   ▼                               │
│            ┌──────────────┐                       │
│            │ sleep(N 秒)  │  N = max(server建议,  │
│            │              │         默认60s)       │
│            └──────┬───────┘                       │
│                   │                               │
│                   └──────→ 回到轮询 ◄── 循环       │
│                                                  │
│  退出: Ctrl+C (KeyboardInterrupt)                 │
│        max_cycles 达到上限                         │
└─────────────────────────────────────────────────┘

轮询间隔策略:
  - 默认: --polling-interval 60 (或 -i 指定)
  - 服务端可覆盖: response.config.polling.sleep → "00:05:00" → 5分钟
  - 有活跃 action 时: 服务端可能缩短间隔 (getPollingTimeForAction)
  - 轮询失败时: 退避到 min(polling_interval, 30s)

4. 单次轮询流程

启动命令:
  ddi-client.py -u http://host:9090 -t DEFAULT \
      --gateway-token my-key --once

┌───────────────────────────────────────────────────────────┐
│               单次模式 (daemon=False, --once)                │
├───────────────────────────────────────────────────────────┤
│                                                            │
│  ① 上报属性 (首次)                                         │
│       │                                                    │
│       ▼                                                    │
│  ② 轮询 GET /controller/v1/{SN}                            │
│       │                                                    │
│   ┌───┴───┬───────────┬───────────┐                       │
│   ▼       ▼           ▼           ▼                       │
│ deploy  cancel    confirm     无任务                        │
│   │       │         │           │                         │
│   ▼       ▼         ▼           │                         │
│ OTA流程  取消流程   确认流程      │                         │
│   │       │         │           │                         │
│   └───┴───┴─────────┴───────────┘                         │
│                   │                                        │
│                   ▼                                        │
│  ③ sys.exit(0)                                            │
│                                                            │
│  用途:                                                     │
│    - systemd timer / cron 定时触发                          │
│    - 测试/调试                                              │
│    - 脚本化: && 链式调用后续操作                              │
└───────────────────────────────────────────────────────────┘

cron 示例:
  */5 * * * * /usr/bin/python3 /opt/hawkbit/ddi-client.py \
      -u https://hawkbit:9090 -t DEFAULT \
      --gateway-token @/etc/hawkbit/gateway.key --once

systemd timer 示例:
  [Unit]
  Description=Hawkbit DDI poll
  [Service]
  ExecStart=/usr/bin/python3 /opt/hawkbit/ddi-client.py \
      -u https://hawkbit:9090 -t DEFAULT \
      --gateway-token @/etc/hawkbit/gateway.key --once
  [Timer]
  OnBootSec=30s
  OnUnitActiveSec=300s

5. OTA 部署流程

前置条件: 轮询返回 _links.deploymentBase

┌───────────────────────────────────────────────────────────────┐
│                    OTA 部署完整生命周期                          │
├───────────────────────────────────────────────────────────────┤
│                                                                │
│  ┌─────────────────────────────────────────────────────┐      │
│  │ Step A: 拉取部署详情                                  │      │
│  │                                                      │      │
│  │  GET /deploymentBase/{actionId}?c=...                │      │
│  │  ← {                                                │      │
│  │      "id": "2",                                      │      │
│  │      "deployment": {                                 │      │
│  │        "download": "forced",                         │      │
│  │        "update": "forced",                           │      │
│  │        "chunks": [{                                  │      │
│  │          "part": "os",                               │      │
│  │          "version": "V1.0.0.0",                     │      │
│  │          "name": "r3576-OS-android",                │      │
│  │          "artifacts": [{                             │      │
│  │            "filename": "update.zip",                 │      │
│  │            "size": 195411,                           │      │
│  │            "hashes": {                               │      │
│  │              "sha256": "438b8f9e...",                │      │
│  │              "md5": "28a17f5f...",                   │      │
│  │              "sha1": "223377ea..."                   │      │
│  │            },                                        │      │
│  │            "_links": {                               │      │
│  │              "download-http": {                      │      │
│  │                "href": "http://.../update.zip"       │      │
│  │              }                                       │      │
│  │            }                                         │      │
│  │          }]                                          │      │
│  │        }]                                            │      │
│  │      }                                               │      │
│  │    }                                                 │      │
│  └──────────────────────┬──────────────────────────────┘      │
│                         │                                     │
│                         ▼                                     │
│  ┌─────────────────────────────────────────────────────┐      │
│  │ Step B: 上报 DOWNLOAD (开始下载)                       │      │
│  │                                                      │      │
│  │  POST /deploymentBase/{id}/feedback                  │      │
│  │  {"status": {                                        │      │
│  │    "execution": "download",                          │      │
│  │    "result": {"finished": "none"},                   │      │
│  │    "details": ["Starting download"]                  │      │
│  │  }}                                                  │      │
│  └──────────────────────┬──────────────────────────────┘      │
│                         │                                     │
│                         ▼                                     │
│  ┌─────────────────────────────────────────────────────┐      │
│  │ Step C: 下载 artifacts (逐个)                          │      │
│  │                                                      │      │
│  │  对每个 chunk.artifacts[]:                            │      │
│  │                                                      │      │
│  │  ① 提取 download URL (优先级)                         │      │
│  │     download (HTTPS) > download-http (HTTP)          │      │
│  │     失败 → fallback: 遍历所有 HAL link 提取 smId       │      │
│  │                                                      │      │
│  │  ② 检查本地缓存 /tmp/hawkbit/{filename}               │      │
│  │     存在 + SHA256 匹配 → 跳过下载                      │      │
│  │     存在 + SHA256 不匹配 → 重新下载                    │      │
│  │                                                      │      │
│  │  ③ 检查 .tmp 断点文件                                 │      │
│  │     存在 → 加 Range: bytes={size}- 头续传              │      │
│  │                                                      │      │
│  │  ④ GET /softwaremodules/{id}/artifacts/{filename}     │      │
│  │     分块读取 64KB, 写入 .tmp                           │      │
│  │     打印下载进度百分比                                  │      │
│  │                                                      │      │
│  │  ⑤ SHA256 校验 .tmp 文件                              │      │
│  │     成功 → rename .tmp → 最终文件                      │      │
│  │     失败 → 返回 None                                  │      │
│  │                                                      │      │
│  │  ⑥ 上报 PROCEEDING (每 artifact)                      │      │
│  │     execution=proceeding, progress={cnt:1, of:1}     │      │
│  └──────────────────────┬──────────────────────────────┘      │
│                         │                                     │
│                         ▼                                     │
│  ┌─────────────────────────────────────────────────────┐      │
│  │ Step D: 上报 DOWNLOADED (全部下载完成)                 │      │
│  │                                                      │      │
│  │  POST /deploymentBase/{id}/feedback                  │      │
│  │  {"status": {                                        │      │
│  │    "execution": "downloaded",                        │      │
│  │    "result": {"finished": "none"},                   │      │
│  │    "details": ["Downloaded 1 artifact(s)"]           │      │
│  │  }}                                                  │      │
│  └──────────────────────┬──────────────────────────────┘      │
│                         │                                     │
│                         ▼                                     │
│  ┌─────────────────────────────────────────────────────┐      │
│  │ Step E: 上报 PROCEEDING (开始安装)                      │      │
│  │                                                      │      │
│  │  POST /deploymentBase/{id}/feedback                  │      │
│  │  {"status": {                                        │      │
│  │    "execution": "proceeding",                        │      │
│  │    "result": {"finished": "none"},                   │      │
│  │    "details": ["Installing update …"]                │      │
│  │  }}                                                  │      │
│  └──────────────────────┬──────────────────────────────┘      │
│                         │                                     │
│                         ▼                                     │
│  ┌─────────────────────────────────────────────────────┐      │
│  │ Step F: 执行安装 _install(files, handling)             │      │
│  │                                                      │      │
│  │  默认行为: 检测到 *.zip 文件 → 返回 True               │      │
│  │  生产: 覆盖此方法执行实际刷写                           │      │
│  └──────────────────────┬──────────────────────────────┘      │
│                         │                                     │
│                    ┌────┴────┐                                │
│                    ▼         ▼                                │
│              成功             失败                              │
│                    │         │                                │
│                    ▼         ▼                                │
│  ┌──────────────────────────────┐                             │
│  │ Step G: 上报 CLOSED (最终)     │                             │
│  │                               │                             │
│  │  成功:                         │                             │
│  │  execution=closed              │                             │
│  │  result.finished=success       │                             │
│  │  code=200                      │                             │
│  │                               │                             │
│  │  失败:                         │                             │
│  │  execution=closed              │                             │
│  │  result.finished=failure       │                             │
│  │  code=500                      │                             │
│  └───────────────────────────────┘                            │
│                                                                │
│  → 返回主循环 sleep → 下次轮询                                  │
└────────────────────────────────────────────────────────────────┘

Hawkbit Action Status 映射:
  execution=download      →  Status.DOWNLOAD
  execution=downloaded    →  Status.DOWNLOADED
  execution=proceeding    →  Status.RUNNING
  execution=closed        →  result.success → Status.FINISHED
                          →  result.failure → Status.ERROR
  execution=canceled      →  Status.CANCELED
  execution=rejected      →  Status.WARNING

6. 认证流程

┌───────────────────────────────────────────────────────────┐
│                     认证方式选择                            │
├───────────────────────────────────────────────────────────┤
│                                                            │
│  ① GatewayToken (推荐,所有设备共享)                         │
│  ──────────────────────────────────────                    │
│    服务端配置:                                               │
│      curl -u admin:admin -X PUT \                          │
│        .../system/configs/authentication.gatewaytoken.key \│
│        -d '{"value": "my-shared-key"}'                     │
│                                                            │
│      curl -u admin:admin -X PUT \                          │
│        .../system/configs/authentication.gatewaytoken.enabled \│
│        -d '{"value": true}'                                │
│                                                            │
│    客户端:                                                   │
│      方式 A: 命令行参数                                      │
│        --gateway-token my-shared-key                        │
│                                                            │
│      方式 B: 从文件读取                                      │
│        echo "my-shared-key" > /etc/hawkbit/gateway.key      │
│        chmod 600 /etc/hawkbit/gateway.key                   │
│        --gateway-token @/etc/hawkbit/gateway.key            │
│                                                            │
│    HTTP: Authorization: GatewayToken my-shared-key          │
│                                                            │
│                                                            │
│  ② TargetToken (每设备独立令牌)                              │
│  ────────────────────────────────                          │
│    服务端配置:                                               │
│      curl -u admin:admin -X PUT \                          │
│        .../system/configs/authentication.targettoken.enabled \│
│        -d '{"value": true}'                                │
│                                                            │
│      # 每个 target 的 securityToken 自动生成或手动设置       │
│      curl -u admin:admin -X PUT \                          │
│        .../targets/{controllerId} \                        │
│        -d '{"securityToken": "device-unique-token"}'       │
│                                                            │
│    客户端:                                                   │
│      --target-token device-unique-token                     │
│      或 --target-token @/etc/hawkbit/target.token           │
│                                                            │
│    HTTP: Authorization: TargetToken device-unique-token     │
│                                                            │
│                                                            │
│  ③ SecurityHeader (反向代理模式, 脚本不支持)                  │
│  ──────────────────────────────────────────                │
│    适用于 nginx/Envoy 前置认证场景                            │
│                                                            │
│                                                            │
│  优先级 (客户端):  --gateway-token > --target-token          │
│  无认证:        服务器返回 401                               │
│  验证失败:      服务器返回 401                               │
└───────────────────────────────────────────────────────────┘

7. 取消任务流程

前置条件: 轮询返回 _links.cancelAction

┌───────────────────────────────────────────────────┐
│                  取消任务处理                        │
├───────────────────────────────────────────────────┤
│                                                    │
│  ① GET /cancelAction/{actionId}                    │
│     ← {"id": "11",                                 │
│        "cancelAction": {"stopId": "11"}}           │
│                                                    │
│  ② POST /cancelAction/{actionId}/feedback          │
│     Body:                                           │
│     {"status": {                                    │
│       "execution": "canceled",                      │
│       "result": {"finished": "none"},               │
│       "details": ["Cancel acknowledged"]            │
│     }}                                              │
│                                                    │
│  → 回到主循环 sleep → 下次轮询                       │
└───────────────────────────────────────────────────┘

8. 确认任务流程

前置条件: action 状态为 WAITING_FOR_CONFIRMATION

┌───────────────────────────────────────────────────┐
│                 确认任务处理 (headless)              │
├───────────────────────────────────────────────────┤
│                                                    │
│  Headless 设备自动确认,不等待人工:                   │
│                                                    │
│  POST /confirmationBase/{actionId}/feedback        │
│  Body:                                              │
│  {"confirmation": "confirmed",                      │
│   "code": 200,                                      │
│   "details": ["Auto-confirmed by DDI client"]}      │
│                                                    │
│  → Action 状态从 WAITING_FOR_CONFIRMATION           │
│    转为 RUNNING                                      │
│  → 下次轮询拿到 deploymentBase 链接                  │
│  → 进入正常 OTA 流程                                 │
│                                                    │
│  如果 autoConfirm.active=true                     │
│    下次轮询时服务端直接返回 deploymentBase            │
│    跳过确认步骤                                      │
└───────────────────────────────────────────────────┘

9. 断点续传流程

┌───────────────────────────────────────────────────────────┐
│                     断点续传 (Range 请求)                    │
├───────────────────────────────────────────────────────────┤
│                                                            │
│  触发条件: --no-resume 未设置 (默认启用)                     │
│                                                            │
│  ┌─────────────────────────────────────────────────┐      │
│  │ 下载 {filename} 之前                              │      │
│  │                                                  │      │
│  │ ① 检查 /tmp/hawkbit/{filename}                   │      │
│  │    存在 + SHA256 匹配                             │      │
│  │    → 跳过下载,直接返回本地路径                       │      │
│  │                                                  │      │
│  │ ② 检查 /tmp/hawkbit/{filename}.tmp               │      │
│  │    存在 → os.path.getsize(tmp_path) = 1048576     │      │
│  │    添加头: Range: bytes=1048576-                  │      │
│  │    打开模式: "ab" (追加)                          │      │
│  │                                                  │      │
│  │ ③ 文件不存在                                     │      │
│  │    无 Range 头                                    │      │
│  │    打开模式: "wb" (新建)                          │      │
│  └──────────────────────┬──────────────────────────┘      │
│                         │                                  │
│                         ▼                                  │
│  ┌─────────────────────────────────────────────────┐      │
│  │ GET /softwaremodules/{id}/artifacts/{filename}    │      │
│  │   Range: bytes=1048576-                           │      │
│  │   ←  206 Partial Content                         │      │
│  │      Content-Range: bytes 1048576-195410/195411   │      │
│  │      (服务端从 offset 开始发送)                     │      │
│  └──────────────────────┬──────────────────────────┘      │
│                         │                                  │
│                         ▼                                  │
│  ┌─────────────────────────────────────────────────┐      │
│  │ 分块写入 + 进度报告                                │      │
│  │                                                  │      │
│  │  while chunk = resp.read(64KB):                   │      │
│  │    fh.write(chunk)                                │      │
│  │    downloaded += len(chunk)                       │      │
│  │    pct = downloaded / total * 100                 │      │
│  │                                                  │      │
│  │  中断 (Exception / 网络断):                        │      │
│  │    .tmp 文件保留                                  │      │
│  │    下次启动时自动续传                               │      │
│  └──────────────────────┬──────────────────────────┘      │
│                         │                                  │
│                         ▼                                  │
│  ┌─────────────────────────────────────────────────┐      │
│  │ SHA256 校验通过                                   │      │
│  │   → os.rename(tmp_path, local_path) 原子重命名     │      │
│  │                                                  │      │
│  │ SHA256 校验失败                                   │      │
│  │   → 日志记录 mismatch                             │      │
│  │   → 返回 None                                    │      │
│  │   → 调用方删除 .tmp 或手动恢复                      │      │
│  └─────────────────────────────────────────────────┘      │
│                                                            │
│  禁用续传:                                                  │
│    --no-resume  → 每次都从头下载                             │
│    → 每次 .tmp 从 0 开始                                    │
└───────────────────────────────────────────────────────────┘

10. 错误处理流程

┌───────────────────────────────────────────────────────────────┐
│                         错误处理矩阵                            │
├───────────────┬───────────────────────┬───────────────────────┤
│    场景        │       行为             │       恢复            │
├───────────────┼───────────────────────┼───────────────────────┤
│ 轮询 HTTP 错误 │ 日志记录,退避 sleep      │ 下次轮询自动重试        │
│ (network/500) │  min(interval, 30s)    │                       │
│               │  守护模式继续            │                       │
│               │  --once: exit(1)       │                       │
├───────────────┼───────────────────────┼───────────────────────┤
│ 获取部署详情   │ send_feedback(closed,  │ 下次轮询后 action       │
│ 失败          │   failure, "Cannot     │ 可能还在 pending        │
│               │   fetch deployment")   │ 状态,重新处理           │
│               │ return False           │                       │
├───────────────┼───────────────────────┼───────────────────────┤
│ 下载失败       │ send_feedback(closed,  │ .tmp 保留              │
│ (HTTP error)  │   failure, "Download   │ 下次轮询自动续传         │
│               │   failed for xxx")     │                       │
│               │ return False           │                       │
├───────────────┼───────────────────────┼───────────────────────┤
│ SHA256 不匹配  │ 日志 error             │ .tmp 保留,可手动删除    │
│               │ "SHA256 MISMATCH"      │ 下次重新下载            │
│               │ return None            │                       │
├───────────────┼───────────────────────┼───────────────────────┤
│ 下载中断       │ 日志 error             │ .tmp 保留              │
│ (Exception)   │ "Download interrupted" │ 下次自动续传            │
│               │ return None            │                       │
├───────────────┼───────────────────────┼───────────────────────┤
│ 安装失败       │ send_feedback(closed,  │ 下次轮询后 action       │
│ (_install 返  │   failure, code=500)   │ 状态 → ERROR           │
│  回 False)    │ return False           │ 管理端可见               │
├───────────────┼───────────────────────┼───────────────────────┤
│ SSL 证书错误   │ 捕获后当作普通 HTTP 错误  │ --no-ssl-verify        │
│               │                       │ 跳过证书验证(不安全)     │
├───────────────┼───────────────────────┼───────────────────────┤
│ 属性上报失败    │ 日志 error             │ _attrs_reported=False │
│               │ 不影响轮询继续          │ 下次轮询自动重试         │
├───────────────┼───────────────────────┼───────────────────────┤
│ Ctrl+C 中断   │ 日志 "Stopped"         │ return True             │
│               │ sys.exit(0)           │                       │
└───────────────┴───────────────────────┴───────────────────────┘

日志级别:
  DEBUG  - HTTP 请求/响应详情, 下载进度, 属性内容      (--verbose / -v)
  INFO   - 状态变化, action 处理, 睡眠倒计时           (默认)
  WARNING- 非致命错误 (poll 失败, 缓存失效, 无认证)     (默认)
  ERROR  - 致命错误 (下载失败, SHA256 不匹配, HTTP 错误) (默认)

11. 安装钩子定制

┌───────────────────────────────────────────────────────────┐
│                   _install() 钩子定制                       │
├───────────────────────────────────────────────────────────┤
│                                                            │
│ 默认行为:                                                    │
│   检测下载文件列表中是否有 *.zip 文件 → 返回 True              │
│   (适合测试,不做实际操作)                                     │
│                                                            │
│  输入:                                                      │
│    files:    List[str]  → ["/tmp/hawkbit/update.zip"]      │
│    handling: dict        → {"download":"forced",            │
│                           "update":"forced",                │
│                           "maintenanceWindow":null}         │
│                                                            │
│  返回:                                                      │
│    True  → send_feedback(closed, success)                   │
│    False → send_feedback(closed, failure)                   │
│                                                            │
├───────────────────────────────────────────────────────────┤
│                                                            │
│  方式 A: 子类继承                                            │
│  ─────────────────                                         │
│                                                            │
│  # my_client.py                                             │
│  from ddi_client import HawkbitDDIClient                    │
│                                                            │
│  class AndroidOTAClient(HawkbitDDIClient):                  │
│      def _install(self, files, handling):                  │
│          import subprocess                                  │
│          for fp in files:                                   │
│              if fp.endswith('.zip'):                        │
│                  # 写 recovery 命令                          │
│                  subprocess.run([                            │
│                      'update_engine_client',                │
│                      '--update',                            │
│                      f'file://{fp}'                         │
│                  ], check=True)                             │
│                  # 写版本号                                  │
│                  with open('/etc/hawkbit/current_version',  │
│                           'w') as f:                        │
│                      f.write('V2.0.0.0')                   │
│          return True                                        │
│                                                            │
│  if __name__ == '__main__':                                 │
│      # 用自定义子类替换 CLI 入口                              │
│      client = AndroidOTAClient(...)                         │
│      client.run()                                           │
│                                                            │
│                                                            │
│  方式 B: Monkey-Patch (快速原型)                              │
│  ─────────────────────────────                             │
│                                                            │
│  client = HawkbitDDIClient(...)                             │
│  original_install = client._install                         │
│  def my_install(files, handling):                           │
│      result = subprocess.run(['unzip', '-o', files[0],      │
│                              '-d', '/data/ota'])            │
│      with open('/etc/hawkbit/current_version','w') as f:   │
│          f.write('V2.0.0.0')                               │
│      return result.returncode == 0                          │
│  client._install = my_install                               │
│  client.run()                                               │
│                                                            │
│                                                            │
│  方式 C: 简单模式 (不需要 Python 继承)                         │
│  ───────────────────────────────────                       │
│                                                            │
│  创建安装脚本: /etc/hawkbit/install.sh                       │
│                                                            │
│  #!/bin/sh                                                 │
│  unzip -o "$1" -d /data/ota                                │
│  echo "V2.0.0.0" > /etc/hawkbit/current_version            │
│  exit $?                                                    │
│                                                            │
│  修改 _install:                                              │
│  def _install(self, files, handling):                       │
│      for fp in files:                                       │
│          if fp.endswith('.zip'):                            │
│              import subprocess                              │
│              result = subprocess.run(                        │
│                  ['/etc/hawkbit/install.sh', fp])           │
│              return result.returncode == 0                  │
│      return True                                            │
└───────────────────────────────────────────────────────────┘

12. 部署与运维

┌───────────────────────────────────────────────────────────────┐
│                        部署方案                                │
├───────────────────────────────────────────────────────────────┤
│                                                                │
│  Android 设备部署:                                               │
│  ────────────────                                              │
│                                                                │
│    # 1. 推送脚本                                                │
│    adb push ddi-client.py /data/local/tmp/                     │
│                                                                │
│    # 2. 创建 token 文件                                         │
│    adb shell "echo 'my-gateway-key' > /data/local/tmp/gateway.key" │
│    adb shell "chmod 600 /data/local/tmp/gateway.key"           │
│                                                                │
│    # 3. 创建属性文件 (可选)                                       │
│    adb push device_attrs.json /data/local/tmp/                 │
│    adb shell "mkdir -p /etc/hawkbit"                           │
│    adb shell "cp /data/local/tmp/device_attrs.json /etc/hawkbit/" │
│                                                                │
│    # 4. 运行                                                    │
│    adb shell "python3 /data/local/tmp/ddi-client.py \          │
│        -u https://hawkbit:9090 -t DEFAULT \                    │
│        --gateway-token @/data/local/tmp/gateway.key \          │
│        -d /data/local/tmp/hawkbit"                             │
│                                                                │
│                                                                │
│  Linux 设备部署 (systemd):                                       │
│  ─────────────────────────                                     │
│                                                                │
│    # /opt/hawkbit/ddi-client.py                                │
│    # /etc/hawkbit/gateway.key                                  │
│    # /etc/hawkbit/device_attrs.json                            │
│                                                                │
│    # /etc/systemd/system/hawkbit-ddi.service                   │
│    [Unit]                                                      │
│    Description=Hawkbit DDI Client                              │
│    After=network-online.target                                 │
│    Wants=network-online.target                                 │
│                                                                │
│    [Service]                                                   │
│    Type=simple                                                 │
│    ExecStart=/usr/bin/python3 /opt/hawkbit/ddi-client.py \     │
│        -u https://hawkbit:9090 -t DEFAULT \                    │
│        --gateway-token @/etc/hawkbit/gateway.key \             │
│        -d /var/cache/hawkbit -i 30                             │
│    Restart=always                                              │
│    RestartSec=30                                               │
│    User=root                                                   │
│                                                                │
│    [Install]                                                   │
│    WantedBy=multi-user.target                                  │
│                                                                │
│    systemctl enable --now hawkbit-ddi                          │
│                                                                │
│                                                                │
│  版本管理:                                                       │
│  ────────                                                     │
│                                                                │
│  安装成功后写入版本号:                                            │
│    echo "V2.0.0.0" > /etc/hawkbit/current_version              │
│                                                                │
│  下次轮询时属性中自动包含 swVersion=V2.0.0.0                      │
│  管理员可在 Hawkbit 中按版本筛选设备                               │
└───────────────────────────────────────────────────────────────┘

附录 A: HTTP 请求/响应速查

操作 方法 路径 Content-Type
轮询 GET /{tenant}/controller/v1/{controllerId} application/hal+json
部署详情 GET /{tenant}/controller/v1/{controllerId}/deploymentBase/{actionId} application/hal+json
上报反馈 POST /{tenant}/controller/v1/{controllerId}/deploymentBase/{actionId}/feedback application/json
下载文件 GET /{tenant}/controller/v1/{controllerId}/softwaremodules/{id}/artifacts/{file} application/octet-stream
上报属性 PUT /{tenant}/controller/v1/{controllerId}/configData application/json
取消反馈 POST /{tenant}/controller/v1/{controllerId}/cancelAction/{id}/feedback application/json
确认反馈 POST /{tenant}/controller/v1/{controllerId}/confirmationBase/{id}/feedback application/json

附录 B: 命令行参数完整列表

-u, --base-url           Hawkbit 服务端地址 (必填)
-t, --tenant             租户名称 (必填)
--gateway-token          Gateway Token 或 @文件路径
--target-token           Target Token 或 @文件路径
--controller-id          控制器ID (默认自动检测SN)
-d, --download-dir       下载目录 (/tmp/hawkbit)
-i, --polling-interval   轮询间隔秒数 (60)
--no-verify              跳过 SHA256 校验
--no-resume              禁用断点续传
--no-ssl-verify          跳过 TLS 证书验证
--once                   单次轮询后退出
-v, --verbose            调试日志
-h, --help               帮助

附录 C: 设备属性文件格式

# /etc/hawkbit/device_attrs.json
{
    "dept": "production",
    "region": "east-1",
    "owner": "team-a",
    "rack": "A12",
    "environment": "staging"
}

附录 D: 状态流转图 (Hawkbit 视角)

  assignment
      │
      ▼
  READY ──→ RETRIEVED ──→ DOWNLOAD ──→ DOWNLOADED ──→ RUNNING ──→ FINISHED
               │                          │              │            (成功)
               │                          │              │
               │                          │              └──→ ERROR
               │                          │                    (失败)
               │                          │
               └──────────┬───────────────┘
                          │
                     CANCELING ──→ CANCELED
                          │
                     CANCEL_REJECTED
                     
  WAITING_FOR_CONFIRMATION ──confirmed──→ RUNNING → ...
                          │
                          └──denied──→ DENIED