升级管理
This commit is contained in:
@@ -156,27 +156,28 @@ Open:
|
||||
- `POST /api/v1/upgrades/previews`
|
||||
- `POST /api/v1/upgrades`
|
||||
- `POST /api/v1/upgrades/{taskId}/execute`
|
||||
- `POST /api/v1/upgrades/{taskId}/rollback`
|
||||
- `GET /api/v1/upgrades`
|
||||
- `GET /api/v1/upgrades/{taskId}`
|
||||
- `GET /api/v1/upgrades/{taskId}/log`
|
||||
|
||||
当前支持范围:
|
||||
- 仅支持离线升级
|
||||
- 当前支持升级对象:
|
||||
- 当前支持任务类型:
|
||||
- `TMS`
|
||||
- `STANDARD_APP`
|
||||
- `DIRECT_APP`
|
||||
- `RECEIVER`
|
||||
- `FIRMWARE`
|
||||
- 当前不支持:
|
||||
- 在线升级
|
||||
- `CVE` / 操作系统补丁升级
|
||||
- 密码卡固件升级
|
||||
|
||||
当前流程:
|
||||
- 先上传离线升级包,复用现有文件上传能力,得到 `fileId`
|
||||
- 后端基于 `fileId` 解析升级包并做预检
|
||||
- 预检结果返回升级对象、当前版本、目标版本和版本变化提示
|
||||
- 预检结果返回 `taskType`、`productType`、当前版本、目标版本和版本变化提示
|
||||
- 用户确认后创建升级任务
|
||||
- 调用执行接口后,任务异步进入后台执行
|
||||
- 升级失败后,如升级包提供 `rollback` 入口,可手工调用回滚接口
|
||||
- 前端通过列表、详情和日志接口轮询任务状态
|
||||
|
||||
当前升级包要求:
|
||||
@@ -184,29 +185,41 @@ Open:
|
||||
- 至少包含:
|
||||
- `manifest.json`
|
||||
- `signature.sig`
|
||||
- `payload.zip`
|
||||
- `payload/` 下按需包含:
|
||||
- `app/`
|
||||
- `web/dist/`
|
||||
- `config/`
|
||||
- `sql/`
|
||||
- `firmware/`
|
||||
- `scripts/`
|
||||
- `signature.sig` 需要能被设备密码卡使用“固件完整性密钥”完成验签
|
||||
- 服务端需要配置 `tms.upgrade.signature-public-key-base64`(Base64 编码的公钥 blob)
|
||||
- 服务端需要配置 `tms.upgrade.signature-public-key-pem-path`(公钥 PEM 文件路径)
|
||||
- `manifest.json` 当前要求包含:
|
||||
- `packageId`
|
||||
- `targetComponent`
|
||||
- `targetVersion`
|
||||
- `taskType`
|
||||
- `productType`
|
||||
- `version`
|
||||
- `minCompatibleVersion`
|
||||
- `description`
|
||||
- `orchestrator`
|
||||
- `fileChecksums.payloadZip`
|
||||
- `entrypoints.execute`
|
||||
- 可选 `entrypoints.precheck`
|
||||
- 可选 `entrypoints.verify`
|
||||
- 可选 `entrypoints.rollback`
|
||||
|
||||
当前执行规则:
|
||||
- 同一时刻只允许一个升级任务处于 `RUNNING`
|
||||
- 不允许目标版本低于当前版本
|
||||
- 不满足最小兼容版本时拒绝升级
|
||||
- `execute.sh` 必填,`precheck.sh`、`verify.sh`、`rollback.sh` 可选
|
||||
- 执行顺序为:`precheck -> execute -> verify`
|
||||
- 回滚不自动触发,需要调用回滚接口
|
||||
- 升级成功后会更新 `tms_device_software_version`
|
||||
- 升级执行日志统一写入 `tms.upgrade.log-dir`
|
||||
|
||||
当前配置项:
|
||||
- `tms.upgrade.staging-root-dir`
|
||||
- `tms.upgrade.log-dir`
|
||||
- `tms.upgrade.signature-public-key-base64`
|
||||
- `tms.upgrade.signature-public-key-pem-path`
|
||||
|
||||
升级包规范见:
|
||||
- [2026-03-19-upgrade-package-spec.md](/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-19-upgrade-package-spec.md)
|
||||
|
||||
@@ -9,7 +9,7 @@ tms:
|
||||
upgrade:
|
||||
staging-root-dir: /home/tmp/tms-upgrade-staging
|
||||
log-dir: /home/tms/tmp/tms-upgrade-logs
|
||||
signature-public-key-base64: change-me-base64-public-key-blob
|
||||
signature-public-key-pem-path: /home/tms/config/upgrade-public-key.pem
|
||||
security:
|
||||
internal-token: change-me-internal-token
|
||||
internal-auth:
|
||||
|
||||
@@ -32,6 +32,317 @@ TMS 负责:
|
||||
- 直参版介质解压到厂商约定目录
|
||||
- 数据库凭据和 RabbitMQ 管理员凭据
|
||||
|
||||
## 2.1 核心原则:先部署预置,再执行初始化
|
||||
|
||||
当前 CISD 初始化功能不是完整替代厂商部署手册,而是建立在“基础环境和介质已就绪”的前提上执行客户化步骤。
|
||||
|
||||
因此现场应严格分成两个阶段:
|
||||
|
||||
### 第一阶段:部署/出厂预置阶段
|
||||
|
||||
由运维或出厂人员提前完成,目标是把机器准备成“可接受初始化”的基线环境。
|
||||
|
||||
### 第二阶段:CISD 初始化阶段
|
||||
|
||||
由 TMS 根据前端输入的客户化参数执行,目标是完成机构号、节点地址、MQ/TLQ、数据库导入、应用配置、WEB 发布和启动验收。
|
||||
|
||||
如果第一阶段未完成,第二阶段不应直接试跑。
|
||||
|
||||
## 2.2 部署阶段必须提前完成的步骤
|
||||
|
||||
以下内容必须在部署阶段完成,不应再依赖 `/api/v1/init/**` 初始化接口去补做。
|
||||
|
||||
### 2.2.1 操作系统与基础目录
|
||||
|
||||
需要提前完成:
|
||||
- 系统用户、用户组创建
|
||||
- 标准收发器运行目录创建
|
||||
- 目录权限和属主设置
|
||||
- `/home/cemp4i`、`/home/tms`、`/home/tlq` 等目录存在且可访问
|
||||
|
||||
这类动作属于一次性环境准备,不属于客户化初始化。
|
||||
|
||||
### 2.2.2 基础软件安装
|
||||
|
||||
需要提前完成:
|
||||
- JDK 安装
|
||||
- nginx 安装
|
||||
- RabbitMQ 安装
|
||||
- TLQ 基础软件安装
|
||||
- TiDB/MySQL 客户端可用
|
||||
- `rabbitmqctl`、`rabbitmqadmin`、`mysql`、`unzip` 等命令可用
|
||||
|
||||
初始化阶段默认依赖这些命令和软件已经存在。
|
||||
|
||||
### 2.2.3 标准收发器介质上传与解压
|
||||
|
||||
需要提前完成:
|
||||
- 企业版 / 间参版标准收发器介质解压到 `/home/cemp4i`
|
||||
- 直参版介质解压到厂商约定目录
|
||||
- `cpconfig.cfg`
|
||||
- `cpackage/toolsh/*.sh`
|
||||
- `cpackage/rabq/*.sh`
|
||||
- `cpackage/software/nginx.conf`
|
||||
- `cpackage/front/front.zip`
|
||||
- SQL 模板和 JAR 介质文件存在
|
||||
|
||||
当前初始化流程假定这些文件已经存在,不负责解压标准介质。
|
||||
|
||||
### 2.2.4 平台级凭据预置
|
||||
|
||||
需要提前完成:
|
||||
- 数据库导库账号/密码
|
||||
- RabbitMQ 管理员账号/密码
|
||||
- 其他平台级敏感配置
|
||||
|
||||
这些凭据不属于前端输入的客户化参数,建议通过:
|
||||
- 环境变量
|
||||
- 独立受保护配置文件
|
||||
- 现场预置脚本
|
||||
|
||||
进行管理。
|
||||
|
||||
### 2.2.5 防火墙、服务注册和开机自启
|
||||
|
||||
需要提前完成:
|
||||
- 防火墙端口放通
|
||||
- RabbitMQ/nginx 服务注册
|
||||
- 必要的开机自启设置
|
||||
|
||||
这类动作属于环境级配置,不应放在初始化阶段重复执行。
|
||||
|
||||
### 2.2.6 标准收发器部署文档中应提前做的脚本
|
||||
|
||||
结合标准收发器部署文档和当前 TMS 初始化边界,以下脚本应视为部署阶段脚本,不纳入初始化主流程:
|
||||
|
||||
- `setpd.sh`
|
||||
- `opencport.sh`
|
||||
- `install_newpackage.sh`
|
||||
- `setrabqstart.sh`
|
||||
- `setxsd.sh`
|
||||
- `setngservice.sh`
|
||||
- `pongx.sh`
|
||||
|
||||
这些脚本覆盖的通常是:
|
||||
- 用户与目录准备
|
||||
- 端口开放
|
||||
- RabbitMQ 编译/安装/服务化
|
||||
- nginx 安装/服务化
|
||||
- 信创依赖和基础环境处理
|
||||
|
||||
### 2.2.7 厂商脚本与部署阶段的对应关系
|
||||
|
||||
下表用于帮助现场快速判断:哪些脚本应在部署阶段提前执行,哪些不应再放到初始化接口中执行。
|
||||
|
||||
| 脚本 | 建议阶段 | 说明 |
|
||||
|---|---|---|
|
||||
| `setpd.sh` | 部署阶段 | 用户、目录、权限等基础准备 |
|
||||
| `opencport.sh` | 部署阶段 | 端口开放,属于系统环境配置 |
|
||||
| `install_newpackage.sh` | 部署阶段 | RabbitMQ/OTP 安装与编译 |
|
||||
| `setrabqstart.sh` | 部署阶段 | RabbitMQ 服务启动/服务化准备 |
|
||||
| `setxsd.sh` | 部署阶段 | 信创/环境依赖准备 |
|
||||
| `setngservice.sh` | 部署阶段 | nginx 服务注册/服务化 |
|
||||
| `pongx.sh` | 部署阶段 | 基础环境处理脚本 |
|
||||
| `setup_nodeps.sh` | 部署阶段 | 基础无依赖环境准备 |
|
||||
| `setsuseunixodbc.sh` | 部署阶段 | ODBC/基础运行依赖准备 |
|
||||
|
||||
## 2.3 标准版初始化阶段执行的步骤
|
||||
|
||||
以下内容属于客户化初始化,保留在 `/api/v1/init/**` 流程中执行。
|
||||
|
||||
### 2.3.1 配置渲染
|
||||
|
||||
包括:
|
||||
- `cpconfig.cfg` 渲染
|
||||
- 机构号、节点地址、组织名称等客户化字段写入
|
||||
|
||||
### 2.3.2 license 和输入文件处理
|
||||
|
||||
包括:
|
||||
- 收发器 license 上传
|
||||
- TLQ license / 配置包上传
|
||||
- CFMQ 配置上传
|
||||
- 通过 `fileId` 暂存并按目标路径落位
|
||||
|
||||
### 2.3.3 MQ/TLQ 客户化初始化
|
||||
|
||||
包括:
|
||||
- `SET_RABBITMQ`
|
||||
- `SET_TLQ`
|
||||
- `TLQ_CONF_APPLY`
|
||||
- RabbitMQ 通道账号和队列初始化
|
||||
|
||||
说明:
|
||||
- 标准版 RabbitMQ 用户/队列初始化是客户化动作
|
||||
- 但 RabbitMQ 软件本体安装、服务注册必须提前完成
|
||||
|
||||
### 2.3.4 数据库初始化
|
||||
|
||||
包括:
|
||||
- `DB_RENDER`
|
||||
- `DB_APPLY`
|
||||
|
||||
说明:
|
||||
- 当前 TMS 负责渲染 SQL 和触发导库命令
|
||||
- 但数据库客户端可用、导库凭据可用,必须由部署阶段保证
|
||||
|
||||
### 2.3.5 应用、nginx 和 WEB 客户化
|
||||
|
||||
包括:
|
||||
- `DEPLOY_RECEIVER`
|
||||
- `APP_MQ_CREDENTIALS_APPLY`
|
||||
- `NGINX_CONFIG_APPLY`
|
||||
- `WEB_CONFIG_APPLY`
|
||||
|
||||
说明:
|
||||
- 这些步骤处理的是客户化配置和发布
|
||||
- 不是安装 nginx 或解压标准介质
|
||||
|
||||
### 2.3.6 启动和校验
|
||||
|
||||
包括:
|
||||
- `START_CMSP_CMTP`
|
||||
- `START_NGINX`
|
||||
- `POST_CHECK`
|
||||
- `VERIFY`
|
||||
|
||||
说明:
|
||||
- 当前初始化阶段负责启动和验收
|
||||
- 但对应启动脚本、停止脚本、运行用户和目标路径需要提前准备好
|
||||
|
||||
### 2.3.7 厂商脚本与初始化阶段的对应关系
|
||||
|
||||
下表用于说明:哪些厂商脚本仍在初始化阶段使用,哪些脚本已经被当前 TMS 内置逻辑替代。
|
||||
|
||||
| 脚本/能力 | 当前阶段 | 当前处理方式 |
|
||||
|---|---|---|
|
||||
| `setfraq.sh` | 初始化阶段 | 仍由 TMS 调用,用于切换标准版 `RABBITMQ` 模式 |
|
||||
| `setftq.sh` | 初始化阶段 | 仍由 TMS 调用,用于切换 `TLQ` 或 `RABBITMQ_TLQ` 模式 |
|
||||
| `settlq.sh` | 初始化阶段 | 仍由 TMS 调用,用于落位和替换 TLQ 配置 |
|
||||
| `setuprabq.sh` | 初始化阶段 | 由 TMS staging 后执行,不直接污染源脚本 |
|
||||
| `addrabq_R_01.sh` / `addrabq_R_02.sh` | 初始化阶段 | 由 TMS staging 后执行,按节点选择 |
|
||||
| `setsql.sh` | 初始化阶段 | 已不再作为主导入口;当前改为 `DB_RENDER + DB_APPLY` |
|
||||
| `setsptp.sh` | 初始化阶段 | 仍由 TMS 调用,用于标准收发器应用部署 |
|
||||
| `setweb.sh` | 初始化阶段 | 当前由 TMS 内置 `WEB_CONFIG_APPLY` 替代 |
|
||||
| `setngx.sh` | 初始化阶段 | 当前由 TMS 内置 `NGINX_CONFIG_APPLY` 替代 |
|
||||
|
||||
需要注意:
|
||||
- `setweb.sh` 和 `setngx.sh` 所代表的“WEB 发布”和“nginx 配置发布”仍属于初始化阶段动作
|
||||
- 只是当前代码不再直接执行这两个脚本,而是由 TMS 内置逻辑完成
|
||||
|
||||
## 2.4 分版本预置清单
|
||||
|
||||
本节在通用边界之外,再按版本给出一份现场可直接对照的清单,帮助运维快速判断:
|
||||
- 当前设备属于哪个版本
|
||||
- 初始化前必须先准备好哪些内容
|
||||
- 初始化阶段还会继续执行哪些客户化动作
|
||||
|
||||
### 2.4.1 企业版预置清单
|
||||
|
||||
适用范围:
|
||||
- `ENTERPRISE + RABBITMQ`
|
||||
- `ENTERPRISE + TLQ`
|
||||
|
||||
部署阶段必须完成:
|
||||
- 标准版介质已解压到 `/home/cemp4i`
|
||||
- `/home/cemp4i/cpconfig.cfg`、`/home/cemp4i/cpackage/toolsh/*.sh`、`/home/cemp4i/cpackage/sql_script/*`、`/home/cemp4i/cpackage/front/front.zip` 已存在
|
||||
- JDK、nginx、RabbitMQ、TLQ、数据库客户端已安装
|
||||
- `/home/tms`、`/home/cemp4i`、`/home/tlq` 目录及权限已准备
|
||||
- 数据库导库凭据、RabbitMQ 管理员凭据已预置
|
||||
- `standard-db-apply-command`、`standard-app-start-command`、`standard-nginx-start-command`、`standard-post-check-command` 对应脚本已部署
|
||||
|
||||
按支持方式补充要求:
|
||||
|
||||
| 支持方式 | 部署前额外要求 | 初始化阶段仍会执行 |
|
||||
|---|---|---|
|
||||
| `RABBITMQ` | RabbitMQ 服务可用,`rabbitmqctl`、`rabbitmqadmin` 可执行 | `SET_RABBITMQ`、RabbitMQ 用户和队列初始化、应用 MQ 账号写回 |
|
||||
| `TLQ` | TLQ 基础软件和目录可用,`/home/tlq` 可写 | `SET_TLQ`、`TLQ_CONF_APPLY`、TLQ 配置包落位 |
|
||||
|
||||
企业版初始化阶段不会补做:
|
||||
- RabbitMQ/TLQ 软件安装
|
||||
- nginx 安装和服务注册
|
||||
- 数据库账号创建
|
||||
- 标准介质解压
|
||||
|
||||
### 2.4.2 间参版预置清单
|
||||
|
||||
适用范围:
|
||||
- `INDIRECT + RABBITMQ`
|
||||
- `INDIRECT + RABBITMQ_TLQ`
|
||||
|
||||
部署阶段必须完成:
|
||||
- 标准版介质已解压到 `/home/cemp4i`
|
||||
- 间参版对应的 `setsptp.sh`、`setfraq.sh`、`setftq.sh`、`settlq.sh`、RabbitMQ 脚本、SQL 模板已齐备
|
||||
- JDK、nginx、RabbitMQ、TLQ、数据库客户端已安装
|
||||
- `/home/tms`、`/home/cemp4i`、`/home/tlq` 目录及权限已准备
|
||||
- 数据库导库凭据、RabbitMQ 管理员凭据已预置
|
||||
- TMS 所依赖的标准初始化辅助脚本已部署到 `/home/tms/bin`
|
||||
|
||||
按支持方式补充要求:
|
||||
|
||||
| 支持方式 | 部署前额外要求 | 初始化阶段仍会执行 |
|
||||
|---|---|---|
|
||||
| `RABBITMQ` | RabbitMQ 服务可用 | `SET_RABBITMQ`、RabbitMQ 用户和队列初始化、应用 MQ 账号写回 |
|
||||
| `RABBITMQ_TLQ` | RabbitMQ 和 TLQ 都要提前可用 | `SET_TLQ`、`TLQ_CONF_APPLY`,同时仍会执行 RabbitMQ 用户和队列初始化 |
|
||||
|
||||
需要特别说明:
|
||||
- 间参版不存在“纯 TLQ”初始化模式
|
||||
- `RABBITMQ_TLQ` 不是替代 RabbitMQ,而是 RabbitMQ 与 TLQ 共存
|
||||
- 因此如果现场只准备了 TLQ、没有准备 RabbitMQ,间参版 `RABBITMQ_TLQ` 初始化仍会失败
|
||||
|
||||
### 2.4.3 轻量直参版预置清单
|
||||
|
||||
适用范围:
|
||||
- `DIRECT + RABBITMQ_TLQ`
|
||||
- `DIRECT + RABBITMQ_CFMQ`
|
||||
|
||||
部署阶段必须完成:
|
||||
- 直参版介质已按厂商要求解压到约定目录
|
||||
- 直参版 `cpconfig.cfg` 已在厂商约定位置存在
|
||||
- 直参版数据库脚本目录、TLQ/CFMQ 相关目录、签名服务依赖目录已存在
|
||||
- JDK、RabbitMQ、TLQ 或 CFMQ、数据库客户端已安装
|
||||
- 数据库导库凭据、RabbitMQ 管理员凭据已预置
|
||||
- 直参版初始化依赖的目标路径具备写权限
|
||||
|
||||
按支持方式补充要求:
|
||||
|
||||
| 支持方式 | 部署前额外要求 | 初始化阶段仍会执行 |
|
||||
|---|---|---|
|
||||
| `RABBITMQ_TLQ` | RabbitMQ 与 TLQ 运行环境都已就绪 | `MQ_USER_APPLY`、`DB_INIT`、`TLQ_INIT_CFG` |
|
||||
| `RABBITMQ_CFMQ` | RabbitMQ 与 CFMQ 运行环境都已就绪 | `MQ_USER_APPLY`、`DB_INIT`、`CFMQ_APPLY` |
|
||||
|
||||
直参版与标准版的差异:
|
||||
- 不走标准版 `/home/cemp4i/cpackage/toolsh/*.sh` 主链
|
||||
- 不走标准版 `DB_RENDER + DB_APPLY`
|
||||
- 不走标准版 `WEB_CONFIG_APPLY`、`NGINX_CONFIG_APPLY`
|
||||
- 初始化更依赖直参版厂商目录结构和配置文件落位
|
||||
|
||||
### 2.4.4 现场使用建议
|
||||
|
||||
现场可以按以下顺序判断:
|
||||
1. 先看设备预制版本是 `ENTERPRISE`、`INDIRECT` 还是 `DIRECT`
|
||||
2. 再看该版本下的支持方式是 `RABBITMQ`、`TLQ`、`RABBITMQ_TLQ` 还是 `RABBITMQ_CFMQ`
|
||||
3. 先完成本节对应版本的预置项
|
||||
4. 再进入 `/api/v1/init/template -> /preview -> /tasks -> /execute` 初始化流程
|
||||
|
||||
如果当前版本对应的预置项没有完成,不要直接尝试执行初始化任务。
|
||||
|
||||
## 2.5 不应再由初始化阶段执行的内容
|
||||
|
||||
为了避免把“环境搭建”与“客户化初始化”混在一起,以下动作不应再由初始化阶段重复承担:
|
||||
|
||||
- 创建系统用户和用户组
|
||||
- 批量创建基础目录
|
||||
- 安装或编译 RabbitMQ
|
||||
- 安装或编译 nginx
|
||||
- 开放系统端口
|
||||
- 解压标准收发器完整介质
|
||||
- 创建数据库导库账号
|
||||
- 创建 RabbitMQ 管理员账号
|
||||
- 注册 systemd / rc.local 服务
|
||||
|
||||
这些动作应在出厂或部署阶段完成。
|
||||
|
||||
## 3. 环境要求
|
||||
|
||||
### 3.1 操作系统
|
||||
@@ -391,9 +702,52 @@ PRODUCT_TYPE=ENTERPRISE /Users/waner/Work/CISD/文档/tms-framework/scripts/cisd
|
||||
- 标准版基础文件是否存在
|
||||
- RabbitMQ 工具是否可用
|
||||
- 直参版场景下的脚本目录是否存在
|
||||
- 标准介质与初始化依赖是否已经完成预置
|
||||
|
||||
建议在首次联调前执行一次。
|
||||
|
||||
## 11.1 现场建议的执行顺序
|
||||
|
||||
建议按以下顺序实施:
|
||||
|
||||
### 步骤 1:完成部署阶段预置
|
||||
|
||||
至少确认:
|
||||
- 基础软件已安装
|
||||
- 标准介质已解压
|
||||
- 运行目录和用户已准备
|
||||
- 平台级凭据已准备
|
||||
- `/home/tms` 已部署完毕
|
||||
|
||||
### 步骤 2:执行本机预检查
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
PRODUCT_TYPE=ENTERPRISE /Users/waner/Work/CISD/文档/tms-framework/scripts/cisd_init_precheck.sh
|
||||
```
|
||||
|
||||
或按实际产品类型设置:
|
||||
- `ENTERPRISE`
|
||||
- `INDIRECT`
|
||||
- `DIRECT`
|
||||
|
||||
### 步骤 3:启动 TMS
|
||||
|
||||
执行:
|
||||
|
||||
```bash
|
||||
/home/tms/scripts/tms.sh start
|
||||
```
|
||||
|
||||
### 步骤 4:通过前端或接口执行初始化
|
||||
|
||||
在确认预置无误后,再执行:
|
||||
- 文件上传
|
||||
- 初始化预检
|
||||
- 创建初始化任务
|
||||
- 执行初始化任务
|
||||
|
||||
## 12. 初始化运行期目录与日志
|
||||
|
||||
### 12.1 初始化步骤日志
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
# 标准收发器离线升级包最小样例
|
||||
# RECEIVER 离线升级包最小样例
|
||||
|
||||
本目录提供 `STANDARD_APP` 离线升级包的最小示例骨架,用于对齐打包方、后端和联调流程。
|
||||
本目录保留原有 `standard-app` 路径,便于继续联调,但样例内容已经切换到当前第一版 `RECEIVER` 升级包契约。
|
||||
|
||||
目录结构:
|
||||
|
||||
```text
|
||||
standard-app/
|
||||
├── manifest.json
|
||||
├── build-package.sh
|
||||
└── payload/
|
||||
└── scripts/
|
||||
└── orchestrator.sh
|
||||
├── execute.sh
|
||||
├── precheck.sh
|
||||
├── verify.sh
|
||||
└── rollback.sh
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 本目录中的 `manifest.json` 使用了占位符 `__PAYLOAD_SHA256__`
|
||||
- 真正打包前,需要先对 `payload.zip` 计算 `SHA-256`,再替换该字段
|
||||
- `signature.sig` 不放在仓库示例里,必须由打包方使用“固件完整性密钥”私钥在打包阶段生成
|
||||
- `execute.sh` 必填
|
||||
- `precheck.sh`、`verify.sh`、`rollback.sh` 可选
|
||||
- 本样例里四个脚本都给出,方便联调
|
||||
- `signature.sig` 不放在仓库示例里,必须由打包方在打包阶段生成
|
||||
- `build-package.sh` 会把 `manifest.json`、`signature.sig` 和 `payload/` 打成最终 `upgrade-package.zip`
|
||||
|
||||
推荐打包步骤:
|
||||
|
||||
1. 进入当前示例目录
|
||||
2. 将 `payload/` 压缩为 `payload.zip`
|
||||
3. 计算 `payload.zip` 的 `SHA-256`
|
||||
4. 用实际摘要替换 `manifest.json` 中的 `__PAYLOAD_SHA256__`
|
||||
5. 对 `manifest.json` 原始字节生成 `signature.sig`
|
||||
6. 将 `manifest.json`、`signature.sig`、`payload.zip` 压缩成最终 `upgrade-package.zip`
|
||||
2. 根据实际设备修改 `manifest.json` 中的 `productType`、`version`、`minCompatibleVersion`
|
||||
3. 对 `manifest.json` 原始字节生成真实的 `signature.sig`
|
||||
4. 执行 `./build-package.sh`
|
||||
|
||||
参考命令:
|
||||
|
||||
```bash
|
||||
cd docs/examples/upgrade-package/standard-app
|
||||
|
||||
zip -rq payload.zip payload
|
||||
PAYLOAD_SHA256="$(shasum -a 256 payload.zip | awk '{print $1}')"
|
||||
sed "s/__PAYLOAD_SHA256__/${PAYLOAD_SHA256}/g" manifest.json > manifest.generated.json
|
||||
# 这里替换为正式签名工具,生成 manifest.json 对应的 signature.sig
|
||||
# sign-manifest manifest.json > signature.sig
|
||||
|
||||
# 这里改为公司正式签名工具,生成 manifest.generated.json 对应的 signature.sig
|
||||
# sign-manifest manifest.generated.json > signature.sig
|
||||
|
||||
zip -j upgrade-package.zip manifest.generated.json signature.sig payload.zip
|
||||
chmod +x build-package.sh
|
||||
./build-package.sh
|
||||
```
|
||||
|
||||
联调时建议:
|
||||
|
||||
- 先用这个目录骨架做一个 `STANDARD_APP` 预检样包
|
||||
- `orchestrator.sh` 可以先只输出日志并返回 `0`
|
||||
- 等预检、建任务、执行链路跑通后,再替换成真实升级脚本
|
||||
- 先用这个目录骨架做一个 `RECEIVER` 预检样包
|
||||
- 脚本可以先只输出日志并返回 `0`
|
||||
- 等预检、建任务、执行、回滚链路跑通后,再替换成真实升级脚本
|
||||
- 如果当前环境启用了真实密码卡验签,占位 `signature.sig` 无法通过预检
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OUTPUT_PATH="${1:-${SCRIPT_DIR}/upgrade-package.zip}"
|
||||
SIGNATURE_PATH="${SCRIPT_DIR}/signature.sig"
|
||||
|
||||
if [[ ! -f "${SIGNATURE_PATH}" ]]; then
|
||||
echo "missing signature file: ${SIGNATURE_PATH}" >&2
|
||||
echo "generate a real signature.sig for manifest.json before building the package" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir="$(mktemp -d)"
|
||||
trap 'rm -rf "${tmpdir}"' EXIT
|
||||
|
||||
cp "${SCRIPT_DIR}/manifest.json" "${tmpdir}/manifest.json"
|
||||
cp "${SIGNATURE_PATH}" "${tmpdir}/signature.sig"
|
||||
cp -R "${SCRIPT_DIR}/payload" "${tmpdir}/payload"
|
||||
|
||||
rm -f "${OUTPUT_PATH}"
|
||||
(
|
||||
cd "${tmpdir}"
|
||||
zip -rq "${OUTPUT_PATH}" manifest.json signature.sig payload
|
||||
)
|
||||
|
||||
echo "package built: ${OUTPUT_PATH}"
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"packageFormatVersion": 1,
|
||||
"packageId": "PKG-20260319-STANDARD-001",
|
||||
"targetComponent": "STANDARD_APP",
|
||||
"targetVersion": "V1.3.2",
|
||||
"packageId": "PKG-20260403-RECEIVER-001",
|
||||
"taskType": "RECEIVER",
|
||||
"productType": "DIRECT",
|
||||
"version": "V1.4.0",
|
||||
"minCompatibleVersion": "V1.3.0",
|
||||
"description": "标准收发器企业版离线升级包最小样例",
|
||||
"createdAt": "2026-03-19T16:30:00+08:00",
|
||||
"orchestrator": "scripts/orchestrator.sh",
|
||||
"fileChecksums": {
|
||||
"payloadZip": "__PAYLOAD_SHA256__"
|
||||
"description": "receiver upgrade package",
|
||||
"entrypoints": {
|
||||
"execute": "payload/scripts/execute.sh",
|
||||
"precheck": "payload/scripts/precheck.sh",
|
||||
"verify": "payload/scripts/verify.sh",
|
||||
"rollback": "payload/scripts/rollback.sh"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TASK_ID="${1:-UNKNOWN}"
|
||||
|
||||
echo "[upgrade] task=${TASK_ID} phase=execute message=execute placeholder"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TASK_ID="${1:-UNKNOWN}"
|
||||
|
||||
echo "[upgrade] task=${TASK_ID} phase=backup message=start backup placeholder"
|
||||
echo "[upgrade] task=${TASK_ID} phase=stop-service message=stop service placeholder"
|
||||
echo "[upgrade] task=${TASK_ID} phase=deploy message=deploy placeholder"
|
||||
echo "[upgrade] task=${TASK_ID} phase=health-check message=health check placeholder"
|
||||
echo "[upgrade] task=${TASK_ID} phase=done message=upgrade completed"
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TASK_ID="${1:-UNKNOWN}"
|
||||
|
||||
echo "[upgrade] task=${TASK_ID} phase=precheck message=precheck placeholder"
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TASK_ID="${1:-UNKNOWN}"
|
||||
|
||||
echo "[upgrade] task=${TASK_ID} phase=rollback message=rollback placeholder"
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TASK_ID="${1:-UNKNOWN}"
|
||||
|
||||
echo "[upgrade] task=${TASK_ID} phase=verify message=verify placeholder"
|
||||
@@ -363,12 +363,12 @@ public interface PcieCryptoService {
|
||||
|
||||
//----------------------------------------SDF通用算法运算类接口------------------------------------------
|
||||
/**
|
||||
* 使用通用内部 ECC 密钥执行签名。
|
||||
* 使用通用内部 ECC 密钥执行签名。(用户密钥使用)
|
||||
*/
|
||||
BackupDataResult eccInternalSign(EccInternalSignRequest request);
|
||||
|
||||
/**
|
||||
* 使用外部 ECC 私钥执行签名。
|
||||
* 使用外部 ECC 私钥执行签名。(用户密钥使用)
|
||||
*/
|
||||
BackupDataResult eccExternalSign(EccExternalSignRequest request);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ public class UpgradeProperties {
|
||||
|
||||
private String stagingRootDir = "/home/tmp/tms-upgrade-staging";
|
||||
private String logDir = "/home/tms/tmp/tms-upgrade-logs";
|
||||
private String signaturePublicKeyBase64 = "";
|
||||
private String signaturePublicKeyPemPath = "";
|
||||
|
||||
public String getStagingRootDir() {
|
||||
return stagingRootDir;
|
||||
@@ -25,11 +25,11 @@ public class UpgradeProperties {
|
||||
this.logDir = logDir;
|
||||
}
|
||||
|
||||
public String getSignaturePublicKeyBase64() {
|
||||
return signaturePublicKeyBase64;
|
||||
public String getSignaturePublicKeyPemPath() {
|
||||
return signaturePublicKeyPemPath;
|
||||
}
|
||||
|
||||
public void setSignaturePublicKeyBase64(String signaturePublicKeyBase64) {
|
||||
this.signaturePublicKeyBase64 = signaturePublicKeyBase64;
|
||||
public void setSignaturePublicKeyPemPath(String signaturePublicKeyPemPath) {
|
||||
this.signaturePublicKeyPemPath = signaturePublicKeyPemPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +67,24 @@ public class UpgradeController {
|
||||
return ApiResponse.success(upgradeService.executeTask(taskId));
|
||||
}
|
||||
|
||||
@PostMapping("/upgrades/{taskId}/rollback")
|
||||
@Operation(summary = "回滚离线升级任务", description = "异步受理升级回滚请求,立即返回任务当前快照。")
|
||||
public ApiResponse<UpgradeTaskResponse> rollback(
|
||||
@Parameter(description = "升级任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(upgradeService.rollbackTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/upgrades")
|
||||
@Operation(summary = "查询升级记录列表", description = "分页查询升级任务列表,支持按升级对象和状态筛选。")
|
||||
public ApiResponse<UpgradeTaskPageResponse> list(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String targetComponent,
|
||||
@RequestParam(required = false) String taskType,
|
||||
@RequestParam(required = false) String status
|
||||
) {
|
||||
return ApiResponse.success(upgradeService.listTasks(page, pageSize, targetComponent, status));
|
||||
return ApiResponse.success(upgradeService.listTasks(page, pageSize, taskType, status));
|
||||
}
|
||||
|
||||
@GetMapping("/upgrades/{taskId}")
|
||||
|
||||
@@ -9,8 +9,10 @@ public class UpgradeCreateTaskResponse {
|
||||
private String taskId;
|
||||
@Schema(description = "升级任务状态", example = "PENDING_CONFIRM")
|
||||
private String status;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "当前版本", example = "V1.3.1")
|
||||
private String currentVersion;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
@@ -32,12 +34,20 @@ public class UpgradeCreateTaskResponse {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
|
||||
@@ -7,18 +7,18 @@ public class UpgradeManifest {
|
||||
|
||||
@Schema(description = "升级包唯一标识", example = "PKG_20260319_001")
|
||||
private String packageId;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
private String targetVersion;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "升级版本", example = "V1.3.2")
|
||||
private String version;
|
||||
@Schema(description = "兼容的最低旧版本", example = "V1.3.0")
|
||||
private String minCompatibleVersion;
|
||||
@Schema(description = "升级包描述", example = "修复标准收发器已知问题")
|
||||
private String description;
|
||||
@Schema(description = "升级执行脚本路径", example = "scripts/orchestrator.sh")
|
||||
private String orchestrator;
|
||||
@Schema(description = "升级包校验信息")
|
||||
private FileChecksums fileChecksums = new FileChecksums();
|
||||
@Schema(description = "升级脚本入口")
|
||||
private Entrypoints entrypoints = new Entrypoints();
|
||||
|
||||
public String getPackageId() {
|
||||
return packageId;
|
||||
@@ -28,20 +28,28 @@ public class UpgradeManifest {
|
||||
this.packageId = packageId;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getTargetVersion() {
|
||||
return targetVersion;
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setTargetVersion(String targetVersion) {
|
||||
this.targetVersion = targetVersion;
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getMinCompatibleVersion() {
|
||||
@@ -60,31 +68,50 @@ public class UpgradeManifest {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getOrchestrator() {
|
||||
return orchestrator;
|
||||
public Entrypoints getEntrypoints() {
|
||||
return entrypoints;
|
||||
}
|
||||
|
||||
public void setOrchestrator(String orchestrator) {
|
||||
this.orchestrator = orchestrator;
|
||||
public void setEntrypoints(Entrypoints entrypoints) {
|
||||
this.entrypoints = entrypoints;
|
||||
}
|
||||
|
||||
public FileChecksums getFileChecksums() {
|
||||
return fileChecksums;
|
||||
}
|
||||
public static class Entrypoints {
|
||||
private String execute;
|
||||
private String precheck;
|
||||
private String verify;
|
||||
private String rollback;
|
||||
|
||||
public void setFileChecksums(FileChecksums fileChecksums) {
|
||||
this.fileChecksums = fileChecksums;
|
||||
}
|
||||
|
||||
public static class FileChecksums {
|
||||
private String payloadZip;
|
||||
|
||||
public String getPayloadZip() {
|
||||
return payloadZip;
|
||||
public String getExecute() {
|
||||
return execute;
|
||||
}
|
||||
|
||||
public void setPayloadZip(String payloadZip) {
|
||||
this.payloadZip = payloadZip;
|
||||
public void setExecute(String execute) {
|
||||
this.execute = execute;
|
||||
}
|
||||
|
||||
public String getPrecheck() {
|
||||
return precheck;
|
||||
}
|
||||
|
||||
public void setPrecheck(String precheck) {
|
||||
this.precheck = precheck;
|
||||
}
|
||||
|
||||
public String getVerify() {
|
||||
return verify;
|
||||
}
|
||||
|
||||
public void setVerify(String verify) {
|
||||
this.verify = verify;
|
||||
}
|
||||
|
||||
public String getRollback() {
|
||||
return rollback;
|
||||
}
|
||||
|
||||
public void setRollback(String rollback) {
|
||||
this.rollback = rollback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@Schema(description = "离线升级包预检响应")
|
||||
public class UpgradePreviewResponse {
|
||||
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "当前版本", example = "V1.3.1")
|
||||
private String currentVersion;
|
||||
@Schema(description = "目标版本", example = "V1.3.2")
|
||||
@@ -24,12 +26,20 @@ public class UpgradePreviewResponse {
|
||||
@Schema(description = "升级包说明", example = "修复标准收发器已知问题")
|
||||
private String description;
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
|
||||
@@ -9,8 +9,10 @@ public class UpgradeTaskResponse {
|
||||
private String taskId;
|
||||
@Schema(description = "升级方式", example = "OFFLINE")
|
||||
private String upgradeMode;
|
||||
@Schema(description = "升级对象", example = "STANDARD_APP")
|
||||
private String targetComponent;
|
||||
@Schema(description = "升级任务类型", example = "RECEIVER")
|
||||
private String taskType;
|
||||
@Schema(description = "设备产品类型", example = "DIRECT")
|
||||
private String productType;
|
||||
@Schema(description = "升级包文件标识", example = "FILE-20260319-000001")
|
||||
private String packageFileId;
|
||||
@Schema(description = "升级包名称", example = "upgrade-package.zip")
|
||||
@@ -48,12 +50,20 @@ public class UpgradeTaskResponse {
|
||||
this.upgradeMode = upgradeMode;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
|
||||
@@ -9,7 +9,8 @@ public class UpgradeTaskEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String upgradeMode;
|
||||
private String targetComponent;
|
||||
private String taskType;
|
||||
private String productType;
|
||||
private String packageFileId;
|
||||
private String packageName;
|
||||
private String packageHash;
|
||||
@@ -41,12 +42,20 @@ public class UpgradeTaskEntity extends BaseEntity {
|
||||
this.upgradeMode = upgradeMode;
|
||||
}
|
||||
|
||||
public String getTargetComponent() {
|
||||
return targetComponent;
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTargetComponent(String targetComponent) {
|
||||
this.targetComponent = targetComponent;
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public String getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
|
||||
@@ -2,6 +2,6 @@ package com.cisd.tms.modules.upgrade.enums;
|
||||
|
||||
public enum UpgradeTargetComponent {
|
||||
TMS,
|
||||
STANDARD_APP,
|
||||
DIRECT_APP
|
||||
RECEIVER,
|
||||
FIRMWARE
|
||||
}
|
||||
|
||||
@@ -4,5 +4,8 @@ public enum UpgradeTaskStatus {
|
||||
PENDING_CONFIRM,
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
FAILED,
|
||||
ROLLING_BACK,
|
||||
ROLLBACK_SUCCESS,
|
||||
ROLLBACK_FAILED
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.lang.ProcessBuilder.Redirect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -55,23 +56,40 @@ public class UpgradeTaskRunner {
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
|
||||
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId(), Path.of(record.getStoragePath()));
|
||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
||||
Path payloadDir = upgradePackageStagingService.stage(task.getTaskId() + "-payload", stagedDir.resolve("payload.zip"));
|
||||
Path orchestrator = payloadDir.resolve(manifest.getOrchestrator()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(orchestrator)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade orchestrator not found");
|
||||
}
|
||||
Path root = stagedDir.normalize().toAbsolutePath();
|
||||
|
||||
Process process = new ProcessBuilder("bash", orchestrator.toString(), task.getTaskId())
|
||||
.directory(payloadDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(logPath.toFile())
|
||||
.start();
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
int precheckCode = runOptionalScript("precheck", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getPrecheck(), task, logPath);
|
||||
if (precheckCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"orchestrator exited with " + exitCode,
|
||||
"precheck exited with " + precheckCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int executeCode = runRequiredScript("execute", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getExecute(), task, logPath);
|
||||
if (executeCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"execute exited with " + executeCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
int verifyCode = runOptionalScript("verify", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getVerify(), task, logPath);
|
||||
if (verifyCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"FAILED",
|
||||
"verify exited with " + verifyCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
@@ -80,8 +98,8 @@ public class UpgradeTaskRunner {
|
||||
}
|
||||
|
||||
DeviceSoftwareVersionEntity versionEntity = new DeviceSoftwareVersionEntity();
|
||||
versionEntity.setComponentCode(task.getTargetComponent());
|
||||
versionEntity.setComponentName(componentName(task.getTargetComponent()));
|
||||
versionEntity.setComponentCode(task.getTaskType());
|
||||
versionEntity.setComponentName(componentName(task.getTaskType()));
|
||||
versionEntity.setCurrentVersion(task.getTargetVersion());
|
||||
versionEntity.setSourceType("UPGRADE");
|
||||
versionEntity.setDetectedAt(LocalDateTime.now());
|
||||
@@ -113,15 +131,88 @@ public class UpgradeTaskRunner {
|
||||
}
|
||||
}
|
||||
|
||||
public void rollback(UpgradeTaskEntity task) {
|
||||
LocalDateTime startedAt = LocalDateTime.now();
|
||||
Path logPath = logPath(task.getTaskId() + "-rollback");
|
||||
try {
|
||||
Files.createDirectories(logPath.getParent());
|
||||
Files.writeString(logPath, "");
|
||||
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(task.getPackageFileId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
|
||||
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId() + "-rollback", Path.of(record.getStoragePath()));
|
||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
|
||||
String rollbackPath = manifest.getEntrypoints() == null ? "" : trim(manifest.getEntrypoints().getRollback());
|
||||
if (rollbackPath.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
|
||||
}
|
||||
int exitCode = runRequiredScript("rollback", stagedDir.normalize().toAbsolutePath(), rollbackPath, task, logPath);
|
||||
if (exitCode != 0) {
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_FAILED",
|
||||
"rollback exited with " + exitCode,
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_SUCCESS",
|
||||
"",
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
try {
|
||||
Files.createDirectories(logPath.getParent());
|
||||
Files.writeString(logPath, ex.getMessage() == null ? "upgrade rollback failed" : ex.getMessage());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
upgradeTaskRepository.updateStatus(
|
||||
task.getTaskId(),
|
||||
"ROLLBACK_FAILED",
|
||||
ex.getMessage(),
|
||||
logPath.toString(),
|
||||
startedAt,
|
||||
LocalDateTime.now()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Path logPath(String taskId) {
|
||||
return Path.of(trim(upgradeProperties.getLogDir())).normalize().toAbsolutePath().resolve(taskId + ".log");
|
||||
}
|
||||
|
||||
private int runOptionalScript(String phase, Path stagedDir, String scriptPath, UpgradeTaskEntity task, Path logPath) throws IOException, InterruptedException {
|
||||
if (trim(scriptPath).isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return runRequiredScript(phase, stagedDir, scriptPath, task, logPath);
|
||||
}
|
||||
|
||||
private int runRequiredScript(String phase, Path stagedDir, String scriptPath, UpgradeTaskEntity task, Path logPath) throws IOException, InterruptedException {
|
||||
Path resolved = stagedDir.resolve(trim(scriptPath)).normalize().toAbsolutePath();
|
||||
if (!resolved.startsWith(stagedDir) || !Files.exists(resolved)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade " + phase + " script not found");
|
||||
}
|
||||
Process process = new ProcessBuilder("bash", resolved.toString(), task.getTaskId())
|
||||
.directory(stagedDir.toFile())
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(Redirect.appendTo(logPath.toFile()))
|
||||
.start();
|
||||
return process.waitFor();
|
||||
}
|
||||
|
||||
private String componentName(String componentCode) {
|
||||
return switch (trim(componentCode)) {
|
||||
case "TMS" -> "设备管理软件";
|
||||
case "STANDARD_APP" -> "标准收发器";
|
||||
case "DIRECT_APP" -> "直参轻量化终端";
|
||||
case "RECEIVER" -> "标准收发器";
|
||||
case "FIRMWARE" -> "固件";
|
||||
default -> componentCode;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ public interface UpgradeTaskMapper extends BaseMapperX<UpgradeTaskEntity> {
|
||||
UpgradeTaskEntity selectRunningTask();
|
||||
|
||||
List<UpgradeTaskEntity> selectPage(
|
||||
@Param("targetComponent") String targetComponent,
|
||||
@Param("taskType") String taskType,
|
||||
@Param("status") String status,
|
||||
@Param("offset") int offset,
|
||||
@Param("limit") int limit
|
||||
);
|
||||
|
||||
long countPage(
|
||||
@Param("targetComponent") String targetComponent,
|
||||
@Param("taskType") String taskType,
|
||||
@Param("status") String status
|
||||
);
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ public interface UpgradeTaskRepository {
|
||||
|
||||
Optional<UpgradeTaskEntity> findRunningTask();
|
||||
|
||||
List<UpgradeTaskEntity> findPage(String targetComponent, String status, int offset, int limit);
|
||||
List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit);
|
||||
|
||||
long countPage(String targetComponent, String status);
|
||||
long countPage(String taskType, String status);
|
||||
|
||||
void updateStatus(
|
||||
String taskId,
|
||||
|
||||
+4
-4
@@ -33,13 +33,13 @@ public class UpgradeTaskRepositoryImpl implements UpgradeTaskRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UpgradeTaskEntity> findPage(String targetComponent, String status, int offset, int limit) {
|
||||
return upgradeTaskMapper.selectPage(targetComponent, status, offset, limit);
|
||||
public List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit) {
|
||||
return upgradeTaskMapper.selectPage(taskType, status, offset, limit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countPage(String targetComponent, String status) {
|
||||
return upgradeTaskMapper.countPage(targetComponent, status);
|
||||
public long countPage(String taskType, String status) {
|
||||
return upgradeTaskMapper.countPage(taskType, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.cisd.tms.modules.upgrade.service;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
@@ -13,12 +14,8 @@ import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -30,6 +27,7 @@ public class UpgradePackageService {
|
||||
private final UpgradePackageStagingService upgradePackageStagingService;
|
||||
private final UpgradePackageSignatureVerifier signatureVerifier;
|
||||
private final UpgradeVersionComparator versionComparator;
|
||||
private final CisdPresetProperties cisdPresetProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public UpgradePackageService(
|
||||
@@ -38,6 +36,7 @@ public class UpgradePackageService {
|
||||
UpgradePackageStagingService upgradePackageStagingService,
|
||||
UpgradePackageSignatureVerifier signatureVerifier,
|
||||
UpgradeVersionComparator versionComparator,
|
||||
CisdPresetProperties cisdPresetProperties,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.fileRecordRepository = fileRecordRepository;
|
||||
@@ -45,32 +44,20 @@ public class UpgradePackageService {
|
||||
this.upgradePackageStagingService = upgradePackageStagingService;
|
||||
this.signatureVerifier = signatureVerifier;
|
||||
this.versionComparator = versionComparator;
|
||||
this.cisdPresetProperties = cisdPresetProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public UpgradePreviewResponse preview(String fileId) {
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(normalize(fileId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + normalize(fileId)));
|
||||
Path packagePath = Path.of(record.getStoragePath()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(packagePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk");
|
||||
}
|
||||
StagedUpgradePackage stagedPackage = stagePackage(fileId);
|
||||
UpgradeManifest manifest = stagedPackage.manifest();
|
||||
validateManifest(manifest);
|
||||
validateProductType(manifest.getProductType());
|
||||
signatureVerifier.verify(manifest, stagedPackage.manifestPath(), stagedPackage.signaturePath());
|
||||
validateExecuteScript(stagedPackage.stagedDir(), manifest);
|
||||
|
||||
Path stagedDir = upgradePackageStagingService.stage(record.getFileId(), packagePath);
|
||||
Path manifestPath = stagedDir.resolve("manifest.json");
|
||||
Path payloadZipPath = stagedDir.resolve("payload.zip");
|
||||
Path signaturePath = stagedDir.resolve("signature.sig");
|
||||
|
||||
if (!Files.exists(manifestPath) || !Files.exists(payloadZipPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package structure invalid");
|
||||
}
|
||||
|
||||
UpgradeManifest manifest = readManifest(manifestPath);
|
||||
verifyPayloadChecksum(manifest, payloadZipPath);
|
||||
signatureVerifier.verify(manifest, manifestPath, payloadZipPath, signaturePath);
|
||||
|
||||
String currentVersion = currentVersion(manifest.getTargetComponent());
|
||||
String targetVersion = normalize(manifest.getTargetVersion());
|
||||
String currentVersion = currentVersion(manifest.getTaskType());
|
||||
String targetVersion = normalize(manifest.getVersion());
|
||||
|
||||
if (!currentVersion.isEmpty() && versionComparator.compare(targetVersion, currentVersion) < 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "target version is lower than current version");
|
||||
@@ -81,10 +68,11 @@ public class UpgradePackageService {
|
||||
}
|
||||
|
||||
UpgradePreviewResponse response = new UpgradePreviewResponse();
|
||||
response.setTargetComponent(normalize(manifest.getTargetComponent()));
|
||||
response.setTaskType(normalize(manifest.getTaskType()));
|
||||
response.setProductType(normalize(manifest.getProductType()));
|
||||
response.setCurrentVersion(currentVersion);
|
||||
response.setTargetVersion(targetVersion);
|
||||
response.setVersionChangeSummary(buildVersionSummary(manifest.getTargetComponent(), currentVersion, targetVersion));
|
||||
response.setVersionChangeSummary(buildVersionSummary(currentVersion, targetVersion));
|
||||
response.setVerifyPassed(true);
|
||||
response.setVerifyMessage("校验通过");
|
||||
response.setAllowed(true);
|
||||
@@ -93,6 +81,10 @@ public class UpgradePackageService {
|
||||
return response;
|
||||
}
|
||||
|
||||
public UpgradeManifest loadManifest(String fileId) {
|
||||
return stagePackage(fileId).manifest();
|
||||
}
|
||||
|
||||
private UpgradeManifest readManifest(Path manifestPath) {
|
||||
try {
|
||||
return objectMapper.readValue(Files.readString(manifestPath), UpgradeManifest.class);
|
||||
@@ -101,30 +93,37 @@ public class UpgradePackageService {
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyPayloadChecksum(UpgradeManifest manifest, Path payloadZipPath) {
|
||||
String expected = normalize(manifest.getFileChecksums() == null ? "" : manifest.getFileChecksums().getPayloadZip());
|
||||
if (expected.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "payload checksum missing");
|
||||
private StagedUpgradePackage stagePackage(String fileId) {
|
||||
FileRecordEntity record = fileRecordRepository.findByFileId(normalize(fileId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + normalize(fileId)));
|
||||
Path packagePath = Path.of(record.getStoragePath()).normalize().toAbsolutePath();
|
||||
if (!Files.exists(packagePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk");
|
||||
}
|
||||
String actual = sha256Hex(payloadZipPath);
|
||||
if (!expected.equalsIgnoreCase(actual)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "payload checksum mismatch");
|
||||
|
||||
Path stagedDir = upgradePackageStagingService.stage(record.getFileId(), packagePath);
|
||||
Path manifestPath = stagedDir.resolve("manifest.json");
|
||||
Path signaturePath = stagedDir.resolve("signature.sig");
|
||||
if (!Files.exists(manifestPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package structure invalid");
|
||||
}
|
||||
return new StagedUpgradePackage(stagedDir, manifestPath, signaturePath, readManifest(manifestPath));
|
||||
}
|
||||
|
||||
private String sha256Hex(Path file) {
|
||||
try (InputStream inputStream = Files.newInputStream(file)) {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) >= 0) {
|
||||
if (read > 0) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (IOException | NoSuchAlgorithmException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to calculate payload checksum");
|
||||
private void validateManifest(UpgradeManifest manifest) {
|
||||
if (manifest == null) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
|
||||
}
|
||||
if (normalize(manifest.getPackageId()).isEmpty()
|
||||
|| normalize(manifest.getTaskType()).isEmpty()
|
||||
|| normalize(manifest.getProductType()).isEmpty()
|
||||
|| normalize(manifest.getVersion()).isEmpty()
|
||||
|| normalize(manifest.getMinCompatibleVersion()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
|
||||
}
|
||||
UpgradeManifest.Entrypoints entrypoints = manifest.getEntrypoints();
|
||||
if (entrypoints == null || normalize(entrypoints.getExecute()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade execute script not found");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,19 +132,40 @@ public class UpgradePackageService {
|
||||
return cached.map(DeviceSoftwareVersionEntity::getCurrentVersion).map(this::normalize).orElse("");
|
||||
}
|
||||
|
||||
private String buildVersionSummary(String componentCode, String currentVersion, String targetVersion) {
|
||||
String label = switch (normalize(componentCode)) {
|
||||
case "TMS" -> "设备管理软件";
|
||||
case "STANDARD_APP" -> "标准收发器";
|
||||
case "DIRECT_APP" -> "直参轻量化终端";
|
||||
default -> "目标组件";
|
||||
};
|
||||
private void validateProductType(String packageProductType) {
|
||||
String currentProductType = normalizeUpper(cisdPresetProperties.getProductType());
|
||||
String packageType = normalizeUpper(packageProductType);
|
||||
if (!currentProductType.isEmpty() && !packageType.equals(currentProductType)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package product type does not match current device");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateExecuteScript(Path stagedDir, UpgradeManifest manifest) {
|
||||
Path executePath = stagedDir.resolve(normalize(manifest.getEntrypoints().getExecute())).normalize().toAbsolutePath();
|
||||
if (!executePath.startsWith(stagedDir.normalize().toAbsolutePath()) || !Files.exists(executePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade execute script not found");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildVersionSummary(String currentVersion, String targetVersion) {
|
||||
String from = currentVersion.isEmpty() ? "未记录版本" : currentVersion;
|
||||
String to = targetVersion.isEmpty() ? "未知目标版本" : targetVersion;
|
||||
return label + "将从 " + from + " 升级到 " + to;
|
||||
return "当前设备将从 " + from + " 升级到 " + to;
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private String normalizeUpper(String value) {
|
||||
return normalize(value).toUpperCase();
|
||||
}
|
||||
|
||||
private record StagedUpgradePackage(
|
||||
Path stagedDir,
|
||||
Path manifestPath,
|
||||
Path signaturePath,
|
||||
UpgradeManifest manifest
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeLogResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse;
|
||||
@@ -102,7 +103,8 @@ public class UpgradeService {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(buildTaskId());
|
||||
entity.setUpgradeMode("OFFLINE");
|
||||
entity.setTargetComponent(preview.getTargetComponent());
|
||||
entity.setTaskType(preview.getTaskType());
|
||||
entity.setProductType(preview.getProductType());
|
||||
entity.setPackageFileId(request.getFileId().trim());
|
||||
entity.setPackageName(fileRecord.getOriginalFilename());
|
||||
entity.setPackageHash("");
|
||||
@@ -120,7 +122,8 @@ public class UpgradeService {
|
||||
UpgradeCreateTaskResponse response = new UpgradeCreateTaskResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setTargetComponent(entity.getTargetComponent());
|
||||
response.setTaskType(entity.getTaskType());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setCurrentVersion(entity.getCurrentVersion());
|
||||
response.setTargetVersion(entity.getTargetVersion());
|
||||
return response;
|
||||
@@ -150,12 +153,37 @@ public class UpgradeService {
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
|
||||
public UpgradeTaskPageResponse listTasks(int page, int pageSize, String targetComponent, String status) {
|
||||
public UpgradeTaskResponse rollbackTask(String taskId) {
|
||||
UpgradeTaskEntity task = loadTask(taskId);
|
||||
UpgradeManifest manifest = upgradePackageService.loadManifest(task.getPackageFileId());
|
||||
if (manifest.getEntrypoints() == null || trim(manifest.getEntrypoints().getRollback()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
|
||||
}
|
||||
UpgradeTaskEntity runningTask = upgradeTaskRepository.findRunningTask().orElse(null);
|
||||
if (runningTask != null && !runningTask.getTaskId().equals(taskId)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "another upgrade task is running");
|
||||
}
|
||||
if (activeExecutions.containsKey(taskId)) {
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
upgradeTaskRepository.updateStatus(taskId, "ROLLING_BACK", "", task.getDetailLogPath(), LocalDateTime.now(), null);
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
upgradeTaskRunner.rollback(loadTask(taskId));
|
||||
} finally {
|
||||
activeExecutions.remove(taskId);
|
||||
}
|
||||
}, backgroundExecutor);
|
||||
activeExecutions.put(taskId, future);
|
||||
return toResponse(loadTask(taskId));
|
||||
}
|
||||
|
||||
public UpgradeTaskPageResponse listTasks(int page, int pageSize, String taskType, String status) {
|
||||
int normalizedPage = Math.max(1, page);
|
||||
int normalizedPageSize = Math.max(1, pageSize);
|
||||
int offset = (normalizedPage - 1) * normalizedPageSize;
|
||||
List<UpgradeTaskEntity> entities = upgradeTaskRepository.findPage(trim(targetComponent), trim(status), offset, normalizedPageSize);
|
||||
long total = upgradeTaskRepository.countPage(trim(targetComponent), trim(status));
|
||||
List<UpgradeTaskEntity> entities = upgradeTaskRepository.findPage(trim(taskType), trim(status), offset, normalizedPageSize);
|
||||
long total = upgradeTaskRepository.countPage(trim(taskType), trim(status));
|
||||
|
||||
UpgradeTaskPageResponse response = new UpgradeTaskPageResponse();
|
||||
response.setPage(normalizedPage);
|
||||
@@ -203,7 +231,8 @@ public class UpgradeService {
|
||||
UpgradeTaskResponse response = new UpgradeTaskResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setUpgradeMode(entity.getUpgradeMode());
|
||||
response.setTargetComponent(entity.getTargetComponent());
|
||||
response.setTaskType(entity.getTaskType());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setPackageFileId(entity.getPackageFileId());
|
||||
response.setPackageName(entity.getPackageName());
|
||||
response.setCurrentVersion(entity.getCurrentVersion());
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import java.nio.file.Path;
|
||||
public class NoopUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PcieUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
private final UpgradeProperties upgradeProperties;
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
|
||||
public PcieUpgradePackageSignatureVerifier(UpgradeProperties upgradeProperties, PcieCryptoService pcieCryptoService) {
|
||||
this.upgradeProperties = upgradeProperties;
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
byte[] publicKeyBlob = decodeConfiguredPublicKey();
|
||||
byte[] signature = loadSignature(signaturePath);
|
||||
byte[] manifestBytes = readBytes(manifestPath, "failed to read manifest for signature verification");
|
||||
|
||||
EccExternalVerifyRequest request = new EccExternalVerifyRequest();
|
||||
request.setAlgId(Gm0018AlgorithmIds.SM2_SIGN);
|
||||
request.setPublicKeyBlob(publicKeyBlob);
|
||||
request.setData(manifestBytes);
|
||||
request.setSignature(signature);
|
||||
|
||||
try {
|
||||
pcieCryptoService.eccExternalVerify(request);
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (RuntimeException ex) {
|
||||
throw new BizException(ErrorCode.CRYPTO_CARD_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeConfiguredPublicKey() {
|
||||
String configured = normalize(upgradeProperties.getSignaturePublicKeyBase64());
|
||||
if (configured.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key not configured");
|
||||
}
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(configured);
|
||||
if (decoded.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key is invalid");
|
||||
}
|
||||
return decoded;
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] loadSignature(Path signaturePath) {
|
||||
byte[] raw = readBytes(signaturePath, "failed to read upgrade signature file");
|
||||
if (raw.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file is empty");
|
||||
}
|
||||
|
||||
String text = new String(raw, StandardCharsets.UTF_8).trim();
|
||||
if (!text.isEmpty()) {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(text);
|
||||
if (decoded.length > 0) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Keep backward compatibility with raw binary signature files.
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
private byte[] readBytes(Path path, String failureMessage) {
|
||||
try {
|
||||
return Files.readAllBytes(path);
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), failureMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.util.Base64;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private static final String SIGNATURE_ALGORITHM = "SM3withSM2";
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
private final UpgradeProperties upgradeProperties;
|
||||
|
||||
public SoftUpgradePackageSignatureVerifier(UpgradeProperties upgradeProperties) {
|
||||
this.upgradeProperties = upgradeProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
|
||||
if (!Files.exists(signaturePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
|
||||
}
|
||||
|
||||
PublicKey publicKey = loadPublicKey();
|
||||
byte[] manifestBytes = readBytes(manifestPath, "failed to read manifest for signature verification");
|
||||
byte[] signatureBytes = loadSignature(signaturePath);
|
||||
|
||||
try {
|
||||
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER);
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(manifestBytes);
|
||||
if (!signature.verify(signatureBytes)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private PublicKey loadPublicKey() {
|
||||
Path pemPath = Path.of(normalize(upgradeProperties.getSignaturePublicKeyPemPath())).normalize().toAbsolutePath();
|
||||
if (normalize(upgradeProperties.getSignaturePublicKeyPemPath()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem path not configured");
|
||||
}
|
||||
if (!Files.exists(pemPath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem file not found");
|
||||
}
|
||||
try {
|
||||
String pemContent = Files.readString(pemPath, StandardCharsets.UTF_8);
|
||||
try (Reader reader = new StringReader(pemContent); PEMParser parser = new PEMParser(reader)) {
|
||||
Object parsed = parser.readObject();
|
||||
if (!(parsed instanceof SubjectPublicKeyInfo publicKeyInfo)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem is invalid");
|
||||
}
|
||||
return new JcaPEMKeyConverter().setProvider(PROVIDER).getPublicKey(publicKeyInfo);
|
||||
}
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read upgrade signature public key pem");
|
||||
} catch (Exception ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] loadSignature(Path signaturePath) {
|
||||
byte[] raw = readBytes(signaturePath, "failed to read upgrade signature file");
|
||||
if (raw.length == 0) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file is empty");
|
||||
}
|
||||
|
||||
String text = new String(raw, StandardCharsets.UTF_8).trim();
|
||||
if (!text.isEmpty()) {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(text);
|
||||
if (decoded.length > 0) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Keep compatibility with raw binary signature files.
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
private byte[] readBytes(Path path, String failureMessage) {
|
||||
try {
|
||||
return Files.readAllBytes(path);
|
||||
} catch (IOException ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), failureMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -5,5 +5,9 @@ import java.nio.file.Path;
|
||||
|
||||
public interface UpgradePackageSignatureVerifier {
|
||||
|
||||
void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath);
|
||||
void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath);
|
||||
|
||||
default void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
verify(manifest, manifestPath, signaturePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ tms:
|
||||
staging-root-dir: ${TMS_UPGRADE_STAGING_ROOT_DIR:/home/tmp/tms-upgrade-staging}
|
||||
# 升级任务统一日志目录。
|
||||
log-dir: ${TMS_UPGRADE_LOG_DIR:/home/tms/tmp/tms-upgrade-logs}
|
||||
# 升级包验签使用的“固件完整性密钥”公钥 blob(Base64 编码);为空时升级预检会拒绝通过。
|
||||
signature-public-key-base64: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_BASE64:}
|
||||
# 升级包验签使用的公钥 PEM 文件路径;为空时升级预检会拒绝通过。
|
||||
signature-public-key-pem-path: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_PEM_PATH:}
|
||||
mk:
|
||||
init-identify:
|
||||
# SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE t_upgrade_task
|
||||
ADD COLUMN product_type VARCHAR(16) NULL AFTER target_component;
|
||||
|
||||
UPDATE t_upgrade_task
|
||||
SET product_type = 'ENTERPRISE'
|
||||
WHERE product_type IS NULL OR product_type = '';
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE t_upgrade_task
|
||||
CHANGE COLUMN target_component task_type VARCHAR(32) NOT NULL;
|
||||
|
||||
ALTER TABLE t_upgrade_task
|
||||
DROP INDEX idx_t_upgrade_task_component,
|
||||
ADD INDEX idx_t_upgrade_task_task_type (task_type);
|
||||
@@ -8,7 +8,8 @@
|
||||
<id property="id" column="id"/>
|
||||
<result property="taskId" column="task_id"/>
|
||||
<result property="upgradeMode" column="upgrade_mode"/>
|
||||
<result property="targetComponent" column="target_component"/>
|
||||
<result property="taskType" column="task_type"/>
|
||||
<result property="productType" column="product_type"/>
|
||||
<result property="packageFileId" column="package_file_id"/>
|
||||
<result property="packageName" column="package_name"/>
|
||||
<result property="packageHash" column="package_hash"/>
|
||||
@@ -28,7 +29,7 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="UpgradeTaskColumns">
|
||||
id, task_id, upgrade_mode, target_component, package_file_id, package_name, package_hash,
|
||||
id, task_id, upgrade_mode, task_type, product_type, package_file_id, package_name, package_hash,
|
||||
current_version, target_version, status, verify_passed, verify_message, manifest_json,
|
||||
detail_log_path, error_message, operator, started_at, finished_at, create_time, update_time
|
||||
</sql>
|
||||
@@ -52,8 +53,8 @@
|
||||
SELECT <include refid="UpgradeTaskColumns"/>
|
||||
FROM t_upgrade_task
|
||||
<where>
|
||||
<if test="targetComponent != null and targetComponent != ''">
|
||||
AND target_component = #{targetComponent}
|
||||
<if test="taskType != null and taskType != ''">
|
||||
AND task_type = #{taskType}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND status = #{status}
|
||||
@@ -67,8 +68,8 @@
|
||||
SELECT COUNT(1)
|
||||
FROM t_upgrade_task
|
||||
<where>
|
||||
<if test="targetComponent != null and targetComponent != ''">
|
||||
AND target_component = #{targetComponent}
|
||||
<if test="taskType != null and taskType != ''">
|
||||
AND task_type = #{taskType}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
AND status = #{status}
|
||||
|
||||
@@ -15,21 +15,23 @@ class UpgradeDtoContractTest {
|
||||
@Test
|
||||
void shouldSerializeUpgradePreviewFields() throws Exception {
|
||||
UpgradePreviewResponse response = new UpgradePreviewResponse();
|
||||
response.setTargetComponent("STANDARD_APP");
|
||||
response.setTaskType("RECEIVER");
|
||||
response.setProductType("DIRECT");
|
||||
response.setCurrentVersion("V1.3.1");
|
||||
response.setTargetVersion("V1.3.2");
|
||||
response.setVersionChangeSummary("标准收发器企业版将从 V1.3.1 升级到 V1.3.2");
|
||||
response.setTargetVersion("V1.4.0");
|
||||
response.setVersionChangeSummary("当前设备将从 V1.3.1 升级到 V1.4.0");
|
||||
response.setVerifyPassed(true);
|
||||
response.setVerifyMessage("校验通过");
|
||||
response.setAllowed(true);
|
||||
response.setBlockReason("");
|
||||
response.setDescription("修复标准收发器已知问题");
|
||||
response.setDescription("receiver upgrade package");
|
||||
|
||||
String json = objectMapper.writeValueAsString(response);
|
||||
|
||||
Assertions.assertTrue(json.contains("\"targetComponent\":\"STANDARD_APP\""));
|
||||
Assertions.assertTrue(json.contains("\"taskType\":\"RECEIVER\""));
|
||||
Assertions.assertTrue(json.contains("\"productType\":\"DIRECT\""));
|
||||
Assertions.assertTrue(json.contains("\"currentVersion\":\"V1.3.1\""));
|
||||
Assertions.assertTrue(json.contains("\"targetVersion\":\"V1.3.2\""));
|
||||
Assertions.assertTrue(json.contains("\"targetVersion\":\"V1.4.0\""));
|
||||
Assertions.assertTrue(json.contains("\"versionChangeSummary\""));
|
||||
Assertions.assertTrue(json.contains("\"verifyPassed\":true"));
|
||||
Assertions.assertTrue(json.contains("\"allowed\":true"));
|
||||
@@ -46,8 +48,8 @@ class UpgradeDtoContractTest {
|
||||
@Test
|
||||
void shouldExposeExpectedUpgradeEnums() {
|
||||
Assertions.assertEquals("TMS", UpgradeTargetComponent.TMS.name());
|
||||
Assertions.assertEquals("STANDARD_APP", UpgradeTargetComponent.STANDARD_APP.name());
|
||||
Assertions.assertEquals("DIRECT_APP", UpgradeTargetComponent.DIRECT_APP.name());
|
||||
Assertions.assertEquals("RECEIVER", UpgradeTargetComponent.RECEIVER.name());
|
||||
Assertions.assertEquals("FIRMWARE", UpgradeTargetComponent.FIRMWARE.name());
|
||||
Assertions.assertEquals("PENDING_CONFIRM", UpgradeTaskStatus.PENDING_CONFIRM.name());
|
||||
Assertions.assertEquals("RUNNING", UpgradeTaskStatus.RUNNING.name());
|
||||
Assertions.assertEquals("SUCCESS", UpgradeTaskStatus.SUCCESS.name());
|
||||
|
||||
@@ -46,7 +46,8 @@ class UpgradeControllerTest {
|
||||
FileService fileService = Mockito.mock(FileService.class);
|
||||
|
||||
UpgradePreviewResponse preview = new UpgradePreviewResponse();
|
||||
preview.setTargetComponent("STANDARD_APP");
|
||||
preview.setTaskType("RECEIVER");
|
||||
preview.setProductType("DIRECT");
|
||||
preview.setCurrentVersion("V1.3.1");
|
||||
preview.setTargetVersion("V1.3.2");
|
||||
preview.setAllowed(true);
|
||||
@@ -56,7 +57,8 @@ class UpgradeControllerTest {
|
||||
UpgradeCreateTaskResponse create = new UpgradeCreateTaskResponse();
|
||||
create.setTaskId("UPG-001");
|
||||
create.setStatus("PENDING_CONFIRM");
|
||||
create.setTargetComponent("STANDARD_APP");
|
||||
create.setTaskType("RECEIVER");
|
||||
create.setProductType("DIRECT");
|
||||
create.setCurrentVersion("V1.3.1");
|
||||
create.setTargetVersion("V1.3.2");
|
||||
Mockito.when(upgradeService.createTask(Mockito.any())).thenReturn(create);
|
||||
@@ -64,7 +66,10 @@ class UpgradeControllerTest {
|
||||
UpgradeTaskResponse task = new UpgradeTaskResponse();
|
||||
task.setTaskId("UPG-001");
|
||||
task.setStatus("RUNNING");
|
||||
task.setTaskType("RECEIVER");
|
||||
task.setProductType("DIRECT");
|
||||
Mockito.when(upgradeService.executeTask("UPG-001")).thenReturn(task);
|
||||
Mockito.when(upgradeService.rollbackTask("UPG-001")).thenReturn(task);
|
||||
Mockito.when(upgradeService.getTask("UPG-001")).thenReturn(task);
|
||||
|
||||
UpgradeTaskPageResponse page = new UpgradeTaskPageResponse();
|
||||
@@ -80,7 +85,8 @@ class UpgradeControllerTest {
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"fileId\":\"FILE-001\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"targetComponent\":\"STANDARD_APP\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskType\":\"RECEIVER\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"productType\":\"DIRECT\"")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/upgrades")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -92,10 +98,14 @@ class UpgradeControllerTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"status\":\"RUNNING\"")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/upgrades/UPG-001/rollback"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"status\":\"RUNNING\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/upgrades")
|
||||
.param("page", "1")
|
||||
.param("pageSize", "10")
|
||||
.param("targetComponent", "")
|
||||
.param("taskType", "")
|
||||
.param("status", ""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"total\":1")));
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package com.cisd.tms.modules.upgrade.executor;
|
||||
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity;
|
||||
import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class UpgradeTaskRunnerTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void shouldRunPrecheckExecuteAndVerifyScriptsInOrder() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-runner-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, true, "#!/bin/sh\necho execute\nexit 0\n", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryUpgradeTaskRepository taskRepository = new InMemoryUpgradeTaskRepository();
|
||||
InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository();
|
||||
|
||||
UpgradeTaskRunner runner = new UpgradeTaskRunner(
|
||||
taskRepository,
|
||||
fileRepository,
|
||||
versionRepository,
|
||||
stagingService(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
properties(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
runner.run(task("UPG-001", fileRecord.getFileId(), "TMS", "V1.0.1"));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", taskRepository.status);
|
||||
Assertions.assertEquals("V1.0.1", versionRepository.store.get("TMS").getCurrentVersion());
|
||||
String log = Files.readString(Path.of(taskRepository.logPath));
|
||||
Assertions.assertTrue(log.contains("precheck"));
|
||||
Assertions.assertTrue(log.contains("execute"));
|
||||
Assertions.assertTrue(log.contains("verify"));
|
||||
Assertions.assertTrue(log.indexOf("precheck") < log.indexOf("execute"));
|
||||
Assertions.assertTrue(log.indexOf("execute") < log.indexOf("verify"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkTaskFailedWhenExecuteScriptFails() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-runner-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, false, "#!/bin/sh\necho execute\nexit 2\n", false);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryUpgradeTaskRepository taskRepository = new InMemoryUpgradeTaskRepository();
|
||||
|
||||
UpgradeTaskRunner runner = new UpgradeTaskRunner(
|
||||
taskRepository,
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
properties(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
runner.run(task("UPG-002", fileRecord.getFileId(), "RECEIVER", "V1.3.2"));
|
||||
|
||||
Assertions.assertEquals("FAILED", taskRepository.status);
|
||||
Assertions.assertTrue(taskRepository.errorMessage.contains("execute"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunRollbackScriptWhenRequested() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-runner-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, false, "#!/bin/sh\necho execute\nexit 0\n", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryUpgradeTaskRepository taskRepository = new InMemoryUpgradeTaskRepository();
|
||||
|
||||
UpgradeTaskRunner runner = new UpgradeTaskRunner(
|
||||
taskRepository,
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
properties(tempDir.resolve("staging"), tempDir.resolve("logs")),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
runner.rollback(task("UPG-003", fileRecord.getFileId(), "FIRMWARE", "V3.0.0"));
|
||||
|
||||
Assertions.assertEquals("ROLLBACK_SUCCESS", taskRepository.status);
|
||||
String log = Files.readString(Path.of(taskRepository.logPath));
|
||||
Assertions.assertTrue(log.contains("rollback"));
|
||||
}
|
||||
|
||||
private UpgradeProperties properties(Path stagingRoot, Path logDir) {
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setStagingRootDir(stagingRoot.toString());
|
||||
properties.setLogDir(logDir.toString());
|
||||
return properties;
|
||||
}
|
||||
|
||||
private UpgradePackageStagingService stagingService(Path stagingRoot, Path logDir) {
|
||||
try {
|
||||
Files.createDirectories(logDir);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
return new UpgradePackageStagingService(properties(stagingRoot, logDir));
|
||||
}
|
||||
|
||||
private FileRecordEntity buildPackage(Path tempDir, boolean includeOptionalScripts, String executeScript, boolean includeRollback)
|
||||
throws Exception {
|
||||
Path packagePath = tempDir.resolve("upgrade-package.zip");
|
||||
|
||||
UpgradeManifest manifest = new UpgradeManifest();
|
||||
manifest.setPackageId("PKG-001");
|
||||
manifest.setTaskType("TMS");
|
||||
manifest.setProductType("ENTERPRISE");
|
||||
manifest.setVersion("V1.0.1");
|
||||
manifest.setMinCompatibleVersion("V1.0.0");
|
||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||
if (includeOptionalScripts) {
|
||||
entrypoints.setPrecheck("payload/scripts/precheck.sh");
|
||||
entrypoints.setVerify("payload/scripts/verify.sh");
|
||||
}
|
||||
if (includeRollback) {
|
||||
entrypoints.setRollback("payload/scripts/rollback.sh");
|
||||
}
|
||||
manifest.setEntrypoints(entrypoints);
|
||||
|
||||
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) {
|
||||
outputStream.putNextEntry(new ZipEntry("manifest.json"));
|
||||
outputStream.write(objectMapper.writeValueAsBytes(manifest));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("signature.sig"));
|
||||
outputStream.write("signature".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
||||
outputStream.closeEntry();
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/"));
|
||||
outputStream.closeEntry();
|
||||
|
||||
if (includeOptionalScripts) {
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/precheck.sh"));
|
||||
outputStream.write("#!/bin/sh\necho precheck\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/verify.sh"));
|
||||
outputStream.write("#!/bin/sh\necho verify\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/execute.sh"));
|
||||
outputStream.write(executeScript.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
if (includeRollback) {
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/rollback.sh"));
|
||||
outputStream.write("#!/bin/sh\necho rollback\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
FileRecordEntity entity = new FileRecordEntity();
|
||||
entity.setFileId("FILE-001");
|
||||
entity.setOriginalFilename("upgrade-package.zip");
|
||||
entity.setStoragePath(packagePath.toString());
|
||||
entity.setStatus("ACTIVE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private UpgradeTaskEntity task(String taskId, String fileId, String taskType, String targetVersion) {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setPackageFileId(fileId);
|
||||
entity.setTaskType(taskType);
|
||||
entity.setTargetVersion(targetVersion);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static class InMemoryUpgradeTaskRepository implements UpgradeTaskRepository {
|
||||
private String status;
|
||||
private String errorMessage;
|
||||
private String logPath;
|
||||
|
||||
@Override
|
||||
public void save(UpgradeTaskEntity entity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UpgradeTaskEntity> findByTaskId(String taskId) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<UpgradeTaskEntity> findRunningTask() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit) {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countPage(String taskType, String status) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(String taskId, String status, String errorMessage, String detailLogPath, LocalDateTime startedAt, LocalDateTime finishedAt) {
|
||||
this.status = status;
|
||||
this.errorMessage = errorMessage;
|
||||
this.logPath = detailLogPath;
|
||||
}
|
||||
}
|
||||
|
||||
private static class InMemoryFileRecordRepository implements FileRecordRepository {
|
||||
private final Map<String, FileRecordEntity> store = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Optional<FileRecordEntity> findByFileId(String fileId) {
|
||||
return Optional.ofNullable(store.get(fileId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(FileRecordEntity entity) {
|
||||
store.put(entity.getFileId(), entity);
|
||||
}
|
||||
}
|
||||
|
||||
private static class InMemoryDeviceSoftwareVersionRepository implements DeviceSoftwareVersionRepository {
|
||||
private final Map<String, DeviceSoftwareVersionEntity> store = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Optional<DeviceSoftwareVersionEntity> findByComponentCode(String componentCode) {
|
||||
return Optional.ofNullable(store.get(componentCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOrUpdate(DeviceSoftwareVersionEntity entity) {
|
||||
store.put(entity.getComponentCode(), entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -25,7 +25,8 @@ class UpgradeTaskRepositoryTest {
|
||||
UpgradeTaskEntity found = repository.findByTaskId("UPG-001").orElseThrow();
|
||||
|
||||
Assertions.assertEquals("UPG-001", found.getTaskId());
|
||||
Assertions.assertEquals("STANDARD_APP", found.getTargetComponent());
|
||||
Assertions.assertEquals("RECEIVER", found.getTaskType());
|
||||
Assertions.assertEquals("DIRECT", found.getProductType());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,7 +89,8 @@ class UpgradeTaskRepositoryTest {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setUpgradeMode("OFFLINE");
|
||||
entity.setTargetComponent("STANDARD_APP");
|
||||
entity.setTaskType("RECEIVER");
|
||||
entity.setProductType("DIRECT");
|
||||
entity.setPackageFileId("FILE-001");
|
||||
entity.setPackageName("upgrade-package.zip");
|
||||
entity.setPackageHash("abc123");
|
||||
@@ -97,7 +99,7 @@ class UpgradeTaskRepositoryTest {
|
||||
entity.setStatus(status);
|
||||
entity.setVerifyPassed(Boolean.TRUE);
|
||||
entity.setVerifyMessage("ok");
|
||||
entity.setManifestJson("{\"targetComponent\":\"STANDARD_APP\"}");
|
||||
entity.setManifestJson("{\"taskType\":\"RECEIVER\",\"productType\":\"DIRECT\"}");
|
||||
entity.setOperator("admin");
|
||||
return entity;
|
||||
}
|
||||
|
||||
+188
-46
@@ -2,6 +2,7 @@ package com.cisd.tms.modules.upgrade.service;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity;
|
||||
import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
@@ -9,37 +10,51 @@ import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse;
|
||||
import com.cisd.tms.modules.upgrade.support.SoftUpgradePackageSignatureVerifier;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradePackageSignatureVerifier;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService;
|
||||
import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.StringWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.HashMap;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemWriter;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class UpgradePackageServiceTest {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreviewUpgradePackageWithVersionChange() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "STANDARD_APP", "V1.3.2", "V1.3.0", "修复标准收发器问题");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "RECEIVER", "DIRECT", "V1.4.0", "V1.3.0", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository();
|
||||
versionRepository.save(component("STANDARD_APP", "标准收发器企业版", "V1.3.1"));
|
||||
versionRepository.save(component("RECEIVER", "标准收发器", "V1.3.1"));
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
@@ -47,47 +62,48 @@ class UpgradePackageServiceTest {
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("DIRECT"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
UpgradePreviewResponse response = service.preview(fileRecord.getFileId());
|
||||
|
||||
Assertions.assertEquals("STANDARD_APP", response.getTargetComponent());
|
||||
Assertions.assertEquals("RECEIVER", response.getTaskType());
|
||||
Assertions.assertEquals("DIRECT", response.getProductType());
|
||||
Assertions.assertEquals("V1.3.1", response.getCurrentVersion());
|
||||
Assertions.assertEquals("V1.3.2", response.getTargetVersion());
|
||||
Assertions.assertEquals("V1.4.0", response.getTargetVersion());
|
||||
Assertions.assertTrue(response.isVerifyPassed());
|
||||
Assertions.assertTrue(response.isAllowed());
|
||||
Assertions.assertTrue(response.getVersionChangeSummary().contains("V1.3.1"));
|
||||
Assertions.assertTrue(response.getVersionChangeSummary().contains("V1.3.2"));
|
||||
Assertions.assertTrue(response.getVersionChangeSummary().contains("V1.4.0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPayloadChecksumMismatch() throws Exception {
|
||||
void shouldRejectWhenProductTypeDoesNotMatchCurrentDevice() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "TMS", "V1.0.1", "V1.0.0", "修复 TMS 问题");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "RECEIVER", "DIRECT", "V1.4.0", "V1.3.0", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
|
||||
tamperManifestChecksum(Path.of(fileRecord.getStoragePath()));
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("ENTERPRISE"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, () -> service.preview(fileRecord.getFileId()));
|
||||
Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("payload checksum mismatch", exception.getMessage());
|
||||
Assertions.assertEquals("upgrade package product type does not match current device", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDowngrade() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "TMS", "V1.0.0", "V0.9.0", "尝试降级");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "TMS", "ENTERPRISE", "V1.0.0", "V0.9.0", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository();
|
||||
@@ -99,6 +115,7 @@ class UpgradePackageServiceTest {
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("ENTERPRISE"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
@@ -109,11 +126,11 @@ class UpgradePackageServiceTest {
|
||||
@Test
|
||||
void shouldRejectWhenCurrentVersionIsLowerThanMinCompatibleVersion() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "DIRECT_APP", "V2.0.0", "V1.5.0", "需要更高起始版本");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "RECEIVER", "DIRECT", "V2.0.0", "V1.5.0", true);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository();
|
||||
versionRepository.save(component("DIRECT_APP", "直参轻量化终端", "V1.4.9"));
|
||||
versionRepository.save(component("RECEIVER", "标准收发器", "V1.4.9"));
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
@@ -121,6 +138,7 @@ class UpgradePackageServiceTest {
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("DIRECT"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
@@ -128,31 +146,96 @@ class UpgradePackageServiceTest {
|
||||
Assertions.assertEquals("current version does not satisfy minimum compatible version", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenExecuteScriptIsMissing() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
FileRecordEntity fileRecord = buildPackage(tempDir, "FIRMWARE", "DIRECT", "V3.0.0", "V2.0.0", false);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new PassThroughSignatureVerifier(),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("DIRECT"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, () -> service.preview(fileRecord.getFileId()));
|
||||
Assertions.assertEquals("upgrade execute script not found", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreviewUpgradePackageWithSoftSignatureVerification() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-preview-test");
|
||||
KeyPair keyPair = generateSm2KeyPair();
|
||||
Path publicKeyPath = tempDir.resolve("upgrade-public-key.pem");
|
||||
Files.writeString(publicKeyPath, toPem("PUBLIC KEY", keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||
|
||||
FileRecordEntity fileRecord = buildSignedPackage(
|
||||
tempDir,
|
||||
"TMS",
|
||||
"ENTERPRISE",
|
||||
"V1.1.0",
|
||||
"V1.0.0",
|
||||
keyPair
|
||||
);
|
||||
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
|
||||
fileRepository.save(fileRecord);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyPemPath(publicKeyPath.toString());
|
||||
|
||||
UpgradePackageService service = new UpgradePackageService(
|
||||
fileRepository,
|
||||
new InMemoryDeviceSoftwareVersionRepository(),
|
||||
stagingService(tempDir.resolve("staging")),
|
||||
new SoftUpgradePackageSignatureVerifier(properties),
|
||||
new UpgradeVersionComparator(),
|
||||
preset("ENTERPRISE"),
|
||||
objectMapper
|
||||
);
|
||||
|
||||
UpgradePreviewResponse response = service.preview(fileRecord.getFileId());
|
||||
|
||||
Assertions.assertTrue(response.isVerifyPassed());
|
||||
Assertions.assertEquals("TMS", response.getTaskType());
|
||||
}
|
||||
|
||||
private UpgradePackageStagingService stagingService(Path stagingRoot) {
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setStagingRootDir(stagingRoot.toString());
|
||||
return new UpgradePackageStagingService(properties);
|
||||
}
|
||||
|
||||
private FileRecordEntity buildPackage(Path tempDir, String targetComponent, String targetVersion, String minCompatibleVersion, String description)
|
||||
throws IOException, NoSuchAlgorithmException {
|
||||
private CisdPresetProperties preset(String productType) {
|
||||
CisdPresetProperties properties = new CisdPresetProperties();
|
||||
properties.setProductType(productType);
|
||||
return properties;
|
||||
}
|
||||
|
||||
private FileRecordEntity buildPackage(Path tempDir, String taskType, String productType, String version, String minCompatibleVersion, boolean includeExecute)
|
||||
throws IOException {
|
||||
Path packagePath = tempDir.resolve("upgrade-package.zip");
|
||||
Path payloadPath = tempDir.resolve("payload.zip");
|
||||
Files.write(payloadPath, "payload-content".getBytes(StandardCharsets.UTF_8));
|
||||
byte[] signatureBytes = "signature".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
UpgradeManifest manifest = new UpgradeManifest();
|
||||
manifest.setPackageId("PKG-001");
|
||||
manifest.setTargetComponent(targetComponent);
|
||||
manifest.setTargetVersion(targetVersion);
|
||||
manifest.setTaskType(taskType);
|
||||
manifest.setProductType(productType);
|
||||
manifest.setVersion(version);
|
||||
manifest.setMinCompatibleVersion(minCompatibleVersion);
|
||||
manifest.setDescription(description);
|
||||
manifest.setOrchestrator("scripts/orchestrator.sh");
|
||||
UpgradeManifest.FileChecksums checksums = new UpgradeManifest.FileChecksums();
|
||||
checksums.setPayloadZip(sha256Hex(payloadPath));
|
||||
manifest.setFileChecksums(checksums);
|
||||
manifest.setDescription(taskType.toLowerCase() + " upgrade package");
|
||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||
entrypoints.setPrecheck("payload/scripts/precheck.sh");
|
||||
entrypoints.setVerify("payload/scripts/verify.sh");
|
||||
entrypoints.setRollback("payload/scripts/rollback.sh");
|
||||
manifest.setEntrypoints(entrypoints);
|
||||
|
||||
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
||||
byte[] signatureBytes = "signature".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) {
|
||||
outputStream.putNextEntry(new ZipEntry("manifest.json"));
|
||||
@@ -163,8 +246,27 @@ class UpgradePackageServiceTest {
|
||||
outputStream.write(signatureBytes);
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload.zip"));
|
||||
outputStream.write(Files.readAllBytes(payloadPath));
|
||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
||||
outputStream.closeEntry();
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/"));
|
||||
outputStream.closeEntry();
|
||||
|
||||
if (includeExecute) {
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/execute.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/precheck.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/verify.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/rollback.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
|
||||
@@ -176,15 +278,29 @@ class UpgradePackageServiceTest {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void tamperManifestChecksum(Path packagePath) throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-manifest-tamper");
|
||||
UpgradePackageStagingService stagingService = stagingService(tempDir.resolve("staging"));
|
||||
Path staged = stagingService.stage("FILE-TAMPER", packagePath);
|
||||
UpgradeManifest manifest = objectMapper.readValue(Files.readString(staged.resolve("manifest.json")), UpgradeManifest.class);
|
||||
manifest.getFileChecksums().setPayloadZip("deadbeef");
|
||||
private FileRecordEntity buildSignedPackage(
|
||||
Path tempDir,
|
||||
String taskType,
|
||||
String productType,
|
||||
String version,
|
||||
String minCompatibleVersion,
|
||||
KeyPair keyPair
|
||||
) throws Exception {
|
||||
Path packagePath = tempDir.resolve("signed-upgrade-package.zip");
|
||||
|
||||
UpgradeManifest manifest = new UpgradeManifest();
|
||||
manifest.setPackageId("PKG-002");
|
||||
manifest.setTaskType(taskType);
|
||||
manifest.setProductType(productType);
|
||||
manifest.setVersion(version);
|
||||
manifest.setMinCompatibleVersion(minCompatibleVersion);
|
||||
manifest.setDescription(taskType.toLowerCase() + " signed upgrade package");
|
||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||
manifest.setEntrypoints(entrypoints);
|
||||
|
||||
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
||||
byte[] payloadBytes = Files.readAllBytes(staged.resolve("payload.zip"));
|
||||
byte[] signatureBytes = Files.readAllBytes(staged.resolve("signature.sig"));
|
||||
byte[] signatureBytes = sign(manifestBytes, keyPair);
|
||||
|
||||
try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) {
|
||||
outputStream.putNextEntry(new ZipEntry("manifest.json"));
|
||||
@@ -195,16 +311,21 @@ class UpgradePackageServiceTest {
|
||||
outputStream.write(signatureBytes);
|
||||
outputStream.closeEntry();
|
||||
|
||||
outputStream.putNextEntry(new ZipEntry("payload.zip"));
|
||||
outputStream.write(payloadBytes);
|
||||
outputStream.putNextEntry(new ZipEntry("payload/"));
|
||||
outputStream.closeEntry();
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/"));
|
||||
outputStream.closeEntry();
|
||||
outputStream.putNextEntry(new ZipEntry("payload/scripts/execute.sh"));
|
||||
outputStream.write("#!/bin/sh\nexit 0\n".getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private static String sha256Hex(Path file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
digest.update(Files.readAllBytes(file));
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
FileRecordEntity entity = new FileRecordEntity();
|
||||
entity.setFileId("FILE-UPGRADE-002");
|
||||
entity.setOriginalFilename("signed-upgrade-package.zip");
|
||||
entity.setStoragePath(packagePath.toString());
|
||||
entity.setStatus("ACTIVE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static DeviceSoftwareVersionEntity component(String code, String name, String version) {
|
||||
@@ -215,9 +336,30 @@ class UpgradePackageServiceTest {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private KeyPair generateSm2KeyPair() throws Exception {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", PROVIDER);
|
||||
generator.initialize(new ECGenParameterSpec("sm2p256v1"));
|
||||
return generator.generateKeyPair();
|
||||
}
|
||||
|
||||
private byte[] sign(byte[] data, KeyPair keyPair) throws Exception {
|
||||
Signature signature = Signature.getInstance("SM3withSM2", PROVIDER);
|
||||
signature.initSign(keyPair.getPrivate());
|
||||
signature.update(data);
|
||||
return signature.sign();
|
||||
}
|
||||
|
||||
private String toPem(String type, byte[] content) throws Exception {
|
||||
StringWriter writer = new StringWriter();
|
||||
try (PemWriter pemWriter = new PemWriter(writer)) {
|
||||
pemWriter.writeObject(new PemObject(type, content));
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private static class PassThroughSignatureVerifier implements UpgradePackageSignatureVerifier {
|
||||
@Override
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) {
|
||||
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeTaskResponse;
|
||||
@@ -40,7 +41,8 @@ class UpgradeServiceTest {
|
||||
Mockito.when(fileRecordRepository.findByFileId("FILE-001")).thenReturn(Optional.of(fileRecord));
|
||||
|
||||
UpgradePreviewResponse preview = new UpgradePreviewResponse();
|
||||
preview.setTargetComponent("STANDARD_APP");
|
||||
preview.setTaskType("RECEIVER");
|
||||
preview.setProductType("DIRECT");
|
||||
preview.setCurrentVersion("V1.3.1");
|
||||
preview.setTargetVersion("V1.3.2");
|
||||
preview.setAllowed(true);
|
||||
@@ -64,7 +66,8 @@ class UpgradeServiceTest {
|
||||
UpgradeCreateTaskResponse response = service.createTask(request);
|
||||
|
||||
Assertions.assertEquals("PENDING_CONFIRM", response.getStatus());
|
||||
Assertions.assertEquals("STANDARD_APP", response.getTargetComponent());
|
||||
Assertions.assertEquals("RECEIVER", response.getTaskType());
|
||||
Assertions.assertEquals("DIRECT", response.getProductType());
|
||||
Assertions.assertEquals("V1.3.1", response.getCurrentVersion());
|
||||
Assertions.assertEquals("V1.3.2", response.getTargetVersion());
|
||||
Assertions.assertTrue(response.getTaskId().startsWith("UPG-"));
|
||||
@@ -134,6 +137,38 @@ class UpgradeServiceTest {
|
||||
Assertions.assertEquals("SUCCESS", response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRollbackWhenPackageDoesNotDeclareRollbackScript() {
|
||||
InMemoryUpgradeTaskRepository repository = new InMemoryUpgradeTaskRepository();
|
||||
repository.save(entity("UPG-001", "FAILED"));
|
||||
|
||||
UpgradePackageService packageService = Mockito.mock(UpgradePackageService.class);
|
||||
UpgradeManifest manifest = new UpgradeManifest();
|
||||
manifest.setPackageId("PKG-001");
|
||||
manifest.setTaskType("RECEIVER");
|
||||
manifest.setProductType("DIRECT");
|
||||
manifest.setVersion("V1.3.2");
|
||||
manifest.setMinCompatibleVersion("V1.3.0");
|
||||
UpgradeManifest.Entrypoints entrypoints = new UpgradeManifest.Entrypoints();
|
||||
entrypoints.setExecute("payload/scripts/execute.sh");
|
||||
manifest.setEntrypoints(entrypoints);
|
||||
Mockito.when(packageService.loadManifest("FILE-001")).thenReturn(manifest);
|
||||
|
||||
UpgradeService service = new UpgradeService(
|
||||
repository,
|
||||
Mockito.mock(FileRecordRepository.class),
|
||||
packageService,
|
||||
Mockito.mock(UpgradeTaskRunner.class),
|
||||
properties(),
|
||||
Runnable::run,
|
||||
new ConcurrentHashMap<>()
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, () -> service.rollbackTask("UPG-001"));
|
||||
|
||||
Assertions.assertEquals("upgrade rollback script not found", exception.getMessage());
|
||||
}
|
||||
|
||||
private static UpgradeProperties properties() {
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setLogDir("/home/tms/tmp/tms-upgrade-logs");
|
||||
@@ -144,7 +179,8 @@ class UpgradeServiceTest {
|
||||
UpgradeTaskEntity entity = new UpgradeTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setUpgradeMode("OFFLINE");
|
||||
entity.setTargetComponent("STANDARD_APP");
|
||||
entity.setTaskType("RECEIVER");
|
||||
entity.setProductType("DIRECT");
|
||||
entity.setPackageFileId("FILE-001");
|
||||
entity.setPackageName("upgrade-package.zip");
|
||||
entity.setCurrentVersion("V1.3.1");
|
||||
@@ -178,9 +214,9 @@ class UpgradeServiceTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UpgradeTaskEntity> findPage(String targetComponent, String status, int offset, int limit) {
|
||||
public List<UpgradeTaskEntity> findPage(String taskType, String status, int offset, int limit) {
|
||||
return store.values().stream()
|
||||
.filter(entity -> targetComponent == null || targetComponent.isBlank() || targetComponent.equals(entity.getTargetComponent()))
|
||||
.filter(entity -> taskType == null || taskType.isBlank() || taskType.equals(entity.getTaskType()))
|
||||
.filter(entity -> status == null || status.isBlank() || status.equals(entity.getStatus()))
|
||||
.sorted(Comparator.comparing(UpgradeTaskEntity::getTaskId).reversed())
|
||||
.skip(offset)
|
||||
@@ -189,8 +225,8 @@ class UpgradeServiceTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countPage(String targetComponent, String status) {
|
||||
return findPage(targetComponent, status, 0, Integer.MAX_VALUE).size();
|
||||
public long countPage(String taskType, String status) {
|
||||
return findPage(taskType, status, 0, Integer.MAX_VALUE).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Base64;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class PcieUpgradePackageSignatureVerifierTest {
|
||||
|
||||
@Test
|
||||
void shouldVerifyUpgradeManifestWithConfiguredIntegrityKey() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path payloadZipPath = tempDir.resolve("payload.zip");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(payloadZipPath, new byte[] {0x01, 0x02});
|
||||
byte[] signatureBytes = new byte[128];
|
||||
Files.writeString(signaturePath, Base64.getEncoder().encodeToString(signatureBytes), StandardCharsets.UTF_8);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
byte[] publicKeyBlob = new byte[132];
|
||||
properties.setSignaturePublicKeyBase64(Base64.getEncoder().encodeToString(publicKeyBlob));
|
||||
|
||||
PcieCryptoService cryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
PcieUpgradePackageSignatureVerifier verifier = new PcieUpgradePackageSignatureVerifier(properties, cryptoService);
|
||||
|
||||
verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath);
|
||||
|
||||
ArgumentCaptor<EccExternalVerifyRequest> requestCaptor = ArgumentCaptor.forClass(EccExternalVerifyRequest.class);
|
||||
Mockito.verify(cryptoService).eccExternalVerify(requestCaptor.capture());
|
||||
EccExternalVerifyRequest request = requestCaptor.getValue();
|
||||
Assertions.assertEquals(Gm0018AlgorithmIds.SM2_SIGN, request.getAlgId());
|
||||
Assertions.assertArrayEquals(publicKeyBlob, request.getPublicKeyBlob());
|
||||
Assertions.assertArrayEquals(signatureBytes, request.getSignature());
|
||||
Assertions.assertArrayEquals(Files.readAllBytes(manifestPath), request.getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenIntegrityKeyIsNotConfigured() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path payloadZipPath = tempDir.resolve("payload.zip");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(payloadZipPath, new byte[] {0x01, 0x02});
|
||||
Files.write(signaturePath, new byte[128]);
|
||||
|
||||
PcieUpgradePackageSignatureVerifier verifier =
|
||||
new PcieUpgradePackageSignatureVerifier(new UpgradeProperties(), Mockito.mock(PcieCryptoService.class));
|
||||
|
||||
BizException exception = Assertions.assertThrows(
|
||||
BizException.class,
|
||||
() -> verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath)
|
||||
);
|
||||
|
||||
Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("upgrade signature public key not configured", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWrapCryptoCardVerifyFailure() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path payloadZipPath = tempDir.resolve("payload.zip");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(payloadZipPath, new byte[] {0x01, 0x02});
|
||||
Files.write(signaturePath, new byte[128]);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyBase64(Base64.getEncoder().encodeToString(new byte[132]));
|
||||
PcieCryptoService cryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.doThrow(new IllegalStateException("verify failed")).when(cryptoService).eccExternalVerify(Mockito.any());
|
||||
PcieUpgradePackageSignatureVerifier verifier = new PcieUpgradePackageSignatureVerifier(properties, cryptoService);
|
||||
|
||||
BizException exception = Assertions.assertThrows(
|
||||
BizException.class,
|
||||
() -> verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath)
|
||||
);
|
||||
|
||||
Assertions.assertEquals(ErrorCode.CRYPTO_CARD_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("upgrade package signature verification failed", exception.getMessage());
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package com.cisd.tms.modules.upgrade.support;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.upgrade.config.UpgradeProperties;
|
||||
import com.cisd.tms.modules.upgrade.dto.UpgradeManifest;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.Base64;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemWriter;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SoftUpgradePackageSignatureVerifierTest {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldVerifyUpgradeManifestWithPemPublicKey() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Path publicKeyPath = tempDir.resolve("public_key.pem");
|
||||
byte[] manifestBytes = "{\"packageId\":\"PKG-001\"}".getBytes(StandardCharsets.UTF_8);
|
||||
Files.write(manifestPath, manifestBytes);
|
||||
|
||||
KeyPair keyPair = generateSm2KeyPair();
|
||||
Files.writeString(publicKeyPath, toPem("PUBLIC KEY", keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||
Files.write(signaturePath, sign(manifestBytes, keyPair));
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyPemPath(publicKeyPath.toString());
|
||||
|
||||
SoftUpgradePackageSignatureVerifier verifier = new SoftUpgradePackageSignatureVerifier(properties);
|
||||
|
||||
verifier.verify(new UpgradeManifest(), manifestPath, signaturePath);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldVerifyBase64SignatureContent() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Path publicKeyPath = tempDir.resolve("public_key.pem");
|
||||
byte[] manifestBytes = "{\"packageId\":\"PKG-001\"}".getBytes(StandardCharsets.UTF_8);
|
||||
Files.write(manifestPath, manifestBytes);
|
||||
|
||||
KeyPair keyPair = generateSm2KeyPair();
|
||||
Files.writeString(publicKeyPath, toPem("PUBLIC KEY", keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||
Files.writeString(signaturePath, Base64.getEncoder().encodeToString(sign(manifestBytes, keyPair)), StandardCharsets.UTF_8);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyPemPath(publicKeyPath.toString());
|
||||
|
||||
SoftUpgradePackageSignatureVerifier verifier = new SoftUpgradePackageSignatureVerifier(properties);
|
||||
|
||||
verifier.verify(new UpgradeManifest(), manifestPath, signaturePath);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenPemPathIsNotConfigured() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(signaturePath, new byte[] {0x01});
|
||||
|
||||
SoftUpgradePackageSignatureVerifier verifier = new SoftUpgradePackageSignatureVerifier(new UpgradeProperties());
|
||||
|
||||
BizException exception = Assertions.assertThrows(
|
||||
BizException.class,
|
||||
() -> verifier.verify(new UpgradeManifest(), manifestPath, signaturePath)
|
||||
);
|
||||
|
||||
Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("upgrade signature public key pem path not configured", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectWhenSignatureIsInvalid() throws Exception {
|
||||
Path tempDir = Files.createTempDirectory("upgrade-signature-verifier");
|
||||
Path manifestPath = tempDir.resolve("manifest.json");
|
||||
Path signaturePath = tempDir.resolve("signature.sig");
|
||||
Path publicKeyPath = tempDir.resolve("public_key.pem");
|
||||
Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8);
|
||||
Files.write(signaturePath, "invalid-signature".getBytes(StandardCharsets.UTF_8));
|
||||
Files.writeString(publicKeyPath, toPem("PUBLIC KEY", generateSm2KeyPair().getPublic().getEncoded()), StandardCharsets.UTF_8);
|
||||
|
||||
UpgradeProperties properties = new UpgradeProperties();
|
||||
properties.setSignaturePublicKeyPemPath(publicKeyPath.toString());
|
||||
SoftUpgradePackageSignatureVerifier verifier = new SoftUpgradePackageSignatureVerifier(properties);
|
||||
|
||||
BizException exception = Assertions.assertThrows(
|
||||
BizException.class,
|
||||
() -> verifier.verify(new UpgradeManifest(), manifestPath, signaturePath)
|
||||
);
|
||||
|
||||
Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode());
|
||||
Assertions.assertEquals("upgrade package signature verification failed", exception.getMessage());
|
||||
}
|
||||
|
||||
private KeyPair generateSm2KeyPair() throws Exception {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", PROVIDER);
|
||||
generator.initialize(new ECGenParameterSpec("sm2p256v1"));
|
||||
return generator.generateKeyPair();
|
||||
}
|
||||
|
||||
private byte[] sign(byte[] data, KeyPair keyPair) throws Exception {
|
||||
Signature signature = Signature.getInstance("SM3withSM2", PROVIDER);
|
||||
signature.initSign(keyPair.getPrivate());
|
||||
signature.update(data);
|
||||
return signature.sign();
|
||||
}
|
||||
|
||||
private String toPem(String type, byte[] content) throws Exception {
|
||||
StringWriter writer = new StringWriter();
|
||||
try (PemWriter pemWriter = new PemWriter(writer)) {
|
||||
pemWriter.writeObject(new PemObject(type, content));
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user