903 lines
59 KiB
Markdown
903 lines
59 KiB
Markdown
# 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
|
||
```
|