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

903 lines
59 KiB
Markdown
Raw Permalink 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.
# Hawkbit DDI Client — 运行流程文档
## 目录
1. [整体架构](#1-整体架构)
2. [首次注册流程](#2-首次注册流程)
3. [守护进程轮询流程](#3-守护进程轮询流程)
4. [单次轮询流程](#4-单次轮询流程)
5. [OTA 部署流程](#5-ota-部署流程)
6. [认证流程](#6-认证流程)
7. [取消任务流程](#7-取消任务流程)
8. [确认任务流程](#8-确认任务流程)
9. [断点续传流程](#9-断点续传流程)
10. [错误处理流程](#10-错误处理流程)
11. [安装钩子定制](#11-安装钩子定制)
12. [部署与运维](#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: 设备属性文件格式
```json
# /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
```