feat:设备配置信息、运行状态、设备画像接口

This commit is contained in:
waner
2026-03-13 17:30:57 +08:00
parent bf8ec7054c
commit 3b4f549dab
24 changed files with 1378 additions and 39 deletions
+64
View File
@@ -70,11 +70,75 @@ Open:
- External sign API: `POST /openapi/v1/sign/signature` - External sign API: `POST /openapi/v1/sign/signature`
- Init template API: `GET /api/v1/init/template` - Init template API: `GET /api/v1/init/template`
- Init preview API: `POST /api/v1/init/preview` - 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` - Auth login API: `POST /api/v1/auth/login`
- Device info API: `GET /api/v1/device/info` - 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 count API: `GET /api/v1/device/crypto/device-count`
- PCIe crypto HMAC API: `POST /api/v1/device/crypto/hmac` - 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://<ip>:8080/swagger-ui.html` 和 `http://<ip>:8080/v3/api-docs/internal-api` 为准
## Architecture (Monolith + Modular) ## Architecture (Monolith + Modular)
- One deployable Spring Boot application. - One deployable Spring Boot application.
+10
View File
@@ -17,6 +17,13 @@ tms:
product-type: ENTERPRISE product-type: ENTERPRISE
version: V1.3.2 version: V1.3.2
source: CONFIG source: CONFIG
device:
profile:
model: SYD7108
name: 跨境支付终端一体机
cpu-model-default: ""
memory-total-default: 16GB
disk-total-default: 256G
file: file:
storage: storage:
upload-base-dir: /home/tms/uploads upload-base-dir: /home/tms/uploads
@@ -36,7 +43,10 @@ tms:
standard-web-organization-json-path: /usr/local/nginx/html/organization.json standard-web-organization-json-path: /usr/local/nginx/html/organization.json
standard-run-user: cmep4i standard-run-user: cmep4i
standard-app-start-command: /home/tms/bin/start_standard_apps.sh 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-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 standard-post-check-command: /home/tms/bin/check_standard_runtime.sh
upload-base-dir: /home/tms/uploads upload-base-dir: /home/tms/uploads
staging-root-dir: /tmp/tms-init-staging staging-root-dir: /tmp/tms-init-staging
+17 -7
View File
@@ -3,19 +3,29 @@ set -euo pipefail
DB_HOST="${DB_HOST:-127.0.0.1}" DB_HOST="${DB_HOST:-127.0.0.1}"
DB_PORT="${DB_PORT:-4000}" 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_USER="${DB_USER:?DB_USER is required}"
DB_PASSWORD="${DB_PASSWORD:?DB_PASSWORD is required}" DB_PASSWORD="${DB_PASSWORD:?DB_PASSWORD is required}"
LOAD_DIR="${LOAD_DIR:?LOAD_DIR 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}" \ 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}" \ mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${CMEP_DB_NAME}" \
< "${LOAD_DIR}/SCHEMA-DDL.sql"
mysql --force -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" \
< "${LOAD_DIR}/CMEP.sql" < "${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" < "${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"
@@ -201,15 +201,15 @@ public class JnaPcieCryptoService implements PcieCryptoService {
} }
@Override @Override
public DeviceStatusResult getDeviceStatus(int statusBufferSize) { public DeviceStatusResult getDeviceStatus() {
int size = requirePositive("statusBufferSize", statusBufferSize); int size = 8;
return sessionTemplate.withSession("SDFE_DeviceStatusGet", (lib, deviceHandle, sessionHandle) -> { 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)); sessionTemplate.ensureSuccess("SDFE_DeviceStatusGet", lib.SDFE_DeviceStatusGet(sessionHandle, statusMemory));
DeviceStatusResult result = new DeviceStatusResult(); DeviceStatusResult result = new DeviceStatusResult();
result.setLength(size); // result.setLength(size);
result.setStatusData(statusMemory.getByteArray(0, size)); result.setStatusData(statusMemory.getByteArray(0, size));
return result; return result;
}); });
@@ -106,8 +106,8 @@ public class MockPcieCryptoService implements PcieCryptoService {
} }
@Override @Override
public DeviceStatusResult getDeviceStatus(int statusBufferSize) { public DeviceStatusResult getDeviceStatus() {
int size = requirePositive("statusBufferSize", statusBufferSize); int size = 8;
DeviceStatusResult result = new DeviceStatusResult(); DeviceStatusResult result = new DeviceStatusResult();
result.setLength(size); result.setLength(size);
result.setStatusData(new byte[size]); result.setStatusData(new byte[size]);
@@ -73,7 +73,7 @@ public interface PcieCryptoService {
void deviceSelfTest(); void deviceSelfTest();
DeviceStatusResult getDeviceStatus(int statusBufferSize); DeviceStatusResult getDeviceStatus();
void deviceFactoryReset(); void deviceFactoryReset();
@@ -3,6 +3,10 @@ package com.cisd.tms.modules.device.controller;
import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.device.dto.DeviceActionResponse; import com.cisd.tms.modules.device.dto.DeviceActionResponse;
import com.cisd.tms.modules.device.dto.DeviceInfoResponse; 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 com.cisd.tms.modules.device.service.DeviceService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@@ -19,9 +23,17 @@ import org.springframework.web.bind.annotation.RestController;
public class DeviceController { public class DeviceController {
private final DeviceService deviceService; 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.deviceService = deviceService;
this.deviceProfileService = deviceProfileService;
this.deviceRuntimeStatusService = deviceRuntimeStatusService;
} }
@GetMapping("/info") @GetMapping("/info")
@@ -31,11 +43,23 @@ public class DeviceController {
} }
@GetMapping("/status") @GetMapping("/status")
@Operation(summary = "查询设备状态", description = "返回当前设备运行状态与版本快照。") @Operation(summary = "查询设备状态", description = "返回当前设备运行状态、初始化状态和版本快照。")
public ApiResponse<DeviceInfoResponse> status() { public ApiResponse<DeviceInfoResponse> status() {
return ApiResponse.success(deviceService.info()); return ApiResponse.success(deviceService.info());
} }
@GetMapping("/profile")
@Operation(summary = "查询设备画像", description = "返回设备静态信息,包括设备信息、硬件信息和按初始化方案裁剪后的软件版本信息。")
public ApiResponse<DeviceProfileResponse> profile() {
return ApiResponse.success(deviceProfileService.profile());
}
@GetMapping("/runtime-status")
@Operation(summary = "查询设备运行状态", description = "返回硬件实时资源使用情况、加密卡状态,以及按初始化配置裁剪后的软件运行状态。")
public ApiResponse<DeviceRuntimeStatusResponse> runtimeStatus() {
return ApiResponse.success(deviceRuntimeStatusService.loadRuntimeStatus());
}
@PostMapping("/restart") @PostMapping("/restart")
@Operation(summary = "重启设备服务", description = "受理设备重启请求,并记录触发操作的内部用户。") @Operation(summary = "重启设备服务", description = "受理设备重启请求,并记录触发操作的内部用户。")
public ApiResponse<DeviceActionResponse> restart( public ApiResponse<DeviceActionResponse> restart(
@@ -17,6 +17,8 @@ public class DeviceInfoResponse {
private String presetVersion; private String presetVersion;
@Schema(description = "预置来源", example = "application.yml") @Schema(description = "预置来源", example = "application.yml")
private String presetSource; private String presetSource;
@Schema(description = "设备初始化状态", example = "INITIALIZED")
private String initState;
public String getDeviceId() { public String getDeviceId() {
return deviceId; return deviceId;
@@ -65,4 +67,12 @@ public class DeviceInfoResponse {
public void setPresetSource(String presetSource) { public void setPresetSource(String presetSource) {
this.presetSource = presetSource; this.presetSource = presetSource;
} }
public String getInitState() {
return initState;
}
public void setInitState(String initState) {
this.initState = initState;
}
} }
@@ -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.dto.DeviceInfoResponse;
import com.cisd.tms.modules.device.entity.DeviceNodeEntity; import com.cisd.tms.modules.device.entity.DeviceNodeEntity;
import com.cisd.tms.modules.device.repository.DeviceNodeRepository; import com.cisd.tms.modules.device.repository.DeviceNodeRepository;
import com.cisd.tms.modules.init.service.InitService;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -13,10 +14,16 @@ public class DeviceService {
private final DeviceNodeRepository deviceNodeRepository; private final DeviceNodeRepository deviceNodeRepository;
private final CisdPresetProperties cisdPresetProperties; 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.deviceNodeRepository = deviceNodeRepository;
this.cisdPresetProperties = cisdPresetProperties; this.cisdPresetProperties = cisdPresetProperties;
this.initService = initService;
} }
public DeviceInfoResponse info() { public DeviceInfoResponse info() {
@@ -36,6 +43,8 @@ public class DeviceService {
response.setPresetProductType(cisdPresetProperties.getProductType()); response.setPresetProductType(cisdPresetProperties.getProductType());
response.setPresetVersion(cisdPresetProperties.getVersion()); response.setPresetVersion(cisdPresetProperties.getVersion());
response.setPresetSource(cisdPresetProperties.getSource()); response.setPresetSource(cisdPresetProperties.getSource());
InitService.DeviceInitStateSnapshot snapshot = initService.getDeviceInitStateSnapshot();
response.setInitState(snapshot.initState());
return response; return response;
} }
@@ -50,8 +50,14 @@ public class InitExecutorProperties {
private String standardRunUser = DEFAULT_STANDARD_RUN_USER; private String standardRunUser = DEFAULT_STANDARD_RUN_USER;
// 标准版 CMSP/CMTP 启动命令 // 标准版 CMSP/CMTP 启动命令
private String standardAppStartCommand = ""; private String standardAppStartCommand = "";
// 标准版 CMSP/CMTP 停止命令
private String standardAppStopCommand = "";
// 标准版 nginx 启动命令 // 标准版 nginx 启动命令
private String standardNginxStartCommand = ""; private String standardNginxStartCommand = "";
// 标准版 nginx 停止命令
private String standardNginxStopCommand = "";
// 标准版 RabbitMQ 停止命令
private String standardRabbitStopCommand = "";
// 标准版启动后检查命令 // 标准版启动后检查命令
private String standardPostCheckCommand = ""; private String standardPostCheckCommand = "";
// 直参版 cfgZipFileId 对应目录下的 cpconfig 文件名 // 直参版 cfgZipFileId 对应目录下的 cpconfig 文件名
@@ -288,6 +294,14 @@ public class InitExecutorProperties {
this.standardAppStartCommand = standardAppStartCommand; this.standardAppStartCommand = standardAppStartCommand;
} }
public String getStandardAppStopCommand() {
return standardAppStopCommand;
}
public void setStandardAppStopCommand(String standardAppStopCommand) {
this.standardAppStopCommand = standardAppStopCommand;
}
public String getStandardNginxStartCommand() { public String getStandardNginxStartCommand() {
return standardNginxStartCommand; return standardNginxStartCommand;
} }
@@ -296,6 +310,22 @@ public class InitExecutorProperties {
this.standardNginxStartCommand = standardNginxStartCommand; 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() { public String getStandardPostCheckCommand() {
return standardPostCheckCommand; return standardPostCheckCommand;
} }
@@ -2,6 +2,7 @@ package com.cisd.tms.modules.init.controller;
import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; 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.InitPlanTemplateResponse;
import com.cisd.tms.modules.init.dto.InitPreviewRequest; import com.cisd.tms.modules.init.dto.InitPreviewRequest;
import com.cisd.tms.modules.init.dto.InitPreviewResponse; 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.InitTaskExecuteResponse;
import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse; import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse;
import com.cisd.tms.modules.init.dto.InitTaskStepResponse; 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 com.cisd.tms.modules.init.service.InitService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@@ -65,6 +68,12 @@ public class InitController {
return ApiResponse.success(initService.createTask(request)); return ApiResponse.success(initService.createTask(request));
} }
@GetMapping("/config")
@Operation(summary = "查询当前初始化配置", description = "从最新一次成功初始化任务快照中返回当前已初始化的 CISD 核心展示字段,不用于编辑。")
public ApiResponse<CurrentInitConfigResponse> currentConfig() {
return ApiResponse.success(initService.loadCurrentInitConfig());
}
@GetMapping("/tasks/{taskId}") @GetMapping("/tasks/{taskId}")
@Operation(summary = "查询任务详情", description = "返回初始化任务的主状态、产品类型和原始计划快照。") @Operation(summary = "查询任务详情", description = "返回初始化任务的主状态、产品类型和原始计划快照。")
public ApiResponse<InitTaskDetailResponse> taskDetail( public ApiResponse<InitTaskDetailResponse> taskDetail(
@@ -109,4 +118,54 @@ public class InitController {
) { ) {
return ApiResponse.success(initService.executeTask(taskId)); return ApiResponse.success(initService.executeTask(taskId));
} }
@PostMapping("/reset/preview")
@Operation(summary = "预检重置请求", description = "自动读取当前设备有效初始化快照,返回重置任务的产品类型与预估步骤数量,不需要前端再次输入机构和 MQ 参数。")
public ApiResponse<ResetPreviewResponse> previewReset() {
return ApiResponse.success(initService.previewReset());
}
@PostMapping("/reset/tasks")
@Operation(summary = "创建重置任务", description = "自动读取当前设备有效初始化快照生成重置任务和步骤记录,任务初始状态为 PENDING。")
public ApiResponse<ResetCreateTaskResponse> createResetTask() {
return ApiResponse.success(initService.createResetTask());
}
@PostMapping("/reset/tasks/{taskId}/execute")
@Operation(summary = "执行重置任务", description = "异步受理重置任务执行请求,立即返回当前任务快照,实际步骤在后台继续运行。")
public ApiResponse<InitTaskExecuteResponse> executeResetTask(
@Parameter(description = "重置任务唯一标识")
@PathVariable String taskId
) {
return ApiResponse.success(initService.executeResetTask(taskId));
}
@GetMapping("/reset/tasks/{taskId}")
@Operation(summary = "查询重置任务详情", description = "返回重置任务的主状态、产品类型和原始计划快照。")
public ApiResponse<InitTaskDetailResponse> resetTaskDetail(
@Parameter(description = "重置任务唯一标识")
@PathVariable String taskId
) {
return ApiResponse.success(initService.getTask(taskId));
}
@GetMapping("/reset/tasks/{taskId}/steps")
@Operation(summary = "查询重置任务步骤", description = "返回重置任务下全部步骤的执行状态和命令信息。")
public ApiResponse<List<InitTaskStepResponse>> resetTaskSteps(
@Parameter(description = "重置任务唯一标识")
@PathVariable String taskId
) {
return ApiResponse.success(initService.getTaskSteps(taskId));
}
@GetMapping("/reset/tasks/{taskId}/steps/{stepNo}/log")
@Operation(summary = "查询重置步骤日志", description = "返回指定重置任务步骤的日志路径和日志内容,用于定位失败原因。")
public ApiResponse<InitTaskStepLogResponse> resetTaskStepLog(
@Parameter(description = "重置任务唯一标识")
@PathVariable String taskId,
@Parameter(description = "任务步骤序号,从 1 开始")
@PathVariable int stepNo
) {
return ApiResponse.success(initService.getTaskStepLog(taskId, stepNo));
}
} }
@@ -9,6 +9,8 @@ public class InitCreateTaskResponse {
private String taskId; private String taskId;
@Schema(description = "任务状态", example = "PENDING") @Schema(description = "任务状态", example = "PENDING")
private String status; private String status;
@Schema(description = "任务类型", example = "INIT")
private String taskType;
@Schema(description = "解析后的产品类型", example = "STANDARD") @Schema(description = "解析后的产品类型", example = "STANDARD")
private String resolvedProductType; private String resolvedProductType;
@Schema(description = "命中的预制版本", example = "6.6.4") @Schema(description = "命中的预制版本", example = "6.6.4")
@@ -30,6 +32,14 @@ public class InitCreateTaskResponse {
this.status = status; this.status = status;
} }
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public String getResolvedProductType() { public String getResolvedProductType() {
return resolvedProductType; return resolvedProductType;
} }
@@ -9,6 +9,8 @@ public class InitTaskDetailResponse {
private String taskId; private String taskId;
@Schema(description = "任务状态", example = "RUNNING") @Schema(description = "任务状态", example = "RUNNING")
private String status; private String status;
@Schema(description = "任务类型", example = "INIT")
private String taskType;
@Schema(description = "解析后的产品类型", example = "STANDARD") @Schema(description = "解析后的产品类型", example = "STANDARD")
private String resolvedProductType; private String resolvedProductType;
@Schema(description = "命中的预制版本", example = "6.6.4") @Schema(description = "命中的预制版本", example = "6.6.4")
@@ -38,6 +40,14 @@ public class InitTaskDetailResponse {
this.status = status; this.status = status;
} }
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public String getResolvedProductType() { public String getResolvedProductType() {
return resolvedProductType; return resolvedProductType;
} }
@@ -7,6 +7,7 @@ import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
public class InitTaskEntity extends BaseEntity { public class InitTaskEntity extends BaseEntity {
private String taskId; private String taskId;
private String taskType;
private String version; private String version;
private String status; private String status;
private String productType; private String productType;
@@ -23,6 +24,14 @@ public class InitTaskEntity extends BaseEntity {
this.taskId = taskId; this.taskId = taskId;
} }
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public String getVersion() { public String getVersion() {
return version; return version;
} }
@@ -8,6 +8,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.SocketException; import java.net.SocketException;
import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
@@ -31,19 +33,34 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class ConfigurableInitStepExecutor implements InitStepExecutor { public class ConfigurableInitStepExecutor implements InitStepExecutor {
private static final String INTERNAL_PREFIX = "internal::"; 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 InitExecutorProperties properties;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final String springDatasourceUrl;
public ConfigurableInitStepExecutor(InitExecutorProperties properties, ObjectMapper objectMapper) { 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.properties = properties;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.springDatasourceUrl = normalize(springDatasourceUrl);
} }
@Override @Override
@@ -121,6 +138,15 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
case "start_cmsp_cmtp" -> executeStandardStartCommand(task, step, "start_cmsp_cmtp", properties.getStandardAppStartCommand()); case "start_cmsp_cmtp" -> executeStandardStartCommand(task, step, "start_cmsp_cmtp", properties.getStandardAppStartCommand());
case "start_nginx" -> executeStandardStartCommand(task, step, "start_nginx", properties.getStandardNginxStartCommand()); case "start_nginx" -> executeStandardStartCommand(task, step, "start_nginx", properties.getStandardNginxStartCommand());
case "post_check" -> executeStandardStartCommand(task, step, "post_check", properties.getStandardPostCheckCommand()); 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 "direct_db_init" -> executeDirectDbInit(task, step);
case "tlq_init_cfg" -> executeTlqInitCfg(task, step); case "tlq_init_cfg" -> executeTlqInitCfg(task, step);
case "cfmq_apply" -> executeCfmqApply(task, step); case "cfmq_apply" -> executeCfmqApply(task, step);
@@ -906,7 +932,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
Arrays.asList("/bin/zsh", "-lc", applyCommand), Arrays.asList("/bin/zsh", "-lc", applyCommand),
false, false,
loadDir, loadDir,
buildStandardDbApplyEnvironment(loadDir) buildStandardDbApplyEnvironment(loadDir, context)
); );
if (code != 0) { if (code != 0) {
return InitStepExecutionResult.failure("db_apply failed", code, logFile.toString()); 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<String> 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 { private InitStepExecutionResult executeTlqInitCfg(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task); PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) { 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"); return firstNonBlank(properties.getStandardTlqLicenseTargetPath(), "/home/tlq/tlqconf.zip");
} }
private List<String> buildStandardDefaultVhostQueues(String orgCode) { private List<String> buildStandardCustomerDefaultVhostQueues(String orgCode) {
List<String> queues = new ArrayList<>(); List<String> queues = new ArrayList<>();
for (String ci : List.of("CI01", "CI02")) { for (String ci : List.of("CI01", "CI02")) {
queues.add("LQ_DTTRSP_IB01_" + ci + "_" + orgCode); 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_SYN_IB00_CI00_" + orgCode);
queues.add("LQ_SYNRSP_IB00_CI00_" + orgCode); queues.add("LQ_SYNRSP_IB00_CI00_" + orgCode);
queues.add("LQ_MSGRSP_GUI");
queues.add("LQ_SYNRSP_GUI");
return queues; return queues;
} }
@@ -1710,8 +1879,11 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return exitCode; return exitCode;
} }
private Map<String, String> buildStandardDbApplyEnvironment(Path loadDir) { private Map<String, String> buildStandardDbApplyEnvironment(Path loadDir, PlanContext context) {
Map<String, String> environment = new LinkedHashMap<>(); Map<String, String> environment = new LinkedHashMap<>();
JdbcEndpoint endpoint = resolveStandardDbEndpoint();
environment.put("DB_HOST", endpoint.host());
environment.put("DB_PORT", endpoint.port());
if (!isBlank(properties.getStandardDbUsername())) { if (!isBlank(properties.getStandardDbUsername())) {
environment.put("DB_USER", normalize(properties.getStandardDbUsername())); environment.put("DB_USER", normalize(properties.getStandardDbUsername()));
} }
@@ -1721,12 +1893,39 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
if (loadDir != null) { if (loadDir != null) {
environment.put("LOAD_DIR", loadDir.toAbsolutePath().normalize().toString()); 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; 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<String, String> buildStandardStartEnvironment(String stepName) { private Map<String, String> buildStandardStartEnvironment(String stepName) {
Map<String, String> environment = new LinkedHashMap<>(); Map<String, String> 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_HOME_DIR", normalize(properties.getStandardHomeDir()));
environment.put("STANDARD_RUN_USER", normalize(properties.getStandardRunUser())); environment.put("STANDARD_RUN_USER", normalize(properties.getStandardRunUser()));
} }
@@ -1840,12 +2039,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return new PlanContext( return new PlanContext(
firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType())), firstNonBlank(normalizeUpper(text(root, "productType")), normalizeUpper(task.getProductType())),
normalizeUpper(text(request, "orgCodeType")), normalizeUpper(text(request, "orgCodeType")),
text(request, "orgCode"), firstNonBlank(text(request, "orgCode"), text(root, "orgCode")),
text(request, "orgNameCn"), text(request, "orgNameCn"),
text(request, "orgNameEn"), text(request, "orgNameEn"),
normalizeUpper(text(request, "deployMode")), normalizeUpper(text(request, "deployMode")),
normalizeUpper(text(mq, "mqType")), firstNonBlank(normalizeUpper(text(mq, "mqType")), firstNonBlank(normalizeUpper(text(request, "mqType")), normalizeUpper(text(root, "mqType")))),
text(mq, "channelUsername"), firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername")),
text(mq, "channelPassword"), text(mq, "channelPassword"),
text(nodes, "node01Ip"), text(nodes, "node01Ip"),
text(nodes, "node02Ip"), text(nodes, "node02Ip"),
@@ -1931,6 +2130,93 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return "'" + escaped + "'"; return "'" + escaped + "'";
} }
private void verifyProcessAbsent(Path logFile, String pattern, String label, List<String> 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<String> 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<String> 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<String> 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<String> 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<String, String> configMap, String key, List<String> failures) { private static void requireNotBlank(Map<String, String> configMap, String key, List<String> failures) {
if (isBlank(unquote(configMap.get(key)))) { if (isBlank(unquote(configMap.get(key)))) {
failures.add(key + " is blank"); 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<Path> candidates) { private record ResolvedCpconfig(Path path, List<Path> candidates) {
} }
@@ -9,4 +9,10 @@ import org.apache.ibatis.annotations.Param;
public interface InitTaskMapper extends BaseMapperX<InitTaskEntity> { public interface InitTaskMapper extends BaseMapperX<InitTaskEntity> {
InitTaskEntity selectByTaskId(@Param("taskId") String taskId); InitTaskEntity selectByTaskId(@Param("taskId") String taskId);
InitTaskEntity selectLatest();
InitTaskEntity selectLatestByStatus(@Param("status") String status);
InitTaskEntity selectLatestByTaskTypeAndStatus(@Param("taskType") String taskType, @Param("status") String status);
} }
@@ -7,6 +7,12 @@ public interface InitTaskRepository {
Optional<InitTaskEntity> findByTaskId(String taskId); Optional<InitTaskEntity> findByTaskId(String taskId);
Optional<InitTaskEntity> findLatest();
Optional<InitTaskEntity> findLatestByStatus(String status);
Optional<InitTaskEntity> findLatestByTaskTypeAndStatus(String taskType, String status);
void save(InitTaskEntity entity); void save(InitTaskEntity entity);
void update(InitTaskEntity entity); void update(InitTaskEntity entity);
@@ -20,6 +20,21 @@ public class InitTaskRepositoryImpl implements InitTaskRepository {
return Optional.ofNullable(initTaskMapper.selectByTaskId(taskId)); return Optional.ofNullable(initTaskMapper.selectByTaskId(taskId));
} }
@Override
public Optional<InitTaskEntity> findLatest() {
return Optional.ofNullable(initTaskMapper.selectLatest());
}
@Override
public Optional<InitTaskEntity> findLatestByStatus(String status) {
return Optional.ofNullable(initTaskMapper.selectLatestByStatus(status));
}
@Override
public Optional<InitTaskEntity> findLatestByTaskTypeAndStatus(String taskType, String status) {
return Optional.ofNullable(initTaskMapper.selectLatestByTaskTypeAndStatus(taskType, status));
}
@Override @Override
public void save(InitTaskEntity entity) { public void save(InitTaskEntity entity) {
initTaskMapper.insert(entity); initTaskMapper.insert(entity);
@@ -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.InitCommandProfileService;
import com.cisd.tms.modules.init.config.InitExecutorProperties; import com.cisd.tms.modules.init.config.InitExecutorProperties;
import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; 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.InitPlanTemplateResponse;
import com.cisd.tms.modules.init.dto.InitPreviewRequest; import com.cisd.tms.modules.init.dto.InitPreviewRequest;
import com.cisd.tms.modules.init.dto.InitPreviewResponse; 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.InitTaskExecuteResponse;
import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse; import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse;
import com.cisd.tms.modules.init.dto.InitTaskStepResponse; 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.InitTaskEntity;
import com.cisd.tms.modules.init.entity.InitTaskStepEntity; import com.cisd.tms.modules.init.entity.InitTaskStepEntity;
import com.cisd.tms.modules.init.executor.InitStepExecutionResult; 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.InitTaskRepository;
import com.cisd.tms.modules.init.repository.InitTaskStepRepository; import com.cisd.tms.modules.init.repository.InitTaskStepRepository;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@@ -27,14 +31,7 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.*;
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.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
@@ -47,6 +44,8 @@ import org.springframework.stereotype.Service;
@Service @Service
public class InitService { public class InitService {
private static final String TASK_TYPE_INIT = "INIT";
private static final String TASK_TYPE_RESET = "RESET";
private static final Set<String> PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT"); private static final Set<String> PRESET_PRODUCT_TYPES = Set.of("ENTERPRISE", "INDIRECT", "DIRECT");
private static final Set<String> DEPLOY_STANDARD = Set.of("SINGLE", "DUAL"); private static final Set<String> DEPLOY_STANDARD = Set.of("SINGLE", "DUAL");
private static final Set<String> DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD"); private static final Set<String> DEPLOY_DIRECT = Set.of("SINGLE", "DUAL", "QUAD");
@@ -145,6 +144,18 @@ public class InitService {
return response; 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) { public InitCreateTaskResponse createTask(InitPreviewRequest request) {
if (request == null) { if (request == null) {
throw new IllegalArgumentException("request must not be null"); throw new IllegalArgumentException("request must not be null");
@@ -154,7 +165,7 @@ public class InitService {
validateCommon(request); validateCommon(request);
validateByProductType(productType, request); validateByProductType(productType, request);
String taskId = buildTaskId(); String taskId = buildTaskId(TASK_TYPE_INIT);
String normalizedMqType = normalizeUpper(request.getMq().getMqType()); String normalizedMqType = normalizeUpper(request.getMq().getMqType());
String deployMode = normalizeUpper(request.getDeployMode()); String deployMode = normalizeUpper(request.getDeployMode());
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
@@ -164,6 +175,7 @@ public class InitService {
InitTaskEntity entity = new InitTaskEntity(); InitTaskEntity entity = new InitTaskEntity();
entity.setTaskId(taskId); entity.setTaskId(taskId);
entity.setTaskType(TASK_TYPE_INIT);
entity.setVersion(cisdPresetProperties.getVersion()); entity.setVersion(cisdPresetProperties.getVersion());
entity.setStatus("PENDING"); entity.setStatus("PENDING");
entity.setProductType(productType); entity.setProductType(productType);
@@ -198,17 +210,98 @@ public class InitService {
InitCreateTaskResponse response = new InitCreateTaskResponse(); InitCreateTaskResponse response = new InitCreateTaskResponse();
response.setTaskId(taskId); response.setTaskId(taskId);
response.setStatus(entity.getStatus()); response.setStatus(entity.getStatus());
response.setTaskType(entity.getTaskType());
response.setResolvedProductType(productType); response.setResolvedProductType(productType);
response.setPresetVersion(cisdPresetProperties.getVersion()); response.setPresetVersion(cisdPresetProperties.getVersion());
return response; 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<String> 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<InitTaskStepEntity> 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<InitTaskEntity> 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<InitTaskEntity> latestInit = initTaskRepository.findLatestByTaskTypeAndStatus(TASK_TYPE_INIT, "SUCCESS");
Optional<InitTaskEntity> 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<InitTaskEntity> 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) { public InitTaskDetailResponse getTask(String taskId) {
InitTaskEntity entity = requireTask(taskId); InitTaskEntity entity = requireTask(taskId);
InitTaskDetailResponse response = new InitTaskDetailResponse(); InitTaskDetailResponse response = new InitTaskDetailResponse();
response.setTaskId(entity.getTaskId()); response.setTaskId(entity.getTaskId());
response.setStatus(entity.getStatus()); response.setStatus(entity.getStatus());
response.setTaskType(normalize(entity.getTaskType()).isBlank() ? TASK_TYPE_INIT : entity.getTaskType());
response.setResolvedProductType(entity.getProductType()); response.setResolvedProductType(entity.getProductType());
response.setPresetVersion(entity.getVersion()); response.setPresetVersion(entity.getVersion());
response.setOrgCode(entity.getOrgCode()); response.setOrgCode(entity.getOrgCode());
@@ -270,6 +363,19 @@ public class InitService {
} }
public InitTaskExecuteResponse executeTask(String taskId) { 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); InitTaskEntity task = requireTask(taskId);
List<InitTaskStepEntity> steps = initTaskStepRepository.findByTaskIdOrderByStepNo(taskId); List<InitTaskStepEntity> steps = initTaskStepRepository.findByTaskIdOrderByStepNo(taskId);
if (steps.isEmpty()) { if (steps.isEmpty()) {
@@ -403,6 +509,29 @@ public class InitService {
} }
} }
private String generateResetPlanJson(ResolvedResetContext context, List<String> steps) {
Map<String, Object> 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<String, Object> 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<String> defaultSteps(String productType, String mqType) { private List<String> defaultSteps(String productType, String mqType) {
// 该步骤序列只描述“逻辑编排”,具体脚本命令在 step-command-profiles.json 中定义 // 该步骤序列只描述“逻辑编排”,具体脚本命令在 step-command-profiles.json 中定义
if ("DIRECT".equals(productType)) { if ("DIRECT".equals(productType)) {
@@ -467,10 +596,27 @@ public class InitService {
); );
} }
private String buildTaskId() { private List<String> 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 ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT); 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() { private String resolvePresetProductType() {
@@ -489,6 +635,14 @@ public class InitService {
return productType; 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) { private void validateCommon(InitPreviewRequest request) {
if (request.getNodes() == null) { if (request.getNodes() == null) {
throw new IllegalArgumentException("nodes is required"); throw new IllegalArgumentException("nodes is required");
@@ -526,6 +680,100 @@ public class InitService {
validateStandard(productType, request, deployMode, mqType); validateStandard(productType, request, deployMode, mqType);
} }
private ResolvedResetContext resolveCurrentResetContext() {
String productType = resolveSupportedResetProductType();
Optional<InitTaskEntity> 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<InitTaskEntity> 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<LocalDateTime> 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) { private void validateStandard(String productType, InitPreviewRequest request, String deployMode, String mqType) {
requireAllowed("deployMode", deployMode, DEPLOY_STANDARD); requireAllowed("deployMode", deployMode, DEPLOY_STANDARD);
requireAllowed("mq.mqType", mqType, resolveStandardMqAllowed(productType)); 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) { private static String normalizeUpper(String value) {
return normalize(value).toUpperCase(Locale.ROOT); return normalize(value).toUpperCase(Locale.ROOT);
} }
@@ -636,7 +910,37 @@ public class InitService {
return value == null ? "" : value.trim(); 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) { private static boolean isBlank(String value) {
return value == null || value.isBlank(); 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
) {
}
} }
+18
View File
@@ -84,8 +84,14 @@ tms:
standard-run-user: ${TMS_INIT_EXECUTOR_STANDARD_RUN_USER:cmep4i} standard-run-user: ${TMS_INIT_EXECUTOR_STANDARD_RUN_USER:cmep4i}
# 标准版 CMSP/CMTP 启动命令。 # 标准版 CMSP/CMTP 启动命令。
standard-app-start-command: ${TMS_INIT_EXECUTOR_STANDARD_APP_START_COMMAND:/home/tms/bin/start_standard_apps.sh} 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 启动命令。 # 标准版 nginx 启动命令。
standard-nginx-start-command: ${TMS_INIT_EXECUTOR_STANDARD_NGINX_START_COMMAND:/home/tms/bin/start_standard_nginx.sh} 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} standard-post-check-command: ${TMS_INIT_EXECUTOR_STANDARD_POST_CHECK_COMMAND:/home/tms/bin/check_standard_runtime.sh}
# 直参版 cpconfig 文件名(按 upload-base-dir/<cfgZipFileId>/<fileName> 解析)。 # 直参版 cpconfig 文件名(按 upload-base-dir/<cfgZipFileId>/<fileName> 解析)。
@@ -149,6 +155,18 @@ tms:
version: ${TMS_CISD_PRESET_VERSION:V1.0.0} version: ${TMS_CISD_PRESET_VERSION:V1.0.0}
# 预制信息来源标识(CONFIG/DB 等自定义值)。 # 预制信息来源标识(CONFIG/DB 等自定义值)。
source: ${TMS_CISD_PRESET_SOURCE:CONFIG} 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: security:
# 内部接口鉴权 token(前后端内部调用需一致)。 # 内部接口鉴权 token(前后端内部调用需一致)。
internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token} internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token}
@@ -7,6 +7,7 @@
<resultMap id="InitTaskResultMap" type="com.cisd.tms.modules.init.entity.InitTaskEntity"> <resultMap id="InitTaskResultMap" type="com.cisd.tms.modules.init.entity.InitTaskEntity">
<id property="id" column="id"/> <id property="id" column="id"/>
<result property="taskId" column="task_id"/> <result property="taskId" column="task_id"/>
<result property="taskType" column="task_type"/>
<result property="version" column="version"/> <result property="version" column="version"/>
<result property="status" column="status"/> <result property="status" column="status"/>
<result property="productType" column="product_type"/> <result property="productType" column="product_type"/>
@@ -19,10 +20,34 @@
</resultMap> </resultMap>
<select id="selectByTaskId" parameterType="string" resultMap="InitTaskResultMap"> <select id="selectByTaskId" parameterType="string" resultMap="InitTaskResultMap">
SELECT id, task_id, version, status, product_type, org_code, mq_type, deploy_mode, init_plan_json, create_time, update_time SELECT id, task_id, task_type, version, status, product_type, org_code, mq_type, deploy_mode, init_plan_json, create_time, update_time
FROM tms_init_task FROM tms_init_task
WHERE task_id = #{taskId} WHERE task_id = #{taskId}
LIMIT 1 LIMIT 1
</select> </select>
<select id="selectLatest" resultMap="InitTaskResultMap">
SELECT id, task_id, task_type, version, status, product_type, org_code, mq_type, deploy_mode, init_plan_json, create_time, update_time
FROM tms_init_task
ORDER BY create_time DESC, id DESC
LIMIT 1
</select>
<select id="selectLatestByStatus" parameterType="string" resultMap="InitTaskResultMap">
SELECT id, task_id, task_type, version, status, product_type, org_code, mq_type, deploy_mode, init_plan_json, create_time, update_time
FROM tms_init_task
WHERE status = #{status}
ORDER BY create_time DESC, id DESC
LIMIT 1
</select>
<select id="selectLatestByTaskTypeAndStatus" resultMap="InitTaskResultMap">
SELECT id, task_id, task_type, version, status, product_type, org_code, mq_type, deploy_mode, init_plan_json, create_time, update_time
FROM tms_init_task
WHERE task_type = #{taskType}
AND status = #{status}
ORDER BY create_time DESC, id DESC
LIMIT 1
</select>
</mapper> </mapper>
@@ -28,6 +28,10 @@ class TmsApplicationTests {
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"description\":\"CISD TMS 内部业务接口文档\""))) .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("\"/api/v1/init/template\"")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"查询初始化模板\""))) .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("\"/api/v1/files/upload\"")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"上传初始化文件\""))) .andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"上传初始化文件\"")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"/api/v1/auth/login\""))) .andExpect(content().string(org.hamcrest.Matchers.containsString("\"/api/v1/auth/login\"")))
@@ -647,11 +647,19 @@ class ConfigurableInitStepExecutorTest {
properties.setStandardDbPassword("db_password"); properties.setStandardDbPassword("db_password");
properties.setStandardDbApplyCommand( properties.setStandardDbApplyCommand(
"test \"$DB_USER\" = \"db_user\"" "test \"$DB_USER\" = \"db_user\""
+ " && test \"$DB_HOST\" = \"tidb-host\""
+ " && test \"$DB_PORT\" = \"4000\""
+ " && test \"$DB_PASSWORD\" = \"db_password\"" + " && test \"$DB_PASSWORD\" = \"db_password\""
+ " && test \"$LOAD_DIR\" = '" + loadDir.toAbsolutePath() + "'" + " && test \"$LOAD_DIR\" = '" + loadDir.toAbsolutePath() + "'"
+ " && test \"$ORG_CODE\" = \"AAAABBBBXXX\""
+ " && test \"$ORG_NAME_CN\" = \"测试机构\""
+ " && echo applied > '" + marker + "'" + " && 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"); InitTaskEntity task = buildStandardEnterpriseTask("TASK-STANDARD-DB-APPLY-ENV-1", "RABBITMQ", "DUAL");
InitTaskStepEntity renderConfigStep = new InitTaskStepEntity(); InitTaskStepEntity renderConfigStep = new InitTaskStepEntity();
@@ -820,18 +828,161 @@ class ConfigurableInitStepExecutorTest {
Assertions.assertTrue(Files.exists(marker)); 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 @Test
void shouldExposeStandardInitScriptTemplates() throws IOException { void shouldExposeStandardInitScriptTemplates() throws IOException {
Path repoRoot = Paths.get("").toAbsolutePath().normalize(); Path repoRoot = Paths.get("").toAbsolutePath().normalize();
Path scriptDir = repoRoot.resolve("scripts/standard-init"); Path scriptDir = repoRoot.resolve("scripts/standard-init");
Path dbScript = scriptDir.resolve("apply_standard_db.sh"); Path dbScript = scriptDir.resolve("apply_standard_db.sh");
Path appScript = scriptDir.resolve("start_standard_apps.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 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"); Path checkScript = scriptDir.resolve("check_standard_runtime.sh");
Assertions.assertTrue(Files.exists(dbScript)); Assertions.assertTrue(Files.exists(dbScript));
Assertions.assertTrue(Files.exists(appScript)); Assertions.assertTrue(Files.exists(appScript));
Assertions.assertTrue(Files.exists(stopAppScript));
Assertions.assertTrue(Files.exists(nginxScript)); Assertions.assertTrue(Files.exists(nginxScript));
Assertions.assertTrue(Files.exists(stopNginxScript));
Assertions.assertTrue(Files.exists(stopRabbitScript));
Assertions.assertTrue(Files.exists(checkScript)); Assertions.assertTrue(Files.exists(checkScript));
String dbContent = Files.readString(dbScript); String dbContent = Files.readString(dbScript);
@@ -839,8 +990,14 @@ class ConfigurableInitStepExecutorTest {
Assertions.assertTrue(dbContent.contains("CMEP.sql")); Assertions.assertTrue(dbContent.contains("CMEP.sql"));
Assertions.assertTrue(dbContent.contains("UP-ORG-INFO.sql")); Assertions.assertTrue(dbContent.contains("UP-ORG-INFO.sql"));
Assertions.assertTrue(dbContent.contains("LOAD_DIR")); 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.assertTrue(dbContent.contains("--force"));
Assertions.assertFalse(dbContent.contains("/home/cemp4i/mysql/loadfilepath")); 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); String appContent = Files.readString(appScript);
Assertions.assertTrue(appContent.contains("STANDARD_HOME_DIR")); 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("/home/cemp4i/cmsp/start_cmsp.sh"));
Assertions.assertFalse(appContent.contains("su - cmep4i")); 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); String nginxContent = Files.readString(nginxScript);
Assertions.assertTrue(nginxContent.contains("/usr/local/nginx/sbin/nginx")); 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); String checkContent = Files.readString(checkScript);
Assertions.assertTrue(checkContent.contains("CMEP-CMSP")); Assertions.assertTrue(checkContent.contains("CMEP-CMSP"));
Assertions.assertTrue(checkContent.contains("CMEP-CMTP")); 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-db-apply-command: /home/tms/bin/apply_standard_db.sh"));
Assertions.assertTrue(exampleContent.contains("standard-run-user: cmep4i")); 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-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-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")); Assertions.assertTrue(exampleContent.contains("standard-post-check-command: /home/tms/bin/check_standard_runtime.sh"));
String applicationContent = Files.readString(applicationConfig); 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-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-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}")); 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); String readmeContent = Files.readString(readme);
@@ -1159,6 +1333,15 @@ class ConfigurableInitStepExecutorTest {
return new ObjectMapper().writeValueAsString(root); return new ObjectMapper().writeValueAsString(root);
} }
private static String buildResetPlanJson(String productType, Map<String, Object> request) throws IOException {
Map<String, Object> 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 { private static InitTaskEntity buildStandardEnterpriseTask(String taskId, String mqType, String deployMode) throws IOException {
InitTaskEntity task = new InitTaskEntity(); InitTaskEntity task = new InitTaskEntity();
task.setTaskId(taskId); task.setTaskId(taskId);
@@ -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.InitCreateTaskResponse;
import com.cisd.tms.modules.init.dto.InitPreviewRequest; import com.cisd.tms.modules.init.dto.InitPreviewRequest;
import com.cisd.tms.modules.init.dto.InitPreviewResponse; 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.InitTaskDetailResponse;
import com.cisd.tms.modules.init.dto.InitTaskExecuteResponse; import com.cisd.tms.modules.init.dto.InitTaskExecuteResponse;
import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse; import com.cisd.tms.modules.init.dto.InitTaskStepLogResponse;
import com.cisd.tms.modules.init.dto.InitTaskStepResponse; 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.InitTaskEntity;
import com.cisd.tms.modules.init.entity.InitTaskStepEntity; import com.cisd.tms.modules.init.entity.InitTaskStepEntity;
import com.cisd.tms.modules.init.executor.InitStepExecutionResult; import com.cisd.tms.modules.init.executor.InitStepExecutionResult;
@@ -152,6 +155,113 @@ class InitServiceTest {
Assertions.assertNotNull(response.getSummary()); 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 @Test
void shouldPlanEnterpriseTlqWithoutRabbitmqSteps() { void shouldPlanEnterpriseTlqWithoutRabbitmqSteps() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
@@ -177,6 +287,58 @@ class InitServiceTest {
assertContainsStandardPostDeploySteps(stepCodes); 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<String> 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<String> 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 @Test
void shouldPlanIndirectRabbitmqTlqWithTlqAndRabbitmqSteps() { void shouldPlanIndirectRabbitmqTlqWithTlqAndRabbitmqSteps() {
InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository(); InMemoryInitTaskRepository repository = new InMemoryInitTaskRepository();
@@ -277,6 +439,49 @@ class InitServiceTest {
steps.stream().anyMatch(s -> "SET_RABBITMQ".equals(s.getStepCode()) steps.stream().anyMatch(s -> "SET_RABBITMQ".equals(s.getStepCode())
&& s.getCommandLine().contains("/home/cemp4i/cpackage/toolsh/setfraq.sh")) && 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<InitTaskStepResponse> 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 @Test
@@ -548,6 +753,12 @@ class InitServiceTest {
return request; 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() { private static InitPreviewRequest.DirectTlqConfig directTlqConfig() {
InitPreviewRequest.DirectTlqConfig config = new InitPreviewRequest.DirectTlqConfig(); InitPreviewRequest.DirectTlqConfig config = new InitPreviewRequest.DirectTlqConfig();
config.setLocalNodeSeq("A"); config.setLocalNodeSeq("A");
@@ -587,6 +798,27 @@ class InitServiceTest {
return Optional.ofNullable(data.get(taskId)); return Optional.ofNullable(data.get(taskId));
} }
@Override
public Optional<InitTaskEntity> findLatest() {
return data.values().stream()
.max((left, right) -> compareTaskRecency(left, right));
}
@Override
public Optional<InitTaskEntity> findLatestByStatus(String status) {
return data.values().stream()
.filter(entity -> status.equals(entity.getStatus()))
.max((left, right) -> compareTaskRecency(left, right));
}
@Override
public Optional<InitTaskEntity> 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 @Override
public void save(InitTaskEntity entity) { public void save(InitTaskEntity entity) {
if (entity.getCreateTime() == null) { if (entity.getCreateTime() == null) {
@@ -601,6 +833,18 @@ class InitServiceTest {
entity.setUpdateTime(LocalDateTime.now()); entity.setUpdateTime(LocalDateTime.now());
data.put(entity.getTaskId(), entity); 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 { private static class InMemoryInitTaskStepRepository implements InitTaskStepRepository {