59 KiB
59 KiB
Hawkbit DDI Client — 运行流程文档
目录
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.142.128:9090 -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.142.128:9090 -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