feat:备份恢复
This commit is contained in:
@@ -218,6 +218,38 @@ Open:
|
||||
- `FIRMWARE` 不增加额外后端流程,具体固件刷写、重启、恢复提示由包内脚本负责
|
||||
- 回滚不自动触发,需要调用回滚接口
|
||||
- `TMS`、`RECEIVER` 升级成功后会更新 `tms_device_software_version`;`FIRMWARE` 暂不维护版本表
|
||||
|
||||
## Resource Backup / Restore
|
||||
|
||||
最小调用顺序:
|
||||
- `GET /api/v1/resource-backups/keyset/status`
|
||||
- `POST /api/v1/resource-backups`
|
||||
- `GET /api/v1/resource-backups/{taskId}`
|
||||
- `GET /api/v1/resource-backups/{taskId}/download`
|
||||
- `POST /api/v1/resource-restores/precheck`
|
||||
- `POST /api/v1/resource-restores`
|
||||
- `GET /api/v1/resource-restores/{taskId}`
|
||||
- `GET /api/v1/resource-restores/{taskId}/steps`
|
||||
- `GET /api/v1/resource-restores/{taskId}/steps/{stepNo}/log`
|
||||
|
||||
当前实现边界:
|
||||
- 资源备份包输出为 `.tmsbak`,包内包含 `manifest.json`、`envelope.json`、`payload.enc`、`signature.sig`
|
||||
- 预检阶段只做包头解析、指纹匹配和验签,不提前解密完整 payload
|
||||
- 恢复任务创建后会生成 `handoff.json`、`restore-state.json`,并进入 `HANDOFF_READY`
|
||||
- `ResourceRestoreRunner` 当前已具备接管、步骤循环、`step-output/*.json`、心跳刷新、`RECONCILE_PENDING` 推进能力
|
||||
- `STOP_SERVICES` / `START_SERVICES` 已对接现有 `tms.sh` 与标准版启停脚本配置
|
||||
|
||||
关键配置:
|
||||
- `tms.backup.output-dir`
|
||||
- `tms.backup.precheck-store-dir`
|
||||
- `tms.backup.restore-task-root-dir`
|
||||
- `tms.backup.tms-script-path`
|
||||
- `tms.backup.runner-auto-launch-enabled`
|
||||
- `tms.backup.runner-heartbeat-timeout-seconds`
|
||||
|
||||
恢复前置条件:
|
||||
- LMK、IK、数据存储加密密钥对、数据真实性保护密钥对需先通过 UKey 恢复
|
||||
- 默认不自动拉起本机 runner;如需在现场机器自动执行,显式开启 `tms.backup.runner-auto-launch-enabled=true`
|
||||
- 升级执行日志统一写入 `tms.upgrade.log-dir`
|
||||
|
||||
当前配置项:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.cisd.tms.modules.backup.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "tms.backup")
|
||||
public class ResourceBackupProperties {
|
||||
|
||||
private String outputDir = "/home/tms/tmp/resource-backup-packages";
|
||||
private String precheckStoreDir = "/home/tms/tmp/resource-restore-precheck";
|
||||
private String restoreTaskRootDir = "/home/tms/tmp/resource-restore";
|
||||
private String tmsScriptPath = "/home/tms/scripts/tms.sh";
|
||||
private boolean runnerAutoLaunchEnabled = false;
|
||||
private int runnerHeartbeatTimeoutSeconds = 300;
|
||||
private int storageEncKeyIndex = 1;
|
||||
private int authenticitySignKeyIndex = 1;
|
||||
|
||||
public String getOutputDir() {
|
||||
return outputDir;
|
||||
}
|
||||
|
||||
public void setOutputDir(String outputDir) {
|
||||
this.outputDir = outputDir;
|
||||
}
|
||||
|
||||
public String getPrecheckStoreDir() {
|
||||
return precheckStoreDir;
|
||||
}
|
||||
|
||||
public void setPrecheckStoreDir(String precheckStoreDir) {
|
||||
this.precheckStoreDir = precheckStoreDir;
|
||||
}
|
||||
|
||||
public String getRestoreTaskRootDir() {
|
||||
return restoreTaskRootDir;
|
||||
}
|
||||
|
||||
public void setRestoreTaskRootDir(String restoreTaskRootDir) {
|
||||
this.restoreTaskRootDir = restoreTaskRootDir;
|
||||
}
|
||||
|
||||
public String getTmsScriptPath() {
|
||||
return tmsScriptPath;
|
||||
}
|
||||
|
||||
public void setTmsScriptPath(String tmsScriptPath) {
|
||||
this.tmsScriptPath = tmsScriptPath;
|
||||
}
|
||||
|
||||
public boolean getRunnerAutoLaunchEnabled() {
|
||||
return runnerAutoLaunchEnabled;
|
||||
}
|
||||
|
||||
public void setRunnerAutoLaunchEnabled(boolean runnerAutoLaunchEnabled) {
|
||||
this.runnerAutoLaunchEnabled = runnerAutoLaunchEnabled;
|
||||
}
|
||||
|
||||
public int getRunnerHeartbeatTimeoutSeconds() {
|
||||
return runnerHeartbeatTimeoutSeconds;
|
||||
}
|
||||
|
||||
public void setRunnerHeartbeatTimeoutSeconds(int runnerHeartbeatTimeoutSeconds) {
|
||||
this.runnerHeartbeatTimeoutSeconds = runnerHeartbeatTimeoutSeconds;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.cisd.tms.modules.backup.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
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.service.ResourceBackupService;
|
||||
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
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 org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource-backups")
|
||||
@Tag(name = "资源备份", description = "资源备份创建、状态查询和步骤日志接口")
|
||||
public class ResourceBackupController {
|
||||
|
||||
private final ResourceBackupService resourceBackupService;
|
||||
|
||||
public ResourceBackupController(ResourceBackupService resourceBackupService) {
|
||||
this.resourceBackupService = resourceBackupService;
|
||||
}
|
||||
|
||||
@GetMapping("/keyset/status")
|
||||
@Operation(summary = "查询资源备份密钥状态", description = "返回资源备份恢复前置密钥状态和公钥指纹。")
|
||||
public ApiResponse<ResourceKeysetStatusResponse> keysetStatus() {
|
||||
return ApiResponse.success(resourceBackupService.getKeysetStatus());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ReplayProtected
|
||||
@Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。")
|
||||
public ApiResponse<CreateResourceBackupResponse> createBackupTask(@Valid @RequestBody CreateResourceBackupRequest request) {
|
||||
return ApiResponse.success(resourceBackupService.createBackupTask(request));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}")
|
||||
@Operation(summary = "查询资源备份任务详情", description = "返回资源备份任务详情。")
|
||||
public ApiResponse<ResourceBackupTaskDetailResponse> taskDetail(
|
||||
@Parameter(description = "备份任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceBackupService.getBackupTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/download")
|
||||
@Operation(summary = "下载资源备份包", description = "以浏览器附件方式下载资源备份包。")
|
||||
public ResponseEntity<Resource> downloadPackage(
|
||||
@Parameter(description = "备份任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
ResourceBackupDownloadResult result = resourceBackupService.downloadBackupPackage(taskId);
|
||||
FileSystemResource resource = new FileSystemResource(result.getPackagePath());
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.contentLength(result.getContentLength())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(result.getFilename()).build().toString())
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps")
|
||||
@Operation(summary = "查询资源备份任务步骤", description = "返回资源备份任务步骤列表。")
|
||||
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
|
||||
@Parameter(description = "备份任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceBackupService.getBackupTaskSteps(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps/{stepNo}/log")
|
||||
@Operation(summary = "查询资源备份步骤日志", description = "返回资源备份任务步骤日志。")
|
||||
public ApiResponse<ResourceTaskStepLogResponse> taskStepLog(
|
||||
@Parameter(description = "备份任务号")
|
||||
@PathVariable String taskId,
|
||||
@Parameter(description = "步骤序号")
|
||||
@PathVariable int stepNo
|
||||
) {
|
||||
return ApiResponse.success(resourceBackupService.getBackupTaskStepLog(taskId, stepNo));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.cisd.tms.modules.backup.controller;
|
||||
|
||||
import com.cisd.tms.common.api.ApiResponse;
|
||||
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.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import com.cisd.tms.security.internal.ReplayProtected;
|
||||
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 org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/resource-restores")
|
||||
@Tag(name = "资源恢复", description = "资源恢复预检、任务创建、详情和步骤日志接口")
|
||||
public class ResourceRestoreController {
|
||||
|
||||
private final ResourcePrecheckService resourcePrecheckService;
|
||||
private final ResourceRestoreService resourceRestoreService;
|
||||
private final ResourceTaskQueryService resourceTaskQueryService;
|
||||
|
||||
public ResourceRestoreController(
|
||||
ResourcePrecheckService resourcePrecheckService,
|
||||
ResourceRestoreService resourceRestoreService,
|
||||
ResourceTaskQueryService resourceTaskQueryService
|
||||
) {
|
||||
this.resourcePrecheckService = resourcePrecheckService;
|
||||
this.resourceRestoreService = resourceRestoreService;
|
||||
this.resourceTaskQueryService = resourceTaskQueryService;
|
||||
}
|
||||
|
||||
@PostMapping("/precheck")
|
||||
@Operation(summary = "预检资源恢复请求", description = "根据 fileId 校验备份包头、签名和恢复范围摘要。")
|
||||
public ApiResponse<ResourceRestorePrecheckResponse> precheck(@Valid @RequestBody ResourceRestorePrecheckRequest request) {
|
||||
return ApiResponse.success(resourcePrecheckService.precheck(request));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ReplayProtected
|
||||
@Operation(summary = "创建资源恢复任务", description = "根据预检号创建资源恢复任务。")
|
||||
public ApiResponse<CreateResourceRestoreResponse> createRestoreTask(@Valid @RequestBody CreateResourceRestoreRequest request) {
|
||||
return ApiResponse.success(resourceRestoreService.createRestoreTask(request));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}")
|
||||
@Operation(summary = "查询资源恢复任务详情", description = "返回资源恢复任务详情和当前控制阶段。")
|
||||
public ApiResponse<ResourceRestoreTaskDetailResponse> taskDetail(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTask(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps")
|
||||
@Operation(summary = "查询资源恢复任务步骤", description = "返回资源恢复任务步骤列表。")
|
||||
public ApiResponse<List<ResourceTaskStepItemResponse>> taskSteps(
|
||||
@Parameter(description = "恢复任务号")
|
||||
@PathVariable String taskId
|
||||
) {
|
||||
return ApiResponse.success(resourceTaskQueryService.getRestoreTaskSteps(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/{taskId}/steps/{stepNo}/log")
|
||||
@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,17 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
public interface ResourceEnvelopeCryptoService {
|
||||
|
||||
ResourceSealedPayload seal(byte[] payloadBytes);
|
||||
|
||||
byte[] sign(byte[] dataToSign);
|
||||
|
||||
void verify(byte[] dataToVerify, byte[] signature);
|
||||
|
||||
byte[] unsealAndVerify(
|
||||
java.util.Map<String, Object> envelope,
|
||||
byte[] cipherBytes,
|
||||
byte[] dataToVerify,
|
||||
byte[] signature
|
||||
);
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
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.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCryptoService {
|
||||
|
||||
private static final String PROVIDER = "BC";
|
||||
private final PcieCryptoService pcieCryptoService;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
static {
|
||||
if (Security.getProvider(PROVIDER) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceEnvelopeCryptoServiceImpl(
|
||||
PcieCryptoService pcieCryptoService,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.pcieCryptoService = pcieCryptoService;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceSealedPayload seal(byte[] payloadBytes) {
|
||||
if (payloadBytes == null) {
|
||||
throw new IllegalArgumentException("payloadBytes is required");
|
||||
}
|
||||
// 资源包正文使用随机 SM4 DEK 加密,再由用户密钥把 DEK 封装成 envelope。
|
||||
// 这样大 payload 仍走对称加密,ECC 只负责保护一次性数据密钥。
|
||||
byte[] sm4Key = randomBytes(16);
|
||||
byte[] iv = randomBytes(16);
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
|
||||
|
||||
// 用户密钥只负责封装随机 DEK;真正的大 payload 仍走对称加密,避免直接把大文件送进 ECC 接口。
|
||||
EccInternalEncryptRequest encryptRequest = new EccInternalEncryptRequest();
|
||||
encryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
encryptRequest.setData(sm4Key);
|
||||
BackupDataResult wrappedDek = pcieCryptoService.sdfeInternalEncryptEcc(encryptRequest);
|
||||
|
||||
Map<String, Object> envelope = new LinkedHashMap<>();
|
||||
envelope.put("version", 1);
|
||||
envelope.put("payloadAlg", "SM4-CBC");
|
||||
envelope.put("keyWrapAlg", "SDFE_InternalEncrypt_ECC");
|
||||
envelope.put("ivBase64", Base64.getEncoder().encodeToString(iv));
|
||||
envelope.put("wrappedDekBase64", Base64.getEncoder().encodeToString(normalize(wrappedDek)));
|
||||
envelope.put("dekLength", sm4Key.length);
|
||||
|
||||
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()));
|
||||
return sealedPayload;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to seal resource payload", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] sign(byte[] dataToSign) {
|
||||
if (dataToSign == null) {
|
||||
throw new IllegalArgumentException("dataToSign is required");
|
||||
}
|
||||
EccInternalSignRequest request = new EccInternalSignRequest();
|
||||
request.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
// 内部签名接口这里统一签 SM3 摘要,避免把整段原文直接交给密码卡并固定后续验签输入语义。
|
||||
request.setData(sm3(dataToSign));
|
||||
return normalize(pcieCryptoService.eccInternalSign(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(byte[] dataToVerify, byte[] signature) {
|
||||
if (dataToVerify == null) {
|
||||
throw new IllegalArgumentException("dataToVerify is required");
|
||||
}
|
||||
if (signature == null || signature.length == 0) {
|
||||
throw new IllegalArgumentException("signature is required");
|
||||
}
|
||||
EccInternalVerifyRequest verifyRequest = new EccInternalVerifyRequest();
|
||||
verifyRequest.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
verifyRequest.setData(sm3(dataToVerify));
|
||||
verifyRequest.setSignature(signature);
|
||||
pcieCryptoService.eccInternalVerify(verifyRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] unsealAndVerify(Map<String, Object> envelope, byte[] cipherBytes, byte[] dataToVerify, byte[] signature) {
|
||||
if (envelope == null) {
|
||||
throw new IllegalArgumentException("envelope is required");
|
||||
}
|
||||
if (cipherBytes == null) {
|
||||
throw new IllegalArgumentException("cipherBytes is required");
|
||||
}
|
||||
if (dataToVerify == null) {
|
||||
throw new IllegalArgumentException("dataToVerify is required");
|
||||
}
|
||||
if (signature == null || signature.length == 0) {
|
||||
throw new IllegalArgumentException("signature is required");
|
||||
}
|
||||
verify(dataToVerify, signature);
|
||||
|
||||
byte[] wrappedDek = Base64.getDecoder().decode(String.valueOf(envelope.get("wrappedDekBase64")));
|
||||
int dekLength = parseInt(envelope.get("dekLength"), 16);
|
||||
// 先验签,再解数字信封,再解 payload;顺序固定,避免篡改包先进入明文处理路径。
|
||||
EccInternalDecryptRequest decryptRequest = new EccInternalDecryptRequest();
|
||||
decryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
decryptRequest.setCipherBlob(wrappedDek);
|
||||
decryptRequest.setOutBufferSize(dekLength);
|
||||
byte[] sm4Key = normalize(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest));
|
||||
byte[] iv = Base64.getDecoder().decode(String.valueOf(envelope.get("ivBase64")));
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
|
||||
return cipher.doFinal(cipherBytes);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to unseal resource payload", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] randomBytes(int size) {
|
||||
byte[] bytes = new byte[size];
|
||||
new java.security.SecureRandom().nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static byte[] sm3(byte[] data) {
|
||||
SM3Digest digest = new SM3Digest();
|
||||
digest.update(data, 0, data.length);
|
||||
byte[] out = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static byte[] normalize(BackupDataResult result) {
|
||||
if (result == null || result.getData() == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
int safeLength = result.getLength() > 0 && result.getLength() <= result.getData().length
|
||||
? result.getLength()
|
||||
: result.getData().length;
|
||||
byte[] normalized = new byte[safeLength];
|
||||
System.arraycopy(result.getData(), 0, normalized, 0, safeLength);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static int parseInt(Object value, int defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(value));
|
||||
} catch (NumberFormatException ex) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildFingerprint(String prefix, int keyIndex) {
|
||||
return prefix + ":" + keyIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.cisd.tms.modules.backup.crypto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ResourceSealedPayload {
|
||||
|
||||
private byte[] cipherBytes;
|
||||
private Map<String, Object> envelope;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
|
||||
public byte[] getCipherBytes() {
|
||||
return cipherBytes;
|
||||
}
|
||||
|
||||
public void setCipherBytes(byte[] cipherBytes) {
|
||||
this.cipherBytes = cipherBytes;
|
||||
}
|
||||
|
||||
public Map<String, Object> getEnvelope() {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
public void setEnvelope(Map<String, Object> envelope) {
|
||||
this.envelope = envelope;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cisd.tms.modules.backup.discovery;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourceDiscoveryService {
|
||||
|
||||
List<DiscoveredResourceItem> discover(InitTaskEntity initTask);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.cisd.tms.modules.backup.discovery;
|
||||
|
||||
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.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
|
||||
private final InitExecutorProperties initExecutorProperties;
|
||||
|
||||
public ResourceDiscoveryServiceImpl(InitExecutorProperties initExecutorProperties) {
|
||||
this.initExecutorProperties = initExecutorProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DiscoveredResourceItem> discover(InitTaskEntity initTask) {
|
||||
if (initTask == null) {
|
||||
throw new IllegalArgumentException("initTask is required");
|
||||
}
|
||||
String productType = upper(initTask.getProductType());
|
||||
String mqType = upper(initTask.getMqType());
|
||||
if (productType.isEmpty() || mqType.isEmpty()) {
|
||||
throw new IllegalArgumentException("productType and mqType are required");
|
||||
}
|
||||
|
||||
List<DiscoveredResourceItem> items = new ArrayList<>();
|
||||
items.add(item("FILE", "TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), true));
|
||||
items.add(item("FILE", "CP_CONFIG", resolveCpConfigPath(productType), true));
|
||||
items.add(item("FILE", "RECEIVER_LICENSE", resolveReceiverLicensePath(productType), true));
|
||||
|
||||
if (mqType.startsWith("RABBITMQ")) {
|
||||
items.add(item("MQ", "RABBITMQ_DEFINITIONS", initExecutorProperties.getRabbitmqAdminCommand(), true));
|
||||
}
|
||||
if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) {
|
||||
items.add(item("FILE", "TLQ_LICENSE", resolveTlqLicensePath(productType), true));
|
||||
}
|
||||
if ("RABBITMQ_CFMQ".equals(mqType)) {
|
||||
items.add(item("FILE", "CFMQ_CONFIG", initExecutorProperties.getCfmqTargetDir(), true));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private String resolveCpConfigPath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectCpconfigTargetPath();
|
||||
}
|
||||
return initExecutorProperties.getStandardCpconfigPath();
|
||||
}
|
||||
|
||||
private String resolveReceiverLicensePath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectReceiverLicenseTargetPath();
|
||||
}
|
||||
return initExecutorProperties.getStandardReceiverLicenseTargetPath();
|
||||
}
|
||||
|
||||
private String resolveTlqLicensePath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectTlqLicenseTargetPath();
|
||||
}
|
||||
return initExecutorProperties.getStandardTlqLicenseTargetPath();
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem item(String type, String code, String path, boolean required) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType(type);
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(required);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static String upper(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(description = "创建资源恢复任务请求")
|
||||
public class CreateResourceRestoreRequest {
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "预检号", example = "RPRE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String precheckId;
|
||||
|
||||
@NotBlank
|
||||
@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;
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getConfirmBackupId() {
|
||||
return confirmBackupId;
|
||||
}
|
||||
|
||||
public void setConfirmBackupId(String confirmBackupId) {
|
||||
this.confirmBackupId = confirmBackupId;
|
||||
}
|
||||
|
||||
public Boolean getConfirmRiskAccepted() {
|
||||
return confirmRiskAccepted;
|
||||
}
|
||||
|
||||
public void setConfirmRiskAccepted(Boolean confirmRiskAccepted) {
|
||||
this.confirmRiskAccepted = confirmRiskAccepted;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.cisd.tms.modules.backup.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Schema(description = "资源恢复预检请求")
|
||||
public class ResourceRestorePrecheckRequest {
|
||||
|
||||
@NotBlank
|
||||
@Schema(description = "备份包 fileId", example = "FILE-20260408-000001", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String fileId;
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "创建资源备份任务响应")
|
||||
public class CreateResourceBackupResponse {
|
||||
|
||||
@Schema(description = "备份任务号", example = "RBKP-TASK-20260408-000001")
|
||||
private String taskId;
|
||||
@Schema(description = "备份号", example = "RBKP-20260408-153000-000001")
|
||||
private String backupId;
|
||||
@Schema(description = "任务状态", example = "PENDING")
|
||||
private String status;
|
||||
@Schema(description = "创建时间", example = "2026-04-08T15:30:00+08:00")
|
||||
private String createdAt;
|
||||
|
||||
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 getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "创建资源恢复任务响应")
|
||||
public class CreateResourceRestoreResponse {
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private String createdAt;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(String createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "资源备份任务详情响应")
|
||||
public class ResourceBackupTaskDetailResponse {
|
||||
|
||||
private String taskId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String productType;
|
||||
private String mqType;
|
||||
private String orgCode;
|
||||
private String sourceDeviceId;
|
||||
private String packageName;
|
||||
private Long packageSize;
|
||||
private String packageHash;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private Object manifestSummary;
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String remark;
|
||||
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 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 getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getMqType() {
|
||||
return mqType;
|
||||
}
|
||||
|
||||
public void setMqType(String mqType) {
|
||||
this.mqType = mqType;
|
||||
}
|
||||
|
||||
public String getOrgCode() {
|
||||
return orgCode;
|
||||
}
|
||||
|
||||
public void setOrgCode(String orgCode) {
|
||||
this.orgCode = orgCode;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public Long getPackageSize() {
|
||||
return packageSize;
|
||||
}
|
||||
|
||||
public void setPackageSize(Long packageSize) {
|
||||
this.packageSize = packageSize;
|
||||
}
|
||||
|
||||
public String getPackageHash() {
|
||||
return packageHash;
|
||||
}
|
||||
|
||||
public void setPackageHash(String packageHash) {
|
||||
this.packageHash = packageHash;
|
||||
}
|
||||
|
||||
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 Object getManifestSummary() {
|
||||
return manifestSummary;
|
||||
}
|
||||
|
||||
public void setManifestSummary(Object manifestSummary) {
|
||||
this.manifestSummary = manifestSummary;
|
||||
}
|
||||
|
||||
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 getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "资源备份密钥状态响应")
|
||||
public class ResourceKeysetStatusResponse {
|
||||
|
||||
@Schema(description = "LMK 是否就绪")
|
||||
private Boolean lmkReady;
|
||||
@Schema(description = "IK 是否就绪")
|
||||
private Boolean ikReady;
|
||||
@Schema(description = "数据存储加密密钥对是否就绪")
|
||||
private Boolean storageEncKeyReady;
|
||||
@Schema(description = "数据真实性保护密钥对是否就绪")
|
||||
private Boolean authenticityKeyReady;
|
||||
@Schema(description = "数据存储加密公钥指纹")
|
||||
private String storageEncKeyFingerprint;
|
||||
@Schema(description = "真实性保护公钥指纹")
|
||||
private String authenticityKeyFingerprint;
|
||||
@Schema(description = "设备产品类型")
|
||||
private String productType;
|
||||
@Schema(description = "来源")
|
||||
private String source;
|
||||
|
||||
public Boolean getLmkReady() {
|
||||
return lmkReady;
|
||||
}
|
||||
|
||||
public void setLmkReady(Boolean lmkReady) {
|
||||
this.lmkReady = lmkReady;
|
||||
}
|
||||
|
||||
public Boolean getIkReady() {
|
||||
return ikReady;
|
||||
}
|
||||
|
||||
public void setIkReady(Boolean ikReady) {
|
||||
this.ikReady = ikReady;
|
||||
}
|
||||
|
||||
public Boolean getStorageEncKeyReady() {
|
||||
return storageEncKeyReady;
|
||||
}
|
||||
|
||||
public void setStorageEncKeyReady(Boolean storageEncKeyReady) {
|
||||
this.storageEncKeyReady = storageEncKeyReady;
|
||||
}
|
||||
|
||||
public Boolean getAuthenticityKeyReady() {
|
||||
return authenticityKeyReady;
|
||||
}
|
||||
|
||||
public void setAuthenticityKeyReady(Boolean authenticityKeyReady) {
|
||||
this.authenticityKeyReady = authenticityKeyReady;
|
||||
}
|
||||
|
||||
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 getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "资源恢复预检响应")
|
||||
public class ResourceRestorePrecheckResponse {
|
||||
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String sourceProductType;
|
||||
private String sourceMqType;
|
||||
private String sourceOrgCode;
|
||||
private String sourceBackupTime;
|
||||
private String targetProductType;
|
||||
private Boolean keysetMatched;
|
||||
private Boolean signatureValid;
|
||||
private Boolean compatible;
|
||||
private Boolean payloadDecrypted;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
private List<String> restorePlan;
|
||||
private Object restoreScopeSummary;
|
||||
private String expiresAt;
|
||||
|
||||
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 getSourceProductType() {
|
||||
return sourceProductType;
|
||||
}
|
||||
|
||||
public void setSourceProductType(String sourceProductType) {
|
||||
this.sourceProductType = sourceProductType;
|
||||
}
|
||||
|
||||
public String getSourceMqType() {
|
||||
return sourceMqType;
|
||||
}
|
||||
|
||||
public void setSourceMqType(String sourceMqType) {
|
||||
this.sourceMqType = sourceMqType;
|
||||
}
|
||||
|
||||
public String getSourceOrgCode() {
|
||||
return sourceOrgCode;
|
||||
}
|
||||
|
||||
public void setSourceOrgCode(String sourceOrgCode) {
|
||||
this.sourceOrgCode = sourceOrgCode;
|
||||
}
|
||||
|
||||
public String getSourceBackupTime() {
|
||||
return sourceBackupTime;
|
||||
}
|
||||
|
||||
public void setSourceBackupTime(String sourceBackupTime) {
|
||||
this.sourceBackupTime = sourceBackupTime;
|
||||
}
|
||||
|
||||
public String getTargetProductType() {
|
||||
return targetProductType;
|
||||
}
|
||||
|
||||
public void setTargetProductType(String targetProductType) {
|
||||
this.targetProductType = targetProductType;
|
||||
}
|
||||
|
||||
public Boolean getKeysetMatched() {
|
||||
return keysetMatched;
|
||||
}
|
||||
|
||||
public void setKeysetMatched(Boolean keysetMatched) {
|
||||
this.keysetMatched = keysetMatched;
|
||||
}
|
||||
|
||||
public Boolean getSignatureValid() {
|
||||
return signatureValid;
|
||||
}
|
||||
|
||||
public void setSignatureValid(Boolean signatureValid) {
|
||||
this.signatureValid = signatureValid;
|
||||
}
|
||||
|
||||
public Boolean getCompatible() {
|
||||
return compatible;
|
||||
}
|
||||
|
||||
public void setCompatible(Boolean compatible) {
|
||||
this.compatible = compatible;
|
||||
}
|
||||
|
||||
public Boolean getPayloadDecrypted() {
|
||||
return payloadDecrypted;
|
||||
}
|
||||
|
||||
public void setPayloadDecrypted(Boolean payloadDecrypted) {
|
||||
this.payloadDecrypted = payloadDecrypted;
|
||||
}
|
||||
|
||||
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 getRestoreScopeSummary() {
|
||||
return restoreScopeSummary;
|
||||
}
|
||||
|
||||
public void setRestoreScopeSummary(Object restoreScopeSummary) {
|
||||
this.restoreScopeSummary = restoreScopeSummary;
|
||||
}
|
||||
|
||||
public String getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(String expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
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 runnerStatePath;
|
||||
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 getRunnerStatePath() {
|
||||
return runnerStatePath;
|
||||
}
|
||||
|
||||
public void setRunnerStatePath(String runnerStatePath) {
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "资源任务步骤项响应")
|
||||
public class ResourceTaskStepItemResponse {
|
||||
|
||||
private Integer stepNo;
|
||||
private String stepCode;
|
||||
private String status;
|
||||
private String message;
|
||||
private Integer exitCode;
|
||||
private String startedAt;
|
||||
private String finishedAt;
|
||||
private Boolean logAvailable;
|
||||
|
||||
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 Integer getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public void setExitCode(Integer exitCode) {
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Boolean getLogAvailable() {
|
||||
return logAvailable;
|
||||
}
|
||||
|
||||
public void setLogAvailable(Boolean logAvailable) {
|
||||
this.logAvailable = logAvailable;
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.cisd.tms.modules.backup.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "资源任务步骤日志响应")
|
||||
public class ResourceTaskStepLogResponse {
|
||||
|
||||
private String taskId;
|
||||
private Integer stepNo;
|
||||
private String stepCode;
|
||||
private String content;
|
||||
private Boolean truncated;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
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 getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Boolean getTruncated() {
|
||||
return truncated;
|
||||
}
|
||||
|
||||
public void setTruncated(Boolean truncated) {
|
||||
this.truncated = truncated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.cisd.tms.modules.backup.dto.support;
|
||||
|
||||
public class DiscoveredResourceItem {
|
||||
|
||||
private String resourceType;
|
||||
private String resourceCode;
|
||||
private String resolvedPath;
|
||||
private boolean required;
|
||||
|
||||
public String getResourceType() {
|
||||
return resourceType;
|
||||
}
|
||||
|
||||
public void setResourceType(String resourceType) {
|
||||
this.resourceType = resourceType;
|
||||
}
|
||||
|
||||
public String getResourceCode() {
|
||||
return resourceCode;
|
||||
}
|
||||
|
||||
public void setResourceCode(String resourceCode) {
|
||||
this.resourceCode = resourceCode;
|
||||
}
|
||||
|
||||
public String getResolvedPath() {
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
public void setResolvedPath(String resolvedPath) {
|
||||
this.resolvedPath = resolvedPath;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
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_backup_task")
|
||||
public class ResourceBackupTaskEntity extends BaseEntity {
|
||||
|
||||
private String taskId;
|
||||
private String backupId;
|
||||
private String status;
|
||||
private String productType;
|
||||
private String mqType;
|
||||
private String orgCode;
|
||||
private String sourceDeviceId;
|
||||
private String packageName;
|
||||
private String packagePath;
|
||||
private Long packageSize;
|
||||
private String packageHash;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private String manifestSummaryJson;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private String remark;
|
||||
private String createBy;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime finishTime;
|
||||
|
||||
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 getProductType() {
|
||||
return productType;
|
||||
}
|
||||
|
||||
public void setProductType(String productType) {
|
||||
this.productType = productType;
|
||||
}
|
||||
|
||||
public String getMqType() {
|
||||
return mqType;
|
||||
}
|
||||
|
||||
public void setMqType(String mqType) {
|
||||
this.mqType = mqType;
|
||||
}
|
||||
|
||||
public String getOrgCode() {
|
||||
return orgCode;
|
||||
}
|
||||
|
||||
public void setOrgCode(String orgCode) {
|
||||
this.orgCode = orgCode;
|
||||
}
|
||||
|
||||
public String getSourceDeviceId() {
|
||||
return sourceDeviceId;
|
||||
}
|
||||
|
||||
public void setSourceDeviceId(String sourceDeviceId) {
|
||||
this.sourceDeviceId = sourceDeviceId;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public Long getPackageSize() {
|
||||
return packageSize;
|
||||
}
|
||||
|
||||
public void setPackageSize(Long packageSize) {
|
||||
this.packageSize = packageSize;
|
||||
}
|
||||
|
||||
public String getPackageHash() {
|
||||
return packageHash;
|
||||
}
|
||||
|
||||
public void setPackageHash(String packageHash) {
|
||||
this.packageHash = packageHash;
|
||||
}
|
||||
|
||||
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 getManifestSummaryJson() {
|
||||
return manifestSummaryJson;
|
||||
}
|
||||
|
||||
public void setManifestSummaryJson(String manifestSummaryJson) {
|
||||
this.manifestSummaryJson = manifestSummaryJson;
|
||||
}
|
||||
|
||||
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 getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
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_restore_task")
|
||||
public class ResourceRestoreTaskEntity extends BaseEntity {
|
||||
|
||||
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 consistencyReportJson;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
private String remark;
|
||||
private String createBy;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime finishTime;
|
||||
|
||||
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 getConsistencyReportJson() {
|
||||
return consistencyReportJson;
|
||||
}
|
||||
|
||||
public void setConsistencyReportJson(String consistencyReportJson) {
|
||||
this.consistencyReportJson = consistencyReportJson;
|
||||
}
|
||||
|
||||
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 getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 恢复任务控制阶段。
|
||||
*/
|
||||
public enum ResourceControlPhase {
|
||||
TMS_PRECHECK,
|
||||
HANDOFF_READY,
|
||||
RUNNER_OWNED,
|
||||
RECONCILE_PENDING,
|
||||
DONE
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源任务步骤状态。
|
||||
*/
|
||||
public enum ResourceStepStatus {
|
||||
PENDING,
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
SUCCESS_WITH_WARNINGS,
|
||||
FAILED,
|
||||
SKIPPED
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源备份/恢复任务主状态。
|
||||
*/
|
||||
public enum ResourceTaskStatus {
|
||||
PENDING,
|
||||
PRECHECKING,
|
||||
PRECHECK_FAILED,
|
||||
READY,
|
||||
HANDOFF,
|
||||
RUNNING,
|
||||
VERIFYING,
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.enums;
|
||||
|
||||
/**
|
||||
* 资源任务类型。
|
||||
*/
|
||||
public enum ResourceTaskType {
|
||||
RESOURCE_BACKUP,
|
||||
RESOURCE_RESTORE
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cisd.tms.modules.backup.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ResourceBackupTaskMapper extends BaseMapperX<ResourceBackupTaskEntity> {
|
||||
|
||||
@Select(
|
||||
"SELECT id, task_id, backup_id, status, product_type, mq_type, org_code, source_device_id, "
|
||||
+ "package_name, package_path, package_size, package_hash, storage_enc_key_fingerprint, "
|
||||
+ "authenticity_key_fingerprint, manifest_summary_json, error_code, error_message, remark, "
|
||||
+ "create_by, create_time, update_time, start_time, finish_time "
|
||||
+ "FROM tms_resource_backup_task WHERE task_id = #{taskId}"
|
||||
)
|
||||
ResourceBackupTaskEntity selectByTaskId(@Param("taskId") String taskId);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.cisd.tms.modules.backup.mapper;
|
||||
|
||||
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ResourceRestoreTaskMapper extends BaseMapperX<ResourceRestoreTaskEntity> {
|
||||
|
||||
@Select(
|
||||
"SELECT id, task_id, precheck_id, backup_id, status, source_device_id, source_product_type, "
|
||||
+ "target_product_type, package_file_id, package_path, staging_dir, runner_state_path, "
|
||||
+ "storage_enc_key_fingerprint, authenticity_key_fingerprint, restore_plan_json, "
|
||||
+ "consistency_report_json, error_code, error_message, remark, create_by, create_time, "
|
||||
+ "update_time, start_time, finish_time "
|
||||
+ "FROM tms_resource_restore_task WHERE task_id = #{taskId}"
|
||||
)
|
||||
ResourceRestoreTaskEntity selectByTaskId(@Param("taskId") String taskId);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
final class ResourcePackageFormatSupport {
|
||||
|
||||
private ResourcePackageFormatSupport() {
|
||||
}
|
||||
|
||||
static byte[] buildSignatureSource(byte[] manifestBytes, byte[] envelopeBytes, byte[] cipherBytes) {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
writePart(outputStream, manifestBytes);
|
||||
writePart(outputStream, envelopeBytes);
|
||||
writePart(outputStream, cipherBytes);
|
||||
return outputStream.toByteArray();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to build package signature source", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writePart(ByteArrayOutputStream outputStream, byte[] data) throws IOException {
|
||||
byte[] safeData = data == null ? new byte[0] : data;
|
||||
outputStream.write(ByteBuffer.allocate(4).putInt(safeData.length).array());
|
||||
outputStream.write(safeData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ResourcePackageInspectResult {
|
||||
|
||||
private Map<String, Object> manifest;
|
||||
private Map<String, Object> envelope;
|
||||
private Map<String, Object> payload;
|
||||
private byte[] payloadArchiveBytes;
|
||||
private byte[] signature;
|
||||
|
||||
public Map<String, Object> getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public void setManifest(Map<String, Object> manifest) {
|
||||
this.manifest = manifest;
|
||||
}
|
||||
|
||||
public Map<String, Object> getEnvelope() {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
public void setEnvelope(Map<String, Object> envelope) {
|
||||
this.envelope = envelope;
|
||||
}
|
||||
|
||||
public Map<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public byte[] getPayloadArchiveBytes() {
|
||||
return payloadArchiveBytes;
|
||||
}
|
||||
|
||||
public void setPayloadArchiveBytes(byte[] payloadArchiveBytes) {
|
||||
this.payloadArchiveBytes = payloadArchiveBytes;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
public void setSignature(byte[] signature) {
|
||||
this.signature = signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
public class ResourcePackageResult {
|
||||
|
||||
private String packageName;
|
||||
private String packagePath;
|
||||
private Long packageSize;
|
||||
private String packageHash;
|
||||
private String storageEncKeyFingerprint;
|
||||
private String authenticityKeyFingerprint;
|
||||
private String manifestSummaryJson;
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public Long getPackageSize() {
|
||||
return packageSize;
|
||||
}
|
||||
|
||||
public void setPackageSize(Long packageSize) {
|
||||
this.packageSize = packageSize;
|
||||
}
|
||||
|
||||
public String getPackageHash() {
|
||||
return packageHash;
|
||||
}
|
||||
|
||||
public void setPackageHash(String packageHash) {
|
||||
this.packageHash = packageHash;
|
||||
}
|
||||
|
||||
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 getManifestSummaryJson() {
|
||||
return manifestSummaryJson;
|
||||
}
|
||||
|
||||
public void setManifestSummaryJson(String manifestSummaryJson) {
|
||||
this.manifestSummaryJson = manifestSummaryJson;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourcePackageService {
|
||||
|
||||
ResourcePackageResult createPackage(
|
||||
String taskId,
|
||||
String backupId,
|
||||
InitTaskEntity initTask,
|
||||
List<DiscoveredResourceItem> discoveredItems,
|
||||
CreateResourceBackupRequest request
|
||||
);
|
||||
|
||||
ResourcePackageInspectResult inspectPackageHeader(Path packagePath);
|
||||
|
||||
ResourcePackageInspectResult inspectPackage(Path packagePath);
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoService;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceSealedPayload;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceEnvelopeCryptoService resourceEnvelopeCryptoService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ResourcePackageServiceImpl(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ResourceEnvelopeCryptoService resourceEnvelopeCryptoService,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.resourceEnvelopeCryptoService = resourceEnvelopeCryptoService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePackageResult createPackage(
|
||||
String taskId,
|
||||
String backupId,
|
||||
InitTaskEntity initTask,
|
||||
List<DiscoveredResourceItem> discoveredItems,
|
||||
CreateResourceBackupRequest request
|
||||
) {
|
||||
if (isBlank(taskId) || isBlank(backupId)) {
|
||||
throw new IllegalArgumentException("taskId and backupId are required");
|
||||
}
|
||||
if (initTask == null) {
|
||||
throw new IllegalArgumentException("initTask is required");
|
||||
}
|
||||
if (discoveredItems == null) {
|
||||
throw new IllegalArgumentException("discoveredItems is required");
|
||||
}
|
||||
try {
|
||||
Path taskDir = Path.of(resourceBackupProperties.getOutputDir()).resolve(taskId).normalize();
|
||||
Files.createDirectories(taskDir);
|
||||
|
||||
// 资源恢复真正消费的是“受保护的内部归档”,而不是一段摘要 JSON。
|
||||
// 这里先把可采集的文件资源打成 payload.zip,再统一交给数字信封加密,
|
||||
// 后续即使补数据库导出/MQ definitions,也仍然沿着这个边界扩展。
|
||||
PayloadArchiveBuildResult payloadArchive = buildPayloadArchive(
|
||||
taskId,
|
||||
backupId,
|
||||
initTask,
|
||||
discoveredItems,
|
||||
request
|
||||
);
|
||||
ResourceSealedPayload sealedPayload = resourceEnvelopeCryptoService.seal(payloadArchive.archiveBytes());
|
||||
|
||||
// manifest 只保留恢复前需要快速识别的元数据,不直接展开敏感资源内容。
|
||||
Map<String, Object> manifest = new LinkedHashMap<>();
|
||||
manifest.put("backupId", backupId);
|
||||
manifest.put("taskId", taskId);
|
||||
manifest.put("backupTime", LocalDateTime.now().toString());
|
||||
manifest.put("productType", initTask.getProductType());
|
||||
manifest.put("mqType", initTask.getMqType());
|
||||
manifest.put("orgCode", initTask.getOrgCode());
|
||||
manifest.put("resourceCount", discoveredItems.size());
|
||||
manifest.put("collectedResourceCount", payloadArchive.collectedResourceCount());
|
||||
manifest.put("storageEncKeyFingerprint", sealedPayload.getStorageEncKeyFingerprint());
|
||||
manifest.put("authenticityKeyFingerprint", sealedPayload.getAuthenticityKeyFingerprint());
|
||||
byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest);
|
||||
byte[] envelopeBytes = objectMapper.writeValueAsBytes(sealedPayload.getEnvelope());
|
||||
// 签名覆盖 manifest + envelope + ciphertext 三段,恢复端按同一字节拼装规则重建验签输入。
|
||||
byte[] signatureBytes = resourceEnvelopeCryptoService.sign(
|
||||
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, sealedPayload.getCipherBytes())
|
||||
);
|
||||
|
||||
String packageName = backupId + ".tmsbak";
|
||||
Path packagePath = taskDir.resolve(packageName);
|
||||
try (OutputStream outputStream = Files.newOutputStream(packagePath);
|
||||
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
|
||||
// 当前包结构已经固定成 manifest + envelope + ciphertext,后续即使替换加密实现也不必重做外层格式。
|
||||
addZipEntry(zipOutputStream, "manifest.json", manifestBytes);
|
||||
addZipEntry(zipOutputStream, "envelope.json", envelopeBytes);
|
||||
addZipEntry(zipOutputStream, "payload.enc", sealedPayload.getCipherBytes());
|
||||
addZipEntry(zipOutputStream, "signature.sig", signatureBytes);
|
||||
}
|
||||
|
||||
Map<String, Object> manifestSummary = new LinkedHashMap<>();
|
||||
manifestSummary.put("resourceCount", discoveredItems.size());
|
||||
manifestSummary.put("collectedResourceCount", payloadArchive.collectedResourceCount());
|
||||
manifestSummary.put("productType", initTask.getProductType());
|
||||
manifestSummary.put("mqType", initTask.getMqType());
|
||||
manifestSummary.put("orgCode", initTask.getOrgCode());
|
||||
manifestSummary.put("packageName", packageName);
|
||||
|
||||
ResourcePackageResult result = new ResourcePackageResult();
|
||||
result.setPackageName(packageName);
|
||||
result.setPackagePath(packagePath.toString());
|
||||
result.setPackageSize(Files.size(packagePath));
|
||||
// 这里记录的是整个备份包文件的 SM3,方便后续下载校验和审计追踪。
|
||||
result.setPackageHash(sm3Hex(Files.readAllBytes(packagePath)));
|
||||
result.setStorageEncKeyFingerprint(sealedPayload.getStorageEncKeyFingerprint());
|
||||
result.setAuthenticityKeyFingerprint(sealedPayload.getAuthenticityKeyFingerprint());
|
||||
result.setManifestSummaryJson(objectMapper.writeValueAsString(manifestSummary));
|
||||
return result;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to create backup package", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePackageInspectResult inspectPackageHeader(Path packagePath) {
|
||||
if (packagePath == null) {
|
||||
throw new IllegalArgumentException("packagePath is required");
|
||||
}
|
||||
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
|
||||
byte[] manifestBytes = readRequiredEntry(zipFile, "manifest.json");
|
||||
byte[] envelopeBytes = readRequiredEntry(zipFile, "envelope.json");
|
||||
byte[] cipherBytes = readRequiredEntry(zipFile, "payload.enc");
|
||||
byte[] signatureBytes = readRequiredEntry(zipFile, "signature.sig");
|
||||
resourceEnvelopeCryptoService.verify(
|
||||
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, cipherBytes),
|
||||
signatureBytes
|
||||
);
|
||||
ResourcePackageInspectResult result = new ResourcePackageInspectResult();
|
||||
result.setManifest(objectMapper.readValue(manifestBytes, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
result.setEnvelope(objectMapper.readValue(envelopeBytes, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
result.setSignature(signatureBytes);
|
||||
return result;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to inspect backup package header", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourcePackageInspectResult inspectPackage(Path packagePath) {
|
||||
if (packagePath == null) {
|
||||
throw new IllegalArgumentException("packagePath is required");
|
||||
}
|
||||
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
|
||||
byte[] manifestBytes = readRequiredEntry(zipFile, "manifest.json");
|
||||
byte[] envelopeBytes = readRequiredEntry(zipFile, "envelope.json");
|
||||
byte[] cipherBytes = readRequiredEntry(zipFile, "payload.enc");
|
||||
byte[] signatureBytes = readRequiredEntry(zipFile, "signature.sig");
|
||||
byte[] payloadBytes = resourceEnvelopeCryptoService.unsealAndVerify(
|
||||
objectMapper.readValue(envelopeBytes, new TypeReference<Map<String, Object>>() {
|
||||
}),
|
||||
cipherBytes,
|
||||
ResourcePackageFormatSupport.buildSignatureSource(manifestBytes, envelopeBytes, cipherBytes),
|
||||
signatureBytes
|
||||
);
|
||||
Map<String, Object> payloadManifest = readPayloadManifest(payloadBytes);
|
||||
|
||||
ResourcePackageInspectResult result = new ResourcePackageInspectResult();
|
||||
result.setManifest(objectMapper.readValue(manifestBytes, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
result.setEnvelope(objectMapper.readValue(envelopeBytes, new TypeReference<HashMap<String, Object>>() {
|
||||
}));
|
||||
result.setPayload(payloadManifest);
|
||||
result.setPayloadArchiveBytes(payloadBytes);
|
||||
result.setSignature(signatureBytes);
|
||||
return result;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to inspect backup package", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private PayloadArchiveBuildResult buildPayloadArchive(
|
||||
String taskId,
|
||||
String backupId,
|
||||
InitTaskEntity initTask,
|
||||
List<DiscoveredResourceItem> discoveredItems,
|
||||
CreateResourceBackupRequest request
|
||||
) throws IOException {
|
||||
Map<String, Object> payloadManifest = new LinkedHashMap<>();
|
||||
payloadManifest.put("archiveFormat", "ZIP");
|
||||
payloadManifest.put("backupId", backupId);
|
||||
payloadManifest.put("taskId", taskId);
|
||||
payloadManifest.put("productType", initTask.getProductType());
|
||||
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;
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
|
||||
for (DiscoveredResourceItem item : discoveredItems) {
|
||||
Map<String, Object> resourceEntry = new LinkedHashMap<>();
|
||||
resourceEntry.put("resourceType", item.getResourceType());
|
||||
resourceEntry.put("resourceCode", item.getResourceCode());
|
||||
resourceEntry.put("resolvedPath", item.getResolvedPath());
|
||||
resourceEntry.put("required", item.isRequired());
|
||||
|
||||
int collectedFiles = collectResourceItem(zipOutputStream, item, resourceEntry);
|
||||
if (collectedFiles > 0) {
|
||||
collectedCount++;
|
||||
}
|
||||
resourceEntries.add(resourceEntry);
|
||||
}
|
||||
payloadManifest.put("resources", resourceEntries);
|
||||
addZipEntry(
|
||||
zipOutputStream,
|
||||
"payload-manifest.json",
|
||||
objectMapper.writeValueAsBytes(payloadManifest)
|
||||
);
|
||||
}
|
||||
return new PayloadArchiveBuildResult(outputStream.toByteArray(), collectedCount);
|
||||
}
|
||||
|
||||
private int collectResourceItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
if (!"FILE".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
return 0;
|
||||
}
|
||||
Path resolvedPath = toResolvedPath(item.getResolvedPath());
|
||||
if (resolvedPath == null || !Files.exists(resolvedPath)) {
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "PATH_NOT_FOUND");
|
||||
return 0;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> archivedFiles = new ArrayList<>();
|
||||
if (Files.isRegularFile(resolvedPath)) {
|
||||
archivedFiles.add(writeRegularFileEntry(zipOutputStream, item, resolvedPath, resolvedPath.getFileName().toString()));
|
||||
} else if (Files.isDirectory(resolvedPath)) {
|
||||
// 目录资源恢复时真正需要的是内部文件集合,而不是目录本身的绝对路径。
|
||||
// 这里把目录拍平成 zip entry,并把相对路径写进 payload-manifest,后续恢复端按 manifest 还原。
|
||||
try (Stream<Path> pathStream = Files.walk(resolvedPath)) {
|
||||
pathStream
|
||||
.filter(Files::isRegularFile)
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.forEach(path -> archivedFiles.add(writeUnchecked(
|
||||
() -> writeRegularFileEntry(
|
||||
zipOutputStream,
|
||||
item,
|
||||
path,
|
||||
resolvedPath.relativize(path).toString()
|
||||
)
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "UNSUPPORTED_FILE_TYPE");
|
||||
return 0;
|
||||
}
|
||||
|
||||
resourceEntry.put("collected", !archivedFiles.isEmpty());
|
||||
resourceEntry.put("fileCount", archivedFiles.size());
|
||||
resourceEntry.put("files", archivedFiles);
|
||||
if (archivedFiles.size() == 1) {
|
||||
resourceEntry.put("entryPath", archivedFiles.get(0).get("entryPath"));
|
||||
}
|
||||
return archivedFiles.size();
|
||||
}
|
||||
|
||||
private Map<String, Object> writeRegularFileEntry(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
Path file,
|
||||
String relativeName
|
||||
) throws IOException {
|
||||
byte[] content = Files.readAllBytes(file);
|
||||
String entryPath = buildArchiveEntryPath(item, relativeName);
|
||||
addZipEntry(zipOutputStream, entryPath, content);
|
||||
|
||||
Map<String, Object> fileEntry = new LinkedHashMap<>();
|
||||
fileEntry.put("entryPath", entryPath);
|
||||
fileEntry.put("sourcePath", file.toString());
|
||||
fileEntry.put("relativeName", relativeName.replace('\\', '/'));
|
||||
fileEntry.put("size", content.length);
|
||||
fileEntry.put("contentSm3", sm3Hex(content));
|
||||
return fileEntry;
|
||||
}
|
||||
|
||||
private Map<String, Object> readPayloadManifest(byte[] payloadArchiveBytes) throws IOException {
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
if ("payload-manifest.json".equals(entry.getName())) {
|
||||
return objectMapper.readValue(zipInputStream.readAllBytes(), new TypeReference<HashMap<String, Object>>() {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("payload archive entry missing: payload-manifest.json");
|
||||
}
|
||||
|
||||
private static String buildArchiveEntryPath(DiscoveredResourceItem item, String relativeName) {
|
||||
String code = trim(item.getResourceCode()).isEmpty() ? "UNKNOWN" : trim(item.getResourceCode());
|
||||
String normalizedRelativeName = trim(relativeName).replace('\\', '/');
|
||||
if (normalizedRelativeName.isEmpty()) {
|
||||
normalizedRelativeName = "content.bin";
|
||||
}
|
||||
return "resources/" + code + "/" + normalizedRelativeName;
|
||||
}
|
||||
|
||||
private static Path toResolvedPath(String path) {
|
||||
String value = trim(path);
|
||||
if (value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Path.of(value).normalize();
|
||||
}
|
||||
|
||||
private static <T> T writeUnchecked(CheckedSupplier<T> supplier) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write payload archive entry", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addZipEntry(ZipOutputStream zipOutputStream, String entryName, byte[] content) throws IOException {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(entryName));
|
||||
zipOutputStream.write(content);
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
|
||||
private static byte[] readRequiredEntry(ZipFile zipFile, String entryName) throws IOException {
|
||||
ZipEntry entry = zipFile.getEntry(entryName);
|
||||
if (entry == null) {
|
||||
throw new IllegalStateException("backup package entry missing: " + entryName);
|
||||
}
|
||||
try (InputStream inputStream = zipFile.getInputStream(entry)) {
|
||||
return inputStream.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private static String sm3Hex(byte[] data) {
|
||||
SM3Digest digest = new SM3Digest();
|
||||
digest.update(data, 0, data.length);
|
||||
byte[] out = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(out, 0);
|
||||
StringBuilder builder = new StringBuilder(out.length * 2);
|
||||
for (byte item : out) {
|
||||
builder.append(String.format("%02x", item));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface CheckedSupplier<T> {
|
||||
T get() throws IOException;
|
||||
}
|
||||
|
||||
private record PayloadArchiveBuildResult(byte[] archiveBytes, int collectedResourceCount) {
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ResourceBackupTaskRepository {
|
||||
|
||||
void save(ResourceBackupTaskEntity entity);
|
||||
|
||||
Optional<ResourceBackupTaskEntity> findByTaskId(String taskId);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ResourceRestoreTaskRepository {
|
||||
|
||||
void save(ResourceRestoreTaskEntity entity);
|
||||
|
||||
void update(ResourceRestoreTaskEntity entity);
|
||||
|
||||
Optional<ResourceRestoreTaskEntity> findByTaskId(String taskId);
|
||||
|
||||
List<ResourceRestoreTaskEntity> findAll();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
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);
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.cisd.tms.modules.backup.repository.impl;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceBackupTaskMapper;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ResourceBackupTaskRepositoryImpl implements ResourceBackupTaskRepository {
|
||||
|
||||
private final ResourceBackupTaskMapper resourceBackupTaskMapper;
|
||||
|
||||
public ResourceBackupTaskRepositoryImpl(ResourceBackupTaskMapper resourceBackupTaskMapper) {
|
||||
this.resourceBackupTaskMapper = resourceBackupTaskMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourceBackupTaskEntity entity) {
|
||||
resourceBackupTaskMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourceBackupTaskEntity> findByTaskId(String taskId) {
|
||||
return Optional.ofNullable(resourceBackupTaskMapper.selectByTaskId(taskId));
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.cisd.tms.modules.backup.repository.impl;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceRestoreTaskMapper;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ResourceRestoreTaskRepositoryImpl implements ResourceRestoreTaskRepository {
|
||||
|
||||
private final ResourceRestoreTaskMapper resourceRestoreTaskMapper;
|
||||
|
||||
public ResourceRestoreTaskRepositoryImpl(ResourceRestoreTaskMapper resourceRestoreTaskMapper) {
|
||||
this.resourceRestoreTaskMapper = resourceRestoreTaskMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourceRestoreTaskEntity entity) {
|
||||
resourceRestoreTaskMapper.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(ResourceRestoreTaskEntity entity) {
|
||||
resourceRestoreTaskMapper.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourceRestoreTaskEntity> findByTaskId(String taskId) {
|
||||
return Optional.ofNullable(resourceRestoreTaskMapper.selectByTaskId(taskId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceRestoreTaskEntity> findAll() {
|
||||
List<ResourceRestoreTaskEntity> tasks = resourceRestoreTaskMapper.selectList(null);
|
||||
return tasks == null ? List.of() : tasks;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AsyncResourceRestoreRunnerLauncher implements ResourceRestoreRunnerLauncher {
|
||||
|
||||
private final ResourceRestoreRunner resourceRestoreRunner;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public AsyncResourceRestoreRunnerLauncher(
|
||||
ResourceRestoreRunner resourceRestoreRunner,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.resourceRestoreRunner = resourceRestoreRunner;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(Path handoffFile) {
|
||||
if (!resourceBackupProperties.getRunnerAutoLaunchEnabled()) {
|
||||
return;
|
||||
}
|
||||
Thread thread = new Thread(() -> resourceRestoreRunner.run(handoffFile), "resource-restore-runner");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public PayloadArchiveRunnerStepExecutor(
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourcePackageService resourcePackageService,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.handoffFileStore = handoffFileStore;
|
||||
this.resourcePackageService = resourcePackageService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "RESTORE_FILES".equals(stepCode) || "CONSISTENCY_VERIFY".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
if ("RESTORE_FILES".equals(context.stepCode())) {
|
||||
return restoreFiles(context);
|
||||
}
|
||||
if ("CONSISTENCY_VERIFY".equals(context.stepCode())) {
|
||||
return verifyExtractedPayload(context);
|
||||
}
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
|
||||
private RunnerStepExecutionResult restoreFiles(RunnerStepContext context) {
|
||||
ResourceRestoreHandoffSnapshot handoff = readRequiredHandoff(context.handoffFile());
|
||||
Path packagePath = resolveRequiredPackagePath(handoff);
|
||||
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackage(packagePath);
|
||||
byte[] payloadArchiveBytes = Optional.ofNullable(inspectResult.getPayloadArchiveBytes())
|
||||
.orElseThrow(() -> new IllegalStateException("payload archive bytes missing"));
|
||||
Path payloadDir = context.taskDir().resolve("staging").resolve("payload").normalize();
|
||||
extractPayloadArchive(payloadArchiveBytes, payloadDir);
|
||||
|
||||
int resourceCount = readResourceCount(payloadDir.resolve("payload-manifest.json"));
|
||||
return RunnerStepExecutionResult.success("payload extracted to staging, resourceCount=" + resourceCount);
|
||||
}
|
||||
|
||||
private RunnerStepExecutionResult verifyExtractedPayload(RunnerStepContext context) {
|
||||
Path manifestFile = context.taskDir().resolve("staging").resolve("payload").resolve("payload-manifest.json").normalize();
|
||||
if (!Files.isRegularFile(manifestFile)) {
|
||||
throw new IllegalStateException("payload manifest missing: " + manifestFile);
|
||||
}
|
||||
int resourceCount = readResourceCount(manifestFile);
|
||||
return RunnerStepExecutionResult.success("payload manifest verified, resourceCount=" + resourceCount);
|
||||
}
|
||||
|
||||
private ResourceRestoreHandoffSnapshot readRequiredHandoff(Path handoffFile) {
|
||||
return handoffFileStore.read(handoffFile)
|
||||
.orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile));
|
||||
}
|
||||
|
||||
private Path resolveRequiredPackagePath(ResourceRestoreHandoffSnapshot handoff) {
|
||||
String packagePath = trim(handoff.getPackagePath());
|
||||
if (packagePath.isEmpty()) {
|
||||
throw new IllegalStateException("restore handoff packagePath is empty");
|
||||
}
|
||||
Path resolved = Path.of(packagePath).normalize();
|
||||
if (!Files.isRegularFile(resolved)) {
|
||||
throw new IllegalStateException("restore package file not found: " + resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void extractPayloadArchive(byte[] payloadArchiveBytes, Path payloadDir) {
|
||||
try {
|
||||
Files.createDirectories(payloadDir);
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
Path target = resolveSafePayloadTarget(payloadDir, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(target);
|
||||
continue;
|
||||
}
|
||||
Files.createDirectories(target.getParent());
|
||||
// 备份包来自外部输入,即使已经验签,也要防御 zip-slip,确保只落到 staging/payload 下。
|
||||
Files.write(target, readAllBytes(zipInputStream));
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to extract payload archive", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveSafePayloadTarget(Path payloadDir, String entryName) {
|
||||
Path target = payloadDir.resolve(entryName).normalize();
|
||||
if (!target.startsWith(payloadDir)) {
|
||||
throw new IllegalStateException("payload archive entry escapes staging dir: " + entryName);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private int readResourceCount(Path manifestFile) {
|
||||
try {
|
||||
Map<String, Object> manifest = objectMapper.readValue(manifestFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
Object resourcesValue = manifest.get("resources");
|
||||
if (resourcesValue instanceof List<?> resources) {
|
||||
return resources.size();
|
||||
}
|
||||
return 0;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read payload manifest: " + manifestFile, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
|
||||
return inputStream.readAllBytes();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ProcessBuilderRunnerCommandInvoker implements RunnerCommandInvoker {
|
||||
|
||||
@Override
|
||||
public int run(List<String> command, Path logFile, Path workingDirectory) {
|
||||
try {
|
||||
if (logFile != null && logFile.getParent() != null) {
|
||||
Files.createDirectories(logFile.getParent());
|
||||
}
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
if (workingDirectory != null) {
|
||||
Files.createDirectories(workingDirectory);
|
||||
pb.directory(workingDirectory.toFile());
|
||||
}
|
||||
pb.redirectErrorStream(true);
|
||||
if (logFile != null) {
|
||||
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(logFile.toFile()));
|
||||
}
|
||||
Process process = pb.start();
|
||||
return process.waitFor();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to start runner command: " + String.join(" ", command), ex);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("runner command interrupted: " + String.join(" ", command), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
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.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourceRestoreRunner {
|
||||
|
||||
private static final String INITIAL_STEP_CODE = "RESTORE_LOCK";
|
||||
private static final int INITIAL_STEP_NO = 1;
|
||||
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final List<RunnerStepExecutor> runnerStepExecutors;
|
||||
|
||||
public ResourceRestoreRunner(
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ObjectMapper objectMapper,
|
||||
List<RunnerStepExecutor> runnerStepExecutors
|
||||
) {
|
||||
this.handoffFileStore = handoffFileStore;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.objectMapper = objectMapper;
|
||||
this.runnerStepExecutors = runnerStepExecutors;
|
||||
}
|
||||
|
||||
public void run(Path handoffFile) {
|
||||
ResourceRestoreHandoffSnapshot handoff = handoffFileStore.read(handoffFile)
|
||||
.orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile));
|
||||
Path stateFile = Path.of(handoff.getRunnerStatePath()).normalize();
|
||||
String runnerInstanceId = nextRunnerInstanceId();
|
||||
long runnerPid = ProcessHandle.current().pid();
|
||||
resourceRestoreStateFileStore.claimOwnership(stateFile, runnerInstanceId, runnerPid, INITIAL_STEP_CODE, INITIAL_STEP_NO);
|
||||
|
||||
List<String> restorePlan = readRestorePlan(handoff.getRestorePlanJson());
|
||||
try {
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
String stepCode = restorePlan.get(i);
|
||||
int stepNo = i + 1;
|
||||
if (INITIAL_STEP_CODE.equals(stepCode)) {
|
||||
continue;
|
||||
}
|
||||
resourceRestoreStateFileStore.markStepRunning(stateFile, stepCode, stepNo, runnerInstanceId);
|
||||
RunnerStepExecutor executor = resolveExecutor(stepCode);
|
||||
RunnerStepContext context = new RunnerStepContext(
|
||||
handoff.getTaskId(),
|
||||
stepCode,
|
||||
stepNo,
|
||||
handoffFile.getParent(),
|
||||
handoffFile,
|
||||
stateFile
|
||||
);
|
||||
RunnerStepExecutionResult result = executor.execute(context);
|
||||
writeStepOutput(context, result);
|
||||
resourceRestoreStateFileStore.touchHeartbeat(stateFile, runnerInstanceId);
|
||||
}
|
||||
String lastStepCode = restorePlan.isEmpty() ? INITIAL_STEP_CODE : restorePlan.get(restorePlan.size() - 1);
|
||||
int lastStepNo = restorePlan.isEmpty() ? INITIAL_STEP_NO : restorePlan.size();
|
||||
resourceRestoreStateFileStore.markReconcilePending(stateFile, "SUCCESS", null, null, lastStepCode, lastStepNo, runnerInstanceId);
|
||||
} catch (RuntimeException ex) {
|
||||
int failedStepNo = detectFailedStepNo(handoff, stateFile);
|
||||
String failedStepCode = detectFailedStepCode(handoff, stateFile);
|
||||
writeStepFailureOutput(handoffFile.getParent(), failedStepCode, failedStepNo, ex);
|
||||
resourceRestoreStateFileStore.markReconcilePending(
|
||||
stateFile,
|
||||
"FAILED",
|
||||
"RUNNER_EXECUTION_FAILED",
|
||||
ex.getMessage(),
|
||||
failedStepCode,
|
||||
failedStepNo,
|
||||
runnerInstanceId
|
||||
);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private String nextRunnerInstanceId() {
|
||||
return "RRUN-" + UUID.randomUUID();
|
||||
}
|
||||
|
||||
private List<String> readRestorePlan(String restorePlanJson) {
|
||||
if (restorePlanJson == null || restorePlanJson.isBlank()) {
|
||||
return List.of(INITIAL_STEP_CODE);
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(restorePlanJson, new TypeReference<List<String>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to parse restore plan json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private RunnerStepExecutor resolveExecutor(String stepCode) {
|
||||
return runnerStepExecutors.stream()
|
||||
.filter(executor -> executor.supports(stepCode))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("no runner step executor for step: " + stepCode));
|
||||
}
|
||||
|
||||
private int detectFailedStepNo(ResourceRestoreHandoffSnapshot handoff, Path stateFile) {
|
||||
return resourceRestoreStateFileStore.read(stateFile)
|
||||
.map(snapshot -> snapshot.getCurrentStepNo() == null ? INITIAL_STEP_NO : snapshot.getCurrentStepNo())
|
||||
.orElse(INITIAL_STEP_NO);
|
||||
}
|
||||
|
||||
private String detectFailedStepCode(ResourceRestoreHandoffSnapshot handoff, Path stateFile) {
|
||||
return resourceRestoreStateFileStore.read(stateFile)
|
||||
.map(snapshot -> snapshot.getCurrentStepCode() == null || snapshot.getCurrentStepCode().isBlank()
|
||||
? INITIAL_STEP_CODE
|
||||
: snapshot.getCurrentStepCode())
|
||||
.orElse(INITIAL_STEP_CODE);
|
||||
}
|
||||
|
||||
private void writeStepOutput(RunnerStepContext context, RunnerStepExecutionResult result) {
|
||||
try {
|
||||
Path outputDir = context.taskDir().resolve("step-output");
|
||||
Files.createDirectories(outputDir);
|
||||
Path outputFile = outputDir.resolve(String.format("%02d-%s.json", context.stepNo(), context.stepCode()));
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("stepCode", context.stepCode());
|
||||
json.put("result", result.getResult());
|
||||
json.put("summary", result.getSummary());
|
||||
objectMapper.writeValue(outputFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write step output for " + context.stepCode(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeStepFailureOutput(Path taskDir, String stepCode, int stepNo, RuntimeException ex) {
|
||||
try {
|
||||
Path outputDir = taskDir.resolve("step-output");
|
||||
Files.createDirectories(outputDir);
|
||||
Path outputFile = outputDir.resolve(String.format("%02d-%s.json", stepNo, stepCode));
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("stepCode", stepCode);
|
||||
json.put("result", "FAILED");
|
||||
json.put("summary", ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage());
|
||||
objectMapper.writeValue(outputFile.toFile(), json);
|
||||
} catch (IOException ioException) {
|
||||
throw new IllegalStateException("failed to write failed step output for " + stepCode, ioException);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface ResourceRestoreRunnerLauncher {
|
||||
|
||||
void launch(Path handoffFile);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RestoreStateReconciler {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final Supplier<LocalDateTime> nowSupplier;
|
||||
|
||||
public RestoreStateReconciler(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore
|
||||
) {
|
||||
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, LocalDateTime::now);
|
||||
}
|
||||
|
||||
RestoreStateReconciler(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
Supplier<LocalDateTime> nowSupplier
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.nowSupplier = nowSupplier;
|
||||
}
|
||||
|
||||
public void reconcileOnce() {
|
||||
for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) {
|
||||
if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
java.nio.file.Path stateFile = java.nio.file.Path.of(entity.getRunnerStatePath()).normalize();
|
||||
resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> {
|
||||
if (!"RECONCILE_PENDING".equals(snapshot.getControlPhase())) {
|
||||
return;
|
||||
}
|
||||
entity.setStatus(snapshot.getStatus());
|
||||
entity.setErrorCode(blankToNull(snapshot.getErrorCode()));
|
||||
entity.setErrorMessage(blankToNull(snapshot.getErrorMessage()));
|
||||
entity.setFinishTime(nowSupplier.get());
|
||||
entity.setUpdateTime(nowSupplier.get());
|
||||
resourceRestoreTaskRepository.update(entity);
|
||||
resourceRestoreStateFileStore.markDone(stateFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static String blankToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface RunnerCommandInvoker {
|
||||
|
||||
int run(List<String> command, Path logFile, Path workingDirectory);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RunnerStallDetector {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final Supplier<LocalDateTime> nowSupplier;
|
||||
|
||||
public RunnerStallDetector(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, resourceBackupProperties, LocalDateTime::now);
|
||||
}
|
||||
|
||||
RunnerStallDetector(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
Supplier<LocalDateTime> nowSupplier
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.nowSupplier = nowSupplier;
|
||||
}
|
||||
|
||||
public void detectOnce() {
|
||||
for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) {
|
||||
if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
Path stateFile = Path.of(entity.getRunnerStatePath()).normalize();
|
||||
resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> {
|
||||
if (!"RUNNER_OWNED".equals(snapshot.getControlPhase())) {
|
||||
return;
|
||||
}
|
||||
if (snapshot.getLastHeartbeatAt() == null || snapshot.getLastHeartbeatAt().isBlank()) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime heartbeat = LocalDateTime.parse(snapshot.getLastHeartbeatAt());
|
||||
if (Duration.between(heartbeat, nowSupplier.get()).getSeconds() <= resourceBackupProperties.getRunnerHeartbeatTimeoutSeconds()) {
|
||||
return;
|
||||
}
|
||||
resourceRestoreStateFileStore.markStalled(stateFile, "RUNNER_STALLED", "restore runner heartbeat timed out");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record RunnerStepContext(
|
||||
String taskId,
|
||||
String stepCode,
|
||||
int stepNo,
|
||||
Path taskDir,
|
||||
Path handoffFile,
|
||||
Path stateFile
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
public class RunnerStepExecutionResult {
|
||||
|
||||
private String result;
|
||||
private String summary;
|
||||
|
||||
public static RunnerStepExecutionResult success(String summary) {
|
||||
RunnerStepExecutionResult result = new RunnerStepExecutionResult();
|
||||
result.setResult("SUCCESS");
|
||||
result.setSummary(summary);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static RunnerStepExecutionResult failed(String summary) {
|
||||
RunnerStepExecutionResult result = new RunnerStepExecutionResult();
|
||||
result.setResult("FAILED");
|
||||
result.setSummary(summary);
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(String result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
public interface RunnerStepExecutor {
|
||||
|
||||
boolean supports(String stepCode);
|
||||
|
||||
RunnerStepExecutionResult execute(RunnerStepContext context);
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ServiceCommandRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final InitExecutorProperties initExecutorProperties;
|
||||
private final RunnerCommandInvoker runnerCommandInvoker;
|
||||
|
||||
public ServiceCommandRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
InitExecutorProperties initExecutorProperties,
|
||||
RunnerCommandInvoker runnerCommandInvoker
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.initExecutorProperties = initExecutorProperties;
|
||||
this.runnerCommandInvoker = runnerCommandInvoker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "STOP_SERVICES".equals(stepCode) || "START_SERVICES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
if (!supports(context.stepCode())) {
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
List<List<String>> commands = resolveCommands(context.stepCode());
|
||||
int executedCount = 0;
|
||||
for (List<String> command : commands) {
|
||||
int exitCode = runnerCommandInvoker.run(command, resolveLogFile(context), context.taskDir());
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("runner step " + context.stepCode() + " failed, exitCode=" + exitCode);
|
||||
}
|
||||
executedCount++;
|
||||
}
|
||||
return RunnerStepExecutionResult.success("executed " + executedCount + " command(s)");
|
||||
}
|
||||
|
||||
private List<List<String>> resolveCommands(String stepCode) {
|
||||
return switch (stepCode) {
|
||||
case "STOP_SERVICES" -> buildStopCommands();
|
||||
case "START_SERVICES" -> buildStartCommands();
|
||||
default -> throw new IllegalArgumentException("unsupported runner step: " + stepCode);
|
||||
};
|
||||
}
|
||||
|
||||
private List<List<String>> buildStopCommands() {
|
||||
List<List<String>> commands = new ArrayList<>();
|
||||
commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "stop"));
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardAppStopCommand());
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardRabbitStopCommand());
|
||||
return commands;
|
||||
}
|
||||
|
||||
private List<List<String>> buildStartCommands() {
|
||||
List<List<String>> commands = new ArrayList<>();
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardAppStartCommand());
|
||||
commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "start"));
|
||||
return commands;
|
||||
}
|
||||
|
||||
private void appendShellCommand(List<List<String>> commands, String shellCommand) {
|
||||
if (shellCommand == null || shellCommand.isBlank()) {
|
||||
return;
|
||||
}
|
||||
commands.add(List.of("/bin/zsh", "-lc", shellCommand.trim()));
|
||||
}
|
||||
|
||||
private Path resolveLogFile(RunnerStepContext context) {
|
||||
return context.taskDir().resolve("steps").resolve(String.format("%02d-%s.log", context.stepNo(), context.stepCode()));
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.cisd.tms.modules.backup.runner.model;
|
||||
|
||||
public class ResourceRestoreHandoffSnapshot {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String packageFileId;
|
||||
private String packagePath;
|
||||
private String stagingDir;
|
||||
private String runnerStatePath;
|
||||
private String restorePlanJson;
|
||||
|
||||
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 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 getRestorePlanJson() {
|
||||
return restorePlanJson;
|
||||
}
|
||||
|
||||
public void setRestorePlanJson(String restorePlanJson) {
|
||||
this.restorePlanJson = restorePlanJson;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
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.support.ResourceBackupDownloadResult;
|
||||
import java.util.List;
|
||||
|
||||
public interface ResourceBackupService {
|
||||
|
||||
ResourceKeysetStatusResponse getKeysetStatus();
|
||||
|
||||
CreateResourceBackupResponse createBackupTask(CreateResourceBackupRequest request);
|
||||
|
||||
ResourceBackupTaskDetailResponse getBackupTask(String taskId);
|
||||
|
||||
ResourceBackupDownloadResult downloadBackupPackage(String taskId);
|
||||
|
||||
List<ResourceTaskStepItemResponse> getBackupTaskSteps(String taskId);
|
||||
|
||||
ResourceTaskStepLogResponse getBackupTaskStepLog(String taskId, int stepNo);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
|
||||
public interface ResourcePrecheckService {
|
||||
|
||||
ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
|
||||
public interface ResourceRestoreService {
|
||||
|
||||
CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
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;
|
||||
|
||||
public interface ResourceTaskQueryService {
|
||||
|
||||
ResourceRestoreTaskDetailResponse getRestoreTask(String taskId);
|
||||
|
||||
List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId);
|
||||
|
||||
ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo);
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
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.discovery.ResourceDiscoveryService;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
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.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.ResourceBackupService;
|
||||
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
|
||||
private final InitTaskRepository initTaskRepository;
|
||||
private final ResourceDiscoveryService resourceDiscoveryService;
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ResourceBackupTaskRepository resourceBackupTaskRepository;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public ResourceBackupServiceImpl(
|
||||
InitTaskRepository initTaskRepository,
|
||||
ResourceDiscoveryService resourceDiscoveryService,
|
||||
ResourcePackageService resourcePackageService,
|
||||
ResourceBackupTaskRepository resourceBackupTaskRepository,
|
||||
ResourceTaskIdGenerator resourceTaskIdGenerator,
|
||||
com.cisd.tms.modules.backup.config.ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.initTaskRepository = initTaskRepository;
|
||||
this.resourceDiscoveryService = resourceDiscoveryService;
|
||||
this.resourcePackageService = resourcePackageService;
|
||||
this.resourceBackupTaskRepository = resourceBackupTaskRepository;
|
||||
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceKeysetStatusResponse getKeysetStatus() {
|
||||
ResourceKeysetStatusResponse response = new ResourceKeysetStatusResponse();
|
||||
// 第一版先把接口骨架和本地备份主链打通;真实密钥状态后续接入密码卡/UKey恢复结果。
|
||||
response.setLmkReady(false);
|
||||
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");
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateResourceBackupResponse createBackupTask(CreateResourceBackupRequest request) {
|
||||
// 资源备份必须基于“最近一次成功初始化”快照收集上下文,避免前端重新传一套可能失真的参数。
|
||||
InitTaskEntity initTask = initTaskRepository.findLatestByTaskTypeAndStatus("INIT", "SUCCESS")
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"));
|
||||
|
||||
String taskId = resourceTaskIdGenerator.nextBackupTaskId();
|
||||
String backupId = resourceTaskIdGenerator.nextBackupId();
|
||||
// 先根据初始化快照收敛资源范围,再交给打包服务生成本地可见的 .tmsbak 文件。
|
||||
ResourcePackageResult packageResult = resourcePackageService.createPackage(
|
||||
taskId,
|
||||
backupId,
|
||||
initTask,
|
||||
resourceDiscoveryService.discover(initTask),
|
||||
request
|
||||
);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ResourceBackupTaskEntity entity = new ResourceBackupTaskEntity();
|
||||
// 第一版先按同步成功模型落库,后续切换为真正异步任务时继续复用这张任务表。
|
||||
entity.setTaskId(taskId);
|
||||
entity.setBackupId(backupId);
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType(initTask.getProductType());
|
||||
entity.setMqType(initTask.getMqType());
|
||||
entity.setOrgCode(initTask.getOrgCode());
|
||||
entity.setSourceDeviceId(resolveSourceDeviceId());
|
||||
entity.setPackageName(packageResult.getPackageName());
|
||||
entity.setPackagePath(packageResult.getPackagePath());
|
||||
entity.setPackageSize(packageResult.getPackageSize());
|
||||
entity.setPackageHash(packageResult.getPackageHash());
|
||||
entity.setStorageEncKeyFingerprint(packageResult.getStorageEncKeyFingerprint());
|
||||
entity.setAuthenticityKeyFingerprint(packageResult.getAuthenticityKeyFingerprint());
|
||||
entity.setManifestSummaryJson(packageResult.getManifestSummaryJson());
|
||||
entity.setRemark(request == null ? null : request.getRemark());
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
entity.setStartTime(now);
|
||||
entity.setFinishTime(now);
|
||||
resourceBackupTaskRepository.save(entity);
|
||||
|
||||
CreateResourceBackupResponse response = new CreateResourceBackupResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setBackupId(backupId);
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setCreatedAt(now.toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceBackupTaskDetailResponse getBackupTask(String taskId) {
|
||||
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
|
||||
ResourceBackupTaskDetailResponse response = new ResourceBackupTaskDetailResponse();
|
||||
response.setTaskId(entity.getTaskId());
|
||||
response.setBackupId(entity.getBackupId());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setProductType(entity.getProductType());
|
||||
response.setMqType(entity.getMqType());
|
||||
response.setOrgCode(entity.getOrgCode());
|
||||
response.setSourceDeviceId(entity.getSourceDeviceId());
|
||||
response.setPackageName(entity.getPackageName());
|
||||
response.setPackageSize(entity.getPackageSize());
|
||||
response.setPackageHash(entity.getPackageHash());
|
||||
response.setStorageEncKeyFingerprint(entity.getStorageEncKeyFingerprint());
|
||||
response.setAuthenticityKeyFingerprint(entity.getAuthenticityKeyFingerprint());
|
||||
response.setManifestSummary(entity.getManifestSummaryJson());
|
||||
response.setRemark(entity.getRemark());
|
||||
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());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceBackupDownloadResult downloadBackupPackage(String taskId) {
|
||||
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
|
||||
Path packagePath = Path.of(trim(entity.getPackagePath())).normalize().toAbsolutePath();
|
||||
if (trim(entity.getPackagePath()).isEmpty() || !Files.exists(packagePath) || !Files.isRegularFile(packagePath)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package file not found");
|
||||
}
|
||||
try {
|
||||
ResourceBackupDownloadResult result = new ResourceBackupDownloadResult();
|
||||
result.setFilename(trim(entity.getPackageName()).isEmpty() ? packagePath.getFileName().toString() : entity.getPackageName());
|
||||
result.setPackagePath(packagePath);
|
||||
result.setContentLength(Files.size(packagePath));
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read resource backup package");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepItemResponse> getBackupTaskSteps(String taskId) {
|
||||
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
|
||||
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
|
||||
item.setStepNo(1);
|
||||
item.setStepCode("PACKAGE_CREATE");
|
||||
item.setStatus(entity.getStatus());
|
||||
item.setMessage("generated backup package " + trim(entity.getPackageName()));
|
||||
item.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
|
||||
item.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
|
||||
item.setLogAvailable(Boolean.FALSE);
|
||||
return List.of(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse getBackupTaskStepLog(String taskId, int stepNo) {
|
||||
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
|
||||
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
|
||||
response.setTaskId(trim(taskId));
|
||||
response.setStepNo(stepNo);
|
||||
response.setStepCode("PACKAGE_CREATE");
|
||||
response.setContent(stepNo == 1 ? objectToJsonLine(Map.of(
|
||||
"packageName", trim(entity.getPackageName()),
|
||||
"packagePath", trim(entity.getPackagePath()),
|
||||
"packageHash", trim(entity.getPackageHash())
|
||||
)) : "");
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String resolveSourceDeviceId() {
|
||||
try {
|
||||
// 先用主机名兜底,后续如有更稳定的设备唯一标识,可在这里统一替换。
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
} catch (Exception ex) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String objectToJsonLine(Map<String, Object> value) {
|
||||
try {
|
||||
return new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(value);
|
||||
} catch (Exception ex) {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package com.cisd.tms.modules.backup.service.impl;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
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.packagex.ResourcePackageInspectResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
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.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
|
||||
private final FileRecordRepository fileRecordRepository;
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final CisdPresetProperties cisdPresetProperties;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
|
||||
public ResourcePrecheckServiceImpl(
|
||||
FileRecordRepository fileRecordRepository,
|
||||
ResourcePackageService resourcePackageService,
|
||||
ResourcePrecheckStateStore resourcePrecheckStateStore,
|
||||
ResourceTaskIdGenerator resourceTaskIdGenerator,
|
||||
CisdPresetProperties cisdPresetProperties,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this.fileRecordRepository = fileRecordRepository;
|
||||
this.resourcePackageService = resourcePackageService;
|
||||
this.resourcePrecheckStateStore = resourcePrecheckStateStore;
|
||||
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
|
||||
this.cisdPresetProperties = cisdPresetProperties;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request) {
|
||||
FileRecordEntity fileRecord = fileRecordRepository.findByFileId(trim(request.getFileId()))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "backup package file not found: " + trim(request.getFileId())));
|
||||
// 预检阶段只解析包头、验签和比对指纹,不提前解密完整 payload。
|
||||
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackageHeader(Path.of(fileRecord.getStoragePath()));
|
||||
String storageFingerprint = stringValue(inspectResult.getManifest().get("storageEncKeyFingerprint"));
|
||||
String authenticityFingerprint = stringValue(inspectResult.getManifest().get("authenticityKeyFingerprint"));
|
||||
if (!expectedStorageFingerprint().equals(storageFingerprint) || !expectedAuthenticityFingerprint().equals(authenticityFingerprint)) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份包密钥指纹不匹配");
|
||||
}
|
||||
ResourceRestorePrecheckResponse response = new ResourceRestorePrecheckResponse();
|
||||
response.setPrecheckId(resourceTaskIdGenerator.nextPrecheckId());
|
||||
response.setBackupId(stringValue(inspectResult.getManifest().get("backupId")));
|
||||
response.setSourceProductType(stringValue(inspectResult.getManifest().get("productType")));
|
||||
response.setSourceMqType(stringValue(inspectResult.getManifest().get("mqType")));
|
||||
response.setSourceOrgCode(stringValue(inspectResult.getManifest().get("orgCode")));
|
||||
response.setSourceBackupTime(stringValue(inspectResult.getManifest().get("backupTime")));
|
||||
response.setTargetProductType(defaultProductType());
|
||||
response.setKeysetMatched(true);
|
||||
response.setSignatureValid(true);
|
||||
response.setCompatible(isCompatible(response.getSourceProductType(), response.getTargetProductType()));
|
||||
response.setPayloadDecrypted(false);
|
||||
response.setWarnings(List.of());
|
||||
response.setBlockingIssues(List.of());
|
||||
response.setRestorePlan(List.of(
|
||||
"RESTORE_LOCK",
|
||||
"STOP_SERVICES",
|
||||
"RESTORE_FILES",
|
||||
"RESTORE_DATABASES",
|
||||
"START_SERVICES",
|
||||
"CONSISTENCY_VERIFY"
|
||||
));
|
||||
response.setRestoreScopeSummary(buildScopeSummary(inspectResult));
|
||||
response.setExpiresAt(LocalDateTime.now().plusMinutes(30).toString());
|
||||
resourcePrecheckStateStore.save(new ResourcePrecheckContext(request.getFileId(), response));
|
||||
return response;
|
||||
}
|
||||
|
||||
private String defaultProductType() {
|
||||
return cisdPresetProperties.getProductType() == null || cisdPresetProperties.getProductType().isBlank()
|
||||
? "UNKNOWN"
|
||||
: cisdPresetProperties.getProductType().trim();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static String stringValue(Object value) {
|
||||
return value == null ? "" : String.valueOf(value);
|
||||
}
|
||||
|
||||
private String expectedStorageFingerprint() {
|
||||
return "USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex();
|
||||
}
|
||||
|
||||
private String expectedAuthenticityFingerprint() {
|
||||
return "USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex();
|
||||
}
|
||||
|
||||
private static java.util.Map<String, Object> buildScopeSummary(ResourcePackageInspectResult inspectResult) {
|
||||
java.util.LinkedHashMap<String, Object> summary = new java.util.LinkedHashMap<>();
|
||||
summary.put("resourceCount", inspectResult.getManifest().get("resourceCount"));
|
||||
summary.put("productType", inspectResult.getManifest().get("productType"));
|
||||
summary.put("mqType", inspectResult.getManifest().get("mqType"));
|
||||
summary.put("orgCode", inspectResult.getManifest().get("orgCode"));
|
||||
return summary;
|
||||
}
|
||||
|
||||
private static boolean isCompatible(String sourceProductType, String targetProductType) {
|
||||
if (targetProductType == null || targetProductType.isBlank() || "UNKNOWN".equalsIgnoreCase(targetProductType)) {
|
||||
return true;
|
||||
}
|
||||
return targetProductType.equalsIgnoreCase(sourceProductType);
|
||||
}
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
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.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.runner.ResourceRestoreRunnerLauncher;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
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.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final FileRecordRepository fileRecordRepository;
|
||||
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceRestoreRunnerLauncher resourceRestoreRunnerLauncher;
|
||||
|
||||
public ResourceRestoreServiceImpl(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
FileRecordRepository fileRecordRepository,
|
||||
ResourcePrecheckStateStore resourcePrecheckStateStore,
|
||||
ResourceTaskIdGenerator resourceTaskIdGenerator,
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ResourceRestoreRunnerLauncher resourceRestoreRunnerLauncher
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.fileRecordRepository = fileRecordRepository;
|
||||
this.resourcePrecheckStateStore = resourcePrecheckStateStore;
|
||||
this.resourceTaskIdGenerator = resourceTaskIdGenerator;
|
||||
this.handoffFileStore = handoffFileStore;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.resourceRestoreRunnerLauncher = resourceRestoreRunnerLauncher;
|
||||
}
|
||||
|
||||
public ResourceRestoreServiceImpl(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
FileRecordRepository fileRecordRepository,
|
||||
ResourcePrecheckStateStore resourcePrecheckStateStore,
|
||||
ResourceTaskIdGenerator resourceTaskIdGenerator,
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this(
|
||||
resourceRestoreTaskRepository,
|
||||
fileRecordRepository,
|
||||
resourcePrecheckStateStore,
|
||||
resourceTaskIdGenerator,
|
||||
handoffFileStore,
|
||||
resourceRestoreStateFileStore,
|
||||
resourceBackupProperties,
|
||||
handoffFile -> {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request) {
|
||||
if (!Boolean.TRUE.equals(request.getConfirmRiskAccepted())) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "restore risk must be accepted");
|
||||
}
|
||||
// 恢复创建必须消费预检结果,确保“用户确认的 backupId”与预检看到的是同一个包。
|
||||
ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found"));
|
||||
ResourceRestorePrecheckResponse precheck = context.getResponse();
|
||||
if (!trim(request.getConfirmBackupId()).equals(trim(precheck.getBackupId()))) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "confirmed backup id does not match precheck result");
|
||||
}
|
||||
FileRecordEntity packageFile = fileRecordRepository.findByFileId(context.getFileId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package file not found: " + trim(context.getFileId())));
|
||||
String packagePath = trim(packageFile.getStoragePath());
|
||||
if (packagePath.isEmpty()) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package path is empty");
|
||||
}
|
||||
|
||||
String taskId = resourceTaskIdGenerator.nextRestoreTaskId();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setPrecheckId(precheck.getPrecheckId());
|
||||
entity.setBackupId(precheck.getBackupId());
|
||||
// 恢复任务创建完成后,就应该已经进入“等待 runner 接管”的交接态,
|
||||
// 前端和运维看到的状态不能还停留在 PRECHECKED。
|
||||
entity.setStatus("HANDOFF");
|
||||
entity.setSourceDeviceId("UNKNOWN");
|
||||
entity.setSourceProductType(precheck.getSourceProductType());
|
||||
entity.setTargetProductType(precheck.getTargetProductType());
|
||||
entity.setPackageFileId(context.getFileId());
|
||||
Path taskDir = Path.of(resourceBackupProperties.getRestoreTaskRootDir()).resolve(taskId).normalize();
|
||||
// 恢复 runner 的工作目录和状态文件路径在任务创建时就固定下来,
|
||||
// 这样后续即使 TMS 重启,控制面路径也不会漂移。
|
||||
// 这里必须把真实备份包路径写进任务和 handoff,后续 runner 才能真正解包,
|
||||
// 否则它只能拿到 fileId,却不知道恢复时去哪里取浏览器上传/下载回来的备份文件。
|
||||
entity.setPackagePath(packagePath);
|
||||
entity.setStagingDir(taskDir.resolve("staging").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("restore-state.json").toString());
|
||||
entity.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
|
||||
entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
entity.setRestorePlanJson(joinAsJsonArray(precheck.getRestorePlan()));
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
resourceRestoreTaskRepository.save(entity);
|
||||
handoffFileStore.write(taskDir, entity);
|
||||
resourceRestoreStateFileStore.initialize(taskDir, entity);
|
||||
resourceRestoreStateFileStore.markHandoffReady(taskDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
resourceRestoreRunnerLauncher.launch(taskDir.resolve("handoff.json"));
|
||||
|
||||
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setCreatedAt(now.toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String joinAsJsonArray(List<String> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
// 这里先用最轻量的 JSON 数组序列化,避免为了一个占位字段引入额外 DTO。
|
||||
StringBuilder builder = new StringBuilder("[");
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (i > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append('"').append(items.get(i)).append('"');
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
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.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService;
|
||||
import com.cisd.tms.modules.backup.support.file.FileSystemResourceTaskStepLogService;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateSnapshot;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ResourceTaskStepLogService resourceTaskStepLogService;
|
||||
|
||||
public ResourceTaskQueryServiceImpl(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, objectMapper, new FileSystemResourceTaskStepLogService());
|
||||
}
|
||||
|
||||
public ResourceTaskQueryServiceImpl(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ObjectMapper objectMapper,
|
||||
ResourceTaskStepLogService resourceTaskStepLogService
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.objectMapper = objectMapper;
|
||||
this.resourceTaskStepLogService = resourceTaskStepLogService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + 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.setRunnerStatePath(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());
|
||||
mergeRunnerState(response, entity);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
ResourceRestoreStateSnapshot snapshot = resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath())).orElse(null);
|
||||
Path taskDir = resolveTaskDir(entity);
|
||||
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);
|
||||
applyStepState(item, taskDir, snapshot, stepCode, stepNo);
|
||||
responses.add(item);
|
||||
}
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo) {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : "";
|
||||
return resourceTaskStepLogService.read(resolveTaskDir(entity), trim(taskId), stepNo, stepCode);
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private void mergeRunnerState(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) {
|
||||
// 恢复任务详情以数据库记录为基线,再用 runner 状态文件覆盖实时字段。
|
||||
// 这样即使 runner 尚未接管,前端也能看到稳定的任务基本信息。
|
||||
resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath()))
|
||||
.ifPresent(snapshot -> applySnapshot(response, snapshot));
|
||||
}
|
||||
|
||||
private void applySnapshot(ResourceRestoreTaskDetailResponse response, ResourceRestoreStateSnapshot snapshot) {
|
||||
response.setStatus(defaultIfBlank(snapshot.getStatus(), response.getStatus()));
|
||||
response.setControlPhase(defaultIfBlank(snapshot.getControlPhase(), response.getControlPhase()));
|
||||
response.setStateSource(defaultIfBlank(snapshot.getStateSource(), response.getStateSource()));
|
||||
response.setCurrentStepCode(defaultIfBlank(snapshot.getCurrentStepCode(), response.getCurrentStepCode()));
|
||||
response.setCurrentStepNo(snapshot.getCurrentStepNo() == null ? response.getCurrentStepNo() : snapshot.getCurrentStepNo());
|
||||
response.setLastHeartbeatAt(defaultIfBlank(snapshot.getLastHeartbeatAt(), response.getLastHeartbeatAt()));
|
||||
response.setOrphaned(snapshot.getOrphaned() == null ? response.getOrphaned() : snapshot.getOrphaned());
|
||||
response.setCleanupWarnings(defaultIfNull(snapshot.getCleanupWarnings(), response.getCleanupWarnings()));
|
||||
response.setWarnings(defaultIfNull(snapshot.getWarnings(), response.getWarnings()));
|
||||
response.setBlockingIssues(defaultIfNull(snapshot.getBlockingIssues(), response.getBlockingIssues()));
|
||||
response.setConsistencyReport(snapshot.getConsistencyReport() == null ? response.getConsistencyReport() : snapshot.getConsistencyReport());
|
||||
response.setErrorCode(defaultIfBlank(snapshot.getErrorCode(), response.getErrorCode()));
|
||||
response.setErrorMessage(defaultIfBlank(snapshot.getErrorMessage(), response.getErrorMessage()));
|
||||
}
|
||||
|
||||
private Path resolveStatePath(String runnerStatePath) {
|
||||
return trim(runnerStatePath).isEmpty() ? null : Path.of(runnerStatePath).normalize();
|
||||
}
|
||||
|
||||
private Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
|
||||
Path statePath = resolveStatePath(entity.getRunnerStatePath());
|
||||
return statePath == null ? null : statePath.getParent();
|
||||
}
|
||||
|
||||
private void applyStepState(
|
||||
ResourceTaskStepItemResponse item,
|
||||
Path taskDir,
|
||||
ResourceRestoreStateSnapshot snapshot,
|
||||
String stepCode,
|
||||
int stepNo
|
||||
) {
|
||||
Map<String, Object> stepOutput = readStepOutput(taskDir, stepNo, stepCode);
|
||||
item.setLogAvailable(taskDir != null && Files.isRegularFile(taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode))));
|
||||
if (stepOutput != null) {
|
||||
item.setStatus(stringValue(stepOutput.get("result"), "UNKNOWN"));
|
||||
item.setMessage(stringValue(stepOutput.get("summary"), ""));
|
||||
return;
|
||||
}
|
||||
if (snapshot != null && stepNo == safeInt(snapshot.getCurrentStepNo())) {
|
||||
if ("RUNNER_OWNED".equals(snapshot.getControlPhase())) {
|
||||
item.setStatus("RUNNING");
|
||||
} else if ("RECONCILE_PENDING".equals(snapshot.getControlPhase()) && "FAILED".equals(snapshot.getStatus())) {
|
||||
item.setStatus("FAILED");
|
||||
item.setMessage(defaultIfBlank(snapshot.getErrorMessage(), ""));
|
||||
} else {
|
||||
item.setStatus("PENDING");
|
||||
}
|
||||
return;
|
||||
}
|
||||
item.setStatus("PENDING");
|
||||
}
|
||||
|
||||
private Map<String, Object> readStepOutput(Path taskDir, int stepNo, String stepCode) {
|
||||
if (taskDir == null) {
|
||||
return null;
|
||||
}
|
||||
Path outputFile = taskDir.resolve("step-output").resolve(String.format("%02d-%s.json", stepNo, stepCode));
|
||||
if (!Files.isRegularFile(outputFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(outputFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to parse resource task step output", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static int safeInt(Integer value) {
|
||||
return value == null ? -1 : value;
|
||||
}
|
||||
|
||||
private static String stringValue(Object value, String defaultValue) {
|
||||
return value == null ? defaultValue : String.valueOf(value);
|
||||
}
|
||||
|
||||
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("failed to parse restore plan 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("failed to parse consistency report json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String defaultIfBlank(String candidate, String fallback) {
|
||||
return trim(candidate).isEmpty() ? fallback : candidate;
|
||||
}
|
||||
|
||||
private static <T> T defaultIfNull(T candidate, T fallback) {
|
||||
return candidate == null ? fallback : candidate;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class InMemoryResourcePrecheckStateStore implements ResourcePrecheckStateStore {
|
||||
|
||||
private final ConcurrentMap<String, ResourcePrecheckContext> contexts = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void save(ResourcePrecheckContext context) {
|
||||
if (context == null || context.getResponse() == null || context.getResponse().getPrecheckId() == null) {
|
||||
throw new IllegalArgumentException("context with precheckId is required");
|
||||
}
|
||||
contexts.put(context.getResponse().getPrecheckId(), context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourcePrecheckContext> find(String precheckId) {
|
||||
return Optional.ofNullable(contexts.get(precheckId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class ResourceBackupDownloadResult {
|
||||
|
||||
private String filename;
|
||||
private Path packagePath;
|
||||
private long contentLength;
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
public Path getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(Path packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public long getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
public void setContentLength(long contentLength) {
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
|
||||
public class ResourcePrecheckContext {
|
||||
|
||||
private final String fileId;
|
||||
private final ResourceRestorePrecheckResponse response;
|
||||
|
||||
public ResourcePrecheckContext(String fileId, ResourceRestorePrecheckResponse response) {
|
||||
this.fileId = fileId;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public ResourceRestorePrecheckResponse getResponse() {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ResourcePrecheckStateStore {
|
||||
|
||||
void save(ResourcePrecheckContext context);
|
||||
|
||||
Optional<ResourcePrecheckContext> find(String precheckId);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.cisd.tms.modules.backup.support;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourceTaskIdGenerator {
|
||||
|
||||
private static final DateTimeFormatter TASK_ID_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
private static final DateTimeFormatter BACKUP_ID_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
|
||||
|
||||
public String nextBackupTaskId() {
|
||||
return "RBKP-TASK-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
|
||||
}
|
||||
|
||||
public String nextBackupId() {
|
||||
return "RBKP-" + LocalDateTime.now().format(BACKUP_ID_FORMATTER) + "-" + randomSuffix();
|
||||
}
|
||||
|
||||
public String nextPrecheckId() {
|
||||
return "RPRE-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
|
||||
}
|
||||
|
||||
public String nextRestoreTaskId() {
|
||||
return "RRST-TASK-" + LocalDateTime.now().format(TASK_ID_FORMATTER) + "-" + randomSuffix();
|
||||
}
|
||||
|
||||
private static String randomSuffix() {
|
||||
return UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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);
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Primary
|
||||
public class FileSystemResourcePrecheckStateStore implements ResourcePrecheckStateStore {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public FileSystemResourcePrecheckStateStore(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(ResourcePrecheckContext context) {
|
||||
if (context == null || context.getResponse() == null || context.getResponse().getPrecheckId() == null) {
|
||||
throw new IllegalArgumentException("context with precheckId is required");
|
||||
}
|
||||
try {
|
||||
Path file = resolvePath(context.getResponse().getPrecheckId());
|
||||
Files.createDirectories(file.getParent());
|
||||
objectMapper.writeValue(file.toFile(), new StoredResourcePrecheckContext(context.getFileId(), context.getResponse()));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to persist resource precheck context", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ResourcePrecheckContext> find(String precheckId) {
|
||||
Path file = resolvePath(precheckId);
|
||||
if (!Files.exists(file)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(file.toFile());
|
||||
String fileId = root.path("fileId").asText();
|
||||
ResourceRestorePrecheckResponse response = objectMapper.treeToValue(root.path("response"), ResourceRestorePrecheckResponse.class);
|
||||
return Optional.of(new ResourcePrecheckContext(fileId, response));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to load resource precheck context", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolvePath(String precheckId) {
|
||||
return Path.of(resourceBackupProperties.getPrecheckStoreDir()).resolve(precheckId + ".json").normalize();
|
||||
}
|
||||
|
||||
private record StoredResourcePrecheckContext(String fileId, ResourceRestorePrecheckResponse response) {
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
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("failed to read resource task step log", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class HandoffFileStore {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public HandoffFileStore(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void write(Path taskDir, ResourceRestoreTaskEntity entity) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
Path file = taskDir.resolve("handoff.json");
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("taskId", entity.getTaskId());
|
||||
json.put("precheckId", entity.getPrecheckId());
|
||||
json.put("backupId", entity.getBackupId());
|
||||
json.put("packageFileId", entity.getPackageFileId());
|
||||
json.put("packagePath", entity.getPackagePath());
|
||||
json.put("stagingDir", entity.getStagingDir());
|
||||
json.put("runnerStatePath", entity.getRunnerStatePath());
|
||||
json.put("restorePlanJson", entity.getRestorePlanJson());
|
||||
objectMapper.writeValue(file.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write restore handoff file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<ResourceRestoreHandoffSnapshot> read(Path handoffFile) {
|
||||
if (handoffFile == null || !Files.isRegularFile(handoffFile)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.ofNullable(objectMapper.readValue(handoffFile.toFile(), ResourceRestoreHandoffSnapshot.class));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read restore handoff file", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
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.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourceRestoreStateFileStore {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ResourceRestoreStateFileStore(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void initialize(Path taskDir, ResourceRestoreTaskEntity entity) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("taskId", entity.getTaskId());
|
||||
json.put("status", entity.getStatus());
|
||||
json.put("controlPhase", "PRECHECKED");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("runnerInstanceId", "");
|
||||
json.put("runnerPid", 0);
|
||||
json.put("ownershipToken", UUID.randomUUID().toString());
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(taskDir.resolve("restore-state.json").toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to initialize restore state file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public java.util.Optional<ResourceRestoreStateSnapshot> read(Path stateFile) {
|
||||
if (stateFile == null || !Files.isRegularFile(stateFile)) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
try {
|
||||
return java.util.Optional.ofNullable(objectMapper.readValue(stateFile.toFile(), ResourceRestoreStateSnapshot.class));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read restore state file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markHandoffReady(Path stateFile, String currentStepCode, Integer currentStepNo) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
json.put("status", "HANDOFF");
|
||||
json.put("controlPhase", "HANDOFF_READY");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark restore state as handoff ready", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void claimOwnership(Path stateFile, String runnerInstanceId, long runnerPid, String currentStepCode, Integer currentStepNo) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
String controlPhase = String.valueOf(json.getOrDefault("controlPhase", ""));
|
||||
if (!"HANDOFF_READY".equals(controlPhase)) {
|
||||
throw new IllegalStateException("restore state is not handoff ready: " + controlPhase);
|
||||
}
|
||||
json.put("status", "RUNNING");
|
||||
json.put("controlPhase", "RUNNER_OWNED");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("runnerInstanceId", runnerInstanceId);
|
||||
json.put("runnerPid", runnerPid);
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to claim restore runner ownership", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markStepRunning(Path stateFile, String currentStepCode, Integer currentStepNo, String runnerInstanceId) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json -> {
|
||||
json.put("status", "RUNNING");
|
||||
json.put("controlPhase", "RUNNER_OWNED");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
}, "failed to mark restore step running");
|
||||
}
|
||||
|
||||
public void touchHeartbeat(Path stateFile, String runnerInstanceId) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json ->
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString()), "failed to touch restore heartbeat");
|
||||
}
|
||||
|
||||
public void markReconcilePending(
|
||||
Path stateFile,
|
||||
String status,
|
||||
String errorCode,
|
||||
String errorMessage,
|
||||
String currentStepCode,
|
||||
Integer currentStepNo,
|
||||
String runnerInstanceId
|
||||
) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json -> {
|
||||
json.put("status", status);
|
||||
json.put("controlPhase", "RECONCILE_PENDING");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("errorCode", errorCode == null ? "" : errorCode);
|
||||
json.put("errorMessage", errorMessage == null ? "" : errorMessage);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
}, "failed to mark restore reconcile pending");
|
||||
}
|
||||
|
||||
private void updateOwnedState(
|
||||
Path stateFile,
|
||||
String runnerInstanceId,
|
||||
JsonMutator mutator,
|
||||
String errorMessage
|
||||
) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
String currentRunnerInstanceId = String.valueOf(json.getOrDefault("runnerInstanceId", ""));
|
||||
if (!runnerInstanceId.equals(currentRunnerInstanceId)) {
|
||||
throw new IllegalStateException("restore runner ownership changed: " + currentRunnerInstanceId);
|
||||
}
|
||||
mutator.accept(json);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markDone(Path stateFile) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
json.put("controlPhase", "DONE");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("lastHeartbeatAt", LocalDateTime.now().toString());
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark restore state done", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markStalled(Path stateFile, String errorCode, String errorMessage) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
if (!"RUNNER_OWNED".equals(String.valueOf(json.getOrDefault("controlPhase", "")))) {
|
||||
return;
|
||||
}
|
||||
json.put("status", "FAILED");
|
||||
json.put("controlPhase", "RECONCILE_PENDING");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("orphaned", true);
|
||||
json.put("errorCode", errorCode);
|
||||
json.put("errorMessage", errorMessage);
|
||||
json.put("lastHeartbeatAt", LocalDateTime.now().toString());
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark stalled restore state", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface JsonMutator {
|
||||
void accept(Map<String, Object> json);
|
||||
}
|
||||
|
||||
private Map<String, Object> readJson(Path stateFile) throws IOException {
|
||||
if (stateFile == null || !Files.isRegularFile(stateFile)) {
|
||||
throw new IllegalStateException("restore state file not found: " + stateFile);
|
||||
}
|
||||
return objectMapper.readValue(
|
||||
stateFile.toFile(),
|
||||
objectMapper.getTypeFactory().constructMapType(LinkedHashMap.class, String.class, Object.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ResourceRestoreStateSnapshot {
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private String controlPhase;
|
||||
private String stateSource;
|
||||
private String runnerInstanceId;
|
||||
private Long runnerPid;
|
||||
private String ownershipToken;
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String lastHeartbeatAt;
|
||||
private Boolean orphaned;
|
||||
private List<String> cleanupWarnings;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
private Object consistencyReport;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
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 getRunnerInstanceId() {
|
||||
return runnerInstanceId;
|
||||
}
|
||||
|
||||
public void setRunnerInstanceId(String runnerInstanceId) {
|
||||
this.runnerInstanceId = runnerInstanceId;
|
||||
}
|
||||
|
||||
public Long getRunnerPid() {
|
||||
return runnerPid;
|
||||
}
|
||||
|
||||
public void setRunnerPid(Long runnerPid) {
|
||||
this.runnerPid = runnerPid;
|
||||
}
|
||||
|
||||
public String getOwnershipToken() {
|
||||
return ownershipToken;
|
||||
}
|
||||
|
||||
public void setOwnershipToken(String ownershipToken) {
|
||||
this.ownershipToken = ownershipToken;
|
||||
}
|
||||
|
||||
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 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 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;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public class InitExecutorProperties {
|
||||
private int timeoutSec = 600;
|
||||
// 所有步骤日志会写入该目录,读取日志接口会做同目录约束校验
|
||||
private String logDir = "/tmp/tms-init-logs";
|
||||
// TMS 外置配置目录(资源备份恢复时使用)
|
||||
private String tmsConfigDir = "/home/tms/config";
|
||||
// 标准版(企业/间参)安装根目录(统一入口)
|
||||
private String standardHomeDir = DEFAULT_STANDARD_HOME_DIR;
|
||||
// 标准版(企业/间参)默认 cpconfig.cfg 路径
|
||||
@@ -133,6 +135,14 @@ public class InitExecutorProperties {
|
||||
this.logDir = logDir;
|
||||
}
|
||||
|
||||
public String getTmsConfigDir() {
|
||||
return tmsConfigDir;
|
||||
}
|
||||
|
||||
public void setTmsConfigDir(String tmsConfigDir) {
|
||||
this.tmsConfigDir = tmsConfigDir;
|
||||
}
|
||||
|
||||
public String getStandardHomeDir() {
|
||||
return isBlank(standardHomeDir) ? DEFAULT_STANDARD_HOME_DIR : standardHomeDir.trim();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ tms:
|
||||
timeout-sec: ${TMS_INIT_EXECUTOR_TIMEOUT_SEC:600}
|
||||
# 初始化步骤日志目录;按 taskId/stepNo 生成日志文件。
|
||||
log-dir: ${TMS_INIT_EXECUTOR_LOG_DIR:/home/tms/tmp/tms-init-logs}
|
||||
# TMS 外置配置目录;资源备份恢复会按此目录收集配置。
|
||||
tms-config-dir: ${TMS_INIT_EXECUTOR_TMS_CONFIG_DIR:/home/tms/config}
|
||||
# 标准版(企业/间参)安装根目录统一变量。
|
||||
standard-home-dir: ${TMS_INIT_STANDARD_HOME_DIR:/home/cmep4i}
|
||||
# 标准版 cpconfig.cfg 路径;默认取 standard-home-dir 下文件。
|
||||
@@ -179,6 +181,15 @@ tms:
|
||||
log-dir: ${TMS_UPGRADE_LOG_DIR:/home/tms/tmp/tms-upgrade-logs}
|
||||
# 升级包验签使用的公钥 PEM 文件路径;为空时升级预检会拒绝通过。
|
||||
signature-public-key-pem-path: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_PEM_PATH:/home/tms/cert/public_key.pem}
|
||||
backup:
|
||||
# 资源备份包输出目录;第一版先在本机生成可见的 .tmsbak 文件。
|
||||
output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages}
|
||||
# 恢复 runner 启停 TMS 时使用的脚本路径。
|
||||
tms-script-path: ${TMS_BACKUP_TMS_SCRIPT_PATH:/home/tms/scripts/tms.sh}
|
||||
# 是否在创建恢复任务后自动拉起本机 runner;默认关闭,避免开发环境误触停机恢复链。
|
||||
runner-auto-launch-enabled: ${TMS_BACKUP_RUNNER_AUTO_LAUNCH_ENABLED:false}
|
||||
# 恢复 runner 心跳失联超时(秒)。
|
||||
runner-heartbeat-timeout-seconds: ${TMS_BACKUP_RUNNER_HEARTBEAT_TIMEOUT_SECONDS:300}
|
||||
mk:
|
||||
init-identify:
|
||||
# SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。
|
||||
|
||||
@@ -51,6 +51,90 @@ CREATE TABLE IF NOT EXISTS tms_init_task_step (
|
||||
KEY idx_tms_init_task_step_task_id (task_id)
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 资源备份与恢复
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_backup_task (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
backup_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
product_type VARCHAR(16) NOT NULL,
|
||||
mq_type VARCHAR(32) NULL,
|
||||
org_code VARCHAR(32) NULL,
|
||||
source_device_id VARCHAR(128) NULL,
|
||||
package_name VARCHAR(255) NULL,
|
||||
package_path VARCHAR(1024) NULL,
|
||||
package_size BIGINT NULL,
|
||||
package_hash VARCHAR(128) NULL,
|
||||
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
manifest_summary_json JSON NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
remark VARCHAR(1024) NULL,
|
||||
create_by VARCHAR(64) 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_backup_task_task_id (task_id),
|
||||
UNIQUE KEY uk_tms_resource_backup_task_backup_id (backup_id),
|
||||
KEY idx_tms_resource_backup_task_status (status),
|
||||
KEY idx_tms_resource_backup_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
precheck_id VARCHAR(64) NOT NULL,
|
||||
backup_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
source_device_id VARCHAR(128) NULL,
|
||||
source_product_type VARCHAR(16) NOT NULL,
|
||||
target_product_type VARCHAR(16) NOT NULL,
|
||||
package_file_id VARCHAR(64) NOT NULL,
|
||||
package_path VARCHAR(1024) NULL,
|
||||
staging_dir VARCHAR(1024) NULL,
|
||||
runner_state_path VARCHAR(1024) NULL,
|
||||
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
restore_plan_json JSON NULL,
|
||||
consistency_report_json JSON NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
remark VARCHAR(1024) NULL,
|
||||
create_by VARCHAR(64) 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_restore_task_task_id (task_id),
|
||||
UNIQUE KEY uk_tms_resource_restore_task_precheck_id (precheck_id),
|
||||
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)
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 认证与设备基础表
|
||||
-- -----------------------------------------------------------------------------
|
||||
@@ -365,4 +449,4 @@ CREATE TABLE IF NOT EXISTS tms_access_whitelist (
|
||||
mask VARCHAR(10) NOT NULL,
|
||||
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
|
||||
);
|
||||
);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Resource backup and restore tables
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_backup_task (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
backup_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
product_type VARCHAR(16) NOT NULL,
|
||||
mq_type VARCHAR(32) NULL,
|
||||
org_code VARCHAR(32) NULL,
|
||||
source_device_id VARCHAR(128) NULL,
|
||||
package_name VARCHAR(255) NULL,
|
||||
package_path VARCHAR(1024) NULL,
|
||||
package_size BIGINT NULL,
|
||||
package_hash VARCHAR(128) NULL,
|
||||
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
manifest_summary_json JSON NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
remark VARCHAR(1024) NULL,
|
||||
create_by VARCHAR(64) 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_backup_task_task_id (task_id),
|
||||
UNIQUE KEY uk_tms_resource_backup_task_backup_id (backup_id),
|
||||
KEY idx_tms_resource_backup_task_status (status),
|
||||
KEY idx_tms_resource_backup_task_create_time (create_time)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
|
||||
id BIGINT PRIMARY KEY,
|
||||
task_id VARCHAR(64) NOT NULL,
|
||||
precheck_id VARCHAR(64) NOT NULL,
|
||||
backup_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
source_device_id VARCHAR(128) NULL,
|
||||
source_product_type VARCHAR(16) NOT NULL,
|
||||
target_product_type VARCHAR(16) NOT NULL,
|
||||
package_file_id VARCHAR(64) NOT NULL,
|
||||
package_path VARCHAR(1024) NULL,
|
||||
staging_dir VARCHAR(1024) NULL,
|
||||
runner_state_path VARCHAR(1024) NULL,
|
||||
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
|
||||
restore_plan_json JSON NULL,
|
||||
consistency_report_json JSON NULL,
|
||||
error_code VARCHAR(64) NULL,
|
||||
error_message VARCHAR(1024) NULL,
|
||||
remark VARCHAR(1024) NULL,
|
||||
create_by VARCHAR(64) 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_restore_task_task_id (task_id),
|
||||
UNIQUE KEY uk_tms_resource_restore_task_precheck_id (precheck_id),
|
||||
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)
|
||||
);
|
||||
@@ -26,6 +26,7 @@ import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class AuthAdminServiceTest {
|
||||
|
||||
@@ -168,12 +169,12 @@ class AuthAdminServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldIssueBindingSignWithBackupPacketForSuperAdmin() {
|
||||
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
||||
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||
org.mockito.Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-001");
|
||||
LmkService lmkService = Mockito.mock(LmkService.class);
|
||||
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||
Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-001");
|
||||
com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket packet = new com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket();
|
||||
packet.setComponentIndex(1);
|
||||
org.mockito.Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
|
||||
packet.setPacketIndex(1);
|
||||
Mockito.when(lmkService.buildBackupPacket(1)).thenReturn(packet);
|
||||
|
||||
AuthAdminService service = new AuthAdminServiceImpl(
|
||||
new InMemoryRoleAccountRepository(),
|
||||
@@ -189,24 +190,23 @@ class AuthAdminServiceTest {
|
||||
dto.setRole("ignored");
|
||||
dto.setUid("1");
|
||||
dto.setRid("RID-001");
|
||||
// dto.setExtra("EXTRA-001");
|
||||
|
||||
UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
|
||||
|
||||
Assertions.assertEquals("ISSUE-SIGN-001", result.getSign());
|
||||
Assertions.assertNotNull(result.getBackupPacket());
|
||||
Assertions.assertEquals(1, result.getBackupPacket().getComponentIndex());
|
||||
org.mockito.Mockito.verify(lmkService).signIk(
|
||||
Assertions.assertEquals(1, result.getBackupPacket().getPacketIndex());
|
||||
Mockito.verify(lmkService).signIk(
|
||||
"{\"pubKey\":\"PUB-001\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"SUPER_ADMIN\",\"uid\":\"1\",\"rid\":\"RID-001\"}"
|
||||
);
|
||||
org.mockito.Mockito.verify(lmkService).buildBackupPacket(1);
|
||||
Mockito.verify(lmkService).buildBackupPacket(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIssueBindingSignAndPreserveExtraForNonSuperAdmin() {
|
||||
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
|
||||
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||
org.mockito.Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-002");
|
||||
LmkService lmkService = Mockito.mock(LmkService.class);
|
||||
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
|
||||
Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-002");
|
||||
|
||||
AuthAdminService service = new AuthAdminServiceImpl(
|
||||
new InMemoryRoleAccountRepository(),
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.cisd.tms.modules.backup;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ResourceBackupDtoContractTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void shouldAcceptBackupCreateRequestFields() {
|
||||
CreateResourceBackupRequest request = new CreateResourceBackupRequest();
|
||||
request.setIncludeUpgradeHistory(Boolean.TRUE);
|
||||
request.setIncludeAuditBackups(Boolean.FALSE);
|
||||
request.setRemark("before hardware replacement");
|
||||
|
||||
Assertions.assertEquals(Boolean.TRUE, request.getIncludeUpgradeHistory());
|
||||
Assertions.assertEquals(Boolean.FALSE, request.getIncludeAuditBackups());
|
||||
Assertions.assertEquals("before hardware replacement", request.getRemark());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSerializeRestorePrecheckResponseFields() throws Exception {
|
||||
ResourceRestorePrecheckResponse response = new ResourceRestorePrecheckResponse();
|
||||
response.setPrecheckId("RPRE-20260408-000001");
|
||||
response.setBackupId("RBKP-20260408-153000-000001");
|
||||
response.setSourceProductType("ENTERPRISE");
|
||||
response.setKeysetMatched(true);
|
||||
response.setSignatureValid(true);
|
||||
response.setCompatible(true);
|
||||
response.setPayloadDecrypted(false);
|
||||
|
||||
String json = objectMapper.writeValueAsString(response);
|
||||
|
||||
Assertions.assertTrue(json.contains("\"precheckId\":\"RPRE-20260408-000001\""));
|
||||
Assertions.assertTrue(json.contains("\"backupId\":\"RBKP-20260408-153000-000001\""));
|
||||
Assertions.assertTrue(json.contains("\"sourceProductType\":\"ENTERPRISE\""));
|
||||
Assertions.assertTrue(json.contains("\"keysetMatched\":true"));
|
||||
Assertions.assertTrue(json.contains("\"payloadDecrypted\":false"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSerializeRestoreTaskDetailControlFields() throws Exception {
|
||||
ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse();
|
||||
response.setTaskId("RRST-TASK-20260408-000001");
|
||||
response.setStatus("RUNNING");
|
||||
response.setControlPhase("RUNNER_OWNED");
|
||||
response.setStateSource("RUNNER_FILE");
|
||||
response.setCurrentStepCode("RESTORE_DATABASES");
|
||||
|
||||
String json = objectMapper.writeValueAsString(response);
|
||||
|
||||
Assertions.assertTrue(json.contains("\"taskId\":\"RRST-TASK-20260408-000001\""));
|
||||
Assertions.assertTrue(json.contains("\"controlPhase\":\"RUNNER_OWNED\""));
|
||||
Assertions.assertTrue(json.contains("\"stateSource\":\"RUNNER_FILE\""));
|
||||
Assertions.assertTrue(json.contains("\"currentStepCode\":\"RESTORE_DATABASES\""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.cisd.tms.modules.backup;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ResourceBackupEndToEndContractTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeColdRestoreEndpointsInControllerContracts() throws Exception {
|
||||
String backupController = Files.readString(Path.of("/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/backup/controller/ResourceBackupController.java"));
|
||||
String restoreController = Files.readString(Path.of("/Users/waner/Work/CISD/文档/tms-framework/src/main/java/com/cisd/tms/modules/backup/controller/ResourceRestoreController.java"));
|
||||
|
||||
Assertions.assertTrue(backupController.contains("/api/v1/resource-backups"));
|
||||
Assertions.assertTrue(restoreController.contains("/api/v1/resource-restores"));
|
||||
Assertions.assertTrue(restoreController.contains("/precheck"));
|
||||
Assertions.assertTrue(restoreController.contains("/steps/{stepNo}/log"));
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package com.cisd.tms.modules.backup.controller;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourceBackupService;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class ResourceBackupControllerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldHandleBackupAndRestoreEndpoints() throws Exception {
|
||||
ResourceBackupService backupService = Mockito.mock(ResourceBackupService.class);
|
||||
ResourcePrecheckService precheckService = Mockito.mock(ResourcePrecheckService.class);
|
||||
ResourceRestoreService restoreService = Mockito.mock(ResourceRestoreService.class);
|
||||
ResourceTaskQueryService taskQueryService = Mockito.mock(ResourceTaskQueryService.class);
|
||||
|
||||
ResourceKeysetStatusResponse keysetStatus = new ResourceKeysetStatusResponse();
|
||||
keysetStatus.setLmkReady(true);
|
||||
keysetStatus.setStorageEncKeyReady(true);
|
||||
keysetStatus.setStorageEncKeyFingerprint("SM3(KEYSET)");
|
||||
Mockito.when(backupService.getKeysetStatus()).thenReturn(keysetStatus);
|
||||
|
||||
CreateResourceBackupResponse createBackup = new CreateResourceBackupResponse();
|
||||
createBackup.setTaskId("RBKP-TASK-20260408-000001");
|
||||
createBackup.setStatus("PENDING");
|
||||
Mockito.when(backupService.createBackupTask(Mockito.any(CreateResourceBackupRequest.class))).thenReturn(createBackup);
|
||||
|
||||
ResourceBackupTaskDetailResponse backupDetail = new ResourceBackupTaskDetailResponse();
|
||||
backupDetail.setTaskId("RBKP-TASK-20260408-000001");
|
||||
backupDetail.setStatus("RUNNING");
|
||||
backupDetail.setProductType("ENTERPRISE");
|
||||
Mockito.when(backupService.getBackupTask("RBKP-TASK-20260408-000001")).thenReturn(backupDetail);
|
||||
|
||||
Path packageFile = tempDir.resolve("RBKP-20260408-153000-000001.tmsbak");
|
||||
Files.writeString(packageFile, "download-content", StandardCharsets.UTF_8);
|
||||
ResourceBackupDownloadResult downloadResult = new ResourceBackupDownloadResult();
|
||||
downloadResult.setFilename("RBKP-20260408-153000-000001.tmsbak");
|
||||
downloadResult.setPackagePath(packageFile);
|
||||
downloadResult.setContentLength(Files.size(packageFile));
|
||||
Mockito.when(backupService.downloadBackupPackage("RBKP-TASK-20260408-000001")).thenReturn(downloadResult);
|
||||
|
||||
ResourceRestorePrecheckResponse precheck = new ResourceRestorePrecheckResponse();
|
||||
precheck.setPrecheckId("RPRE-20260408-000001");
|
||||
precheck.setBackupId("RBKP-20260408-153000-000001");
|
||||
precheck.setKeysetMatched(true);
|
||||
precheck.setSignatureValid(true);
|
||||
Mockito.when(precheckService.precheck(Mockito.any())).thenReturn(precheck);
|
||||
|
||||
CreateResourceRestoreResponse createRestore = new CreateResourceRestoreResponse();
|
||||
createRestore.setTaskId("RRST-TASK-20260408-000001");
|
||||
createRestore.setStatus("PENDING");
|
||||
Mockito.when(restoreService.createRestoreTask(Mockito.any(CreateResourceRestoreRequest.class))).thenReturn(createRestore);
|
||||
|
||||
ResourceRestoreTaskDetailResponse restoreDetail = new ResourceRestoreTaskDetailResponse();
|
||||
restoreDetail.setTaskId("RRST-TASK-20260408-000001");
|
||||
restoreDetail.setStatus("RUNNING");
|
||||
restoreDetail.setControlPhase("RUNNER_OWNED");
|
||||
restoreDetail.setStateSource("RUNNER_FILE");
|
||||
Mockito.when(taskQueryService.getRestoreTask("RRST-TASK-20260408-000001")).thenReturn(restoreDetail);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||
new ResourceBackupController(backupService),
|
||||
new ResourceRestoreController(precheckService, restoreService, taskQueryService)
|
||||
).build();
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-backups/keyset/status"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"lmkReady\":true")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"storageEncKeyFingerprint\":\"SM3(KEYSET)\"")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-backups")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"includeUpgradeHistory\":true,\"includeAuditBackups\":false,\"remark\":\"before hardware replacement\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"RBKP-TASK-20260408-000001\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-backups/RBKP-TASK-20260408-000001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"status\":\"RUNNING\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-backups/RBKP-TASK-20260408-000001/download"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.header().string("Content-Disposition", org.hamcrest.Matchers.containsString("attachment")))
|
||||
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.header().string("Content-Disposition", org.hamcrest.Matchers.containsString("RBKP-20260408-153000-000001.tmsbak")))
|
||||
.andExpect(content().string("download-content"));
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-restores/precheck")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"fileId\":\"FILE-20260408-000001\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"precheckId\":\"RPRE-20260408-000001\"")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-restores")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"precheckId\":\"RPRE-20260408-000001\",\"confirmBackupId\":\"RBKP-20260408-153000-000001\",\"confirmRiskAccepted\":true}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"RRST-TASK-20260408-000001\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260408-000001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"RUNNER_OWNED\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"stateSource\":\"RUNNER_FILE\"")));
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.cisd.tms.modules.backup.controller;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourcePrecheckService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
class ResourceRestoreControllerTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeRestoreEndpointsIncludingStepsAndLogs() throws Exception {
|
||||
ResourcePrecheckService precheckService = Mockito.mock(ResourcePrecheckService.class);
|
||||
ResourceRestoreService restoreService = Mockito.mock(ResourceRestoreService.class);
|
||||
ResourceTaskQueryService taskQueryService = Mockito.mock(ResourceTaskQueryService.class);
|
||||
|
||||
ResourceRestorePrecheckResponse precheck = new ResourceRestorePrecheckResponse();
|
||||
precheck.setPrecheckId("RPRE-20260414-000001");
|
||||
precheck.setPayloadDecrypted(false);
|
||||
Mockito.when(precheckService.precheck(Mockito.any(ResourceRestorePrecheckRequest.class))).thenReturn(precheck);
|
||||
|
||||
CreateResourceRestoreResponse create = new CreateResourceRestoreResponse();
|
||||
create.setTaskId("RRST-TASK-20260414-000001");
|
||||
create.setStatus("HANDOFF");
|
||||
Mockito.when(restoreService.createRestoreTask(Mockito.any(CreateResourceRestoreRequest.class))).thenReturn(create);
|
||||
|
||||
ResourceRestoreTaskDetailResponse detail = new ResourceRestoreTaskDetailResponse();
|
||||
detail.setTaskId("RRST-TASK-20260414-000001");
|
||||
detail.setControlPhase("RECONCILE_PENDING");
|
||||
detail.setStateSource("RUNNER_FILE");
|
||||
Mockito.when(taskQueryService.getRestoreTask("RRST-TASK-20260414-000001")).thenReturn(detail);
|
||||
|
||||
ResourceTaskStepItemResponse step = new ResourceTaskStepItemResponse();
|
||||
step.setStepNo(2);
|
||||
step.setStepCode("STOP_SERVICES");
|
||||
step.setStatus("SUCCESS");
|
||||
Mockito.when(taskQueryService.getRestoreTaskSteps("RRST-TASK-20260414-000001")).thenReturn(List.of(step));
|
||||
|
||||
ResourceTaskStepLogResponse log = new ResourceTaskStepLogResponse();
|
||||
log.setTaskId("RRST-TASK-20260414-000001");
|
||||
log.setStepNo(2);
|
||||
log.setStepCode("STOP_SERVICES");
|
||||
log.setContent("stop ok");
|
||||
log.setTruncated(false);
|
||||
Mockito.when(taskQueryService.getRestoreTaskStepLog("RRST-TASK-20260414-000001", 2)).thenReturn(log);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||
new ResourceRestoreController(precheckService, restoreService, taskQueryService)
|
||||
).build();
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-restores/precheck")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"fileId\":\"FILE-20260414-000001\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"payloadDecrypted\":false")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-restores")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"precheckId\":\"RPRE-20260414-000001\",\"confirmBackupId\":\"RBKP-20260414-000001\",\"confirmRiskAccepted\":true}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"RRST-TASK-20260414-000001\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"RECONCILE_PENDING\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001/steps"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"stepCode\":\"STOP_SERVICES\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001/steps/2/log"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"content\":\"stop ok\"")));
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
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.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourceEnvelopeCryptoServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldWrapDekWithEncUserKeyAndSignWithAuthUserKey() {
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setStorageEncKeyIndex(7);
|
||||
properties.setAuthenticitySignKeyIndex(8);
|
||||
|
||||
AtomicReference<byte[]> capturedDek = new AtomicReference<>();
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
capturedDek.set(request.getData());
|
||||
return backup(new byte[]{0x11, 0x22, 0x33});
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any(EccInternalSignRequest.class)))
|
||||
.thenReturn(backup(new byte[]{0x44, 0x55, 0x66}));
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> backup(capturedDek.get()));
|
||||
|
||||
ResourceEnvelopeCryptoServiceImpl service = new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties);
|
||||
byte[] payload = "payload-data".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
byte[] signSource = "manifest|envelope|cipher".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
ResourceSealedPayload sealed = service.seal(payload);
|
||||
byte[] signature = service.sign(signSource);
|
||||
byte[] opened = service.unsealAndVerify(sealed.getEnvelope(), sealed.getCipherBytes(), signSource, signature);
|
||||
|
||||
Assertions.assertArrayEquals(payload, opened);
|
||||
Assertions.assertArrayEquals(new byte[]{0x44, 0x55, 0x66}, signature);
|
||||
|
||||
ArgumentCaptor<EccInternalEncryptRequest> encryptCaptor = ArgumentCaptor.forClass(EccInternalEncryptRequest.class);
|
||||
Mockito.verify(pcieCryptoService).sdfeInternalEncryptEcc(encryptCaptor.capture());
|
||||
Assertions.assertEquals(7, encryptCaptor.getValue().getKeyIndex());
|
||||
|
||||
ArgumentCaptor<EccInternalSignRequest> signCaptor = ArgumentCaptor.forClass(EccInternalSignRequest.class);
|
||||
Mockito.verify(pcieCryptoService).eccInternalSign(signCaptor.capture());
|
||||
Assertions.assertEquals(8, signCaptor.getValue().getKeyIndex());
|
||||
|
||||
ArgumentCaptor<EccInternalVerifyRequest> verifyCaptor = ArgumentCaptor.forClass(EccInternalVerifyRequest.class);
|
||||
Mockito.verify(pcieCryptoService).eccInternalVerify(verifyCaptor.capture());
|
||||
Assertions.assertEquals(8, verifyCaptor.getValue().getKeyIndex());
|
||||
|
||||
ArgumentCaptor<EccInternalDecryptRequest> decryptCaptor = ArgumentCaptor.forClass(EccInternalDecryptRequest.class);
|
||||
Mockito.verify(pcieCryptoService).sdfeInternalDecryptEcc(decryptCaptor.capture());
|
||||
Assertions.assertEquals(7, decryptCaptor.getValue().getKeyIndex());
|
||||
}
|
||||
|
||||
private static BackupDataResult backup(byte[] data) {
|
||||
BackupDataResult result = new BackupDataResult();
|
||||
result.setData(data);
|
||||
result.setLength(data.length);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.cisd.tms.modules.backup.discovery;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ResourceDiscoveryServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldDiscoverStandardRabbitResourcesWithoutExcludedArtifacts() {
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(sampleProperties());
|
||||
|
||||
List<DiscoveredResourceItem> items = service.discover(sampleInitTask("ENTERPRISE", "RABBITMQ"));
|
||||
|
||||
Assertions.assertTrue(containsCode(items, "TMS_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "CP_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "RECEIVER_LICENSE"));
|
||||
Assertions.assertTrue(containsCode(items, "RABBITMQ_DEFINITIONS"));
|
||||
Assertions.assertFalse(containsCode(items, "NGINX_CONFIG"));
|
||||
Assertions.assertFalse(containsCode(items, "WEB_STATIC"));
|
||||
Assertions.assertFalse(containsCode(items, "ORGANIZATION_JSON"));
|
||||
Assertions.assertFalse(containsCode(items, "TMS_UPLOADS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDirectPathsForDirectTlqResources() {
|
||||
ResourceDiscoveryService service = new ResourceDiscoveryServiceImpl(sampleProperties());
|
||||
|
||||
List<DiscoveredResourceItem> items = service.discover(sampleInitTask("DIRECT", "RABBITMQ_TLQ"));
|
||||
|
||||
Assertions.assertTrue(hasPath(items, "CP_CONFIG", "/opt/cpconfig.cfg"));
|
||||
Assertions.assertTrue(hasPath(items, "RECEIVER_LICENSE", "/home/cmep4d/cmep/license/cmep.license"));
|
||||
Assertions.assertTrue(hasPath(items, "TLQ_LICENSE", "/home/tlq/TLQ8/license.dat"));
|
||||
Assertions.assertTrue(containsCode(items, "RABBITMQ_DEFINITIONS"));
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask(String productType, String mqType) {
|
||||
InitTaskEntity entity = new InitTaskEntity();
|
||||
entity.setTaskId("INIT-20260408-000001");
|
||||
entity.setTaskType("INIT");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType(productType);
|
||||
entity.setMqType(mqType);
|
||||
entity.setOrgCode("AAAABBBBXXX");
|
||||
entity.setDeployMode("SINGLE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static InitExecutorProperties sampleProperties() {
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setTmsConfigDir("/home/tms/config");
|
||||
properties.setStandardCpconfigPath("/home/cmep4i/cpconfig.cfg");
|
||||
properties.setDirectCpconfigTargetPath("/opt/cpconfig.cfg");
|
||||
properties.setStandardReceiverLicenseTargetPath("/home/cmep4i/cmep/license/cmep.license");
|
||||
properties.setDirectReceiverLicenseTargetPath("/home/cmep4d/cmep/license/cmep.license");
|
||||
properties.setStandardTlqLicenseTargetPath("/home/tlq/tlqconf.zip");
|
||||
properties.setDirectTlqLicenseTargetPath("/home/tlq/TLQ8/license.dat");
|
||||
properties.setCfmqTargetDir("/pmts/cfg");
|
||||
properties.setRabbitmqAdminCommand("/usr/sbin/rabbitmqadmin");
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static boolean containsCode(List<DiscoveredResourceItem> items, String code) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()));
|
||||
}
|
||||
|
||||
private static boolean hasPath(List<DiscoveredResourceItem> items, String code, String path) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && path.equals(item.getResolvedPath()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl;
|
||||
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.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import org.mockito.Mockito;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipFile;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ResourcePackageServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldWriteManifestEnvelopeAndCiphertextIntoBackupPackage() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setOutputDir(tempDir.toString());
|
||||
Path payloadFile = writePayloadFile("resource/application.yml", "server.port=8080");
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getData());
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any()))
|
||||
.thenReturn(backup(new byte[]{0x11, 0x22, 0x33}));
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalDecryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getCipherBlob());
|
||||
});
|
||||
Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any());
|
||||
ResourcePackageService service = new ResourcePackageServiceImpl(
|
||||
properties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties),
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
ResourcePackageResult result = service.createPackage(
|
||||
"RBKP-TASK-20260413-000001",
|
||||
"RBKP-20260413-153000-000001",
|
||||
sampleInitTask(),
|
||||
List.of(resource("TMS_CONFIG", payloadFile.toString())),
|
||||
sampleRequest()
|
||||
);
|
||||
|
||||
Path packagePath = Path.of(result.getPackagePath());
|
||||
Assertions.assertTrue(Files.exists(packagePath));
|
||||
Assertions.assertTrue(result.getPackageName().endsWith(".tmsbak"));
|
||||
Assertions.assertTrue(result.getPackageSize() > 0);
|
||||
Assertions.assertFalse(result.getPackageHash().isBlank());
|
||||
Assertions.assertFalse(result.getStorageEncKeyFingerprint().isBlank());
|
||||
Assertions.assertFalse(result.getAuthenticityKeyFingerprint().isBlank());
|
||||
Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":1"));
|
||||
|
||||
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
|
||||
Assertions.assertNotNull(zipFile.getEntry("manifest.json"));
|
||||
Assertions.assertNotNull(zipFile.getEntry("envelope.json"));
|
||||
Assertions.assertNotNull(zipFile.getEntry("payload.enc"));
|
||||
Assertions.assertNotNull(zipFile.getEntry("signature.sig"));
|
||||
}
|
||||
|
||||
ResourcePackageInspectResult inspectResult = service.inspectPackage(packagePath);
|
||||
Assertions.assertEquals("RBKP-20260413-153000-000001", inspectResult.getManifest().get("backupId"));
|
||||
Assertions.assertNotNull(inspectResult.getPayloadArchiveBytes());
|
||||
Assertions.assertTrue(inspectResult.getPayloadArchiveBytes().length > 0);
|
||||
Assertions.assertEquals("ZIP", inspectResult.getPayload().get("archiveFormat"));
|
||||
Assertions.assertTrue(inspectResult.getPayload().containsKey("resources"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Map<String, Object>> resources = (java.util.List<Map<String, Object>>) inspectResult.getPayload().get("resources");
|
||||
Assertions.assertEquals(1, resources.size());
|
||||
Assertions.assertEquals(Boolean.TRUE, resources.get(0).get("collected"));
|
||||
Assertions.assertTrue(String.valueOf(resources.get(0).get("entryPath")).contains("application.yml"));
|
||||
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new java.io.ByteArrayInputStream(inspectResult.getPayloadArchiveBytes()))) {
|
||||
java.util.Map<String, String> entries = new java.util.LinkedHashMap<>();
|
||||
java.util.zip.ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
entries.put(entry.getName(), new String(zipInputStream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8));
|
||||
}
|
||||
Assertions.assertTrue(entries.containsKey("payload-manifest.json"));
|
||||
Assertions.assertTrue(entries.values().stream().anyMatch(value -> value.contains("server.port=8080")));
|
||||
}
|
||||
}
|
||||
|
||||
private static BackupDataResult backup(byte[] data) {
|
||||
BackupDataResult result = new BackupDataResult();
|
||||
result.setData(data);
|
||||
result.setLength(data.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask() {
|
||||
InitTaskEntity entity = new InitTaskEntity();
|
||||
entity.setTaskId("INIT-20260408-000001");
|
||||
entity.setTaskType("INIT");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType("ENTERPRISE");
|
||||
entity.setMqType("RABBITMQ");
|
||||
entity.setOrgCode("AAAABBBBXXX");
|
||||
entity.setDeployMode("SINGLE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem resource(String code, String path) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("FILE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
|
||||
private Path writePayloadFile(String relativePath, String content) throws Exception {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, content);
|
||||
return file;
|
||||
}
|
||||
|
||||
private static CreateResourceBackupRequest sampleRequest() {
|
||||
CreateResourceBackupRequest request = new CreateResourceBackupRequest();
|
||||
request.setIncludeUpgradeHistory(true);
|
||||
request.setIncludeAuditBackups(false);
|
||||
request.setRemark("before hardware replacement");
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceTaskStepEntity;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceBackupTaskMapper;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceRestoreTaskMapper;
|
||||
import com.cisd.tms.modules.backup.mapper.ResourceTaskStepMapper;
|
||||
import com.cisd.tms.modules.backup.repository.impl.ResourceBackupTaskRepositoryImpl;
|
||||
import com.cisd.tms.modules.backup.repository.impl.ResourceRestoreTaskRepositoryImpl;
|
||||
import com.cisd.tms.modules.backup.repository.impl.ResourceTaskStepRepositoryImpl;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourceTaskRepositoryTest {
|
||||
|
||||
@Test
|
||||
void shouldSaveAndQueryBackupTask() {
|
||||
ResourceBackupTaskMapper mapper = Mockito.mock(ResourceBackupTaskMapper.class);
|
||||
ResourceBackupTaskRepository repository = new ResourceBackupTaskRepositoryImpl(mapper);
|
||||
ResourceBackupTaskEntity entity = new ResourceBackupTaskEntity();
|
||||
entity.setTaskId("RBKP-001");
|
||||
entity.setBackupId("RBACKUP-001");
|
||||
entity.setStatus("PENDING");
|
||||
entity.setProductType("DIRECT");
|
||||
entity.setPackagePath("/home/tms/tmp/resource-backup/RBKP-001/package.tmsbak");
|
||||
entity.setManifestSummaryJson("{\"resourceCount\":3}");
|
||||
entity.setErrorCode("NONE");
|
||||
entity.setRemark("nightly");
|
||||
|
||||
repository.save(entity);
|
||||
|
||||
Mockito.verify(mapper).insert(entity);
|
||||
Mockito.when(mapper.selectByTaskId("RBKP-001")).thenReturn(entity);
|
||||
|
||||
ResourceBackupTaskEntity found = repository.findByTaskId("RBKP-001").orElseThrow();
|
||||
|
||||
Assertions.assertEquals("RBKP-001", found.getTaskId());
|
||||
Assertions.assertEquals("RBACKUP-001", found.getBackupId());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-backup/RBKP-001/package.tmsbak", found.getPackagePath());
|
||||
Assertions.assertEquals("{\"resourceCount\":3}", found.getManifestSummaryJson());
|
||||
Assertions.assertEquals("NONE", found.getErrorCode());
|
||||
Assertions.assertEquals("nightly", found.getRemark());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveAndQueryRestoreTask() {
|
||||
ResourceRestoreTaskMapper mapper = Mockito.mock(ResourceRestoreTaskMapper.class);
|
||||
ResourceRestoreTaskRepository repository = new ResourceRestoreTaskRepositoryImpl(mapper);
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-001");
|
||||
entity.setPrecheckId("RPRE-001");
|
||||
entity.setBackupId("RBACKUP-001");
|
||||
entity.setStatus("READY");
|
||||
entity.setPackageFileId("FILE-001");
|
||||
entity.setStagingDir("/home/tms/tmp/resource-restore/RRST-001/staging");
|
||||
entity.setRunnerStatePath("/home/tms/tmp/resource-restore/RRST-001/restore-state.json");
|
||||
entity.setSourceProductType("DIRECT");
|
||||
entity.setTargetProductType("DIRECT");
|
||||
entity.setConsistencyReportJson("{\"ok\":true}");
|
||||
entity.setErrorCode("NONE");
|
||||
|
||||
repository.save(entity);
|
||||
|
||||
Mockito.verify(mapper).insert(entity);
|
||||
Mockito.when(mapper.selectByTaskId("RRST-001")).thenReturn(entity);
|
||||
|
||||
ResourceRestoreTaskEntity found = repository.findByTaskId("RRST-001").orElseThrow();
|
||||
|
||||
Assertions.assertEquals("RRST-001", found.getTaskId());
|
||||
Assertions.assertEquals("RPRE-001", found.getPrecheckId());
|
||||
Assertions.assertEquals("FILE-001", found.getPackageFileId());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/staging", found.getStagingDir());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/restore-state.json", found.getRunnerStatePath());
|
||||
Assertions.assertEquals("DIRECT", found.getSourceProductType());
|
||||
Assertions.assertEquals("DIRECT", found.getTargetProductType());
|
||||
Assertions.assertEquals("{\"ok\":true}", found.getConsistencyReportJson());
|
||||
Assertions.assertEquals("NONE", found.getErrorCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSaveAndQueryTaskStepsByTaskId() {
|
||||
ResourceTaskStepMapper mapper = Mockito.mock(ResourceTaskStepMapper.class);
|
||||
ResourceTaskStepRepository repository = new ResourceTaskStepRepositoryImpl(mapper);
|
||||
ResourceTaskStepEntity entity = new ResourceTaskStepEntity();
|
||||
entity.setTaskId("RRST-001");
|
||||
entity.setTaskType("RESOURCE_RESTORE");
|
||||
entity.setStepNo(1);
|
||||
entity.setStepCode("STOP_SERVICES");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setLogPath("/home/tms/tmp/resource-restore/RRST-001/steps/01-STOP_SERVICES.log");
|
||||
entity.setExitCode(0);
|
||||
|
||||
repository.save(entity);
|
||||
|
||||
Mockito.verify(mapper).insert(entity);
|
||||
Mockito.when(mapper.selectByTaskIdOrderByStepNo("RRST-001")).thenReturn(List.of(entity));
|
||||
|
||||
List<ResourceTaskStepEntity> steps = repository.findByTaskIdOrderByStepNo("RRST-001");
|
||||
|
||||
Assertions.assertEquals(1, steps.size());
|
||||
Assertions.assertEquals("RESOURCE_RESTORE", steps.get(0).getTaskType());
|
||||
Assertions.assertEquals("STOP_SERVICES", steps.get(0).getStepCode());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/steps/01-STOP_SERVICES.log", steps.get(0).getLogPath());
|
||||
Assertions.assertEquals(0, steps.get(0).getExitCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeclareExplicitSelectColumnsInMappers() throws Exception {
|
||||
assertSelectSql(
|
||||
ResourceBackupTaskMapper.class.getMethod("selectByTaskId", String.class),
|
||||
"tms_resource_backup_task",
|
||||
"task_id",
|
||||
"backup_id",
|
||||
"package_path",
|
||||
"manifest_summary_json",
|
||||
"error_code",
|
||||
"remark"
|
||||
);
|
||||
assertSelectSql(
|
||||
ResourceRestoreTaskMapper.class.getMethod("selectByTaskId", String.class),
|
||||
"tms_resource_restore_task",
|
||||
"task_id",
|
||||
"precheck_id",
|
||||
"package_file_id",
|
||||
"staging_dir",
|
||||
"runner_state_path",
|
||||
"consistency_report_json",
|
||||
"error_code"
|
||||
);
|
||||
assertSelectSql(
|
||||
ResourceTaskStepMapper.class.getMethod("selectByTaskIdOrderByStepNo", String.class),
|
||||
"tms_resource_task_step",
|
||||
"task_id",
|
||||
"task_type",
|
||||
"step_no",
|
||||
"step_code",
|
||||
"log_path",
|
||||
"exit_code"
|
||||
);
|
||||
}
|
||||
|
||||
private static void assertSelectSql(Method method, String tableName, String... requiredColumns) {
|
||||
Select select = method.getAnnotation(Select.class);
|
||||
Assertions.assertNotNull(select, method.getName() + " should declare @Select");
|
||||
String sql = String.join(" ", select.value());
|
||||
Assertions.assertFalse(sql.contains("*"), method.getName() + " should not use SELECT *");
|
||||
Assertions.assertTrue(sql.contains(tableName), method.getName() + " should target " + tableName);
|
||||
for (String requiredColumn : requiredColumns) {
|
||||
Assertions.assertTrue(sql.contains(requiredColumn), method.getName() + " should include column " + requiredColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package com.cisd.tms.modules.backup.repository;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
class ResourceTaskSchemaContractTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeResourceTaskSchemaAndEnums() throws Exception {
|
||||
String v6Sql = readClasspathResource("db/migration/V6__add_resource_backup_restore_tables.sql");
|
||||
String v1Sql = readClasspathResource("db/migration/V1__tms_schema_full.sql");
|
||||
|
||||
assertContainsAll(v6Sql,
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_backup_task",
|
||||
"backup_id VARCHAR(64) NOT NULL",
|
||||
"package_path VARCHAR(1024) NULL",
|
||||
"manifest_summary_json JSON NULL",
|
||||
"error_code VARCHAR(64) NULL",
|
||||
"remark VARCHAR(1024) NULL",
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_restore_task",
|
||||
"precheck_id VARCHAR(64) NOT NULL",
|
||||
"package_file_id VARCHAR(64) NOT NULL",
|
||||
"staging_dir VARCHAR(1024) NULL",
|
||||
"runner_state_path VARCHAR(1024) NULL",
|
||||
"source_product_type VARCHAR(16) NOT NULL",
|
||||
"target_product_type VARCHAR(16) NOT NULL",
|
||||
"consistency_report_json JSON NULL",
|
||||
"error_code VARCHAR(64) NULL",
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_task_step",
|
||||
"task_type VARCHAR(32) NOT NULL",
|
||||
"step_code VARCHAR(64) NOT NULL",
|
||||
"log_path VARCHAR(1024) NULL",
|
||||
"exit_code INT NULL");
|
||||
|
||||
assertContainsAll(v1Sql,
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_backup_task",
|
||||
"backup_id VARCHAR(64) NOT NULL",
|
||||
"package_path VARCHAR(1024) NULL",
|
||||
"manifest_summary_json JSON NULL",
|
||||
"error_code VARCHAR(64) NULL",
|
||||
"remark VARCHAR(1024) NULL",
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_restore_task",
|
||||
"precheck_id VARCHAR(64) NOT NULL",
|
||||
"package_file_id VARCHAR(64) NOT NULL",
|
||||
"staging_dir VARCHAR(1024) NULL",
|
||||
"runner_state_path VARCHAR(1024) NULL",
|
||||
"source_product_type VARCHAR(16) NOT NULL",
|
||||
"target_product_type VARCHAR(16) NOT NULL",
|
||||
"consistency_report_json JSON NULL",
|
||||
"error_code VARCHAR(64) NULL",
|
||||
"CREATE TABLE IF NOT EXISTS tms_resource_task_step",
|
||||
"task_type VARCHAR(32) NOT NULL",
|
||||
"step_code VARCHAR(64) NOT NULL",
|
||||
"log_path VARCHAR(1024) NULL",
|
||||
"exit_code INT NULL");
|
||||
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceTaskStatus",
|
||||
"PENDING",
|
||||
"PRECHECKING",
|
||||
"PRECHECK_FAILED",
|
||||
"READY",
|
||||
"HANDOFF",
|
||||
"RUNNING",
|
||||
"VERIFYING",
|
||||
"SUCCESS",
|
||||
"FAILED",
|
||||
"CANCELLED"
|
||||
);
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceTaskType",
|
||||
"RESOURCE_BACKUP",
|
||||
"RESOURCE_RESTORE"
|
||||
);
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceControlPhase",
|
||||
"TMS_PRECHECK",
|
||||
"HANDOFF_READY",
|
||||
"RUNNER_OWNED",
|
||||
"RECONCILE_PENDING",
|
||||
"DONE"
|
||||
);
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceStepStatus",
|
||||
"PENDING",
|
||||
"RUNNING",
|
||||
"SUCCESS",
|
||||
"SUCCESS_WITH_WARNINGS",
|
||||
"FAILED",
|
||||
"SKIPPED"
|
||||
);
|
||||
}
|
||||
|
||||
private static String readClasspathResource(String path) throws Exception {
|
||||
ClassPathResource resource = new ClassPathResource(path);
|
||||
Assertions.assertTrue(resource.exists(), "Missing classpath resource: " + path);
|
||||
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void assertContainsAll(String text, String... expectedSnippets) {
|
||||
for (String expectedSnippet : expectedSnippets) {
|
||||
Assertions.assertTrue(text.contains(expectedSnippet), "Missing snippet: " + expectedSnippet);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private static void assertEnumConstants(String className, String... expectedNames) throws Exception {
|
||||
Class<?> enumClass = Class.forName(className);
|
||||
Assertions.assertTrue(enumClass.isEnum(), className + " should be an enum");
|
||||
Class<? extends Enum> typedEnumClass = (Class<? extends Enum>) enumClass.asSubclass(Enum.class);
|
||||
for (String expectedName : expectedNames) {
|
||||
Enum.valueOf(typedEnumClass, expectedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
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.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageServiceImpl;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class PayloadArchiveRunnerStepExecutorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExtractPayloadArchiveIntoStagingDirectory() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourcePackageService packageService = new ResourcePackageServiceImpl(
|
||||
backupProperties(),
|
||||
new ResourceEnvelopeCryptoServiceImpl(mockCrypto(), backupProperties()),
|
||||
objectMapper
|
||||
);
|
||||
Path sourceFile = writeFile("source/application.yml", "server.port=8088");
|
||||
Path taskDir = tempDir.resolve("restore-task");
|
||||
Files.createDirectories(taskDir);
|
||||
|
||||
String packagePath = packageService.createPackage(
|
||||
"RBKP-TASK-20260414-000101",
|
||||
"RBKP-20260414-000101",
|
||||
sampleInitTask(),
|
||||
List.of(resource("TMS_CONFIG", sourceFile.toString())),
|
||||
new CreateResourceBackupRequest()
|
||||
).getPackagePath();
|
||||
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260414-000001");
|
||||
entity.setBackupId("RBKP-20260414-000101");
|
||||
entity.setPackageFileId("FILE-20260414-000001");
|
||||
entity.setPackagePath(packagePath);
|
||||
entity.setStagingDir(taskDir.resolve("staging").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("restore-state.json").toString());
|
||||
entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"RESTORE_FILES\",\"CONSISTENCY_VERIFY\"]");
|
||||
new HandoffFileStore(objectMapper).write(taskDir, entity);
|
||||
|
||||
PayloadArchiveRunnerStepExecutor executor = new PayloadArchiveRunnerStepExecutor(
|
||||
new HandoffFileStore(objectMapper),
|
||||
packageService,
|
||||
objectMapper
|
||||
);
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
entity.getTaskId(),
|
||||
"RESTORE_FILES",
|
||||
2,
|
||||
taskDir,
|
||||
taskDir.resolve("handoff.json"),
|
||||
taskDir.resolve("restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertTrue(result.getSummary().contains("payload extracted"));
|
||||
Assertions.assertTrue(Files.exists(taskDir.resolve("staging/payload/payload-manifest.json")));
|
||||
Assertions.assertEquals(
|
||||
"server.port=8088",
|
||||
Files.readString(taskDir.resolve("staging/payload/resources/TMS_CONFIG/application.yml"))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldVerifyExtractedPayloadManifest() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourcePackageService packageService = new ResourcePackageServiceImpl(
|
||||
backupProperties(),
|
||||
new ResourceEnvelopeCryptoServiceImpl(mockCrypto(), backupProperties()),
|
||||
objectMapper
|
||||
);
|
||||
Path taskDir = tempDir.resolve("restore-task-verify");
|
||||
Files.createDirectories(taskDir.resolve("staging/payload"));
|
||||
Files.writeString(taskDir.resolve("staging/payload/payload-manifest.json"), "{\"resources\":[{\"resourceCode\":\"TMS_CONFIG\"}]}");
|
||||
Files.writeString(taskDir.resolve("handoff.json"), "{}");
|
||||
|
||||
PayloadArchiveRunnerStepExecutor executor = new PayloadArchiveRunnerStepExecutor(
|
||||
new HandoffFileStore(objectMapper),
|
||||
packageService,
|
||||
objectMapper
|
||||
);
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000002",
|
||||
"CONSISTENCY_VERIFY",
|
||||
3,
|
||||
taskDir,
|
||||
taskDir.resolve("handoff.json"),
|
||||
taskDir.resolve("restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertTrue(result.getSummary().contains("resourceCount=1"));
|
||||
}
|
||||
|
||||
private ResourceBackupProperties backupProperties() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setOutputDir(tempDir.resolve("backup-output").toString());
|
||||
return properties;
|
||||
}
|
||||
|
||||
private Path writeFile(String relativePath, String content) throws Exception {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, content);
|
||||
return file;
|
||||
}
|
||||
|
||||
private static PcieCryptoService mockCrypto() {
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getData());
|
||||
});
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalDecryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getCipherBlob());
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any()))
|
||||
.thenReturn(backup(new byte[]{0x21, 0x22, 0x23}));
|
||||
Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any());
|
||||
return pcieCryptoService;
|
||||
}
|
||||
|
||||
private static BackupDataResult backup(byte[] data) {
|
||||
BackupDataResult result = new BackupDataResult();
|
||||
result.setData(data);
|
||||
result.setLength(data.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask() {
|
||||
InitTaskEntity entity = new InitTaskEntity();
|
||||
entity.setTaskId("INIT-20260408-000001");
|
||||
entity.setTaskType("INIT");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType("ENTERPRISE");
|
||||
entity.setMqType("RABBITMQ");
|
||||
entity.setOrgCode("AAAABBBBXXX");
|
||||
entity.setDeployMode("SINGLE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem resource(String code, String path) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("FILE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ResourceRestoreRunnerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldTakeOwnershipFromHandoffReadyState() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper);
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
RecordingRunnerStepExecutor stepExecutor = new RecordingRunnerStepExecutor();
|
||||
ResourceRestoreRunner runner = new ResourceRestoreRunner(
|
||||
handoffFileStore,
|
||||
stateFileStore,
|
||||
objectMapper,
|
||||
List.of(stepExecutor)
|
||||
);
|
||||
|
||||
ResourceRestoreTaskEntity entity = sampleTask();
|
||||
handoffFileStore.write(tempDir, entity);
|
||||
stateFileStore.initialize(tempDir, entity);
|
||||
stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
|
||||
runner.run(tempDir.resolve("handoff.json"));
|
||||
|
||||
JsonNode state = objectMapper.readTree(Files.readString(tempDir.resolve("restore-state.json")));
|
||||
Assertions.assertEquals("SUCCESS", state.path("status").asText());
|
||||
Assertions.assertEquals("RECONCILE_PENDING", state.path("controlPhase").asText());
|
||||
Assertions.assertEquals("RUNNER_FILE", state.path("stateSource").asText());
|
||||
Assertions.assertEquals("START_SERVICES", state.path("currentStepCode").asText());
|
||||
Assertions.assertEquals(3, state.path("currentStepNo").asInt());
|
||||
Assertions.assertFalse(state.path("runnerInstanceId").asText().isBlank());
|
||||
Assertions.assertTrue(state.path("runnerPid").asLong() > 0);
|
||||
Assertions.assertFalse(state.path("lastHeartbeatAt").asText().isBlank());
|
||||
Assertions.assertEquals(List.of("STOP_SERVICES", "START_SERVICES"), stepExecutor.executedSteps);
|
||||
JsonNode stepOutput = objectMapper.readTree(Files.readString(tempDir.resolve("step-output/02-STOP_SERVICES.json")));
|
||||
Assertions.assertEquals("STOP_SERVICES", stepOutput.path("stepCode").asText());
|
||||
Assertions.assertEquals("SUCCESS", stepOutput.path("result").asText());
|
||||
Assertions.assertEquals("executed STOP_SERVICES", stepOutput.path("summary").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDuplicateOwnershipWhenStateAlreadyTaken() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper);
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
RecordingRunnerStepExecutor stepExecutor = new RecordingRunnerStepExecutor();
|
||||
ResourceRestoreRunner runner = new ResourceRestoreRunner(
|
||||
handoffFileStore,
|
||||
stateFileStore,
|
||||
objectMapper,
|
||||
List.of(stepExecutor)
|
||||
);
|
||||
|
||||
ResourceRestoreTaskEntity entity = sampleTask();
|
||||
handoffFileStore.write(tempDir, entity);
|
||||
stateFileStore.initialize(tempDir, entity);
|
||||
stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
|
||||
runner.run(tempDir.resolve("handoff.json"));
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> runner.run(tempDir.resolve("handoff.json"))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("handoff ready"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistFailureWhenStepExecutorMissing() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper);
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
ResourceRestoreRunner runner = new ResourceRestoreRunner(
|
||||
handoffFileStore,
|
||||
stateFileStore,
|
||||
objectMapper,
|
||||
List.of()
|
||||
);
|
||||
|
||||
ResourceRestoreTaskEntity entity = sampleTask();
|
||||
handoffFileStore.write(tempDir, entity);
|
||||
stateFileStore.initialize(tempDir, entity);
|
||||
stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> runner.run(tempDir.resolve("handoff.json"))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("no runner step executor"));
|
||||
|
||||
JsonNode state = objectMapper.readTree(Files.readString(tempDir.resolve("restore-state.json")));
|
||||
Assertions.assertEquals("FAILED", state.path("status").asText());
|
||||
Assertions.assertEquals("RECONCILE_PENDING", state.path("controlPhase").asText());
|
||||
Assertions.assertEquals("STOP_SERVICES", state.path("currentStepCode").asText());
|
||||
Assertions.assertEquals(2, state.path("currentStepNo").asInt());
|
||||
Assertions.assertEquals("RUNNER_EXECUTION_FAILED", state.path("errorCode").asText());
|
||||
Assertions.assertTrue(state.path("errorMessage").asText().contains("no runner step executor"));
|
||||
JsonNode stepOutput = objectMapper.readTree(Files.readString(tempDir.resolve("step-output/02-STOP_SERVICES.json")));
|
||||
Assertions.assertEquals("FAILED", stepOutput.path("result").asText());
|
||||
Assertions.assertTrue(stepOutput.path("summary").asText().contains("no runner step executor"));
|
||||
}
|
||||
|
||||
private ResourceRestoreTaskEntity sampleTask() {
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260413-000001");
|
||||
entity.setPrecheckId("RPRE-20260413-000001");
|
||||
entity.setBackupId("RBKP-20260413-153000-000001");
|
||||
entity.setPackageFileId("FILE-20260413-000001");
|
||||
entity.setPackagePath("/tmp/backup.tmsbak");
|
||||
entity.setStagingDir(tempDir.resolve("staging").toString());
|
||||
entity.setRunnerStatePath(tempDir.resolve("restore-state.json").toString());
|
||||
entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"STOP_SERVICES\",\"START_SERVICES\"]");
|
||||
entity.setStatus("HANDOFF");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static class RecordingRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final List<String> executedSteps = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "STOP_SERVICES".equals(stepCode) || "START_SERVICES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
executedSteps.add(context.stepCode());
|
||||
return RunnerStepExecutionResult.success("executed " + context.stepCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class RestoreStateReconcilerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldOnlyReconcileTasksInReconcilePendingPhase() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
|
||||
ResourceRestoreTaskEntity running = task("RRST-TASK-001", tempDir.resolve("running/restore-state.json"));
|
||||
Files.createDirectories(runningStateDir(running));
|
||||
Files.writeString(Path.of(running.getRunnerStatePath()), """
|
||||
{"taskId":"RRST-TASK-001","status":"RUNNING","controlPhase":"RUNNER_OWNED","stateSource":"RUNNER_FILE"}
|
||||
""");
|
||||
ResourceRestoreTaskEntity pending = task("RRST-TASK-002", tempDir.resolve("pending/restore-state.json"));
|
||||
Files.createDirectories(runningStateDir(pending));
|
||||
Files.writeString(Path.of(pending.getRunnerStatePath()), """
|
||||
{"taskId":"RRST-TASK-002","status":"SUCCESS","controlPhase":"RECONCILE_PENDING","stateSource":"RUNNER_FILE","errorCode":"","errorMessage":""}
|
||||
""");
|
||||
Mockito.when(repository.findAll()).thenReturn(List.of(running, pending));
|
||||
|
||||
RestoreStateReconciler reconciler = new RestoreStateReconciler(repository, stateFileStore, LocalDateTime::now);
|
||||
reconciler.reconcileOnce();
|
||||
|
||||
ArgumentCaptor<ResourceRestoreTaskEntity> captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class);
|
||||
Mockito.verify(repository, Mockito.times(1)).update(captor.capture());
|
||||
Assertions.assertEquals("RRST-TASK-002", captor.getValue().getTaskId());
|
||||
|
||||
String stateContent = Files.readString(Path.of(pending.getRunnerStatePath()));
|
||||
Assertions.assertTrue(stateContent.contains("\"controlPhase\":\"DONE\""));
|
||||
}
|
||||
|
||||
private ResourceRestoreTaskEntity task(String taskId, Path statePath) {
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setStatus("HANDOFF");
|
||||
entity.setRunnerStatePath(statePath.toString());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Path runningStateDir(ResourceRestoreTaskEntity entity) {
|
||||
return Path.of(entity.getRunnerStatePath()).getParent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class RunnerCommandInvokerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExecuteCommandAndAppendLog() throws Exception {
|
||||
ProcessBuilderRunnerCommandInvoker invoker = new ProcessBuilderRunnerCommandInvoker();
|
||||
Path logFile = tempDir.resolve("steps/01-STOP_SERVICES.log");
|
||||
|
||||
int exitCode = invoker.run(
|
||||
List.of("/bin/zsh", "-lc", "printf 'hello-runner'"),
|
||||
logFile,
|
||||
tempDir
|
||||
);
|
||||
|
||||
Assertions.assertEquals(0, exitCode);
|
||||
Assertions.assertEquals("hello-runner", Files.readString(logFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class RunnerStallDetectorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldMarkRunnerOwnedTaskAsFailedWhenHeartbeatExpired() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRunnerHeartbeatTimeoutSeconds(60);
|
||||
|
||||
ResourceRestoreTaskEntity stale = task("RRST-TASK-003", tempDir.resolve("stale/restore-state.json"));
|
||||
Files.createDirectories(Path.of(stale.getRunnerStatePath()).getParent());
|
||||
Files.writeString(Path.of(stale.getRunnerStatePath()), """
|
||||
{"taskId":"RRST-TASK-003","status":"RUNNING","controlPhase":"RUNNER_OWNED","stateSource":"RUNNER_FILE","runnerInstanceId":"RRUN-001","lastHeartbeatAt":"2026-04-14T08:00:00","currentStepCode":"STOP_SERVICES","currentStepNo":2,"orphaned":false}
|
||||
""");
|
||||
Mockito.when(repository.findAll()).thenReturn(List.of(stale));
|
||||
|
||||
RunnerStallDetector detector = new RunnerStallDetector(repository, stateFileStore, properties, () -> LocalDateTime.parse("2026-04-14T08:05:01"));
|
||||
detector.detectOnce();
|
||||
|
||||
String content = Files.readString(Path.of(stale.getRunnerStatePath()));
|
||||
Assertions.assertTrue(content.contains("\"status\":\"FAILED\""));
|
||||
Assertions.assertTrue(content.contains("\"controlPhase\":\"RECONCILE_PENDING\""));
|
||||
Assertions.assertTrue(content.contains("\"orphaned\":true"));
|
||||
Assertions.assertTrue(content.contains("RUNNER_STALLED"));
|
||||
}
|
||||
|
||||
private ResourceRestoreTaskEntity task(String taskId, Path statePath) {
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setRunnerStatePath(statePath.toString());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ServiceCommandRunnerStepExecutorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldInvokeStopCommandsInExpectedOrder() {
|
||||
RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker();
|
||||
ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor(
|
||||
backupProperties(),
|
||||
initProperties(),
|
||||
invoker
|
||||
);
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000001",
|
||||
"STOP_SERVICES",
|
||||
2,
|
||||
tempDir,
|
||||
tempDir.resolve("handoff.json"),
|
||||
tempDir.resolve("restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertEquals("executed 3 command(s)", result.getSummary());
|
||||
Assertions.assertEquals(List.of(
|
||||
List.of("bash", "/mock/scripts/tms.sh", "stop"),
|
||||
List.of("/bin/zsh", "-lc", "/mock/bin/stop_standard_apps.sh"),
|
||||
List.of("/bin/zsh", "-lc", "/mock/bin/stop_standard_rabbitmq.sh")
|
||||
), invoker.commands);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInvokeStartCommandsInExpectedOrder() {
|
||||
RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker();
|
||||
ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor(
|
||||
backupProperties(),
|
||||
initProperties(),
|
||||
invoker
|
||||
);
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000001",
|
||||
"START_SERVICES",
|
||||
3,
|
||||
tempDir,
|
||||
tempDir.resolve("handoff.json"),
|
||||
tempDir.resolve("restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertEquals("executed 2 command(s)", result.getSummary());
|
||||
Assertions.assertEquals(List.of(
|
||||
List.of("/bin/zsh", "-lc", "/mock/bin/start_standard_apps.sh"),
|
||||
List.of("bash", "/mock/scripts/tms.sh", "start")
|
||||
), invoker.commands);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUnsupportedStep() {
|
||||
RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker();
|
||||
ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor(
|
||||
backupProperties(),
|
||||
initProperties(),
|
||||
invoker
|
||||
);
|
||||
|
||||
IllegalArgumentException exception = Assertions.assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000001",
|
||||
"RESTORE_DATABASES",
|
||||
4,
|
||||
tempDir,
|
||||
tempDir.resolve("handoff.json"),
|
||||
tempDir.resolve("restore-state.json")
|
||||
))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("unsupported runner step"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSurfaceCommandFailure() {
|
||||
RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker();
|
||||
invoker.exitCodes.add(1);
|
||||
ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor(
|
||||
backupProperties(),
|
||||
initProperties(),
|
||||
invoker
|
||||
);
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000001",
|
||||
"STOP_SERVICES",
|
||||
2,
|
||||
tempDir,
|
||||
tempDir.resolve("handoff.json"),
|
||||
tempDir.resolve("restore-state.json")
|
||||
))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("exitCode=1"));
|
||||
}
|
||||
|
||||
private ResourceBackupProperties backupProperties() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setTmsScriptPath("/mock/scripts/tms.sh");
|
||||
return properties;
|
||||
}
|
||||
|
||||
private InitExecutorProperties initProperties() {
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setStandardAppStartCommand("/mock/bin/start_standard_apps.sh");
|
||||
properties.setStandardAppStopCommand("/mock/bin/stop_standard_apps.sh");
|
||||
properties.setStandardRabbitStopCommand("/mock/bin/stop_standard_rabbitmq.sh");
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static class RecordingRunnerCommandInvoker implements RunnerCommandInvoker {
|
||||
|
||||
private final List<List<String>> commands = new ArrayList<>();
|
||||
private final List<Integer> exitCodes = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public int run(List<String> command, Path logFile, Path workingDirectory) {
|
||||
commands.add(List.copyOf(command));
|
||||
if (exitCodes.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return exitCodes.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceBackupServiceImpl;
|
||||
import com.cisd.tms.modules.backup.support.ResourceBackupDownloadResult;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourceBackupDownloadServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldLoadBackupPackageForBrowserDownload() throws Exception {
|
||||
Path packageFile = tempDir.resolve("RBKP-20260413-153000-000001.tmsbak");
|
||||
Files.writeString(packageFile, "backup-payload", StandardCharsets.UTF_8);
|
||||
|
||||
ResourceBackupTaskEntity entity = new ResourceBackupTaskEntity();
|
||||
entity.setTaskId("RBKP-TASK-20260413-000001");
|
||||
entity.setPackageName("RBKP-20260413-153000-000001.tmsbak");
|
||||
entity.setPackagePath(packageFile.toString());
|
||||
|
||||
ResourceBackupTaskRepository repository = Mockito.mock(ResourceBackupTaskRepository.class);
|
||||
Mockito.when(repository.findByTaskId("RBKP-TASK-20260413-000001")).thenReturn(Optional.of(entity));
|
||||
|
||||
ResourceBackupService service = new ResourceBackupServiceImpl(
|
||||
Mockito.mock(com.cisd.tms.modules.init.repository.InitTaskRepository.class),
|
||||
Mockito.mock(com.cisd.tms.modules.backup.discovery.ResourceDiscoveryService.class),
|
||||
Mockito.mock(com.cisd.tms.modules.backup.packagex.ResourcePackageService.class),
|
||||
repository,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new ResourceBackupProperties()
|
||||
);
|
||||
|
||||
ResourceBackupDownloadResult result = service.downloadBackupPackage("RBKP-TASK-20260413-000001");
|
||||
|
||||
Assertions.assertEquals("RBKP-20260413-153000-000001.tmsbak", result.getFilename());
|
||||
Assertions.assertEquals(packageFile.toAbsolutePath().normalize(), result.getPackagePath());
|
||||
Assertions.assertEquals(Files.size(packageFile), result.getContentLength());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.discovery.ResourceDiscoveryService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceBackupServiceImpl;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.cisd.tms.modules.init.repository.InitTaskRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourceBackupServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldCreateBackupTaskFromLatestSuccessfulInitSnapshot() {
|
||||
InitTaskRepository initTaskRepository = Mockito.mock(InitTaskRepository.class);
|
||||
ResourceDiscoveryService resourceDiscoveryService = Mockito.mock(ResourceDiscoveryService.class);
|
||||
ResourcePackageService resourcePackageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourceBackupTaskRepository resourceBackupTaskRepository = Mockito.mock(ResourceBackupTaskRepository.class);
|
||||
|
||||
InitTaskEntity initTask = sampleInitTask();
|
||||
List<DiscoveredResourceItem> discoveredItems = List.of(resource("TMS_CONFIG", "/home/tms/config"));
|
||||
ResourcePackageResult packageResult = new ResourcePackageResult();
|
||||
packageResult.setPackageName("RBKP-20260413-153000-000001.tmsbak");
|
||||
packageResult.setPackagePath("/tmp/resource-backup/RBKP-TASK-20260413-000001/RBKP-20260413-153000-000001.tmsbak");
|
||||
packageResult.setPackageSize(2048L);
|
||||
packageResult.setPackageHash("SM3-HASH");
|
||||
packageResult.setStorageEncKeyFingerprint("LOCAL-DEV-STORAGE-KEY");
|
||||
packageResult.setAuthenticityKeyFingerprint("LOCAL-DEV-AUTH-KEY");
|
||||
packageResult.setManifestSummaryJson("{\"resourceCount\":1}");
|
||||
|
||||
Mockito.when(initTaskRepository.findLatestByTaskTypeAndStatus("INIT", "SUCCESS")).thenReturn(Optional.of(initTask));
|
||||
Mockito.when(resourceDiscoveryService.discover(initTask)).thenReturn(discoveredItems);
|
||||
Mockito.when(resourcePackageService.createPackage(
|
||||
Mockito.anyString(),
|
||||
Mockito.anyString(),
|
||||
Mockito.same(initTask),
|
||||
Mockito.eq(discoveredItems),
|
||||
Mockito.any(CreateResourceBackupRequest.class)
|
||||
)).thenReturn(packageResult);
|
||||
|
||||
ResourceBackupService service = new ResourceBackupServiceImpl(
|
||||
initTaskRepository,
|
||||
resourceDiscoveryService,
|
||||
resourcePackageService,
|
||||
resourceBackupTaskRepository,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new ResourceBackupProperties()
|
||||
);
|
||||
|
||||
CreateResourceBackupRequest request = new CreateResourceBackupRequest();
|
||||
request.setIncludeUpgradeHistory(true);
|
||||
request.setIncludeAuditBackups(false);
|
||||
request.setRemark("before hardware replacement");
|
||||
|
||||
CreateResourceBackupResponse response = service.createBackupTask(request);
|
||||
|
||||
Assertions.assertTrue(response.getTaskId().startsWith("RBKP-TASK-"));
|
||||
Assertions.assertTrue(response.getBackupId().startsWith("RBKP-"));
|
||||
Assertions.assertEquals("SUCCESS", response.getStatus());
|
||||
Assertions.assertNotNull(response.getCreatedAt());
|
||||
|
||||
ArgumentCaptor<ResourceBackupTaskEntity> captor = ArgumentCaptor.forClass(ResourceBackupTaskEntity.class);
|
||||
Mockito.verify(resourceBackupTaskRepository).save(captor.capture());
|
||||
ResourceBackupTaskEntity saved = captor.getValue();
|
||||
Assertions.assertEquals(response.getTaskId(), saved.getTaskId());
|
||||
Assertions.assertEquals(response.getBackupId(), saved.getBackupId());
|
||||
Assertions.assertEquals("SUCCESS", saved.getStatus());
|
||||
Assertions.assertEquals("ENTERPRISE", saved.getProductType());
|
||||
Assertions.assertEquals("RABBITMQ", saved.getMqType());
|
||||
Assertions.assertEquals("AAAABBBBXXX", saved.getOrgCode());
|
||||
Assertions.assertEquals("before hardware replacement", saved.getRemark());
|
||||
Assertions.assertEquals(packageResult.getPackagePath(), saved.getPackagePath());
|
||||
Assertions.assertEquals(packageResult.getPackageHash(), saved.getPackageHash());
|
||||
Assertions.assertEquals(packageResult.getManifestSummaryJson(), saved.getManifestSummaryJson());
|
||||
Assertions.assertEquals(packageResult.getStorageEncKeyFingerprint(), saved.getStorageEncKeyFingerprint());
|
||||
Assertions.assertEquals(packageResult.getAuthenticityKeyFingerprint(), saved.getAuthenticityKeyFingerprint());
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask() {
|
||||
InitTaskEntity entity = new InitTaskEntity();
|
||||
entity.setTaskId("INIT-20260408-000001");
|
||||
entity.setTaskType("INIT");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType("ENTERPRISE");
|
||||
entity.setMqType("RABBITMQ");
|
||||
entity.setOrgCode("AAAABBBBXXX");
|
||||
entity.setDeployMode("SINGLE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem resource(String code, String path) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("FILE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageServiceImpl;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourcePrecheckServiceImpl;
|
||||
import com.cisd.tms.modules.backup.support.InMemoryResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourcePrecheckServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldOnlyVerifyHeaderWithoutDecryptingPayload() {
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourceBackupProperties backupProperties = new ResourceBackupProperties();
|
||||
backupProperties.setOutputDir(tempDir.toString());
|
||||
backupProperties.setStorageEncKeyIndex(7);
|
||||
backupProperties.setAuthenticitySignKeyIndex(9);
|
||||
PcieCryptoService pcieCryptoService = mockCrypto();
|
||||
ResourcePackageService packageService = new ResourcePackageServiceImpl(
|
||||
backupProperties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, backupProperties),
|
||||
new ObjectMapper()
|
||||
);
|
||||
ResourcePackageResult packageResult = packageService.createPackage(
|
||||
"RBKP-TASK-20260414-000001",
|
||||
"RBKP-20260414-000001",
|
||||
sampleInitTask(),
|
||||
List.of(resource("TMS_CONFIG", "/home/tms/config")),
|
||||
new CreateResourceBackupRequest()
|
||||
);
|
||||
FileRecordEntity fileRecordEntity = new FileRecordEntity();
|
||||
fileRecordEntity.setFileId("FILE-20260414-000001");
|
||||
fileRecordEntity.setStoragePath(packageResult.getPackagePath());
|
||||
Mockito.when(fileRecordRepository.findByFileId("FILE-20260414-000001")).thenReturn(Optional.of(fileRecordEntity));
|
||||
|
||||
CisdPresetProperties presetProperties = new CisdPresetProperties();
|
||||
presetProperties.setProductType("ENTERPRISE");
|
||||
ResourcePrecheckService service = new ResourcePrecheckServiceImpl(
|
||||
fileRecordRepository,
|
||||
packageService,
|
||||
new InMemoryResourcePrecheckStateStore(),
|
||||
new ResourceTaskIdGenerator(),
|
||||
presetProperties,
|
||||
backupProperties
|
||||
);
|
||||
|
||||
ResourceRestorePrecheckResponse response = service.precheck(sampleRequest());
|
||||
|
||||
Assertions.assertTrue(response.getSignatureValid());
|
||||
Assertions.assertTrue(response.getKeysetMatched());
|
||||
Assertions.assertFalse(response.getPayloadDecrypted());
|
||||
Assertions.assertInstanceOf(java.util.Map.class, response.getRestoreScopeSummary());
|
||||
Assertions.assertTrue(response.getRestoreScopeSummary().toString().contains("resourceCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRestoreWhenKeyFingerprintDoesNotMatch() {
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourceBackupProperties backupProperties = new ResourceBackupProperties();
|
||||
backupProperties.setOutputDir(tempDir.toString());
|
||||
backupProperties.setStorageEncKeyIndex(1);
|
||||
backupProperties.setAuthenticitySignKeyIndex(1);
|
||||
PcieCryptoService pcieCryptoService = mockCrypto();
|
||||
ResourcePackageService packageService = new ResourcePackageServiceImpl(
|
||||
backupProperties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, backupProperties),
|
||||
new ObjectMapper()
|
||||
);
|
||||
ResourcePackageResult packageResult = packageService.createPackage(
|
||||
"RBKP-TASK-20260414-000002",
|
||||
"RBKP-20260414-000002",
|
||||
sampleInitTask(),
|
||||
List.of(resource("TMS_CONFIG", "/home/tms/config")),
|
||||
new CreateResourceBackupRequest()
|
||||
);
|
||||
FileRecordEntity fileRecordEntity = new FileRecordEntity();
|
||||
fileRecordEntity.setFileId("FILE-20260414-000002");
|
||||
fileRecordEntity.setStoragePath(packageResult.getPackagePath());
|
||||
Mockito.when(fileRecordRepository.findByFileId("FILE-20260414-000002")).thenReturn(Optional.of(fileRecordEntity));
|
||||
|
||||
CisdPresetProperties presetProperties = new CisdPresetProperties();
|
||||
presetProperties.setProductType("ENTERPRISE");
|
||||
ResourceBackupProperties mismatched = new ResourceBackupProperties();
|
||||
mismatched.setStorageEncKeyIndex(3);
|
||||
mismatched.setAuthenticitySignKeyIndex(5);
|
||||
ResourcePrecheckService service = new ResourcePrecheckServiceImpl(
|
||||
fileRecordRepository,
|
||||
packageService,
|
||||
new InMemoryResourcePrecheckStateStore(),
|
||||
new ResourceTaskIdGenerator(),
|
||||
presetProperties,
|
||||
mismatched
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class, () -> service.precheck(sampleMismatchRequest()));
|
||||
Assertions.assertTrue(exception.getMessage().contains("指纹"));
|
||||
Assertions.assertTrue(exception.getMessage().contains("不匹配"));
|
||||
}
|
||||
|
||||
private ResourceRestorePrecheckRequest sampleRequest() {
|
||||
ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest();
|
||||
request.setFileId("FILE-20260414-000001");
|
||||
return request;
|
||||
}
|
||||
|
||||
private ResourceRestorePrecheckRequest sampleMismatchRequest() {
|
||||
ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest();
|
||||
request.setFileId("FILE-20260414-000002");
|
||||
return request;
|
||||
}
|
||||
|
||||
private static PcieCryptoService mockCrypto() {
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getData());
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any()))
|
||||
.thenReturn(backup(new byte[]{0x21, 0x22, 0x23}));
|
||||
Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any());
|
||||
return pcieCryptoService;
|
||||
}
|
||||
|
||||
private static BackupDataResult backup(byte[] data) {
|
||||
BackupDataResult result = new BackupDataResult();
|
||||
result.setData(data);
|
||||
result.setLength(data.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask() {
|
||||
InitTaskEntity entity = new InitTaskEntity();
|
||||
entity.setTaskId("INIT-20260408-000001");
|
||||
entity.setTaskType("INIT");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType("ENTERPRISE");
|
||||
entity.setMqType("RABBITMQ");
|
||||
entity.setOrgCode("AAAABBBBXXX");
|
||||
entity.setDeployMode("SINGLE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem resource(String code, String path) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("FILE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceRestoreServiceImpl;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.backup.support.file.FileSystemResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourceRestoreHandoffServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldCreateRestoreTaskAndWriteHandoffFiles() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setPrecheckStoreDir(tempDir.resolve("prechecks").toString());
|
||||
properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString());
|
||||
FileSystemResourcePrecheckStateStore store = new FileSystemResourcePrecheckStateStore(properties, new ObjectMapper());
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Path packagePath = tempDir.resolve("download").resolve("RBKP-20260413-153000-000001.tmsbak");
|
||||
Files.createDirectories(packagePath.getParent());
|
||||
Files.writeString(packagePath, "package-bytes");
|
||||
|
||||
ResourceRestorePrecheckResponse precheckResponse = new ResourceRestorePrecheckResponse();
|
||||
precheckResponse.setPrecheckId("RPRE-20260413-000001");
|
||||
precheckResponse.setBackupId("RBKP-20260413-153000-000001");
|
||||
precheckResponse.setSourceProductType("ENTERPRISE");
|
||||
precheckResponse.setTargetProductType("ENTERPRISE");
|
||||
precheckResponse.setKeysetMatched(true);
|
||||
precheckResponse.setSignatureValid(true);
|
||||
precheckResponse.setCompatible(true);
|
||||
precheckResponse.setRestorePlan(java.util.List.of("STOP_SERVICES", "RESTORE_DATABASES", "START_SERVICES"));
|
||||
store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse));
|
||||
FileRecordEntity fileRecordEntity = new FileRecordEntity();
|
||||
fileRecordEntity.setFileId("FILE-20260413-000001");
|
||||
fileRecordEntity.setStoragePath(packagePath.toString());
|
||||
Mockito.when(fileRecordRepository.findByFileId("FILE-20260413-000001"))
|
||||
.thenReturn(java.util.Optional.of(fileRecordEntity));
|
||||
|
||||
ResourceRestoreService service = new ResourceRestoreServiceImpl(
|
||||
repository,
|
||||
fileRecordRepository,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new HandoffFileStore(objectMapper),
|
||||
new ResourceRestoreStateFileStore(objectMapper),
|
||||
properties
|
||||
);
|
||||
|
||||
CreateResourceRestoreRequest request = new CreateResourceRestoreRequest();
|
||||
request.setPrecheckId("RPRE-20260413-000001");
|
||||
request.setConfirmBackupId("RBKP-20260413-153000-000001");
|
||||
request.setConfirmRiskAccepted(true);
|
||||
|
||||
CreateResourceRestoreResponse response = service.createRestoreTask(request);
|
||||
|
||||
Path taskDir = tempDir.resolve("resource-restore").resolve(response.getTaskId());
|
||||
Path handoffFile = taskDir.resolve("handoff.json");
|
||||
Path stateFile = taskDir.resolve("restore-state.json");
|
||||
Assertions.assertTrue(Files.exists(handoffFile));
|
||||
Assertions.assertTrue(Files.exists(stateFile));
|
||||
|
||||
JsonNode handoff = objectMapper.readTree(Files.readString(handoffFile));
|
||||
Assertions.assertEquals(response.getTaskId(), handoff.path("taskId").asText());
|
||||
Assertions.assertEquals("RBKP-20260413-153000-000001", handoff.path("backupId").asText());
|
||||
Assertions.assertEquals("FILE-20260413-000001", handoff.path("packageFileId").asText());
|
||||
Assertions.assertEquals(packagePath.toString(), handoff.path("packagePath").asText());
|
||||
|
||||
JsonNode state = objectMapper.readTree(Files.readString(stateFile));
|
||||
Assertions.assertEquals("HANDOFF_READY", state.path("controlPhase").asText());
|
||||
Assertions.assertEquals("HANDOFF", state.path("status").asText());
|
||||
Assertions.assertEquals("RESTORE_LOCK", state.path("currentStepCode").asText());
|
||||
Assertions.assertEquals(1, state.path("currentStepNo").asInt());
|
||||
|
||||
ArgumentCaptor<ResourceRestoreTaskEntity> captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class);
|
||||
Mockito.verify(repository).save(captor.capture());
|
||||
ResourceRestoreTaskEntity saved = captor.getValue();
|
||||
Assertions.assertEquals(stateFile.toString(), saved.getRunnerStatePath());
|
||||
Assertions.assertTrue(saved.getStagingDir().contains(response.getTaskId()));
|
||||
Assertions.assertEquals("HANDOFF", saved.getStatus());
|
||||
Assertions.assertEquals(packagePath.toString(), saved.getPackagePath());
|
||||
}
|
||||
}
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl;
|
||||
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.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageServiceImpl;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourcePrecheckServiceImpl;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceRestoreServiceImpl;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl;
|
||||
import com.cisd.tms.modules.backup.support.InMemoryResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourceRestoreSupportServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldParseRealBackupPackageByFileIdAndCachePrecheckContext() {
|
||||
InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore();
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
CisdPresetProperties presetProperties = new CisdPresetProperties();
|
||||
presetProperties.setProductType("ENTERPRISE");
|
||||
ResourceBackupProperties backupProperties = new ResourceBackupProperties();
|
||||
backupProperties.setOutputDir(tempDir.toString());
|
||||
Path payloadFile = writePayloadFile("runtime/application.yml", "server.port=9090");
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getData());
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any()))
|
||||
.thenReturn(backup(new byte[]{0x21, 0x22, 0x23}));
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any()))
|
||||
.thenAnswer(invocation -> {
|
||||
com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getCipherBlob());
|
||||
});
|
||||
|
||||
ResourcePackageService packageService = new ResourcePackageServiceImpl(
|
||||
backupProperties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, backupProperties),
|
||||
new ObjectMapper()
|
||||
);
|
||||
ResourcePackageResult packageResult = packageService.createPackage(
|
||||
"RBKP-TASK-20260413-000001",
|
||||
"RBKP-20260413-153000-000001",
|
||||
sampleInitTask(),
|
||||
java.util.List.of(resource("TMS_CONFIG", payloadFile.toString())),
|
||||
new com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest()
|
||||
);
|
||||
|
||||
FileRecordEntity fileRecordEntity = new FileRecordEntity();
|
||||
fileRecordEntity.setFileId("FILE-20260413-000001");
|
||||
fileRecordEntity.setStoragePath(packageResult.getPackagePath());
|
||||
Mockito.when(fileRecordRepository.findByFileId("FILE-20260413-000001")).thenReturn(Optional.of(fileRecordEntity));
|
||||
|
||||
ResourcePrecheckService service = new ResourcePrecheckServiceImpl(
|
||||
fileRecordRepository,
|
||||
packageService,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
presetProperties,
|
||||
backupProperties
|
||||
);
|
||||
|
||||
ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest();
|
||||
request.setFileId("FILE-20260413-000001");
|
||||
|
||||
ResourceRestorePrecheckResponse response = service.precheck(request);
|
||||
|
||||
Assertions.assertTrue(response.getPrecheckId().startsWith("RPRE-"));
|
||||
Assertions.assertEquals("RBKP-20260413-153000-000001", response.getBackupId());
|
||||
Assertions.assertEquals("ENTERPRISE", response.getSourceProductType());
|
||||
Assertions.assertEquals("RABBITMQ", response.getSourceMqType());
|
||||
Assertions.assertEquals("AAAABBBBXXX", response.getSourceOrgCode());
|
||||
Assertions.assertEquals("ENTERPRISE", response.getTargetProductType());
|
||||
Assertions.assertTrue(response.getKeysetMatched());
|
||||
Assertions.assertTrue(response.getSignatureValid());
|
||||
Assertions.assertTrue(response.getCompatible());
|
||||
Assertions.assertFalse(response.getPayloadDecrypted());
|
||||
Assertions.assertEquals("RESTORE_LOCK", response.getRestorePlan().get(0));
|
||||
|
||||
ResourcePrecheckContext context = store.find(response.getPrecheckId()).orElseThrow();
|
||||
Assertions.assertEquals("FILE-20260413-000001", context.getFileId());
|
||||
Assertions.assertEquals(response.getBackupId(), context.getResponse().getBackupId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateAndQueryRestoreTaskFromCachedPrecheck() {
|
||||
InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore();
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
Path packagePath = tempDir.resolve("download").resolve("RBKP-20260413-153000-000001.tmsbak");
|
||||
try {
|
||||
Files.createDirectories(packagePath.getParent());
|
||||
Files.writeString(packagePath, "backup-package");
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
|
||||
ResourceRestorePrecheckResponse precheckResponse = new ResourceRestorePrecheckResponse();
|
||||
precheckResponse.setPrecheckId("RPRE-20260413-000001");
|
||||
precheckResponse.setBackupId("RBKP-20260413-153000-000001");
|
||||
precheckResponse.setSourceProductType("ENTERPRISE");
|
||||
precheckResponse.setTargetProductType("ENTERPRISE");
|
||||
precheckResponse.setKeysetMatched(true);
|
||||
precheckResponse.setSignatureValid(true);
|
||||
precheckResponse.setCompatible(true);
|
||||
store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse));
|
||||
FileRecordEntity fileRecordEntity = new FileRecordEntity();
|
||||
fileRecordEntity.setFileId("FILE-20260413-000001");
|
||||
fileRecordEntity.setStoragePath(packagePath.toString());
|
||||
Mockito.when(fileRecordRepository.findByFileId("FILE-20260413-000001"))
|
||||
.thenReturn(Optional.of(fileRecordEntity));
|
||||
|
||||
ResourceRestoreService restoreService = new ResourceRestoreServiceImpl(
|
||||
repository,
|
||||
fileRecordRepository,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new HandoffFileStore(new ObjectMapper()),
|
||||
new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
restoreProperties()
|
||||
);
|
||||
|
||||
CreateResourceRestoreRequest request = new CreateResourceRestoreRequest();
|
||||
request.setPrecheckId("RPRE-20260413-000001");
|
||||
request.setConfirmBackupId("RBKP-20260413-153000-000001");
|
||||
request.setConfirmRiskAccepted(true);
|
||||
|
||||
CreateResourceRestoreResponse response = restoreService.createRestoreTask(request);
|
||||
|
||||
Assertions.assertTrue(response.getTaskId().startsWith("RRST-TASK-"));
|
||||
Assertions.assertEquals("HANDOFF", response.getStatus());
|
||||
|
||||
ArgumentCaptor<ResourceRestoreTaskEntity> captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class);
|
||||
Mockito.verify(repository).save(captor.capture());
|
||||
ResourceRestoreTaskEntity saved = captor.getValue();
|
||||
Assertions.assertEquals(response.getTaskId(), saved.getTaskId());
|
||||
Assertions.assertEquals("RPRE-20260413-000001", saved.getPrecheckId());
|
||||
Assertions.assertEquals("RBKP-20260413-153000-000001", saved.getBackupId());
|
||||
Assertions.assertEquals("FILE-20260413-000001", saved.getPackageFileId());
|
||||
Assertions.assertEquals(packagePath.toString(), saved.getPackagePath());
|
||||
Assertions.assertEquals("ENTERPRISE", saved.getSourceProductType());
|
||||
Assertions.assertEquals("ENTERPRISE", saved.getTargetProductType());
|
||||
Assertions.assertTrue(saved.getStagingDir().contains(response.getTaskId()));
|
||||
Assertions.assertTrue(saved.getRunnerStatePath().contains(response.getTaskId()));
|
||||
|
||||
Mockito.when(repository.findByTaskId(response.getTaskId())).thenReturn(Optional.of(saved));
|
||||
ResourceTaskQueryService queryService = new ResourceTaskQueryServiceImpl(
|
||||
repository,
|
||||
new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
ResourceRestoreTaskDetailResponse detail = queryService.getRestoreTask(response.getTaskId());
|
||||
|
||||
Assertions.assertEquals(response.getTaskId(), detail.getTaskId());
|
||||
Assertions.assertEquals("HANDOFF", detail.getStatus());
|
||||
Assertions.assertEquals("DB", detail.getStateSource());
|
||||
Assertions.assertEquals("HANDOFF_READY", detail.getControlPhase());
|
||||
Assertions.assertEquals("FILE-20260413-000001", detail.getPackageFileId());
|
||||
Assertions.assertEquals("RESTORE_LOCK", detail.getCurrentStepCode());
|
||||
Assertions.assertEquals(1, detail.getCurrentStepNo());
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask() {
|
||||
InitTaskEntity entity = new InitTaskEntity();
|
||||
entity.setTaskId("INIT-20260408-000001");
|
||||
entity.setTaskType("INIT");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType("ENTERPRISE");
|
||||
entity.setMqType("RABBITMQ");
|
||||
entity.setOrgCode("AAAABBBBXXX");
|
||||
entity.setDeployMode("SINGLE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem resource(String code, String path) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("FILE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
|
||||
private Path writePayloadFile(String relativePath, String content) {
|
||||
try {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, content);
|
||||
return file;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static BackupDataResult backup(byte[] data) {
|
||||
BackupDataResult result = new BackupDataResult();
|
||||
result.setData(data);
|
||||
result.setLength(data.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private ResourceBackupProperties restoreProperties() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString());
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourceTaskQueryServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldPreferRunnerStateFileWhenPresent() throws Exception {
|
||||
Path taskDir = tempDir.resolve("RRST-TASK-20260413-000001");
|
||||
Files.createDirectories(taskDir);
|
||||
Path stateFile = taskDir.resolve("restore-state.json");
|
||||
Files.writeString(stateFile, """
|
||||
{
|
||||
"taskId":"RRST-TASK-20260413-000001",
|
||||
"status":"RUNNING",
|
||||
"controlPhase":"RUNNER_OWNED",
|
||||
"stateSource":"RUNNER_FILE",
|
||||
"runnerInstanceId":"RRUN-20260413-000001",
|
||||
"runnerPid":1234,
|
||||
"ownershipToken":"TOKEN-001",
|
||||
"lastHeartbeatAt":"2026-04-13T17:10:00",
|
||||
"orphaned":false,
|
||||
"cleanupWarnings":["cleanup skipped"]
|
||||
}
|
||||
""");
|
||||
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260413-000001");
|
||||
entity.setPrecheckId("RPRE-20260413-000001");
|
||||
entity.setBackupId("RBKP-20260413-153000-000001");
|
||||
entity.setStatus("PENDING");
|
||||
entity.setSourceProductType("ENTERPRISE");
|
||||
entity.setTargetProductType("ENTERPRISE");
|
||||
entity.setPackageFileId("FILE-20260413-000001");
|
||||
entity.setRunnerStatePath(stateFile.toString());
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(LocalDateTime.parse("2026-04-13T17:00:00"));
|
||||
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000001")).thenReturn(Optional.of(entity));
|
||||
|
||||
ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
repository,
|
||||
new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
var detail = service.getRestoreTask("RRST-TASK-20260413-000001");
|
||||
|
||||
Assertions.assertEquals("RUNNING", detail.getStatus());
|
||||
Assertions.assertEquals("RUNNER_OWNED", detail.getControlPhase());
|
||||
Assertions.assertEquals("RUNNER_FILE", detail.getStateSource());
|
||||
Assertions.assertEquals("2026-04-13T17:10:00", detail.getLastHeartbeatAt());
|
||||
Assertions.assertEquals(Boolean.FALSE, detail.getOrphaned());
|
||||
Assertions.assertEquals(1, detail.getCleanupWarnings().size());
|
||||
Assertions.assertEquals("cleanup skipped", detail.getCleanupWarnings().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFallbackToDatabaseStateWhenRunnerStateFileMissing() {
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260413-000002");
|
||||
entity.setPrecheckId("RPRE-20260413-000002");
|
||||
entity.setBackupId("RBKP-20260413-153500-000002");
|
||||
entity.setStatus("PENDING");
|
||||
entity.setSourceProductType("ENTERPRISE");
|
||||
entity.setTargetProductType("ENTERPRISE");
|
||||
entity.setPackageFileId("FILE-20260413-000002");
|
||||
entity.setRunnerStatePath(tempDir.resolve("missing.json").toString());
|
||||
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000002")).thenReturn(Optional.of(entity));
|
||||
|
||||
ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
repository,
|
||||
new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
var detail = service.getRestoreTask("RRST-TASK-20260413-000002");
|
||||
|
||||
Assertions.assertEquals("PENDING", detail.getStatus());
|
||||
Assertions.assertEquals("PRECHECKED", detail.getControlPhase());
|
||||
Assertions.assertEquals("DB", detail.getStateSource());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeRestoreStepsAndLogsFromRunnerArtifacts() throws Exception {
|
||||
Path taskDir = tempDir.resolve("RRST-TASK-20260413-000003");
|
||||
Files.createDirectories(taskDir.resolve("step-output"));
|
||||
Files.createDirectories(taskDir.resolve("steps"));
|
||||
Path stateFile = taskDir.resolve("restore-state.json");
|
||||
Files.writeString(stateFile, """
|
||||
{
|
||||
"taskId":"RRST-TASK-20260413-000003",
|
||||
"status":"RUNNING",
|
||||
"controlPhase":"RUNNER_OWNED",
|
||||
"stateSource":"RUNNER_FILE",
|
||||
"currentStepCode":"START_SERVICES",
|
||||
"currentStepNo":3
|
||||
}
|
||||
""");
|
||||
Files.writeString(taskDir.resolve("step-output/02-STOP_SERVICES.json"), """
|
||||
{
|
||||
"stepCode":"STOP_SERVICES",
|
||||
"result":"SUCCESS",
|
||||
"summary":"stopped services"
|
||||
}
|
||||
""");
|
||||
Files.writeString(taskDir.resolve("steps/02-STOP_SERVICES.log"), "stop log line");
|
||||
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260413-000003");
|
||||
entity.setStatus("HANDOFF");
|
||||
entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"STOP_SERVICES\",\"START_SERVICES\"]");
|
||||
entity.setRunnerStatePath(stateFile.toString());
|
||||
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000003")).thenReturn(Optional.of(entity));
|
||||
|
||||
ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
repository,
|
||||
new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
List<ResourceTaskStepItemResponse> steps = service.getRestoreTaskSteps("RRST-TASK-20260413-000003");
|
||||
Assertions.assertEquals(3, steps.size());
|
||||
Assertions.assertEquals("PENDING", steps.get(0).getStatus());
|
||||
Assertions.assertEquals("SUCCESS", steps.get(1).getStatus());
|
||||
Assertions.assertEquals("stopped services", steps.get(1).getMessage());
|
||||
Assertions.assertEquals(Boolean.TRUE, steps.get(1).getLogAvailable());
|
||||
Assertions.assertEquals("RUNNING", steps.get(2).getStatus());
|
||||
|
||||
ResourceTaskStepLogResponse log = service.getRestoreTaskStepLog("RRST-TASK-20260413-000003", 2);
|
||||
Assertions.assertEquals("STOP_SERVICES", log.getStepCode());
|
||||
Assertions.assertEquals("stop log line", log.getContent());
|
||||
Assertions.assertFalse(log.getTruncated());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user