diff --git a/README.md b/README.md index 361d15c..226a5c2 100644 --- a/README.md +++ b/README.md @@ -70,11 +70,75 @@ Open: - External sign API: `POST /openapi/v1/sign/signature` - Init template API: `GET /api/v1/init/template` - Init preview API: `POST /api/v1/init/preview` +- Current init config API: `GET /api/v1/init/config` +- Reset preview API: `POST /api/v1/init/reset/preview` +- Reset create task API: `POST /api/v1/init/reset/tasks` +- Reset execute API: `POST /api/v1/init/reset/tasks/{taskId}/execute` - Auth login API: `POST /api/v1/auth/login` - Device info API: `GET /api/v1/device/info` +- Device profile API: `GET /api/v1/device/profile` +- Device runtime status API: `GET /api/v1/device/runtime-status` - PCIe crypto count API: `GET /api/v1/device/crypto/device-count` - PCIe crypto HMAC API: `POST /api/v1/device/crypto/hmac` +## CISD Reset + +最小调用顺序: +- `POST /api/v1/init/reset/preview` +- `POST /api/v1/init/reset/tasks` +- `POST /api/v1/init/reset/tasks/{taskId}/execute` +- `GET /api/v1/init/reset/tasks/{taskId}` +- `GET /api/v1/init/reset/tasks/{taskId}/steps` +- `GET /api/v1/init/reset/tasks/{taskId}/steps/{stepNo}/log` + +当前支持范围: +- 仅支持设备预置产品类型为 `ENTERPRISE` 或 `INDIRECT` +- `DIRECT` 当前明确不支持 reset + +请求说明: +- reset 接口不再要求前端重复提交机构号、MQ 类型和 RabbitMQ 通道用户 +- 后端会自动读取“最近一次成功初始化任务”的快照,提取 `orgCode`、`mqType`、`channelUsername` +- 如果当前设备没有有效初始化快照,reset 预检和任务创建会直接失败 + +设备状态接口: +- `GET /api/v1/device/status` +- 返回字段 `initState` + - `UNINITIALIZED`: 当前没有有效初始化快照 + - `INITIALIZING`: 初始化任务正在执行 + - `INITIALIZED`: 当前设备已初始化 + - `RESETTING`: 重置任务正在执行 +- 前端仅根据 `initState` 判断展示初始化页、已初始化页或执行中页面 + +当前初始化配置展示接口: +- `GET /api/v1/init/config` +- 数据来源固定为最新一次成功初始化任务快照 +- 只返回机构信息、部署信息、中间件信息和节点/签名核心字段 +- 只用于展示,不用于编辑 + +当前 reset 实际会做的事情: +- 停止 `CMSP/CMTP` +- 删除数据库 `CMEP` +- 删除数据库 `orgCode` +- 删除 RabbitMQ 客户化账号、`/RQ` 权限、客户化队列 +- 停止 RabbitMQ 进程 +- 删除 TLQ license 文件 +- 清理当前 reset 任务的 staging 目录 + +当前 reset 不做的事情: +- 不回滚 `cpconfig.cfg` +- 不删除 nginx 配置和已发布前端目录 +- 不回滚应用配置文件中的 RabbitMQ 账号密码 +- 不删除标准安装介质目录 + +运维前置条件: +- `scripts/standard-init/*.sh` 已部署到 `/home/tms/bin/` +- `/home/tms/bin/stop_standard_apps.sh` +- `/home/tms/bin/stop_standard_rabbitmq.sh` +- `/home/tms/bin/apply_standard_db.sh` +- `tms.init.executor.standard-db-username/password` 已正确配置 +- `tms.init.executor.rabbitmq-ctl-command` 指向可执行的 `rabbitmqctl` +- 运行时 Swagger 文档以 `http://:8080/swagger-ui.html` 和 `http://:8080/v3/api-docs/internal-api` 为准 + ## Architecture (Monolith + Modular) - One deployable Spring Boot application. diff --git a/config/application.yml.example b/config/application.yml.example index 5c6edc2..4be643a 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -17,6 +17,13 @@ tms: product-type: ENTERPRISE version: V1.3.2 source: CONFIG + device: + profile: + model: SYD7108 + name: 跨境支付终端一体机 + cpu-model-default: "" + memory-total-default: 16GB + disk-total-default: 256G file: storage: upload-base-dir: /home/tms/uploads @@ -36,7 +43,10 @@ tms: standard-web-organization-json-path: /usr/local/nginx/html/organization.json standard-run-user: cmep4i standard-app-start-command: /home/tms/bin/start_standard_apps.sh + standard-app-stop-command: /home/tms/bin/stop_standard_apps.sh standard-nginx-start-command: /home/tms/bin/start_standard_nginx.sh + standard-nginx-stop-command: /home/tms/bin/stop_standard_nginx.sh + standard-rabbit-stop-command: /home/tms/bin/stop_standard_rabbitmq.sh standard-post-check-command: /home/tms/bin/check_standard_runtime.sh upload-base-dir: /home/tms/uploads staging-root-dir: /tmp/tms-init-staging diff --git a/scripts/standard-init/apply_standard_db.sh b/scripts/standard-init/apply_standard_db.sh index 84b7180..b4530b5 100644 --- a/scripts/standard-init/apply_standard_db.sh +++ b/scripts/standard-init/apply_standard_db.sh @@ -3,19 +3,29 @@ set -euo pipefail DB_HOST="${DB_HOST:-127.0.0.1}" DB_PORT="${DB_PORT:-4000}" -DB_NAME="${DB_NAME:-CMEP}" +CMEP_DB_NAME="${DB_NAME:-CMEP}" DB_USER="${DB_USER:?DB_USER is required}" DB_PASSWORD="${DB_PASSWORD:?DB_PASSWORD is required}" LOAD_DIR="${LOAD_DIR:?LOAD_DIR is required}" +ORG_CODE="${ORG_CODE:?ORG_CODE is required}" +ORG_NAME_CN="${ORG_NAME_CN:?ORG_NAME_CN is required}" + +escaped_org_name=$(printf "%s" "${ORG_NAME_CN}" | sed "s/'/''/g") mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" \ - -e "CREATE DATABASE IF NOT EXISTS ${DB_NAME} DEFAULT CHARACTER SET utf8mb4;" + -e "CREATE DATABASE IF NOT EXISTS \`${CMEP_DB_NAME}\` DEFAULT CHARACTER SET utf8mb4;" -mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \ - < "${LOAD_DIR}/SCHEMA-DDL.sql" - -mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \ +mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${CMEP_DB_NAME}" \ < "${LOAD_DIR}/CMEP.sql" -mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \ +mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${CMEP_DB_NAME}" \ + -e "UPDATE CIPS_USER_PARAM_INFO SET ORG_CHINESE_NAME='${escaped_org_name}';" + +mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${CMEP_DB_NAME}" \ < "${LOAD_DIR}/UP-ORG-INFO.sql" + +mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" \ + -e "CREATE DATABASE IF NOT EXISTS \`${ORG_CODE}\` DEFAULT CHARACTER SET utf8mb4;" + +mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${ORG_CODE}" \ + < "${LOAD_DIR}/SCHEMA-DDL.sql" diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java index 338f148..4118dc6 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java @@ -201,15 +201,15 @@ public class JnaPcieCryptoService implements PcieCryptoService { } @Override - public DeviceStatusResult getDeviceStatus(int statusBufferSize) { - int size = requirePositive("statusBufferSize", statusBufferSize); + public DeviceStatusResult getDeviceStatus() { + int size = 8; return sessionTemplate.withSession("SDFE_DeviceStatusGet", (lib, deviceHandle, sessionHandle) -> { - Memory statusMemory = new Memory(size); + Memory statusMemory = new Memory(8); sessionTemplate.ensureSuccess("SDFE_DeviceStatusGet", lib.SDFE_DeviceStatusGet(sessionHandle, statusMemory)); DeviceStatusResult result = new DeviceStatusResult(); - result.setLength(size); +// result.setLength(size); result.setStatusData(statusMemory.getByteArray(0, size)); return result; }); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java index 8259f98..e362304 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java @@ -106,8 +106,8 @@ public class MockPcieCryptoService implements PcieCryptoService { } @Override - public DeviceStatusResult getDeviceStatus(int statusBufferSize) { - int size = requirePositive("statusBufferSize", statusBufferSize); + public DeviceStatusResult getDeviceStatus() { + int size = 8; DeviceStatusResult result = new DeviceStatusResult(); result.setLength(size); result.setStatusData(new byte[size]); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java index ce7e8aa..cae7ccb 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java @@ -73,7 +73,7 @@ public interface PcieCryptoService { void deviceSelfTest(); - DeviceStatusResult getDeviceStatus(int statusBufferSize); + DeviceStatusResult getDeviceStatus(); void deviceFactoryReset(); diff --git a/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java b/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java index 03ad44e..b1590e5 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java @@ -3,6 +3,10 @@ package com.cisd.tms.modules.device.controller; import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.modules.device.dto.DeviceActionResponse; import com.cisd.tms.modules.device.dto.DeviceInfoResponse; +import com.cisd.tms.modules.device.dto.DeviceProfileResponse; +import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse; +import com.cisd.tms.modules.device.service.DeviceProfileService; +import com.cisd.tms.modules.device.service.DeviceRuntimeStatusService; import com.cisd.tms.modules.device.service.DeviceService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -19,9 +23,17 @@ import org.springframework.web.bind.annotation.RestController; public class DeviceController { private final DeviceService deviceService; + private final DeviceProfileService deviceProfileService; + private final DeviceRuntimeStatusService deviceRuntimeStatusService; - public DeviceController(DeviceService deviceService) { + public DeviceController( + DeviceService deviceService, + DeviceProfileService deviceProfileService, + DeviceRuntimeStatusService deviceRuntimeStatusService + ) { this.deviceService = deviceService; + this.deviceProfileService = deviceProfileService; + this.deviceRuntimeStatusService = deviceRuntimeStatusService; } @GetMapping("/info") @@ -31,11 +43,23 @@ public class DeviceController { } @GetMapping("/status") - @Operation(summary = "查询设备状态", description = "返回当前设备运行状态与版本快照。") + @Operation(summary = "查询设备状态", description = "返回当前设备运行状态、初始化状态和版本快照。") public ApiResponse status() { return ApiResponse.success(deviceService.info()); } + @GetMapping("/profile") + @Operation(summary = "查询设备画像", description = "返回设备静态信息,包括设备信息、硬件信息和按初始化方案裁剪后的软件版本信息。") + public ApiResponse profile() { + return ApiResponse.success(deviceProfileService.profile()); + } + + @GetMapping("/runtime-status") + @Operation(summary = "查询设备运行状态", description = "返回硬件实时资源使用情况、加密卡状态,以及按初始化配置裁剪后的软件运行状态。") + public ApiResponse runtimeStatus() { + return ApiResponse.success(deviceRuntimeStatusService.loadRuntimeStatus()); + } + @PostMapping("/restart") @Operation(summary = "重启设备服务", description = "受理设备重启请求,并记录触发操作的内部用户。") public ApiResponse restart( diff --git a/src/main/java/com/cisd/tms/modules/device/dto/DeviceInfoResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/DeviceInfoResponse.java index 4067158..b1bc327 100644 --- a/src/main/java/com/cisd/tms/modules/device/dto/DeviceInfoResponse.java +++ b/src/main/java/com/cisd/tms/modules/device/dto/DeviceInfoResponse.java @@ -17,6 +17,8 @@ public class DeviceInfoResponse { private String presetVersion; @Schema(description = "预置来源", example = "application.yml") private String presetSource; + @Schema(description = "设备初始化状态", example = "INITIALIZED") + private String initState; public String getDeviceId() { return deviceId; @@ -65,4 +67,12 @@ public class DeviceInfoResponse { public void setPresetSource(String presetSource) { this.presetSource = presetSource; } + + public String getInitState() { + return initState; + } + + public void setInitState(String initState) { + this.initState = initState; + } } diff --git a/src/main/java/com/cisd/tms/modules/device/service/DeviceService.java b/src/main/java/com/cisd/tms/modules/device/service/DeviceService.java index faa7ec9..a7c2a3f 100644 --- a/src/main/java/com/cisd/tms/modules/device/service/DeviceService.java +++ b/src/main/java/com/cisd/tms/modules/device/service/DeviceService.java @@ -5,6 +5,7 @@ import com.cisd.tms.modules.device.dto.DeviceActionResponse; import com.cisd.tms.modules.device.dto.DeviceInfoResponse; import com.cisd.tms.modules.device.entity.DeviceNodeEntity; import com.cisd.tms.modules.device.repository.DeviceNodeRepository; +import com.cisd.tms.modules.init.service.InitService; import java.time.OffsetDateTime; import org.springframework.stereotype.Service; @@ -13,10 +14,16 @@ public class DeviceService { private final DeviceNodeRepository deviceNodeRepository; private final CisdPresetProperties cisdPresetProperties; + private final InitService initService; - public DeviceService(DeviceNodeRepository deviceNodeRepository, CisdPresetProperties cisdPresetProperties) { + public DeviceService( + DeviceNodeRepository deviceNodeRepository, + CisdPresetProperties cisdPresetProperties, + InitService initService + ) { this.deviceNodeRepository = deviceNodeRepository; this.cisdPresetProperties = cisdPresetProperties; + this.initService = initService; } public DeviceInfoResponse info() { @@ -36,6 +43,8 @@ public class DeviceService { response.setPresetProductType(cisdPresetProperties.getProductType()); response.setPresetVersion(cisdPresetProperties.getVersion()); response.setPresetSource(cisdPresetProperties.getSource()); + InitService.DeviceInitStateSnapshot snapshot = initService.getDeviceInitStateSnapshot(); + response.setInitState(snapshot.initState()); return response; } diff --git a/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java b/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java index 391650c..d32e623 100644 --- a/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java +++ b/src/main/java/com/cisd/tms/modules/init/config/InitExecutorProperties.java @@ -50,8 +50,14 @@ public class InitExecutorProperties { private String standardRunUser = DEFAULT_STANDARD_RUN_USER; // 标准版 CMSP/CMTP 启动命令 private String standardAppStartCommand = ""; + // 标准版 CMSP/CMTP 停止命令 + private String standardAppStopCommand = ""; // 标准版 nginx 启动命令 private String standardNginxStartCommand = ""; + // 标准版 nginx 停止命令 + private String standardNginxStopCommand = ""; + // 标准版 RabbitMQ 停止命令 + private String standardRabbitStopCommand = ""; // 标准版启动后检查命令 private String standardPostCheckCommand = ""; // 直参版 cfgZipFileId 对应目录下的 cpconfig 文件名 @@ -288,6 +294,14 @@ public class InitExecutorProperties { this.standardAppStartCommand = standardAppStartCommand; } + public String getStandardAppStopCommand() { + return standardAppStopCommand; + } + + public void setStandardAppStopCommand(String standardAppStopCommand) { + this.standardAppStopCommand = standardAppStopCommand; + } + public String getStandardNginxStartCommand() { return standardNginxStartCommand; } @@ -296,6 +310,22 @@ public class InitExecutorProperties { this.standardNginxStartCommand = standardNginxStartCommand; } + public String getStandardNginxStopCommand() { + return standardNginxStopCommand; + } + + public void setStandardNginxStopCommand(String standardNginxStopCommand) { + this.standardNginxStopCommand = standardNginxStopCommand; + } + + public String getStandardRabbitStopCommand() { + return standardRabbitStopCommand; + } + + public void setStandardRabbitStopCommand(String standardRabbitStopCommand) { + this.standardRabbitStopCommand = standardRabbitStopCommand; + } + public String getStandardPostCheckCommand() { return standardPostCheckCommand; } diff --git a/src/main/java/com/cisd/tms/modules/init/controller/InitController.java b/src/main/java/com/cisd/tms/modules/init/controller/InitController.java index 25959c8..3028f89 100644 --- a/src/main/java/com/cisd/tms/modules/init/controller/InitController.java +++ b/src/main/java/com/cisd/tms/modules/init/controller/InitController.java @@ -2,6 +2,7 @@ package com.cisd.tms.modules.init.controller; import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; +import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; import com.cisd.tms.modules.init.dto.InitPlanTemplateResponse; import com.cisd.tms.modules.init.dto.InitPreviewRequest; import com.cisd.tms.modules.init.dto.InitPreviewResponse; @@ -9,6 +10,8 @@ import com.cisd.tms.modules.init.dto.InitTaskDetailResponse; import com.cisd.tms.modules.init.dto.InitTaskExecuteResponse; import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse; import com.cisd.tms.modules.init.dto.InitTaskStepResponse; +import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse; +import com.cisd.tms.modules.init.dto.ResetPreviewResponse; import com.cisd.tms.modules.init.service.InitService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -65,6 +68,12 @@ public class InitController { return ApiResponse.success(initService.createTask(request)); } + @GetMapping("/config") + @Operation(summary = "查询当前初始化配置", description = "从最新一次成功初始化任务快照中返回当前已初始化的 CISD 核心展示字段,不用于编辑。") + public ApiResponse currentConfig() { + return ApiResponse.success(initService.loadCurrentInitConfig()); + } + @GetMapping("/tasks/{taskId}") @Operation(summary = "查询任务详情", description = "返回初始化任务的主状态、产品类型和原始计划快照。") public ApiResponse taskDetail( @@ -109,4 +118,54 @@ public class InitController { ) { return ApiResponse.success(initService.executeTask(taskId)); } + + @PostMapping("/reset/preview") + @Operation(summary = "预检重置请求", description = "自动读取当前设备有效初始化快照,返回重置任务的产品类型与预估步骤数量,不需要前端再次输入机构和 MQ 参数。") + public ApiResponse previewReset() { + return ApiResponse.success(initService.previewReset()); + } + + @PostMapping("/reset/tasks") + @Operation(summary = "创建重置任务", description = "自动读取当前设备有效初始化快照生成重置任务和步骤记录,任务初始状态为 PENDING。") + public ApiResponse createResetTask() { + return ApiResponse.success(initService.createResetTask()); + } + + @PostMapping("/reset/tasks/{taskId}/execute") + @Operation(summary = "执行重置任务", description = "异步受理重置任务执行请求,立即返回当前任务快照,实际步骤在后台继续运行。") + public ApiResponse executeResetTask( + @Parameter(description = "重置任务唯一标识") + @PathVariable String taskId + ) { + return ApiResponse.success(initService.executeResetTask(taskId)); + } + + @GetMapping("/reset/tasks/{taskId}") + @Operation(summary = "查询重置任务详情", description = "返回重置任务的主状态、产品类型和原始计划快照。") + public ApiResponse resetTaskDetail( + @Parameter(description = "重置任务唯一标识") + @PathVariable String taskId + ) { + return ApiResponse.success(initService.getTask(taskId)); + } + + @GetMapping("/reset/tasks/{taskId}/steps") + @Operation(summary = "查询重置任务步骤", description = "返回重置任务下全部步骤的执行状态和命令信息。") + public ApiResponse> resetTaskSteps( + @Parameter(description = "重置任务唯一标识") + @PathVariable String taskId + ) { + return ApiResponse.success(initService.getTaskSteps(taskId)); + } + + @GetMapping("/reset/tasks/{taskId}/steps/{stepNo}/log") + @Operation(summary = "查询重置步骤日志", description = "返回指定重置任务步骤的日志路径和日志内容,用于定位失败原因。") + public ApiResponse resetTaskStepLog( + @Parameter(description = "重置任务唯一标识") + @PathVariable String taskId, + @Parameter(description = "任务步骤序号,从 1 开始") + @PathVariable int stepNo + ) { + return ApiResponse.success(initService.getTaskStepLog(taskId, stepNo)); + } } diff --git a/src/main/java/com/cisd/tms/modules/init/dto/InitCreateTaskResponse.java b/src/main/java/com/cisd/tms/modules/init/dto/InitCreateTaskResponse.java index 86c2ebb..1161e48 100644 --- a/src/main/java/com/cisd/tms/modules/init/dto/InitCreateTaskResponse.java +++ b/src/main/java/com/cisd/tms/modules/init/dto/InitCreateTaskResponse.java @@ -9,6 +9,8 @@ public class InitCreateTaskResponse { private String taskId; @Schema(description = "任务状态", example = "PENDING") private String status; + @Schema(description = "任务类型", example = "INIT") + private String taskType; @Schema(description = "解析后的产品类型", example = "STANDARD") private String resolvedProductType; @Schema(description = "命中的预制版本", example = "6.6.4") @@ -30,6 +32,14 @@ public class InitCreateTaskResponse { this.status = status; } + public String getTaskType() { + return taskType; + } + + public void setTaskType(String taskType) { + this.taskType = taskType; + } + public String getResolvedProductType() { return resolvedProductType; } diff --git a/src/main/java/com/cisd/tms/modules/init/dto/InitTaskDetailResponse.java b/src/main/java/com/cisd/tms/modules/init/dto/InitTaskDetailResponse.java index e672467..ceb6f21 100644 --- a/src/main/java/com/cisd/tms/modules/init/dto/InitTaskDetailResponse.java +++ b/src/main/java/com/cisd/tms/modules/init/dto/InitTaskDetailResponse.java @@ -9,6 +9,8 @@ public class InitTaskDetailResponse { private String taskId; @Schema(description = "任务状态", example = "RUNNING") private String status; + @Schema(description = "任务类型", example = "INIT") + private String taskType; @Schema(description = "解析后的产品类型", example = "STANDARD") private String resolvedProductType; @Schema(description = "命中的预制版本", example = "6.6.4") @@ -38,6 +40,14 @@ public class InitTaskDetailResponse { this.status = status; } + public String getTaskType() { + return taskType; + } + + public void setTaskType(String taskType) { + this.taskType = taskType; + } + public String getResolvedProductType() { return resolvedProductType; } diff --git a/src/main/java/com/cisd/tms/modules/init/entity/InitTaskEntity.java b/src/main/java/com/cisd/tms/modules/init/entity/InitTaskEntity.java index 070dc00..fd15e0d 100644 --- a/src/main/java/com/cisd/tms/modules/init/entity/InitTaskEntity.java +++ b/src/main/java/com/cisd/tms/modules/init/entity/InitTaskEntity.java @@ -7,6 +7,7 @@ import com.cisd.tms.infrastructure.persistence.entity.BaseEntity; public class InitTaskEntity extends BaseEntity { private String taskId; + private String taskType; private String version; private String status; private String productType; @@ -23,6 +24,14 @@ public class InitTaskEntity extends BaseEntity { this.taskId = taskId; } + public String getTaskType() { + return taskType; + } + + public void setTaskType(String taskType) { + this.taskType = taskType; + } + public String getVersion() { return version; } diff --git a/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java b/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java index 49fb439..71aab26 100644 --- a/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java +++ b/src/main/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutor.java @@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.net.InetAddress; import java.net.NetworkInterface; +import java.net.URI; +import java.net.URISyntaxException; import java.net.SocketException; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -31,19 +33,34 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class ConfigurableInitStepExecutor implements InitStepExecutor { private static final String INTERNAL_PREFIX = "internal::"; + private static final String DEFAULT_DB_HOST = "127.0.0.1"; + private static final String DEFAULT_DB_PORT = "4000"; private final InitExecutorProperties properties; private final ObjectMapper objectMapper; + private final String springDatasourceUrl; public ConfigurableInitStepExecutor(InitExecutorProperties properties, ObjectMapper objectMapper) { + this(properties, objectMapper, ""); + } + + @Autowired + public ConfigurableInitStepExecutor( + InitExecutorProperties properties, + ObjectMapper objectMapper, + @Value("${spring.datasource.url:}") String springDatasourceUrl + ) { this.properties = properties; this.objectMapper = objectMapper; + this.springDatasourceUrl = normalize(springDatasourceUrl); } @Override @@ -121,6 +138,15 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { case "start_cmsp_cmtp" -> executeStandardStartCommand(task, step, "start_cmsp_cmtp", properties.getStandardAppStartCommand()); case "start_nginx" -> executeStandardStartCommand(task, step, "start_nginx", properties.getStandardNginxStartCommand()); case "post_check" -> executeStandardStartCommand(task, step, "post_check", properties.getStandardPostCheckCommand()); + case "stop_apps" -> executeStandardStartCommand(task, step, "stop_apps", properties.getStandardAppStopCommand()); + case "stop_nginx" -> executeStandardStartCommand(task, step, "stop_nginx", properties.getStandardNginxStopCommand()); + case "db_drop_cmep" -> executeResetDropDatabase(task, step, "CMEP"); + case "db_drop_org" -> executeResetDropDatabase(task, step, null); + case "mq_cleanup" -> executeResetMqCleanup(task, step); + case "stop_rabbitmq" -> executeResetStopRabbitmq(task, step); + case "tlq_license_cleanup" -> executeResetTlqLicenseCleanup(task, step); + case "clean_staging" -> executeResetCleanStaging(task, step); + case "verify_reset" -> executeVerifyReset(task, step); case "direct_db_init" -> executeDirectDbInit(task, step); case "tlq_init_cfg" -> executeTlqInitCfg(task, step); case "cfmq_apply" -> executeCfmqApply(task, step); @@ -906,7 +932,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { Arrays.asList("/bin/zsh", "-lc", applyCommand), false, loadDir, - buildStandardDbApplyEnvironment(loadDir) + buildStandardDbApplyEnvironment(loadDir, context) ); if (code != 0) { return InitStepExecutionResult.failure("db_apply failed", code, logFile.toString()); @@ -1052,6 +1078,151 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { } } + private InitStepExecutionResult executeResetDropDatabase( + InitTaskEntity task, + InitTaskStepEntity step, + String fixedDatabaseName + ) throws IOException { + PlanContext context = loadPlanContext(task); + if ("DIRECT".equals(context.productType())) { + String logPath = writeInternalLog(task, step, "reset db drop skipped for direct product type"); + return InitStepExecutionResult.success("reset db drop skipped", 0, logPath); + } + String databaseName = isBlank(fixedDatabaseName) ? normalize(context.orgCode()) : fixedDatabaseName; + String identifier = requireMysqlIdentifier(databaseName); + if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) { + String logPath = writeInternalLog(task, step, "reset db drop simulated, database=" + identifier); + return InitStepExecutionResult.success("reset db drop simulated", 0, logPath); + } + + Path logFile = prepareLogPath(task, step); + String command = "DB_HOST=\"${DB_HOST:-127.0.0.1}\" DB_PORT=\"${DB_PORT:-4000}\" " + + "mysql -h\"${DB_HOST}\" -P\"${DB_PORT}\" -u\"${DB_USER}\" -p\"${DB_PASSWORD}\" " + + "-e \"DROP DATABASE IF EXISTS \\`" + identifier + "\\`;\""; + try { + int code = runCommand( + logFile, + Arrays.asList("/bin/zsh", "-lc", command), + false, + null, + buildStandardDbApplyEnvironment(null, context) + ); + if (code != 0) { + return InitStepExecutionResult.failure("reset db drop failed", code, logFile.toString()); + } + return InitStepExecutionResult.success("reset db dropped: " + identifier, 0, logFile.toString()); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return InitStepExecutionResult.failure("reset db drop interrupted", -1, logFile.toString()); + } + } + + private InitStepExecutionResult executeResetMqCleanup(InitTaskEntity task, InitTaskStepEntity step) throws IOException { + PlanContext context = loadPlanContext(task); + if (!shouldApplyStandardRabbitmq(context)) { + String logPath = writeInternalLog(task, step, "mq_cleanup skipped for current mode"); + return InitStepExecutionResult.success("mq_cleanup skipped", 0, logPath); + } + if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) { + String logPath = writeInternalLog(task, step, "mq_cleanup simulated"); + return InitStepExecutionResult.success("mq_cleanup simulated", 0, logPath); + } + if (isBlank(context.channelUsername())) { + String logPath = writeInternalLog(task, step, "mq_cleanup failed: channel username is blank"); + return InitStepExecutionResult.failure("mq_cleanup failed: channel username is blank", -1, logPath); + } + + Path logFile = prepareLogPath(task, step); + String ctl = resolveRabbitmqCtlCommand(); + String vhost = firstNonBlank(properties.getRabbitVhost(), "/RQ"); + String user = normalize(context.channelUsername()); + try { + if (!waitRabbitmqReady(logFile, ctl)) { + appendLog(logFile, "rabbitmqctl status check failed, continue with cleanup flow"); + } + + for (String queue : buildStandardCustomerDefaultVhostQueues(normalize(context.orgCode()))) { + runCommand(logFile, Arrays.asList(ctl, "delete_queue", queue), true); + } + for (String queue : buildStandardRqVhostQueues(normalize(context.orgCode()))) { + runCommand(logFile, Arrays.asList(ctl, "delete_queue", "-p", vhost, queue), true); + } + runCommand(logFile, Arrays.asList(ctl, "clear_permissions", "-p", vhost, user), true); + runCommand(logFile, Arrays.asList(ctl, "delete_user", user), true); + return InitStepExecutionResult.success("mq cleanup applied", 0, logFile.toString()); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return InitStepExecutionResult.failure("mq cleanup interrupted", -1, logFile.toString()); + } + } + + private InitStepExecutionResult executeResetStopRabbitmq(InitTaskEntity task, InitTaskStepEntity step) throws IOException { + PlanContext context = loadPlanContext(task); + if (!shouldApplyStandardRabbitmq(context)) { + String logPath = writeInternalLog(task, step, "stop_rabbitmq skipped for current mode"); + return InitStepExecutionResult.success("stop_rabbitmq skipped", 0, logPath); + } + return executeStandardStartCommand(task, step, "stop_rabbitmq", properties.getStandardRabbitStopCommand()); + } + + private InitStepExecutionResult executeResetTlqLicenseCleanup(InitTaskEntity task, InitTaskStepEntity step) throws IOException { + PlanContext context = loadPlanContext(task); + if (!"TLQ".equals(context.mqType()) && !"RABBITMQ_TLQ".equals(context.mqType())) { + String logPath = writeInternalLog(task, step, "tlq_license_cleanup skipped for non-TLQ mode"); + return InitStepExecutionResult.success("tlq_license_cleanup skipped", 0, logPath); + } + Path target = Paths.get(resolveTlqLicenseTargetPath(context.productType())).normalize().toAbsolutePath(); + Path logFile = prepareLogPath(task, step); + if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) { + appendLog(logFile, "tlq license cleanup simulated: " + target); + return InitStepExecutionResult.success("tlq license cleanup simulated", 0, logFile.toString()); + } + Files.deleteIfExists(target); + appendLog(logFile, "tlq license deleted if exists: " + target); + return InitStepExecutionResult.success("tlq license cleanup applied", 0, logFile.toString()); + } + + private InitStepExecutionResult executeResetCleanStaging(InitTaskEntity task, InitTaskStepEntity step) throws IOException { + Path stageDir = Paths.get(properties.getStagingRootDir()).resolve(task.getTaskId()).normalize().toAbsolutePath(); + Path logFile = prepareLogPath(task, step); + if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) { + appendLog(logFile, "clean_staging simulated: " + stageDir); + return InitStepExecutionResult.success("clean_staging simulated", 0, logFile.toString()); + } + deleteRecursively(stageDir); + appendLog(logFile, "clean_staging removed: " + stageDir); + return InitStepExecutionResult.success("clean_staging applied", 0, logFile.toString()); + } + + private InitStepExecutionResult executeVerifyReset(InitTaskEntity task, InitTaskStepEntity step) throws IOException { + PlanContext context = loadPlanContext(task); + if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) { + String logPath = writeInternalLog(task, step, "verify_reset simulated"); + return InitStepExecutionResult.success("verify_reset simulated", 0, logPath); + } + + Path logFile = prepareLogPath(task, step); + List failures = new ArrayList<>(); + verifyProcessAbsent(logFile, properties.getStandardHomeDir() + "/cmsp", "cmsp", failures); + verifyProcessAbsent(logFile, properties.getStandardHomeDir() + "/cmtp", "cmtp", failures); + if (shouldApplyStandardRabbitmq(context)) { + verifyProcessAbsent(logFile, "rabbitmq-server", "rabbitmq", failures); + } + verifyDatabaseAbsent(logFile, "CMEP", context, failures); + verifyDatabaseAbsent(logFile, normalize(context.orgCode()), context, failures); + verifyMqCleanup(logFile, context, failures); + verifyTlqLicenseCleanup(context, failures); + + if (!failures.isEmpty()) { + String detail = String.join("; ", failures); + appendLog(logFile, "verify_reset failed: " + detail); + return InitStepExecutionResult.failure("verify_reset failed: " + detail, -1, logFile.toString()); + } + + appendLog(logFile, "verify_reset success"); + return InitStepExecutionResult.success("verify_reset success", 0, logFile.toString()); + } + private InitStepExecutionResult executeTlqInitCfg(InitTaskEntity task, InitTaskStepEntity step) throws IOException { PlanContext context = loadPlanContext(task); if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) { @@ -1633,7 +1804,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { return firstNonBlank(properties.getStandardTlqLicenseTargetPath(), "/home/tlq/tlqconf.zip"); } - private List buildStandardDefaultVhostQueues(String orgCode) { + private List buildStandardCustomerDefaultVhostQueues(String orgCode) { List queues = new ArrayList<>(); for (String ci : List.of("CI01", "CI02")) { queues.add("LQ_DTTRSP_IB01_" + ci + "_" + orgCode); @@ -1646,8 +1817,6 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { } queues.add("LQ_SYN_IB00_CI00_" + orgCode); queues.add("LQ_SYNRSP_IB00_CI00_" + orgCode); - queues.add("LQ_MSGRSP_GUI"); - queues.add("LQ_SYNRSP_GUI"); return queues; } @@ -1710,8 +1879,11 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { return exitCode; } - private Map buildStandardDbApplyEnvironment(Path loadDir) { + private Map buildStandardDbApplyEnvironment(Path loadDir, PlanContext context) { Map environment = new LinkedHashMap<>(); + JdbcEndpoint endpoint = resolveStandardDbEndpoint(); + environment.put("DB_HOST", endpoint.host()); + environment.put("DB_PORT", endpoint.port()); if (!isBlank(properties.getStandardDbUsername())) { environment.put("DB_USER", normalize(properties.getStandardDbUsername())); } @@ -1721,12 +1893,39 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { if (loadDir != null) { environment.put("LOAD_DIR", loadDir.toAbsolutePath().normalize().toString()); } + if (context != null && !isBlank(context.orgCode())) { + environment.put("ORG_CODE", normalize(context.orgCode())); + } + if (context != null && !isBlank(context.orgNameCn())) { + environment.put("ORG_NAME_CN", normalize(context.orgNameCn())); + } return environment; } + private JdbcEndpoint resolveStandardDbEndpoint() { + if (isBlank(springDatasourceUrl)) { + return new JdbcEndpoint(DEFAULT_DB_HOST, DEFAULT_DB_PORT); + } + String candidate = normalize(springDatasourceUrl); + if (candidate.startsWith("jdbc:")) { + candidate = candidate.substring("jdbc:".length()); + } + try { + URI uri = new URI(candidate); + String host = normalize(uri.getHost()); + int port = uri.getPort(); + if (isBlank(host)) { + return new JdbcEndpoint(DEFAULT_DB_HOST, DEFAULT_DB_PORT); + } + return new JdbcEndpoint(host, port > 0 ? String.valueOf(port) : DEFAULT_DB_PORT); + } catch (URISyntaxException ex) { + return new JdbcEndpoint(DEFAULT_DB_HOST, DEFAULT_DB_PORT); + } + } + private Map buildStandardStartEnvironment(String stepName) { Map environment = new LinkedHashMap<>(); - if ("start_cmsp_cmtp".equals(stepName)) { + if ("start_cmsp_cmtp".equals(stepName) || "stop_apps".equals(stepName)) { environment.put("STANDARD_HOME_DIR", normalize(properties.getStandardHomeDir())); environment.put("STANDARD_RUN_USER", normalize(properties.getStandardRunUser())); } @@ -1840,12 +2039,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { return new PlanContext( firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType())), normalizeUpper(text(request, "orgCodeType")), - text(request, "orgCode"), + firstNonBlank(text(request, "orgCode"), text(root, "orgCode")), text(request, "orgNameCn"), text(request, "orgNameEn"), normalizeUpper(text(request, "deployMode")), - normalizeUpper(text(mq, "mqType")), - text(mq, "channelUsername"), + firstNonBlank(normalizeUpper(text(mq, "mqType")), firstNonBlank(normalizeUpper(text(request, "mqType")), normalizeUpper(text(root, "mqType")))), + firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername")), text(mq, "channelPassword"), text(nodes, "node01Ip"), text(nodes, "node02Ip"), @@ -1931,6 +2130,93 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { return "'" + escaped + "'"; } + private void verifyProcessAbsent(Path logFile, String pattern, String label, List failures) throws IOException { + try { + int code = runCommand( + logFile, + Arrays.asList("/bin/zsh", "-lc", "pgrep -f " + quoteShellValue(pattern) + " >/dev/null"), + true + ); + if (code == 0) { + failures.add(label + " process still running"); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + failures.add(label + " process verification interrupted"); + } + } + + private void verifyDatabaseAbsent(Path logFile, String databaseName, PlanContext context, List failures) throws IOException { + if (isBlank(databaseName)) { + return; + } + String identifier = requireMysqlIdentifier(databaseName); + String command = "DB_HOST=\"${DB_HOST:-127.0.0.1}\" DB_PORT=\"${DB_PORT:-4000}\" " + + "mysql -h\"${DB_HOST}\" -P\"${DB_PORT}\" -u\"${DB_USER}\" -p\"${DB_PASSWORD}\" -Nse " + + quoteShellValue("SHOW DATABASES LIKE '" + identifier + "';") + + " | grep -q ."; + try { + int code = runCommand( + logFile, + Arrays.asList("/bin/zsh", "-lc", command), + true, + null, + buildStandardDbApplyEnvironment(null, context) + ); + if (code == 0) { + failures.add("database still exists: " + identifier); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + failures.add("database verification interrupted: " + identifier); + } + } + + private void verifyMqCleanup(Path logFile, PlanContext context, List failures) throws IOException { + if (!shouldApplyStandardRabbitmq(context) || isBlank(context.channelUsername())) { + return; + } + String ctl = resolveRabbitmqCtlCommand(); + String vhost = firstNonBlank(properties.getRabbitVhost(), "/RQ"); + verifyRabbitObjectAbsent(logFile, ctl + " list_users | awk 'NR>1 {print $1}' | grep -Fx " + quoteShellValue(context.channelUsername()) + " >/dev/null", "rabbit user still exists", failures); + for (String queue : buildStandardCustomerDefaultVhostQueues(normalize(context.orgCode()))) { + verifyRabbitObjectAbsent(logFile, ctl + " list_queues name | awk 'NR>1 {print $1}' | grep -Fx " + quoteShellValue(queue) + " >/dev/null", "default vhost queue still exists: " + queue, failures); + } + for (String queue : buildStandardRqVhostQueues(normalize(context.orgCode()))) { + verifyRabbitObjectAbsent(logFile, ctl + " list_queues -p " + quoteShellValue(vhost) + " name | awk 'NR>1 {print $1}' | grep -Fx " + quoteShellValue(queue) + " >/dev/null", "RQ vhost queue still exists: " + queue, failures); + } + } + + private void verifyRabbitObjectAbsent(Path logFile, String shellCommand, String message, List failures) throws IOException { + try { + int code = runCommand(logFile, Arrays.asList("/bin/zsh", "-lc", shellCommand), true); + if (code == 0) { + failures.add(message); + } + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + failures.add(message + " (verification interrupted)"); + } + } + + private void verifyTlqLicenseCleanup(PlanContext context, List failures) { + if (!"TLQ".equals(context.mqType()) && !"RABBITMQ_TLQ".equals(context.mqType())) { + return; + } + Path target = Paths.get(resolveTlqLicenseTargetPath(context.productType())).normalize().toAbsolutePath(); + if (Files.exists(target)) { + failures.add("tlq license still exists: " + target); + } + } + + private String requireMysqlIdentifier(String databaseName) { + String normalized = normalize(databaseName); + if (!normalized.matches("[A-Za-z0-9_]+")) { + throw new IllegalArgumentException("invalid database identifier: " + databaseName); + } + return normalized; + } + private static void requireNotBlank(Map configMap, String key, List failures) { if (isBlank(unquote(configMap.get(key)))) { failures.add(key + " is blank"); @@ -1978,6 +2264,9 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor { ) { } + private record JdbcEndpoint(String host, String port) { + } + private record ResolvedCpconfig(Path path, List candidates) { } diff --git a/src/main/java/com/cisd/tms/modules/init/mapper/InitTaskMapper.java b/src/main/java/com/cisd/tms/modules/init/mapper/InitTaskMapper.java index edf2d9a..f6e2319 100644 --- a/src/main/java/com/cisd/tms/modules/init/mapper/InitTaskMapper.java +++ b/src/main/java/com/cisd/tms/modules/init/mapper/InitTaskMapper.java @@ -9,4 +9,10 @@ import org.apache.ibatis.annotations.Param; public interface InitTaskMapper extends BaseMapperX { InitTaskEntity selectByTaskId(@Param("taskId") String taskId); + + InitTaskEntity selectLatest(); + + InitTaskEntity selectLatestByStatus(@Param("status") String status); + + InitTaskEntity selectLatestByTaskTypeAndStatus(@Param("taskType") String taskType, @Param("status") String status); } diff --git a/src/main/java/com/cisd/tms/modules/init/repository/InitTaskRepository.java b/src/main/java/com/cisd/tms/modules/init/repository/InitTaskRepository.java index 9e10a45..97a5b34 100644 --- a/src/main/java/com/cisd/tms/modules/init/repository/InitTaskRepository.java +++ b/src/main/java/com/cisd/tms/modules/init/repository/InitTaskRepository.java @@ -7,6 +7,12 @@ public interface InitTaskRepository { Optional findByTaskId(String taskId); + Optional findLatest(); + + Optional findLatestByStatus(String status); + + Optional findLatestByTaskTypeAndStatus(String taskType, String status); + void save(InitTaskEntity entity); void update(InitTaskEntity entity); diff --git a/src/main/java/com/cisd/tms/modules/init/repository/impl/InitTaskRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/init/repository/impl/InitTaskRepositoryImpl.java index d2f6b2c..c9ffeb0 100644 --- a/src/main/java/com/cisd/tms/modules/init/repository/impl/InitTaskRepositoryImpl.java +++ b/src/main/java/com/cisd/tms/modules/init/repository/impl/InitTaskRepositoryImpl.java @@ -20,6 +20,21 @@ public class InitTaskRepositoryImpl implements InitTaskRepository { return Optional.ofNullable(initTaskMapper.selectByTaskId(taskId)); } + @Override + public Optional findLatest() { + return Optional.ofNullable(initTaskMapper.selectLatest()); + } + + @Override + public Optional findLatestByStatus(String status) { + return Optional.ofNullable(initTaskMapper.selectLatestByStatus(status)); + } + + @Override + public Optional findLatestByTaskTypeAndStatus(String taskType, String status) { + return Optional.ofNullable(initTaskMapper.selectLatestByTaskTypeAndStatus(taskType, status)); + } + @Override public void save(InitTaskEntity entity) { initTaskMapper.insert(entity); diff --git a/src/main/java/com/cisd/tms/modules/init/service/InitService.java b/src/main/java/com/cisd/tms/modules/init/service/InitService.java index b02c1eb..2c31568 100644 --- a/src/main/java/com/cisd/tms/modules/init/service/InitService.java +++ b/src/main/java/com/cisd/tms/modules/init/service/InitService.java @@ -6,6 +6,7 @@ import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.init.config.InitCommandProfileService; import com.cisd.tms.modules.init.config.InitExecutorProperties; import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; +import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; import com.cisd.tms.modules.init.dto.InitPlanTemplateResponse; import com.cisd.tms.modules.init.dto.InitPreviewRequest; import com.cisd.tms.modules.init.dto.InitPreviewResponse; @@ -13,6 +14,8 @@ import com.cisd.tms.modules.init.dto.InitTaskDetailResponse; import com.cisd.tms.modules.init.dto.InitTaskExecuteResponse; import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse; import com.cisd.tms.modules.init.dto.InitTaskStepResponse; +import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse; +import com.cisd.tms.modules.init.dto.ResetPreviewResponse; import com.cisd.tms.modules.init.entity.InitTaskEntity; import com.cisd.tms.modules.init.entity.InitTaskStepEntity; import com.cisd.tms.modules.init.executor.InitStepExecutionResult; @@ -20,6 +23,7 @@ import com.cisd.tms.modules.init.executor.InitStepExecutor; import com.cisd.tms.modules.init.repository.InitTaskRepository; import com.cisd.tms.modules.init.repository.InitTaskStepRepository; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.nio.file.Files; @@ -27,14 +31,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -47,6 +44,8 @@ import org.springframework.stereotype.Service; @Service public class InitService { + private static final String TASK_TYPE_INIT = "INIT"; + private static final String TASK_TYPE_RESET = "RESET"; private static final Set PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT"); private static final Set DEPLOY_STANDARD = Set.of("SINGLE", "DUAL"); private static final Set DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD"); @@ -145,6 +144,18 @@ public class InitService { return response; } + public ResetPreviewResponse previewReset() { + ResolvedResetContext context = resolveCurrentResetContext(); + String productType = context.productType(); + + ResetPreviewResponse response = new ResetPreviewResponse(); + response.setResolvedProductType(productType); + response.setPresetVersion(cisdPresetProperties.getVersion()); + response.setStepCount(resetSteps(productType, context.mqType()).size()); + response.setSummary("reset preview generated from current initialized state"); + return response; + } + public InitCreateTaskResponse createTask(InitPreviewRequest request) { if (request == null) { throw new IllegalArgumentException("request must not be null"); @@ -154,7 +165,7 @@ public class InitService { validateCommon(request); validateByProductType(productType, request); - String taskId = buildTaskId(); + String taskId = buildTaskId(TASK_TYPE_INIT); String normalizedMqType = normalizeUpper(request.getMq().getMqType()); String deployMode = normalizeUpper(request.getDeployMode()); LocalDateTime now = LocalDateTime.now(); @@ -164,6 +175,7 @@ public class InitService { InitTaskEntity entity = new InitTaskEntity(); entity.setTaskId(taskId); + entity.setTaskType(TASK_TYPE_INIT); entity.setVersion(cisdPresetProperties.getVersion()); entity.setStatus("PENDING"); entity.setProductType(productType); @@ -198,17 +210,98 @@ public class InitService { InitCreateTaskResponse response = new InitCreateTaskResponse(); response.setTaskId(taskId); response.setStatus(entity.getStatus()); + response.setTaskType(entity.getTaskType()); response.setResolvedProductType(productType); response.setPresetVersion(cisdPresetProperties.getVersion()); return response; } + public ResetCreateTaskResponse createResetTask() { + ResolvedResetContext context = resolveCurrentResetContext(); + String productType = context.productType(); + String taskId = buildTaskId(TASK_TYPE_RESET); + String normalizedMqType = context.mqType(); + LocalDateTime now = LocalDateTime.now(); + List stepCodes = resetSteps(productType, normalizedMqType); + + InitTaskEntity entity = new InitTaskEntity(); + entity.setTaskId(taskId); + entity.setTaskType(TASK_TYPE_RESET); + entity.setVersion(cisdPresetProperties.getVersion()); + entity.setStatus("PENDING"); + entity.setProductType(productType); + entity.setOrgCode(context.orgCode()); + entity.setMqType(normalizedMqType); + entity.setDeployMode(""); + entity.setCreateTime(now); + entity.setUpdateTime(now); + entity.setInitPlanJson(generateResetPlanJson(context, stepCodes)); + initTaskRepository.save(entity); + + List stepEntities = new ArrayList<>(); + for (int i = 0; i < stepCodes.size(); i++) { + String stepCode = stepCodes.get(i); + InitTaskStepEntity step = new InitTaskStepEntity(); + step.setTaskId(taskId); + step.setStepNo(i + 1); + step.setStepCode(stepCode); + step.setStatus("PENDING"); + step.setMessage("waiting"); + step.setCommandLine(initCommandProfileService.resolveCommand(productType, normalizedMqType, stepCode)); + step.setCreateTime(now); + step.setUpdateTime(now); + stepEntities.add(step); + } + initTaskStepRepository.saveBatch(stepEntities); + + ResetCreateTaskResponse response = new ResetCreateTaskResponse(); + response.setTaskId(taskId); + response.setStatus(entity.getStatus()); + response.setTaskType(entity.getTaskType()); + response.setResolvedProductType(productType); + response.setPresetVersion(cisdPresetProperties.getVersion()); + return response; + } + + public DeviceInitStateSnapshot getDeviceInitStateSnapshot() { + Optional running = initTaskRepository.findLatestByStatus("RUNNING"); + if (running.isPresent()) { + InitTaskEntity task = running.get(); + String taskType = normalize(task.getTaskType()); + if (TASK_TYPE_RESET.equals(taskType)) { + return new DeviceInitStateSnapshot("RESETTING", TASK_TYPE_RESET, task.getStatus(), null, null); + } + return new DeviceInitStateSnapshot("INITIALIZING", TASK_TYPE_INIT, task.getStatus(), null, null); + } + + Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); + Optional latestReset = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_RESET, "SUCCESS"); + if (latestInit.isEmpty()) { + return new DeviceInitStateSnapshot("UNINITIALIZED", null, null, null, null); + } + if (latestReset.isPresent() && compareTaskRecency(latestReset.get(), latestInit.get()) >= 0) { + return new DeviceInitStateSnapshot("UNINITIALIZED", TASK_TYPE_RESET, latestReset.get().getStatus(), null, null); + } + + ResolvedResetContext context = resolveResetContextFromInitTask(latestInit.get()); + return new DeviceInitStateSnapshot("INITIALIZED", TASK_TYPE_INIT, latestInit.get().getStatus(), context.orgCode(), context.mqType()); + } + + public CurrentInitConfigResponse loadCurrentInitConfig() { + Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); + if (latestInit.isEmpty()) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized"); + } + return resolveCurrentInitConfigFromTask(latestInit.get()); + } + public InitTaskDetailResponse getTask(String taskId) { InitTaskEntity entity = requireTask(taskId); InitTaskDetailResponse response = new InitTaskDetailResponse(); response.setTaskId(entity.getTaskId()); response.setStatus(entity.getStatus()); + response.setTaskType(normalize(entity.getTaskType()).isBlank() ? TASK_TYPE_INIT : entity.getTaskType()); response.setResolvedProductType(entity.getProductType()); response.setPresetVersion(entity.getVersion()); response.setOrgCode(entity.getOrgCode()); @@ -270,6 +363,19 @@ public class InitService { } public InitTaskExecuteResponse executeTask(String taskId) { + return executeTaskInternal(taskId); + } + + public InitTaskExecuteResponse executeResetTask(String taskId) { + InitTaskEntity task = requireTask(taskId); + String taskType = normalize(task.getTaskType()); + if (!TASK_TYPE_RESET.equals(taskType)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset task not found: " + taskId); + } + return executeTaskInternal(taskId); + } + + private InitTaskExecuteResponse executeTaskInternal(String taskId) { InitTaskEntity task = requireTask(taskId); List steps = initTaskStepRepository.findByTaskIdOrderByStepNo(taskId); if (steps.isEmpty()) { @@ -403,6 +509,29 @@ public class InitService { } } + private String generateResetPlanJson(ResolvedResetContext context, List steps) { + Map root = new LinkedHashMap<>(); + root.put("schemaVersion", "1.0"); + root.put("taskType", TASK_TYPE_RESET); + root.put("productType", context.productType()); + root.put("orgCode", context.orgCode()); + root.put("mqType", context.mqType()); + root.put("sourceTaskId", context.sourceTaskId()); + Map request = new LinkedHashMap<>(); + request.put("orgCode", context.orgCode()); + request.put("mqType", context.mqType()); + request.put("channelUsername", context.channelUsername()); + root.put("request", request); + root.put("steps", steps); + root.put("generatedAt", LocalDateTime.now().toString()); + + try { + return objectMapper.writeValueAsString(root); + } catch (JsonProcessingException ex) { + throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to serialize reset plan"); + } + } + private List defaultSteps(String productType, String mqType) { // 该步骤序列只描述“逻辑编排”,具体脚本命令在 step-command-profiles.json 中定义 if ("DIRECT".equals(productType)) { @@ -467,10 +596,27 @@ public class InitService { ); } - private String buildTaskId() { + private List resetSteps(String productType, String mqType) { + if ("DIRECT".equals(productType)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset is not supported for current product type: " + productType); + } + return List.of( + "STOP_APPS", + "DB_DROP_CMEP", + "DB_DROP_ORG", + "MQ_CLEANUP", + "STOP_RABBITMQ", + "TLQ_LICENSE_CLEANUP", + "CLEAN_STAGING", + "VERIFY_RESET" + ); + } + + private String buildTaskId(String taskType) { String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT); - return "INIT-" + ts + "-" + suffix; + String prefix = TASK_TYPE_RESET.equals(taskType) ? TASK_TYPE_RESET : TASK_TYPE_INIT; + return prefix + "-" + ts + "-" + suffix; } private String resolvePresetProductType() { @@ -489,6 +635,14 @@ public class InitService { return productType; } + private String resolveSupportedResetProductType() { + String productType = resolvePresetProductType(); + if ("DIRECT".equals(productType)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset is not supported for current product type: " + productType); + } + return productType; + } + private void validateCommon(InitPreviewRequest request) { if (request.getNodes() == null) { throw new IllegalArgumentException("nodes is required"); @@ -526,6 +680,100 @@ public class InitService { validateStandard(productType, request, deployMode, mqType); } + private ResolvedResetContext resolveCurrentResetContext() { + String productType = resolveSupportedResetProductType(); + Optional latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS"); + if (latestInit.isEmpty()) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized, reset is forbidden"); + } + Optional latestReset = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_RESET, "SUCCESS"); + if (latestReset.isPresent() && compareTaskRecency(latestReset.get(), latestInit.get()) >= 0) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device is not initialized, reset is forbidden"); + } + + ResolvedResetContext context = resolveResetContextFromInitTask(latestInit.get()); + if (!productType.equals(context.productType())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device preset product type changed, reset context is invalid"); + } + requireAllowed("mqType", context.mqType(), resolveStandardMqAllowed(productType)); + if (("RABBITMQ".equals(context.mqType()) || "RABBITMQ_TLQ".equals(context.mqType())) && isBlank(context.channelUsername())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "current init context missing rabbitmq channel username"); + } + return context; + } + + private ResolvedResetContext resolveResetContextFromInitTask(InitTaskEntity task) { + if (task == null) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"); + } + if (isBlank(task.getInitPlanJson())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is missing"); + } + try { + JsonNode root = objectMapper.readTree(task.getInitPlanJson()); + JsonNode request = root.path("request"); + JsonNode mq = request.path("mq"); + String productType = firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType())); + String orgCode = firstNonBlank(text(request, "orgCode"), text(root, "orgCode"), task.getOrgCode()); + String mqType = firstNonBlank(normalizeUpper(text(mq, "mqType")), normalizeUpper(text(root, "mqType")), normalizeUpper(task.getMqType())); + String channelUsername = firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername")); + + if (isBlank(productType) || isBlank(orgCode) || isBlank(mqType)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is incomplete"); + } + return new ResolvedResetContext(productType, normalize(orgCode), normalizeUpper(mqType), normalize(channelUsername), task.getTaskId()); + } catch (JsonProcessingException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot"); + } + } + + private CurrentInitConfigResponse resolveCurrentInitConfigFromTask(InitTaskEntity task) { + if (task == null) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"); + } + if (isBlank(task.getInitPlanJson())) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is missing"); + } + try { + JsonNode root = objectMapper.readTree(task.getInitPlanJson()); + JsonNode request = root.path("request"); + JsonNode mq = request.path("mq"); + JsonNode nodes = request.path("nodes"); + JsonNode signServer = request.path("signServer"); + + CurrentInitConfigResponse response = new CurrentInitConfigResponse(); + response.setOrgCodeType(firstNonBlank(normalizeUpper(text(request, "orgCodeType")), normalizeUpper(text(root, "orgCodeType")))); + response.setOrgCode(firstNonBlank(text(request, "orgCode"), text(root, "orgCode"), task.getOrgCode())); + response.setOrgNameCn(firstNonBlank(text(request, "orgNameCn"), text(root, "orgNameCn"))); + response.setOrgNameEn(firstNonBlank(text(request, "orgNameEn"), text(root, "orgNameEn"))); + response.setProductType(firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType()))); + response.setDeployMode(firstNonBlank(normalizeUpper(text(request, "deployMode")), normalizeUpper(task.getDeployMode()))); + response.setMqType(firstNonBlank(normalizeUpper(text(mq, "mqType")), normalizeUpper(text(root, "mqType")), normalizeUpper(task.getMqType()))); + response.setChannelUsername(firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername"))); + response.setNode01Ip(firstNonBlank(text(nodes, "node01Ip"), text(request, "node01Ip"))); + response.setNode02Ip(firstNonBlank(text(nodes, "node02Ip"), text(request, "node02Ip"))); + response.setSignHost(firstNonBlank(text(signServer, "signHost"), text(request, "signHost"))); + response.setSignPort(integerValue(firstNonBlank(text(signServer, "signPort"), text(request, "signPort")))); + response.setSignType(firstNonBlank(text(signServer, "signType"), text(request, "signType"))); + return response; + } catch (JsonProcessingException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot"); + } + } + + private int compareTaskRecency(InitTaskEntity left, InitTaskEntity right) { + Comparator comparator = Comparator.nullsFirst(LocalDateTime::compareTo); + int byUpdate = comparator.compare(left.getUpdateTime(), right.getUpdateTime()); + if (byUpdate != 0) { + return byUpdate; + } + int byCreate = comparator.compare(left.getCreateTime(), right.getCreateTime()); + if (byCreate != 0) { + return byCreate; + } + return normalize(left.getTaskId()).compareTo(normalize(right.getTaskId())); + } + private void validateStandard(String productType, InitPreviewRequest request, String deployMode, String mqType) { requireAllowed("deployMode", deployMode, DEPLOY_STANDARD); requireAllowed("mq.mqType", mqType, resolveStandardMqAllowed(productType)); @@ -628,6 +876,32 @@ public class InitService { } } + private static String firstNonBlank(String... values) { + if (values == null) { + return ""; + } + for (String value : values) { + if (!isBlank(value)) { + return normalize(value); + } + } + return ""; + } + + private static String text(JsonNode node, String fieldName) { + if (node == null || fieldName == null || node.isMissingNode() || node.isNull()) { + return ""; + } + JsonNode child = node.path(fieldName); + if (child.isMissingNode() || child.isNull()) { + return ""; + } + if (child.isTextual()) { + return normalize(child.asText()); + } + return normalize(child.toString()); + } + private static String normalizeUpper(String value) { return normalize(value).toUpperCase(Locale.ROOT); } @@ -636,7 +910,37 @@ public class InitService { return value == null ? "" : value.trim(); } + private static Integer integerValue(String value) { + String normalized = normalize(value); + if (isBlank(normalized)) { + return null; + } + try { + return Integer.valueOf(normalized); + } catch (NumberFormatException ex) { + return null; + } + } + private static boolean isBlank(String value) { return value == null || value.isBlank(); } + + public record DeviceInitStateSnapshot( + String initState, + String currentTaskType, + String currentTaskStatus, + String initializedOrgCode, + String initializedMqType + ) { + } + + private record ResolvedResetContext( + String productType, + String orgCode, + String mqType, + String channelUsername, + String sourceTaskId + ) { + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bcb3e2a..c8b35b6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -84,8 +84,14 @@ tms: standard-run-user: ${TMS_INIT_EXECUTOR_STANDARD_RUN_USER:cmep4i} # 标准版 CMSP/CMTP 启动命令。 standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh} + # 标准版 CMSP/CMTP 停止命令。 + standard-app-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_STOP_COMMAND:/home/tms/bin/stop_standard_apps.sh} # 标准版 nginx 启动命令。 standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:/home/tms/bin/start_standard_nginx.sh} + # 标准版 nginx 停止命令。 + standard-nginx-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_STOP_COMMAND:/home/tms/bin/stop_standard_nginx.sh} + # 标准版 RabbitMQ 停止命令。 + standard-rabbit-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_RABBIT_STOP_COMMAND:/home/tms/bin/stop_standard_rabbitmq.sh} # 标准版启动后检查命令。 standard-post-check-command: ${TMS_INIT_EXECUTOR_STANDARD_POST_CHECK_COMMAND:/home/tms/bin/check_standard_runtime.sh} # 直参版 cpconfig 文件名(按 upload-base-dir// 解析)。 @@ -149,6 +155,18 @@ tms: version: ${TMS_CISD_PRESET_VERSION:V1.0.0} # 预制信息来源标识(CONFIG/DB 等自定义值)。 source: ${TMS_CISD_PRESET_SOURCE:CONFIG} + device: + profile: + # 设备型号展示值。 + model: ${TMS_DEVICE_PROFILE_MODEL:SYD7108} + # 设备名称展示值。 + name: ${TMS_DEVICE_PROFILE_NAME:跨境支付终端一体机} + # CPU 型号默认展示值;命令读取失败或为空时回退。 + cpu-model-default: ${TMS_DEVICE_PROFILE_CPU_MODEL_DEFAULT:Hygon C86-3G (OPN:3350)} + # 内存总量默认展示值;命令读取失败或为空时回退。 + memory-total-default: ${TMS_DEVICE_PROFILE_MEMORY_TOTAL_DEFAULT:16GB} + # 磁盘总量默认展示值;命令读取失败或为空时回退。 + disk-total-default: ${TMS_DEVICE_PROFILE_DISK_TOTAL_DEFAULT:256G} security: # 内部接口鉴权 token(前后端内部调用需一致)。 internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token} diff --git a/src/main/resources/mapper/init/InitTaskMapper.xml b/src/main/resources/mapper/init/InitTaskMapper.xml index f57c2ba..12ce466 100644 --- a/src/main/resources/mapper/init/InitTaskMapper.xml +++ b/src/main/resources/mapper/init/InitTaskMapper.xml @@ -7,6 +7,7 @@ + @@ -19,10 +20,34 @@ + + + + + + diff --git a/src/test/java/com/cisd/tms/TmsApplicationTests.java b/src/test/java/com/cisd/tms/TmsApplicationTests.java index bd20ed4..a0ec30f 100644 --- a/src/test/java/com/cisd/tms/TmsApplicationTests.java +++ b/src/test/java/com/cisd/tms/TmsApplicationTests.java @@ -28,6 +28,10 @@ class TmsApplicationTests { .andExpect(content().string(org.hamcrest.Matchers.containsString("\"description\":\"CISD TMS 内部业务接口文档\""))) .andExpect(content().string(org.hamcrest.Matchers.containsString("\"/api/v1/init/template\""))) .andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"查询初始化模板\""))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"/api/v1/init/reset/preview\""))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"预检重置请求\""))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"/api/v1/init/reset/tasks/{taskId}/execute\""))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"执行重置任务\""))) .andExpect(content().string(org.hamcrest.Matchers.containsString("\"/api/v1/files/upload\""))) .andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"上传初始化文件\""))) .andExpect(content().string(org.hamcrest.Matchers.containsString("\"/api/v1/auth/login\""))) diff --git a/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java b/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java index c6174cb..dfef904 100644 --- a/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java +++ b/src/test/java/com/cisd/tms/modules/init/executor/ConfigurableInitStepExecutorTest.java @@ -647,11 +647,19 @@ class ConfigurableInitStepExecutorTest { properties.setStandardDbPassword("db_password"); properties.setStandardDbApplyCommand( "test \"$DB_USER\" = \"db_user\"" + + " && test \"$DB_HOST\" = \"tidb-host\"" + + " && test \"$DB_PORT\" = \"4000\"" + " && test \"$DB_PASSWORD\" = \"db_password\"" + " && test \"$LOAD_DIR\" = '" + loadDir.toAbsolutePath() + "'" + + " && test \"$ORG_CODE\" = \"AAAABBBBXXX\"" + + " && test \"$ORG_NAME_CN\" = \"测试机构\"" + " && echo applied > '" + marker + "'" ); - ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper()); + ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor( + properties, + new ObjectMapper(), + "jdbc:mysql://tidb-host:4000/tms?useUnicode=true" + ); InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-DB-APPLY-ENV-1", "RABBITMQ", "DUAL"); InitTaskStepEntity renderConfigStep = new InitTaskStepEntity(); @@ -820,18 +828,161 @@ class ConfigurableInitStepExecutorTest { Assertions.assertTrue(Files.exists(marker)); } + @Test + void shouldStopStandardAppsWithConfiguredCommand() throws IOException { + Path tempRoot = Files.createTempDirectory("init-executor-standard-app-stop-test"); + Path marker = tempRoot.resolve("app-stop.done"); + + InitExecutorProperties properties = new InitExecutorProperties(); + properties.setMode(InitExecutorProperties.Mode.LOCAL); + properties.setLogDir(tempRoot.resolve("logs").toString()); + properties.setStandardHomeDir(tempRoot.resolve("cmep-home").toString()); + properties.setStandardRunUser("cmep4i"); + properties.setStandardAppStopCommand( + "test \"$STANDARD_HOME_DIR\" = '" + tempRoot.resolve("cmep-home").toAbsolutePath() + "'" + + " && test \"$STANDARD_RUN_USER\" = 'cmep4i'" + + " && echo ok > '" + marker + "'" + ); + ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper()); + + InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-APP-STOP-1", "RABBITMQ", "SINGLE"); + InitTaskStepEntity step = new InitTaskStepEntity(); + step.setStepNo(1); + step.setStepCode("STOP_APPS"); + step.setCommandLine("internal::stop_apps"); + + InitStepExecutionResult result = executor.execute(task, step); + + Assertions.assertTrue(result.isSuccess()); + Assertions.assertEquals("ok", Files.readString(marker).trim()); + } + + @Test + void shouldCleanupStandardTlqLicenseForResetStep() throws IOException { + Path tempRoot = Files.createTempDirectory("init-executor-reset-tlq-cleanup-test"); + Path tlqTarget = tempRoot.resolve("tlq/tlqconf.zip"); + Files.createDirectories(tlqTarget.getParent()); + Files.writeString(tlqTarget, "license"); + + InitExecutorProperties properties = new InitExecutorProperties(); + properties.setMode(InitExecutorProperties.Mode.LOCAL); + properties.setLogDir(tempRoot.resolve("logs").toString()); + properties.setStandardTlqLicenseTargetPath(tlqTarget.toString()); + ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper()); + + InitTaskEntity task = buildStandardEnterpriseTask("TASK-RESET-TLQ-CLEANUP-1", "TLQ", "SINGLE"); + InitTaskStepEntity step = new InitTaskStepEntity(); + step.setStepNo(1); + step.setStepCode("TLQ_LICENSE_CLEANUP"); + step.setCommandLine("internal::tlq_license_cleanup"); + + InitStepExecutionResult result = executor.execute(task, step); + + Assertions.assertTrue(result.isSuccess()); + Assertions.assertFalse(Files.exists(tlqTarget)); + } + + @Test + void shouldCleanupOnlyCustomerRabbitResourcesForResetStep() throws IOException { + Path tempRoot = Files.createTempDirectory("init-executor-reset-mq-cleanup-test"); + Path calls = tempRoot.resolve("rabbit.calls.log"); + Path rabbitmqctl = tempRoot.resolve("rabbitmqctl"); + Files.writeString( + rabbitmqctl, + "#!/bin/bash\n" + + "echo \"$*\" >> \"" + calls + "\"\n" + + "exit 0\n" + ); + Assertions.assertTrue(rabbitmqctl.toFile().setExecutable(true)); + + InitExecutorProperties properties = new InitExecutorProperties(); + properties.setMode(InitExecutorProperties.Mode.LOCAL); + properties.setLogDir(tempRoot.resolve("logs").toString()); + properties.setRabbitmqCtlCommand(rabbitmqctl.toString()); + ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper()); + + InitTaskEntity task = new InitTaskEntity(); + task.setTaskId("TASK-RESET-MQ-CLEANUP-1"); + task.setProductType("ENTERPRISE"); + task.setInitPlanJson(buildResetPlanJson( + "ENTERPRISE", + Map.of( + "orgCode", "AAAABBBBXXX", + "mqType", "RABBITMQ", + "channelUsername", "bank_user" + ) + )); + + InitTaskStepEntity step = new InitTaskStepEntity(); + step.setStepNo(1); + step.setStepCode("MQ_CLEANUP"); + step.setCommandLine("internal::mq_cleanup"); + + InitStepExecutionResult result = executor.execute(task, step); + + Assertions.assertTrue(result.isSuccess()); + String content = Files.readString(calls); + Assertions.assertTrue(content.contains("status")); + Assertions.assertTrue(content.contains("delete_queue LQ_DTTRSP_IB01_CI01_AAAABBBBXXX")); + Assertions.assertTrue(content.contains("delete_queue -p /RQ LQ_IMG_CD00_CI00_AAAABBBBXXX")); + Assertions.assertTrue(content.contains("clear_permissions -p /RQ bank_user")); + Assertions.assertTrue(content.contains("delete_user bank_user")); + Assertions.assertFalse(content.contains("LQ_MSGRSP_GUI")); + Assertions.assertFalse(content.contains("LQ_SYNRSP_GUI")); + } + + @Test + void shouldStopRabbitmqWithConfiguredCommand() throws IOException { + Path tempRoot = Files.createTempDirectory("init-executor-standard-rabbit-stop-test"); + Path marker = tempRoot.resolve("rabbit-stop.done"); + + InitExecutorProperties properties = new InitExecutorProperties(); + properties.setMode(InitExecutorProperties.Mode.LOCAL); + properties.setLogDir(tempRoot.resolve("logs").toString()); + properties.setStandardRabbitStopCommand("echo ok > '" + marker + "'"); + ConfigurableInitStepExecutor executor = new ConfigurableInitStepExecutor(properties, new ObjectMapper()); + + InitTaskEntity task = new InitTaskEntity(); + task.setTaskId("TASK-RESET-RABBIT-STOP-1"); + task.setProductType("ENTERPRISE"); + task.setInitPlanJson(buildResetPlanJson( + "ENTERPRISE", + Map.of( + "orgCode", "AAAABBBBXXX", + "mqType", "RABBITMQ", + "channelUsername", "bank_user" + ) + )); + + InitTaskStepEntity step = new InitTaskStepEntity(); + step.setStepNo(1); + step.setStepCode("STOP_RABBITMQ"); + step.setCommandLine("internal::stop_rabbitmq"); + + InitStepExecutionResult result = executor.execute(task, step); + + Assertions.assertTrue(result.isSuccess()); + Assertions.assertEquals("ok", Files.readString(marker).trim()); + } + @Test void shouldExposeStandardInitScriptTemplates() throws IOException { Path repoRoot = Paths.get("").toAbsolutePath().normalize(); Path scriptDir = repoRoot.resolve("scripts/standard-init"); Path dbScript = scriptDir.resolve("apply_standard_db.sh"); Path appScript = scriptDir.resolve("start_standard_apps.sh"); + Path stopAppScript = scriptDir.resolve("stop_standard_apps.sh"); Path nginxScript = scriptDir.resolve("start_standard_nginx.sh"); + Path stopNginxScript = scriptDir.resolve("stop_standard_nginx.sh"); + Path stopRabbitScript = scriptDir.resolve("stop_standard_rabbitmq.sh"); Path checkScript = scriptDir.resolve("check_standard_runtime.sh"); Assertions.assertTrue(Files.exists(dbScript)); Assertions.assertTrue(Files.exists(appScript)); + Assertions.assertTrue(Files.exists(stopAppScript)); Assertions.assertTrue(Files.exists(nginxScript)); + Assertions.assertTrue(Files.exists(stopNginxScript)); + Assertions.assertTrue(Files.exists(stopRabbitScript)); Assertions.assertTrue(Files.exists(checkScript)); String dbContent = Files.readString(dbScript); @@ -839,8 +990,14 @@ class ConfigurableInitStepExecutorTest { Assertions.assertTrue(dbContent.contains("CMEP.sql")); Assertions.assertTrue(dbContent.contains("UP-ORG-INFO.sql")); Assertions.assertTrue(dbContent.contains("LOAD_DIR")); + Assertions.assertTrue(dbContent.contains("ORG_CODE")); + Assertions.assertTrue(dbContent.contains("ORG_NAME_CN")); + Assertions.assertTrue(dbContent.contains("CIPS_USER_PARAM_INFO")); + Assertions.assertTrue(dbContent.contains("ORG_CHINESE_NAME")); Assertions.assertTrue(dbContent.contains("--force")); Assertions.assertFalse(dbContent.contains("/home/cemp4i/mysql/loadfilepath")); + Assertions.assertTrue(dbContent.indexOf("CMEP.sql") < dbContent.indexOf("UP-ORG-INFO.sql")); + Assertions.assertTrue(dbContent.indexOf("UP-ORG-INFO.sql") < dbContent.indexOf("SCHEMA-DDL.sql")); String appContent = Files.readString(appScript); Assertions.assertTrue(appContent.contains("STANDARD_HOME_DIR")); @@ -850,9 +1007,22 @@ class ConfigurableInitStepExecutorTest { Assertions.assertFalse(appContent.contains("/home/cemp4i/cmsp/start_cmsp.sh")); Assertions.assertFalse(appContent.contains("su - cmep4i")); + String stopAppContent = Files.readString(stopAppScript); + Assertions.assertTrue(stopAppContent.contains("STANDARD_HOME_DIR")); + Assertions.assertTrue(stopAppContent.contains("STANDARD_RUN_USER")); + Assertions.assertTrue(stopAppContent.contains("stop_cmsp.sh")); + Assertions.assertTrue(stopAppContent.contains("stop_cmtp.sh")); + + String stopRabbitContent = Files.readString(stopRabbitScript); + Assertions.assertTrue(stopRabbitContent.contains("systemctl stop")); + Assertions.assertTrue(stopRabbitContent.contains("rabbitmq-server")); + String nginxContent = Files.readString(nginxScript); Assertions.assertTrue(nginxContent.contains("/usr/local/nginx/sbin/nginx")); + String stopNginxContent = Files.readString(stopNginxScript); + Assertions.assertTrue(stopNginxContent.contains("/usr/local/nginx/sbin/nginx")); + String checkContent = Files.readString(checkScript); Assertions.assertTrue(checkContent.contains("CMEP-CMSP")); Assertions.assertTrue(checkContent.contains("CMEP-CMTP")); @@ -870,12 +1040,16 @@ class ConfigurableInitStepExecutorTest { Assertions.assertTrue(exampleContent.contains("standard-db-apply-command: /home/tms/bin/apply_standard_db.sh")); Assertions.assertTrue(exampleContent.contains("standard-run-user: cmep4i")); Assertions.assertTrue(exampleContent.contains("standard-app-start-command: /home/tms/bin/start_standard_apps.sh")); + Assertions.assertTrue(exampleContent.contains("standard-app-stop-command: /home/tms/bin/stop_standard_apps.sh")); Assertions.assertTrue(exampleContent.contains("standard-nginx-start-command: /home/tms/bin/start_standard_nginx.sh")); + Assertions.assertTrue(exampleContent.contains("standard-nginx-stop-command: /home/tms/bin/stop_standard_nginx.sh")); Assertions.assertTrue(exampleContent.contains("standard-post-check-command: /home/tms/bin/check_standard_runtime.sh")); String applicationContent = Files.readString(applicationConfig); Assertions.assertTrue(applicationContent.contains("standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh}")); + Assertions.assertTrue(applicationContent.contains("standard-app-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_STOP_COMMAND:/home/tms/bin/stop_standard_apps.sh}")); Assertions.assertTrue(applicationContent.contains("standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:/home/tms/bin/start_standard_nginx.sh}")); + Assertions.assertTrue(applicationContent.contains("standard-nginx-stop-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_STOP_COMMAND:/home/tms/bin/stop_standard_nginx.sh}")); Assertions.assertTrue(applicationContent.contains("standard-post-check-command: ${TMS_INIT_EXECUTOR_STANDARD_POST_CHECK_COMMAND:/home/tms/bin/check_standard_runtime.sh}")); String readmeContent = Files.readString(readme); @@ -1159,6 +1333,15 @@ class ConfigurableInitStepExecutorTest { return new ObjectMapper().writeValueAsString(root); } + private static String buildResetPlanJson(String productType, Map request) throws IOException { + Map root = new LinkedHashMap<>(); + root.put("schemaVersion", "1.0"); + root.put("taskType", "RESET"); + root.put("productType", productType); + root.put("request", request); + return new ObjectMapper().writeValueAsString(root); + } + private static InitTaskEntity buildStandardEnterpriseTask(String taskId, String mqType, String deployMode) throws IOException { InitTaskEntity task = new InitTaskEntity(); task.setTaskId(taskId); diff --git a/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java b/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java index e9c68d1..65f5c5d 100644 --- a/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/init/service/InitServiceTest.java @@ -7,10 +7,13 @@ import com.cisd.tms.modules.init.config.InitExecutorProperties; import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; import com.cisd.tms.modules.init.dto.InitPreviewRequest; import com.cisd.tms.modules.init.dto.InitPreviewResponse; +import com.cisd.tms.modules.init.dto.ResetCreateTaskResponse; +import com.cisd.tms.modules.init.dto.ResetPreviewResponse; import com.cisd.tms.modules.init.dto.InitTaskDetailResponse; import com.cisd.tms.modules.init.dto.InitTaskExecuteResponse; import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse; import com.cisd.tms.modules.init.dto.InitTaskStepResponse; +import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; import com.cisd.tms.modules.init.entity.InitTaskEntity; import com.cisd.tms.modules.init.entity.InitTaskStepEntity; import com.cisd.tms.modules.init.executor.InitStepExecutionResult; @@ -152,6 +155,113 @@ class InitServiceTest { Assertions.assertNotNull(response.getSummary()); } + @Test + void shouldGenerateResetPreviewForEnterpriseRequest() { + InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + createSuccessfulInitTask(service, validEnterpriseRequest()); + + ResetPreviewResponse response = service.previewReset(); + + Assertions.assertEquals("ENTERPRISE", response.getResolvedProductType()); + Assertions.assertEquals(8, response.getStepCount()); + } + + @Test + void shouldGenerateResetPreviewForIndirectRequest() { + InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService service = newService(repository, stepRepository, preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + InitPreviewRequest request = validEnterpriseRequest(); + request.getMq().setMqType("RABBITMQ_TLQ"); + request.getMq().setTlqLicenseFileId("file-tlq-001"); + createSuccessfulInitTask(service, request); + + ResetPreviewResponse response = service.previewReset(); + + Assertions.assertEquals("INDIRECT", response.getResolvedProductType()); + Assertions.assertEquals(8, response.getStepCount()); + } + + @Test + void shouldRejectResetForDirectMode() { + InitService service = newService(emptyRepository(), emptyStepRepository(), preset("DIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + + BizException ex = Assertions.assertThrows(BizException.class, service::previewReset); + Assertions.assertTrue(ex.getMessage().contains("reset")); + } + + @Test + void shouldRejectResetWhenNoSuccessfulInitSnapshotExists() { + InitService service = newService(emptyRepository(), emptyStepRepository(), preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + + BizException ex = Assertions.assertThrows(BizException.class, service::previewReset); + + Assertions.assertTrue(ex.getMessage().contains("not initialized")); + } + + @Test + void shouldLoadCurrentInitConfigFromLatestSuccessfulInitTask() { + InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + InitPreviewRequest request = validEnterpriseRequest(); + request.setOrgNameEn("Test Organization"); + request.getNodes().setNode02Ip("10.0.0.2"); + InitPreviewRequest.SignServer signServer = new InitPreviewRequest.SignServer(); + signServer.setSignHost("172.30.1.101"); + signServer.setSignPort(50006); + signServer.setSignType("INFSEC"); + request.setSignServer(signServer); + createSuccessfulInitTask(service, request); + + CurrentInitConfigResponse response = service.loadCurrentInitConfig(); + + Assertions.assertEquals("BIC", response.getOrgCodeType()); + Assertions.assertEquals("AAAABBBBXXX", response.getOrgCode()); + Assertions.assertEquals("测试机构", response.getOrgNameCn()); + Assertions.assertEquals("Test Organization", response.getOrgNameEn()); + Assertions.assertEquals("ENTERPRISE", response.getProductType()); + Assertions.assertEquals("SINGLE", response.getDeployMode()); + Assertions.assertEquals("RABBITMQ", response.getMqType()); + Assertions.assertEquals("cisd_channel_user", response.getChannelUsername()); + Assertions.assertEquals("10.0.0.1", response.getNode01Ip()); + Assertions.assertEquals("10.0.0.2", response.getNode02Ip()); + Assertions.assertEquals("172.30.1.101", response.getSignHost()); + Assertions.assertEquals(50006, response.getSignPort()); + Assertions.assertEquals("INFSEC", response.getSignType()); + } + + @Test + void shouldRejectCurrentInitConfigWhenDeviceNotInitialized() { + InitService service = newService(emptyRepository(), emptyStepRepository(), preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + + BizException ex = Assertions.assertThrows(BizException.class, service::loadCurrentInitConfig); + + Assertions.assertTrue(ex.getMessage().contains("not initialized")); + } + + @Test + void shouldLoadCurrentInitConfigFromLatestSuccessfulInitTaskEvenAfterSuccessfulReset() { + InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + InitPreviewRequest request = validEnterpriseRequest(); + request.setOrgNameEn("Test Organization"); + createSuccessfulInitTask(service, request); + + ResetCreateTaskResponse resetTask = service.createResetTask(); + service.executeResetTask(resetTask.getTaskId()); + awaitTaskStatus(service, resetTask.getTaskId(), "SUCCESS"); + + CurrentInitConfigResponse response = service.loadCurrentInitConfig(); + + Assertions.assertEquals("AAAABBBBXXX", response.getOrgCode()); + Assertions.assertEquals("RABBITMQ", response.getMqType()); + Assertions.assertEquals("Test Organization", response.getOrgNameEn()); + } + @Test void shouldPlanEnterpriseTlqWithoutRabbitmqSteps() { InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); @@ -177,6 +287,58 @@ class InitServiceTest { assertContainsStandardPostDeploySteps(stepCodes); } + @Test + void shouldPlanEnterpriseResetSteps() { + InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + + createSuccessfulInitTask(service, validEnterpriseRequest()); + + ResetCreateTaskResponse created = service.createResetTask(); + InitTaskDetailResponse detail = service.getTask(created.getTaskId()); + List stepCodes = service.getTaskSteps(created.getTaskId()).stream().map(InitTaskStepResponse::getStepCode).toList(); + + Assertions.assertEquals("RESET", created.getTaskType()); + Assertions.assertEquals("RESET", detail.getTaskType()); + Assertions.assertEquals(List.of( + "STOP_APPS", + "DB_DROP_CMEP", + "DB_DROP_ORG", + "MQ_CLEANUP", + "STOP_RABBITMQ", + "TLQ_LICENSE_CLEANUP", + "CLEAN_STAGING", + "VERIFY_RESET" + ), stepCodes); + } + + @Test + void shouldPlanIndirectResetSteps() { + InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService service = newService(repository, stepRepository, preset("INDIRECT", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + + InitPreviewRequest request = validEnterpriseRequest(); + request.getMq().setMqType("RABBITMQ_TLQ"); + request.getMq().setTlqLicenseFileId("file-tlq-001"); + createSuccessfulInitTask(service, request); + + ResetCreateTaskResponse created = service.createResetTask(); + List stepCodes = service.getTaskSteps(created.getTaskId()).stream().map(InitTaskStepResponse::getStepCode).toList(); + + Assertions.assertEquals(List.of( + "STOP_APPS", + "DB_DROP_CMEP", + "DB_DROP_ORG", + "MQ_CLEANUP", + "STOP_RABBITMQ", + "TLQ_LICENSE_CLEANUP", + "CLEAN_STAGING", + "VERIFY_RESET" + ), stepCodes); + } + @Test void shouldPlanIndirectRabbitmqTlqWithTlqAndRabbitmqSteps() { InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); @@ -277,6 +439,49 @@ class InitServiceTest { steps.stream().anyMatch(s -> "SET_RABBITMQ".equals(s.getStepCode()) && s.getCommandLine().contains("/home/cemp4i/cpackage/toolsh/setfraq.sh")) ); + Assertions.assertEquals("INIT", detail.getTaskType()); + } + + @Test + void shouldExecuteResetTaskAndMarkAllStepsSuccess() { + InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), successExecutor(), "/tmp/tms-init-logs"); + + createSuccessfulInitTask(service, validEnterpriseRequest()); + + ResetCreateTaskResponse created = service.createResetTask(); + InitTaskExecuteResponse executeResponse = service.executeResetTask(created.getTaskId()); + awaitTaskStatus(service, created.getTaskId(), "SUCCESS"); + InitTaskDetailResponse detail = service.getTask(created.getTaskId()); + List steps = service.getTaskSteps(created.getTaskId()); + + Assertions.assertEquals("RUNNING", executeResponse.getStatus()); + Assertions.assertEquals("RESET", detail.getTaskType()); + Assertions.assertEquals("SUCCESS", detail.getStatus()); + Assertions.assertTrue(steps.stream().allMatch(s -> "SUCCESS".equals(s.getStatus()))); + } + + @Test + void shouldResumeResetTaskFromFirstFailedStepOnRetry() { + InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + FailOnceOnStepExecutor executor = new FailOnceOnStepExecutor("MQ_CLEANUP"); + InitService service = newService(repository, stepRepository, preset("ENTERPRISE", "V1.3.2", "CONFIG"), executor, "/tmp/tms-init-logs"); + + createSuccessfulInitTask(service, validEnterpriseRequest()); + + ResetCreateTaskResponse created = service.createResetTask(); + + service.executeResetTask(created.getTaskId()); + awaitTaskStatus(service, created.getTaskId(), "FAILED"); + + service.executeResetTask(created.getTaskId()); + awaitTaskStatus(service, created.getTaskId(), "SUCCESS"); + + Assertions.assertEquals(1, executor.executionCount("STOP_APPS")); + Assertions.assertEquals(2, executor.executionCount("MQ_CLEANUP")); + Assertions.assertEquals(1, executor.executionCount("VERIFY_RESET")); } @Test @@ -548,6 +753,12 @@ class InitServiceTest { return request; } + private static void createSuccessfulInitTask(InitService service, InitPreviewRequest request) { + InitCreateTaskResponse created = service.createTask(request); + service.executeTask(created.getTaskId()); + awaitTaskStatus(service, created.getTaskId(), "SUCCESS"); + } + private static InitPreviewRequest.DirectTlqConfig directTlqConfig() { InitPreviewRequest.DirectTlqConfig config = new InitPreviewRequest.DirectTlqConfig(); config.setLocalNodeSeq("A"); @@ -587,6 +798,27 @@ class InitServiceTest { return Optional.ofNullable(data.get(taskId)); } + @Override + public Optional findLatest() { + return data.values().stream() + .max((left, right) -> compareTaskRecency(left, right)); + } + + @Override + public Optional findLatestByStatus(String status) { + return data.values().stream() + .filter(entity -> status.equals(entity.getStatus())) + .max((left, right) -> compareTaskRecency(left, right)); + } + + @Override + public Optional findLatestByTaskTypeAndStatus(String taskType, String status) { + return data.values().stream() + .filter(entity -> taskType.equals(entity.getTaskType())) + .filter(entity -> status.equals(entity.getStatus())) + .max((left, right) -> compareTaskRecency(left, right)); + } + @Override public void save(InitTaskEntity entity) { if (entity.getCreateTime() == null) { @@ -601,6 +833,18 @@ class InitServiceTest { entity.setUpdateTime(LocalDateTime.now()); data.put(entity.getTaskId(), entity); } + + private static int compareTaskRecency(InitTaskEntity left, InitTaskEntity right) { + int byUpdate = java.util.Comparator.nullsFirst(LocalDateTime::compareTo).compare(left.getUpdateTime(), right.getUpdateTime()); + if (byUpdate != 0) { + return byUpdate; + } + int byCreate = java.util.Comparator.nullsFirst(LocalDateTime::compareTo).compare(left.getCreateTime(), right.getCreateTime()); + if (byCreate != 0) { + return byCreate; + } + return left.getTaskId().compareTo(right.getTaskId()); + } } private static class InMemoryInitTaskStepRepository implements InitTaskStepRepository {