资源备份流程整理
This commit is contained in:
@@ -257,9 +257,10 @@ Open:
|
||||
- `GET /api/v1/resource-backups/{taskId}/download`
|
||||
- `POST /api/v1/resource-restores/precheck`
|
||||
- `POST /api/v1/resource-restores`
|
||||
- `GET /api/v1/resource-restores/{taskId}`
|
||||
- `GET /api/v1/resource-restores/{taskId}/steps`
|
||||
- `GET /api/v1/resource-restores/{taskId}/steps/{stepNo}/log`
|
||||
- `POST /api/v1/resource-restores/{taskId}/apply`
|
||||
- `GET /api/v1/resource-restores/latest`
|
||||
|
||||
恢复任务创建接口只完成验签、解密、展开和计划生成;前端只做一次“确认恢复”,确认后先调用创建接口,返回 `READY_TO_APPLY` 后立即调用 apply 接口进入停机恢复。TMS 重启后前端可调用 latest 接口展示最近一次恢复时间、结果和失败原因。
|
||||
|
||||
前后端字段、状态枚举和联调流程见 `docs/openapi/resource-backup-restore-frontend-integration.md`。
|
||||
|
||||
@@ -269,20 +270,33 @@ Open:
|
||||
- `TMS_CONFIG`
|
||||
- `TMS_DB`
|
||||
- `CMEP_DB`
|
||||
- `ORG_DB`(标准/间参收发器以 `orgCode` 命名的机构业务库;`DIRECT` 模式不包含)
|
||||
- `CP_CONFIG`
|
||||
- `RECEIVER_LICENSE`
|
||||
- `STANDARD_CMSP_APP_CONFIG`(标准版 CMSP 应用配置,由默认 `tms.backup.file-resources` 配置匹配 `/home/cmep4i/cmsp/application-prd*.properties`)
|
||||
- `STANDARD_CMTP_APP_CONFIG`(标准版 CMTP 应用配置,由默认 `tms.backup.file-resources` 配置匹配 `/home/cmep4i/cmtp/application-prd*.properties`)
|
||||
- `USER_KEY`(从实体证书 `KeyEntity.keyIdx` 采集用户密钥备份)
|
||||
- `MQ_REPLAY`
|
||||
- 预检阶段只做包头解析、指纹匹配和验签,不提前解密完整 payload
|
||||
- 恢复任务创建时 Java 完成验签、PCIe 解密、payload 展开、用户密钥恢复和恢复路径白名单校验
|
||||
- Java 会生成 shell 友好的 `restore-files.tsv`、`restore-databases.tsv`、`restore-mq.tsv`
|
||||
- 创建恢复任务后启动 `apply-resource-backup.sh --work-dir <workDir>`,由 shell 执行停服务、文件覆盖、SQL 导入、MQ replay、起服务和健康检查
|
||||
- Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`、`restore-stop-commands.tsv`、`restore-start-commands.tsv`;停启服务命令来自 `tms.backup.restore-stop-commands` / `tms.backup.restore-start-commands`
|
||||
- 标准/间参默认恢复后会启动 TMS、CMSP/CMTP 和 nginx;现场需要启动其它软件时,只需在上述命令列表里增删配置项
|
||||
- shell 侧状态写入 `status.json`,日志写入 `restore.log`
|
||||
- 第一版资源恢复是停机恢复。创建恢复任务后当前TMS服务可能中断,前端应提示用户恢复期间页面/API可能不可用,恢复结果以后续健康检查、重新登录和 `status.json` / `restore.log` 为准。
|
||||
- `FILE` 回写 manifest 明确声明的配置/License 文件
|
||||
- 可通过 `tms.backup.file-resources` 自行增删直接复制恢复的文件类资源:
|
||||
- `mode=FILE`:单文件,`restore-path` 是目标文件路径
|
||||
- `mode=DIR`:目录递归,`restore-path` 是目标目录
|
||||
- `mode=GLOB`:通配符匹配同一目录下的一批文件,`restore-path` 是目标目录
|
||||
- `product-types` / `mq-types`:可选过滤条件;不配置表示所有产品类型或 MQ 类型都适用
|
||||
- `required=true` 时匹配不到或读取失败会导致备份失败,`required=false` 时跳过并写入 manifest
|
||||
- `DATABASE` 恢复:
|
||||
- `db/TMS.sql`
|
||||
- `db/CMEP.sql`(包内存在时)
|
||||
- `db/<orgCode>.sql`(标准/间参收发器包内存在时,导回同名机构库)
|
||||
- `TMS_DB` 备份默认通过 `tms.backup.tms-db-excluded-tables` 排除角色、授权、会话和审计表,恢复时不会覆盖新机器安全状态
|
||||
- `MQ_REPLAY` 通过 `replay-mq.sh` 消费 `mq/mq-restore-context.json`,也可用 `MQ_REPLAY_COMMAND` 委派给现场脚本
|
||||
- Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`,传递数据库连接、恢复脚本路径、MQ replay 脚本路径和健康检查配置
|
||||
|
||||
@@ -291,13 +305,17 @@ Open:
|
||||
- `tms.backup.precheck-store-dir`
|
||||
- `tms.backup.restore-task-root-dir`
|
||||
- `tms.backup.tms-script-path`
|
||||
- `tms.backup.restore-stop-commands`
|
||||
- `tms.backup.restore-start-commands`
|
||||
- `tms.backup.restore-apply-script-path`
|
||||
- `tms.backup.allowed-restore-roots`
|
||||
- `tms.backup.file-resources`
|
||||
- `tms.backup.mysqldump-path`
|
||||
- `tms.backup.mysql-path`
|
||||
- `tms.backup.restore-db-script-path`
|
||||
- `tms.backup.mq-replay-script-path`
|
||||
- `tms.backup.tms-database-name`
|
||||
- `tms.backup.tms-db-excluded-tables`
|
||||
- `tms.backup.cmep-database-name`
|
||||
- `tms.backup.health-check-url`
|
||||
- `tms.backup.health-check-timeout-seconds`
|
||||
|
||||
@@ -50,7 +50,7 @@ tms:
|
||||
mode: LOCAL
|
||||
timeout-sec: 600
|
||||
log-dir: /home/tms/tmp/tms-init-logs
|
||||
standard-home-dir: /home/cemp4i
|
||||
standard-home-dir: /home/cmep4i
|
||||
standard-db-username: root
|
||||
standard-db-password: change-me
|
||||
standard-db-apply-command: /home/tms/bin/apply_standard_db.sh
|
||||
@@ -77,3 +77,119 @@ tms:
|
||||
rabbit-vhost: /RQ
|
||||
standard-rabbit-credential-encrypt-enabled: true
|
||||
standard-rabbit-credential-secret-key: ABCDEFGHIJKLMN1234567890
|
||||
backup:
|
||||
output-dir: /home/tms/tmp/resource-backup-packages
|
||||
restore-task-root-dir: /home/tms/tmp/resource-restore
|
||||
tms-script-path: /home/tms/scripts/tms.sh
|
||||
# 资源恢复停服务命令,可按现场需要增删。
|
||||
restore-stop-commands:
|
||||
- code: STANDARD_NGINX
|
||||
command: /home/tms/bin/stop_standard_nginx.sh
|
||||
required: false
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: STANDARD_APPS
|
||||
command: /home/tms/bin/stop_standard_apps.sh
|
||||
required: false
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: TMS
|
||||
command: /home/tms/scripts/tms.sh stop
|
||||
required: false
|
||||
# 资源恢复启动服务命令。标准/间参默认启动 TMS、CMSP/CMTP 和 nginx。
|
||||
restore-start-commands:
|
||||
- code: TMS
|
||||
command: /home/tms/scripts/tms.sh start
|
||||
required: true
|
||||
- code: STANDARD_APPS
|
||||
command: /home/tms/bin/start_standard_apps.sh
|
||||
required: true
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: STANDARD_NGINX
|
||||
command: /home/tms/bin/start_standard_nginx.sh
|
||||
required: true
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
# TMS 库备份排除表。默认跳过角色、授权、会话和审计表,避免恢复时覆盖新机器安全状态。
|
||||
tms-db-excluded-tables:
|
||||
- tms_role_account
|
||||
- tms_role_ukey_binding
|
||||
- tms_auth_full_account
|
||||
- tms_auth_session
|
||||
- tms_auth_challenge
|
||||
- tms_auth_audit_log
|
||||
- tms_operation_audit_log
|
||||
allowed-restore-roots:
|
||||
- /home/tms/config
|
||||
- /home/cmep4i
|
||||
- /home/cmep4d
|
||||
- /home/tms/tmp/resource-restore
|
||||
# 直接复制恢复的文件类资源,可按现场需要增删改。
|
||||
# FILE 的 restore-path 是目标文件;DIR/GLOB 的 restore-path 是目标目录。
|
||||
file-resources:
|
||||
- code: TMS_CONFIG
|
||||
mode: DIR
|
||||
path: /home/tms/config
|
||||
restore-path: /home/tms/config
|
||||
required: true
|
||||
- code: CP_CONFIG
|
||||
mode: FILE
|
||||
path: /home/cmep4i/cpconfig.cfg
|
||||
restore-path: /home/cmep4i/cpconfig.cfg
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: true
|
||||
- code: CP_CONFIG
|
||||
mode: FILE
|
||||
path: /opt/cpconfig.cfg
|
||||
restore-path: /opt/cpconfig.cfg
|
||||
product-types:
|
||||
- DIRECT
|
||||
required: true
|
||||
- code: RECEIVER_LICENSE
|
||||
mode: FILE
|
||||
path: /home/cmep4i/cmep/license/cmep.license
|
||||
restore-path: /home/cmep4i/cmep/license/cmep.license
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: true
|
||||
- code: RECEIVER_LICENSE
|
||||
mode: FILE
|
||||
path: /home/cmep4d/cmep/license/cmep.license
|
||||
restore-path: /home/cmep4d/cmep/license/cmep.license
|
||||
product-types:
|
||||
- DIRECT
|
||||
required: true
|
||||
- code: STANDARD_CMSP_APP_CONFIG
|
||||
mode: GLOB
|
||||
path: /home/cmep4i/cmsp/application-prd*.properties
|
||||
restore-path: /home/cmep4i/cmsp
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: true
|
||||
- code: STANDARD_CMTP_APP_CONFIG
|
||||
mode: GLOB
|
||||
path: /home/cmep4i/cmtp/application-prd*.properties
|
||||
restore-path: /home/cmep4i/cmtp
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: true
|
||||
- code: CUSTOM_CERT
|
||||
mode: FILE
|
||||
path: /home/tms/config/custom-cert.pem
|
||||
restore-path: /home/tms/config/custom-cert.pem
|
||||
required: false
|
||||
- code: CUSTOM_APP_CONFIG
|
||||
mode: GLOB
|
||||
path: /home/cmep4i/cmsp/custom-*.properties
|
||||
restore-path: /home/cmep4i/cmsp
|
||||
required: false
|
||||
|
||||
@@ -0,0 +1,843 @@
|
||||
# 资源备份恢复前端对接文档
|
||||
|
||||
## 1. 对接目标
|
||||
|
||||
本文说明当前 `tms-framework` 资源备份和资源恢复接口的前端调用方式、接口顺序、页面判断规则和错误处理建议。
|
||||
|
||||
当前前端只需要支持两个主流程:
|
||||
|
||||
- 创建资源备份包并下载 `.tmsbak`
|
||||
- 上传 `.tmsbak`,预检通过后执行停机恢复,并在 TMS 重启后展示最近恢复结果
|
||||
|
||||
恢复侧已经收敛为轻量流程,不再提供恢复任务详情、恢复步骤和恢复步骤日志接口。恢复完成后的页面展示只依赖 `GET /api/v1/resource-restores/latest`。
|
||||
|
||||
## 2. 通用约定
|
||||
|
||||
### 2.1 基础路径
|
||||
|
||||
| 模块 | 基础路径 |
|
||||
| --- | --- |
|
||||
| 文件上传 | `/api/v1/files` |
|
||||
| 资源备份 | `/api/v1/resource-backups` |
|
||||
| 资源恢复 | `/api/v1/resource-restores` |
|
||||
|
||||
### 2.2 统一响应
|
||||
|
||||
除备份包下载接口外,其它接口返回 `ApiResponse<T>`:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": {},
|
||||
"timestamp": 1777459200000,
|
||||
"traceId": "optional-trace-id"
|
||||
}
|
||||
```
|
||||
|
||||
前端统一处理规则:
|
||||
|
||||
- HTTP 非 2xx:请求失败,展示 HTTP 错误或后端 `msg`
|
||||
- `success=false`:业务失败,展示 `msg`
|
||||
- `success=true`:请求成功,继续按 `data` 中的业务字段判断流程
|
||||
|
||||
### 2.3 权限和防重放
|
||||
|
||||
资源备份/恢复接口要求内部登录态,角色为 `OPS_ADMIN` 或 `SUPER_ADMIN`,认证等级为 `FULL`。
|
||||
|
||||
需要防重放头的接口:
|
||||
|
||||
- `POST /api/v1/resource-backups`
|
||||
- `POST /api/v1/resource-restores`
|
||||
- `POST /api/v1/resource-restores/{taskId}/apply`
|
||||
|
||||
请求头示例:
|
||||
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
X-Request-Timestamp: <unix秒时间戳>
|
||||
X-Request-Nonce: <唯一随机串>
|
||||
```
|
||||
|
||||
`GET` 查询类接口和 `POST /api/v1/resource-restores/precheck` 当前不需要防重放头,但仍需要登录态。
|
||||
|
||||
### 2.4 时间字段
|
||||
|
||||
时间字段前端按字符串展示即可:
|
||||
|
||||
- Java 任务时间:`2026-04-29T16:00:00`
|
||||
- shell 状态时间:`2026-04-29T16:00:00+0800`
|
||||
|
||||
## 3. 页面流程总览
|
||||
|
||||
### 3.1 备份流程
|
||||
|
||||
1. 用户点击“创建资源备份”。
|
||||
2. 前端调用 `POST /api/v1/resource-backups`。
|
||||
3. 根据返回的 `data.status` 判断:
|
||||
- `SUCCESS`:展示“下载备份包”
|
||||
- `FAILED`:展示 `errorMessage`,可查看备份步骤/日志
|
||||
- 其它状态:调用 `GET /api/v1/resource-backups/{taskId}` 轮询
|
||||
4. 用户点击“下载”,调用 `GET /api/v1/resource-backups/{taskId}/download`。
|
||||
5. 浏览器保存 `.tmsbak` 文件。
|
||||
|
||||
当前后端创建备份通常同步完成,`POST /api/v1/resource-backups` 可能直接返回 `SUCCESS` 或 `FAILED`。前端保留轮询逻辑即可。
|
||||
|
||||
### 3.2 恢复流程
|
||||
|
||||
1. 用户选择 `.tmsbak` 文件。
|
||||
2. 前端调用 `POST /api/v1/files/upload` 上传文件,拿到 `fileId`。
|
||||
3. 前端调用 `POST /api/v1/resource-restores/precheck`,传入 `fileId`。
|
||||
4. 前端展示预检信息,并按 `data.precheckPassed` 判断是否允许下一步。
|
||||
5. `precheckPassed=true` 时,前端展示一次“确认恢复”弹窗,弹窗中明确提示会进入停机恢复。
|
||||
6. 用户确认后,前端调用 `POST /api/v1/resource-restores` 创建恢复准备任务。
|
||||
7. 创建成功且 `data.status=READY_TO_APPLY` 时,前端不要再次弹确认框,直接调用 `POST /api/v1/resource-restores/{taskId}/apply`。
|
||||
8. `apply` 接口返回后,TMS 可能很快停机、恢复、重启。前端应提示页面/API 短暂不可用。
|
||||
9. TMS 重启后,恢复页面调用 `GET /api/v1/resource-restores/latest` 展示最近恢复结果。
|
||||
|
||||
### 3.3 恢复流程判断点
|
||||
|
||||
| 阶段 | 判断字段 | 前端动作 |
|
||||
| --- | --- | --- |
|
||||
| 上传备份包 | `data.fileId` 非空 | 保存 `fileId`,进入预检 |
|
||||
| 预检 | `data.precheckPassed === true` | 允许点击“确认恢复” |
|
||||
| 预检 | `data.precheckPassed !== true` | 禁止下一步,展示 `blockingIssues` / `warnings` |
|
||||
| 创建恢复任务 | `data.status === "READY_TO_APPLY"` | 立即调用 apply,不再弹确认框 |
|
||||
| 执行恢复任务 | `data.status === "APPLYING"` | 展示“恢复中,服务可能重启” |
|
||||
| 重启后查询 latest | `data.status === "SUCCESS"` | 展示恢复成功 |
|
||||
| 重启后查询 latest | `data.status === "FAILED"` | 展示恢复失败、`phase`、`message` |
|
||||
| 重启后查询 latest | `data.status === "NONE"` | 展示暂无恢复记录 |
|
||||
|
||||
## 4. 接口明细
|
||||
|
||||
### 4.1 上传备份包
|
||||
|
||||
`POST /api/v1/files/upload`
|
||||
|
||||
请求类型:`multipart/form-data`
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `file` | `File` | 是 | 用户选择的 `.tmsbak` 文件 |
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": {
|
||||
"fileId": "FILE-20260429-000001",
|
||||
"filename": "RBKP-20260429-160000-ABCDEF.tmsbak",
|
||||
"size": 1048576
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端只需要保存 `fileId`,不要依赖服务端文件路径。
|
||||
|
||||
### 4.2 创建资源备份
|
||||
|
||||
`POST /api/v1/resource-backups`
|
||||
|
||||
需要防重放头。
|
||||
|
||||
请求体可为空,也可以传备注:
|
||||
|
||||
```json
|
||||
{
|
||||
"remark": "硬件更换前备份"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `remark` | `string` | 否 | 备份备注 |
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": {
|
||||
"taskId": "RBKP-TASK-20260429-000001",
|
||||
"backupId": "RBKP-20260429-160000-ABCDEF",
|
||||
"status": "SUCCESS",
|
||||
"createdAt": "2026-04-29T16:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端判断:
|
||||
|
||||
- `status=SUCCESS`:允许下载
|
||||
- `status=FAILED`:展示失败原因,可查询任务详情或步骤日志
|
||||
- 其它状态:使用 `taskId` 查询任务详情
|
||||
|
||||
### 4.3 查询备份任务详情
|
||||
|
||||
`GET /api/v1/resource-backups/{taskId}`
|
||||
|
||||
常用响应字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `taskId` | `string` | 备份任务号 |
|
||||
| `backupId` | `string` | 备份包身份号,恢复预检后会显示给用户确认 |
|
||||
| `status` | `ResourceTaskStatus` | 任务状态 |
|
||||
| `productType` | `ProductType` | 产品类型 |
|
||||
| `mqType` | `MqType` | MQ 类型 |
|
||||
| `orgCode` | `string` | 机构号 |
|
||||
| `packageName` | `string` | 备份包文件名 |
|
||||
| `packageSize` | `number` | 包大小,字节 |
|
||||
| `packageHash` | `string` | 包摘要 |
|
||||
| `remark` | `string` | 创建备份时传入的备注 |
|
||||
| `errorMessage` | `string` | 失败原因 |
|
||||
| `createdAt` | `string` | 创建时间 |
|
||||
| `finishedAt` | `string` | 完成时间 |
|
||||
|
||||
### 4.4 下载备份包
|
||||
|
||||
`GET /api/v1/resource-backups/{taskId}/download`
|
||||
|
||||
该接口返回文件流,不是 `ApiResponse`。
|
||||
|
||||
响应头示例:
|
||||
|
||||
```http
|
||||
Content-Type: application/octet-stream
|
||||
Content-Disposition: attachment; filename="RBKP-20260429-160000-ABCDEF.tmsbak"
|
||||
```
|
||||
|
||||
前端建议:
|
||||
|
||||
- 仅当备份任务 `status=SUCCESS` 时展示下载按钮
|
||||
- 使用浏览器下载能力或 `blob` 保存文件
|
||||
|
||||
### 4.5 查询备份步骤
|
||||
|
||||
`GET /api/v1/resource-backups/{taskId}/steps`
|
||||
|
||||
用途:备份失败时展示执行上下文。
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": [
|
||||
{
|
||||
"stepNo": 1,
|
||||
"stepCode": "PACKAGE_CREATE",
|
||||
"status": "SUCCESS",
|
||||
"message": "generated backup package RBKP-20260429-160000-ABCDEF.tmsbak",
|
||||
"startedAt": "2026-04-29T16:00:00",
|
||||
"finishedAt": "2026-04-29T16:00:03",
|
||||
"logAvailable": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 查询备份步骤日志
|
||||
|
||||
`GET /api/v1/resource-backups/{taskId}/steps/{stepNo}/log`
|
||||
|
||||
用途:备份失败或排障时展示详细日志。
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": {
|
||||
"taskId": "RBKP-TASK-20260429-000001",
|
||||
"stepNo": 1,
|
||||
"stepCode": "PACKAGE_CREATE",
|
||||
"content": "{\"packageName\":\"RBKP-20260429-160000-ABCDEF.tmsbak\"}",
|
||||
"truncated": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.7 恢复预检
|
||||
|
||||
`POST /api/v1/resource-restores/precheck`
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"fileId": "FILE-20260429-000001"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `fileId` | `string` | 是 | 上传接口返回的文件 ID |
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": {
|
||||
"precheckId": "RPRE-20260429-000001",
|
||||
"backupId": "RBKP-20260429-160000-ABCDEF",
|
||||
"sourceProductType": "ENTERPRISE",
|
||||
"sourceMqType": "RABBITMQ",
|
||||
"sourceOrgCode": "AAAABBBBXXX",
|
||||
"sourceBackupTime": "2026-04-29T16:00:00",
|
||||
"targetProductType": "ENTERPRISE",
|
||||
"keysetMatched": true,
|
||||
"signatureValid": true,
|
||||
"compatible": true,
|
||||
"precheckPassed": true,
|
||||
"payloadDecrypted": false,
|
||||
"warnings": [],
|
||||
"blockingIssues": [],
|
||||
"restorePlan": ["JAVA_EXTRACT", "SHELL_APPLY"],
|
||||
"restoreScopeSummary": {
|
||||
"resourceCount": 6,
|
||||
"productType": "ENTERPRISE",
|
||||
"mqType": "RABBITMQ",
|
||||
"orgCode": "AAAABBBBXXX"
|
||||
},
|
||||
"expiresAt": "2026-04-29T16:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端判断:
|
||||
|
||||
- 下一步只看 `precheckPassed`
|
||||
- `precheckPassed=true`:允许创建恢复任务
|
||||
- `precheckPassed=false`:禁止下一步,展示 `blockingIssues`;如只有 `warnings`,按页面需要提示用户
|
||||
|
||||
说明:
|
||||
|
||||
- `precheckPassed` 是后端聚合结果,当前等价于 `keysetMatched=true`、`signatureValid=true`、`compatible=true` 且 `blockingIssues` 为空
|
||||
- `payloadDecrypted=false` 是正常值,预检阶段不解密完整 payload
|
||||
- 真正解密、展开 payload、路径白名单校验发生在创建恢复任务阶段
|
||||
|
||||
### 4.8 创建恢复任务
|
||||
|
||||
`POST /api/v1/resource-restores`
|
||||
|
||||
需要防重放头。
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"precheckId": "RPRE-20260429-000001",
|
||||
"confirmBackupId": "RBKP-20260429-160000-ABCDEF",
|
||||
"deployMode": "DUAL",
|
||||
"nodes": {
|
||||
"node01Ip": "192.168.1.11",
|
||||
"node02Ip": "192.168.1.12"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `precheckId` | `string` | 是 | 预检接口返回的预检号 |
|
||||
| `confirmBackupId` | `string` | 是 | 用户确认的备份号,必须等于预检响应 `backupId` |
|
||||
| `deployMode` | `DeployMode` | 是 | `SINGLE`、`DUAL`、`QUAD` |
|
||||
| `nodes.node01Ip` | `string` | 是 | 节点 1 IPv4 |
|
||||
| `nodes.node02Ip` | `string` | `DUAL` / `QUAD` 必填 | 节点 2 IPv4 |
|
||||
| `nodes.node03Ip` | `string` | `QUAD` 必填 | 节点 3 IPv4 |
|
||||
| `nodes.node04Ip` | `string` | `QUAD` 必填 | 节点 4 IPv4 |
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": {
|
||||
"taskId": "RRST-TASK-20260429-000001",
|
||||
"status": "READY_TO_APPLY",
|
||||
"createdAt": "2026-04-29T16:05:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端判断:
|
||||
|
||||
- `status=READY_TO_APPLY`:立即调用 apply,不再弹确认框
|
||||
- 其它状态:展示异常提示,不调用 apply
|
||||
|
||||
后端在该阶段会完成验签、解密、payload 展开、用户密钥恢复、路径白名单校验和恢复计划文件生成,但不会停止 TMS。
|
||||
|
||||
### 4.9 执行恢复任务
|
||||
|
||||
`POST /api/v1/resource-restores/{taskId}/apply`
|
||||
|
||||
需要防重放头。
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": {
|
||||
"taskId": "RRST-TASK-20260429-000001",
|
||||
"status": "APPLYING",
|
||||
"createdAt": "2026-04-29T16:05:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端判断:
|
||||
|
||||
- `status=APPLYING`:提示用户恢复执行中,页面和接口可能短暂不可用
|
||||
- 之后不要继续轮询恢复步骤,等待 TMS 重启后查 latest
|
||||
|
||||
后端在该阶段会启动后台脚本,执行停服务、文件覆盖、数据库导入、MQ replay、启动服务和健康检查。
|
||||
|
||||
### 4.10 查询最近恢复结果
|
||||
|
||||
`GET /api/v1/resource-restores/latest`
|
||||
|
||||
用途:TMS 重启后,恢复页面展示最近一次恢复结果。
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"msg": "成功",
|
||||
"data": {
|
||||
"taskId": "RRST-TASK-20260429-000001",
|
||||
"backupId": "RBKP-20260429-160000-ABCDEF",
|
||||
"status": "FAILED",
|
||||
"phase": "DATABASE",
|
||||
"message": "数据库恢复失败",
|
||||
"createdAt": "2026-04-29T16:05:00",
|
||||
"finishedAt": "2026-04-29T16:12:00+0800"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
前端判断:
|
||||
|
||||
- `status=SUCCESS`:展示最近恢复成功和 `finishedAt`
|
||||
- `status=FAILED`:展示恢复失败、`phase`、`message`
|
||||
- `status=APPLYING` 或 `RUNNING`:展示恢复仍在执行或等待刷新
|
||||
- `status=NONE`:展示暂无恢复记录
|
||||
|
||||
## 5. 状态和枚举
|
||||
|
||||
### 5.1 ProductType
|
||||
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
| `ENTERPRISE` | 企参模式 |
|
||||
| `INDIRECT` | 间参模式 |
|
||||
| `DIRECT` | 直参模式 |
|
||||
| `UNKNOWN` | 后端无法识别或未配置 |
|
||||
|
||||
### 5.2 MqType
|
||||
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
| `RABBITMQ` | 标准 RabbitMQ |
|
||||
| `TLQ` | TLQ |
|
||||
| `RABBITMQ_TLQ` | RabbitMQ + TLQ |
|
||||
| `RABBITMQ_CFMQ` | RabbitMQ + CFMQ |
|
||||
|
||||
### 5.3 DeployMode
|
||||
|
||||
| 值 | 说明 | 节点要求 |
|
||||
| --- | --- | --- |
|
||||
| `SINGLE` | 单节点部署 | `node01Ip` |
|
||||
| `DUAL` | 双节点部署 | `node01Ip`、`node02Ip` |
|
||||
| `QUAD` | 四节点部署 | `node01Ip`、`node02Ip`、`node03Ip`、`node04Ip` |
|
||||
|
||||
第一版中,节点参数只写入恢复上下文用于审计和排障,不自动改写恢复后的配置文件。
|
||||
|
||||
### 5.4 ResourceTaskStatus
|
||||
|
||||
| 值 | 前端文案建议 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `RUNNING` | 执行中 | 备份或 shell 恢复执行中 |
|
||||
| `SUCCESS` | 成功 | 任务成功 |
|
||||
| `FAILED` | 失败 | 任务失败 |
|
||||
| `READY_TO_APPLY` | 待停机恢复 | 恢复任务已准备好,前端可继续调用 apply |
|
||||
| `APPLYING` | 恢复执行中 | 恢复脚本已启动或即将启动 |
|
||||
| `NONE` | 暂无记录 | 仅 latest 接口可能返回 |
|
||||
|
||||
其它预留状态前端可以兜底展示原值。
|
||||
|
||||
### 5.5 RestorePhase
|
||||
|
||||
| 值 | 说明 |
|
||||
| --- | --- |
|
||||
| `APPLY_DELAY` | 后台脚本启动后短暂等待 |
|
||||
| `SERVICE_STOP` | 停止服务 |
|
||||
| `FILE` | 恢复文件 |
|
||||
| `DATABASE` | 导入数据库 |
|
||||
| `MQ_REPLAY` | 重放 MQ 配置 |
|
||||
| `SERVICE_START` | 启动服务 |
|
||||
| `HEALTH_CHECK` | 健康检查 |
|
||||
| `DONE` | 完成 |
|
||||
|
||||
### 5.6 资源范围说明
|
||||
|
||||
当前备份/恢复资源包括:
|
||||
|
||||
- `TMS_CONFIG`
|
||||
- `TMS_DB`
|
||||
- `CMEP_DB`
|
||||
- `ORG_DB`
|
||||
- `CP_CONFIG`
|
||||
- `RECEIVER_LICENSE`
|
||||
- `STANDARD_CMSP_APP_CONFIG`
|
||||
- `STANDARD_CMTP_APP_CONFIG`
|
||||
- `USER_KEY`
|
||||
- `MQ_REPLAY`
|
||||
|
||||
`TMS_DB` 默认排除 `tms.backup.tms-db-excluded-tables` 中声明的表。默认排除角色、授权、会话和审计表,避免恢复时覆盖新机器安全状态。
|
||||
|
||||
## 6. TypeScript 类型建议
|
||||
|
||||
```ts
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
timestamp?: number;
|
||||
traceId?: string;
|
||||
}
|
||||
|
||||
export type ProductType = 'ENTERPRISE' | 'INDIRECT' | 'DIRECT' | 'UNKNOWN';
|
||||
export type MqType = 'RABBITMQ' | 'TLQ' | 'RABBITMQ_TLQ' | 'RABBITMQ_CFMQ';
|
||||
export type DeployMode = 'SINGLE' | 'DUAL' | 'QUAD';
|
||||
|
||||
export type ResourceTaskStatus =
|
||||
| 'RUNNING'
|
||||
| 'SUCCESS'
|
||||
| 'FAILED'
|
||||
| 'READY_TO_APPLY'
|
||||
| 'APPLYING'
|
||||
| 'NONE'
|
||||
| string;
|
||||
|
||||
export interface CreateResourceBackupRequest {
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface CreateResourceBackupResponse {
|
||||
taskId: string;
|
||||
backupId?: string;
|
||||
status: ResourceTaskStatus;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface ResourceBackupTaskDetailResponse {
|
||||
taskId: string;
|
||||
backupId?: string;
|
||||
status: ResourceTaskStatus;
|
||||
productType?: ProductType;
|
||||
mqType?: MqType;
|
||||
orgCode?: string;
|
||||
packageName?: string;
|
||||
packageSize?: number;
|
||||
packageHash?: string;
|
||||
remark?: string;
|
||||
errorMessage?: string;
|
||||
createdAt?: string;
|
||||
finishedAt?: string;
|
||||
}
|
||||
|
||||
export interface ResourceRestorePrecheckRequest {
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
export interface ResourceRestorePrecheckResponse {
|
||||
precheckId: string;
|
||||
backupId: string;
|
||||
sourceProductType?: ProductType;
|
||||
sourceMqType?: MqType;
|
||||
sourceOrgCode?: string;
|
||||
sourceBackupTime?: string;
|
||||
targetProductType?: ProductType;
|
||||
keysetMatched: boolean;
|
||||
signatureValid: boolean;
|
||||
compatible: boolean;
|
||||
precheckPassed: boolean;
|
||||
payloadDecrypted: boolean;
|
||||
warnings: string[];
|
||||
blockingIssues: string[];
|
||||
restorePlan: string[];
|
||||
restoreScopeSummary?: unknown;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface CreateResourceRestoreRequest {
|
||||
precheckId: string;
|
||||
confirmBackupId: string;
|
||||
deployMode: DeployMode;
|
||||
nodes: {
|
||||
node01Ip: string;
|
||||
node02Ip?: string;
|
||||
node03Ip?: string;
|
||||
node04Ip?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateResourceRestoreResponse {
|
||||
taskId: string;
|
||||
status: ResourceTaskStatus;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface ResourceRestoreLatestResponse {
|
||||
taskId?: string;
|
||||
backupId?: string;
|
||||
status: ResourceTaskStatus;
|
||||
phase?: string;
|
||||
message?: string;
|
||||
createdAt?: string;
|
||||
finishedAt?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Vue 3 对接示例
|
||||
|
||||
以下示例按 Vue 3 Composition API 编写。`request`、`message`、`confirmRestore`、`downloadByUrl` 可替换为项目内已有的 axios 封装、弹窗组件和下载工具。
|
||||
|
||||
### 7.1 防重放头
|
||||
|
||||
```ts
|
||||
function replayHeaders() {
|
||||
return {
|
||||
'X-Request-Timestamp': String(Math.floor(Date.now() / 1000)),
|
||||
'X-Request-Nonce': crypto.randomUUID()
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 备份 composable
|
||||
|
||||
```ts
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export function useResourceBackup() {
|
||||
const backupTask = ref<ResourceBackupTaskDetailResponse | CreateResourceBackupResponse>();
|
||||
const backupLoading = ref(false);
|
||||
|
||||
const canDownload = computed(() => backupTask.value?.status === 'SUCCESS');
|
||||
const backupFailed = computed(() => backupTask.value?.status === 'FAILED');
|
||||
|
||||
async function createBackup(remark?: string) {
|
||||
backupLoading.value = true;
|
||||
try {
|
||||
const res = await request.post<ApiResponse<CreateResourceBackupResponse>>(
|
||||
'/api/v1/resource-backups',
|
||||
remark ? { remark } : {},
|
||||
{ headers: replayHeaders() }
|
||||
);
|
||||
if (!res.data.success) throw new Error(res.data.msg);
|
||||
|
||||
backupTask.value = res.data.data;
|
||||
if (!['SUCCESS', 'FAILED'].includes(res.data.data.status)) {
|
||||
await refreshBackup(res.data.data.taskId);
|
||||
}
|
||||
} finally {
|
||||
backupLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshBackup(taskId: string) {
|
||||
const res = await request.get<ApiResponse<ResourceBackupTaskDetailResponse>>(
|
||||
`/api/v1/resource-backups/${taskId}`
|
||||
);
|
||||
if (!res.data.success) throw new Error(res.data.msg);
|
||||
backupTask.value = res.data.data;
|
||||
}
|
||||
|
||||
function downloadBackup() {
|
||||
if (!backupTask.value || backupTask.value.status !== 'SUCCESS') return;
|
||||
downloadByUrl(`/api/v1/resource-backups/${backupTask.value.taskId}/download`);
|
||||
}
|
||||
|
||||
return {
|
||||
backupTask,
|
||||
backupLoading,
|
||||
canDownload,
|
||||
backupFailed,
|
||||
createBackup,
|
||||
refreshBackup,
|
||||
downloadBackup
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
页面判断:
|
||||
|
||||
- `canDownload=true` 时展示“下载备份包”。
|
||||
- `backupFailed=true` 时展示 `backupTask.errorMessage`,并允许进入备份步骤/日志排查。
|
||||
- 备份侧可以继续保留任务详情、步骤、日志查询。
|
||||
|
||||
### 7.3 恢复 composable
|
||||
|
||||
```ts
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export function useResourceRestore() {
|
||||
const selectedFile = ref<File>();
|
||||
const precheckResult = ref<ResourceRestorePrecheckResponse>();
|
||||
const latestRestore = ref<ResourceRestoreLatestResponse>();
|
||||
const restoreLoading = ref(false);
|
||||
|
||||
const canCreateRestore = computed(() => precheckResult.value?.precheckPassed === true);
|
||||
const precheckBlocked = computed(() => precheckResult.value?.precheckPassed === false);
|
||||
|
||||
async function uploadAndPrecheck(file: File) {
|
||||
selectedFile.value = file;
|
||||
restoreLoading.value = true;
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
|
||||
const upload = await request.post<ApiResponse<{ fileId: string }>>(
|
||||
'/api/v1/files/upload',
|
||||
form
|
||||
);
|
||||
if (!upload.data.success) throw new Error(upload.data.msg);
|
||||
|
||||
const precheck = await request.post<ApiResponse<ResourceRestorePrecheckResponse>>(
|
||||
'/api/v1/resource-restores/precheck',
|
||||
{ fileId: upload.data.data.fileId }
|
||||
);
|
||||
if (!precheck.data.success) throw new Error(precheck.data.msg);
|
||||
|
||||
precheckResult.value = precheck.data.data;
|
||||
} finally {
|
||||
restoreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmAndRestore(deployMode: DeployMode, nodes: CreateResourceRestoreRequest['nodes']) {
|
||||
if (!precheckResult.value || !canCreateRestore.value) return;
|
||||
|
||||
await confirmRestore({
|
||||
title: '确认恢复资源',
|
||||
message: `确认使用备份包 ${precheckResult.value.backupId} 进行恢复?恢复过程中服务会停启,页面可能短暂不可访问。`
|
||||
});
|
||||
|
||||
restoreLoading.value = true;
|
||||
try {
|
||||
const created = await request.post<ApiResponse<CreateResourceRestoreResponse>>(
|
||||
'/api/v1/resource-restores',
|
||||
{
|
||||
precheckId: precheckResult.value.precheckId,
|
||||
confirmBackupId: precheckResult.value.backupId,
|
||||
deployMode,
|
||||
nodes
|
||||
},
|
||||
{ headers: replayHeaders() }
|
||||
);
|
||||
if (!created.data.success) throw new Error(created.data.msg);
|
||||
if (created.data.data.status !== 'READY_TO_APPLY') {
|
||||
throw new Error('恢复任务状态异常');
|
||||
}
|
||||
|
||||
const applied = await request.post<ApiResponse<CreateResourceRestoreResponse>>(
|
||||
`/api/v1/resource-restores/${created.data.data.taskId}/apply`,
|
||||
{},
|
||||
{ headers: replayHeaders() }
|
||||
);
|
||||
if (!applied.data.success) throw new Error(applied.data.msg);
|
||||
|
||||
message.info('恢复已开始,TMS 将停机并重启。重启后请返回恢复页面查看最近恢复结果。');
|
||||
} finally {
|
||||
restoreLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLatestRestore() {
|
||||
const res = await request.get<ApiResponse<ResourceRestoreLatestResponse>>(
|
||||
'/api/v1/resource-restores/latest'
|
||||
);
|
||||
if (!res.data.success) throw new Error(res.data.msg);
|
||||
latestRestore.value = res.data.data;
|
||||
}
|
||||
|
||||
return {
|
||||
selectedFile,
|
||||
precheckResult,
|
||||
latestRestore,
|
||||
restoreLoading,
|
||||
canCreateRestore,
|
||||
precheckBlocked,
|
||||
uploadAndPrecheck,
|
||||
confirmAndRestore,
|
||||
loadLatestRestore
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
页面判断:
|
||||
|
||||
- 文件选择后调用 `uploadAndPrecheck(file)`。
|
||||
- “确认恢复”按钮只在 `canCreateRestore=true` 时启用。
|
||||
- `precheckBlocked=true` 时展示 `blockingIssues`,不要允许用户继续。
|
||||
- `confirmAndRestore` 内部只弹一次确认;用户确认后按顺序调用创建恢复任务和 apply。
|
||||
- apply 返回后不要轮询恢复任务详情、恢复步骤或恢复日志。
|
||||
|
||||
### 7.4 重启后最近恢复结果展示
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
|
||||
const { latestRestore, loadLatestRestore } = useResourceRestore();
|
||||
|
||||
onMounted(loadLatestRestore);
|
||||
|
||||
const latestText = computed(() => {
|
||||
const latest = latestRestore.value;
|
||||
if (!latest || latest.status === 'NONE') return '暂无资源恢复记录';
|
||||
if (latest.status === 'SUCCESS') return `最近恢复成功:${latest.finishedAt ?? ''}`;
|
||||
if (latest.status === 'FAILED') {
|
||||
return `最近恢复失败:${latest.phase ?? ''} ${latest.message ?? ''}`;
|
||||
}
|
||||
return `最近恢复状态:${latest.status}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>{{ latestText }}</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 8. 交互和展示建议
|
||||
|
||||
- 备份创建按钮点击后立即禁用,避免重复提交。
|
||||
- 恢复预检通过后,必须展示备份号 `backupId`,让用户确认正在恢复哪个包。
|
||||
- `precheckPassed=false` 时不要允许创建恢复任务。
|
||||
- 恢复流程只做一次用户确认;确认后前端按顺序调用创建恢复任务和 apply。
|
||||
- apply 返回后不要继续请求恢复步骤接口;恢复侧没有步骤接口。
|
||||
- TMS 重启后只调用 latest 展示最近恢复结果。
|
||||
- `payloadDecrypted=false` 不表示失败,它表示预检阶段没有解密完整 payload。
|
||||
- `TMS_DB` 默认不包含角色、授权、会话和审计表,因此恢复不会覆盖新机器已有登录授权状态。
|
||||
- 服务端路径类字段不展示给普通用户,只用于运维排查。
|
||||
@@ -30,6 +30,8 @@ FILES_PLAN="${WORK_DIR}/restore-files.tsv"
|
||||
DATABASES_PLAN="${WORK_DIR}/restore-databases.tsv"
|
||||
MQ_PLAN="${WORK_DIR}/restore-mq.tsv"
|
||||
ENV_FILE="${WORK_DIR}/restore-env.sh"
|
||||
STOP_COMMANDS_PLAN="${WORK_DIR}/restore-stop-commands.tsv"
|
||||
START_COMMANDS_PLAN="${WORK_DIR}/restore-start-commands.tsv"
|
||||
|
||||
json_escape() {
|
||||
local value="$1"
|
||||
@@ -131,6 +133,26 @@ replay_mq() {
|
||||
done < "${MQ_PLAN}"
|
||||
}
|
||||
|
||||
run_service_commands() {
|
||||
local plan_file="$1"
|
||||
local phase="$2"
|
||||
[[ -f "${plan_file}" ]] || return 1
|
||||
[[ -s "${plan_file}" ]] || return 1
|
||||
while IFS=$'\t' read -r code required command || [[ -n "${code:-}" ]]; do
|
||||
[[ -n "${code:-}" ]] || continue
|
||||
[[ -n "${command:-}" ]] || fail "service command is empty: ${code}" "${phase}"
|
||||
log "${phase} ${code}: ${command}"
|
||||
if bash -lc "${command}" >> "${LOG_FILE}" 2>&1; then
|
||||
log "${phase} ${code}: success"
|
||||
elif [[ "${required}" == "true" ]]; then
|
||||
fail "${phase} command failed: ${code}" "${phase}"
|
||||
else
|
||||
log "[WARN] ${phase} ${code}: failed but ignored"
|
||||
fi
|
||||
done < "${plan_file}"
|
||||
return 0
|
||||
}
|
||||
|
||||
health_check() {
|
||||
local url="${HEALTH_CHECK_URL:-}"
|
||||
[[ -n "${url}" ]] || {
|
||||
@@ -159,11 +181,19 @@ fi
|
||||
|
||||
[[ -f "${MANIFEST}" ]] || fail "payload manifest not found" "PRECHECK"
|
||||
|
||||
delay_seconds="${RESTORE_APPLY_DELAY_SECONDS:-3}"
|
||||
if [[ "${delay_seconds}" =~ ^[0-9]+$ ]] && [[ "${delay_seconds}" -gt 0 ]]; then
|
||||
write_status "RUNNING" "APPLY_DELAY" "restore apply will start"
|
||||
log "APPLY_DELAY ${delay_seconds}s"
|
||||
sleep "${delay_seconds}"
|
||||
fi
|
||||
|
||||
# 从这里开始进入停机恢复窗口。TMS_SCRIPT_PATH stop/start 允许失败后继续,
|
||||
# 因为某些部署环境可能由 systemd/外部平台接管服务状态,但文件/数据库/MQ 失败必须中断。
|
||||
write_status "RUNNING" "SERVICE_STOP" "stopping services"
|
||||
log "SERVICE_STOP"
|
||||
if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then
|
||||
if ! run_service_commands "${STOP_COMMANDS_PLAN}" "SERVICE_STOP" \
|
||||
&& [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then
|
||||
"${TMS_SCRIPT_PATH}" stop >> "${LOG_FILE}" 2>&1 || true
|
||||
fi
|
||||
|
||||
@@ -181,7 +211,8 @@ replay_mq
|
||||
|
||||
write_status "RUNNING" "SERVICE_START" "starting services"
|
||||
log "SERVICE_START"
|
||||
if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then
|
||||
if ! run_service_commands "${START_COMMANDS_PLAN}" "SERVICE_START" \
|
||||
&& [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then
|
||||
"${TMS_SCRIPT_PATH}" start >> "${LOG_FILE}" 2>&1 || true
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.config;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.support.BackupResourceKind;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredRestoreCommand;
|
||||
import java.util.List;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@@ -17,12 +17,21 @@ public class ResourceBackupProperties {
|
||||
private String restoreDbScriptPath = "/home/tms/bin/resource-restore/restore-db.sh";
|
||||
private String mqReplayScriptPath = "/home/tms/bin/resource-restore/replay-mq.sh";
|
||||
private String tmsDatabaseName = "";
|
||||
private List<String> tmsDbExcludedTables = List.of(
|
||||
"tms_role_account",
|
||||
"tms_role_ukey_binding",
|
||||
"tms_auth_full_account",
|
||||
"tms_auth_session",
|
||||
"tms_auth_challenge",
|
||||
"tms_auth_audit_log",
|
||||
"tms_operation_audit_log"
|
||||
);
|
||||
private String cmepDatabaseName = "CMEP";
|
||||
private String healthCheckUrl = "http://127.0.0.1:8080/actuator/health";
|
||||
private int healthCheckTimeoutSeconds = 5;
|
||||
private int storageEncKeyIndex = 1;
|
||||
private int authenticitySignKeyIndex = 1;
|
||||
private String restoreApplyScriptPath = "/home/tms/bin/resource-restore/apply-resource-backup.sh";
|
||||
private List<ConfiguredRestoreCommand> restoreStopCommands = List.of();
|
||||
private List<ConfiguredRestoreCommand> restoreStartCommands = List.of();
|
||||
private List<String> allowedRestoreRoots = List.of(
|
||||
"/home/tms/config",
|
||||
"/home/cmep4i",
|
||||
@@ -103,6 +112,14 @@ public class ResourceBackupProperties {
|
||||
this.tmsDatabaseName = tmsDatabaseName;
|
||||
}
|
||||
|
||||
public List<String> getTmsDbExcludedTables() {
|
||||
return tmsDbExcludedTables;
|
||||
}
|
||||
|
||||
public void setTmsDbExcludedTables(List<String> tmsDbExcludedTables) {
|
||||
this.tmsDbExcludedTables = tmsDbExcludedTables;
|
||||
}
|
||||
|
||||
public String getCmepDatabaseName() {
|
||||
return cmepDatabaseName;
|
||||
}
|
||||
@@ -127,22 +144,6 @@ public class ResourceBackupProperties {
|
||||
this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds;
|
||||
}
|
||||
|
||||
public int getStorageEncKeyIndex() {
|
||||
return storageEncKeyIndex;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyIndex(int storageEncKeyIndex) {
|
||||
this.storageEncKeyIndex = storageEncKeyIndex;
|
||||
}
|
||||
|
||||
public int getAuthenticitySignKeyIndex() {
|
||||
return authenticitySignKeyIndex;
|
||||
}
|
||||
|
||||
public void setAuthenticitySignKeyIndex(int authenticitySignKeyIndex) {
|
||||
this.authenticitySignKeyIndex = authenticitySignKeyIndex;
|
||||
}
|
||||
|
||||
public String getRestoreApplyScriptPath() {
|
||||
return restoreApplyScriptPath;
|
||||
}
|
||||
@@ -151,6 +152,22 @@ public class ResourceBackupProperties {
|
||||
this.restoreApplyScriptPath = restoreApplyScriptPath;
|
||||
}
|
||||
|
||||
public List<ConfiguredRestoreCommand> getRestoreStopCommands() {
|
||||
return restoreStopCommands;
|
||||
}
|
||||
|
||||
public void setRestoreStopCommands(List<ConfiguredRestoreCommand> restoreStopCommands) {
|
||||
this.restoreStopCommands = restoreStopCommands;
|
||||
}
|
||||
|
||||
public List<ConfiguredRestoreCommand> getRestoreStartCommands() {
|
||||
return restoreStartCommands;
|
||||
}
|
||||
|
||||
public void setRestoreStartCommands(List<ConfiguredRestoreCommand> restoreStartCommands) {
|
||||
this.restoreStartCommands = restoreStartCommands;
|
||||
}
|
||||
|
||||
public List<String> getAllowedRestoreRoots() {
|
||||
return allowedRestoreRoots;
|
||||
}
|
||||
@@ -167,33 +184,4 @@ public class ResourceBackupProperties {
|
||||
this.fileResources = fileResources;
|
||||
}
|
||||
|
||||
public List<ConfiguredFileBackupResource> getDefaultFileResources() {
|
||||
ConfiguredFileBackupResource tmsConfig = fileResource(
|
||||
"TMS_CONFIG",
|
||||
"DIR",
|
||||
"/home/tms/config",
|
||||
"/home/tms/config",
|
||||
true
|
||||
);
|
||||
ConfiguredFileBackupResource cpConfig = fileResource("CP_CONFIG", "FILE", "", "", true);
|
||||
ConfiguredFileBackupResource receiverLicense = fileResource("RECEIVER_LICENSE", "FILE", "", "", true);
|
||||
return List.of(tmsConfig, cpConfig, receiverLicense);
|
||||
}
|
||||
|
||||
private static ConfiguredFileBackupResource fileResource(
|
||||
String code,
|
||||
String mode,
|
||||
String path,
|
||||
String restorePath,
|
||||
boolean required
|
||||
) {
|
||||
ConfiguredFileBackupResource resource = new ConfiguredFileBackupResource();
|
||||
resource.setCode(code);
|
||||
resource.setKind(BackupResourceKind.FILE.name());
|
||||
resource.setMode(mode);
|
||||
resource.setPath(path);
|
||||
resource.setRestorePath(restorePath);
|
||||
resource.setRequired(required);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public class ResourceBackupController {
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
|
||||
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.BACKUP, summary = "创建资源备份任务")
|
||||
@Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。")
|
||||
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody CreateResourceBackupRequest request) {
|
||||
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody(required = false) CreateResourceBackupRequest request) {
|
||||
return ApiResponse.success(resourceBackupService.createBackupTask(request));
|
||||
}
|
||||
|
||||
|
||||
+12
-27
@@ -7,10 +7,8 @@ import com.cisd.tms.modules.auth.security.RequireInternalAuth;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
@@ -22,7 +20,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -34,7 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource-restores")
|
||||
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、详情和步骤日志接口")
|
||||
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、执行和最近结果接口")
|
||||
@RequiredArgsConstructor
|
||||
public class ResourceRestoreController {
|
||||
|
||||
@@ -59,35 +56,23 @@ public class ResourceRestoreController {
|
||||
return ApiResponse.success(resourceRestoreService.createRestoreTask(request));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}")
|
||||
@PostMapping("/{taskId}/apply")
|
||||
@ReplayProtected
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
|
||||
@Operation(summary = "查询资源恢复任务详情", description = "返回资源恢复任务详情和当前控制阶段。")
|
||||
public ApiResponse<ResourceRestoreTaskDetailResponse> taskDetail(
|
||||
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.RECOVER, summary = "执行资源恢复任务")
|
||||
@Operation(summary = "执行资源恢复任务", description = "用户确认停机恢复后,启动后台恢复脚本。")
|
||||
public ApiResponse<CreateResourceRestoreResponse> applyRestoreTask(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTask(taskId));
|
||||
return ApiResponse.success(resourceRestoreService.applyRestoreTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps")
|
||||
@GetMapping("/latest")
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
|
||||
@Operation(summary = "查询资源恢复任务步骤", description = "返回资源恢复任务步骤列表。")
|
||||
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskSteps(taskId));
|
||||
@Operation(summary = "查询最近资源恢复结果", description = "TMS 重启后用于恢复页面展示最近一次恢复时间、结果和失败原因。")
|
||||
public ApiResponse<ResourceRestoreLatestResponse> latestRestore() {
|
||||
return ApiResponse.success(resourceTaskQueryService.getLatestRestore());
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps/{stepNo}/log")
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
|
||||
@Operation(summary = "查询资源恢复步骤日志", description = "返回资源恢复步骤日志。")
|
||||
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId,
|
||||
@Parameter(description = "步骤序号")
|
||||
@PathVariable int stepNo
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskStepLog(taskId, stepNo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
public final class ResourceCryptoKeyConstants {
|
||||
|
||||
public static final int STORAGE_ENC_KEY_INDEX = 1;
|
||||
public static final int AUTHENTICITY_SIGN_KEY_INDEX = 1;
|
||||
public static final String STORAGE_ENC_KEY_FINGERPRINT = "USER_KEY_ENC:1";
|
||||
public static final String AUTHENTICITY_KEY_FINGERPRINT = "USER_KEY_SIGN:1";
|
||||
|
||||
private ResourceCryptoKeyConstants() {
|
||||
}
|
||||
}
|
||||
+12
-26
@@ -1,12 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.*;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
@@ -24,7 +19,6 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
@@ -32,12 +26,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceEnvelopeCryptoServiceImpl(
|
||||
PcieCryptoService pcieCryptoService,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
public ResourceEnvelopeCryptoServiceImpl(PcieCryptoService pcieCryptoService) {
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,7 +45,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
|
||||
// 用户密钥只负责封装随机 DEK;真正的大 payload 仍走对称加密,避免直接把大文件送进 ECC 接口。
|
||||
EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest();
|
||||
encryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
encryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX);
|
||||
encryptRequest.setData(sm4Key);
|
||||
BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest);
|
||||
|
||||
@@ -70,8 +60,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
ResourceSealedPayload sealedPayload = new ResourceSealedPayload();
|
||||
sealedPayload.setCipherBytes(cipher.doFinal(payloadBytes));
|
||||
sealedPayload.setEnvelope(envelope);
|
||||
sealedPayload.setStorageEncKeyFingerprint(buildFingerprint("USER_KEY_ENC", resourceBackupProperties.getStorageEncKeyIndex()));
|
||||
sealedPayload.setAuthenticityKeyFingerprint(buildFingerprint("USER_KEY_SIGN", resourceBackupProperties.getAuthenticitySignKeyIndex()));
|
||||
sealedPayload.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
|
||||
sealedPayload.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
|
||||
return sealedPayload;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("资源备份载荷加封失败", ex);
|
||||
@@ -85,11 +75,11 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
if (dataToSign == null) {
|
||||
throw new IllegalArgumentException("dataToSign不能为空");
|
||||
}
|
||||
EccInternalSignRequest request = new EccInternalSignRequest();
|
||||
request.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
// 内部签名接口这里统一签 SM3 摘要,避免把整段原文直接交给密码卡并固定后续验签输入语义。
|
||||
request.setData(sm3(dataToSign));
|
||||
return normalize(pcieCryptoService.eccInternalSign(request));
|
||||
UserKeySignRequest request = new UserKeySignRequest();
|
||||
request.setData(dataToSign);
|
||||
request.setKeyIndex(ResourceCryptoKeyConstants.AUTHENTICITY_SIGN_KEY_INDEX);
|
||||
return normalize(pcieCryptoService.userKeySignWithSm3(request));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,7 +91,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
throw new IllegalArgumentException("signature不能为空");
|
||||
}
|
||||
EccInternalVerifyRequest verifyRequest = new EccInternalVerifyRequest();
|
||||
verifyRequest.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
verifyRequest.setKeyIndex(ResourceCryptoKeyConstants.AUTHENTICITY_SIGN_KEY_INDEX);
|
||||
verifyRequest.setData(sm3(dataToVerify));
|
||||
verifyRequest.setSignature(signature);
|
||||
pcieCryptoService.eccInternalVerify(verifyRequest);
|
||||
@@ -127,7 +117,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
int dekLength = parseInt(envelope.get("dekLength"), 16);
|
||||
// 先验签,再解数字信封,再解 payload;顺序固定,避免篡改包先进入明文处理路径。
|
||||
EccInternalDecryptRequest decryptRequest = new EccInternalDecryptRequest();
|
||||
decryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
decryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX);
|
||||
decryptRequest.setCipherBlob(wrappedDek);
|
||||
decryptRequest.setOutBufferSize(dekLength);
|
||||
byte[] sm4Key = normalizeAndWipe(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest));
|
||||
@@ -193,10 +183,6 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildFingerprint(String prefix, int keyIndex) {
|
||||
return prefix + ":" + keyIndex;
|
||||
}
|
||||
|
||||
private static void wipe(byte[] bytes) {
|
||||
if (bytes != null) {
|
||||
Arrays.fill(bytes, (byte) 0);
|
||||
|
||||
+76
-19
@@ -1,19 +1,22 @@
|
||||
package com.cisd.tms.modules.backup.discovery;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
|
||||
private final InitExecutorProperties initExecutorProperties;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public ResourceDiscoveryServiceImpl(InitExecutorProperties initExecutorProperties) {
|
||||
this.initExecutorProperties = initExecutorProperties;
|
||||
@Autowired
|
||||
public ResourceDiscoveryServiceImpl(ResourceBackupProperties resourceBackupProperties) {
|
||||
this.resourceBackupProperties = resourceBackupProperties == null ? new ResourceBackupProperties() : resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,16 +33,15 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
// 资源发现只决定“应该备份什么”,不在这里触碰文件系统、数据库或密码卡。
|
||||
// 真实采集放在 ResourcePackageServiceImpl 中完成,便于统一处理必选资源缺失和包内 manifest。
|
||||
List<DiscoveredResourceItem> items = new ArrayList<>();
|
||||
// TMS 自身配置目录是恢复后服务能否按原部署参数启动的基础资源,按目录递归打包。
|
||||
items.add(file("TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), initExecutorProperties.getTmsConfigDir(), "DIR", true));
|
||||
// 收发器配置和 license 按产品形态切换路径:标准版走 cmep4i,直连版走 direct 目标路径。
|
||||
items.add(file("CP_CONFIG", resolveCpConfigPath(productType), resolveCpConfigPath(productType), "FILE", true));
|
||||
items.add(file("RECEIVER_LICENSE", resolveReceiverLicensePath(productType), resolveReceiverLicensePath(productType), "FILE", true));
|
||||
appendConfiguredFileResources(items, productType, mqType);
|
||||
// 数据库资源在包内固定成 db/*.sql,恢复脚本通过 restore-databases.tsv 再决定导入目标库名。
|
||||
items.add(database("TMS_DB", "TMS", "db/TMS.sql", true));
|
||||
if (!"DIRECT".equals(productType)) {
|
||||
// 第一版标准版迁移需要把收发器业务库一起带走,DIRECT 暂时不要求这份库。
|
||||
items.add(database("CMEP_DB", "CMEP", "db/CMEP.sql", true));
|
||||
// 标准收发器初始化还会创建一个以机构号命名的业务库;这份库和 CMEP 一样属于不可重建业务数据。
|
||||
String orgCode = requireDatabaseIdentifier(initTask.getOrgCode(), "orgCode");
|
||||
items.add(database("ORG_DB", orgCode, "db/" + orgCode + ".sql", true));
|
||||
}
|
||||
// 实体证书表记录的 keyIdx 对应密码卡内用户签名密钥;备份时会按这些索引逐个调用 backupUserKey。
|
||||
items.add(userKey(true));
|
||||
@@ -48,18 +50,35 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
return items;
|
||||
}
|
||||
|
||||
private String resolveCpConfigPath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectCpconfigTargetPath();
|
||||
private void appendConfiguredFileResources(List<DiscoveredResourceItem> items, String productType, String mqType) {
|
||||
List<ConfiguredFileBackupResource> resources = resourceBackupProperties.getFileResources();
|
||||
if (resources == null || resources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
return initExecutorProperties.getStandardCpconfigPath();
|
||||
}
|
||||
|
||||
private String resolveReceiverLicensePath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectReceiverLicenseTargetPath();
|
||||
for (ConfiguredFileBackupResource resource : resources) {
|
||||
if (!matches(resource.getProductTypes(), productType) || !matches(resource.getMqTypes(), mqType)) {
|
||||
continue;
|
||||
}
|
||||
String code = trim(resource.getCode());
|
||||
String path = trim(resource.getPath());
|
||||
if (code.isEmpty() || path.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String mode = trim(resource.getMode()).isEmpty() ? "FILE" : trim(resource.getMode()).toUpperCase();
|
||||
String restorePath = trim(resource.getRestorePath());
|
||||
if (restorePath.isEmpty()) {
|
||||
restorePath = "GLOB".equals(mode) ? globRestoreDir(path) : path;
|
||||
}
|
||||
DiscoveredResourceItem item = file(
|
||||
code,
|
||||
path,
|
||||
restorePath,
|
||||
mode,
|
||||
resource.isRequired()
|
||||
);
|
||||
removeExistingCode(items, code);
|
||||
items.add(item);
|
||||
}
|
||||
return initExecutorProperties.getStandardReceiverLicenseTargetPath();
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem file(String code, String path, String restorePath, String mode, boolean required) {
|
||||
@@ -73,6 +92,29 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
return item;
|
||||
}
|
||||
|
||||
private static String globRestoreDir(String pattern) {
|
||||
String value = trim(pattern);
|
||||
int slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
|
||||
if (slash <= 0) {
|
||||
return ".";
|
||||
}
|
||||
return value.substring(0, slash);
|
||||
}
|
||||
|
||||
private static void removeExistingCode(List<DiscoveredResourceItem> items, String code) {
|
||||
items.removeIf(item -> code.equals(item.getResourceCode()));
|
||||
}
|
||||
|
||||
private static boolean matches(List<String> configuredValues, String actualValue) {
|
||||
if (configuredValues == null || configuredValues.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String actual = upper(actualValue);
|
||||
return configuredValues.stream()
|
||||
.map(ResourceDiscoveryServiceImpl::upper)
|
||||
.anyMatch(actual::equals);
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem database(String code, String databaseName, String entryPath, boolean required) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("DATABASE");
|
||||
@@ -104,4 +146,19 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
private static String upper(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private static String requireDatabaseIdentifier(String value, String label) {
|
||||
String identifier = trim(value);
|
||||
if (identifier.isEmpty()) {
|
||||
throw new IllegalArgumentException(label + "不能为空");
|
||||
}
|
||||
if (!identifier.matches("[A-Za-z0-9_]+")) {
|
||||
throw new IllegalArgumentException(label + "数据库名无效:" + identifier);
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-23
@@ -2,34 +2,12 @@ package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "创建资源备份任务请求")
|
||||
@Schema(description = "创建资源备份任务请求")
|
||||
public class CreateResourceBackupRequest {
|
||||
|
||||
@Schema(description = "是否包含升级历史", example = "true")
|
||||
private Boolean includeUpgradeHistory;
|
||||
|
||||
@Schema(description = "是否包含审计备份", example = "false")
|
||||
private Boolean includeAuditBackups;
|
||||
|
||||
@Schema(description = "备注", example = "before hardware replacement")
|
||||
private String remark;
|
||||
|
||||
public Boolean getIncludeUpgradeHistory() {
|
||||
return includeUpgradeHistory;
|
||||
}
|
||||
|
||||
public void setIncludeUpgradeHistory(Boolean includeUpgradeHistory) {
|
||||
this.includeUpgradeHistory = includeUpgradeHistory;
|
||||
}
|
||||
|
||||
public Boolean getIncludeAuditBackups() {
|
||||
return includeAuditBackups;
|
||||
}
|
||||
|
||||
public void setIncludeAuditBackups(Boolean includeAuditBackups) {
|
||||
this.includeAuditBackups = includeAuditBackups;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
+79
-7
@@ -1,12 +1,17 @@
|
||||
package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
@Schema(description = "创建资源恢复任务请求")
|
||||
public class CreateResourceRestoreRequest {
|
||||
|
||||
private static final String IPV4_REGEX =
|
||||
"^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$";
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "预检号", example = "RPRE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String precheckId;
|
||||
@@ -15,9 +20,15 @@ public class CreateResourceRestoreRequest {
|
||||
@Schema(description = "确认的备份号", example = "RBKP-20260408-153000-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String confirmBackupId;
|
||||
|
||||
@NotNull
|
||||
@Schema(description = "是否确认接受恢复风险", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean confirmRiskAccepted;
|
||||
@NotBlank(message = "deployMode不能为空")
|
||||
@Pattern(regexp = "SINGLE|DUAL|QUAD", message = "deployMode必须为SINGLE/DUAL/QUAD")
|
||||
@Schema(description = "目标部署方式", example = "DUAL", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String deployMode;
|
||||
|
||||
@Valid
|
||||
@NotNull(message = "nodes不能为空")
|
||||
@Schema(description = "目标节点 IP 配置", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Nodes nodes;
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
@@ -35,11 +46,72 @@ public class CreateResourceRestoreRequest {
|
||||
this.confirmBackupId = confirmBackupId;
|
||||
}
|
||||
|
||||
public Boolean getConfirmRiskAccepted() {
|
||||
return confirmRiskAccepted;
|
||||
public String getDeployMode() {
|
||||
return deployMode;
|
||||
}
|
||||
|
||||
public void setConfirmRiskAccepted(Boolean confirmRiskAccepted) {
|
||||
this.confirmRiskAccepted = confirmRiskAccepted;
|
||||
public void setDeployMode(String deployMode) {
|
||||
this.deployMode = deployMode;
|
||||
}
|
||||
|
||||
public Nodes getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public void setNodes(Nodes nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
@Schema(description = "目标节点 IP 配置")
|
||||
public static class Nodes {
|
||||
|
||||
@NotBlank(message = "nodes.node01Ip不能为空")
|
||||
@Pattern(regexp = IPV4_REGEX, message = "nodes.node01Ip必须是有效IPv4地址")
|
||||
@Schema(description = "节点 1 IP", example = "192.168.1.11", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String node01Ip;
|
||||
|
||||
@Pattern(regexp = IPV4_REGEX, message = "nodes.node02Ip必须是有效IPv4地址")
|
||||
@Schema(description = "节点 2 IP", example = "192.168.1.12")
|
||||
private String node02Ip;
|
||||
|
||||
@Pattern(regexp = IPV4_REGEX, message = "nodes.node03Ip必须是有效IPv4地址")
|
||||
@Schema(description = "节点 3 IP", example = "192.168.1.13")
|
||||
private String node03Ip;
|
||||
|
||||
@Pattern(regexp = IPV4_REGEX, message = "nodes.node04Ip必须是有效IPv4地址")
|
||||
@Schema(description = "节点 4 IP", example = "192.168.1.14")
|
||||
private String node04Ip;
|
||||
|
||||
public String getNode01Ip() {
|
||||
return node01Ip;
|
||||
}
|
||||
|
||||
public void setNode01Ip(String node01Ip) {
|
||||
this.node01Ip = node01Ip;
|
||||
}
|
||||
|
||||
public String getNode02Ip() {
|
||||
return node02Ip;
|
||||
}
|
||||
|
||||
public void setNode02Ip(String node02Ip) {
|
||||
this.node02Ip = node02Ip;
|
||||
}
|
||||
|
||||
public String getNode03Ip() {
|
||||
return node03Ip;
|
||||
}
|
||||
|
||||
public void setNode03Ip(String node03Ip) {
|
||||
this.node03Ip = node03Ip;
|
||||
}
|
||||
|
||||
public String getNode04Ip() {
|
||||
return node04Ip;
|
||||
}
|
||||
|
||||
public void setNode04Ip(String node04Ip) {
|
||||
this.node04Ip = node04Ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "最近一次资源恢复结果响应")
|
||||
public class ResourceRestoreLatestResponse {
|
||||
|
||||
private String taskId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String phase;
|
||||
private String message;
|
||||
private String createdAt;
|
||||
private String finishedAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public void setPhase(String phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
+9
@@ -16,6 +16,7 @@ public class ResourceRestorePrecheckResponse {
|
||||
private Boolean keysetMatched;
|
||||
private Boolean signatureValid;
|
||||
private Boolean compatible;
|
||||
private Boolean precheckPassed;
|
||||
private Boolean payloadDecrypted;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
@@ -103,6 +104,14 @@ public class ResourceRestorePrecheckResponse {
|
||||
this.compatible = compatible;
|
||||
}
|
||||
|
||||
public Boolean getPrecheckPassed() {
|
||||
return precheckPassed;
|
||||
}
|
||||
|
||||
public void setPrecheckPassed(Boolean precheckPassed) {
|
||||
this.precheckPassed = precheckPassed;
|
||||
}
|
||||
|
||||
public Boolean getPayloadDecrypted() {
|
||||
return payloadDecrypted;
|
||||
}
|
||||
|
||||
-261
@@ -1,261 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "资源恢复任务详情响应")
|
||||
public class ResourceRestoreTaskDetailResponse {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String controlPhase;
|
||||
private String stateSource;
|
||||
private String sourceDeviceId;
|
||||
private String sourceProductType;
|
||||
private String targetProductType;
|
||||
private String packageFileId;
|
||||
private String packageName;
|
||||
private Boolean packagePathVisible;
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String lastHeartbeatAt;
|
||||
private String statusFilePath;
|
||||
private Boolean orphaned;
|
||||
private List<String> cleanupWarnings;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
private List<String> restorePlan;
|
||||
private Object consistencyReport;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private String createdBy;
|
||||
private String createdAt;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getControlPhase() {
|
||||
return controlPhase;
|
||||
}
|
||||
|
||||
public void setControlPhase(String controlPhase) {
|
||||
this.controlPhase = controlPhase;
|
||||
}
|
||||
|
||||
public String getStateSource() {
|
||||
return stateSource;
|
||||
}
|
||||
|
||||
public void setStateSource(String stateSource) {
|
||||
this.stateSource = stateSource;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getSourceProductType() {
|
||||
return sourceProductType;
|
||||
}
|
||||
|
||||
public void setSourceProductType(String sourceProductType) {
|
||||
this.sourceProductType = sourceProductType;
|
||||
}
|
||||
|
||||
public String getTargetProductType() {
|
||||
return targetProductType;
|
||||
}
|
||||
|
||||
public void setTargetProductType(String targetProductType) {
|
||||
this.targetProductType = targetProductType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
return packageFileId;
|
||||
}
|
||||
|
||||
public void setPackageFileId(String packageFileId) {
|
||||
this.packageFileId = packageFileId;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public Boolean getPackagePathVisible() {
|
||||
return packagePathVisible;
|
||||
}
|
||||
|
||||
public void setPackagePathVisible(Boolean packagePathVisible) {
|
||||
this.packagePathVisible = packagePathVisible;
|
||||
}
|
||||
|
||||
public String getCurrentStepCode() {
|
||||
return currentStepCode;
|
||||
}
|
||||
|
||||
public void setCurrentStepCode(String currentStepCode) {
|
||||
this.currentStepCode = currentStepCode;
|
||||
}
|
||||
|
||||
public Integer getCurrentStepNo() {
|
||||
return currentStepNo;
|
||||
}
|
||||
|
||||
public void setCurrentStepNo(Integer currentStepNo) {
|
||||
this.currentStepNo = currentStepNo;
|
||||
}
|
||||
|
||||
public String getLastHeartbeatAt() {
|
||||
return lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public void setLastHeartbeatAt(String lastHeartbeatAt) {
|
||||
this.lastHeartbeatAt = lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public String getStatusFilePath() {
|
||||
return statusFilePath;
|
||||
}
|
||||
|
||||
public void setStatusFilePath(String statusFilePath) {
|
||||
this.statusFilePath = statusFilePath;
|
||||
}
|
||||
|
||||
public Boolean getOrphaned() {
|
||||
return orphaned;
|
||||
}
|
||||
|
||||
public void setOrphaned(Boolean orphaned) {
|
||||
this.orphaned = orphaned;
|
||||
}
|
||||
|
||||
public List<String> getCleanupWarnings() {
|
||||
return cleanupWarnings;
|
||||
}
|
||||
|
||||
public void setCleanupWarnings(List<String> cleanupWarnings) {
|
||||
this.cleanupWarnings = cleanupWarnings;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public void setWarnings(List<String> warnings) {
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<String> getBlockingIssues() {
|
||||
return blockingIssues;
|
||||
}
|
||||
|
||||
public void setBlockingIssues(List<String> blockingIssues) {
|
||||
this.blockingIssues = blockingIssues;
|
||||
}
|
||||
|
||||
public List<String> getRestorePlan() {
|
||||
return restorePlan;
|
||||
}
|
||||
|
||||
public void setRestorePlan(List<String> restorePlan) {
|
||||
this.restorePlan = restorePlan;
|
||||
}
|
||||
|
||||
public Object getConsistencyReport() {
|
||||
return consistencyReport;
|
||||
}
|
||||
|
||||
public void setConsistencyReport(Object consistencyReport) {
|
||||
this.consistencyReport = consistencyReport;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(String startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
+20
@@ -1,5 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.dto.support;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConfiguredFileBackupResource {
|
||||
|
||||
private String code;
|
||||
@@ -7,6 +9,8 @@ public class ConfiguredFileBackupResource {
|
||||
private String mode = "FILE";
|
||||
private String path;
|
||||
private String restorePath;
|
||||
private List<String> productTypes = List.of();
|
||||
private List<String> mqTypes = List.of();
|
||||
private boolean required = true;
|
||||
|
||||
public String getCode() {
|
||||
@@ -49,6 +53,22 @@ public class ConfiguredFileBackupResource {
|
||||
this.restorePath = restorePath;
|
||||
}
|
||||
|
||||
public java.util.List<String> getProductTypes() {
|
||||
return productTypes;
|
||||
}
|
||||
|
||||
public void setProductTypes(java.util.List<String> productTypes) {
|
||||
this.productTypes = productTypes;
|
||||
}
|
||||
|
||||
public java.util.List<String> getMqTypes() {
|
||||
return mqTypes;
|
||||
}
|
||||
|
||||
public void setMqTypes(java.util.List<String> mqTypes) {
|
||||
this.mqTypes = mqTypes;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.cisd.tms.modules.backup.dto.support;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConfiguredRestoreCommand {
|
||||
|
||||
private String code;
|
||||
private String command;
|
||||
private boolean required = true;
|
||||
private List<String> productTypes = List.of();
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
public void setCommand(String command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public List<String> getProductTypes() {
|
||||
return productTypes;
|
||||
}
|
||||
|
||||
public void setProductTypes(List<String> productTypes) {
|
||||
this.productTypes = productTypes;
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@TableName("tms_resource_task_step")
|
||||
public class ResourceTaskStepEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String taskType;
|
||||
private Integer stepNo;
|
||||
private String stepCode;
|
||||
private String status;
|
||||
private String message;
|
||||
private String logPath;
|
||||
private Integer exitCode;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime finishTime;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public Integer getStepNo() {
|
||||
return stepNo;
|
||||
}
|
||||
|
||||
public void setStepNo(Integer stepNo) {
|
||||
this.stepNo = stepNo;
|
||||
}
|
||||
|
||||
public String getStepCode() {
|
||||
return stepCode;
|
||||
}
|
||||
|
||||
public void setStepCode(String stepCode) {
|
||||
this.stepCode = stepCode;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getLogPath() {
|
||||
return logPath;
|
||||
}
|
||||
|
||||
public void setLogPath(String logPath) {
|
||||
this.logPath = logPath;
|
||||
}
|
||||
|
||||
public Integer getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public void setExitCode(Integer exitCode) {
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getFinishTime() {
|
||||
return finishTime;
|
||||
}
|
||||
|
||||
public void setFinishTime(LocalDateTime finishTime) {
|
||||
this.finishTime = finishTime;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 恢复任务控制阶段。
|
||||
*/
|
||||
public enum ResourceControlPhase {
|
||||
TMS_PRECHECK,
|
||||
JAVA_EXTRACT,
|
||||
SHELL_APPLY,
|
||||
DONE
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源任务步骤状态。
|
||||
*/
|
||||
public enum ResourceStepStatus {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
SUCCESS_WITH_WARNINGS,
|
||||
FAILED,
|
||||
SKIPPED
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源备份/恢复任务主状态。
|
||||
*/
|
||||
public enum ResourceTaskStatus {
|
||||
PENDING,
|
||||
PRECHECKING,
|
||||
PRECHECK_FAILED,
|
||||
READY,
|
||||
APPLYING,
|
||||
VERIFYING,
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
CANCELLED
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源任务类型。
|
||||
*/
|
||||
public enum ResourceTaskType {
|
||||
RESOURCE_BACKUP,
|
||||
RESOURCE_RESTORE
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ResourceTaskStepMapper extends BaseMapperX<ResourceTaskStepEntity> {
|
||||
|
||||
@Select(
|
||||
"SELECT id, task_id, task_type, step_no, step_code, status, message, log_path, exit_code, "
|
||||
+ "create_time, update_time, start_time, finish_time "
|
||||
+ "FROM tms_resource_task_step WHERE task_id = #{taskId} ORDER BY step_no ASC"
|
||||
)
|
||||
List<ResourceTaskStepEntity> selectByTaskIdOrderByStepNo(@Param("taskId") String taskId);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ public interface DatabaseBackupCollector {
|
||||
|
||||
CollectedDatabaseDump collectCmepDatabaseDump();
|
||||
|
||||
CollectedDatabaseDump collectDatabaseDump(String databaseName, String fileName);
|
||||
|
||||
class CollectedDatabaseDump {
|
||||
|
||||
private String databaseName;
|
||||
|
||||
+33
-2
@@ -34,7 +34,7 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
@Override
|
||||
public CollectedDatabaseDump collectTmsDatabaseDump() {
|
||||
MysqlConnectionTarget target = resolveTarget(resourceBackupProperties.getTmsDatabaseName(), "TMS");
|
||||
return collectDatabaseDump(target, "TMS.sql");
|
||||
return collectDatabaseDump(target, "TMS.sql", resourceBackupProperties.getTmsDbExcludedTables());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -43,7 +43,21 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
return collectDatabaseDump(target, "CMEP.sql");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CollectedDatabaseDump collectDatabaseDump(String databaseName, String fileName) {
|
||||
MysqlConnectionTarget target = resolveTarget(databaseName, databaseName);
|
||||
return collectDatabaseDump(target, fileName);
|
||||
}
|
||||
|
||||
private CollectedDatabaseDump collectDatabaseDump(MysqlConnectionTarget target, String fileName) {
|
||||
return collectDatabaseDump(target, fileName, List.of());
|
||||
}
|
||||
|
||||
private CollectedDatabaseDump collectDatabaseDump(
|
||||
MysqlConnectionTarget target,
|
||||
String fileName,
|
||||
List<String> excludedTables
|
||||
) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(resourceBackupProperties.getMysqldumpPath());
|
||||
command.add("--host=" + target.host());
|
||||
@@ -52,8 +66,14 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
if (!target.password().isEmpty()) {
|
||||
command.add("--password=" + target.password());
|
||||
}
|
||||
command.add("--single-transaction");
|
||||
// command.add("--single-transaction");
|
||||
command.add("--skip-lock-tables");
|
||||
for (String table : excludedTables == null ? List.<String>of() : excludedTables) {
|
||||
String ignoreTable = normalizeIgnoreTable(target.databaseName(), table);
|
||||
if (!ignoreTable.isEmpty()) {
|
||||
command.add("--ignore-table=" + ignoreTable);
|
||||
}
|
||||
}
|
||||
command.add("--databases");
|
||||
command.add(target.databaseName());
|
||||
|
||||
@@ -126,6 +146,17 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String normalizeIgnoreTable(String databaseName, String table) {
|
||||
String value = trim(table);
|
||||
if (value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (value.contains(".")) {
|
||||
return value;
|
||||
}
|
||||
return trim(databaseName) + "." + value;
|
||||
}
|
||||
|
||||
private record MysqlConnectionTarget(String host, int port, String username, String password, String databaseName) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -271,8 +273,6 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
payloadManifest.put("mqType", initTask.getMqType());
|
||||
payloadManifest.put("orgCode", initTask.getOrgCode());
|
||||
payloadManifest.put("remark", request == null ? null : request.getRemark());
|
||||
payloadManifest.put("includeUpgradeHistory", request != null && Boolean.TRUE.equals(request.getIncludeUpgradeHistory()));
|
||||
payloadManifest.put("includeAuditBackups", request != null && Boolean.TRUE.equals(request.getIncludeAuditBackups()));
|
||||
|
||||
List<Map<String, Object>> resourceEntries = new ArrayList<>();
|
||||
int collectedCount = 0;
|
||||
@@ -332,6 +332,9 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
resourceEntry.put("mode", trim(item.getMode()).isEmpty() ? "FILE" : trim(item.getMode()));
|
||||
resourceEntry.put("sourcePath", item.getResolvedPath());
|
||||
resourceEntry.put("restorePath", trim(item.getRestorePath()).isEmpty() ? item.getResolvedPath() : item.getRestorePath());
|
||||
if ("GLOB".equalsIgnoreCase(trim(item.getMode()))) {
|
||||
return collectGlobFileItem(zipOutputStream, item, resourceEntry);
|
||||
}
|
||||
// 文件类资源在备份端只允许采集真实存在的文件/目录;恢复端还会再次校验 restorePath 白名单。
|
||||
Path resolvedPath = toResolvedPath(item.getResolvedPath());
|
||||
if (resolvedPath == null || !Files.exists(resolvedPath)) {
|
||||
@@ -376,6 +379,51 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
return archivedFiles.size();
|
||||
}
|
||||
|
||||
private int collectGlobFileItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
Path globParent = globParent(item.getResolvedPath());
|
||||
if (globParent == null || !Files.isDirectory(globParent)) {
|
||||
assertOptionalResource(item, "PATH_NOT_FOUND");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "PATH_NOT_FOUND");
|
||||
return 0;
|
||||
}
|
||||
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + trim(item.getResolvedPath()));
|
||||
List<Path> matchedFiles;
|
||||
try (Stream<Path> pathStream = Files.list(globParent)) {
|
||||
matchedFiles = pathStream
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> matcher.matches(path.normalize()))
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.toList();
|
||||
}
|
||||
if (matchedFiles.isEmpty()) {
|
||||
assertOptionalResource(item, "GLOB_NOT_MATCHED");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "GLOB_NOT_MATCHED");
|
||||
return 0;
|
||||
}
|
||||
List<Map<String, Object>> archivedFiles = new ArrayList<>();
|
||||
for (Path matchedFile : matchedFiles) {
|
||||
Map<String, Object> fileEntry = writeRegularFileEntry(
|
||||
zipOutputStream,
|
||||
item,
|
||||
matchedFile,
|
||||
matchedFile.getFileName().toString()
|
||||
);
|
||||
String restoreDir = trim(item.getRestorePath()).isEmpty() ? globParent.toString() : trim(item.getRestorePath());
|
||||
fileEntry.put("restorePath", Path.of(restoreDir).resolve(matchedFile.getFileName()).normalize().toString());
|
||||
archivedFiles.add(fileEntry);
|
||||
}
|
||||
resourceEntry.put("collected", Boolean.TRUE);
|
||||
resourceEntry.put("fileCount", archivedFiles.size());
|
||||
resourceEntry.put("files", archivedFiles);
|
||||
return archivedFiles.size();
|
||||
}
|
||||
|
||||
private int collectDatabaseItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
@@ -466,6 +514,16 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
if ("CMEP_DB".equalsIgnoreCase(resourceCode) || "CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
return databaseBackupCollector.collectCmepDatabaseDump();
|
||||
}
|
||||
if ("ORG_DB".equalsIgnoreCase(resourceCode) || "ORG_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
String databaseName = trim(item.getDatabaseName()).isEmpty() ? trim(item.getResolvedPath()) : trim(item.getDatabaseName());
|
||||
if (databaseName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String fileName = trim(item.getEntryPath()).isEmpty()
|
||||
? databaseName + ".sql"
|
||||
: Path.of(trim(item.getEntryPath())).getFileName().toString();
|
||||
return databaseBackupCollector.collectDatabaseDump(databaseName, fileName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -531,6 +589,18 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
return Path.of(value).normalize();
|
||||
}
|
||||
|
||||
private static Path globParent(String pattern) {
|
||||
String value = trim(pattern);
|
||||
if (value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
|
||||
if (slash < 0) {
|
||||
return Path.of(".").normalize();
|
||||
}
|
||||
return Path.of(value.substring(0, slash)).normalize();
|
||||
}
|
||||
|
||||
private static <T> T writeUnchecked(CheckedSupplier<T> supplier) {
|
||||
try {
|
||||
return supplier.get();
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourceTaskStepRepository {
|
||||
|
||||
void save(ResourceTaskStepEntity entity);
|
||||
|
||||
List<ResourceTaskStepEntity> findByTaskIdOrderByStepNo(String taskId);
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.repository.impl;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceTaskStepMapper;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceTaskStepRepository;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ResourceTaskStepRepositoryImpl implements ResourceTaskStepRepository {
|
||||
|
||||
private final ResourceTaskStepMapper resourceTaskStepMapper;
|
||||
|
||||
public ResourceTaskStepRepositoryImpl(ResourceTaskStepMapper resourceTaskStepMapper) {
|
||||
this.resourceTaskStepMapper = resourceTaskStepMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourceTaskStepEntity entity) {
|
||||
resourceTaskStepMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepEntity> findByTaskIdOrderByStepNo(String taskId) {
|
||||
List<ResourceTaskStepEntity> steps = resourceTaskStepMapper.selectByTaskIdOrderByStepNo(taskId);
|
||||
return steps == null ? List.of() : steps;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ActiveRestoreStateStore {
|
||||
|
||||
private static final String ACTIVE_FILE_NAME = "active-restore.json";
|
||||
|
||||
private final ResourceBackupProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ActiveRestoreStateStore(ResourceBackupProperties properties, ObjectMapper objectMapper) {
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void write(ResourceRestoreTaskEntity entity, String status) {
|
||||
ActiveRestoreSnapshot snapshot = fromEntity(entity, status);
|
||||
Path path = activeFilePath();
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), snapshot);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("写入当前资源恢复状态失败:" + path, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<ActiveRestoreSnapshot> read() {
|
||||
Path path = activeFilePath();
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(objectMapper.readValue(path.toFile(), ActiveRestoreSnapshot.class));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("读取当前资源恢复状态失败:" + path, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path activeFilePath() {
|
||||
return Path.of(properties.getRestoreTaskRootDir()).resolve(ACTIVE_FILE_NAME).normalize();
|
||||
}
|
||||
|
||||
private static ActiveRestoreSnapshot fromEntity(ResourceRestoreTaskEntity entity, String status) {
|
||||
return new ActiveRestoreSnapshot(
|
||||
entity.getTaskId(),
|
||||
entity.getPrecheckId(),
|
||||
entity.getBackupId(),
|
||||
status,
|
||||
entity.getSourceDeviceId(),
|
||||
entity.getSourceProductType(),
|
||||
entity.getTargetProductType(),
|
||||
entity.getPackageFileId(),
|
||||
entity.getPackagePath(),
|
||||
entity.getStagingDir(),
|
||||
entity.getRunnerStatePath(),
|
||||
entity.getStorageEncKeyFingerprint(),
|
||||
entity.getAuthenticityKeyFingerprint(),
|
||||
entity.getRestorePlanJson(),
|
||||
entity.getCreateBy(),
|
||||
text(entity.getCreateTime()),
|
||||
text(entity.getStartTime()),
|
||||
text(entity.getFinishTime())
|
||||
);
|
||||
}
|
||||
|
||||
private static String text(LocalDateTime value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
public static class ActiveRestoreSnapshot {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String sourceDeviceId;
|
||||
private String sourceProductType;
|
||||
private String targetProductType;
|
||||
private String packageFileId;
|
||||
private String packagePath;
|
||||
private String stagingDir;
|
||||
private String runnerStatePath;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private String restorePlanJson;
|
||||
private String createBy;
|
||||
private String createdAt;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
|
||||
public ActiveRestoreSnapshot() {
|
||||
}
|
||||
|
||||
public ActiveRestoreSnapshot(
|
||||
String taskId,
|
||||
String precheckId,
|
||||
String backupId,
|
||||
String status,
|
||||
String sourceDeviceId,
|
||||
String sourceProductType,
|
||||
String targetProductType,
|
||||
String packageFileId,
|
||||
String packagePath,
|
||||
String stagingDir,
|
||||
String runnerStatePath,
|
||||
String storageEncKeyFingerprint,
|
||||
String authenticityKeyFingerprint,
|
||||
String restorePlanJson,
|
||||
String createBy,
|
||||
String createdAt,
|
||||
String startedAt,
|
||||
String finishedAt
|
||||
) {
|
||||
this.taskId = taskId;
|
||||
this.precheckId = precheckId;
|
||||
this.backupId = backupId;
|
||||
this.status = status;
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
this.sourceProductType = sourceProductType;
|
||||
this.targetProductType = targetProductType;
|
||||
this.packageFileId = packageFileId;
|
||||
this.packagePath = packagePath;
|
||||
this.stagingDir = stagingDir;
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
this.restorePlanJson = restorePlanJson;
|
||||
this.createBy = createBy;
|
||||
this.createdAt = createdAt;
|
||||
this.startedAt = startedAt;
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getSourceProductType() {
|
||||
return sourceProductType;
|
||||
}
|
||||
|
||||
public void setSourceProductType(String sourceProductType) {
|
||||
this.sourceProductType = sourceProductType;
|
||||
}
|
||||
|
||||
public String getTargetProductType() {
|
||||
return targetProductType;
|
||||
}
|
||||
|
||||
public void setTargetProductType(String targetProductType) {
|
||||
this.targetProductType = targetProductType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
return packageFileId;
|
||||
}
|
||||
|
||||
public void setPackageFileId(String packageFileId) {
|
||||
this.packageFileId = packageFileId;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public String getStagingDir() {
|
||||
return stagingDir;
|
||||
}
|
||||
|
||||
public void setStagingDir(String stagingDir) {
|
||||
this.stagingDir = stagingDir;
|
||||
}
|
||||
|
||||
public String getRunnerStatePath() {
|
||||
return runnerStatePath;
|
||||
}
|
||||
|
||||
public void setRunnerStatePath(String runnerStatePath) {
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
}
|
||||
|
||||
public String getStorageEncKeyFingerprint() {
|
||||
return storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getAuthenticityKeyFingerprint() {
|
||||
return authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getRestorePlanJson() {
|
||||
return restorePlanJson;
|
||||
}
|
||||
|
||||
public void setRestorePlanJson(String restorePlanJson) {
|
||||
this.restorePlanJson = restorePlanJson;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(String startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -1,6 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredRestoreCommand;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
@@ -35,10 +36,24 @@ public class LightweightRestoreApplierLauncher {
|
||||
}
|
||||
|
||||
public void launch(Path workDir) {
|
||||
launch(workDir, "");
|
||||
}
|
||||
|
||||
public void launch(Path workDir, String targetProductType) {
|
||||
Path normalizedWorkDir = workDir.toAbsolutePath().normalize();
|
||||
// shell 脚本需要数据库连接、恢复脚本路径、健康检查地址等运行时参数。
|
||||
// 这些信息来自当前 TMS 配置,写入任务目录下的 restore-env.sh,避免在命令行暴露复杂参数。
|
||||
writeRestoreEnv(normalizedWorkDir);
|
||||
writeCommandPlan(
|
||||
normalizedWorkDir.resolve("restore-stop-commands.tsv"),
|
||||
properties.getRestoreStopCommands(),
|
||||
targetProductType
|
||||
);
|
||||
writeCommandPlan(
|
||||
normalizedWorkDir.resolve("restore-start-commands.tsv"),
|
||||
properties.getRestoreStartCommands(),
|
||||
targetProductType
|
||||
);
|
||||
Path logFile = normalizedWorkDir.resolve("restore-launch.log");
|
||||
// Java 阶段到这里已经完成包验签、解密、payload 展开和用户密钥恢复。
|
||||
// 后续进入停机恢复脚本,当前 TMS 服务可能被脚本停止。
|
||||
@@ -54,6 +69,35 @@ public class LightweightRestoreApplierLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCommandPlan(Path planFile, List<ConfiguredRestoreCommand> commands, String targetProductType) {
|
||||
try {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (ConfiguredRestoreCommand command : commands == null ? List.<ConfiguredRestoreCommand>of() : commands) {
|
||||
String shellCommand = trim(command.getCommand());
|
||||
if (shellCommand.isEmpty() || !matchesProductType(command.getProductTypes(), targetProductType)) {
|
||||
continue;
|
||||
}
|
||||
builder.append(defaultIfBlank(command.getCode(), "COMMAND"))
|
||||
.append('\t')
|
||||
.append(command.isRequired())
|
||||
.append('\t')
|
||||
.append(shellCommand.replace('\t', ' '))
|
||||
.append(System.lineSeparator());
|
||||
}
|
||||
Files.writeString(planFile, builder.toString());
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("写入恢复停启命令计划失败:" + planFile, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matchesProductType(List<String> productTypes, String targetProductType) {
|
||||
if (productTypes == null || productTypes.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String target = trim(targetProductType);
|
||||
return productTypes.stream().anyMatch(item -> trim(item).equalsIgnoreCase(target));
|
||||
}
|
||||
|
||||
private void writeRestoreEnv(Path workDir) {
|
||||
try {
|
||||
Files.createDirectories(workDir);
|
||||
@@ -71,6 +115,7 @@ public class LightweightRestoreApplierLauncher {
|
||||
values.put("DB_PASSWORD", trim(datasourcePassword));
|
||||
values.put("HEALTH_CHECK_URL", trim(properties.getHealthCheckUrl()));
|
||||
values.put("HEALTH_CHECK_TIMEOUT_SECONDS", String.valueOf(properties.getHealthCheckTimeoutSeconds()));
|
||||
values.put("RESTORE_APPLY_DELAY_SECONDS", "3");
|
||||
|
||||
StringBuilder builder = new StringBuilder("# generated by TMS resource restore\n");
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
@@ -105,6 +150,10 @@ public class LightweightRestoreApplierLauncher {
|
||||
return "'" + trim(value).replace("'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
private static String defaultIfBlank(String candidate, String fallback) {
|
||||
return trim(candidate).isEmpty() ? fallback : candidate;
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
+2
-5
@@ -24,13 +24,10 @@ public class ProcessBuilderRestoreCommandInvoker implements RestoreCommandInvoke
|
||||
if (logFile != null) {
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
|
||||
}
|
||||
Process process = pb.start();
|
||||
return process.waitFor();
|
||||
pb.start();
|
||||
return 0;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("启动恢复命令失败:" + String.join(" ", command), ex);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("restore command 被中断: " + String.join(" ", command), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@ import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
public interface ResourceRestoreService {
|
||||
|
||||
CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request);
|
||||
|
||||
CreateResourceRestoreResponse applyRestoreTask(String taskId);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import java.util.List;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
|
||||
|
||||
public interface ResourceTaskQueryService {
|
||||
|
||||
ResourceRestoreTaskDetailResponse getRestoreTask(String taskId);
|
||||
|
||||
List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId);
|
||||
|
||||
ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo);
|
||||
ResourceRestoreLatestResponse getLatestRestore();
|
||||
}
|
||||
|
||||
+6
-4
@@ -10,6 +10,7 @@ import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceCryptoKeyConstants;
|
||||
import com.cisd.tms.modules.backup.packagex.RequiredBackupResourceMissingException;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
@@ -39,7 +40,6 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceTaskAdmissionGuard resourceTaskAdmissionGuard;
|
||||
|
||||
@Override
|
||||
@@ -50,9 +50,9 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
response.setIkReady(false);
|
||||
response.setStorageEncKeyReady(false);
|
||||
response.setAuthenticityKeyReady(false);
|
||||
response.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
|
||||
response.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
response.setSource("CONFIGURED_KEY_INDEX");
|
||||
response.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
|
||||
response.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
|
||||
response.setSource("FIXED_KEY_INDEX");
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
entity.setSourceDeviceId(resolveSourceDeviceId());
|
||||
entity.setRemark(request == null ? null : request.getRemark());
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
|
||||
entity.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
entity.setStartTime(now);
|
||||
|
||||
+11
-4
@@ -5,7 +5,7 @@ import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceCryptoKeyConstants;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
@@ -30,7 +30,6 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final CisdPresetProperties cisdPresetProperties;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
@Override
|
||||
public ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request) {
|
||||
@@ -61,6 +60,7 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
response.setPayloadDecrypted(false);
|
||||
response.setWarnings(List.of());
|
||||
response.setBlockingIssues(List.of());
|
||||
response.setPrecheckPassed(isPrecheckPassed(response));
|
||||
// 第一版恢复计划固定为两段:Java 负责验签/解密/展开/用户密钥恢复,shell 负责停服后的落盘恢复。
|
||||
response.setRestorePlan(List.of("JAVA_EXTRACT", "SHELL_APPLY"));
|
||||
response.setRestoreScopeSummary(buildScopeSummary(inspectResult));
|
||||
@@ -84,11 +84,11 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
}
|
||||
|
||||
private String expectedStorageFingerprint() {
|
||||
return "USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex();
|
||||
return ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT;
|
||||
}
|
||||
|
||||
private String expectedAuthenticityFingerprint() {
|
||||
return "USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex();
|
||||
return ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT;
|
||||
}
|
||||
|
||||
private static java.util.Map<String, Object> buildScopeSummary(ResourcePackageInspectResult inspectResult) {
|
||||
@@ -106,4 +106,11 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
}
|
||||
return targetProductType.equalsIgnoreCase(sourceProductType);
|
||||
}
|
||||
|
||||
private static boolean isPrecheckPassed(ResourceRestorePrecheckResponse response) {
|
||||
return Boolean.TRUE.equals(response.getKeysetMatched())
|
||||
&& Boolean.TRUE.equals(response.getSignatureValid())
|
||||
&& Boolean.TRUE.equals(response.getCompatible())
|
||||
&& (response.getBlockingIssues() == null || response.getBlockingIssues().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
+109
-11
@@ -10,19 +10,25 @@ import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractor;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractionResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourceUserKeyRestoreService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.ActiveRestoreStateStore;
|
||||
import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceCryptoKeyConstants;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -40,6 +46,8 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
private final ResourceUserKeyRestoreService resourceUserKeyRestoreService;
|
||||
private final LightweightRestoreApplierLauncher lightweightRestoreApplierLauncher;
|
||||
private final ResourceTaskAdmissionGuard resourceTaskAdmissionGuard;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ActiveRestoreStateStore activeRestoreStateStore;
|
||||
|
||||
|
||||
@Override
|
||||
@@ -47,9 +55,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
// 恢复会导入用户密钥、覆盖配置文件、导入数据库并停启服务,
|
||||
// 必须在入口处和其它高风险任务互斥。
|
||||
resourceTaskAdmissionGuard.assertCanStartRestore();
|
||||
if (!Boolean.TRUE.equals(request.getConfirmRiskAccepted())) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "必须确认恢复风险");
|
||||
}
|
||||
validateTargetNodes(request);
|
||||
// 恢复创建必须消费预检结果,确保“用户确认的 backupId”与预检看到的是同一个包。
|
||||
ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检不存在"));
|
||||
@@ -71,6 +77,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
createDirectories(taskDir);
|
||||
// Java 阶段完成包的完整验签、解密和 payload 展开,并生成 shell 可消费的 TSV 计划文件。
|
||||
ResourcePayloadExtractionResult extractionResult = resourcePayloadExtractor.extract(Path.of(packagePath), taskDir);
|
||||
writeTargetNodeContext(taskDir, request);
|
||||
// 用户密钥恢复必须放在 shell 停服/导库之前:数据库和证书数据恢复后会引用这些 keyIdx。
|
||||
// 如果这里失败,不保存 APPLYING 任务、不启动 shell,避免出现“数据已覆盖但密钥不可用”的半恢复状态。
|
||||
resourceUserKeyRestoreService.recover(extractionResult.getPayloadDir());
|
||||
@@ -79,8 +86,8 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
entity.setTaskId(taskId);
|
||||
entity.setPrecheckId(precheck.getPrecheckId());
|
||||
entity.setBackupId(precheck.getBackupId());
|
||||
// 轻量恢复模型下,Java 已完成包验签/解密/展开,后续由 shell apply 脚本接管实际覆盖/导入。
|
||||
entity.setStatus("APPLYING");
|
||||
// 创建接口只准备恢复并返回任务号,避免在 HTTP 响应返回前停掉当前 TMS。
|
||||
entity.setStatus("READY_TO_APPLY");
|
||||
entity.setSourceDeviceId("UNKNOWN");
|
||||
entity.setSourceProductType(precheck.getSourceProductType());
|
||||
entity.setTargetProductType(precheck.getTargetProductType());
|
||||
@@ -88,17 +95,17 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
entity.setPackagePath(packagePath);
|
||||
entity.setStagingDir(taskDir.resolve("payload").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("status.json").toString());
|
||||
entity.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
|
||||
entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
entity.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
|
||||
entity.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
|
||||
entity.setRestorePlanJson("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]");
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
// 只有 Java 解包和用户密钥恢复都成功后,任务才进入 APPLYING。
|
||||
// 后续状态由 shell 写 status.json,查询接口会读取该状态文件展示实时恢复进度。
|
||||
// 只有 Java 解包和用户密钥恢复都成功后,任务才进入 READY_TO_APPLY。
|
||||
// 前端一次确认后可连续调用 create 和 apply;shell 只在 apply 阶段启动,
|
||||
// 避免创建接口返回前停掉当前 TMS。
|
||||
resourceRestoreTaskRepository.save(entity);
|
||||
// shell apply 是停机恢复的执行点:停服务、覆盖文件、导 SQL、MQ replay、起服务、健康检查。
|
||||
lightweightRestoreApplierLauncher.launch(taskDir);
|
||||
activeRestoreStateStore.write(entity, entity.getStatus());
|
||||
|
||||
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
|
||||
response.setTaskId(taskId);
|
||||
@@ -107,6 +114,36 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateResourceRestoreResponse applyRestoreTask(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
|
||||
String status = trim(entity.getStatus());
|
||||
if ("SUCCESS".equalsIgnoreCase(status) || "FAILED".equalsIgnoreCase(status)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务已结束:" + trim(taskId));
|
||||
}
|
||||
if (!"READY_TO_APPLY".equalsIgnoreCase(status) && !"APPLYING".equalsIgnoreCase(status)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务状态不允许执行:" + status);
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setStatus("APPLYING");
|
||||
if (entity.getStartTime() == null) {
|
||||
entity.setStartTime(now);
|
||||
}
|
||||
entity.setUpdateTime(now);
|
||||
resourceRestoreTaskRepository.update(entity);
|
||||
activeRestoreStateStore.write(entity, entity.getStatus());
|
||||
Path taskDir = resolveTaskDir(entity);
|
||||
// shell apply 是停机恢复的执行点:停服务、覆盖文件、导 SQL、MQ replay、起服务、健康检查。
|
||||
lightweightRestoreApplierLauncher.launch(taskDir, entity.getTargetProductType());
|
||||
|
||||
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
private static void createDirectories(Path taskDir) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
@@ -115,10 +152,71 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTargetNodeContext(Path taskDir, CreateResourceRestoreRequest request) {
|
||||
Path contextPath = taskDir.resolve("restore-context.json").normalize();
|
||||
try {
|
||||
Map<String, Object> context = Files.isRegularFile(contextPath)
|
||||
? objectMapper.readValue(contextPath.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
})
|
||||
: new LinkedHashMap<>();
|
||||
context.put("targetDeployMode", trim(request.getDeployMode()).toUpperCase());
|
||||
context.put("targetNodes", targetNodes(request.getNodes()));
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(contextPath.toFile(), context);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("写入恢复目标节点上下文失败:" + contextPath, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> targetNodes(CreateResourceRestoreRequest.Nodes nodes) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
putIfNotBlank(values, "node01Ip", nodes.getNode01Ip());
|
||||
putIfNotBlank(values, "node02Ip", nodes.getNode02Ip());
|
||||
putIfNotBlank(values, "node03Ip", nodes.getNode03Ip());
|
||||
putIfNotBlank(values, "node04Ip", nodes.getNode04Ip());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static void putIfNotBlank(Map<String, Object> values, String key, String value) {
|
||||
String text = trim(value);
|
||||
if (!text.isEmpty()) {
|
||||
values.put(key, text);
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
|
||||
String runnerStatePath = trim(entity.getRunnerStatePath());
|
||||
if (runnerStatePath.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务状态文件路径为空");
|
||||
}
|
||||
return Path.of(runnerStatePath).normalize().getParent();
|
||||
}
|
||||
|
||||
private static void validateTargetNodes(CreateResourceRestoreRequest request) {
|
||||
String deployMode = trim(request.getDeployMode()).toUpperCase();
|
||||
CreateResourceRestoreRequest.Nodes nodes = request.getNodes();
|
||||
if (nodes == null) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes不能为空");
|
||||
}
|
||||
if (trim(nodes.getNode01Ip()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node01Ip不能为空");
|
||||
}
|
||||
if (("DUAL".equals(deployMode) || "QUAD".equals(deployMode)) && trim(nodes.getNode02Ip()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node02Ip不能为空");
|
||||
}
|
||||
if ("QUAD".equals(deployMode)) {
|
||||
if (trim(nodes.getNode03Ip()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node03Ip不能为空");
|
||||
}
|
||||
if (trim(nodes.getNode04Ip()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node04Ip不能为空");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validatePrecheck(ResourceRestorePrecheckResponse precheck) {
|
||||
if (precheck == null) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检不存在");
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import org.springframework.stereotype.Component;
|
||||
public class ResourceTaskAdmissionGuard {
|
||||
|
||||
private static final Set<String> ACTIVE_BACKUP_STATUSES = Set.of("RUNNING");
|
||||
private static final Set<String> ACTIVE_RESTORE_STATUSES = Set.of("APPLYING", "RUNNING", "HANDOFF", "PENDING");
|
||||
private static final Set<String> ACTIVE_RESTORE_STATUSES = Set.of("READY_TO_APPLY", "APPLYING", "RUNNING", "HANDOFF", "PENDING");
|
||||
|
||||
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
|
||||
+118
-137
@@ -1,20 +1,17 @@
|
||||
package com.cisd.tms.modules.backup.service.impl;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.ActiveRestoreStateStore;
|
||||
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader;
|
||||
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader.RestoreStatusSnapshot;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -24,160 +21,144 @@ import org.springframework.stereotype.Service;
|
||||
public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RestoreStatusFileReader restoreStatusFileReader;
|
||||
private final ActiveRestoreStateStore activeRestoreStateStore;
|
||||
|
||||
@Override
|
||||
public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
|
||||
ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setPrecheckId(entity.getPrecheckId());
|
||||
response.setBackupId(entity.getBackupId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setControlPhase("PRECHECKED");
|
||||
response.setStateSource("DB");
|
||||
response.setSourceDeviceId(entity.getSourceDeviceId());
|
||||
response.setSourceProductType(entity.getSourceProductType());
|
||||
response.setTargetProductType(entity.getTargetProductType());
|
||||
response.setPackageFileId(entity.getPackageFileId());
|
||||
response.setPackagePathVisible(Boolean.FALSE);
|
||||
response.setStatusFilePath(entity.getRunnerStatePath());
|
||||
response.setRestorePlan(readStringList(entity.getRestorePlanJson()));
|
||||
response.setConsistencyReport(readObject(entity.getConsistencyReportJson()));
|
||||
response.setErrorCode(entity.getErrorCode());
|
||||
response.setErrorMessage(entity.getErrorMessage());
|
||||
response.setCreatedBy(entity.getCreateBy());
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
response.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
|
||||
response.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
|
||||
mergeLightweightStatus(response, entity);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
Path taskDir = resolveTaskDir(entity);
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(taskDir);
|
||||
List<ResourceTaskStepItemResponse> responses = new ArrayList<>();
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
String stepCode = restorePlan.get(i);
|
||||
int stepNo = i + 1;
|
||||
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
|
||||
item.setStepNo(stepNo);
|
||||
item.setStepCode(stepCode);
|
||||
applyLightweightStepState(item, snapshot, stepCode);
|
||||
responses.add(item);
|
||||
public ResourceRestoreLatestResponse getLatestRestore() {
|
||||
Optional<ActiveRestoreStateStore.ActiveRestoreSnapshot> active = activeRestoreStateStore.read();
|
||||
if (active.isPresent()) {
|
||||
return latestFromActive(active.get());
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : "";
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
|
||||
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
|
||||
response.setTaskId(trim(taskId));
|
||||
response.setStepNo(stepNo);
|
||||
response.setStepCode(stepCode);
|
||||
response.setContent(snapshot.log());
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
return latestFromDatabase();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private void mergeLightweightStatus(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) {
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
|
||||
if (trim(snapshot.status()).isEmpty()) {
|
||||
private ResourceRestoreLatestResponse latestFromActive(ActiveRestoreStateStore.ActiveRestoreSnapshot active) {
|
||||
RestoreStatusSnapshot status = restoreStatusFileReader.read(resolveTaskDir(active.getRunnerStatePath()));
|
||||
String latestStatus = defaultIfBlank(status.status(), active.getStatus());
|
||||
repairMissingTask(active, status, latestStatus);
|
||||
ResourceRestoreLatestResponse response = new ResourceRestoreLatestResponse();
|
||||
response.setTaskId(active.getTaskId());
|
||||
response.setBackupId(active.getBackupId());
|
||||
response.setStatus(latestStatus);
|
||||
response.setPhase(status.phase());
|
||||
response.setMessage(displayMessage(status.message(), latestStatus));
|
||||
response.setCreatedAt(active.getCreatedAt());
|
||||
response.setFinishedAt(isTerminal(latestStatus) ? defaultIfBlank(status.updatedAt(), active.getFinishedAt()) : null);
|
||||
return response;
|
||||
}
|
||||
|
||||
private ResourceRestoreLatestResponse latestFromDatabase() {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findAll().stream()
|
||||
.filter(item -> !trim(item.getTaskId()).isEmpty())
|
||||
.max(Comparator.comparing(item -> item.getCreateTime() == null ? LocalDateTime.MIN : item.getCreateTime()))
|
||||
.orElse(null);
|
||||
ResourceRestoreLatestResponse response = new ResourceRestoreLatestResponse();
|
||||
if (entity == null) {
|
||||
response.setStatus("NONE");
|
||||
response.setMessage("暂无资源恢复记录");
|
||||
return response;
|
||||
}
|
||||
RestoreStatusSnapshot status = restoreStatusFileReader.read(resolveTaskDir(entity));
|
||||
String latestStatus = defaultIfBlank(status.status(), entity.getStatus());
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setBackupId(entity.getBackupId());
|
||||
response.setStatus(latestStatus);
|
||||
response.setPhase(status.phase());
|
||||
response.setMessage(displayMessage(status.message(), latestStatus));
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
response.setFinishedAt(isTerminal(latestStatus)
|
||||
? defaultIfBlank(status.updatedAt(), entity.getFinishTime() == null ? null : entity.getFinishTime().toString())
|
||||
: null);
|
||||
return response;
|
||||
}
|
||||
|
||||
private void repairMissingTask(
|
||||
ActiveRestoreStateStore.ActiveRestoreSnapshot active,
|
||||
RestoreStatusSnapshot status,
|
||||
String latestStatus
|
||||
) {
|
||||
if (trim(active.getTaskId()).isEmpty() || resourceRestoreTaskRepository.findByTaskId(active.getTaskId()).isPresent()) {
|
||||
return;
|
||||
}
|
||||
response.setStatus(defaultIfBlank(snapshot.status(), response.getStatus()));
|
||||
response.setControlPhase(defaultIfBlank(snapshot.phase(), response.getControlPhase()));
|
||||
response.setStateSource("SHELL_STATUS");
|
||||
response.setCurrentStepCode(defaultIfBlank(snapshot.phase(), response.getCurrentStepCode()));
|
||||
response.setCurrentStepNo(currentStepNo(snapshot.phase(), response.getRestorePlan()));
|
||||
response.setLastHeartbeatAt(defaultIfBlank(snapshot.updatedAt(), response.getLastHeartbeatAt()));
|
||||
response.setErrorMessage(defaultIfBlank(snapshot.message(), response.getErrorMessage()));
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(active.getTaskId());
|
||||
entity.setPrecheckId(defaultIfBlank(active.getPrecheckId(), active.getTaskId()));
|
||||
entity.setBackupId(defaultIfBlank(active.getBackupId(), "UNKNOWN"));
|
||||
entity.setStatus(defaultIfBlank(latestStatus, "UNKNOWN"));
|
||||
entity.setSourceDeviceId(active.getSourceDeviceId());
|
||||
entity.setSourceProductType(defaultIfBlank(active.getSourceProductType(), "UNKNOWN"));
|
||||
entity.setTargetProductType(defaultIfBlank(active.getTargetProductType(), "UNKNOWN"));
|
||||
entity.setPackageFileId(defaultIfBlank(active.getPackageFileId(), "UNKNOWN"));
|
||||
entity.setPackagePath(active.getPackagePath());
|
||||
entity.setStagingDir(active.getStagingDir());
|
||||
entity.setRunnerStatePath(active.getRunnerStatePath());
|
||||
entity.setStorageEncKeyFingerprint(defaultIfBlank(active.getStorageEncKeyFingerprint(), "UNKNOWN"));
|
||||
entity.setAuthenticityKeyFingerprint(defaultIfBlank(active.getAuthenticityKeyFingerprint(), "UNKNOWN"));
|
||||
entity.setRestorePlanJson(defaultIfBlank(active.getRestorePlanJson(), "[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]"));
|
||||
entity.setErrorMessage(status.message());
|
||||
entity.setCreateBy(defaultIfBlank(active.getCreateBy(), "SYSTEM"));
|
||||
entity.setCreateTime(parseLocalDateTime(active.getCreatedAt(), LocalDateTime.now()));
|
||||
entity.setStartTime(parseLocalDateTime(active.getStartedAt(), null));
|
||||
entity.setFinishTime(isTerminal(latestStatus) ? parseLocalDateTime(status.updatedAt(), null) : null);
|
||||
entity.setUpdateTime(LocalDateTime.now());
|
||||
resourceRestoreTaskRepository.save(entity);
|
||||
}
|
||||
|
||||
private Path resolveStatePath(String statusFilePath) {
|
||||
return trim(statusFilePath).isEmpty() ? null : Path.of(statusFilePath).normalize();
|
||||
}
|
||||
|
||||
private Path resolveTaskDir(String statusFilePath) {
|
||||
Path statePath = resolveStatePath(statusFilePath);
|
||||
return statePath == null ? null : statePath.getParent();
|
||||
}
|
||||
|
||||
private Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
|
||||
Path statePath = resolveStatePath(entity.getRunnerStatePath());
|
||||
return statePath == null ? null : statePath.getParent();
|
||||
}
|
||||
|
||||
private void applyLightweightStepState(ResourceTaskStepItemResponse item, RestoreStatusSnapshot snapshot, String stepCode) {
|
||||
item.setLogAvailable(!trim(snapshot.log()).isEmpty());
|
||||
if (trim(snapshot.status()).isEmpty()) {
|
||||
item.setStatus("PENDING");
|
||||
return;
|
||||
}
|
||||
if ("JAVA_EXTRACT".equals(stepCode)) {
|
||||
item.setStatus("SUCCESS");
|
||||
return;
|
||||
}
|
||||
if ("SHELL_APPLY".equals(stepCode)) {
|
||||
item.setStatus(snapshot.status());
|
||||
item.setMessage(snapshot.message());
|
||||
return;
|
||||
}
|
||||
if (stepCode.equals(snapshot.phase())) {
|
||||
item.setStatus(snapshot.status());
|
||||
item.setMessage(snapshot.message());
|
||||
return;
|
||||
}
|
||||
item.setStatus("PENDING");
|
||||
}
|
||||
|
||||
private List<String> readStringList(String json) {
|
||||
if (trim(json).isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, new TypeReference<List<String>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("解析恢复计划JSON失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Object readObject(String json) {
|
||||
if (trim(json).isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, Object.class);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("解析一致性报告JSON失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String defaultIfBlank(String candidate, String fallback) {
|
||||
return trim(candidate).isEmpty() ? fallback : candidate;
|
||||
}
|
||||
|
||||
private static Integer currentStepNo(String phase, List<String> restorePlan) {
|
||||
if (restorePlan == null || restorePlan.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
if (restorePlan.get(i).equals(phase)) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return restorePlan.contains("SHELL_APPLY") ? restorePlan.indexOf("SHELL_APPLY") + 1 : null;
|
||||
private static boolean isTerminal(String status) {
|
||||
String value = trim(status);
|
||||
return "SUCCESS".equalsIgnoreCase(value) || "FAILED".equalsIgnoreCase(value);
|
||||
}
|
||||
|
||||
private static String displayMessage(String message, String status) {
|
||||
String text = trim(message);
|
||||
if (!text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
if ("SUCCESS".equalsIgnoreCase(trim(status))) {
|
||||
return "恢复成功";
|
||||
}
|
||||
if ("FAILED".equalsIgnoreCase(trim(status))) {
|
||||
return "恢复失败";
|
||||
}
|
||||
if ("NONE".equalsIgnoreCase(trim(status))) {
|
||||
return "暂无资源恢复记录";
|
||||
}
|
||||
return "恢复执行中";
|
||||
}
|
||||
|
||||
private static LocalDateTime parseLocalDateTime(String value, LocalDateTime fallback) {
|
||||
String text = trim(value);
|
||||
if (text.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return LocalDateTime.parse(text);
|
||||
} catch (DateTimeException ex) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface ResourceTaskStepLogService {
|
||||
|
||||
ResourceTaskStepLogResponse read(Path taskDir, String taskId, int stepNo, String stepCode);
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class FileSystemResourceTaskStepLogService implements ResourceTaskStepLogService {
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse read(Path taskDir, String taskId, int stepNo, String stepCode) {
|
||||
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setStepNo(stepNo);
|
||||
response.setStepCode(stepCode);
|
||||
if (taskDir == null || stepCode == null || stepCode.isBlank()) {
|
||||
response.setContent("");
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
Path logFile = taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode));
|
||||
try {
|
||||
if (!Files.isRegularFile(logFile)) {
|
||||
response.setContent("");
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
response.setContent(Files.readString(logFile));
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("读取资源任务步骤日志失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,41 @@ tms:
|
||||
output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages}
|
||||
# 轻量恢复 apply 脚本启停 TMS 时使用的脚本路径。
|
||||
tms-script-path: ${TMS_BACKUP_TMS_SCRIPT_PATH:/home/tms/scripts/tms.sh}
|
||||
# 轻量恢复停服务命令。未配置时 apply 脚本会回退到 tms-script-path stop。
|
||||
# command 支持现场脚本路径或完整 shell 命令;required=false 表示失败只记录 warning。
|
||||
restore-stop-commands:
|
||||
- code: STANDARD_NGINX
|
||||
command: ${TMS_BACKUP_RESTORE_STOP_STANDARD_NGINX_COMMAND:${tms.init.executor.standard-nginx-stop-command}}
|
||||
required: ${TMS_BACKUP_RESTORE_STOP_STANDARD_NGINX_REQUIRED:false}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: STANDARD_APPS
|
||||
command: ${TMS_BACKUP_RESTORE_STOP_STANDARD_APPS_COMMAND:${tms.init.executor.standard-app-stop-command}}
|
||||
required: ${TMS_BACKUP_RESTORE_STOP_STANDARD_APPS_REQUIRED:false}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: TMS
|
||||
command: ${TMS_BACKUP_RESTORE_STOP_TMS_COMMAND:${tms.backup.tms-script-path} stop}
|
||||
required: ${TMS_BACKUP_RESTORE_STOP_TMS_REQUIRED:false}
|
||||
# 轻量恢复启动服务命令。标准/间参默认启动 TMS、CMSP/CMTP 和 nginx。
|
||||
restore-start-commands:
|
||||
- code: TMS
|
||||
command: ${TMS_BACKUP_RESTORE_START_TMS_COMMAND:${tms.backup.tms-script-path} start}
|
||||
required: ${TMS_BACKUP_RESTORE_START_TMS_REQUIRED:true}
|
||||
- code: STANDARD_APPS
|
||||
command: ${TMS_BACKUP_RESTORE_START_STANDARD_APPS_COMMAND:${tms.init.executor.standard-app-start-command}}
|
||||
required: ${TMS_BACKUP_RESTORE_START_STANDARD_APPS_REQUIRED:true}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: STANDARD_NGINX
|
||||
command: ${TMS_BACKUP_RESTORE_START_STANDARD_NGINX_COMMAND:${tms.init.executor.standard-nginx-start-command}}
|
||||
required: ${TMS_BACKUP_RESTORE_START_STANDARD_NGINX_REQUIRED:true}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
# 轻量恢复 apply 脚本路径;Java 完成验签/解密/展开后调用该脚本执行覆盖、SQL、MQ replay。
|
||||
restore-apply-script-path: ${TMS_BACKUP_RESTORE_APPLY_SCRIPT_PATH:/home/tms/bin/resource-restore/apply-resource-backup.sh}
|
||||
# 文件恢复白名单根路径;manifest 中 FILE 恢复路径必须落在这些目录下。
|
||||
@@ -202,6 +237,61 @@ tms:
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4I:/home/cmep4i}
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4D:/home/cmep4d}
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_WORK:/home/tms/tmp/resource-restore}
|
||||
# 现场可追加或覆盖直接复制恢复的文件类资源。
|
||||
# mode=FILE 表示单文件,mode=DIR 表示目录递归,mode=GLOB 表示按通配符匹配同一目录下的一批文件。
|
||||
# restore-path 对 FILE 是目标文件路径,对 DIR/GLOB 是目标目录;目标仍必须落在 allowed-restore-roots 下。
|
||||
file-resources:
|
||||
- code: TMS_CONFIG
|
||||
mode: DIR
|
||||
path: ${TMS_BACKUP_TMS_CONFIG_PATH:${tms.init.executor.tms-config-dir}}
|
||||
restore-path: ${TMS_BACKUP_TMS_CONFIG_RESTORE_PATH:${tms.init.executor.tms-config-dir}}
|
||||
required: ${TMS_BACKUP_TMS_CONFIG_REQUIRED:true}
|
||||
- code: CP_CONFIG
|
||||
mode: FILE
|
||||
path: ${TMS_BACKUP_STANDARD_CP_CONFIG_PATH:${tms.init.executor.standard-cpconfig-path}}
|
||||
restore-path: ${TMS_BACKUP_STANDARD_CP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-cpconfig-path}}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: ${TMS_BACKUP_STANDARD_CP_CONFIG_REQUIRED:true}
|
||||
- code: CP_CONFIG
|
||||
mode: FILE
|
||||
path: ${TMS_BACKUP_DIRECT_CP_CONFIG_PATH:${tms.init.executor.direct-cpconfig-target-path}}
|
||||
restore-path: ${TMS_BACKUP_DIRECT_CP_CONFIG_RESTORE_PATH:${tms.init.executor.direct-cpconfig-target-path}}
|
||||
product-types:
|
||||
- DIRECT
|
||||
required: ${TMS_BACKUP_DIRECT_CP_CONFIG_REQUIRED:true}
|
||||
- code: RECEIVER_LICENSE
|
||||
mode: FILE
|
||||
path: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_PATH:${tms.init.executor.standard-receiver-license-target-path}}
|
||||
restore-path: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_RESTORE_PATH:${tms.init.executor.standard-receiver-license-target-path}}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_REQUIRED:true}
|
||||
- code: RECEIVER_LICENSE
|
||||
mode: FILE
|
||||
path: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_PATH:${tms.init.executor.direct-receiver-license-target-path}}
|
||||
restore-path: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_RESTORE_PATH:${tms.init.executor.direct-receiver-license-target-path}}
|
||||
product-types:
|
||||
- DIRECT
|
||||
required: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_REQUIRED:true}
|
||||
- code: STANDARD_CMSP_APP_CONFIG
|
||||
mode: GLOB
|
||||
path: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_PATH:${tms.init.executor.standard-home-dir}/cmsp/application-prd*.properties}
|
||||
restore-path: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-home-dir}/cmsp}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_REQUIRED:true}
|
||||
- code: STANDARD_CMTP_APP_CONFIG
|
||||
mode: GLOB
|
||||
path: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_PATH:${tms.init.executor.standard-home-dir}/cmtp/application-prd*.properties}
|
||||
restore-path: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-home-dir}/cmtp}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_REQUIRED:true}
|
||||
# 导出 TMS 库时使用的 mysqldump 命令;现场可按实际安装路径覆盖。
|
||||
mysqldump-path: ${TMS_BACKUP_MYSQLDUMP_PATH:mysqldump}
|
||||
# 恢复 TMS 库时使用的 mysql 客户端命令。
|
||||
@@ -212,6 +302,13 @@ tms:
|
||||
mq-replay-script-path: ${TMS_BACKUP_MQ_REPLAY_SCRIPT_PATH:/home/tms/bin/resource-restore/replay-mq.sh}
|
||||
# 备份 TMS 库名;为空时默认从 spring.datasource.url 中解析。
|
||||
tms-database-name: ${TMS_BACKUP_TMS_DATABASE_NAME:}
|
||||
# TMS 库备份排除表。默认不备份角色、授权、会话和审计表,避免恢复时覆盖新机器安全状态。
|
||||
tms-db-excluded-tables:
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_ROLE_ACCOUNT:tms_role_account}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_ROLE_UKEY:tms_role_ukey_binding}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_FULL:tms_auth_full_account}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_SESSION:tms_auth_session}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_CHALLENGE:tms_auth_challenge}
|
||||
# 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。
|
||||
cmep-database-name: ${TMS_BACKUP_CMEP_DATABASE_NAME:CMEP}
|
||||
# 恢复完成后的最小健康检查地址。
|
||||
@@ -229,9 +326,9 @@ tms:
|
||||
internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token}
|
||||
internal-auth:
|
||||
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
|
||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:true}
|
||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试角色。
|
||||
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:KEY_ADMIN}
|
||||
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:OPS_ADMIN}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试认证等级。
|
||||
debug-auth-level: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_AUTH_LEVEL:FULL}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试 session token。
|
||||
|
||||
@@ -115,26 +115,6 @@ CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
|
||||
KEY idx_tms_resource_restore_task_status (status),
|
||||
KEY idx_tms_resource_restore_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_task_step (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'RESOURCE_BACKUP / RESOURCE_RESTORE',
|
||||
step_no INT NOT NULL,
|
||||
step_code VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
message VARCHAR(512) NULL,
|
||||
log_path VARCHAR(1024) NULL,
|
||||
exit_code INT NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_task_step_task_no (task_id, step_no),
|
||||
KEY idx_tms_resource_task_step_task_id (task_id),
|
||||
KEY idx_tms_resource_task_step_task_type (task_type)
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 认证与设备基础表
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
@@ -62,22 +62,3 @@ CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
|
||||
KEY idx_tms_resource_restore_task_status (status),
|
||||
KEY idx_tms_resource_restore_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_task_step (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'RESOURCE_BACKUP / RESOURCE_RESTORE',
|
||||
step_no INT NOT NULL,
|
||||
step_code VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
message VARCHAR(512) NULL,
|
||||
log_path VARCHAR(1024) NULL,
|
||||
exit_code INT NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_task_step_task_no (task_id, step_no),
|
||||
KEY idx_tms_resource_task_step_task_id (task_id),
|
||||
KEY idx_tms_resource_task_step_task_type (task_type)
|
||||
);
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.cisd.tms.modules.backup;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -12,15 +11,25 @@ class ResourceBackupDtoContractTest {
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void shouldAcceptBackupCreateRequestFields() {
|
||||
void shouldOnlyExposeOptionalRemarkOnBackupCreateRequest() throws Exception {
|
||||
CreateResourceBackupRequest request = new CreateResourceBackupRequest();
|
||||
request.setIncludeUpgradeHistory(Boolean.TRUE);
|
||||
request.setIncludeAuditBackups(Boolean.FALSE);
|
||||
request.setRemark("before hardware replacement");
|
||||
|
||||
Assertions.assertEquals(Boolean.TRUE, request.getIncludeUpgradeHistory());
|
||||
Assertions.assertEquals(Boolean.FALSE, request.getIncludeAuditBackups());
|
||||
Assertions.assertEquals("before hardware replacement", request.getRemark());
|
||||
Assertions.assertEquals(1, CreateResourceBackupRequest.class.getDeclaredFields().length);
|
||||
Assertions.assertThrows(
|
||||
NoSuchMethodException.class,
|
||||
() -> CreateResourceBackupRequest.class.getDeclaredMethod("getIncludeUpgradeHistory")
|
||||
);
|
||||
Assertions.assertThrows(
|
||||
NoSuchMethodException.class,
|
||||
() -> CreateResourceBackupRequest.class.getDeclaredMethod("getIncludeAuditBackups")
|
||||
);
|
||||
|
||||
String json = objectMapper.writeValueAsString(new CreateResourceBackupRequest());
|
||||
|
||||
Assertions.assertFalse(json.contains("includeUpgradeHistory"));
|
||||
Assertions.assertFalse(json.contains("includeAuditBackups"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -32,6 +41,7 @@ class ResourceBackupDtoContractTest {
|
||||
response.setKeysetMatched(true);
|
||||
response.setSignatureValid(true);
|
||||
response.setCompatible(true);
|
||||
response.setPrecheckPassed(true);
|
||||
response.setPayloadDecrypted(false);
|
||||
|
||||
String json = objectMapper.writeValueAsString(response);
|
||||
@@ -40,23 +50,35 @@ class ResourceBackupDtoContractTest {
|
||||
Assertions.assertTrue(json.contains("\"backupId\":\"RBKP-20260408-153000-000001\""));
|
||||
Assertions.assertTrue(json.contains("\"sourceProductType\":\"ENTERPRISE\""));
|
||||
Assertions.assertTrue(json.contains("\"keysetMatched\":true"));
|
||||
Assertions.assertTrue(json.contains("\"precheckPassed\":true"));
|
||||
Assertions.assertTrue(json.contains("\"payloadDecrypted\":false"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSerializeRestoreTaskDetailControlFields() throws Exception {
|
||||
ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse();
|
||||
response.setTaskId("RRST-TASK-20260408-000001");
|
||||
response.setStatus("APPLYING");
|
||||
response.setControlPhase("SHELL_APPLY");
|
||||
response.setStateSource("SHELL_STATUS");
|
||||
response.setCurrentStepCode("SHELL_APPLY");
|
||||
void shouldSerializeRestoreCreateTargetNodeFields() throws Exception {
|
||||
com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest request =
|
||||
new com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest();
|
||||
request.setPrecheckId("RPRE-20260408-000001");
|
||||
request.setConfirmBackupId("RBKP-20260408-153000-000001");
|
||||
request.setDeployMode("DUAL");
|
||||
com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest.Nodes nodes =
|
||||
new com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest.Nodes();
|
||||
nodes.setNode01Ip("192.168.1.11");
|
||||
nodes.setNode02Ip("192.168.1.12");
|
||||
request.setNodes(nodes);
|
||||
|
||||
String json = objectMapper.writeValueAsString(response);
|
||||
String json = objectMapper.writeValueAsString(request);
|
||||
|
||||
Assertions.assertTrue(json.contains("\"taskId\":\"RRST-TASK-20260408-000001\""));
|
||||
Assertions.assertTrue(json.contains("\"controlPhase\":\"SHELL_APPLY\""));
|
||||
Assertions.assertTrue(json.contains("\"stateSource\":\"SHELL_STATUS\""));
|
||||
Assertions.assertTrue(json.contains("\"currentStepCode\":\"SHELL_APPLY\""));
|
||||
Assertions.assertThrows(
|
||||
NoSuchMethodException.class,
|
||||
() -> com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest.class
|
||||
.getDeclaredMethod("getConfirmRiskAccepted")
|
||||
);
|
||||
Assertions.assertFalse(json.contains("confirmRiskAccepted"));
|
||||
Assertions.assertTrue(json.contains("\"deployMode\":\"DUAL\""));
|
||||
Assertions.assertTrue(json.contains("\"nodes\""));
|
||||
Assertions.assertTrue(json.contains("\"node01Ip\":\"192.168.1.11\""));
|
||||
Assertions.assertTrue(json.contains("\"node02Ip\":\"192.168.1.12\""));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ class ResourceBackupEndToEndContractTest {
|
||||
Assertions.assertTrue(backupController.contains("/api/v1/resource-backups"));
|
||||
Assertions.assertTrue(restoreController.contains("/api/v1/resource-restores"));
|
||||
Assertions.assertTrue(restoreController.contains("/precheck"));
|
||||
Assertions.assertTrue(restoreController.contains("/steps/{stepNo}/log"));
|
||||
Assertions.assertTrue(restoreController.contains("/{taskId}/apply"));
|
||||
Assertions.assertTrue(restoreController.contains("/latest"));
|
||||
Assertions.assertFalse(restoreController.contains("/steps/{stepNo}/log"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -23,6 +25,6 @@ class ResourceBackupEndToEndContractTest {
|
||||
String readme = Files.readString(Path.of("/Users/waner/Work/CISD/文档/tms-framework/README.md"));
|
||||
|
||||
Assertions.assertTrue(readme.contains("停机恢复"));
|
||||
Assertions.assertTrue(readme.contains("创建恢复任务后当前TMS服务可能中断"));
|
||||
Assertions.assertTrue(readme.contains("确认后调用 apply 接口进入停机恢复"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cisd.tms.modules.backup;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.support.BackupResourceKind;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredRestoreCommand;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -21,9 +22,12 @@ class ResourceBackupPropertiesTest {
|
||||
properties.getMqReplayScriptPath()
|
||||
);
|
||||
Assertions.assertTrue(properties.getAllowedRestoreRoots().contains("/home/tms/config"));
|
||||
Assertions.assertTrue(properties.getDefaultFileResources().stream()
|
||||
.anyMatch(item -> "TMS_CONFIG".equals(item.getCode())
|
||||
&& BackupResourceKind.FILE.name().equals(item.getKind())));
|
||||
Assertions.assertTrue(properties.getFileResources().isEmpty());
|
||||
Assertions.assertTrue(properties.getRestoreStopCommands().isEmpty());
|
||||
Assertions.assertTrue(properties.getRestoreStartCommands().isEmpty());
|
||||
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_role_account"));
|
||||
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_role_ukey_binding"));
|
||||
Assertions.assertTrue(properties.getTmsDbExcludedTables().contains("tms_auth_session"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -35,11 +39,37 @@ class ResourceBackupPropertiesTest {
|
||||
custom.setMode("FILE");
|
||||
custom.setPath("/home/tms/certs/custom.crt");
|
||||
custom.setRestorePath("/home/tms/certs/custom.crt");
|
||||
custom.setProductTypes(java.util.List.of("ENTERPRISE", "INDIRECT"));
|
||||
custom.setRequired(false);
|
||||
|
||||
properties.setFileResources(java.util.List.of(custom));
|
||||
|
||||
Assertions.assertEquals("CUSTOM_CERT", properties.getFileResources().get(0).getCode());
|
||||
Assertions.assertEquals(java.util.List.of("ENTERPRISE", "INDIRECT"), properties.getFileResources().get(0).getProductTypes());
|
||||
Assertions.assertFalse(properties.getFileResources().get(0).isRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptConfiguredRestoreStartAndStopCommands() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
ConfiguredRestoreCommand stop = restoreCommand("STANDARD_APPS", "/home/tms/bin/stop_standard_apps.sh", false);
|
||||
ConfiguredRestoreCommand start = restoreCommand("STANDARD_APPS", "/home/tms/bin/start_standard_apps.sh", true);
|
||||
|
||||
properties.setRestoreStopCommands(java.util.List.of(stop));
|
||||
properties.setRestoreStartCommands(java.util.List.of(start));
|
||||
|
||||
Assertions.assertEquals("STANDARD_APPS", properties.getRestoreStopCommands().get(0).getCode());
|
||||
Assertions.assertFalse(properties.getRestoreStopCommands().get(0).isRequired());
|
||||
Assertions.assertEquals("/home/tms/bin/start_standard_apps.sh", properties.getRestoreStartCommands().get(0).getCommand());
|
||||
Assertions.assertEquals(java.util.List.of("ENTERPRISE", "INDIRECT"), properties.getRestoreStartCommands().get(0).getProductTypes());
|
||||
}
|
||||
|
||||
private static ConfiguredRestoreCommand restoreCommand(String code, String command, boolean required) {
|
||||
ConfiguredRestoreCommand item = new ConfiguredRestoreCommand();
|
||||
item.setCode(code);
|
||||
item.setCommand(command);
|
||||
item.setRequired(required);
|
||||
item.setProductTypes(java.util.List.of("ENTERPRISE", "INDIRECT"));
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-16
@@ -6,7 +6,6 @@ import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourceBackupService;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
@@ -42,7 +41,7 @@ class ResourceBackupControllerTest {
|
||||
CreateResourceBackupResponse createBackup = new CreateResourceBackupResponse();
|
||||
createBackup.setTaskId("RBKP-TASK-20260408-000001");
|
||||
createBackup.setStatus("PENDING");
|
||||
Mockito.when(backupService.createBackupTask(Mockito.any(CreateResourceBackupRequest.class))).thenReturn(createBackup);
|
||||
Mockito.when(backupService.createBackupTask(Mockito.nullable(CreateResourceBackupRequest.class))).thenReturn(createBackup);
|
||||
|
||||
ResourceBackupTaskDetailResponse backupDetail = new ResourceBackupTaskDetailResponse();
|
||||
backupDetail.setTaskId("RBKP-TASK-20260408-000001");
|
||||
@@ -70,13 +69,6 @@ class ResourceBackupControllerTest {
|
||||
createRestore.setStatus("PENDING");
|
||||
Mockito.when(restoreService.createRestoreTask(Mockito.any(CreateResourceRestoreRequest.class))).thenReturn(createRestore);
|
||||
|
||||
ResourceRestoreTaskDetailResponse restoreDetail = new ResourceRestoreTaskDetailResponse();
|
||||
restoreDetail.setTaskId("RRST-TASK-20260408-000001");
|
||||
restoreDetail.setStatus("APPLYING");
|
||||
restoreDetail.setControlPhase("SHELL_APPLY");
|
||||
restoreDetail.setStateSource("SHELL_STATUS");
|
||||
Mockito.when(taskQueryService.getRestoreTask("RRST-TASK-20260408-000001")).thenReturn(restoreDetail);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||
new ResourceBackupController(backupService),
|
||||
new ResourceRestoreController(precheckService, restoreService, taskQueryService)
|
||||
@@ -84,7 +76,11 @@ class ResourceBackupControllerTest {
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-backups")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"includeUpgradeHistory\":true,\"includeAuditBackups\":false,\"remark\":\"before hardware replacement\"}"))
|
||||
.content("{\"remark\":\"before hardware replacement\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"RBKP-TASK-20260408-000001\"")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-backups"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"RBKP-TASK-20260408-000001\"")));
|
||||
|
||||
@@ -106,13 +102,8 @@ class ResourceBackupControllerTest {
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-restores")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"precheckId\":\"RPRE-20260408-000001\",\"confirmBackupId\":\"RBKP-20260408-153000-000001\",\"confirmRiskAccepted\":true}"))
|
||||
.content("{\"precheckId\":\"RPRE-20260408-000001\",\"confirmBackupId\":\"RBKP-20260408-153000-000001\",\"deployMode\":\"DUAL\",\"nodes\":{\"node01Ip\":\"192.168.1.11\",\"node02Ip\":\"192.168.1.12\"}}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"RRST-TASK-20260408-000001\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260408-000001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"SHELL_APPLY\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"stateSource\":\"SHELL_STATUS\"")));
|
||||
}
|
||||
}
|
||||
|
||||
+26
-34
@@ -3,14 +3,11 @@ package com.cisd.tms.modules.backup.controller;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -25,7 +22,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
class ResourceRestoreControllerTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeRestoreEndpointsIncludingStepsAndLogs() throws Exception {
|
||||
void shouldExposeTwoPhaseRestoreEndpointsAndLatestResult() throws Exception {
|
||||
ResourcePrecheckService precheckService = Mockito.mock(ResourcePrecheckService.class);
|
||||
ResourceRestoreService restoreService = Mockito.mock(ResourceRestoreService.class);
|
||||
ResourceTaskQueryService taskQueryService = Mockito.mock(ResourceTaskQueryService.class);
|
||||
@@ -37,28 +34,20 @@ class ResourceRestoreControllerTest {
|
||||
|
||||
CreateResourceRestoreResponse create = new CreateResourceRestoreResponse();
|
||||
create.setTaskId("RRST-TASK-20260414-000001");
|
||||
create.setStatus("APPLYING");
|
||||
create.setStatus("READY_TO_APPLY");
|
||||
Mockito.when(restoreService.createRestoreTask(Mockito.any(CreateResourceRestoreRequest.class))).thenReturn(create);
|
||||
|
||||
ResourceRestoreTaskDetailResponse detail = new ResourceRestoreTaskDetailResponse();
|
||||
detail.setTaskId("RRST-TASK-20260414-000001");
|
||||
detail.setControlPhase("SHELL_APPLY");
|
||||
detail.setStateSource("SHELL_STATUS");
|
||||
Mockito.when(taskQueryService.getRestoreTask("RRST-TASK-20260414-000001")).thenReturn(detail);
|
||||
|
||||
ResourceTaskStepItemResponse step = new ResourceTaskStepItemResponse();
|
||||
step.setStepNo(2);
|
||||
step.setStepCode("SHELL_APPLY");
|
||||
step.setStatus("SUCCESS");
|
||||
Mockito.when(taskQueryService.getRestoreTaskSteps("RRST-TASK-20260414-000001")).thenReturn(List.of(step));
|
||||
|
||||
ResourceTaskStepLogResponse log = new ResourceTaskStepLogResponse();
|
||||
log.setTaskId("RRST-TASK-20260414-000001");
|
||||
log.setStepNo(2);
|
||||
log.setStepCode("SHELL_APPLY");
|
||||
log.setContent("apply ok");
|
||||
log.setTruncated(false);
|
||||
Mockito.when(taskQueryService.getRestoreTaskStepLog("RRST-TASK-20260414-000001", 2)).thenReturn(log);
|
||||
CreateResourceRestoreResponse apply = new CreateResourceRestoreResponse();
|
||||
apply.setTaskId("RRST-TASK-20260414-000001");
|
||||
apply.setStatus("APPLYING");
|
||||
Mockito.when(restoreService.applyRestoreTask("RRST-TASK-20260414-000001")).thenReturn(apply);
|
||||
ResourceRestoreLatestResponse latest = new ResourceRestoreLatestResponse();
|
||||
latest.setTaskId("RRST-TASK-20260414-000001");
|
||||
latest.setBackupId("RBKP-20260414-000001");
|
||||
latest.setStatus("FAILED");
|
||||
latest.setPhase("DATABASE");
|
||||
latest.setMessage("数据库恢复失败");
|
||||
latest.setFinishedAt("2026-04-14T10:05:00+0800");
|
||||
Mockito.when(taskQueryService.getLatestRestore()).thenReturn(latest);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||
new ResourceRestoreController(precheckService, restoreService, taskQueryService)
|
||||
@@ -72,20 +61,23 @@ class ResourceRestoreControllerTest {
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-restores")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"precheckId\":\"RPRE-20260414-000001\",\"confirmBackupId\":\"RBKP-20260414-000001\",\"confirmRiskAccepted\":true}"))
|
||||
.content("{\"precheckId\":\"RPRE-20260414-000001\",\"confirmBackupId\":\"RBKP-20260414-000001\",\"deployMode\":\"DUAL\",\"nodes\":{\"node01Ip\":\"192.168.1.11\",\"node02Ip\":\"192.168.1.12\"}}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"RRST-TASK-20260414-000001\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001"))
|
||||
mockMvc.perform(post("/api/v1/resource-restores/RRST-TASK-20260414-000001/apply"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"SHELL_APPLY\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"status\":\"APPLYING\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001/steps"))
|
||||
mockMvc.perform(get("/api/v1/resource-restores/latest"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"stepCode\":\"SHELL_APPLY\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"status\":\"FAILED\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"phase\":\"DATABASE\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"message\":\"数据库恢复失败\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001/steps/2/log"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"content\":\"apply ok\"")));
|
||||
mockMvc.perform(post("/api/v1/resource-restores")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"precheckId\":\"RPRE-20260414-000001\",\"confirmBackupId\":\"RBKP-20260414-000001\",\"deployMode\":\"DUAL\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
|
||||
+9
-13
@@ -3,10 +3,9 @@ package com.cisd.tms.modules.backup.crypto;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.UserKeySignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@@ -19,9 +18,6 @@ class ResourceEnvelopeCryptoServiceTest {
|
||||
@Test
|
||||
void shouldWrapDekWithEncUserKeyAndSignWithAuthUserKey() {
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setStorageEncKeyIndex(7);
|
||||
properties.setAuthenticitySignKeyIndex(8);
|
||||
|
||||
AtomicReference<byte[]> capturedDek = new AtomicReference<>();
|
||||
AtomicReference<byte[]> decryptableDek = new AtomicReference<>();
|
||||
@@ -33,7 +29,7 @@ class ResourceEnvelopeCryptoServiceTest {
|
||||
decryptableDek.set(Arrays.copyOf(request.getData(), request.getData().length));
|
||||
return backup(new byte[]{0x11, 0x22, 0x33});
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any(EccInternalSignRequest.class)))
|
||||
Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any(UserKeySignRequest.class)))
|
||||
.thenReturn(backup(new byte[]{0x44, 0x55, 0x66}));
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
@@ -42,7 +38,7 @@ class ResourceEnvelopeCryptoServiceTest {
|
||||
return backup(decryptedDek);
|
||||
});
|
||||
|
||||
ResourceEnvelopeCryptoServiceImpl service = new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties);
|
||||
ResourceEnvelopeCryptoServiceImpl service = new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService);
|
||||
byte[] payload = "payload-data".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
byte[] signSource = "manifest|envelope|cipher".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
@@ -55,19 +51,19 @@ class ResourceEnvelopeCryptoServiceTest {
|
||||
|
||||
ArgumentCaptor<EccInternalEncryptRequest> encryptCaptor = ArgumentCaptor.forClass(EccInternalEncryptRequest.class);
|
||||
Mockito.verify(pcieCryptoService).sdfeInternalEncryptEcc(encryptCaptor.capture());
|
||||
Assertions.assertEquals(7, encryptCaptor.getValue().getKeyIndex());
|
||||
Assertions.assertEquals(1, encryptCaptor.getValue().getKeyIndex());
|
||||
|
||||
ArgumentCaptor<EccInternalSignRequest> signCaptor = ArgumentCaptor.forClass(EccInternalSignRequest.class);
|
||||
Mockito.verify(pcieCryptoService).eccInternalSign(signCaptor.capture());
|
||||
Assertions.assertEquals(8, signCaptor.getValue().getKeyIndex());
|
||||
ArgumentCaptor<UserKeySignRequest> signCaptor = ArgumentCaptor.forClass(UserKeySignRequest.class);
|
||||
Mockito.verify(pcieCryptoService).userKeySignWithSm3(signCaptor.capture());
|
||||
Assertions.assertEquals(1, signCaptor.getValue().getKeyIndex());
|
||||
|
||||
ArgumentCaptor<EccInternalVerifyRequest> verifyCaptor = ArgumentCaptor.forClass(EccInternalVerifyRequest.class);
|
||||
Mockito.verify(pcieCryptoService).eccInternalVerify(verifyCaptor.capture());
|
||||
Assertions.assertEquals(8, verifyCaptor.getValue().getKeyIndex());
|
||||
Assertions.assertEquals(1, verifyCaptor.getValue().getKeyIndex());
|
||||
|
||||
ArgumentCaptor<EccInternalDecryptRequest> decryptCaptor = ArgumentCaptor.forClass(EccInternalDecryptRequest.class);
|
||||
Mockito.verify(pcieCryptoService).sdfeInternalDecryptEcc(decryptCaptor.capture());
|
||||
Assertions.assertEquals(7, decryptCaptor.getValue().getKeyIndex());
|
||||
Assertions.assertEquals(1, decryptCaptor.getValue().getKeyIndex());
|
||||
Assertions.assertTrue(isAllZero(capturedDek.get()), "generated SM4 DEK should be wiped after sealing");
|
||||
Assertions.assertTrue(isAllZero(decryptResultDek.get()), "decrypted SM4 DEK result buffer should be wiped after unsealing");
|
||||
}
|
||||
|
||||
+115
-14
@@ -1,7 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.discovery;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@@ -11,15 +12,20 @@ class ResourceDiscoveryServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldDiscoverStandardRabbitResourcesWithoutExcludedArtifacts() {
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(sampleProperties());
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(defaultBackupProperties());
|
||||
|
||||
List<DiscoveredResourceItem> items = service.discover(sampleInitTask("ENTERPRISE", "RABBITMQ"));
|
||||
|
||||
Assertions.assertTrue(containsCode(items, "TMS_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "TMS_DB"));
|
||||
Assertions.assertTrue(containsCode(items, "CMEP_DB"));
|
||||
Assertions.assertTrue(containsCode(items, "ORG_DB"));
|
||||
Assertions.assertTrue(hasDatabaseName(items, "ORG_DB", "AAAABBBBXXX"));
|
||||
Assertions.assertTrue(hasEntryPath(items, "ORG_DB", "db/AAAABBBBXXX.sql"));
|
||||
Assertions.assertTrue(containsCode(items, "CP_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "RECEIVER_LICENSE"));
|
||||
Assertions.assertTrue(containsCode(items, "STANDARD_CMSP_APP_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "STANDARD_CMTP_APP_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "USER_KEY"));
|
||||
Assertions.assertTrue(containsCode(items, "MQ_REPLAY"));
|
||||
Assertions.assertFalse(containsCode(items, "RABBITMQ_DEFINITIONS"));
|
||||
@@ -38,7 +44,7 @@ class ResourceDiscoveryServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldUseDirectPathsForDirectTlqResources() {
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(sampleProperties());
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(defaultBackupProperties());
|
||||
|
||||
List<DiscoveredResourceItem> items = service.discover(sampleInitTask("DIRECT", "RABBITMQ_TLQ"));
|
||||
|
||||
@@ -50,6 +56,70 @@ class ResourceDiscoveryServiceTest {
|
||||
Assertions.assertTrue(hasRequired(items, "USER_KEY"));
|
||||
Assertions.assertTrue(hasRequired(items, "MQ_REPLAY"));
|
||||
Assertions.assertFalse(containsCode(items, "CMEP_DB"));
|
||||
Assertions.assertFalse(containsCode(items, "ORG_DB"));
|
||||
Assertions.assertFalse(containsCode(items, "STANDARD_CMSP_APP_CONFIG"));
|
||||
Assertions.assertFalse(containsCode(items, "STANDARD_CMTP_APP_CONFIG"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotDiscoverDirectCopyFileResourcesWhenFileResourcesAreEmpty() {
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(new ResourceBackupProperties());
|
||||
|
||||
List<DiscoveredResourceItem> items = service.discover(sampleInitTask("ENTERPRISE", "RABBITMQ"));
|
||||
|
||||
Assertions.assertFalse(containsCode(items, "TMS_CONFIG"));
|
||||
Assertions.assertFalse(containsCode(items, "CP_CONFIG"));
|
||||
Assertions.assertFalse(containsCode(items, "RECEIVER_LICENSE"));
|
||||
Assertions.assertFalse(containsCode(items, "STANDARD_CMSP_APP_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "TMS_DB"));
|
||||
Assertions.assertTrue(containsCode(items, "CMEP_DB"));
|
||||
Assertions.assertTrue(containsCode(items, "ORG_DB"));
|
||||
Assertions.assertTrue(containsCode(items, "USER_KEY"));
|
||||
Assertions.assertTrue(containsCode(items, "MQ_REPLAY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendConfiguredFileResources() {
|
||||
ResourceBackupProperties backupProperties = new ResourceBackupProperties();
|
||||
ConfiguredFileBackupResource custom = new ConfiguredFileBackupResource();
|
||||
custom.setCode("CUSTOM_CERT");
|
||||
custom.setMode("FILE");
|
||||
custom.setPath("/home/tms/config/custom-cert.pem");
|
||||
custom.setRestorePath("/home/tms/config/custom-cert.pem");
|
||||
custom.setRequired(false);
|
||||
backupProperties.setFileResources(List.of(custom));
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(backupProperties);
|
||||
|
||||
List<DiscoveredResourceItem> items = service.discover(sampleInitTask("ENTERPRISE", "RABBITMQ"));
|
||||
|
||||
Assertions.assertTrue(hasPath(items, "CUSTOM_CERT", "/home/tms/config/custom-cert.pem"));
|
||||
Assertions.assertFalse(hasRequired(items, "CUSTOM_CERT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDiscoverStandardAppConfigOnlyWhenConfiguredAsFileResources() {
|
||||
ResourceBackupProperties backupProperties = new ResourceBackupProperties();
|
||||
ConfiguredFileBackupResource cmsp = new ConfiguredFileBackupResource();
|
||||
cmsp.setCode("STANDARD_CMSP_APP_CONFIG");
|
||||
cmsp.setMode("GLOB");
|
||||
cmsp.setPath("/home/cmep4i/cmsp/application-prd*.properties");
|
||||
cmsp.setRestorePath("/home/cmep4i/cmsp");
|
||||
cmsp.setRequired(true);
|
||||
ConfiguredFileBackupResource cmtp = new ConfiguredFileBackupResource();
|
||||
cmtp.setCode("STANDARD_CMTP_APP_CONFIG");
|
||||
cmtp.setMode("GLOB");
|
||||
cmtp.setPath("/home/cmep4i/cmtp/application-prd*.properties");
|
||||
cmtp.setRestorePath("/home/cmep4i/cmtp");
|
||||
cmtp.setRequired(true);
|
||||
backupProperties.setFileResources(List.of(cmsp, cmtp));
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(backupProperties);
|
||||
|
||||
List<DiscoveredResourceItem> items = service.discover(sampleInitTask("ENTERPRISE", "RABBITMQ"));
|
||||
|
||||
Assertions.assertTrue(hasPath(items, "STANDARD_CMSP_APP_CONFIG", "/home/cmep4i/cmsp/application-prd*.properties"));
|
||||
Assertions.assertTrue(hasPath(items, "STANDARD_CMTP_APP_CONFIG", "/home/cmep4i/cmtp/application-prd*.properties"));
|
||||
Assertions.assertTrue(hasMode(items, "STANDARD_CMSP_APP_CONFIG", "GLOB"));
|
||||
Assertions.assertTrue(hasRestorePath(items, "STANDARD_CMTP_APP_CONFIG", "/home/cmep4i/cmtp"));
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask(String productType, String mqType) {
|
||||
@@ -64,20 +134,35 @@ class ResourceDiscoveryServiceTest {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static InitExecutorProperties sampleProperties() {
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setTmsConfigDir("/home/tms/config");
|
||||
properties.setStandardCpconfigPath("/home/cmep4i/cpconfig.cfg");
|
||||
properties.setDirectCpconfigTargetPath("/opt/cpconfig.cfg");
|
||||
properties.setStandardReceiverLicenseTargetPath("/home/cmep4i/cmep/license/cmep.license");
|
||||
properties.setDirectReceiverLicenseTargetPath("/home/cmep4d/cmep/license/cmep.license");
|
||||
properties.setStandardTlqLicenseTargetPath("/home/tlq/tlqconf.zip");
|
||||
properties.setDirectTlqLicenseTargetPath("/home/tlq/TLQ8/license.dat");
|
||||
properties.setCfmqTargetDir("/pmts/cfg");
|
||||
properties.setRabbitmqAdminCommand("/usr/sbin/rabbitmqadmin");
|
||||
private static ResourceBackupProperties defaultBackupProperties() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
ConfiguredFileBackupResource tmsConfig = configured("TMS_CONFIG", "DIR", "/home/tms/config", "/home/tms/config", true);
|
||||
ConfiguredFileBackupResource standardCpConfig = configured("CP_CONFIG", "FILE", "/home/cmep4i/cpconfig.cfg", "/home/cmep4i/cpconfig.cfg", true);
|
||||
standardCpConfig.setProductTypes(List.of("ENTERPRISE", "INDIRECT"));
|
||||
ConfiguredFileBackupResource directCpConfig = configured("CP_CONFIG", "FILE", "/opt/cpconfig.cfg", "/opt/cpconfig.cfg", true);
|
||||
directCpConfig.setProductTypes(List.of("DIRECT"));
|
||||
ConfiguredFileBackupResource standardLicense = configured("RECEIVER_LICENSE", "FILE", "/home/cmep4i/cmep/license/cmep.license", "/home/cmep4i/cmep/license/cmep.license", true);
|
||||
standardLicense.setProductTypes(List.of("ENTERPRISE", "INDIRECT"));
|
||||
ConfiguredFileBackupResource directLicense = configured("RECEIVER_LICENSE", "FILE", "/home/cmep4d/cmep/license/cmep.license", "/home/cmep4d/cmep/license/cmep.license", true);
|
||||
directLicense.setProductTypes(List.of("DIRECT"));
|
||||
ConfiguredFileBackupResource cmsp = configured("STANDARD_CMSP_APP_CONFIG", "GLOB", "/home/cmep4i/cmsp/application-prd*.properties", "/home/cmep4i/cmsp", true);
|
||||
cmsp.setProductTypes(List.of("ENTERPRISE", "INDIRECT"));
|
||||
ConfiguredFileBackupResource cmtp = configured("STANDARD_CMTP_APP_CONFIG", "GLOB", "/home/cmep4i/cmtp/application-prd*.properties", "/home/cmep4i/cmtp", true);
|
||||
cmtp.setProductTypes(List.of("ENTERPRISE", "INDIRECT"));
|
||||
properties.setFileResources(List.of(tmsConfig, standardCpConfig, directCpConfig, standardLicense, directLicense, cmsp, cmtp));
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static ConfiguredFileBackupResource configured(String code, String mode, String path, String restorePath, boolean required) {
|
||||
ConfiguredFileBackupResource resource = new ConfiguredFileBackupResource();
|
||||
resource.setCode(code);
|
||||
resource.setMode(mode);
|
||||
resource.setPath(path);
|
||||
resource.setRestorePath(restorePath);
|
||||
resource.setRequired(required);
|
||||
return resource;
|
||||
}
|
||||
|
||||
private static boolean containsCode(List<DiscoveredResourceItem> items, String code) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()));
|
||||
}
|
||||
@@ -86,6 +171,22 @@ class ResourceDiscoveryServiceTest {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && path.equals(item.getResolvedPath()));
|
||||
}
|
||||
|
||||
private static boolean hasRestorePath(List<DiscoveredResourceItem> items, String code, String path) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && path.equals(item.getRestorePath()));
|
||||
}
|
||||
|
||||
private static boolean hasMode(List<DiscoveredResourceItem> items, String code, String mode) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && mode.equals(item.getMode()));
|
||||
}
|
||||
|
||||
private static boolean hasDatabaseName(List<DiscoveredResourceItem> items, String code, String databaseName) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && databaseName.equals(item.getDatabaseName()));
|
||||
}
|
||||
|
||||
private static boolean hasEntryPath(List<DiscoveredResourceItem> items, String code, String entryPath) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && entryPath.equals(item.getEntryPath()));
|
||||
}
|
||||
|
||||
private static boolean hasRequired(List<DiscoveredResourceItem> items, String code) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && item.isRequired());
|
||||
}
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class MysqlDatabaseBackupCollectorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExcludeConfiguredTablesOnlyWhenDumpingTmsDatabase() throws Exception {
|
||||
Path argsLog = tempDir.resolve("mysqldump-args.log");
|
||||
Path mysqldump = fakeMysqldump(argsLog);
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setMysqldumpPath(mysqldump.toString());
|
||||
properties.setTmsDbExcludedTables(List.of(
|
||||
"tms_role_account",
|
||||
"tms_auth_session",
|
||||
"CustomDb.already_qualified"
|
||||
));
|
||||
MysqlDatabaseBackupCollector collector = new MysqlDatabaseBackupCollector(
|
||||
properties,
|
||||
"jdbc:mysql://127.0.0.1:4000/TMS?useSSL=false",
|
||||
"root",
|
||||
"secret"
|
||||
);
|
||||
|
||||
DatabaseBackupCollector.CollectedDatabaseDump tmsDump = collector.collectTmsDatabaseDump();
|
||||
collector.collectDatabaseDump("ORG001", "ORG001.sql");
|
||||
|
||||
Assertions.assertEquals("TMS", tmsDump.getDatabaseName());
|
||||
Assertions.assertEquals("TMS.sql", tmsDump.getFileName());
|
||||
Assertions.assertEquals("-- synthetic dump\n", new String(tmsDump.getContent()));
|
||||
String[] invocations = Files.readString(argsLog).split(System.lineSeparator());
|
||||
Assertions.assertTrue(invocations[0].contains("--ignore-table=TMS.tms_role_account"));
|
||||
Assertions.assertTrue(invocations[0].contains("--ignore-table=TMS.tms_auth_session"));
|
||||
Assertions.assertTrue(invocations[0].contains("--ignore-table=CustomDb.already_qualified"));
|
||||
Assertions.assertFalse(invocations[1].contains("--ignore-table="));
|
||||
}
|
||||
|
||||
private Path fakeMysqldump(Path argsLog) throws Exception {
|
||||
Path script = tempDir.resolve("mysqldump");
|
||||
Files.writeString(script, """
|
||||
#!/usr/bin/env bash
|
||||
printf '%%s\\n' "$*" >> "%s"
|
||||
printf -- '-- synthetic dump\\n'
|
||||
""".formatted(argsLog));
|
||||
script.toFile().setExecutable(true);
|
||||
return script;
|
||||
}
|
||||
}
|
||||
+74
-12
@@ -50,7 +50,7 @@ class ResourcePackageServiceTest {
|
||||
EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getData());
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any()))
|
||||
Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any()))
|
||||
.thenReturn(backup(new byte[]{0x11, 0x22, 0x33}));
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
@@ -60,7 +60,7 @@ class ResourcePackageServiceTest {
|
||||
Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any());
|
||||
ResourcePackageService service = new ResourcePackageServiceImpl(
|
||||
properties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties),
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService),
|
||||
mockDatabaseBackupCollector(),
|
||||
new ObjectMapper()
|
||||
);
|
||||
@@ -73,6 +73,7 @@ class ResourcePackageServiceTest {
|
||||
resource("TMS_CONFIG", payloadFile.toString()),
|
||||
databaseResource("TMS_DB", "TMS", "db/TMS.sql"),
|
||||
databaseResource("CMEP_DB", "CMEP", "db/CMEP.sql"),
|
||||
databaseResource("ORG_DB", "AAAABBBBXXX", "db/AAAABBBBXXX.sql"),
|
||||
mqReplayResource()
|
||||
),
|
||||
sampleRequest()
|
||||
@@ -85,7 +86,7 @@ class ResourcePackageServiceTest {
|
||||
Assertions.assertFalse(result.getPackageHash().isBlank());
|
||||
Assertions.assertFalse(result.getStorageEncKeyFingerprint().isBlank());
|
||||
Assertions.assertFalse(result.getAuthenticityKeyFingerprint().isBlank());
|
||||
Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":4"));
|
||||
Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":5"));
|
||||
|
||||
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
|
||||
Assertions.assertNotNull(zipFile.getEntry("manifest.json"));
|
||||
@@ -103,7 +104,7 @@ class ResourcePackageServiceTest {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Map<String, Object>> resources = (java.util.List<Map<String, Object>>) inspectResult.getPayload().get("resources");
|
||||
Assertions.assertEquals(4, resources.size());
|
||||
Assertions.assertEquals(5, resources.size());
|
||||
Assertions.assertEquals(Boolean.TRUE, resources.get(0).get("collected"));
|
||||
Assertions.assertTrue(String.valueOf(resources.get(0).get("entryPath")).contains("application.yml"));
|
||||
Assertions.assertEquals("TMS_DB", resources.get(1).get("resourceCode"));
|
||||
@@ -112,9 +113,13 @@ class ResourcePackageServiceTest {
|
||||
Assertions.assertEquals("CMEP_DB", resources.get(2).get("resourceCode"));
|
||||
Assertions.assertEquals("DATABASE", resources.get(2).get("kind"));
|
||||
Assertions.assertEquals("db/CMEP.sql", resources.get(2).get("entryPath"));
|
||||
Assertions.assertEquals("MQ_REPLAY", resources.get(3).get("resourceCode"));
|
||||
Assertions.assertEquals("MQ_REPLAY", resources.get(3).get("kind"));
|
||||
Assertions.assertEquals("mq/mq-restore-context.json", resources.get(3).get("entryPath"));
|
||||
Assertions.assertEquals("ORG_DB", resources.get(3).get("resourceCode"));
|
||||
Assertions.assertEquals("DATABASE", resources.get(3).get("kind"));
|
||||
Assertions.assertEquals("AAAABBBBXXX", resources.get(3).get("databaseName"));
|
||||
Assertions.assertEquals("db/AAAABBBBXXX.sql", resources.get(3).get("entryPath"));
|
||||
Assertions.assertEquals("MQ_REPLAY", resources.get(4).get("resourceCode"));
|
||||
Assertions.assertEquals("MQ_REPLAY", resources.get(4).get("kind"));
|
||||
Assertions.assertEquals("mq/mq-restore-context.json", resources.get(4).get("entryPath"));
|
||||
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new java.io.ByteArrayInputStream(inspectResult.getPayloadArchiveBytes()))) {
|
||||
java.util.Map<String, String> entries = new java.util.LinkedHashMap<>();
|
||||
@@ -128,6 +133,8 @@ class ResourcePackageServiceTest {
|
||||
Assertions.assertTrue(entries.get("db/TMS.sql").contains("INSERT INTO t_demo"));
|
||||
Assertions.assertTrue(entries.containsKey("db/CMEP.sql"));
|
||||
Assertions.assertTrue(entries.get("db/CMEP.sql").contains("INSERT INTO c_demo"));
|
||||
Assertions.assertTrue(entries.containsKey("db/AAAABBBBXXX.sql"));
|
||||
Assertions.assertTrue(entries.get("db/AAAABBBBXXX.sql").contains("INSERT INTO org_demo"));
|
||||
Assertions.assertTrue(entries.containsKey("mq/mq-restore-context.json"));
|
||||
Assertions.assertTrue(entries.get("mq/mq-restore-context.json").contains("\"channelUsername\":\"tms\""));
|
||||
}
|
||||
@@ -140,7 +147,7 @@ class ResourcePackageServiceTest {
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
ResourcePackageService service = new ResourcePackageServiceImpl(
|
||||
properties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties),
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService),
|
||||
mockDatabaseBackupCollector(),
|
||||
new ObjectMapper()
|
||||
);
|
||||
@@ -160,6 +167,58 @@ class ResourcePackageServiceTest {
|
||||
Assertions.assertTrue(exception.getMessage().contains("PATH_NOT_FOUND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCollectGlobFileResourcesWithRestoreDirectory() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setOutputDir(tempDir.toString());
|
||||
Path cmspDir = tempDir.resolve("cmsp");
|
||||
Files.createDirectories(cmspDir);
|
||||
Files.writeString(cmspDir.resolve("application-prd.properties"), "spring.rabbitmq.username=user1");
|
||||
Files.writeString(cmspDir.resolve("application-prd-node2.properties"), "spring.rabbitmq.username=user2");
|
||||
Files.writeString(cmspDir.resolve("application-dev.properties"), "spring.rabbitmq.username=dev");
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> backup(invocation.<EccInternalEncryptRequest>getArgument(0).getData()));
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> backup(invocation.<EccInternalDecryptRequest>getArgument(0).getCipherBlob()));
|
||||
Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any()))
|
||||
.thenReturn(backup(new byte[]{0x11, 0x22, 0x33}));
|
||||
Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any());
|
||||
ResourcePackageService service = new ResourcePackageServiceImpl(
|
||||
properties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService),
|
||||
mockDatabaseBackupCollector(),
|
||||
new ObjectMapper()
|
||||
);
|
||||
DiscoveredResourceItem globResource = resource(
|
||||
"STANDARD_CMSP_APP_CONFIG",
|
||||
cmspDir.resolve("application-prd*.properties").toString()
|
||||
);
|
||||
globResource.setMode("GLOB");
|
||||
globResource.setRestorePath("/home/cmep4i/cmsp");
|
||||
|
||||
ResourcePackageResult result = service.createPackage(
|
||||
"RBKP-TASK-20260413-000001",
|
||||
"RBKP-20260413-153000-000001",
|
||||
sampleInitTask(),
|
||||
List.of(globResource),
|
||||
sampleRequest()
|
||||
);
|
||||
|
||||
ResourcePackageInspectResult inspectResult = service.inspectPackage(Path.of(result.getPackagePath()));
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Map<String, Object>> resources = (java.util.List<Map<String, Object>>) inspectResult.getPayload().get("resources");
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Map<String, Object>> files = (java.util.List<Map<String, Object>>) resources.get(0).get("files");
|
||||
Assertions.assertEquals(2, files.size());
|
||||
Assertions.assertTrue(files.stream().anyMatch(file ->
|
||||
"resources/STANDARD_CMSP_APP_CONFIG/application-prd.properties".equals(file.get("entryPath"))
|
||||
&& "/home/cmep4i/cmsp/application-prd.properties".equals(file.get("restorePath"))));
|
||||
Assertions.assertTrue(files.stream().anyMatch(file ->
|
||||
"resources/STANDARD_CMSP_APP_CONFIG/application-prd-node2.properties".equals(file.get("entryPath"))
|
||||
&& "/home/cmep4i/cmsp/application-prd-node2.properties".equals(file.get("restorePath"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCollectUserKeysFromEntityCertificateKeyIndexes() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
@@ -167,7 +226,7 @@ class ResourcePackageServiceTest {
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> backup(invocation.<EccInternalEncryptRequest>getArgument(0).getData()));
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any()))
|
||||
Mockito.when(pcieCryptoService.userKeySignWithSm3(Mockito.any()))
|
||||
.thenReturn(backup(new byte[]{0x11, 0x22, 0x33}));
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> backup(invocation.<EccInternalDecryptRequest>getArgument(0).getCipherBlob()));
|
||||
@@ -180,7 +239,7 @@ class ResourcePackageServiceTest {
|
||||
Mockito.when(keyEntityRepository.findAll()).thenReturn(List.of(keyEntity(3), keyEntity(2), keyEntity(2)));
|
||||
ResourcePackageService service = new ResourcePackageServiceImpl(
|
||||
properties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties),
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService),
|
||||
mockDatabaseBackupCollector(),
|
||||
new ResourceUserKeyBackupCollector(keyEntityRepository, pcieCryptoService, new ObjectMapper()),
|
||||
new MqReplayContextBuilder(new ObjectMapper()),
|
||||
@@ -297,6 +356,11 @@ class ResourcePackageServiceTest {
|
||||
cmepDump.setContent("CREATE TABLE c_demo(id bigint);\nINSERT INTO c_demo VALUES (2);\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
Mockito.when(collector.collectTmsDatabaseDump()).thenReturn(tmsDump);
|
||||
Mockito.when(collector.collectCmepDatabaseDump()).thenReturn(cmepDump);
|
||||
DatabaseBackupCollector.CollectedDatabaseDump orgDump = new DatabaseBackupCollector.CollectedDatabaseDump();
|
||||
orgDump.setDatabaseName("AAAABBBBXXX");
|
||||
orgDump.setFileName("AAAABBBBXXX.sql");
|
||||
orgDump.setContent("CREATE TABLE org_demo(id bigint);\nINSERT INTO org_demo VALUES (3);\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
Mockito.when(collector.collectDatabaseDump("AAAABBBBXXX", "AAAABBBBXXX.sql")).thenReturn(orgDump);
|
||||
return collector;
|
||||
}
|
||||
|
||||
@@ -309,8 +373,6 @@ class ResourcePackageServiceTest {
|
||||
|
||||
private static CreateResourceBackupRequest sampleRequest() {
|
||||
CreateResourceBackupRequest request = new CreateResourceBackupRequest();
|
||||
request.setIncludeUpgradeHistory(true);
|
||||
request.setIncludeAuditBackups(false);
|
||||
request.setRemark("before hardware replacement");
|
||||
return request;
|
||||
}
|
||||
|
||||
+3
-1
@@ -29,6 +29,7 @@ class ResourcePayloadExtractorTest {
|
||||
{"code":"TMS_CONFIG","kind":"FILE","restorePath":"%s/config",
|
||||
"files":[{"entryPath":"files/TMS_CONFIG/application.yml","restorePath":"%s/config/application.yml"}]},
|
||||
{"code":"TMS_DB","kind":"DATABASE","entryPath":"db/TMS.sql"},
|
||||
{"code":"ORG_DB","kind":"DATABASE","databaseName":"AAAABBBBXXX","entryPath":"db/AAAABBBBXXX.sql"},
|
||||
{"code":"MQ_REPLAY","kind":"MQ_REPLAY","entryPath":"mq/mq-restore-context.json"}
|
||||
]}
|
||||
""".formatted(tempDir, tempDir),
|
||||
@@ -53,7 +54,8 @@ class ResourcePayloadExtractorTest {
|
||||
Files.readString(workDir.resolve("restore-files.tsv"))
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
"db/TMS.sql\tTMS" + System.lineSeparator(),
|
||||
"db/TMS.sql\tTMS" + System.lineSeparator()
|
||||
+ "db/AAAABBBBXXX.sql\tAAAABBBBXXX" + System.lineSeparator(),
|
||||
Files.readString(workDir.resolve("restore-databases.tsv"))
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
|
||||
@@ -2,15 +2,11 @@ package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceBackupTaskMapper;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceRestoreTaskMapper;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceTaskStepMapper;
|
||||
import com.cisd.tms.modules.backup.repository.impl.ResourceBackupTaskRepositoryImpl;
|
||||
import com.cisd.tms.modules.backup.repository.impl.ResourceRestoreTaskRepositoryImpl;
|
||||
import com.cisd.tms.modules.backup.repository.impl.ResourceTaskStepRepositoryImpl;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
@@ -82,33 +78,6 @@ class ResourceTaskRepositoryTest {
|
||||
Assertions.assertEquals("NONE", found.getErrorCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveAndQueryTaskStepsByTaskId() {
|
||||
ResourceTaskStepMapper mapper = Mockito.mock(ResourceTaskStepMapper.class);
|
||||
ResourceTaskStepRepository repository = new ResourceTaskStepRepositoryImpl(mapper);
|
||||
ResourceTaskStepEntity entity = new ResourceTaskStepEntity();
|
||||
entity.setTaskId("RRST-001");
|
||||
entity.setTaskType("RESOURCE_RESTORE");
|
||||
entity.setStepNo(1);
|
||||
entity.setStepCode("SHELL_APPLY");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setLogPath("/home/tms/tmp/resource-restore/RRST-001/restore.log");
|
||||
entity.setExitCode(0);
|
||||
|
||||
repository.save(entity);
|
||||
|
||||
Mockito.verify(mapper).insert(entity);
|
||||
Mockito.when(mapper.selectByTaskIdOrderByStepNo("RRST-001")).thenReturn(List.of(entity));
|
||||
|
||||
List<ResourceTaskStepEntity> steps = repository.findByTaskIdOrderByStepNo("RRST-001");
|
||||
|
||||
Assertions.assertEquals(1, steps.size());
|
||||
Assertions.assertEquals("RESOURCE_RESTORE", steps.get(0).getTaskType());
|
||||
Assertions.assertEquals("SHELL_APPLY", steps.get(0).getStepCode());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/restore.log", steps.get(0).getLogPath());
|
||||
Assertions.assertEquals(0, steps.get(0).getExitCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeclareExplicitSelectColumnsInMappers() throws Exception {
|
||||
assertSelectSql(
|
||||
@@ -132,16 +101,6 @@ class ResourceTaskRepositoryTest {
|
||||
"consistency_report_json",
|
||||
"error_code"
|
||||
);
|
||||
assertSelectSql(
|
||||
ResourceTaskStepMapper.class.getMethod("selectByTaskIdOrderByStepNo", String.class),
|
||||
"tms_resource_task_step",
|
||||
"task_id",
|
||||
"task_type",
|
||||
"step_no",
|
||||
"step_code",
|
||||
"log_path",
|
||||
"exit_code"
|
||||
);
|
||||
}
|
||||
|
||||
private static void assertSelectSql(Method method, String tableName, String... requiredColumns) {
|
||||
|
||||
+4
-54
@@ -27,12 +27,7 @@ class ResourceTaskSchemaContractTest {
|
||||
"source_product_type VARCHAR(16) NOT NULL",
|
||||
"target_product_type VARCHAR(16) NOT NULL",
|
||||
"consistency_report_json JSON NULL",
|
||||
"error_code VARCHAR(64) NULL",
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_task_step",
|
||||
"task_type VARCHAR(32) NOT NULL",
|
||||
"step_code VARCHAR(64) NOT NULL",
|
||||
"log_path VARCHAR(1024) NULL",
|
||||
"exit_code INT NULL");
|
||||
"error_code VARCHAR(64) NULL");
|
||||
|
||||
assertContainsAll(v1Sql,
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_backup_task",
|
||||
@@ -49,46 +44,10 @@ class ResourceTaskSchemaContractTest {
|
||||
"source_product_type VARCHAR(16) NOT NULL",
|
||||
"target_product_type VARCHAR(16) NOT NULL",
|
||||
"consistency_report_json JSON NULL",
|
||||
"error_code VARCHAR(64) NULL",
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_task_step",
|
||||
"task_type VARCHAR(32) NOT NULL",
|
||||
"step_code VARCHAR(64) NOT NULL",
|
||||
"log_path VARCHAR(1024) NULL",
|
||||
"exit_code INT NULL");
|
||||
"error_code VARCHAR(64) NULL");
|
||||
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceTaskStatus",
|
||||
"PENDING",
|
||||
"PRECHECKING",
|
||||
"PRECHECK_FAILED",
|
||||
"READY",
|
||||
"APPLYING",
|
||||
"VERIFYING",
|
||||
"SUCCESS",
|
||||
"FAILED",
|
||||
"CANCELLED"
|
||||
);
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceTaskType",
|
||||
"RESOURCE_BACKUP",
|
||||
"RESOURCE_RESTORE"
|
||||
);
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceControlPhase",
|
||||
"TMS_PRECHECK",
|
||||
"JAVA_EXTRACT",
|
||||
"SHELL_APPLY",
|
||||
"DONE"
|
||||
);
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceStepStatus",
|
||||
"PENDING",
|
||||
"RUNNING",
|
||||
"SUCCESS",
|
||||
"SUCCESS_WITH_WARNINGS",
|
||||
"FAILED",
|
||||
"SKIPPED"
|
||||
);
|
||||
Assertions.assertFalse(v6Sql.contains("tms_resource_task_step"));
|
||||
Assertions.assertFalse(v1Sql.contains("tms_resource_task_step"));
|
||||
}
|
||||
|
||||
private static String readClasspathResource(String path) throws Exception {
|
||||
@@ -103,13 +62,4 @@ class ResourceTaskSchemaContractTest {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private static void assertEnumConstants(String className, String... expectedNames) throws Exception {
|
||||
Class<?> enumClass = Class.forName(className);
|
||||
Assertions.assertTrue(enumClass.isEnum(), className + " should be an enum");
|
||||
Class<? extends Enum> typedEnumClass = (Class<? extends Enum>) enumClass.asSubclass(Enum.class);
|
||||
for (String expectedName : expectedNames) {
|
||||
Enum.valueOf(typedEnumClass, expectedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -128,6 +128,29 @@ class ApplyResourceBackupScriptContractTest {
|
||||
Assertions.assertTrue(log.contains("http://127.0.0.1:8080/actuator/health"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunConfiguredStopAndStartCommandsInOrder() throws Exception {
|
||||
Path workDir = tempDir.resolve("work");
|
||||
Path payloadDir = workDir.resolve("payload");
|
||||
Files.createDirectories(payloadDir);
|
||||
Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}");
|
||||
Path commandLog = tempDir.resolve("service-commands.log");
|
||||
Path stopScript = fakeScript("stop-standard-apps.sh", commandLog);
|
||||
Path startTmsScript = fakeScript("start-tms.sh", commandLog);
|
||||
Path startStandardScript = fakeScript("start-standard-apps.sh", commandLog);
|
||||
Files.writeString(workDir.resolve("restore-stop-commands.tsv"),
|
||||
"STANDARD_APPS\ttrue\t" + stopScript + System.lineSeparator());
|
||||
Files.writeString(workDir.resolve("restore-start-commands.tsv"),
|
||||
"TMS\ttrue\t" + startTmsScript + System.lineSeparator()
|
||||
+ "STANDARD_APPS\ttrue\t" + startStandardScript + System.lineSeparator());
|
||||
|
||||
runScript(workDir, Map.of());
|
||||
|
||||
String log = Files.readString(commandLog);
|
||||
Assertions.assertTrue(log.indexOf("stop-standard-apps.sh") < log.indexOf("start-tms.sh"));
|
||||
Assertions.assertTrue(log.indexOf("start-tms.sh") < log.indexOf("start-standard-apps.sh"));
|
||||
}
|
||||
|
||||
private static void runScript(Path workDir, Map<String, String> environment) throws Exception {
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
"bash",
|
||||
|
||||
+40
-1
@@ -1,6 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredRestoreCommand;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
@@ -19,6 +20,22 @@ class LightweightRestoreApplierLauncherTest {
|
||||
properties.setRestoreApplyScriptPath("/home/tms/bin/resource-restore/apply-resource-backup.sh");
|
||||
properties.setRestoreDbScriptPath("/home/tms/bin/resource-restore/restore-db.sh");
|
||||
properties.setMqReplayScriptPath("/home/tms/bin/resource-restore/replay-mq.sh");
|
||||
ConfiguredRestoreCommand tmsStart = restoreCommand("TMS", "/home/tms/scripts/tms.sh start", true);
|
||||
ConfiguredRestoreCommand standardStart = restoreCommand(
|
||||
"STANDARD_APPS",
|
||||
"/home/tms/bin/start_standard_apps.sh",
|
||||
true,
|
||||
"ENTERPRISE",
|
||||
"INDIRECT"
|
||||
);
|
||||
ConfiguredRestoreCommand directOnlyStart = restoreCommand(
|
||||
"DIRECT_ONLY",
|
||||
"/home/tms/bin/start_direct_receiver.sh",
|
||||
false,
|
||||
"DIRECT"
|
||||
);
|
||||
properties.setRestoreStartCommands(List.of(tmsStart, standardStart, directOnlyStart));
|
||||
properties.setRestoreStopCommands(List.of(restoreCommand("TMS", "/home/tms/scripts/tms.sh stop", false)));
|
||||
RecordingInvoker invoker = new RecordingInvoker();
|
||||
LightweightRestoreApplierLauncher launcher = new LightweightRestoreApplierLauncher(
|
||||
properties,
|
||||
@@ -29,7 +46,7 @@ class LightweightRestoreApplierLauncherTest {
|
||||
);
|
||||
|
||||
Path workDir = tempDir.resolve("RRST-001");
|
||||
launcher.launch(workDir);
|
||||
launcher.launch(workDir, "ENTERPRISE");
|
||||
|
||||
Assertions.assertEquals(List.of(
|
||||
"/bin/bash",
|
||||
@@ -48,6 +65,28 @@ class LightweightRestoreApplierLauncherTest {
|
||||
Assertions.assertTrue(env.contains("export DB_PORT='4000'"));
|
||||
Assertions.assertTrue(env.contains("export DB_USER='root'"));
|
||||
Assertions.assertTrue(env.contains("export DB_PASSWORD='p'\\''ass'"));
|
||||
|
||||
String stopCommands = Files.readString(workDir.toAbsolutePath().normalize().resolve("restore-stop-commands.tsv"));
|
||||
Assertions.assertTrue(stopCommands.contains("TMS\tfalse\t/home/tms/scripts/tms.sh stop"));
|
||||
|
||||
String startCommands = Files.readString(workDir.toAbsolutePath().normalize().resolve("restore-start-commands.tsv"));
|
||||
Assertions.assertTrue(startCommands.contains("TMS\ttrue\t/home/tms/scripts/tms.sh start"));
|
||||
Assertions.assertTrue(startCommands.contains("STANDARD_APPS\ttrue\t/home/tms/bin/start_standard_apps.sh"));
|
||||
Assertions.assertFalse(startCommands.contains("DIRECT_ONLY"));
|
||||
}
|
||||
|
||||
private static ConfiguredRestoreCommand restoreCommand(
|
||||
String code,
|
||||
String command,
|
||||
boolean required,
|
||||
String... productTypes
|
||||
) {
|
||||
ConfiguredRestoreCommand item = new ConfiguredRestoreCommand();
|
||||
item.setCode(code);
|
||||
item.setCommand(command);
|
||||
item.setRequired(required);
|
||||
item.setProductTypes(List.of(productTypes));
|
||||
return item;
|
||||
}
|
||||
|
||||
private static final class RecordingInvoker implements RestoreCommandInvoker {
|
||||
|
||||
@@ -24,6 +24,34 @@ class RestoreCommandInvokerTest {
|
||||
);
|
||||
|
||||
Assertions.assertEquals(0, exitCode);
|
||||
waitUntilLogContains(logFile, "hello-restore");
|
||||
Assertions.assertEquals("hello-restore", Files.readString(logFile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStartCommandWithoutWaitingForCompletion() {
|
||||
ProcessBuilderRestoreCommandInvoker invoker = new ProcessBuilderRestoreCommandInvoker();
|
||||
Path logFile = tempDir.resolve("restore-detached.log");
|
||||
|
||||
long started = System.nanoTime();
|
||||
int exitCode = invoker.run(
|
||||
List.of("/bin/zsh", "-lc", "sleep 2; printf 'done'"),
|
||||
logFile,
|
||||
tempDir
|
||||
);
|
||||
long elapsedMillis = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
|
||||
|
||||
Assertions.assertEquals(0, exitCode);
|
||||
Assertions.assertTrue(elapsedMillis < 1000, "restore command should be detached from caller");
|
||||
}
|
||||
|
||||
private static void waitUntilLogContains(Path logFile, String expected) throws Exception {
|
||||
long deadline = System.currentTimeMillis() + 1000;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
if (Files.isRegularFile(logFile) && Files.readString(logFile).contains(expected)) {
|
||||
return;
|
||||
}
|
||||
Thread.sleep(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.discovery.ResourceDiscoveryService;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
@@ -17,6 +16,7 @@ import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -59,7 +59,6 @@ class ResourceBackupServiceTest {
|
||||
resourcePackageService,
|
||||
resourceBackupTaskRepository,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new ResourceBackupProperties(),
|
||||
admissionGuard
|
||||
);
|
||||
|
||||
@@ -107,7 +106,6 @@ class ResourceBackupServiceTest {
|
||||
resourcePackageService,
|
||||
resourceBackupTaskRepository,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new ResourceBackupProperties(),
|
||||
admissionGuard
|
||||
);
|
||||
|
||||
@@ -124,6 +122,62 @@ class ResourceBackupServiceTest {
|
||||
Assertions.assertNull(saved.getPackagePath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistRunningTaskWithConfiguredKeyFingerprintsBeforePackaging() {
|
||||
InitTaskRepository initTaskRepository = Mockito.mock(InitTaskRepository.class);
|
||||
ResourceDiscoveryService resourceDiscoveryService = Mockito.mock(ResourceDiscoveryService.class);
|
||||
ResourcePackageService resourcePackageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourceBackupTaskRepository resourceBackupTaskRepository = Mockito.mock(ResourceBackupTaskRepository.class);
|
||||
ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class);
|
||||
|
||||
InitTaskEntity initTask = sampleInitTask();
|
||||
List<DiscoveredResourceItem> discoveredItems = List.of(resource("TMS_CONFIG", "/home/tms/config", true));
|
||||
ResourcePackageResult packageResult = new ResourcePackageResult();
|
||||
packageResult.setPackageName("RBKP-20260413-153000-000001.tmsbak");
|
||||
packageResult.setPackagePath("/tmp/resource-backup/RBKP-TASK-20260413-000001/RBKP-20260413-153000-000001.tmsbak");
|
||||
packageResult.setPackageSize(2048L);
|
||||
packageResult.setPackageHash("SM3-HASH");
|
||||
packageResult.setStorageEncKeyFingerprint("PACKAGE-STORAGE-KEY");
|
||||
packageResult.setAuthenticityKeyFingerprint("PACKAGE-AUTH-KEY");
|
||||
packageResult.setManifestSummaryJson("{\"resourceCount\":1}");
|
||||
|
||||
AtomicReference<String> runningStatus = new AtomicReference<>();
|
||||
AtomicReference<String> runningStorageFingerprint = new AtomicReference<>();
|
||||
AtomicReference<String> runningAuthenticityFingerprint = new AtomicReference<>();
|
||||
Mockito.doAnswer(invocation -> {
|
||||
ResourceBackupTaskEntity entity = invocation.getArgument(0);
|
||||
runningStatus.set(entity.getStatus());
|
||||
runningStorageFingerprint.set(entity.getStorageEncKeyFingerprint());
|
||||
runningAuthenticityFingerprint.set(entity.getAuthenticityKeyFingerprint());
|
||||
return null;
|
||||
}).when(resourceBackupTaskRepository).save(Mockito.any(ResourceBackupTaskEntity.class));
|
||||
|
||||
Mockito.when(initTaskRepository.findLatestByTaskTypeAndStatus("INIT", "SUCCESS")).thenReturn(Optional.of(initTask));
|
||||
Mockito.when(resourceDiscoveryService.discover(initTask)).thenReturn(discoveredItems);
|
||||
Mockito.when(resourcePackageService.createPackage(
|
||||
Mockito.anyString(),
|
||||
Mockito.anyString(),
|
||||
Mockito.same(initTask),
|
||||
Mockito.eq(discoveredItems),
|
||||
Mockito.any(CreateResourceBackupRequest.class)
|
||||
)).thenReturn(packageResult);
|
||||
|
||||
ResourceBackupService service = new ResourceBackupServiceImpl(
|
||||
initTaskRepository,
|
||||
resourceDiscoveryService,
|
||||
resourcePackageService,
|
||||
resourceBackupTaskRepository,
|
||||
new ResourceTaskIdGenerator(),
|
||||
admissionGuard
|
||||
);
|
||||
|
||||
service.createBackupTask(sampleRequest());
|
||||
|
||||
Assertions.assertEquals("RUNNING", runningStatus.get());
|
||||
Assertions.assertEquals("USER_KEY_ENC:1", runningStorageFingerprint.get());
|
||||
Assertions.assertEquals("USER_KEY_SIGN:1", runningAuthenticityFingerprint.get());
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask() {
|
||||
InitTaskEntity entity = new InitTaskEntity();
|
||||
entity.setTaskId("INIT-20260408-000001");
|
||||
@@ -147,8 +201,6 @@ class ResourceBackupServiceTest {
|
||||
|
||||
private static CreateResourceBackupRequest sampleRequest() {
|
||||
CreateResourceBackupRequest request = new CreateResourceBackupRequest();
|
||||
request.setIncludeUpgradeHistory(true);
|
||||
request.setIncludeAuditBackups(false);
|
||||
request.setRemark("before hardware replacement");
|
||||
return request;
|
||||
}
|
||||
|
||||
+4
-8
@@ -1,7 +1,6 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
|
||||
@@ -30,9 +29,6 @@ class ResourcePrecheckLightweightServiceTest {
|
||||
void shouldExposeLightweightRestorePlanWithoutDecryptingPayload() {
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourceBackupProperties backupProperties = new ResourceBackupProperties();
|
||||
backupProperties.setStorageEncKeyIndex(7);
|
||||
backupProperties.setAuthenticitySignKeyIndex(9);
|
||||
CisdPresetProperties presetProperties = new CisdPresetProperties();
|
||||
presetProperties.setProductType("ENTERPRISE");
|
||||
InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore();
|
||||
@@ -50,8 +46,7 @@ class ResourcePrecheckLightweightServiceTest {
|
||||
packageService,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
presetProperties,
|
||||
backupProperties
|
||||
presetProperties
|
||||
);
|
||||
ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest();
|
||||
request.setFileId("FILE-20260429-000001");
|
||||
@@ -59,6 +54,7 @@ class ResourcePrecheckLightweightServiceTest {
|
||||
ResourceRestorePrecheckResponse response = service.precheck(request);
|
||||
|
||||
Assertions.assertEquals(List.of("JAVA_EXTRACT", "SHELL_APPLY"), response.getRestorePlan());
|
||||
Assertions.assertTrue(response.getPrecheckPassed());
|
||||
Assertions.assertFalse(response.getPayloadDecrypted());
|
||||
Assertions.assertTrue(response.getSignatureValid());
|
||||
Assertions.assertTrue(store.find(response.getPrecheckId()).isPresent());
|
||||
@@ -72,8 +68,8 @@ class ResourcePrecheckLightweightServiceTest {
|
||||
manifest.put("orgCode", "AAAABBBBXXX");
|
||||
manifest.put("backupTime", "2026-04-29T10:00:00");
|
||||
manifest.put("resourceCount", 3);
|
||||
manifest.put("storageEncKeyFingerprint", "USER_KEY_ENC:7");
|
||||
manifest.put("authenticityKeyFingerprint", "USER_KEY_SIGN:9");
|
||||
manifest.put("storageEncKeyFingerprint", "USER_KEY_ENC:1");
|
||||
manifest.put("authenticityKeyFingerprint", "USER_KEY_SIGN:1");
|
||||
ResourcePackageInspectResult result = new ResourcePackageInspectResult();
|
||||
result.setManifest(manifest);
|
||||
return result;
|
||||
|
||||
+120
-11
@@ -10,6 +10,7 @@ import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractor;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractionResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourceUserKeyRestoreService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.ActiveRestoreStateStore;
|
||||
import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceRestoreServiceImpl;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceTaskAdmissionGuard;
|
||||
@@ -34,7 +35,7 @@ class ResourceRestoreHandoffServiceTest {
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldCreateRestoreTaskExtractPayloadAndLaunchLightweightApplier() throws Exception {
|
||||
void shouldCreateRestoreTaskExtractPayloadAndWaitForApplyConfirmation() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setPrecheckStoreDir(tempDir.resolve("prechecks").toString());
|
||||
properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString());
|
||||
@@ -70,6 +71,8 @@ class ResourceRestoreHandoffServiceTest {
|
||||
Mockito.when(payloadExtractor.extract(Mockito.eq(packagePath), Mockito.any(Path.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
Path taskDir = invocation.getArgument(1);
|
||||
Files.createDirectories(taskDir);
|
||||
Files.writeString(taskDir.resolve("restore-context.json"), "{\"schemaVersion\":\"1.0\"}");
|
||||
ResourcePayloadExtractionResult result = new ResourcePayloadExtractionResult();
|
||||
result.setPayloadDir(taskDir.resolve("payload"));
|
||||
return result;
|
||||
@@ -84,13 +87,12 @@ class ResourceRestoreHandoffServiceTest {
|
||||
payloadExtractor,
|
||||
userKeyRestoreService,
|
||||
applierLauncher,
|
||||
admissionGuard
|
||||
admissionGuard,
|
||||
new ObjectMapper(),
|
||||
new ActiveRestoreStateStore(properties, new ObjectMapper())
|
||||
);
|
||||
|
||||
CreateResourceRestoreRequest request = new CreateResourceRestoreRequest();
|
||||
request.setPrecheckId("RPRE-20260413-000001");
|
||||
request.setConfirmBackupId("RBKP-20260413-153000-000001");
|
||||
request.setConfirmRiskAccepted(true);
|
||||
CreateResourceRestoreRequest request = validCreateRequest();
|
||||
|
||||
CreateResourceRestoreResponse response = service.createRestoreTask(request);
|
||||
|
||||
@@ -100,14 +102,77 @@ class ResourceRestoreHandoffServiceTest {
|
||||
Mockito.verify(repository).save(captor.capture());
|
||||
ResourceRestoreTaskEntity saved = captor.getValue();
|
||||
Assertions.assertEquals(packagePath.toString(), saved.getPackagePath());
|
||||
Assertions.assertEquals("APPLYING", saved.getStatus());
|
||||
Assertions.assertEquals("READY_TO_APPLY", saved.getStatus());
|
||||
Assertions.assertEquals(taskDir.resolve("payload").toString(), saved.getStagingDir());
|
||||
Assertions.assertEquals(taskDir.resolve("status.json").toString(), saved.getRunnerStatePath());
|
||||
Assertions.assertEquals("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]", saved.getRestorePlanJson());
|
||||
String restoreContext = Files.readString(taskDir.resolve("restore-context.json"));
|
||||
Assertions.assertTrue(restoreContext.contains("\"targetDeployMode\" : \"DUAL\""));
|
||||
Assertions.assertTrue(restoreContext.contains("\"node01Ip\" : \"192.168.1.11\""));
|
||||
Assertions.assertTrue(restoreContext.contains("\"node02Ip\" : \"192.168.1.12\""));
|
||||
|
||||
Mockito.verify(payloadExtractor).extract(packagePath, taskDir);
|
||||
Mockito.verify(userKeyRestoreService).recover(taskDir.resolve("payload"));
|
||||
Mockito.verify(applierLauncher).launch(taskDir);
|
||||
Mockito.verifyNoInteractions(applierLauncher);
|
||||
|
||||
String activeRestore = Files.readString(tempDir.resolve("resource-restore").resolve("active-restore.json"));
|
||||
Assertions.assertTrue(activeRestore.contains("\"taskId\" : \"" + response.getTaskId() + "\""));
|
||||
Assertions.assertTrue(activeRestore.contains("\"status\" : \"READY_TO_APPLY\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldApplyPreparedRestoreTaskAndLaunchLightweightApplier() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString());
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class);
|
||||
ResourceUserKeyRestoreService userKeyRestoreService = Mockito.mock(ResourceUserKeyRestoreService.class);
|
||||
LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class);
|
||||
ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class);
|
||||
Path taskDir = tempDir.resolve("resource-restore").resolve("RRST-TASK-20260413-000001");
|
||||
Files.createDirectories(taskDir);
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260413-000001");
|
||||
entity.setPrecheckId("RPRE-20260413-000001");
|
||||
entity.setBackupId("RBKP-20260413-153000-000001");
|
||||
entity.setStatus("READY_TO_APPLY");
|
||||
entity.setSourceProductType("ENTERPRISE");
|
||||
entity.setTargetProductType("ENTERPRISE");
|
||||
entity.setPackageFileId("FILE-20260413-000001");
|
||||
entity.setPackagePath(tempDir.resolve("download").resolve("RBKP.tmsbak").toString());
|
||||
entity.setStagingDir(taskDir.resolve("payload").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("status.json").toString());
|
||||
entity.setStorageEncKeyFingerprint("USER_KEY_ENC:1");
|
||||
entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:1");
|
||||
entity.setRestorePlanJson("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]");
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(LocalDateTime.parse("2026-04-13T15:30:00"));
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000001")).thenReturn(java.util.Optional.of(entity));
|
||||
ResourceRestoreService service = new ResourceRestoreServiceImpl(
|
||||
repository,
|
||||
fileRecordRepository,
|
||||
new FileSystemResourcePrecheckStateStore(properties, new ObjectMapper()),
|
||||
new ResourceTaskIdGenerator(),
|
||||
properties,
|
||||
payloadExtractor,
|
||||
userKeyRestoreService,
|
||||
applierLauncher,
|
||||
admissionGuard,
|
||||
new ObjectMapper(),
|
||||
new ActiveRestoreStateStore(properties, new ObjectMapper())
|
||||
);
|
||||
|
||||
CreateResourceRestoreResponse response = service.applyRestoreTask("RRST-TASK-20260413-000001");
|
||||
|
||||
Assertions.assertEquals("RRST-TASK-20260413-000001", response.getTaskId());
|
||||
Assertions.assertEquals("APPLYING", response.getStatus());
|
||||
Mockito.verify(repository).update(entity);
|
||||
Mockito.verify(applierLauncher).launch(taskDir, "ENTERPRISE");
|
||||
Assertions.assertEquals("APPLYING", entity.getStatus());
|
||||
Assertions.assertNotNull(entity.getStartTime());
|
||||
String activeRestore = Files.readString(tempDir.resolve("resource-restore").resolve("active-restore.json"));
|
||||
Assertions.assertTrue(activeRestore.contains("\"status\" : \"APPLYING\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,7 +199,9 @@ class ResourceRestoreHandoffServiceTest {
|
||||
payloadExtractor,
|
||||
userKeyRestoreService,
|
||||
applierLauncher,
|
||||
admissionGuard
|
||||
admissionGuard,
|
||||
new ObjectMapper(),
|
||||
new ActiveRestoreStateStore(properties, new ObjectMapper())
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class,
|
||||
@@ -168,7 +235,9 @@ class ResourceRestoreHandoffServiceTest {
|
||||
payloadExtractor,
|
||||
userKeyRestoreService,
|
||||
applierLauncher,
|
||||
admissionGuard
|
||||
admissionGuard,
|
||||
new ObjectMapper(),
|
||||
new ActiveRestoreStateStore(properties, new ObjectMapper())
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class,
|
||||
@@ -178,6 +247,42 @@ class ResourceRestoreHandoffServiceTest {
|
||||
Mockito.verifyNoInteractions(repository, fileRecordRepository, payloadExtractor, applierLauncher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDualRestoreWhenNode02IpIsMissing() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setPrecheckStoreDir(tempDir.resolve("prechecks").toString());
|
||||
properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString());
|
||||
FileSystemResourcePrecheckStateStore store = new FileSystemResourcePrecheckStateStore(properties, new ObjectMapper());
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class);
|
||||
ResourceUserKeyRestoreService userKeyRestoreService = Mockito.mock(ResourceUserKeyRestoreService.class);
|
||||
LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class);
|
||||
ResourceTaskAdmissionGuard admissionGuard = Mockito.mock(ResourceTaskAdmissionGuard.class);
|
||||
store.save(new ResourcePrecheckContext("FILE-20260413-000001", validPrecheckResponse()));
|
||||
ResourceRestoreService service = new ResourceRestoreServiceImpl(
|
||||
repository,
|
||||
fileRecordRepository,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
properties,
|
||||
payloadExtractor,
|
||||
userKeyRestoreService,
|
||||
applierLauncher,
|
||||
admissionGuard,
|
||||
new ObjectMapper(),
|
||||
new ActiveRestoreStateStore(properties, new ObjectMapper())
|
||||
);
|
||||
CreateResourceRestoreRequest request = validCreateRequest();
|
||||
request.getNodes().setNode02Ip("");
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class,
|
||||
() -> service.createRestoreTask(request));
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("nodes.node02Ip不能为空"));
|
||||
Mockito.verifyNoInteractions(repository, fileRecordRepository, payloadExtractor, applierLauncher);
|
||||
}
|
||||
|
||||
private static ResourceRestorePrecheckResponse validPrecheckResponse() {
|
||||
ResourceRestorePrecheckResponse precheckResponse = new ResourceRestorePrecheckResponse();
|
||||
precheckResponse.setPrecheckId("RPRE-20260413-000001");
|
||||
@@ -196,7 +301,11 @@ class ResourceRestoreHandoffServiceTest {
|
||||
CreateResourceRestoreRequest request = new CreateResourceRestoreRequest();
|
||||
request.setPrecheckId("RPRE-20260413-000001");
|
||||
request.setConfirmBackupId("RBKP-20260413-153000-000001");
|
||||
request.setConfirmRiskAccepted(true);
|
||||
request.setDeployMode("DUAL");
|
||||
CreateResourceRestoreRequest.Nodes nodes = new CreateResourceRestoreRequest.Nodes();
|
||||
nodes.setNode01Ip("192.168.1.11");
|
||||
nodes.setNode02Ip("192.168.1.12");
|
||||
request.setNodes(nodes);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
+32
-58
@@ -1,16 +1,16 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.ActiveRestoreStateStore;
|
||||
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -23,74 +23,42 @@ class ResourceTaskQueryServiceTest {
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldPreferLightweightStatusFileWhenPresent() throws Exception {
|
||||
void shouldReturnLatestRestoreFromActiveFilesAndRepairMissingDbRecord() throws Exception {
|
||||
ResourceBackupProperties properties = resourceBackupProperties();
|
||||
Path taskDir = tempDir.resolve("RRST-TASK-20260429-000001");
|
||||
Files.createDirectories(taskDir);
|
||||
Files.writeString(taskDir.resolve("status.json"), """
|
||||
{
|
||||
"status":"RUNNING",
|
||||
"phase":"MQ_REPLAY",
|
||||
"message":"replaying mq",
|
||||
"updatedAt":"2026-04-29T10:00:00+0800"
|
||||
"status":"FAILED",
|
||||
"phase":"DATABASE",
|
||||
"message":"数据库恢复失败",
|
||||
"updatedAt":"2026-04-29T10:05:00+0800"
|
||||
}
|
||||
""");
|
||||
Files.writeString(taskDir.resolve("restore.log"), "line1\nline2\n");
|
||||
|
||||
ActiveRestoreStateStore activeStore = new ActiveRestoreStateStore(properties, new ObjectMapper());
|
||||
ResourceRestoreTaskEntity entity = restoreTask(taskDir);
|
||||
activeStore.write(entity, "APPLYING");
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260429-000001")).thenReturn(Optional.of(entity));
|
||||
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260429-000001")).thenReturn(Optional.empty());
|
||||
ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
repository,
|
||||
new ObjectMapper(),
|
||||
new RestoreStatusFileReader(new ObjectMapper())
|
||||
new RestoreStatusFileReader(new ObjectMapper()),
|
||||
activeStore
|
||||
);
|
||||
|
||||
var detail = service.getRestoreTask("RRST-TASK-20260429-000001");
|
||||
ResourceRestoreLatestResponse latest = service.getLatestRestore();
|
||||
|
||||
Assertions.assertEquals("RUNNING", detail.getStatus());
|
||||
Assertions.assertEquals("MQ_REPLAY", detail.getControlPhase());
|
||||
Assertions.assertEquals("SHELL_STATUS", detail.getStateSource());
|
||||
Assertions.assertEquals("MQ_REPLAY", detail.getCurrentStepCode());
|
||||
Assertions.assertEquals("2026-04-29T10:00:00+0800", detail.getLastHeartbeatAt());
|
||||
Assertions.assertEquals("replaying mq", detail.getErrorMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeLightweightStepsAndRestoreLog() throws Exception {
|
||||
Path taskDir = tempDir.resolve("RRST-TASK-20260429-000001");
|
||||
Files.createDirectories(taskDir);
|
||||
Files.writeString(taskDir.resolve("status.json"), """
|
||||
{
|
||||
"status":"RUNNING",
|
||||
"phase":"MQ_REPLAY",
|
||||
"message":"replaying mq",
|
||||
"updatedAt":"2026-04-29T10:00:00+0800"
|
||||
}
|
||||
""");
|
||||
Files.writeString(taskDir.resolve("restore.log"), "line1\nline2\n");
|
||||
|
||||
ResourceRestoreTaskEntity entity = restoreTask(taskDir);
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260429-000001")).thenReturn(Optional.of(entity));
|
||||
|
||||
ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
repository,
|
||||
new ObjectMapper(),
|
||||
new RestoreStatusFileReader(new ObjectMapper())
|
||||
);
|
||||
|
||||
List<ResourceTaskStepItemResponse> steps = service.getRestoreTaskSteps("RRST-TASK-20260429-000001");
|
||||
Assertions.assertEquals(2, steps.size());
|
||||
Assertions.assertEquals("SUCCESS", steps.get(0).getStatus());
|
||||
Assertions.assertEquals("RUNNING", steps.get(1).getStatus());
|
||||
Assertions.assertEquals("replaying mq", steps.get(1).getMessage());
|
||||
Assertions.assertEquals(Boolean.TRUE, steps.get(1).getLogAvailable());
|
||||
|
||||
ResourceTaskStepLogResponse log = service.getRestoreTaskStepLog("RRST-TASK-20260429-000001", 2);
|
||||
Assertions.assertEquals("SHELL_APPLY", log.getStepCode());
|
||||
Assertions.assertEquals("line1\nline2\n", log.getContent());
|
||||
Assertions.assertFalse(log.getTruncated());
|
||||
Assertions.assertEquals("RRST-TASK-20260429-000001", latest.getTaskId());
|
||||
Assertions.assertEquals("RBKP-20260429-153000-000001", latest.getBackupId());
|
||||
Assertions.assertEquals("FAILED", latest.getStatus());
|
||||
Assertions.assertEquals("DATABASE", latest.getPhase());
|
||||
Assertions.assertEquals("数据库恢复失败", latest.getMessage());
|
||||
Assertions.assertEquals("2026-04-29T10:05:00+0800", latest.getFinishedAt());
|
||||
Mockito.verify(repository).save(Mockito.argThat(repaired ->
|
||||
"RRST-TASK-20260429-000001".equals(repaired.getTaskId())
|
||||
&& "FAILED".equals(repaired.getStatus())
|
||||
&& "数据库恢复失败".equals(repaired.getErrorMessage())
|
||||
));
|
||||
}
|
||||
|
||||
private static ResourceRestoreTaskEntity restoreTask(Path taskDir) {
|
||||
@@ -108,4 +76,10 @@ class ResourceTaskQueryServiceTest {
|
||||
entity.setCreateTime(LocalDateTime.parse("2026-04-29T09:59:00"));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private ResourceBackupProperties resourceBackupProperties() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRestoreTaskRootDir(tempDir.toString());
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user