资源备份流程整理
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.config;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.support.BackupResourceKind;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredRestoreCommand;
|
||||
import java.util.List;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@@ -17,12 +17,21 @@ public class ResourceBackupProperties {
|
||||
private String restoreDbScriptPath = "/home/tms/bin/resource-restore/restore-db.sh";
|
||||
private String mqReplayScriptPath = "/home/tms/bin/resource-restore/replay-mq.sh";
|
||||
private String tmsDatabaseName = "";
|
||||
private List<String> tmsDbExcludedTables = List.of(
|
||||
"tms_role_account",
|
||||
"tms_role_ukey_binding",
|
||||
"tms_auth_full_account",
|
||||
"tms_auth_session",
|
||||
"tms_auth_challenge",
|
||||
"tms_auth_audit_log",
|
||||
"tms_operation_audit_log"
|
||||
);
|
||||
private String cmepDatabaseName = "CMEP";
|
||||
private String healthCheckUrl = "http://127.0.0.1:8080/actuator/health";
|
||||
private int healthCheckTimeoutSeconds = 5;
|
||||
private int storageEncKeyIndex = 1;
|
||||
private int authenticitySignKeyIndex = 1;
|
||||
private String restoreApplyScriptPath = "/home/tms/bin/resource-restore/apply-resource-backup.sh";
|
||||
private List<ConfiguredRestoreCommand> restoreStopCommands = List.of();
|
||||
private List<ConfiguredRestoreCommand> restoreStartCommands = List.of();
|
||||
private List<String> allowedRestoreRoots = List.of(
|
||||
"/home/tms/config",
|
||||
"/home/cmep4i",
|
||||
@@ -103,6 +112,14 @@ public class ResourceBackupProperties {
|
||||
this.tmsDatabaseName = tmsDatabaseName;
|
||||
}
|
||||
|
||||
public List<String> getTmsDbExcludedTables() {
|
||||
return tmsDbExcludedTables;
|
||||
}
|
||||
|
||||
public void setTmsDbExcludedTables(List<String> tmsDbExcludedTables) {
|
||||
this.tmsDbExcludedTables = tmsDbExcludedTables;
|
||||
}
|
||||
|
||||
public String getCmepDatabaseName() {
|
||||
return cmepDatabaseName;
|
||||
}
|
||||
@@ -127,22 +144,6 @@ public class ResourceBackupProperties {
|
||||
this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds;
|
||||
}
|
||||
|
||||
public int getStorageEncKeyIndex() {
|
||||
return storageEncKeyIndex;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyIndex(int storageEncKeyIndex) {
|
||||
this.storageEncKeyIndex = storageEncKeyIndex;
|
||||
}
|
||||
|
||||
public int getAuthenticitySignKeyIndex() {
|
||||
return authenticitySignKeyIndex;
|
||||
}
|
||||
|
||||
public void setAuthenticitySignKeyIndex(int authenticitySignKeyIndex) {
|
||||
this.authenticitySignKeyIndex = authenticitySignKeyIndex;
|
||||
}
|
||||
|
||||
public String getRestoreApplyScriptPath() {
|
||||
return restoreApplyScriptPath;
|
||||
}
|
||||
@@ -151,6 +152,22 @@ public class ResourceBackupProperties {
|
||||
this.restoreApplyScriptPath = restoreApplyScriptPath;
|
||||
}
|
||||
|
||||
public List<ConfiguredRestoreCommand> getRestoreStopCommands() {
|
||||
return restoreStopCommands;
|
||||
}
|
||||
|
||||
public void setRestoreStopCommands(List<ConfiguredRestoreCommand> restoreStopCommands) {
|
||||
this.restoreStopCommands = restoreStopCommands;
|
||||
}
|
||||
|
||||
public List<ConfiguredRestoreCommand> getRestoreStartCommands() {
|
||||
return restoreStartCommands;
|
||||
}
|
||||
|
||||
public void setRestoreStartCommands(List<ConfiguredRestoreCommand> restoreStartCommands) {
|
||||
this.restoreStartCommands = restoreStartCommands;
|
||||
}
|
||||
|
||||
public List<String> getAllowedRestoreRoots() {
|
||||
return allowedRestoreRoots;
|
||||
}
|
||||
@@ -167,33 +184,4 @@ public class ResourceBackupProperties {
|
||||
this.fileResources = fileResources;
|
||||
}
|
||||
|
||||
public List<ConfiguredFileBackupResource> getDefaultFileResources() {
|
||||
ConfiguredFileBackupResource tmsConfig = fileResource(
|
||||
"TMS_CONFIG",
|
||||
"DIR",
|
||||
"/home/tms/config",
|
||||
"/home/tms/config",
|
||||
true
|
||||
);
|
||||
ConfiguredFileBackupResource cpConfig = fileResource("CP_CONFIG", "FILE", "", "", true);
|
||||
ConfiguredFileBackupResource receiverLicense = fileResource("RECEIVER_LICENSE", "FILE", "", "", true);
|
||||
return List.of(tmsConfig, cpConfig, receiverLicense);
|
||||
}
|
||||
|
||||
private static ConfiguredFileBackupResource fileResource(
|
||||
String code,
|
||||
String mode,
|
||||
String path,
|
||||
String restorePath,
|
||||
boolean required
|
||||
) {
|
||||
ConfiguredFileBackupResource resource = new ConfiguredFileBackupResource();
|
||||
resource.setCode(code);
|
||||
resource.setKind(BackupResourceKind.FILE.name());
|
||||
resource.setMode(mode);
|
||||
resource.setPath(path);
|
||||
resource.setRestorePath(restorePath);
|
||||
resource.setRequired(required);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public class ResourceBackupController {
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
|
||||
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.BACKUP, summary = "创建资源备份任务")
|
||||
@Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。")
|
||||
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody CreateResourceBackupRequest request) {
|
||||
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody(required = false) CreateResourceBackupRequest request) {
|
||||
return ApiResponse.success(resourceBackupService.createBackupTask(request));
|
||||
}
|
||||
|
||||
|
||||
+12
-27
@@ -7,10 +7,8 @@ import com.cisd.tms.modules.auth.security.RequireInternalAuth;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
@@ -22,7 +20,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -34,7 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource-restores")
|
||||
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、详情和步骤日志接口")
|
||||
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、执行和最近结果接口")
|
||||
@RequiredArgsConstructor
|
||||
public class ResourceRestoreController {
|
||||
|
||||
@@ -59,35 +56,23 @@ public class ResourceRestoreController {
|
||||
return ApiResponse.success(resourceRestoreService.createRestoreTask(request));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}")
|
||||
@PostMapping("/{taskId}/apply")
|
||||
@ReplayProtected
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
|
||||
@Operation(summary = "查询资源恢复任务详情", description = "返回资源恢复任务详情和当前控制阶段。")
|
||||
public ApiResponse<ResourceRestoreTaskDetailResponse> taskDetail(
|
||||
@AuditedOperation(module = ModuleCode.BACKUP, action = ActionType.RECOVER, summary = "执行资源恢复任务")
|
||||
@Operation(summary = "执行资源恢复任务", description = "用户确认停机恢复后,启动后台恢复脚本。")
|
||||
public ApiResponse<CreateResourceRestoreResponse> applyRestoreTask(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTask(taskId));
|
||||
return ApiResponse.success(resourceRestoreService.applyRestoreTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps")
|
||||
@GetMapping("/latest")
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
|
||||
@Operation(summary = "查询资源恢复任务步骤", description = "返回资源恢复任务步骤列表。")
|
||||
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskSteps(taskId));
|
||||
@Operation(summary = "查询最近资源恢复结果", description = "TMS 重启后用于恢复页面展示最近一次恢复时间、结果和失败原因。")
|
||||
public ApiResponse<ResourceRestoreLatestResponse> latestRestore() {
|
||||
return ApiResponse.success(resourceTaskQueryService.getLatestRestore());
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps/{stepNo}/log")
|
||||
@RequireInternalAuth(anyRole = {RoleCode.OPS_ADMIN, RoleCode.SUPER_ADMIN}, authLevel = AuthLevel.FULL)
|
||||
@Operation(summary = "查询资源恢复步骤日志", description = "返回资源恢复步骤日志。")
|
||||
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId,
|
||||
@Parameter(description = "步骤序号")
|
||||
@PathVariable int stepNo
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskStepLog(taskId, stepNo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
public final class ResourceCryptoKeyConstants {
|
||||
|
||||
public static final int STORAGE_ENC_KEY_INDEX = 1;
|
||||
public static final int AUTHENTICITY_SIGN_KEY_INDEX = 1;
|
||||
public static final String STORAGE_ENC_KEY_FINGERPRINT = "USER_KEY_ENC:1";
|
||||
public static final String AUTHENTICITY_KEY_FINGERPRINT = "USER_KEY_SIGN:1";
|
||||
|
||||
private ResourceCryptoKeyConstants() {
|
||||
}
|
||||
}
|
||||
+12
-26
@@ -1,12 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.*;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
@@ -24,7 +19,6 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
@@ -32,12 +26,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceEnvelopeCryptoServiceImpl(
|
||||
PcieCryptoService pcieCryptoService,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
public ResourceEnvelopeCryptoServiceImpl(PcieCryptoService pcieCryptoService) {
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,7 +45,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
|
||||
// 用户密钥只负责封装随机 DEK;真正的大 payload 仍走对称加密,避免直接把大文件送进 ECC 接口。
|
||||
EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest();
|
||||
encryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
encryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX);
|
||||
encryptRequest.setData(sm4Key);
|
||||
BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest);
|
||||
|
||||
@@ -70,8 +60,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
ResourceSealedPayload sealedPayload = new ResourceSealedPayload();
|
||||
sealedPayload.setCipherBytes(cipher.doFinal(payloadBytes));
|
||||
sealedPayload.setEnvelope(envelope);
|
||||
sealedPayload.setStorageEncKeyFingerprint(buildFingerprint("USER_KEY_ENC", resourceBackupProperties.getStorageEncKeyIndex()));
|
||||
sealedPayload.setAuthenticityKeyFingerprint(buildFingerprint("USER_KEY_SIGN", resourceBackupProperties.getAuthenticitySignKeyIndex()));
|
||||
sealedPayload.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
|
||||
sealedPayload.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
|
||||
return sealedPayload;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("资源备份载荷加封失败", ex);
|
||||
@@ -85,11 +75,11 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
if (dataToSign == null) {
|
||||
throw new IllegalArgumentException("dataToSign不能为空");
|
||||
}
|
||||
EccInternalSignRequest request = new EccInternalSignRequest();
|
||||
request.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
// 内部签名接口这里统一签 SM3 摘要,避免把整段原文直接交给密码卡并固定后续验签输入语义。
|
||||
request.setData(sm3(dataToSign));
|
||||
return normalize(pcieCryptoService.eccInternalSign(request));
|
||||
UserKeySignRequest request = new UserKeySignRequest();
|
||||
request.setData(dataToSign);
|
||||
request.setKeyIndex(ResourceCryptoKeyConstants.AUTHENTICITY_SIGN_KEY_INDEX);
|
||||
return normalize(pcieCryptoService.userKeySignWithSm3(request));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,7 +91,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
throw new IllegalArgumentException("signature不能为空");
|
||||
}
|
||||
EccInternalVerifyRequest verifyRequest = new EccInternalVerifyRequest();
|
||||
verifyRequest.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
verifyRequest.setKeyIndex(ResourceCryptoKeyConstants.AUTHENTICITY_SIGN_KEY_INDEX);
|
||||
verifyRequest.setData(sm3(dataToVerify));
|
||||
verifyRequest.setSignature(signature);
|
||||
pcieCryptoService.eccInternalVerify(verifyRequest);
|
||||
@@ -127,7 +117,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
int dekLength = parseInt(envelope.get("dekLength"), 16);
|
||||
// 先验签,再解数字信封,再解 payload;顺序固定,避免篡改包先进入明文处理路径。
|
||||
EccInternalDecryptRequest decryptRequest = new EccInternalDecryptRequest();
|
||||
decryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
decryptRequest.setKeyIndex(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_INDEX);
|
||||
decryptRequest.setCipherBlob(wrappedDek);
|
||||
decryptRequest.setOutBufferSize(dekLength);
|
||||
byte[] sm4Key = normalizeAndWipe(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest));
|
||||
@@ -193,10 +183,6 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildFingerprint(String prefix, int keyIndex) {
|
||||
return prefix + ":" + keyIndex;
|
||||
}
|
||||
|
||||
private static void wipe(byte[] bytes) {
|
||||
if (bytes != null) {
|
||||
Arrays.fill(bytes, (byte) 0);
|
||||
|
||||
+76
-19
@@ -1,19 +1,22 @@
|
||||
package com.cisd.tms.modules.backup.discovery;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
|
||||
private final InitExecutorProperties initExecutorProperties;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public ResourceDiscoveryServiceImpl(InitExecutorProperties initExecutorProperties) {
|
||||
this.initExecutorProperties = initExecutorProperties;
|
||||
@Autowired
|
||||
public ResourceDiscoveryServiceImpl(ResourceBackupProperties resourceBackupProperties) {
|
||||
this.resourceBackupProperties = resourceBackupProperties == null ? new ResourceBackupProperties() : resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,16 +33,15 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
// 资源发现只决定“应该备份什么”,不在这里触碰文件系统、数据库或密码卡。
|
||||
// 真实采集放在 ResourcePackageServiceImpl 中完成,便于统一处理必选资源缺失和包内 manifest。
|
||||
List<DiscoveredResourceItem> items = new ArrayList<>();
|
||||
// TMS 自身配置目录是恢复后服务能否按原部署参数启动的基础资源,按目录递归打包。
|
||||
items.add(file("TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), initExecutorProperties.getTmsConfigDir(), "DIR", true));
|
||||
// 收发器配置和 license 按产品形态切换路径:标准版走 cmep4i,直连版走 direct 目标路径。
|
||||
items.add(file("CP_CONFIG", resolveCpConfigPath(productType), resolveCpConfigPath(productType), "FILE", true));
|
||||
items.add(file("RECEIVER_LICENSE", resolveReceiverLicensePath(productType), resolveReceiverLicensePath(productType), "FILE", true));
|
||||
appendConfiguredFileResources(items, productType, mqType);
|
||||
// 数据库资源在包内固定成 db/*.sql,恢复脚本通过 restore-databases.tsv 再决定导入目标库名。
|
||||
items.add(database("TMS_DB", "TMS", "db/TMS.sql", true));
|
||||
if (!"DIRECT".equals(productType)) {
|
||||
// 第一版标准版迁移需要把收发器业务库一起带走,DIRECT 暂时不要求这份库。
|
||||
items.add(database("CMEP_DB", "CMEP", "db/CMEP.sql", true));
|
||||
// 标准收发器初始化还会创建一个以机构号命名的业务库;这份库和 CMEP 一样属于不可重建业务数据。
|
||||
String orgCode = requireDatabaseIdentifier(initTask.getOrgCode(), "orgCode");
|
||||
items.add(database("ORG_DB", orgCode, "db/" + orgCode + ".sql", true));
|
||||
}
|
||||
// 实体证书表记录的 keyIdx 对应密码卡内用户签名密钥;备份时会按这些索引逐个调用 backupUserKey。
|
||||
items.add(userKey(true));
|
||||
@@ -48,18 +50,35 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
return items;
|
||||
}
|
||||
|
||||
private String resolveCpConfigPath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectCpconfigTargetPath();
|
||||
private void appendConfiguredFileResources(List<DiscoveredResourceItem> items, String productType, String mqType) {
|
||||
List<ConfiguredFileBackupResource> resources = resourceBackupProperties.getFileResources();
|
||||
if (resources == null || resources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
return initExecutorProperties.getStandardCpconfigPath();
|
||||
}
|
||||
|
||||
private String resolveReceiverLicensePath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectReceiverLicenseTargetPath();
|
||||
for (ConfiguredFileBackupResource resource : resources) {
|
||||
if (!matches(resource.getProductTypes(), productType) || !matches(resource.getMqTypes(), mqType)) {
|
||||
continue;
|
||||
}
|
||||
String code = trim(resource.getCode());
|
||||
String path = trim(resource.getPath());
|
||||
if (code.isEmpty() || path.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String mode = trim(resource.getMode()).isEmpty() ? "FILE" : trim(resource.getMode()).toUpperCase();
|
||||
String restorePath = trim(resource.getRestorePath());
|
||||
if (restorePath.isEmpty()) {
|
||||
restorePath = "GLOB".equals(mode) ? globRestoreDir(path) : path;
|
||||
}
|
||||
DiscoveredResourceItem item = file(
|
||||
code,
|
||||
path,
|
||||
restorePath,
|
||||
mode,
|
||||
resource.isRequired()
|
||||
);
|
||||
removeExistingCode(items, code);
|
||||
items.add(item);
|
||||
}
|
||||
return initExecutorProperties.getStandardReceiverLicenseTargetPath();
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem file(String code, String path, String restorePath, String mode, boolean required) {
|
||||
@@ -73,6 +92,29 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
return item;
|
||||
}
|
||||
|
||||
private static String globRestoreDir(String pattern) {
|
||||
String value = trim(pattern);
|
||||
int slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
|
||||
if (slash <= 0) {
|
||||
return ".";
|
||||
}
|
||||
return value.substring(0, slash);
|
||||
}
|
||||
|
||||
private static void removeExistingCode(List<DiscoveredResourceItem> items, String code) {
|
||||
items.removeIf(item -> code.equals(item.getResourceCode()));
|
||||
}
|
||||
|
||||
private static boolean matches(List<String> configuredValues, String actualValue) {
|
||||
if (configuredValues == null || configuredValues.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String actual = upper(actualValue);
|
||||
return configuredValues.stream()
|
||||
.map(ResourceDiscoveryServiceImpl::upper)
|
||||
.anyMatch(actual::equals);
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem database(String code, String databaseName, String entryPath, boolean required) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("DATABASE");
|
||||
@@ -104,4 +146,19 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
private static String upper(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private static String requireDatabaseIdentifier(String value, String label) {
|
||||
String identifier = trim(value);
|
||||
if (identifier.isEmpty()) {
|
||||
throw new IllegalArgumentException(label + "不能为空");
|
||||
}
|
||||
if (!identifier.matches("[A-Za-z0-9_]+")) {
|
||||
throw new IllegalArgumentException(label + "数据库名无效:" + identifier);
|
||||
}
|
||||
return identifier;
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-23
@@ -2,34 +2,12 @@ package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "创建资源备份任务请求")
|
||||
@Schema(description = "创建资源备份任务请求")
|
||||
public class CreateResourceBackupRequest {
|
||||
|
||||
@Schema(description = "是否包含升级历史", example = "true")
|
||||
private Boolean includeUpgradeHistory;
|
||||
|
||||
@Schema(description = "是否包含审计备份", example = "false")
|
||||
private Boolean includeAuditBackups;
|
||||
|
||||
@Schema(description = "备注", example = "before hardware replacement")
|
||||
private String remark;
|
||||
|
||||
public Boolean getIncludeUpgradeHistory() {
|
||||
return includeUpgradeHistory;
|
||||
}
|
||||
|
||||
public void setIncludeUpgradeHistory(Boolean includeUpgradeHistory) {
|
||||
this.includeUpgradeHistory = includeUpgradeHistory;
|
||||
}
|
||||
|
||||
public Boolean getIncludeAuditBackups() {
|
||||
return includeAuditBackups;
|
||||
}
|
||||
|
||||
public void setIncludeAuditBackups(Boolean includeAuditBackups) {
|
||||
this.includeAuditBackups = includeAuditBackups;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
+79
-7
@@ -1,12 +1,17 @@
|
||||
package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
@Schema(description = "创建资源恢复任务请求")
|
||||
public class CreateResourceRestoreRequest {
|
||||
|
||||
private static final String IPV4_REGEX =
|
||||
"^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$";
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "预检号", example = "RPRE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String precheckId;
|
||||
@@ -15,9 +20,15 @@ public class CreateResourceRestoreRequest {
|
||||
@Schema(description = "确认的备份号", example = "RBKP-20260408-153000-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String confirmBackupId;
|
||||
|
||||
@NotNull
|
||||
@Schema(description = "是否确认接受恢复风险", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Boolean confirmRiskAccepted;
|
||||
@NotBlank(message = "deployMode不能为空")
|
||||
@Pattern(regexp = "SINGLE|DUAL|QUAD", message = "deployMode必须为SINGLE/DUAL/QUAD")
|
||||
@Schema(description = "目标部署方式", example = "DUAL", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String deployMode;
|
||||
|
||||
@Valid
|
||||
@NotNull(message = "nodes不能为空")
|
||||
@Schema(description = "目标节点 IP 配置", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Nodes nodes;
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
@@ -35,11 +46,72 @@ public class CreateResourceRestoreRequest {
|
||||
this.confirmBackupId = confirmBackupId;
|
||||
}
|
||||
|
||||
public Boolean getConfirmRiskAccepted() {
|
||||
return confirmRiskAccepted;
|
||||
public String getDeployMode() {
|
||||
return deployMode;
|
||||
}
|
||||
|
||||
public void setConfirmRiskAccepted(Boolean confirmRiskAccepted) {
|
||||
this.confirmRiskAccepted = confirmRiskAccepted;
|
||||
public void setDeployMode(String deployMode) {
|
||||
this.deployMode = deployMode;
|
||||
}
|
||||
|
||||
public Nodes getNodes() {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public void setNodes(Nodes nodes) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
@Schema(description = "目标节点 IP 配置")
|
||||
public static class Nodes {
|
||||
|
||||
@NotBlank(message = "nodes.node01Ip不能为空")
|
||||
@Pattern(regexp = IPV4_REGEX, message = "nodes.node01Ip必须是有效IPv4地址")
|
||||
@Schema(description = "节点 1 IP", example = "192.168.1.11", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String node01Ip;
|
||||
|
||||
@Pattern(regexp = IPV4_REGEX, message = "nodes.node02Ip必须是有效IPv4地址")
|
||||
@Schema(description = "节点 2 IP", example = "192.168.1.12")
|
||||
private String node02Ip;
|
||||
|
||||
@Pattern(regexp = IPV4_REGEX, message = "nodes.node03Ip必须是有效IPv4地址")
|
||||
@Schema(description = "节点 3 IP", example = "192.168.1.13")
|
||||
private String node03Ip;
|
||||
|
||||
@Pattern(regexp = IPV4_REGEX, message = "nodes.node04Ip必须是有效IPv4地址")
|
||||
@Schema(description = "节点 4 IP", example = "192.168.1.14")
|
||||
private String node04Ip;
|
||||
|
||||
public String getNode01Ip() {
|
||||
return node01Ip;
|
||||
}
|
||||
|
||||
public void setNode01Ip(String node01Ip) {
|
||||
this.node01Ip = node01Ip;
|
||||
}
|
||||
|
||||
public String getNode02Ip() {
|
||||
return node02Ip;
|
||||
}
|
||||
|
||||
public void setNode02Ip(String node02Ip) {
|
||||
this.node02Ip = node02Ip;
|
||||
}
|
||||
|
||||
public String getNode03Ip() {
|
||||
return node03Ip;
|
||||
}
|
||||
|
||||
public void setNode03Ip(String node03Ip) {
|
||||
this.node03Ip = node03Ip;
|
||||
}
|
||||
|
||||
public String getNode04Ip() {
|
||||
return node04Ip;
|
||||
}
|
||||
|
||||
public void setNode04Ip(String node04Ip) {
|
||||
this.node04Ip = node04Ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "最近一次资源恢复结果响应")
|
||||
public class ResourceRestoreLatestResponse {
|
||||
|
||||
private String taskId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String phase;
|
||||
private String message;
|
||||
private String createdAt;
|
||||
private String finishedAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
public void setPhase(String phase) {
|
||||
this.phase = phase;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
+9
@@ -16,6 +16,7 @@ public class ResourceRestorePrecheckResponse {
|
||||
private Boolean keysetMatched;
|
||||
private Boolean signatureValid;
|
||||
private Boolean compatible;
|
||||
private Boolean precheckPassed;
|
||||
private Boolean payloadDecrypted;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
@@ -103,6 +104,14 @@ public class ResourceRestorePrecheckResponse {
|
||||
this.compatible = compatible;
|
||||
}
|
||||
|
||||
public Boolean getPrecheckPassed() {
|
||||
return precheckPassed;
|
||||
}
|
||||
|
||||
public void setPrecheckPassed(Boolean precheckPassed) {
|
||||
this.precheckPassed = precheckPassed;
|
||||
}
|
||||
|
||||
public Boolean getPayloadDecrypted() {
|
||||
return payloadDecrypted;
|
||||
}
|
||||
|
||||
-261
@@ -1,261 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "资源恢复任务详情响应")
|
||||
public class ResourceRestoreTaskDetailResponse {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String controlPhase;
|
||||
private String stateSource;
|
||||
private String sourceDeviceId;
|
||||
private String sourceProductType;
|
||||
private String targetProductType;
|
||||
private String packageFileId;
|
||||
private String packageName;
|
||||
private Boolean packagePathVisible;
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String lastHeartbeatAt;
|
||||
private String statusFilePath;
|
||||
private Boolean orphaned;
|
||||
private List<String> cleanupWarnings;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
private List<String> restorePlan;
|
||||
private Object consistencyReport;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private String createdBy;
|
||||
private String createdAt;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getControlPhase() {
|
||||
return controlPhase;
|
||||
}
|
||||
|
||||
public void setControlPhase(String controlPhase) {
|
||||
this.controlPhase = controlPhase;
|
||||
}
|
||||
|
||||
public String getStateSource() {
|
||||
return stateSource;
|
||||
}
|
||||
|
||||
public void setStateSource(String stateSource) {
|
||||
this.stateSource = stateSource;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getSourceProductType() {
|
||||
return sourceProductType;
|
||||
}
|
||||
|
||||
public void setSourceProductType(String sourceProductType) {
|
||||
this.sourceProductType = sourceProductType;
|
||||
}
|
||||
|
||||
public String getTargetProductType() {
|
||||
return targetProductType;
|
||||
}
|
||||
|
||||
public void setTargetProductType(String targetProductType) {
|
||||
this.targetProductType = targetProductType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
return packageFileId;
|
||||
}
|
||||
|
||||
public void setPackageFileId(String packageFileId) {
|
||||
this.packageFileId = packageFileId;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public Boolean getPackagePathVisible() {
|
||||
return packagePathVisible;
|
||||
}
|
||||
|
||||
public void setPackagePathVisible(Boolean packagePathVisible) {
|
||||
this.packagePathVisible = packagePathVisible;
|
||||
}
|
||||
|
||||
public String getCurrentStepCode() {
|
||||
return currentStepCode;
|
||||
}
|
||||
|
||||
public void setCurrentStepCode(String currentStepCode) {
|
||||
this.currentStepCode = currentStepCode;
|
||||
}
|
||||
|
||||
public Integer getCurrentStepNo() {
|
||||
return currentStepNo;
|
||||
}
|
||||
|
||||
public void setCurrentStepNo(Integer currentStepNo) {
|
||||
this.currentStepNo = currentStepNo;
|
||||
}
|
||||
|
||||
public String getLastHeartbeatAt() {
|
||||
return lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public void setLastHeartbeatAt(String lastHeartbeatAt) {
|
||||
this.lastHeartbeatAt = lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public String getStatusFilePath() {
|
||||
return statusFilePath;
|
||||
}
|
||||
|
||||
public void setStatusFilePath(String statusFilePath) {
|
||||
this.statusFilePath = statusFilePath;
|
||||
}
|
||||
|
||||
public Boolean getOrphaned() {
|
||||
return orphaned;
|
||||
}
|
||||
|
||||
public void setOrphaned(Boolean orphaned) {
|
||||
this.orphaned = orphaned;
|
||||
}
|
||||
|
||||
public List<String> getCleanupWarnings() {
|
||||
return cleanupWarnings;
|
||||
}
|
||||
|
||||
public void setCleanupWarnings(List<String> cleanupWarnings) {
|
||||
this.cleanupWarnings = cleanupWarnings;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public void setWarnings(List<String> warnings) {
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<String> getBlockingIssues() {
|
||||
return blockingIssues;
|
||||
}
|
||||
|
||||
public void setBlockingIssues(List<String> blockingIssues) {
|
||||
this.blockingIssues = blockingIssues;
|
||||
}
|
||||
|
||||
public List<String> getRestorePlan() {
|
||||
return restorePlan;
|
||||
}
|
||||
|
||||
public void setRestorePlan(List<String> restorePlan) {
|
||||
this.restorePlan = restorePlan;
|
||||
}
|
||||
|
||||
public Object getConsistencyReport() {
|
||||
return consistencyReport;
|
||||
}
|
||||
|
||||
public void setConsistencyReport(Object consistencyReport) {
|
||||
this.consistencyReport = consistencyReport;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(String startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
+20
@@ -1,5 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.dto.support;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConfiguredFileBackupResource {
|
||||
|
||||
private String code;
|
||||
@@ -7,6 +9,8 @@ public class ConfiguredFileBackupResource {
|
||||
private String mode = "FILE";
|
||||
private String path;
|
||||
private String restorePath;
|
||||
private List<String> productTypes = List.of();
|
||||
private List<String> mqTypes = List.of();
|
||||
private boolean required = true;
|
||||
|
||||
public String getCode() {
|
||||
@@ -49,6 +53,22 @@ public class ConfiguredFileBackupResource {
|
||||
this.restorePath = restorePath;
|
||||
}
|
||||
|
||||
public java.util.List<String> getProductTypes() {
|
||||
return productTypes;
|
||||
}
|
||||
|
||||
public void setProductTypes(java.util.List<String> productTypes) {
|
||||
this.productTypes = productTypes;
|
||||
}
|
||||
|
||||
public java.util.List<String> getMqTypes() {
|
||||
return mqTypes;
|
||||
}
|
||||
|
||||
public void setMqTypes(java.util.List<String> mqTypes) {
|
||||
this.mqTypes = mqTypes;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.cisd.tms.modules.backup.dto.support;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConfiguredRestoreCommand {
|
||||
|
||||
private String code;
|
||||
private String command;
|
||||
private boolean required = true;
|
||||
private List<String> productTypes = List.of();
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
public void setCommand(String command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public List<String> getProductTypes() {
|
||||
return productTypes;
|
||||
}
|
||||
|
||||
public void setProductTypes(List<String> productTypes) {
|
||||
this.productTypes = productTypes;
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@TableName("tms_resource_task_step")
|
||||
public class ResourceTaskStepEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String taskType;
|
||||
private Integer stepNo;
|
||||
private String stepCode;
|
||||
private String status;
|
||||
private String message;
|
||||
private String logPath;
|
||||
private Integer exitCode;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime finishTime;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getTaskType() {
|
||||
return taskType;
|
||||
}
|
||||
|
||||
public void setTaskType(String taskType) {
|
||||
this.taskType = taskType;
|
||||
}
|
||||
|
||||
public Integer getStepNo() {
|
||||
return stepNo;
|
||||
}
|
||||
|
||||
public void setStepNo(Integer stepNo) {
|
||||
this.stepNo = stepNo;
|
||||
}
|
||||
|
||||
public String getStepCode() {
|
||||
return stepCode;
|
||||
}
|
||||
|
||||
public void setStepCode(String stepCode) {
|
||||
this.stepCode = stepCode;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getLogPath() {
|
||||
return logPath;
|
||||
}
|
||||
|
||||
public void setLogPath(String logPath) {
|
||||
this.logPath = logPath;
|
||||
}
|
||||
|
||||
public Integer getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public void setExitCode(Integer exitCode) {
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getFinishTime() {
|
||||
return finishTime;
|
||||
}
|
||||
|
||||
public void setFinishTime(LocalDateTime finishTime) {
|
||||
this.finishTime = finishTime;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 恢复任务控制阶段。
|
||||
*/
|
||||
public enum ResourceControlPhase {
|
||||
TMS_PRECHECK,
|
||||
JAVA_EXTRACT,
|
||||
SHELL_APPLY,
|
||||
DONE
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源任务步骤状态。
|
||||
*/
|
||||
public enum ResourceStepStatus {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
SUCCESS_WITH_WARNINGS,
|
||||
FAILED,
|
||||
SKIPPED
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源备份/恢复任务主状态。
|
||||
*/
|
||||
public enum ResourceTaskStatus {
|
||||
PENDING,
|
||||
PRECHECKING,
|
||||
PRECHECK_FAILED,
|
||||
READY,
|
||||
APPLYING,
|
||||
VERIFYING,
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
CANCELLED
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源任务类型。
|
||||
*/
|
||||
public enum ResourceTaskType {
|
||||
RESOURCE_BACKUP,
|
||||
RESOURCE_RESTORE
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ResourceTaskStepMapper extends BaseMapperX<ResourceTaskStepEntity> {
|
||||
|
||||
@Select(
|
||||
"SELECT id, task_id, task_type, step_no, step_code, status, message, log_path, exit_code, "
|
||||
+ "create_time, update_time, start_time, finish_time "
|
||||
+ "FROM tms_resource_task_step WHERE task_id = #{taskId} ORDER BY step_no ASC"
|
||||
)
|
||||
List<ResourceTaskStepEntity> selectByTaskIdOrderByStepNo(@Param("taskId") String taskId);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ public interface DatabaseBackupCollector {
|
||||
|
||||
CollectedDatabaseDump collectCmepDatabaseDump();
|
||||
|
||||
CollectedDatabaseDump collectDatabaseDump(String databaseName, String fileName);
|
||||
|
||||
class CollectedDatabaseDump {
|
||||
|
||||
private String databaseName;
|
||||
|
||||
+33
-2
@@ -34,7 +34,7 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
@Override
|
||||
public CollectedDatabaseDump collectTmsDatabaseDump() {
|
||||
MysqlConnectionTarget target = resolveTarget(resourceBackupProperties.getTmsDatabaseName(), "TMS");
|
||||
return collectDatabaseDump(target, "TMS.sql");
|
||||
return collectDatabaseDump(target, "TMS.sql", resourceBackupProperties.getTmsDbExcludedTables());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -43,7 +43,21 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
return collectDatabaseDump(target, "CMEP.sql");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CollectedDatabaseDump collectDatabaseDump(String databaseName, String fileName) {
|
||||
MysqlConnectionTarget target = resolveTarget(databaseName, databaseName);
|
||||
return collectDatabaseDump(target, fileName);
|
||||
}
|
||||
|
||||
private CollectedDatabaseDump collectDatabaseDump(MysqlConnectionTarget target, String fileName) {
|
||||
return collectDatabaseDump(target, fileName, List.of());
|
||||
}
|
||||
|
||||
private CollectedDatabaseDump collectDatabaseDump(
|
||||
MysqlConnectionTarget target,
|
||||
String fileName,
|
||||
List<String> excludedTables
|
||||
) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(resourceBackupProperties.getMysqldumpPath());
|
||||
command.add("--host=" + target.host());
|
||||
@@ -52,8 +66,14 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
if (!target.password().isEmpty()) {
|
||||
command.add("--password=" + target.password());
|
||||
}
|
||||
command.add("--single-transaction");
|
||||
// command.add("--single-transaction");
|
||||
command.add("--skip-lock-tables");
|
||||
for (String table : excludedTables == null ? List.<String>of() : excludedTables) {
|
||||
String ignoreTable = normalizeIgnoreTable(target.databaseName(), table);
|
||||
if (!ignoreTable.isEmpty()) {
|
||||
command.add("--ignore-table=" + ignoreTable);
|
||||
}
|
||||
}
|
||||
command.add("--databases");
|
||||
command.add(target.databaseName());
|
||||
|
||||
@@ -126,6 +146,17 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String normalizeIgnoreTable(String databaseName, String table) {
|
||||
String value = trim(table);
|
||||
if (value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (value.contains(".")) {
|
||||
return value;
|
||||
}
|
||||
return trim(databaseName) + "." + value;
|
||||
}
|
||||
|
||||
private record MysqlConnectionTarget(String host, int port, String username, String password, String databaseName) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -271,8 +273,6 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
payloadManifest.put("mqType", initTask.getMqType());
|
||||
payloadManifest.put("orgCode", initTask.getOrgCode());
|
||||
payloadManifest.put("remark", request == null ? null : request.getRemark());
|
||||
payloadManifest.put("includeUpgradeHistory", request != null && Boolean.TRUE.equals(request.getIncludeUpgradeHistory()));
|
||||
payloadManifest.put("includeAuditBackups", request != null && Boolean.TRUE.equals(request.getIncludeAuditBackups()));
|
||||
|
||||
List<Map<String, Object>> resourceEntries = new ArrayList<>();
|
||||
int collectedCount = 0;
|
||||
@@ -332,6 +332,9 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
resourceEntry.put("mode", trim(item.getMode()).isEmpty() ? "FILE" : trim(item.getMode()));
|
||||
resourceEntry.put("sourcePath", item.getResolvedPath());
|
||||
resourceEntry.put("restorePath", trim(item.getRestorePath()).isEmpty() ? item.getResolvedPath() : item.getRestorePath());
|
||||
if ("GLOB".equalsIgnoreCase(trim(item.getMode()))) {
|
||||
return collectGlobFileItem(zipOutputStream, item, resourceEntry);
|
||||
}
|
||||
// 文件类资源在备份端只允许采集真实存在的文件/目录;恢复端还会再次校验 restorePath 白名单。
|
||||
Path resolvedPath = toResolvedPath(item.getResolvedPath());
|
||||
if (resolvedPath == null || !Files.exists(resolvedPath)) {
|
||||
@@ -376,6 +379,51 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
return archivedFiles.size();
|
||||
}
|
||||
|
||||
private int collectGlobFileItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
Path globParent = globParent(item.getResolvedPath());
|
||||
if (globParent == null || !Files.isDirectory(globParent)) {
|
||||
assertOptionalResource(item, "PATH_NOT_FOUND");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "PATH_NOT_FOUND");
|
||||
return 0;
|
||||
}
|
||||
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + trim(item.getResolvedPath()));
|
||||
List<Path> matchedFiles;
|
||||
try (Stream<Path> pathStream = Files.list(globParent)) {
|
||||
matchedFiles = pathStream
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> matcher.matches(path.normalize()))
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.toList();
|
||||
}
|
||||
if (matchedFiles.isEmpty()) {
|
||||
assertOptionalResource(item, "GLOB_NOT_MATCHED");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "GLOB_NOT_MATCHED");
|
||||
return 0;
|
||||
}
|
||||
List<Map<String, Object>> archivedFiles = new ArrayList<>();
|
||||
for (Path matchedFile : matchedFiles) {
|
||||
Map<String, Object> fileEntry = writeRegularFileEntry(
|
||||
zipOutputStream,
|
||||
item,
|
||||
matchedFile,
|
||||
matchedFile.getFileName().toString()
|
||||
);
|
||||
String restoreDir = trim(item.getRestorePath()).isEmpty() ? globParent.toString() : trim(item.getRestorePath());
|
||||
fileEntry.put("restorePath", Path.of(restoreDir).resolve(matchedFile.getFileName()).normalize().toString());
|
||||
archivedFiles.add(fileEntry);
|
||||
}
|
||||
resourceEntry.put("collected", Boolean.TRUE);
|
||||
resourceEntry.put("fileCount", archivedFiles.size());
|
||||
resourceEntry.put("files", archivedFiles);
|
||||
return archivedFiles.size();
|
||||
}
|
||||
|
||||
private int collectDatabaseItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
@@ -466,6 +514,16 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
if ("CMEP_DB".equalsIgnoreCase(resourceCode) || "CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
return databaseBackupCollector.collectCmepDatabaseDump();
|
||||
}
|
||||
if ("ORG_DB".equalsIgnoreCase(resourceCode) || "ORG_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
String databaseName = trim(item.getDatabaseName()).isEmpty() ? trim(item.getResolvedPath()) : trim(item.getDatabaseName());
|
||||
if (databaseName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String fileName = trim(item.getEntryPath()).isEmpty()
|
||||
? databaseName + ".sql"
|
||||
: Path.of(trim(item.getEntryPath())).getFileName().toString();
|
||||
return databaseBackupCollector.collectDatabaseDump(databaseName, fileName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -531,6 +589,18 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
return Path.of(value).normalize();
|
||||
}
|
||||
|
||||
private static Path globParent(String pattern) {
|
||||
String value = trim(pattern);
|
||||
if (value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int slash = Math.max(value.lastIndexOf('/'), value.lastIndexOf('\\'));
|
||||
if (slash < 0) {
|
||||
return Path.of(".").normalize();
|
||||
}
|
||||
return Path.of(value.substring(0, slash)).normalize();
|
||||
}
|
||||
|
||||
private static <T> T writeUnchecked(CheckedSupplier<T> supplier) {
|
||||
try {
|
||||
return supplier.get();
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourceTaskStepRepository {
|
||||
|
||||
void save(ResourceTaskStepEntity entity);
|
||||
|
||||
List<ResourceTaskStepEntity> findByTaskIdOrderByStepNo(String taskId);
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.repository.impl;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceTaskStepMapper;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceTaskStepRepository;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ResourceTaskStepRepositoryImpl implements ResourceTaskStepRepository {
|
||||
|
||||
private final ResourceTaskStepMapper resourceTaskStepMapper;
|
||||
|
||||
public ResourceTaskStepRepositoryImpl(ResourceTaskStepMapper resourceTaskStepMapper) {
|
||||
this.resourceTaskStepMapper = resourceTaskStepMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourceTaskStepEntity entity) {
|
||||
resourceTaskStepMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepEntity> findByTaskIdOrderByStepNo(String taskId) {
|
||||
List<ResourceTaskStepEntity> steps = resourceTaskStepMapper.selectByTaskIdOrderByStepNo(taskId);
|
||||
return steps == null ? List.of() : steps;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ActiveRestoreStateStore {
|
||||
|
||||
private static final String ACTIVE_FILE_NAME = "active-restore.json";
|
||||
|
||||
private final ResourceBackupProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ActiveRestoreStateStore(ResourceBackupProperties properties, ObjectMapper objectMapper) {
|
||||
this.properties = properties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void write(ResourceRestoreTaskEntity entity, String status) {
|
||||
ActiveRestoreSnapshot snapshot = fromEntity(entity, status);
|
||||
Path path = activeFilePath();
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), snapshot);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("写入当前资源恢复状态失败:" + path, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<ActiveRestoreSnapshot> read() {
|
||||
Path path = activeFilePath();
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(objectMapper.readValue(path.toFile(), ActiveRestoreSnapshot.class));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("读取当前资源恢复状态失败:" + path, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path activeFilePath() {
|
||||
return Path.of(properties.getRestoreTaskRootDir()).resolve(ACTIVE_FILE_NAME).normalize();
|
||||
}
|
||||
|
||||
private static ActiveRestoreSnapshot fromEntity(ResourceRestoreTaskEntity entity, String status) {
|
||||
return new ActiveRestoreSnapshot(
|
||||
entity.getTaskId(),
|
||||
entity.getPrecheckId(),
|
||||
entity.getBackupId(),
|
||||
status,
|
||||
entity.getSourceDeviceId(),
|
||||
entity.getSourceProductType(),
|
||||
entity.getTargetProductType(),
|
||||
entity.getPackageFileId(),
|
||||
entity.getPackagePath(),
|
||||
entity.getStagingDir(),
|
||||
entity.getRunnerStatePath(),
|
||||
entity.getStorageEncKeyFingerprint(),
|
||||
entity.getAuthenticityKeyFingerprint(),
|
||||
entity.getRestorePlanJson(),
|
||||
entity.getCreateBy(),
|
||||
text(entity.getCreateTime()),
|
||||
text(entity.getStartTime()),
|
||||
text(entity.getFinishTime())
|
||||
);
|
||||
}
|
||||
|
||||
private static String text(LocalDateTime value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
public static class ActiveRestoreSnapshot {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String sourceDeviceId;
|
||||
private String sourceProductType;
|
||||
private String targetProductType;
|
||||
private String packageFileId;
|
||||
private String packagePath;
|
||||
private String stagingDir;
|
||||
private String runnerStatePath;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private String restorePlanJson;
|
||||
private String createBy;
|
||||
private String createdAt;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
|
||||
public ActiveRestoreSnapshot() {
|
||||
}
|
||||
|
||||
public ActiveRestoreSnapshot(
|
||||
String taskId,
|
||||
String precheckId,
|
||||
String backupId,
|
||||
String status,
|
||||
String sourceDeviceId,
|
||||
String sourceProductType,
|
||||
String targetProductType,
|
||||
String packageFileId,
|
||||
String packagePath,
|
||||
String stagingDir,
|
||||
String runnerStatePath,
|
||||
String storageEncKeyFingerprint,
|
||||
String authenticityKeyFingerprint,
|
||||
String restorePlanJson,
|
||||
String createBy,
|
||||
String createdAt,
|
||||
String startedAt,
|
||||
String finishedAt
|
||||
) {
|
||||
this.taskId = taskId;
|
||||
this.precheckId = precheckId;
|
||||
this.backupId = backupId;
|
||||
this.status = status;
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
this.sourceProductType = sourceProductType;
|
||||
this.targetProductType = targetProductType;
|
||||
this.packageFileId = packageFileId;
|
||||
this.packagePath = packagePath;
|
||||
this.stagingDir = stagingDir;
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
this.restorePlanJson = restorePlanJson;
|
||||
this.createBy = createBy;
|
||||
this.createdAt = createdAt;
|
||||
this.startedAt = startedAt;
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getSourceProductType() {
|
||||
return sourceProductType;
|
||||
}
|
||||
|
||||
public void setSourceProductType(String sourceProductType) {
|
||||
this.sourceProductType = sourceProductType;
|
||||
}
|
||||
|
||||
public String getTargetProductType() {
|
||||
return targetProductType;
|
||||
}
|
||||
|
||||
public void setTargetProductType(String targetProductType) {
|
||||
this.targetProductType = targetProductType;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
return packageFileId;
|
||||
}
|
||||
|
||||
public void setPackageFileId(String packageFileId) {
|
||||
this.packageFileId = packageFileId;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public String getStagingDir() {
|
||||
return stagingDir;
|
||||
}
|
||||
|
||||
public void setStagingDir(String stagingDir) {
|
||||
this.stagingDir = stagingDir;
|
||||
}
|
||||
|
||||
public String getRunnerStatePath() {
|
||||
return runnerStatePath;
|
||||
}
|
||||
|
||||
public void setRunnerStatePath(String runnerStatePath) {
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
}
|
||||
|
||||
public String getStorageEncKeyFingerprint() {
|
||||
return storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyFingerprint(String storageEncKeyFingerprint) {
|
||||
this.storageEncKeyFingerprint = storageEncKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getAuthenticityKeyFingerprint() {
|
||||
return authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyFingerprint(String authenticityKeyFingerprint) {
|
||||
this.authenticityKeyFingerprint = authenticityKeyFingerprint;
|
||||
}
|
||||
|
||||
public String getRestorePlanJson() {
|
||||
return restorePlanJson;
|
||||
}
|
||||
|
||||
public void setRestorePlanJson(String restorePlanJson) {
|
||||
this.restorePlanJson = restorePlanJson;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public String getStartedAt() {
|
||||
return startedAt;
|
||||
}
|
||||
|
||||
public void setStartedAt(String startedAt) {
|
||||
this.startedAt = startedAt;
|
||||
}
|
||||
|
||||
public String getFinishedAt() {
|
||||
return finishedAt;
|
||||
}
|
||||
|
||||
public void setFinishedAt(String finishedAt) {
|
||||
this.finishedAt = finishedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -1,6 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredRestoreCommand;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
@@ -35,10 +36,24 @@ public class LightweightRestoreApplierLauncher {
|
||||
}
|
||||
|
||||
public void launch(Path workDir) {
|
||||
launch(workDir, "");
|
||||
}
|
||||
|
||||
public void launch(Path workDir, String targetProductType) {
|
||||
Path normalizedWorkDir = workDir.toAbsolutePath().normalize();
|
||||
// shell 脚本需要数据库连接、恢复脚本路径、健康检查地址等运行时参数。
|
||||
// 这些信息来自当前 TMS 配置,写入任务目录下的 restore-env.sh,避免在命令行暴露复杂参数。
|
||||
writeRestoreEnv(normalizedWorkDir);
|
||||
writeCommandPlan(
|
||||
normalizedWorkDir.resolve("restore-stop-commands.tsv"),
|
||||
properties.getRestoreStopCommands(),
|
||||
targetProductType
|
||||
);
|
||||
writeCommandPlan(
|
||||
normalizedWorkDir.resolve("restore-start-commands.tsv"),
|
||||
properties.getRestoreStartCommands(),
|
||||
targetProductType
|
||||
);
|
||||
Path logFile = normalizedWorkDir.resolve("restore-launch.log");
|
||||
// Java 阶段到这里已经完成包验签、解密、payload 展开和用户密钥恢复。
|
||||
// 后续进入停机恢复脚本,当前 TMS 服务可能被脚本停止。
|
||||
@@ -54,6 +69,35 @@ public class LightweightRestoreApplierLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCommandPlan(Path planFile, List<ConfiguredRestoreCommand> commands, String targetProductType) {
|
||||
try {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (ConfiguredRestoreCommand command : commands == null ? List.<ConfiguredRestoreCommand>of() : commands) {
|
||||
String shellCommand = trim(command.getCommand());
|
||||
if (shellCommand.isEmpty() || !matchesProductType(command.getProductTypes(), targetProductType)) {
|
||||
continue;
|
||||
}
|
||||
builder.append(defaultIfBlank(command.getCode(), "COMMAND"))
|
||||
.append('\t')
|
||||
.append(command.isRequired())
|
||||
.append('\t')
|
||||
.append(shellCommand.replace('\t', ' '))
|
||||
.append(System.lineSeparator());
|
||||
}
|
||||
Files.writeString(planFile, builder.toString());
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("写入恢复停启命令计划失败:" + planFile, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matchesProductType(List<String> productTypes, String targetProductType) {
|
||||
if (productTypes == null || productTypes.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String target = trim(targetProductType);
|
||||
return productTypes.stream().anyMatch(item -> trim(item).equalsIgnoreCase(target));
|
||||
}
|
||||
|
||||
private void writeRestoreEnv(Path workDir) {
|
||||
try {
|
||||
Files.createDirectories(workDir);
|
||||
@@ -71,6 +115,7 @@ public class LightweightRestoreApplierLauncher {
|
||||
values.put("DB_PASSWORD", trim(datasourcePassword));
|
||||
values.put("HEALTH_CHECK_URL", trim(properties.getHealthCheckUrl()));
|
||||
values.put("HEALTH_CHECK_TIMEOUT_SECONDS", String.valueOf(properties.getHealthCheckTimeoutSeconds()));
|
||||
values.put("RESTORE_APPLY_DELAY_SECONDS", "3");
|
||||
|
||||
StringBuilder builder = new StringBuilder("# generated by TMS resource restore\n");
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
@@ -105,6 +150,10 @@ public class LightweightRestoreApplierLauncher {
|
||||
return "'" + trim(value).replace("'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
private static String defaultIfBlank(String candidate, String fallback) {
|
||||
return trim(candidate).isEmpty() ? fallback : candidate;
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
+2
-5
@@ -24,13 +24,10 @@ public class ProcessBuilderRestoreCommandInvoker implements RestoreCommandInvoke
|
||||
if (logFile != null) {
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
|
||||
}
|
||||
Process process = pb.start();
|
||||
return process.waitFor();
|
||||
pb.start();
|
||||
return 0;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("启动恢复命令失败:" + String.join(" ", command), ex);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("restore command 被中断: " + String.join(" ", command), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@ import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
public interface ResourceRestoreService {
|
||||
|
||||
CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request);
|
||||
|
||||
CreateResourceRestoreResponse applyRestoreTask(String taskId);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import java.util.List;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
|
||||
|
||||
public interface ResourceTaskQueryService {
|
||||
|
||||
ResourceRestoreTaskDetailResponse getRestoreTask(String taskId);
|
||||
|
||||
List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId);
|
||||
|
||||
ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo);
|
||||
ResourceRestoreLatestResponse getLatestRestore();
|
||||
}
|
||||
|
||||
+6
-4
@@ -10,6 +10,7 @@ import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceCryptoKeyConstants;
|
||||
import com.cisd.tms.modules.backup.packagex.RequiredBackupResourceMissingException;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
@@ -39,7 +40,6 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceTaskAdmissionGuard resourceTaskAdmissionGuard;
|
||||
|
||||
@Override
|
||||
@@ -50,9 +50,9 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
response.setIkReady(false);
|
||||
response.setStorageEncKeyReady(false);
|
||||
response.setAuthenticityKeyReady(false);
|
||||
response.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
|
||||
response.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
response.setSource("CONFIGURED_KEY_INDEX");
|
||||
response.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
|
||||
response.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
|
||||
response.setSource("FIXED_KEY_INDEX");
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
entity.setSourceDeviceId(resolveSourceDeviceId());
|
||||
entity.setRemark(request == null ? null : request.getRemark());
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
|
||||
entity.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
entity.setStartTime(now);
|
||||
|
||||
+11
-4
@@ -5,7 +5,7 @@ import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceCryptoKeyConstants;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
@@ -30,7 +30,6 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final CisdPresetProperties cisdPresetProperties;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
@Override
|
||||
public ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request) {
|
||||
@@ -61,6 +60,7 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
response.setPayloadDecrypted(false);
|
||||
response.setWarnings(List.of());
|
||||
response.setBlockingIssues(List.of());
|
||||
response.setPrecheckPassed(isPrecheckPassed(response));
|
||||
// 第一版恢复计划固定为两段:Java 负责验签/解密/展开/用户密钥恢复,shell 负责停服后的落盘恢复。
|
||||
response.setRestorePlan(List.of("JAVA_EXTRACT", "SHELL_APPLY"));
|
||||
response.setRestoreScopeSummary(buildScopeSummary(inspectResult));
|
||||
@@ -84,11 +84,11 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
}
|
||||
|
||||
private String expectedStorageFingerprint() {
|
||||
return "USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex();
|
||||
return ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT;
|
||||
}
|
||||
|
||||
private String expectedAuthenticityFingerprint() {
|
||||
return "USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex();
|
||||
return ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT;
|
||||
}
|
||||
|
||||
private static java.util.Map<String, Object> buildScopeSummary(ResourcePackageInspectResult inspectResult) {
|
||||
@@ -106,4 +106,11 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
}
|
||||
return targetProductType.equalsIgnoreCase(sourceProductType);
|
||||
}
|
||||
|
||||
private static boolean isPrecheckPassed(ResourceRestorePrecheckResponse response) {
|
||||
return Boolean.TRUE.equals(response.getKeysetMatched())
|
||||
&& Boolean.TRUE.equals(response.getSignatureValid())
|
||||
&& Boolean.TRUE.equals(response.getCompatible())
|
||||
&& (response.getBlockingIssues() == null || response.getBlockingIssues().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
+109
-11
@@ -10,19 +10,25 @@ import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractor;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractionResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourceUserKeyRestoreService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.ActiveRestoreStateStore;
|
||||
import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceCryptoKeyConstants;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -40,6 +46,8 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
private final ResourceUserKeyRestoreService resourceUserKeyRestoreService;
|
||||
private final LightweightRestoreApplierLauncher lightweightRestoreApplierLauncher;
|
||||
private final ResourceTaskAdmissionGuard resourceTaskAdmissionGuard;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ActiveRestoreStateStore activeRestoreStateStore;
|
||||
|
||||
|
||||
@Override
|
||||
@@ -47,9 +55,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
// 恢复会导入用户密钥、覆盖配置文件、导入数据库并停启服务,
|
||||
// 必须在入口处和其它高风险任务互斥。
|
||||
resourceTaskAdmissionGuard.assertCanStartRestore();
|
||||
if (!Boolean.TRUE.equals(request.getConfirmRiskAccepted())) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "必须确认恢复风险");
|
||||
}
|
||||
validateTargetNodes(request);
|
||||
// 恢复创建必须消费预检结果,确保“用户确认的 backupId”与预检看到的是同一个包。
|
||||
ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检不存在"));
|
||||
@@ -71,6 +77,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
createDirectories(taskDir);
|
||||
// Java 阶段完成包的完整验签、解密和 payload 展开,并生成 shell 可消费的 TSV 计划文件。
|
||||
ResourcePayloadExtractionResult extractionResult = resourcePayloadExtractor.extract(Path.of(packagePath), taskDir);
|
||||
writeTargetNodeContext(taskDir, request);
|
||||
// 用户密钥恢复必须放在 shell 停服/导库之前:数据库和证书数据恢复后会引用这些 keyIdx。
|
||||
// 如果这里失败,不保存 APPLYING 任务、不启动 shell,避免出现“数据已覆盖但密钥不可用”的半恢复状态。
|
||||
resourceUserKeyRestoreService.recover(extractionResult.getPayloadDir());
|
||||
@@ -79,8 +86,8 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
entity.setTaskId(taskId);
|
||||
entity.setPrecheckId(precheck.getPrecheckId());
|
||||
entity.setBackupId(precheck.getBackupId());
|
||||
// 轻量恢复模型下,Java 已完成包验签/解密/展开,后续由 shell apply 脚本接管实际覆盖/导入。
|
||||
entity.setStatus("APPLYING");
|
||||
// 创建接口只准备恢复并返回任务号,避免在 HTTP 响应返回前停掉当前 TMS。
|
||||
entity.setStatus("READY_TO_APPLY");
|
||||
entity.setSourceDeviceId("UNKNOWN");
|
||||
entity.setSourceProductType(precheck.getSourceProductType());
|
||||
entity.setTargetProductType(precheck.getTargetProductType());
|
||||
@@ -88,17 +95,17 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
entity.setPackagePath(packagePath);
|
||||
entity.setStagingDir(taskDir.resolve("payload").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("status.json").toString());
|
||||
entity.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
|
||||
entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
entity.setStorageEncKeyFingerprint(ResourceCryptoKeyConstants.STORAGE_ENC_KEY_FINGERPRINT);
|
||||
entity.setAuthenticityKeyFingerprint(ResourceCryptoKeyConstants.AUTHENTICITY_KEY_FINGERPRINT);
|
||||
entity.setRestorePlanJson("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]");
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
// 只有 Java 解包和用户密钥恢复都成功后,任务才进入 APPLYING。
|
||||
// 后续状态由 shell 写 status.json,查询接口会读取该状态文件展示实时恢复进度。
|
||||
// 只有 Java 解包和用户密钥恢复都成功后,任务才进入 READY_TO_APPLY。
|
||||
// 前端一次确认后可连续调用 create 和 apply;shell 只在 apply 阶段启动,
|
||||
// 避免创建接口返回前停掉当前 TMS。
|
||||
resourceRestoreTaskRepository.save(entity);
|
||||
// shell apply 是停机恢复的执行点:停服务、覆盖文件、导 SQL、MQ replay、起服务、健康检查。
|
||||
lightweightRestoreApplierLauncher.launch(taskDir);
|
||||
activeRestoreStateStore.write(entity, entity.getStatus());
|
||||
|
||||
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
|
||||
response.setTaskId(taskId);
|
||||
@@ -107,6 +114,36 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateResourceRestoreResponse applyRestoreTask(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
|
||||
String status = trim(entity.getStatus());
|
||||
if ("SUCCESS".equalsIgnoreCase(status) || "FAILED".equalsIgnoreCase(status)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务已结束:" + trim(taskId));
|
||||
}
|
||||
if (!"READY_TO_APPLY".equalsIgnoreCase(status) && !"APPLYING".equalsIgnoreCase(status)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务状态不允许执行:" + status);
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setStatus("APPLYING");
|
||||
if (entity.getStartTime() == null) {
|
||||
entity.setStartTime(now);
|
||||
}
|
||||
entity.setUpdateTime(now);
|
||||
resourceRestoreTaskRepository.update(entity);
|
||||
activeRestoreStateStore.write(entity, entity.getStatus());
|
||||
Path taskDir = resolveTaskDir(entity);
|
||||
// shell apply 是停机恢复的执行点:停服务、覆盖文件、导 SQL、MQ replay、起服务、健康检查。
|
||||
lightweightRestoreApplierLauncher.launch(taskDir, entity.getTargetProductType());
|
||||
|
||||
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
private static void createDirectories(Path taskDir) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
@@ -115,10 +152,71 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTargetNodeContext(Path taskDir, CreateResourceRestoreRequest request) {
|
||||
Path contextPath = taskDir.resolve("restore-context.json").normalize();
|
||||
try {
|
||||
Map<String, Object> context = Files.isRegularFile(contextPath)
|
||||
? objectMapper.readValue(contextPath.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
})
|
||||
: new LinkedHashMap<>();
|
||||
context.put("targetDeployMode", trim(request.getDeployMode()).toUpperCase());
|
||||
context.put("targetNodes", targetNodes(request.getNodes()));
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(contextPath.toFile(), context);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("写入恢复目标节点上下文失败:" + contextPath, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> targetNodes(CreateResourceRestoreRequest.Nodes nodes) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
putIfNotBlank(values, "node01Ip", nodes.getNode01Ip());
|
||||
putIfNotBlank(values, "node02Ip", nodes.getNode02Ip());
|
||||
putIfNotBlank(values, "node03Ip", nodes.getNode03Ip());
|
||||
putIfNotBlank(values, "node04Ip", nodes.getNode04Ip());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static void putIfNotBlank(Map<String, Object> values, String key, String value) {
|
||||
String text = trim(value);
|
||||
if (!text.isEmpty()) {
|
||||
values.put(key, text);
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
|
||||
String runnerStatePath = trim(entity.getRunnerStatePath());
|
||||
if (runnerStatePath.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务状态文件路径为空");
|
||||
}
|
||||
return Path.of(runnerStatePath).normalize().getParent();
|
||||
}
|
||||
|
||||
private static void validateTargetNodes(CreateResourceRestoreRequest request) {
|
||||
String deployMode = trim(request.getDeployMode()).toUpperCase();
|
||||
CreateResourceRestoreRequest.Nodes nodes = request.getNodes();
|
||||
if (nodes == null) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes不能为空");
|
||||
}
|
||||
if (trim(nodes.getNode01Ip()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node01Ip不能为空");
|
||||
}
|
||||
if (("DUAL".equals(deployMode) || "QUAD".equals(deployMode)) && trim(nodes.getNode02Ip()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node02Ip不能为空");
|
||||
}
|
||||
if ("QUAD".equals(deployMode)) {
|
||||
if (trim(nodes.getNode03Ip()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node03Ip不能为空");
|
||||
}
|
||||
if (trim(nodes.getNode04Ip()).isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "nodes.node04Ip不能为空");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validatePrecheck(ResourceRestorePrecheckResponse precheck) {
|
||||
if (precheck == null) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检不存在");
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import org.springframework.stereotype.Component;
|
||||
public class ResourceTaskAdmissionGuard {
|
||||
|
||||
private static final Set<String> ACTIVE_BACKUP_STATUSES = Set.of("RUNNING");
|
||||
private static final Set<String> ACTIVE_RESTORE_STATUSES = Set.of("APPLYING", "RUNNING", "HANDOFF", "PENDING");
|
||||
private static final Set<String> ACTIVE_RESTORE_STATUSES = Set.of("READY_TO_APPLY", "APPLYING", "RUNNING", "HANDOFF", "PENDING");
|
||||
|
||||
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
|
||||
+118
-137
@@ -1,20 +1,17 @@
|
||||
package com.cisd.tms.modules.backup.service.impl;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreLatestResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.ActiveRestoreStateStore;
|
||||
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader;
|
||||
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader.RestoreStatusSnapshot;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -24,160 +21,144 @@ import org.springframework.stereotype.Service;
|
||||
public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RestoreStatusFileReader restoreStatusFileReader;
|
||||
private final ActiveRestoreStateStore activeRestoreStateStore;
|
||||
|
||||
@Override
|
||||
public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
|
||||
ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setPrecheckId(entity.getPrecheckId());
|
||||
response.setBackupId(entity.getBackupId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setControlPhase("PRECHECKED");
|
||||
response.setStateSource("DB");
|
||||
response.setSourceDeviceId(entity.getSourceDeviceId());
|
||||
response.setSourceProductType(entity.getSourceProductType());
|
||||
response.setTargetProductType(entity.getTargetProductType());
|
||||
response.setPackageFileId(entity.getPackageFileId());
|
||||
response.setPackagePathVisible(Boolean.FALSE);
|
||||
response.setStatusFilePath(entity.getRunnerStatePath());
|
||||
response.setRestorePlan(readStringList(entity.getRestorePlanJson()));
|
||||
response.setConsistencyReport(readObject(entity.getConsistencyReportJson()));
|
||||
response.setErrorCode(entity.getErrorCode());
|
||||
response.setErrorMessage(entity.getErrorMessage());
|
||||
response.setCreatedBy(entity.getCreateBy());
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
response.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
|
||||
response.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
|
||||
mergeLightweightStatus(response, entity);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
Path taskDir = resolveTaskDir(entity);
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(taskDir);
|
||||
List<ResourceTaskStepItemResponse> responses = new ArrayList<>();
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
String stepCode = restorePlan.get(i);
|
||||
int stepNo = i + 1;
|
||||
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
|
||||
item.setStepNo(stepNo);
|
||||
item.setStepCode(stepCode);
|
||||
applyLightweightStepState(item, snapshot, stepCode);
|
||||
responses.add(item);
|
||||
public ResourceRestoreLatestResponse getLatestRestore() {
|
||||
Optional<ActiveRestoreStateStore.ActiveRestoreSnapshot> active = activeRestoreStateStore.read();
|
||||
if (active.isPresent()) {
|
||||
return latestFromActive(active.get());
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : "";
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
|
||||
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
|
||||
response.setTaskId(trim(taskId));
|
||||
response.setStepNo(stepNo);
|
||||
response.setStepCode(stepCode);
|
||||
response.setContent(snapshot.log());
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
return latestFromDatabase();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private void mergeLightweightStatus(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) {
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
|
||||
if (trim(snapshot.status()).isEmpty()) {
|
||||
private ResourceRestoreLatestResponse latestFromActive(ActiveRestoreStateStore.ActiveRestoreSnapshot active) {
|
||||
RestoreStatusSnapshot status = restoreStatusFileReader.read(resolveTaskDir(active.getRunnerStatePath()));
|
||||
String latestStatus = defaultIfBlank(status.status(), active.getStatus());
|
||||
repairMissingTask(active, status, latestStatus);
|
||||
ResourceRestoreLatestResponse response = new ResourceRestoreLatestResponse();
|
||||
response.setTaskId(active.getTaskId());
|
||||
response.setBackupId(active.getBackupId());
|
||||
response.setStatus(latestStatus);
|
||||
response.setPhase(status.phase());
|
||||
response.setMessage(displayMessage(status.message(), latestStatus));
|
||||
response.setCreatedAt(active.getCreatedAt());
|
||||
response.setFinishedAt(isTerminal(latestStatus) ? defaultIfBlank(status.updatedAt(), active.getFinishedAt()) : null);
|
||||
return response;
|
||||
}
|
||||
|
||||
private ResourceRestoreLatestResponse latestFromDatabase() {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findAll().stream()
|
||||
.filter(item -> !trim(item.getTaskId()).isEmpty())
|
||||
.max(Comparator.comparing(item -> item.getCreateTime() == null ? LocalDateTime.MIN : item.getCreateTime()))
|
||||
.orElse(null);
|
||||
ResourceRestoreLatestResponse response = new ResourceRestoreLatestResponse();
|
||||
if (entity == null) {
|
||||
response.setStatus("NONE");
|
||||
response.setMessage("暂无资源恢复记录");
|
||||
return response;
|
||||
}
|
||||
RestoreStatusSnapshot status = restoreStatusFileReader.read(resolveTaskDir(entity));
|
||||
String latestStatus = defaultIfBlank(status.status(), entity.getStatus());
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setBackupId(entity.getBackupId());
|
||||
response.setStatus(latestStatus);
|
||||
response.setPhase(status.phase());
|
||||
response.setMessage(displayMessage(status.message(), latestStatus));
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
response.setFinishedAt(isTerminal(latestStatus)
|
||||
? defaultIfBlank(status.updatedAt(), entity.getFinishTime() == null ? null : entity.getFinishTime().toString())
|
||||
: null);
|
||||
return response;
|
||||
}
|
||||
|
||||
private void repairMissingTask(
|
||||
ActiveRestoreStateStore.ActiveRestoreSnapshot active,
|
||||
RestoreStatusSnapshot status,
|
||||
String latestStatus
|
||||
) {
|
||||
if (trim(active.getTaskId()).isEmpty() || resourceRestoreTaskRepository.findByTaskId(active.getTaskId()).isPresent()) {
|
||||
return;
|
||||
}
|
||||
response.setStatus(defaultIfBlank(snapshot.status(), response.getStatus()));
|
||||
response.setControlPhase(defaultIfBlank(snapshot.phase(), response.getControlPhase()));
|
||||
response.setStateSource("SHELL_STATUS");
|
||||
response.setCurrentStepCode(defaultIfBlank(snapshot.phase(), response.getCurrentStepCode()));
|
||||
response.setCurrentStepNo(currentStepNo(snapshot.phase(), response.getRestorePlan()));
|
||||
response.setLastHeartbeatAt(defaultIfBlank(snapshot.updatedAt(), response.getLastHeartbeatAt()));
|
||||
response.setErrorMessage(defaultIfBlank(snapshot.message(), response.getErrorMessage()));
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(active.getTaskId());
|
||||
entity.setPrecheckId(defaultIfBlank(active.getPrecheckId(), active.getTaskId()));
|
||||
entity.setBackupId(defaultIfBlank(active.getBackupId(), "UNKNOWN"));
|
||||
entity.setStatus(defaultIfBlank(latestStatus, "UNKNOWN"));
|
||||
entity.setSourceDeviceId(active.getSourceDeviceId());
|
||||
entity.setSourceProductType(defaultIfBlank(active.getSourceProductType(), "UNKNOWN"));
|
||||
entity.setTargetProductType(defaultIfBlank(active.getTargetProductType(), "UNKNOWN"));
|
||||
entity.setPackageFileId(defaultIfBlank(active.getPackageFileId(), "UNKNOWN"));
|
||||
entity.setPackagePath(active.getPackagePath());
|
||||
entity.setStagingDir(active.getStagingDir());
|
||||
entity.setRunnerStatePath(active.getRunnerStatePath());
|
||||
entity.setStorageEncKeyFingerprint(defaultIfBlank(active.getStorageEncKeyFingerprint(), "UNKNOWN"));
|
||||
entity.setAuthenticityKeyFingerprint(defaultIfBlank(active.getAuthenticityKeyFingerprint(), "UNKNOWN"));
|
||||
entity.setRestorePlanJson(defaultIfBlank(active.getRestorePlanJson(), "[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]"));
|
||||
entity.setErrorMessage(status.message());
|
||||
entity.setCreateBy(defaultIfBlank(active.getCreateBy(), "SYSTEM"));
|
||||
entity.setCreateTime(parseLocalDateTime(active.getCreatedAt(), LocalDateTime.now()));
|
||||
entity.setStartTime(parseLocalDateTime(active.getStartedAt(), null));
|
||||
entity.setFinishTime(isTerminal(latestStatus) ? parseLocalDateTime(status.updatedAt(), null) : null);
|
||||
entity.setUpdateTime(LocalDateTime.now());
|
||||
resourceRestoreTaskRepository.save(entity);
|
||||
}
|
||||
|
||||
private Path resolveStatePath(String statusFilePath) {
|
||||
return trim(statusFilePath).isEmpty() ? null : Path.of(statusFilePath).normalize();
|
||||
}
|
||||
|
||||
private Path resolveTaskDir(String statusFilePath) {
|
||||
Path statePath = resolveStatePath(statusFilePath);
|
||||
return statePath == null ? null : statePath.getParent();
|
||||
}
|
||||
|
||||
private Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
|
||||
Path statePath = resolveStatePath(entity.getRunnerStatePath());
|
||||
return statePath == null ? null : statePath.getParent();
|
||||
}
|
||||
|
||||
private void applyLightweightStepState(ResourceTaskStepItemResponse item, RestoreStatusSnapshot snapshot, String stepCode) {
|
||||
item.setLogAvailable(!trim(snapshot.log()).isEmpty());
|
||||
if (trim(snapshot.status()).isEmpty()) {
|
||||
item.setStatus("PENDING");
|
||||
return;
|
||||
}
|
||||
if ("JAVA_EXTRACT".equals(stepCode)) {
|
||||
item.setStatus("SUCCESS");
|
||||
return;
|
||||
}
|
||||
if ("SHELL_APPLY".equals(stepCode)) {
|
||||
item.setStatus(snapshot.status());
|
||||
item.setMessage(snapshot.message());
|
||||
return;
|
||||
}
|
||||
if (stepCode.equals(snapshot.phase())) {
|
||||
item.setStatus(snapshot.status());
|
||||
item.setMessage(snapshot.message());
|
||||
return;
|
||||
}
|
||||
item.setStatus("PENDING");
|
||||
}
|
||||
|
||||
private List<String> readStringList(String json) {
|
||||
if (trim(json).isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, new TypeReference<List<String>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("解析恢复计划JSON失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Object readObject(String json) {
|
||||
if (trim(json).isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, Object.class);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("解析一致性报告JSON失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String defaultIfBlank(String candidate, String fallback) {
|
||||
return trim(candidate).isEmpty() ? fallback : candidate;
|
||||
}
|
||||
|
||||
private static Integer currentStepNo(String phase, List<String> restorePlan) {
|
||||
if (restorePlan == null || restorePlan.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
if (restorePlan.get(i).equals(phase)) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return restorePlan.contains("SHELL_APPLY") ? restorePlan.indexOf("SHELL_APPLY") + 1 : null;
|
||||
private static boolean isTerminal(String status) {
|
||||
String value = trim(status);
|
||||
return "SUCCESS".equalsIgnoreCase(value) || "FAILED".equalsIgnoreCase(value);
|
||||
}
|
||||
|
||||
private static String displayMessage(String message, String status) {
|
||||
String text = trim(message);
|
||||
if (!text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
if ("SUCCESS".equalsIgnoreCase(trim(status))) {
|
||||
return "恢复成功";
|
||||
}
|
||||
if ("FAILED".equalsIgnoreCase(trim(status))) {
|
||||
return "恢复失败";
|
||||
}
|
||||
if ("NONE".equalsIgnoreCase(trim(status))) {
|
||||
return "暂无资源恢复记录";
|
||||
}
|
||||
return "恢复执行中";
|
||||
}
|
||||
|
||||
private static LocalDateTime parseLocalDateTime(String value, LocalDateTime fallback) {
|
||||
String text = trim(value);
|
||||
if (text.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return LocalDateTime.parse(text);
|
||||
} catch (DateTimeException ex) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface ResourceTaskStepLogService {
|
||||
|
||||
ResourceTaskStepLogResponse read(Path taskDir, String taskId, int stepNo, String stepCode);
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class FileSystemResourceTaskStepLogService implements ResourceTaskStepLogService {
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse read(Path taskDir, String taskId, int stepNo, String stepCode) {
|
||||
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setStepNo(stepNo);
|
||||
response.setStepCode(stepCode);
|
||||
if (taskDir == null || stepCode == null || stepCode.isBlank()) {
|
||||
response.setContent("");
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
Path logFile = taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode));
|
||||
try {
|
||||
if (!Files.isRegularFile(logFile)) {
|
||||
response.setContent("");
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
response.setContent(Files.readString(logFile));
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("读取资源任务步骤日志失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,41 @@ tms:
|
||||
output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages}
|
||||
# 轻量恢复 apply 脚本启停 TMS 时使用的脚本路径。
|
||||
tms-script-path: ${TMS_BACKUP_TMS_SCRIPT_PATH:/home/tms/scripts/tms.sh}
|
||||
# 轻量恢复停服务命令。未配置时 apply 脚本会回退到 tms-script-path stop。
|
||||
# command 支持现场脚本路径或完整 shell 命令;required=false 表示失败只记录 warning。
|
||||
restore-stop-commands:
|
||||
- code: STANDARD_NGINX
|
||||
command: ${TMS_BACKUP_RESTORE_STOP_STANDARD_NGINX_COMMAND:${tms.init.executor.standard-nginx-stop-command}}
|
||||
required: ${TMS_BACKUP_RESTORE_STOP_STANDARD_NGINX_REQUIRED:false}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: STANDARD_APPS
|
||||
command: ${TMS_BACKUP_RESTORE_STOP_STANDARD_APPS_COMMAND:${tms.init.executor.standard-app-stop-command}}
|
||||
required: ${TMS_BACKUP_RESTORE_STOP_STANDARD_APPS_REQUIRED:false}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: TMS
|
||||
command: ${TMS_BACKUP_RESTORE_STOP_TMS_COMMAND:${tms.backup.tms-script-path} stop}
|
||||
required: ${TMS_BACKUP_RESTORE_STOP_TMS_REQUIRED:false}
|
||||
# 轻量恢复启动服务命令。标准/间参默认启动 TMS、CMSP/CMTP 和 nginx。
|
||||
restore-start-commands:
|
||||
- code: TMS
|
||||
command: ${TMS_BACKUP_RESTORE_START_TMS_COMMAND:${tms.backup.tms-script-path} start}
|
||||
required: ${TMS_BACKUP_RESTORE_START_TMS_REQUIRED:true}
|
||||
- code: STANDARD_APPS
|
||||
command: ${TMS_BACKUP_RESTORE_START_STANDARD_APPS_COMMAND:${tms.init.executor.standard-app-start-command}}
|
||||
required: ${TMS_BACKUP_RESTORE_START_STANDARD_APPS_REQUIRED:true}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
- code: STANDARD_NGINX
|
||||
command: ${TMS_BACKUP_RESTORE_START_STANDARD_NGINX_COMMAND:${tms.init.executor.standard-nginx-start-command}}
|
||||
required: ${TMS_BACKUP_RESTORE_START_STANDARD_NGINX_REQUIRED:true}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
# 轻量恢复 apply 脚本路径;Java 完成验签/解密/展开后调用该脚本执行覆盖、SQL、MQ replay。
|
||||
restore-apply-script-path: ${TMS_BACKUP_RESTORE_APPLY_SCRIPT_PATH:/home/tms/bin/resource-restore/apply-resource-backup.sh}
|
||||
# 文件恢复白名单根路径;manifest 中 FILE 恢复路径必须落在这些目录下。
|
||||
@@ -202,6 +237,61 @@ tms:
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4I:/home/cmep4i}
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4D:/home/cmep4d}
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_WORK:/home/tms/tmp/resource-restore}
|
||||
# 现场可追加或覆盖直接复制恢复的文件类资源。
|
||||
# mode=FILE 表示单文件,mode=DIR 表示目录递归,mode=GLOB 表示按通配符匹配同一目录下的一批文件。
|
||||
# restore-path 对 FILE 是目标文件路径,对 DIR/GLOB 是目标目录;目标仍必须落在 allowed-restore-roots 下。
|
||||
file-resources:
|
||||
- code: TMS_CONFIG
|
||||
mode: DIR
|
||||
path: ${TMS_BACKUP_TMS_CONFIG_PATH:${tms.init.executor.tms-config-dir}}
|
||||
restore-path: ${TMS_BACKUP_TMS_CONFIG_RESTORE_PATH:${tms.init.executor.tms-config-dir}}
|
||||
required: ${TMS_BACKUP_TMS_CONFIG_REQUIRED:true}
|
||||
- code: CP_CONFIG
|
||||
mode: FILE
|
||||
path: ${TMS_BACKUP_STANDARD_CP_CONFIG_PATH:${tms.init.executor.standard-cpconfig-path}}
|
||||
restore-path: ${TMS_BACKUP_STANDARD_CP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-cpconfig-path}}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: ${TMS_BACKUP_STANDARD_CP_CONFIG_REQUIRED:true}
|
||||
- code: CP_CONFIG
|
||||
mode: FILE
|
||||
path: ${TMS_BACKUP_DIRECT_CP_CONFIG_PATH:${tms.init.executor.direct-cpconfig-target-path}}
|
||||
restore-path: ${TMS_BACKUP_DIRECT_CP_CONFIG_RESTORE_PATH:${tms.init.executor.direct-cpconfig-target-path}}
|
||||
product-types:
|
||||
- DIRECT
|
||||
required: ${TMS_BACKUP_DIRECT_CP_CONFIG_REQUIRED:true}
|
||||
- code: RECEIVER_LICENSE
|
||||
mode: FILE
|
||||
path: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_PATH:${tms.init.executor.standard-receiver-license-target-path}}
|
||||
restore-path: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_RESTORE_PATH:${tms.init.executor.standard-receiver-license-target-path}}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: ${TMS_BACKUP_STANDARD_RECEIVER_LICENSE_REQUIRED:true}
|
||||
- code: RECEIVER_LICENSE
|
||||
mode: FILE
|
||||
path: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_PATH:${tms.init.executor.direct-receiver-license-target-path}}
|
||||
restore-path: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_RESTORE_PATH:${tms.init.executor.direct-receiver-license-target-path}}
|
||||
product-types:
|
||||
- DIRECT
|
||||
required: ${TMS_BACKUP_DIRECT_RECEIVER_LICENSE_REQUIRED:true}
|
||||
- code: STANDARD_CMSP_APP_CONFIG
|
||||
mode: GLOB
|
||||
path: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_PATH:${tms.init.executor.standard-home-dir}/cmsp/application-prd*.properties}
|
||||
restore-path: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-home-dir}/cmsp}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: ${TMS_BACKUP_STANDARD_CMSP_APP_CONFIG_REQUIRED:true}
|
||||
- code: STANDARD_CMTP_APP_CONFIG
|
||||
mode: GLOB
|
||||
path: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_PATH:${tms.init.executor.standard-home-dir}/cmtp/application-prd*.properties}
|
||||
restore-path: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_RESTORE_PATH:${tms.init.executor.standard-home-dir}/cmtp}
|
||||
product-types:
|
||||
- ENTERPRISE
|
||||
- INDIRECT
|
||||
required: ${TMS_BACKUP_STANDARD_CMTP_APP_CONFIG_REQUIRED:true}
|
||||
# 导出 TMS 库时使用的 mysqldump 命令;现场可按实际安装路径覆盖。
|
||||
mysqldump-path: ${TMS_BACKUP_MYSQLDUMP_PATH:mysqldump}
|
||||
# 恢复 TMS 库时使用的 mysql 客户端命令。
|
||||
@@ -212,6 +302,13 @@ tms:
|
||||
mq-replay-script-path: ${TMS_BACKUP_MQ_REPLAY_SCRIPT_PATH:/home/tms/bin/resource-restore/replay-mq.sh}
|
||||
# 备份 TMS 库名;为空时默认从 spring.datasource.url 中解析。
|
||||
tms-database-name: ${TMS_BACKUP_TMS_DATABASE_NAME:}
|
||||
# TMS 库备份排除表。默认不备份角色、授权、会话和审计表,避免恢复时覆盖新机器安全状态。
|
||||
tms-db-excluded-tables:
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_ROLE_ACCOUNT:tms_role_account}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_ROLE_UKEY:tms_role_ukey_binding}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_FULL:tms_auth_full_account}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_SESSION:tms_auth_session}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_CHALLENGE:tms_auth_challenge}
|
||||
# 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。
|
||||
cmep-database-name: ${TMS_BACKUP_CMEP_DATABASE_NAME:CMEP}
|
||||
# 恢复完成后的最小健康检查地址。
|
||||
@@ -229,9 +326,9 @@ tms:
|
||||
internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token}
|
||||
internal-auth:
|
||||
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
|
||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:true}
|
||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试角色。
|
||||
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:KEY_ADMIN}
|
||||
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:OPS_ADMIN}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试认证等级。
|
||||
debug-auth-level: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_AUTH_LEVEL:FULL}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试 session token。
|
||||
|
||||
@@ -115,26 +115,6 @@ CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
|
||||
KEY idx_tms_resource_restore_task_status (status),
|
||||
KEY idx_tms_resource_restore_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_task_step (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'RESOURCE_BACKUP / RESOURCE_RESTORE',
|
||||
step_no INT NOT NULL,
|
||||
step_code VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
message VARCHAR(512) NULL,
|
||||
log_path VARCHAR(1024) NULL,
|
||||
exit_code INT NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_task_step_task_no (task_id, step_no),
|
||||
KEY idx_tms_resource_task_step_task_id (task_id),
|
||||
KEY idx_tms_resource_task_step_task_type (task_type)
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 认证与设备基础表
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
@@ -62,22 +62,3 @@ CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
|
||||
KEY idx_tms_resource_restore_task_status (status),
|
||||
KEY idx_tms_resource_restore_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_task_step (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
task_type VARCHAR(32) NOT NULL COMMENT 'RESOURCE_BACKUP / RESOURCE_RESTORE',
|
||||
step_no INT NOT NULL,
|
||||
step_code VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
message VARCHAR(512) NULL,
|
||||
log_path VARCHAR(1024) NULL,
|
||||
exit_code INT NULL,
|
||||
create_time DATETIME(3) NOT NULL,
|
||||
update_time DATETIME(3) NOT NULL,
|
||||
start_time DATETIME(3) NULL,
|
||||
finish_time DATETIME(3) NULL,
|
||||
UNIQUE KEY uk_tms_resource_task_step_task_no (task_id, step_no),
|
||||
KEY idx_tms_resource_task_step_task_id (task_id),
|
||||
KEY idx_tms_resource_task_step_task_type (task_type)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user