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