diff --git a/README.md b/README.md index 4bfd422..9f22efe 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/config/application.yml.example b/config/application.yml.example index dd7aee8..43f9abe 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -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: diff --git a/docs/deployment/tms-deployment.md b/docs/deployment/tms-deployment.md index cf4840f..49577bb 100644 --- a/docs/deployment/tms-deployment.md +++ b/docs/deployment/tms-deployment.md @@ -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 初始化步骤日志 diff --git a/docs/examples/upgrade-package/standard-app/README.md b/docs/examples/upgrade-package/standard-app/README.md index 1b95d54..0ec846c 100644 --- a/docs/examples/upgrade-package/standard-app/README.md +++ b/docs/examples/upgrade-package/standard-app/README.md @@ -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` 无法通过预检 diff --git a/docs/examples/upgrade-package/standard-app/build-package.sh b/docs/examples/upgrade-package/standard-app/build-package.sh new file mode 100644 index 0000000..adb4faa --- /dev/null +++ b/docs/examples/upgrade-package/standard-app/build-package.sh @@ -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}" diff --git a/docs/examples/upgrade-package/standard-app/manifest.json b/docs/examples/upgrade-package/standard-app/manifest.json index c9c461e..e25e593 100644 --- a/docs/examples/upgrade-package/standard-app/manifest.json +++ b/docs/examples/upgrade-package/standard-app/manifest.json @@ -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" } } diff --git a/docs/examples/upgrade-package/standard-app/payload/scripts/execute.sh b/docs/examples/upgrade-package/standard-app/payload/scripts/execute.sh new file mode 100644 index 0000000..ffa09a5 --- /dev/null +++ b/docs/examples/upgrade-package/standard-app/payload/scripts/execute.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" diff --git a/docs/examples/upgrade-package/standard-app/payload/scripts/orchestrator.sh b/docs/examples/upgrade-package/standard-app/payload/scripts/orchestrator.sh deleted file mode 100644 index 77ef40d..0000000 --- a/docs/examples/upgrade-package/standard-app/payload/scripts/orchestrator.sh +++ /dev/null @@ -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" diff --git a/docs/examples/upgrade-package/standard-app/payload/scripts/precheck.sh b/docs/examples/upgrade-package/standard-app/payload/scripts/precheck.sh new file mode 100644 index 0000000..b57ae5d --- /dev/null +++ b/docs/examples/upgrade-package/standard-app/payload/scripts/precheck.sh @@ -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" diff --git a/docs/examples/upgrade-package/standard-app/payload/scripts/rollback.sh b/docs/examples/upgrade-package/standard-app/payload/scripts/rollback.sh new file mode 100644 index 0000000..8c274ef --- /dev/null +++ b/docs/examples/upgrade-package/standard-app/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=rollback message=rollback placeholder" diff --git a/docs/examples/upgrade-package/standard-app/payload/scripts/verify.sh b/docs/examples/upgrade-package/standard-app/payload/scripts/verify.sh new file mode 100644 index 0000000..af695f1 --- /dev/null +++ b/docs/examples/upgrade-package/standard-app/payload/scripts/verify.sh @@ -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" diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java index 892e27e..a7c3270 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java @@ -363,12 +363,12 @@ public interface PcieCryptoService { //----------------------------------------SDF通用算法运算类接口------------------------------------------ /** - * 使用通用内部 ECC 密钥执行签名。 + * 使用通用内部 ECC 密钥执行签名。(用户密钥使用) */ BackupDataResult eccInternalSign(EccInternalSignRequest request); /** - * 使用外部 ECC 私钥执行签名。 + * 使用外部 ECC 私钥执行签名。(用户密钥使用) */ BackupDataResult eccExternalSign(EccExternalSignRequest request); diff --git a/src/main/java/com/cisd/tms/modules/upgrade/config/UpgradeProperties.java b/src/main/java/com/cisd/tms/modules/upgrade/config/UpgradeProperties.java index 5c4fae0..9563ca8 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/config/UpgradeProperties.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/config/UpgradeProperties.java @@ -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; } } diff --git a/src/main/java/com/cisd/tms/modules/upgrade/controller/UpgradeController.java b/src/main/java/com/cisd/tms/modules/upgrade/controller/UpgradeController.java index e4040a6..ea7b9e6 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/controller/UpgradeController.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/controller/UpgradeController.java @@ -67,15 +67,24 @@ public class UpgradeController { return ApiResponse.success(upgradeService.executeTask(taskId)); } + @PostMapping("/upgrades/{taskId}/rollback") + @Operation(summary = "回滚离线升级任务", description = "异步受理升级回滚请求,立即返回任务当前快照。") + public ApiResponse rollback( + @Parameter(description = "升级任务号") + @PathVariable String taskId + ) { + return ApiResponse.success(upgradeService.rollbackTask(taskId)); + } + @GetMapping("/upgrades") @Operation(summary = "查询升级记录列表", description = "分页查询升级任务列表,支持按升级对象和状态筛选。") public ApiResponse 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}") diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskResponse.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskResponse.java index 2c4d644..4d23482 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskResponse.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskResponse.java @@ -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() { diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java index 315a712..7e54c58 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java @@ -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; } } } diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewResponse.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewResponse.java index 34ee50f..dc81a6c 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewResponse.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewResponse.java @@ -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() { diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskResponse.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskResponse.java index c90bdd8..addc87e 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskResponse.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskResponse.java @@ -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() { diff --git a/src/main/java/com/cisd/tms/modules/upgrade/entity/UpgradeTaskEntity.java b/src/main/java/com/cisd/tms/modules/upgrade/entity/UpgradeTaskEntity.java index fce08e9..d3cca4a 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/entity/UpgradeTaskEntity.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/entity/UpgradeTaskEntity.java @@ -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() { diff --git a/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTargetComponent.java b/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTargetComponent.java index 259b7e5..28cffd9 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTargetComponent.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTargetComponent.java @@ -2,6 +2,6 @@ package com.cisd.tms.modules.upgrade.enums; public enum UpgradeTargetComponent { TMS, - STANDARD_APP, - DIRECT_APP + RECEIVER, + FIRMWARE } diff --git a/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTaskStatus.java b/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTaskStatus.java index 56d1343..e6f5e65 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTaskStatus.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTaskStatus.java @@ -4,5 +4,8 @@ public enum UpgradeTaskStatus { PENDING_CONFIRM, RUNNING, SUCCESS, - FAILED + FAILED, + ROLLING_BACK, + ROLLBACK_SUCCESS, + ROLLBACK_FAILED } diff --git a/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java b/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java index 0799e08..9dbbd9f 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java @@ -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; }; } diff --git a/src/main/java/com/cisd/tms/modules/upgrade/mapper/UpgradeTaskMapper.java b/src/main/java/com/cisd/tms/modules/upgrade/mapper/UpgradeTaskMapper.java index d0327f1..7f395ef 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/mapper/UpgradeTaskMapper.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/mapper/UpgradeTaskMapper.java @@ -15,14 +15,14 @@ public interface UpgradeTaskMapper extends BaseMapperX { UpgradeTaskEntity selectRunningTask(); List 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 ); diff --git a/src/main/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepository.java b/src/main/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepository.java index 31e0694..fa13a56 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepository.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepository.java @@ -13,9 +13,9 @@ public interface UpgradeTaskRepository { Optional findRunningTask(); - List findPage(String targetComponent, String status, int offset, int limit); + List 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, diff --git a/src/main/java/com/cisd/tms/modules/upgrade/repository/impl/UpgradeTaskRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/upgrade/repository/impl/UpgradeTaskRepositoryImpl.java index 58bbd5f..d0f956b 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/repository/impl/UpgradeTaskRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/repository/impl/UpgradeTaskRepositoryImpl.java @@ -33,13 +33,13 @@ public class UpgradeTaskRepositoryImpl implements UpgradeTaskRepository { } @Override - public List findPage(String targetComponent, String status, int offset, int limit) { - return upgradeTaskMapper.selectPage(targetComponent, status, offset, limit); + public List 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 diff --git a/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java index c069555..98f21a1 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java @@ -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 + ) { + } } diff --git a/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradeService.java b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradeService.java index 3883873..45e6944 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradeService.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradeService.java @@ -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 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 entities = upgradeTaskRepository.findPage(trim(targetComponent), trim(status), offset, normalizedPageSize); - long total = upgradeTaskRepository.countPage(trim(targetComponent), trim(status)); + List 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()); diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/NoopUpgradePackageSignatureVerifier.java b/src/main/java/com/cisd/tms/modules/upgrade/support/NoopUpgradePackageSignatureVerifier.java index a304a40..593c539 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/support/NoopUpgradePackageSignatureVerifier.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/NoopUpgradePackageSignatureVerifier.java @@ -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"); } diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifier.java b/src/main/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifier.java deleted file mode 100644 index fffa850..0000000 --- a/src/main/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifier.java +++ /dev/null @@ -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(); - } -} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/SoftUpgradePackageSignatureVerifier.java b/src/main/java/com/cisd/tms/modules/upgrade/support/SoftUpgradePackageSignatureVerifier.java new file mode 100644 index 0000000..57c0d4f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/SoftUpgradePackageSignatureVerifier.java @@ -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(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageSignatureVerifier.java b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageSignatureVerifier.java index 8878ad4..a74e7e8 100644 --- a/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageSignatureVerifier.java +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageSignatureVerifier.java @@ -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); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6008e39..def075e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 编码);主密钥初始化接口会读取该值。 diff --git a/src/main/resources/db/migration/V2__upgrade_task_add_product_type.sql b/src/main/resources/db/migration/V2__upgrade_task_add_product_type.sql new file mode 100644 index 0000000..0780a77 --- /dev/null +++ b/src/main/resources/db/migration/V2__upgrade_task_add_product_type.sql @@ -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 = ''; diff --git a/src/main/resources/db/migration/V3__upgrade_task_rename_target_component_to_task_type.sql b/src/main/resources/db/migration/V3__upgrade_task_rename_target_component_to_task_type.sql new file mode 100644 index 0000000..988c37b --- /dev/null +++ b/src/main/resources/db/migration/V3__upgrade_task_rename_target_component_to_task_type.sql @@ -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); diff --git a/src/main/resources/mapper/upgrade/UpgradeTaskMapper.xml b/src/main/resources/mapper/upgrade/UpgradeTaskMapper.xml index bf541d1..bec30a0 100644 --- a/src/main/resources/mapper/upgrade/UpgradeTaskMapper.xml +++ b/src/main/resources/mapper/upgrade/UpgradeTaskMapper.xml @@ -8,7 +8,8 @@ - + + @@ -28,7 +29,7 @@ - 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 @@ -52,8 +53,8 @@ SELECT FROM t_upgrade_task - - AND target_component = #{targetComponent} + + AND task_type = #{taskType} AND status = #{status} @@ -67,8 +68,8 @@ SELECT COUNT(1) FROM t_upgrade_task - - AND target_component = #{targetComponent} + + AND task_type = #{taskType} AND status = #{status} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/UpgradeDtoContractTest.java b/src/test/java/com/cisd/tms/modules/upgrade/UpgradeDtoContractTest.java index 2f44518..4c7c0ba 100644 --- a/src/test/java/com/cisd/tms/modules/upgrade/UpgradeDtoContractTest.java +++ b/src/test/java/com/cisd/tms/modules/upgrade/UpgradeDtoContractTest.java @@ -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()); diff --git a/src/test/java/com/cisd/tms/modules/upgrade/controller/UpgradeControllerTest.java b/src/test/java/com/cisd/tms/modules/upgrade/controller/UpgradeControllerTest.java index 4692e29..28013e4 100644 --- a/src/test/java/com/cisd/tms/modules/upgrade/controller/UpgradeControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/upgrade/controller/UpgradeControllerTest.java @@ -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"))); diff --git a/src/test/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunnerTest.java b/src/test/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunnerTest.java new file mode 100644 index 0000000..ce2fca1 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunnerTest.java @@ -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 findByTaskId(String taskId) { + return Optional.empty(); + } + + @Override + public Optional findRunningTask() { + return Optional.empty(); + } + + @Override + public java.util.List 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 store = new HashMap<>(); + + @Override + public Optional 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 store = new HashMap<>(); + + @Override + public Optional findByComponentCode(String componentCode) { + return Optional.ofNullable(store.get(componentCode)); + } + + @Override + public void saveOrUpdate(DeviceSoftwareVersionEntity entity) { + store.put(entity.getComponentCode(), entity); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepositoryTest.java b/src/test/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepositoryTest.java index a2fbb59..1c7a148 100644 --- a/src/test/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepositoryTest.java +++ b/src/test/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepositoryTest.java @@ -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; } diff --git a/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java index be96441..66df8f5 100644 --- a/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java @@ -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) { } } diff --git a/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradeServiceTest.java b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradeServiceTest.java index e138e27..bbc31af 100644 --- a/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradeServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradeServiceTest.java @@ -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 findPage(String targetComponent, String status, int offset, int limit) { + public List 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 diff --git a/src/test/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifierTest.java b/src/test/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifierTest.java deleted file mode 100644 index c1d2e94..0000000 --- a/src/test/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifierTest.java +++ /dev/null @@ -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 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()); - } -} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/support/SoftUpgradePackageSignatureVerifierTest.java b/src/test/java/com/cisd/tms/modules/upgrade/support/SoftUpgradePackageSignatureVerifierTest.java new file mode 100644 index 0000000..4a9e31a --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/upgrade/support/SoftUpgradePackageSignatureVerifierTest.java @@ -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(); + } +}