资源备份恢复
This commit is contained in:
parent
350fa4d89d
commit
2efdc047e3
49
README.md
49
README.md
@ -32,11 +32,14 @@ cp target/tms-framework-*.jar /home/tms/tms-framework.jar
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /home/tms/{bin,config,scripts,logs,run}
|
||||
sudo mkdir -p /home/tms/bin/resource-restore
|
||||
sudo chown -R "$(whoami)":"$(whoami)" /home/tms
|
||||
cp scripts/tms.sh /home/tms/scripts/tms.sh
|
||||
cp scripts/standard-init/*.sh /home/tms/bin/
|
||||
cp scripts/resource-restore/*.sh /home/tms/bin/resource-restore/
|
||||
cp config/application.yml.example /home/tms/config/application.yml
|
||||
chmod +x /home/tms/bin/*.sh
|
||||
chmod +x /home/tms/bin/resource-restore/*.sh
|
||||
chmod +x /home/tms/scripts/tms.sh
|
||||
```
|
||||
|
||||
@ -55,6 +58,7 @@ chmod +x /home/tms/scripts/tms.sh
|
||||
- 可通过 `JAR_PATH=/home/tms/tms-framework.jar` 覆盖 jar 路径。
|
||||
- 如果部署路径不是 `/home/tms`,使用 `APP_HOME=/your/path /your/path/scripts/tms.sh start`。
|
||||
- 标准 CISD 初始化辅助脚本位于 `scripts/standard-init/`,部署时需复制到 `/home/tms/bin/`。
|
||||
- 资源恢复辅助脚本位于 `scripts/resource-restore/`,部署时需复制到 `/home/tms/bin/resource-restore/`。
|
||||
- `apply_standard_db.sh` 依赖预置环境变量,例如 `DB_USER`、`DB_PASSWORD`,不要直接写入 `application.yml`。
|
||||
- 运行目录结构、配置项说明、文件上传 `fileId` 流程和故障排查,请查看上面的完整部署手册。
|
||||
|
||||
@ -234,24 +238,67 @@ Open:
|
||||
|
||||
当前实现边界:
|
||||
- 资源备份包输出为 `.tmsbak`,包内包含 `manifest.json`、`envelope.json`、`payload.enc`、`signature.sig`
|
||||
- 当前第一版备份/恢复只覆盖最小资源集:
|
||||
- `TMS_CONFIG`
|
||||
- `TMS_DB_DUMP`
|
||||
- `CMEP_DB_DUMP`
|
||||
- `CP_CONFIG`
|
||||
- `RECEIVER_LICENSE`
|
||||
- 预检阶段只做包头解析、指纹匹配和验签,不提前解密完整 payload
|
||||
- 恢复任务创建后会生成 `handoff.json`、`restore-state.json`,并进入 `HANDOFF_READY`
|
||||
- `ResourceRestoreRunner` 当前已具备接管、步骤循环、`step-output/*.json`、心跳刷新、`RECONCILE_PENDING` 推进能力
|
||||
- `STOP_SERVICES` / `START_SERVICES` 已对接现有 `tms.sh` 与标准版启停脚本配置
|
||||
- runner 现已通过外部脚本启动独立进程,不再运行在 TMS Web 进程内
|
||||
- 当前已落地的恢复步骤:
|
||||
- `STOP_SERVICES`
|
||||
- `RESTORE_FILES`
|
||||
- `RESTORE_DATABASES`
|
||||
- `START_SERVICES`
|
||||
- `CONSISTENCY_VERIFY`
|
||||
- `RESTORE_FILES` 当前只回写关键配置/License 文件,不做目录级全量恢复
|
||||
- `RESTORE_DATABASES` 当前恢复:
|
||||
- `db/TMS.sql`
|
||||
- `db/CMEP.sql`(包内存在时)
|
||||
- `CONSISTENCY_VERIFY` 当前只检查:
|
||||
- `RESTORE_FILES` 成功
|
||||
- `RESTORE_DATABASES` 成功
|
||||
- `START_SERVICES` 成功
|
||||
- 健康检查地址返回 `UP`
|
||||
|
||||
关键配置:
|
||||
- `tms.backup.output-dir`
|
||||
- `tms.backup.precheck-store-dir`
|
||||
- `tms.backup.restore-task-root-dir`
|
||||
- `tms.backup.tms-script-path`
|
||||
- `tms.backup.runner-launch-script-path`
|
||||
- `tms.backup.mysqldump-path`
|
||||
- `tms.backup.mysql-path`
|
||||
- `tms.backup.restore-db-script-path`
|
||||
- `tms.backup.tms-database-name`
|
||||
- `tms.backup.cmep-database-name`
|
||||
- `tms.backup.health-check-url`
|
||||
- `tms.backup.health-check-timeout-seconds`
|
||||
- `tms.backup.runner-auto-launch-enabled`
|
||||
- `tms.backup.runner-heartbeat-timeout-seconds`
|
||||
|
||||
恢复前置条件:
|
||||
- LMK、IK、数据存储加密密钥对、数据真实性保护密钥对需先通过 UKey 恢复
|
||||
- 默认不自动拉起本机 runner;如需在现场机器自动执行,显式开启 `tms.backup.runner-auto-launch-enabled=true`
|
||||
- `/home/tms/bin/resource-restore/start-runner.sh` 和 `/home/tms/bin/resource-restore/restore-db.sh` 必须已部署并可执行
|
||||
- `mysql` / `mysqldump` 客户端必须可用
|
||||
|
||||
当前明确不在第一版恢复范围内:
|
||||
- nginx 配置
|
||||
- `organization.json`
|
||||
- 已发布 web 静态资源
|
||||
- `/home/tms/uploads`
|
||||
- RabbitMQ / TLQ / CFMQ
|
||||
- `CMEP` / 机构库
|
||||
- 在线恢复 / 增量恢复 / 回滚
|
||||
- 升级执行日志统一写入 `tms.upgrade.log-dir`
|
||||
|
||||
联调清单:
|
||||
- [资源备份恢复 V1 联调清单](/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-04-18-resource-backup-restore-v1-checklist.md)
|
||||
|
||||
当前配置项:
|
||||
- `tms.upgrade.staging-root-dir`
|
||||
- `tms.upgrade.log-dir`
|
||||
|
||||
64
scripts/resource-restore/restore-db.sh
Executable file
64
scripts/resource-restore/restore-db.sh
Executable file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MYSQL_PATH="mysql"
|
||||
HOST=""
|
||||
PORT=""
|
||||
USERNAME=""
|
||||
PASSWORD=""
|
||||
DB_NAME=""
|
||||
SQL_FILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mysql-path)
|
||||
MYSQL_PATH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--host)
|
||||
HOST="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--port)
|
||||
PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--username)
|
||||
USERNAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--password)
|
||||
PASSWORD="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--db-name)
|
||||
DB_NAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--sql-file)
|
||||
SQL_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] unsupported argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${HOST}" || -z "${PORT}" || -z "${USERNAME}" || -z "${DB_NAME}" || -z "${SQL_FILE}" ]]; then
|
||||
echo "[ERROR] missing required database restore arguments" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "${SQL_FILE}" ]]; then
|
||||
echo "[ERROR] sql file not found: ${SQL_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"${MYSQL_PATH}" \
|
||||
--host="${HOST}" \
|
||||
--port="${PORT}" \
|
||||
--user="${USERNAME}" \
|
||||
--password="${PASSWORD}" \
|
||||
"${DB_NAME}" < "${SQL_FILE}"
|
||||
48
scripts/resource-restore/start-runner.sh
Executable file
48
scripts/resource-restore/start-runner.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HANDOFF_FILE=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--handoff-file)
|
||||
HANDOFF_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] unsupported argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${HANDOFF_FILE}" ]]; then
|
||||
echo "[ERROR] --handoff-file is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SOURCE_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
APP_HOME="${APP_HOME:-/home/tms}"
|
||||
CONF_DIR="${CONF_DIR:-${APP_HOME}/config}"
|
||||
JAVA_BIN="${JAVA_BIN:-java}"
|
||||
RUNNER_JAR_PATH="${RUNNER_JAR_PATH:-${APP_HOME}/tms-framework.jar}"
|
||||
RUNNER_LOG_DIR="${RUNNER_LOG_DIR:-${APP_HOME}/logs}"
|
||||
RUNNER_STDOUT_LOG="${RUNNER_STDOUT_LOG:-${RUNNER_LOG_DIR}/resource-restore-runner.out.log}"
|
||||
|
||||
if [[ -f "${SOURCE_ROOT}/target/tms-framework.jar" ]]; then
|
||||
RUNNER_JAR_PATH="${SOURCE_ROOT}/target/tms-framework.jar"
|
||||
fi
|
||||
|
||||
mkdir -p "${RUNNER_LOG_DIR}"
|
||||
|
||||
nohup "${JAVA_BIN}" \
|
||||
-cp "${RUNNER_JAR_PATH}" \
|
||||
-Dloader.main=com.cisd.tms.modules.backup.runner.ResourceRestoreRunnerMain \
|
||||
org.springframework.boot.loader.launch.PropertiesLauncher \
|
||||
--spring.main.web-application-type=none \
|
||||
--spring.main.banner-mode=off \
|
||||
--spring.config.additional-location="optional:file:${CONF_DIR}/" \
|
||||
--tms.backup.runner-handoff-file="${HANDOFF_FILE}" \
|
||||
>> "${RUNNER_STDOUT_LOG}" 2>&1 &
|
||||
|
||||
echo $!
|
||||
@ -9,6 +9,14 @@ public class ResourceBackupProperties {
|
||||
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 String runnerLaunchScriptPath = "/home/tms/bin/resource-restore/start-runner.sh";
|
||||
private String mysqldumpPath = "mysqldump";
|
||||
private String mysqlPath = "mysql";
|
||||
private String restoreDbScriptPath = "/home/tms/bin/resource-restore/restore-db.sh";
|
||||
private String tmsDatabaseName = "";
|
||||
private String cmepDatabaseName = "CMEP";
|
||||
private String healthCheckUrl = "http://127.0.0.1:8080/actuator/health";
|
||||
private int healthCheckTimeoutSeconds = 5;
|
||||
private boolean runnerAutoLaunchEnabled = false;
|
||||
private int runnerHeartbeatTimeoutSeconds = 300;
|
||||
private int storageEncKeyIndex = 1;
|
||||
@ -46,6 +54,70 @@ public class ResourceBackupProperties {
|
||||
this.tmsScriptPath = tmsScriptPath;
|
||||
}
|
||||
|
||||
public String getRunnerLaunchScriptPath() {
|
||||
return runnerLaunchScriptPath;
|
||||
}
|
||||
|
||||
public void setRunnerLaunchScriptPath(String runnerLaunchScriptPath) {
|
||||
this.runnerLaunchScriptPath = runnerLaunchScriptPath;
|
||||
}
|
||||
|
||||
public String getMysqldumpPath() {
|
||||
return mysqldumpPath;
|
||||
}
|
||||
|
||||
public void setMysqldumpPath(String mysqldumpPath) {
|
||||
this.mysqldumpPath = mysqldumpPath;
|
||||
}
|
||||
|
||||
public String getMysqlPath() {
|
||||
return mysqlPath;
|
||||
}
|
||||
|
||||
public void setMysqlPath(String mysqlPath) {
|
||||
this.mysqlPath = mysqlPath;
|
||||
}
|
||||
|
||||
public String getRestoreDbScriptPath() {
|
||||
return restoreDbScriptPath;
|
||||
}
|
||||
|
||||
public void setRestoreDbScriptPath(String restoreDbScriptPath) {
|
||||
this.restoreDbScriptPath = restoreDbScriptPath;
|
||||
}
|
||||
|
||||
public String getTmsDatabaseName() {
|
||||
return tmsDatabaseName;
|
||||
}
|
||||
|
||||
public void setTmsDatabaseName(String tmsDatabaseName) {
|
||||
this.tmsDatabaseName = tmsDatabaseName;
|
||||
}
|
||||
|
||||
public String getCmepDatabaseName() {
|
||||
return cmepDatabaseName;
|
||||
}
|
||||
|
||||
public void setCmepDatabaseName(String cmepDatabaseName) {
|
||||
this.cmepDatabaseName = cmepDatabaseName;
|
||||
}
|
||||
|
||||
public String getHealthCheckUrl() {
|
||||
return healthCheckUrl;
|
||||
}
|
||||
|
||||
public void setHealthCheckUrl(String healthCheckUrl) {
|
||||
this.healthCheckUrl = healthCheckUrl;
|
||||
}
|
||||
|
||||
public int getHealthCheckTimeoutSeconds() {
|
||||
return healthCheckTimeoutSeconds;
|
||||
}
|
||||
|
||||
public void setHealthCheckTimeoutSeconds(int healthCheckTimeoutSeconds) {
|
||||
this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds;
|
||||
}
|
||||
|
||||
public boolean getRunnerAutoLaunchEnabled() {
|
||||
return runnerAutoLaunchEnabled;
|
||||
}
|
||||
|
||||
@ -29,6 +29,11 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
|
||||
List<DiscoveredResourceItem> items = new ArrayList<>();
|
||||
items.add(item("FILE", "TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), true));
|
||||
items.add(item("DB", "TMS_DB_DUMP", "TMS", true));
|
||||
if (!"DIRECT".equals(productType)) {
|
||||
// 第一版标准版迁移需要把收发器业务库一起带走,DIRECT 暂时不要求这份库。
|
||||
items.add(item("DB", "CMEP_DB_DUMP", "CMEP", true));
|
||||
}
|
||||
items.add(item("FILE", "CP_CONFIG", resolveCpConfigPath(productType), true));
|
||||
items.add(item("FILE", "RECEIVER_LICENSE", resolveReceiverLicensePath(productType), true));
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
public interface DatabaseBackupCollector {
|
||||
|
||||
CollectedDatabaseDump collectTmsDatabaseDump();
|
||||
|
||||
CollectedDatabaseDump collectCmepDatabaseDump();
|
||||
|
||||
class CollectedDatabaseDump {
|
||||
|
||||
private String databaseName;
|
||||
private String fileName;
|
||||
private byte[] content;
|
||||
|
||||
public String getDatabaseName() {
|
||||
return databaseName;
|
||||
}
|
||||
|
||||
public void setDatabaseName(String databaseName) {
|
||||
this.databaseName = databaseName;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public byte[] getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(byte[] content) {
|
||||
this.content = content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final String datasourceUrl;
|
||||
private final String datasourceUsername;
|
||||
private final String datasourcePassword;
|
||||
|
||||
public MysqlDatabaseBackupCollector(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
@Value("${spring.datasource.url:}") String datasourceUrl,
|
||||
@Value("${spring.datasource.username:}") String datasourceUsername,
|
||||
@Value("${spring.datasource.password:}") String datasourcePassword
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.datasourceUrl = datasourceUrl;
|
||||
this.datasourceUsername = datasourceUsername;
|
||||
this.datasourcePassword = datasourcePassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CollectedDatabaseDump collectTmsDatabaseDump() {
|
||||
MysqlConnectionTarget target = resolveTarget(resourceBackupProperties.getTmsDatabaseName(), "TMS");
|
||||
return collectDatabaseDump(target, "TMS.sql");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CollectedDatabaseDump collectCmepDatabaseDump() {
|
||||
MysqlConnectionTarget target = resolveTarget(resourceBackupProperties.getCmepDatabaseName(), "CMEP");
|
||||
return collectDatabaseDump(target, "CMEP.sql");
|
||||
}
|
||||
|
||||
private CollectedDatabaseDump collectDatabaseDump(MysqlConnectionTarget target, String fileName) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(resourceBackupProperties.getMysqldumpPath());
|
||||
command.add("--host=" + target.host());
|
||||
command.add("--port=" + target.port());
|
||||
command.add("--user=" + target.username());
|
||||
if (!target.password().isEmpty()) {
|
||||
command.add("--password=" + target.password());
|
||||
}
|
||||
command.add("--single-transaction");
|
||||
command.add("--skip-lock-tables");
|
||||
command.add("--databases");
|
||||
command.add(target.databaseName());
|
||||
|
||||
try {
|
||||
Process process = new ProcessBuilder(command).start();
|
||||
byte[] stdout = readAllBytes(process.getInputStream());
|
||||
byte[] stderr = readAllBytes(process.getErrorStream());
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
String error = new String(stderr, StandardCharsets.UTF_8).trim();
|
||||
throw new IllegalStateException("mysqldump failed with exitCode=" + exitCode
|
||||
+ (error.isEmpty() ? "" : ", error=" + error));
|
||||
}
|
||||
if (stdout.length == 0) {
|
||||
throw new IllegalStateException("mysqldump produced empty output");
|
||||
}
|
||||
CollectedDatabaseDump dump = new CollectedDatabaseDump();
|
||||
dump.setDatabaseName(target.databaseName());
|
||||
dump.setFileName(fileName);
|
||||
dump.setContent(stdout);
|
||||
return dump;
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("mysqldump interrupted", ex);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to run mysqldump", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private MysqlConnectionTarget resolveTarget(String configuredDatabaseName, String fallbackDatabaseName) {
|
||||
String url = trim(datasourceUrl);
|
||||
if (url.isEmpty()) {
|
||||
throw new IllegalStateException("spring.datasource.url is required for database backup");
|
||||
}
|
||||
if (!url.startsWith("jdbc:mysql://")) {
|
||||
throw new IllegalStateException("unsupported datasource url for backup: " + url);
|
||||
}
|
||||
try {
|
||||
String normalizedUrl = url.substring("jdbc:".length());
|
||||
String[] segments = normalizedUrl.split("\\?", 2);
|
||||
URI uri = URI.create(segments[0]);
|
||||
String databaseName = trim(configuredDatabaseName);
|
||||
if (databaseName.isEmpty()) {
|
||||
String path = trim(uri.getPath());
|
||||
databaseName = path.startsWith("/") ? path.substring(1) : path;
|
||||
}
|
||||
if (databaseName.isEmpty()) {
|
||||
databaseName = fallbackDatabaseName;
|
||||
}
|
||||
return new MysqlConnectionTarget(
|
||||
uri.getHost() == null ? "127.0.0.1" : uri.getHost(),
|
||||
uri.getPort() <= 0 ? 3306 : uri.getPort(),
|
||||
trim(datasourceUsername),
|
||||
trim(datasourcePassword),
|
||||
databaseName
|
||||
);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("invalid datasource url for backup: " + url, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
|
||||
try (inputStream; ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
inputStream.transferTo(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private record MysqlConnectionTarget(String host, int port, String username, String password, String databaseName) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
public class RequiredBackupResourceMissingException extends IllegalStateException {
|
||||
|
||||
public RequiredBackupResourceMissingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -36,15 +36,18 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceEnvelopeCryptoService resourceEnvelopeCryptoService;
|
||||
private final DatabaseBackupCollector databaseBackupCollector;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ResourcePackageServiceImpl(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ResourceEnvelopeCryptoService resourceEnvelopeCryptoService,
|
||||
DatabaseBackupCollector databaseBackupCollector,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.resourceEnvelopeCryptoService = resourceEnvelopeCryptoService;
|
||||
this.databaseBackupCollector = databaseBackupCollector;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@ -243,13 +246,18 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
if ("DB".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
return collectDatabaseItem(zipOutputStream, item, resourceEntry);
|
||||
}
|
||||
if (!"FILE".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
assertOptionalResource(item, "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
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)) {
|
||||
assertOptionalResource(item, "PATH_NOT_FOUND");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "PATH_NOT_FOUND");
|
||||
return 0;
|
||||
@ -275,6 +283,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
assertOptionalResource(item, "UNSUPPORTED_FILE_TYPE");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "UNSUPPORTED_FILE_TYPE");
|
||||
return 0;
|
||||
@ -289,6 +298,73 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
return archivedFiles.size();
|
||||
}
|
||||
|
||||
private int collectDatabaseItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
DatabaseBackupCollector.CollectedDatabaseDump dump = resolveDatabaseDump(item);
|
||||
if (dump == null) {
|
||||
assertOptionalResource(item, "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "RESOURCE_TYPE_NOT_SUPPORTED_YET");
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
byte[] content = dump.getContent();
|
||||
if (content == null || content.length == 0) {
|
||||
throw new IllegalStateException("database backup collector returned empty dump");
|
||||
}
|
||||
String databaseName = trim(dump.getDatabaseName()).isEmpty() ? "TMS" : trim(dump.getDatabaseName());
|
||||
String fileName = trim(dump.getFileName()).isEmpty() ? "TMS.sql" : trim(dump.getFileName());
|
||||
String entryPath = "db/" + fileName;
|
||||
addZipEntry(zipOutputStream, entryPath, content);
|
||||
|
||||
Map<String, Object> fileEntry = new LinkedHashMap<>();
|
||||
fileEntry.put("entryPath", entryPath);
|
||||
fileEntry.put("sourcePath", "database:" + databaseName);
|
||||
fileEntry.put("relativeName", fileName);
|
||||
fileEntry.put("size", content.length);
|
||||
fileEntry.put("contentSm3", sm3Hex(content));
|
||||
|
||||
resourceEntry.put("collected", Boolean.TRUE);
|
||||
resourceEntry.put("fileCount", 1);
|
||||
resourceEntry.put("entryPath", entryPath);
|
||||
resourceEntry.put("databaseName", databaseName);
|
||||
resourceEntry.put("files", List.of(fileEntry));
|
||||
return 1;
|
||||
} catch (RuntimeException ex) {
|
||||
assertOptionalResource(item, "DB_DUMP_FAILED");
|
||||
resourceEntry.put("collected", Boolean.FALSE);
|
||||
resourceEntry.put("skipReason", "DB_DUMP_FAILED");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseBackupCollector.CollectedDatabaseDump resolveDatabaseDump(DiscoveredResourceItem item) {
|
||||
String resourceCode = trim(item.getResourceCode());
|
||||
if ("TMS_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
return databaseBackupCollector.collectTmsDatabaseDump();
|
||||
}
|
||||
if ("CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
return databaseBackupCollector.collectCmepDatabaseDump();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void assertOptionalResource(DiscoveredResourceItem item, String reason) {
|
||||
if (!item.isRequired()) {
|
||||
return;
|
||||
}
|
||||
String resourceCode = trim(item.getResourceCode()).isEmpty() ? "UNKNOWN" : trim(item.getResourceCode());
|
||||
String resolvedPath = trim(item.getResolvedPath());
|
||||
String message = "required backup resource is unavailable: code=" + resourceCode + ", reason=" + reason;
|
||||
if (!resolvedPath.isEmpty()) {
|
||||
message += ", path=" + resolvedPath;
|
||||
}
|
||||
throw new RequiredBackupResourceMissingException(message);
|
||||
}
|
||||
|
||||
private Map<String, Object> writeRegularFileEntry(
|
||||
ZipOutputStream zipOutputStream,
|
||||
DiscoveredResourceItem item,
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ConsistencyVerifyRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private static final List<String> REQUIRED_SUCCESS_STEPS = List.of(
|
||||
"RESTORE_FILES",
|
||||
"RESTORE_DATABASES",
|
||||
"START_SERVICES"
|
||||
);
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
public ConsistencyVerifyRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this(resourceBackupProperties, objectMapper, HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.build());
|
||||
}
|
||||
|
||||
ConsistencyVerifyRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ObjectMapper objectMapper,
|
||||
HttpClient httpClient
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "CONSISTENCY_VERIFY".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
Path stepOutputDir = context.taskDir().resolve("step-output");
|
||||
for (String requiredStep : REQUIRED_SUCCESS_STEPS) {
|
||||
assertStepSucceeded(stepOutputDir, requiredStep);
|
||||
}
|
||||
verifyHealthEndpoint();
|
||||
return RunnerStepExecutionResult.success("required steps succeeded and health endpoint is UP");
|
||||
}
|
||||
|
||||
private void assertStepSucceeded(Path stepOutputDir, String stepCode) {
|
||||
try {
|
||||
Path stepFile = resolveRequiredStepOutput(stepOutputDir, stepCode);
|
||||
Map<String, Object> payload = objectMapper.readValue(stepFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
String result = String.valueOf(payload.get("result"));
|
||||
if (!"SUCCESS".equalsIgnoreCase(result)) {
|
||||
throw new IllegalStateException("required step is not successful: " + stepCode);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read step output for " + stepCode, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveRequiredStepOutput(Path stepOutputDir, String stepCode) throws IOException {
|
||||
if (!Files.isDirectory(stepOutputDir)) {
|
||||
throw new IllegalStateException("step output directory missing: " + stepOutputDir);
|
||||
}
|
||||
try (var pathStream = Files.list(stepOutputDir)) {
|
||||
return pathStream
|
||||
.filter(path -> path.getFileName().toString().endsWith("-" + stepCode + ".json"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("step output missing for " + stepCode));
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyHealthEndpoint() {
|
||||
String healthUrl = resourceBackupProperties.getHealthCheckUrl();
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(healthUrl))
|
||||
.timeout(Duration.ofSeconds(resourceBackupProperties.getHealthCheckTimeoutSeconds()))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200 || !response.body().contains("\"status\":\"UP\"")) {
|
||||
throw new IllegalStateException("health endpoint is not UP: statusCode=" + response.statusCode());
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("health verification interrupted", ex);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to verify health endpoint: " + healthUrl, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class DatabaseRestoreRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final RunnerCommandInvoker runnerCommandInvoker;
|
||||
private final String datasourceUrl;
|
||||
private final String datasourceUsername;
|
||||
private final String datasourcePassword;
|
||||
|
||||
public DatabaseRestoreRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
RunnerCommandInvoker runnerCommandInvoker,
|
||||
@Value("${spring.datasource.url:}") String datasourceUrl,
|
||||
@Value("${spring.datasource.username:}") String datasourceUsername,
|
||||
@Value("${spring.datasource.password:}") String datasourcePassword
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.runnerCommandInvoker = runnerCommandInvoker;
|
||||
this.datasourceUrl = datasourceUrl;
|
||||
this.datasourceUsername = datasourceUsername;
|
||||
this.datasourcePassword = datasourcePassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "RESTORE_DATABASES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
if (!supports(context.stepCode())) {
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
List<String> restoredDatabases = new ArrayList<>();
|
||||
restoreDatabase(context, "TMS.sql", resourceBackupProperties.getTmsDatabaseName(), "TMS", true, restoredDatabases);
|
||||
// 标准版第一版还需要恢复收发器业务库;DIRECT 当前没有这份库时允许跳过。
|
||||
restoreDatabase(context, "CMEP.sql", resourceBackupProperties.getCmepDatabaseName(), "CMEP", false, restoredDatabases);
|
||||
return RunnerStepExecutionResult.success("restored databases " + String.join(",", restoredDatabases));
|
||||
}
|
||||
|
||||
private void restoreDatabase(
|
||||
RunnerStepContext context,
|
||||
String sqlFileName,
|
||||
String configuredDatabaseName,
|
||||
String fallbackDatabaseName,
|
||||
boolean required,
|
||||
List<String> restoredDatabases
|
||||
) {
|
||||
Path sqlFile = context.taskDir().resolve("staging").resolve("payload").resolve("db").resolve(sqlFileName).normalize();
|
||||
if (!Files.isRegularFile(sqlFile)) {
|
||||
if (required) {
|
||||
throw new IllegalStateException("restore database sql file missing: " + sqlFile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
MysqlTarget target = resolveTarget(configuredDatabaseName, fallbackDatabaseName);
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("/bin/bash");
|
||||
command.add(resourceBackupProperties.getRestoreDbScriptPath());
|
||||
command.add("--mysql-path");
|
||||
command.add(resourceBackupProperties.getMysqlPath());
|
||||
command.add("--host");
|
||||
command.add(target.host());
|
||||
command.add("--port");
|
||||
command.add(String.valueOf(target.port()));
|
||||
command.add("--username");
|
||||
command.add(target.username());
|
||||
command.add("--password");
|
||||
command.add(target.password());
|
||||
command.add("--db-name");
|
||||
command.add(target.databaseName());
|
||||
command.add("--sql-file");
|
||||
command.add(sqlFile.toString());
|
||||
|
||||
int exitCode = runnerCommandInvoker.run(command, resolveLogFile(context), context.taskDir());
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("runner step RESTORE_DATABASES failed for " + target.databaseName() + ", exitCode=" + exitCode);
|
||||
}
|
||||
restoredDatabases.add(target.databaseName());
|
||||
}
|
||||
|
||||
private MysqlTarget resolveTarget(String configuredDatabaseName, String fallbackDatabaseName) {
|
||||
String url = trim(datasourceUrl);
|
||||
if (url.isEmpty()) {
|
||||
throw new IllegalStateException("spring.datasource.url is required for database restore");
|
||||
}
|
||||
if (!url.startsWith("jdbc:mysql://")) {
|
||||
throw new IllegalStateException("unsupported datasource url for restore: " + url);
|
||||
}
|
||||
try {
|
||||
String normalizedUrl = url.substring("jdbc:".length());
|
||||
String[] segments = normalizedUrl.split("\\?", 2);
|
||||
URI uri = URI.create(segments[0]);
|
||||
String databaseName = trim(configuredDatabaseName);
|
||||
if (databaseName.isEmpty()) {
|
||||
String path = trim(uri.getPath());
|
||||
databaseName = path.startsWith("/") ? path.substring(1) : path;
|
||||
}
|
||||
if (databaseName.isEmpty()) {
|
||||
databaseName = fallbackDatabaseName;
|
||||
}
|
||||
return new MysqlTarget(
|
||||
uri.getHost() == null ? "127.0.0.1" : uri.getHost(),
|
||||
uri.getPort() <= 0 ? 3306 : uri.getPort(),
|
||||
trim(datasourceUsername),
|
||||
trim(datasourcePassword),
|
||||
databaseName
|
||||
);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("invalid datasource url for restore: " + url, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveLogFile(RunnerStepContext context) {
|
||||
return context.taskDir().resolve("steps").resolve(String.format("%02d-%s.log", context.stepNo(), context.stepCode()));
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private record MysqlTarget(String host, int port, String username, String password, String databaseName) {
|
||||
}
|
||||
}
|
||||
@ -10,10 +10,12 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -21,6 +23,9 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private static final Set<String> RESTORE_FILE_RESOURCE_CODES =
|
||||
Set.of("TMS_CONFIG", "CP_CONFIG", "RECEIVER_LICENSE");
|
||||
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ObjectMapper objectMapper;
|
||||
@ -37,7 +42,7 @@ public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "RESTORE_FILES".equals(stepCode) || "CONSISTENCY_VERIFY".equals(stepCode);
|
||||
return "RESTORE_FILES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -45,9 +50,6 @@ public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
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());
|
||||
}
|
||||
|
||||
@ -60,17 +62,12 @@ public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
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);
|
||||
Path payloadManifestFile = payloadDir.resolve("payload-manifest.json");
|
||||
int resourceCount = readResourceCount(payloadManifestFile);
|
||||
int restoredFileCount = restoreSupportedFiles(payloadDir, payloadManifestFile);
|
||||
return RunnerStepExecutionResult.success(
|
||||
"payload extracted to staging, resourceCount=" + resourceCount + ", restoredFileCount=" + restoredFileCount
|
||||
);
|
||||
}
|
||||
|
||||
private ResourceRestoreHandoffSnapshot readRequiredHandoff(Path handoffFile) {
|
||||
@ -133,6 +130,64 @@ public class PayloadArchiveRunnerStepExecutor implements RunnerStepExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private int restoreSupportedFiles(Path payloadDir, 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 0;
|
||||
}
|
||||
int restoredFileCount = 0;
|
||||
for (Object resourceObject : resources) {
|
||||
if (!(resourceObject instanceof Map<?, ?> rawResource)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> resource = castMap(rawResource);
|
||||
String resourceCode = String.valueOf(resource.get("resourceCode"));
|
||||
if (!RESTORE_FILE_RESOURCE_CODES.contains(resourceCode)) {
|
||||
continue;
|
||||
}
|
||||
Object filesValue = resource.get("files");
|
||||
if (!(filesValue instanceof List<?> rawFiles)) {
|
||||
continue;
|
||||
}
|
||||
for (Object fileObject : rawFiles) {
|
||||
if (!(fileObject instanceof Map<?, ?> rawFile)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> fileEntry = castMap(rawFile);
|
||||
Path sourceFile = payloadDir.resolve(String.valueOf(fileEntry.get("entryPath"))).normalize();
|
||||
if (!sourceFile.startsWith(payloadDir) || !Files.isRegularFile(sourceFile)) {
|
||||
throw new IllegalStateException("restored file source missing: " + sourceFile);
|
||||
}
|
||||
Path targetFile = resolveTargetFile(resourceCode, String.valueOf(resource.get("resolvedPath")), String.valueOf(fileEntry.get("relativeName")));
|
||||
Files.createDirectories(targetFile.getParent());
|
||||
// 第一版只回写最关键的配置/license 文件,不做目录合并或增量策略。
|
||||
Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
restoredFileCount++;
|
||||
}
|
||||
}
|
||||
return restoredFileCount;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to restore payload files", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveTargetFile(String resourceCode, String resolvedPath, String relativeName) {
|
||||
Path targetPath = Path.of(resolvedPath).normalize();
|
||||
if ("TMS_CONFIG".equals(resourceCode)) {
|
||||
return targetPath.resolve(relativeName).normalize();
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
private static Map<String, Object> castMap(Map<?, ?> rawMap) {
|
||||
Map<String, Object> result = new java.util.LinkedHashMap<>();
|
||||
rawMap.forEach((key, value) -> result.put(String.valueOf(key), value));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
|
||||
return inputStream.readAllBytes();
|
||||
}
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ProcessResourceRestoreRunnerLauncher implements ResourceRestoreRunnerLauncher {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final RunnerCommandInvoker runnerCommandInvoker;
|
||||
|
||||
public ProcessResourceRestoreRunnerLauncher(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
RunnerCommandInvoker runnerCommandInvoker
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.runnerCommandInvoker = runnerCommandInvoker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(Path handoffFile) {
|
||||
if (!resourceBackupProperties.getRunnerAutoLaunchEnabled()) {
|
||||
return;
|
||||
}
|
||||
Path normalizedHandoffFile = handoffFile.normalize().toAbsolutePath();
|
||||
Path taskDir = normalizedHandoffFile.getParent();
|
||||
Path launcherLog = taskDir.resolve("runner-launch.log");
|
||||
List<String> command = List.of(
|
||||
"/bin/bash",
|
||||
resourceBackupProperties.getRunnerLaunchScriptPath(),
|
||||
"--handoff-file",
|
||||
normalizedHandoffFile.toString()
|
||||
);
|
||||
int exitCode = runnerCommandInvoker.run(command, launcherLog, taskDir);
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("failed to launch external restore runner, exitCode=" + exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.TmsApplication;
|
||||
import java.nio.file.Path;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
public final class ResourceRestoreRunnerMain {
|
||||
|
||||
private ResourceRestoreRunnerMain() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(TmsApplication.class)
|
||||
.web(WebApplicationType.NONE)
|
||||
.logStartupInfo(false)
|
||||
.run(args);
|
||||
int exitCode = 0;
|
||||
try {
|
||||
Environment environment = context.getEnvironment();
|
||||
String handoffFile = environment.getProperty("tms.backup.runner-handoff-file", "");
|
||||
if (handoffFile == null || handoffFile.isBlank()) {
|
||||
throw new IllegalStateException("tms.backup.runner-handoff-file is required");
|
||||
}
|
||||
context.getBean(ResourceRestoreRunner.class).run(Path.of(handoffFile));
|
||||
} catch (RuntimeException ex) {
|
||||
exitCode = 1;
|
||||
throw ex;
|
||||
} finally {
|
||||
final int finalExitCode = exitCode;
|
||||
int springExitCode = SpringApplication.exit(context, () -> finalExitCode);
|
||||
System.exit(springExitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.packagex.RequiredBackupResourceMissingException;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceBackupTaskRepository;
|
||||
@ -62,6 +63,22 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
|
||||
String taskId = resourceTaskIdGenerator.nextBackupTaskId();
|
||||
String backupId = resourceTaskIdGenerator.nextBackupId();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ResourceBackupTaskEntity entity = new ResourceBackupTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setBackupId(backupId);
|
||||
entity.setStatus("RUNNING");
|
||||
entity.setProductType(initTask.getProductType());
|
||||
entity.setMqType(initTask.getMqType());
|
||||
entity.setOrgCode(initTask.getOrgCode());
|
||||
entity.setSourceDeviceId(resolveSourceDeviceId());
|
||||
entity.setRemark(request == null ? null : request.getRemark());
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
entity.setStartTime(now);
|
||||
|
||||
try {
|
||||
// 先根据初始化快照收敛资源范围,再交给打包服务生成本地可见的 .tmsbak 文件。
|
||||
ResourcePackageResult packageResult = resourcePackageService.createPackage(
|
||||
taskId,
|
||||
@ -70,17 +87,8 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
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());
|
||||
@ -88,19 +96,23 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
|
||||
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);
|
||||
} catch (RequiredBackupResourceMissingException ex) {
|
||||
entity.setStatus("FAILED");
|
||||
entity.setErrorCode("REQUIRED_RESOURCE_MISSING");
|
||||
entity.setErrorMessage(ex.getMessage());
|
||||
} catch (RuntimeException ex) {
|
||||
entity.setStatus("FAILED");
|
||||
entity.setErrorCode("PACKAGE_CREATE_FAILED");
|
||||
entity.setErrorMessage(ex.getMessage());
|
||||
}
|
||||
entity.setFinishTime(LocalDateTime.now());
|
||||
resourceBackupTaskRepository.save(entity);
|
||||
|
||||
CreateResourceBackupResponse response = new CreateResourceBackupResponse();
|
||||
response.setTaskId(taskId);
|
||||
response.setBackupId(backupId);
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setCreatedAt(now.toString());
|
||||
response.setCreatedAt(entity.getCreateTime().toString());
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@ -186,6 +186,22 @@ tms:
|
||||
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。
|
||||
runner-launch-script-path: ${TMS_BACKUP_RUNNER_LAUNCH_SCRIPT_PATH:/home/tms/bin/resource-restore/start-runner.sh}
|
||||
# 导出 TMS 库时使用的 mysqldump 命令;现场可按实际安装路径覆盖。
|
||||
mysqldump-path: ${TMS_BACKUP_MYSQLDUMP_PATH:mysqldump}
|
||||
# 恢复 TMS 库时使用的 mysql 客户端命令。
|
||||
mysql-path: ${TMS_BACKUP_MYSQL_PATH:mysql}
|
||||
# 数据库恢复脚本路径;runner 在 RESTORE_DATABASES 阶段调用。
|
||||
restore-db-script-path: ${TMS_BACKUP_RESTORE_DB_SCRIPT_PATH:/home/tms/bin/resource-restore/restore-db.sh}
|
||||
# 备份 TMS 库名;为空时默认从 spring.datasource.url 中解析。
|
||||
tms-database-name: ${TMS_BACKUP_TMS_DATABASE_NAME:}
|
||||
# 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。
|
||||
cmep-database-name: ${TMS_BACKUP_CMEP_DATABASE_NAME:CMEP}
|
||||
# 恢复完成后的最小健康检查地址。
|
||||
health-check-url: ${TMS_BACKUP_HEALTH_CHECK_URL:http://127.0.0.1:8080/actuator/health}
|
||||
# 健康检查请求超时(秒)。
|
||||
health-check-timeout-seconds: ${TMS_BACKUP_HEALTH_CHECK_TIMEOUT_SECONDS:5}
|
||||
# 是否在创建恢复任务后自动拉起本机 runner;默认关闭,避免开发环境误触停机恢复链。
|
||||
runner-auto-launch-enabled: ${TMS_BACKUP_RUNNER_AUTO_LAUNCH_ENABLED:false}
|
||||
# 恢复 runner 心跳失联超时(秒)。
|
||||
|
||||
@ -16,6 +16,8 @@ class ResourceDiscoveryServiceTest {
|
||||
List<DiscoveredResourceItem> items = service.discover(sampleInitTask("ENTERPRISE", "RABBITMQ"));
|
||||
|
||||
Assertions.assertTrue(containsCode(items, "TMS_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "TMS_DB_DUMP"));
|
||||
Assertions.assertTrue(containsCode(items, "CMEP_DB_DUMP"));
|
||||
Assertions.assertTrue(containsCode(items, "CP_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "RECEIVER_LICENSE"));
|
||||
Assertions.assertTrue(containsCode(items, "RABBITMQ_DEFINITIONS"));
|
||||
@ -35,6 +37,8 @@ class ResourceDiscoveryServiceTest {
|
||||
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"));
|
||||
Assertions.assertTrue(hasRequired(items, "TMS_DB_DUMP"));
|
||||
Assertions.assertFalse(containsCode(items, "CMEP_DB_DUMP"));
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask(String productType, String mqType) {
|
||||
@ -70,4 +74,8 @@ class ResourceDiscoveryServiceTest {
|
||||
private static boolean hasPath(List<DiscoveredResourceItem> items, String code, String path) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && path.equals(item.getResolvedPath()));
|
||||
}
|
||||
|
||||
private static boolean hasRequired(List<DiscoveredResourceItem> items, String code) {
|
||||
return items.stream().anyMatch(item -> code.equals(item.getResourceCode()) && item.isRequired());
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class RequiredBackupResourceMissingExceptionTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeMessage() {
|
||||
RequiredBackupResourceMissingException exception =
|
||||
new RequiredBackupResourceMissingException("required backup resource is unavailable");
|
||||
|
||||
Assertions.assertEquals("required backup resource is unavailable", exception.getMessage());
|
||||
}
|
||||
}
|
||||
@ -48,6 +48,7 @@ class ResourcePackageServiceTest {
|
||||
ResourcePackageService service = new ResourcePackageServiceImpl(
|
||||
properties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties),
|
||||
mockDatabaseBackupCollector(),
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
@ -55,7 +56,11 @@ class ResourcePackageServiceTest {
|
||||
"RBKP-TASK-20260413-000001",
|
||||
"RBKP-20260413-153000-000001",
|
||||
sampleInitTask(),
|
||||
List.of(resource("TMS_CONFIG", payloadFile.toString())),
|
||||
List.of(
|
||||
resource("TMS_CONFIG", payloadFile.toString()),
|
||||
databaseResource("TMS_DB_DUMP", "TMS"),
|
||||
databaseResource("CMEP_DB_DUMP", "CMEP")
|
||||
),
|
||||
sampleRequest()
|
||||
);
|
||||
|
||||
@ -66,7 +71,7 @@ class ResourcePackageServiceTest {
|
||||
Assertions.assertFalse(result.getPackageHash().isBlank());
|
||||
Assertions.assertFalse(result.getStorageEncKeyFingerprint().isBlank());
|
||||
Assertions.assertFalse(result.getAuthenticityKeyFingerprint().isBlank());
|
||||
Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":1"));
|
||||
Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":3"));
|
||||
|
||||
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
|
||||
Assertions.assertNotNull(zipFile.getEntry("manifest.json"));
|
||||
@ -84,9 +89,13 @@ class ResourcePackageServiceTest {
|
||||
|
||||
@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(3, resources.size());
|
||||
Assertions.assertEquals(Boolean.TRUE, resources.get(0).get("collected"));
|
||||
Assertions.assertTrue(String.valueOf(resources.get(0).get("entryPath")).contains("application.yml"));
|
||||
Assertions.assertEquals("TMS_DB_DUMP", resources.get(1).get("resourceCode"));
|
||||
Assertions.assertEquals("db/TMS.sql", resources.get(1).get("entryPath"));
|
||||
Assertions.assertEquals("CMEP_DB_DUMP", resources.get(2).get("resourceCode"));
|
||||
Assertions.assertEquals("db/CMEP.sql", resources.get(2).get("entryPath"));
|
||||
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new java.io.ByteArrayInputStream(inspectResult.getPayloadArchiveBytes()))) {
|
||||
java.util.Map<String, String> entries = new java.util.LinkedHashMap<>();
|
||||
@ -96,9 +105,40 @@ class ResourcePackageServiceTest {
|
||||
}
|
||||
Assertions.assertTrue(entries.containsKey("payload-manifest.json"));
|
||||
Assertions.assertTrue(entries.values().stream().anyMatch(value -> value.contains("server.port=8080")));
|
||||
Assertions.assertTrue(entries.containsKey("db/TMS.sql"));
|
||||
Assertions.assertTrue(entries.get("db/TMS.sql").contains("INSERT INTO t_demo"));
|
||||
Assertions.assertTrue(entries.containsKey("db/CMEP.sql"));
|
||||
Assertions.assertTrue(entries.get("db/CMEP.sql").contains("INSERT INTO c_demo"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailFastWhenRequiredFileResourceIsMissing() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setOutputDir(tempDir.toString());
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
ResourcePackageService service = new ResourcePackageServiceImpl(
|
||||
properties,
|
||||
new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties),
|
||||
mockDatabaseBackupCollector(),
|
||||
new ObjectMapper()
|
||||
);
|
||||
|
||||
RequiredBackupResourceMissingException exception = Assertions.assertThrows(
|
||||
RequiredBackupResourceMissingException.class,
|
||||
() -> service.createPackage(
|
||||
"RBKP-TASK-20260413-000001",
|
||||
"RBKP-20260413-153000-000001",
|
||||
sampleInitTask(),
|
||||
List.of(resource("TMS_CONFIG", tempDir.resolve("missing-config").toString())),
|
||||
sampleRequest()
|
||||
)
|
||||
);
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("TMS_CONFIG"));
|
||||
Assertions.assertTrue(exception.getMessage().contains("PATH_NOT_FOUND"));
|
||||
}
|
||||
|
||||
private static BackupDataResult backup(byte[] data) {
|
||||
BackupDataResult result = new BackupDataResult();
|
||||
result.setData(data);
|
||||
@ -127,6 +167,30 @@ class ResourcePackageServiceTest {
|
||||
return item;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem databaseResource(String code, String path) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("DB");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static DatabaseBackupCollector mockDatabaseBackupCollector() {
|
||||
DatabaseBackupCollector collector = Mockito.mock(DatabaseBackupCollector.class);
|
||||
DatabaseBackupCollector.CollectedDatabaseDump tmsDump = new DatabaseBackupCollector.CollectedDatabaseDump();
|
||||
tmsDump.setDatabaseName("TMS");
|
||||
tmsDump.setFileName("TMS.sql");
|
||||
tmsDump.setContent("CREATE TABLE t_demo(id bigint);\nINSERT INTO t_demo VALUES (1);\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
DatabaseBackupCollector.CollectedDatabaseDump cmepDump = new DatabaseBackupCollector.CollectedDatabaseDump();
|
||||
cmepDump.setDatabaseName("CMEP");
|
||||
cmepDump.setFileName("CMEP.sql");
|
||||
cmepDump.setContent("CREATE TABLE c_demo(id bigint);\nINSERT INTO c_demo VALUES (2);\n".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
Mockito.when(collector.collectTmsDatabaseDump()).thenReturn(tmsDump);
|
||||
Mockito.when(collector.collectCmepDatabaseDump()).thenReturn(cmepDump);
|
||||
return collector;
|
||||
}
|
||||
|
||||
private Path writePayloadFile(String relativePath, String content) throws Exception {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
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 ConsistencyVerifyRunnerStepExecutorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldVerifyRequiredStepOutputsAndHealthEndpoint() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setHealthCheckUrl("http://127.0.0.1:8080/actuator/health");
|
||||
HttpClient httpClient = Mockito.mock(HttpClient.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
HttpResponse<String> response = Mockito.mock(HttpResponse.class);
|
||||
Mockito.when(response.statusCode()).thenReturn(200);
|
||||
Mockito.when(response.body()).thenReturn("{\"status\":\"UP\"}");
|
||||
Mockito.when(httpClient.send(Mockito.any(HttpRequest.class), Mockito.<HttpResponse.BodyHandler<String>>any()))
|
||||
.thenReturn(response);
|
||||
ConsistencyVerifyRunnerStepExecutor executor = new ConsistencyVerifyRunnerStepExecutor(
|
||||
properties,
|
||||
new ObjectMapper(),
|
||||
httpClient
|
||||
);
|
||||
|
||||
Path taskDir = tempDir.resolve("task");
|
||||
Files.createDirectories(taskDir.resolve("step-output"));
|
||||
Files.writeString(taskDir.resolve("step-output/02-RESTORE_FILES.json"), "{\"result\":\"SUCCESS\"}");
|
||||
Files.writeString(taskDir.resolve("step-output/03-RESTORE_DATABASES.json"), "{\"result\":\"SUCCESS\"}");
|
||||
Files.writeString(taskDir.resolve("step-output/04-START_SERVICES.json"), "{\"result\":\"SUCCESS\"}");
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260418-000004",
|
||||
"CONSISTENCY_VERIFY",
|
||||
5,
|
||||
taskDir,
|
||||
taskDir.resolve("handoff.json"),
|
||||
taskDir.resolve("restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertTrue(result.getSummary().contains("health endpoint is UP"));
|
||||
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
Mockito.verify(httpClient).send(requestCaptor.capture(), Mockito.<HttpResponse.BodyHandler<String>>any());
|
||||
Assertions.assertEquals("http://127.0.0.1:8080/actuator/health", requestCaptor.getValue().uri().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenRequiredStepOutputIsMissing() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
ConsistencyVerifyRunnerStepExecutor executor = new ConsistencyVerifyRunnerStepExecutor(
|
||||
properties,
|
||||
new ObjectMapper(),
|
||||
Mockito.mock(HttpClient.class)
|
||||
);
|
||||
Path taskDir = tempDir.resolve("task-missing");
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260418-000005",
|
||||
"CONSISTENCY_VERIFY",
|
||||
5,
|
||||
taskDir,
|
||||
taskDir.resolve("handoff.json"),
|
||||
taskDir.resolve("restore-state.json")
|
||||
))
|
||||
);
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("step output"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenHealthEndpointIsNotUp() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setHealthCheckUrl("http://127.0.0.1:8080/actuator/health");
|
||||
HttpClient httpClient = Mockito.mock(HttpClient.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
HttpResponse<String> response = Mockito.mock(HttpResponse.class);
|
||||
Mockito.when(response.statusCode()).thenReturn(503);
|
||||
Mockito.when(response.body()).thenReturn("{\"status\":\"DOWN\"}");
|
||||
Mockito.when(httpClient.send(Mockito.any(HttpRequest.class), Mockito.<HttpResponse.BodyHandler<String>>any()))
|
||||
.thenReturn(response);
|
||||
ConsistencyVerifyRunnerStepExecutor executor = new ConsistencyVerifyRunnerStepExecutor(
|
||||
properties,
|
||||
new ObjectMapper(),
|
||||
httpClient
|
||||
);
|
||||
|
||||
Path taskDir = tempDir.resolve("task-down");
|
||||
Files.createDirectories(taskDir.resolve("step-output"));
|
||||
Files.writeString(taskDir.resolve("step-output/02-RESTORE_FILES.json"), "{\"result\":\"SUCCESS\"}");
|
||||
Files.writeString(taskDir.resolve("step-output/03-RESTORE_DATABASES.json"), "{\"result\":\"SUCCESS\"}");
|
||||
Files.writeString(taskDir.resolve("step-output/04-START_SERVICES.json"), "{\"result\":\"SUCCESS\"}");
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260418-000006",
|
||||
"CONSISTENCY_VERIFY",
|
||||
5,
|
||||
taskDir,
|
||||
taskDir.resolve("handoff.json"),
|
||||
taskDir.resolve("restore-state.json")
|
||||
))
|
||||
);
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("failed to verify health endpoint"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,196 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
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.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class DatabaseRestoreRunnerStepExecutorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExecuteRestoreScriptForTmsAndCmepSql() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRestoreDbScriptPath("/tmp/restore-db.sh");
|
||||
properties.setMysqlPath("/usr/bin/mysql");
|
||||
properties.setTmsDatabaseName("TMS");
|
||||
properties.setCmepDatabaseName("CMEP");
|
||||
RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class);
|
||||
Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(0);
|
||||
|
||||
DatabaseRestoreRunnerStepExecutor executor = new DatabaseRestoreRunnerStepExecutor(
|
||||
properties,
|
||||
invoker,
|
||||
"jdbc:mysql://127.0.0.1:4000/TMS?useSSL=false",
|
||||
"root",
|
||||
"secret"
|
||||
);
|
||||
Path sqlFile = tempDir.resolve("task/staging/payload/db/TMS.sql");
|
||||
Path cmepSqlFile = tempDir.resolve("task/staging/payload/db/CMEP.sql");
|
||||
Files.createDirectories(sqlFile.getParent());
|
||||
Files.writeString(sqlFile, "CREATE TABLE t_demo(id bigint);");
|
||||
Files.writeString(cmepSqlFile, "CREATE TABLE c_demo(id bigint);");
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260418-000001",
|
||||
"RESTORE_DATABASES",
|
||||
4,
|
||||
tempDir.resolve("task"),
|
||||
tempDir.resolve("task/handoff.json"),
|
||||
tempDir.resolve("task/restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertTrue(result.getSummary().contains("TMS"));
|
||||
Assertions.assertTrue(result.getSummary().contains("CMEP"));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
|
||||
Mockito.verify(invoker, Mockito.times(2)).run(commandCaptor.capture(), Mockito.any(), Mockito.eq(tempDir.resolve("task")));
|
||||
List<List<String>> commands = commandCaptor.getAllValues();
|
||||
Assertions.assertEquals(
|
||||
List.of(
|
||||
"/bin/bash",
|
||||
"/tmp/restore-db.sh",
|
||||
"--mysql-path",
|
||||
"/usr/bin/mysql",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
"4000",
|
||||
"--username",
|
||||
"root",
|
||||
"--password",
|
||||
"secret",
|
||||
"--db-name",
|
||||
"TMS",
|
||||
"--sql-file",
|
||||
sqlFile.toString()
|
||||
),
|
||||
commands.get(0)
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
List.of(
|
||||
"/bin/bash",
|
||||
"/tmp/restore-db.sh",
|
||||
"--mysql-path",
|
||||
"/usr/bin/mysql",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
"4000",
|
||||
"--username",
|
||||
"root",
|
||||
"--password",
|
||||
"secret",
|
||||
"--db-name",
|
||||
"CMEP",
|
||||
"--sql-file",
|
||||
cmepSqlFile.toString()
|
||||
),
|
||||
commands.get(1)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenSqlFileIsMissing() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class);
|
||||
DatabaseRestoreRunnerStepExecutor executor = new DatabaseRestoreRunnerStepExecutor(
|
||||
properties,
|
||||
invoker,
|
||||
"jdbc:mysql://127.0.0.1:4000/TMS",
|
||||
"root",
|
||||
"secret"
|
||||
);
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260418-000002",
|
||||
"RESTORE_DATABASES",
|
||||
4,
|
||||
tempDir.resolve("task"),
|
||||
tempDir.resolve("task/handoff.json"),
|
||||
tempDir.resolve("task/restore-state.json")
|
||||
))
|
||||
);
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("sql file missing"));
|
||||
Mockito.verifyNoInteractions(invoker);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipCmepWhenSqlFileIsAbsent() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRestoreDbScriptPath("/tmp/restore-db.sh");
|
||||
properties.setMysqlPath("/usr/bin/mysql");
|
||||
properties.setTmsDatabaseName("TMS");
|
||||
properties.setCmepDatabaseName("CMEP");
|
||||
RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class);
|
||||
Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(0);
|
||||
DatabaseRestoreRunnerStepExecutor executor = new DatabaseRestoreRunnerStepExecutor(
|
||||
properties,
|
||||
invoker,
|
||||
"jdbc:mysql://127.0.0.1:4000/TMS",
|
||||
"root",
|
||||
"secret"
|
||||
);
|
||||
Path sqlFile = tempDir.resolve("task/staging/payload/db/TMS.sql");
|
||||
Files.createDirectories(sqlFile.getParent());
|
||||
Files.writeString(sqlFile, "CREATE TABLE t_demo(id bigint);");
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260418-000004",
|
||||
"RESTORE_DATABASES",
|
||||
4,
|
||||
tempDir.resolve("task"),
|
||||
tempDir.resolve("task/handoff.json"),
|
||||
tempDir.resolve("task/restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertTrue(result.getSummary().contains("TMS"));
|
||||
Assertions.assertFalse(result.getSummary().contains("CMEP"));
|
||||
Mockito.verify(invoker, Mockito.times(1)).run(Mockito.anyList(), Mockito.any(), Mockito.eq(tempDir.resolve("task")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenRestoreScriptReturnsNonZeroExitCode() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRestoreDbScriptPath("/tmp/restore-db.sh");
|
||||
RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class);
|
||||
Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(7);
|
||||
DatabaseRestoreRunnerStepExecutor executor = new DatabaseRestoreRunnerStepExecutor(
|
||||
properties,
|
||||
invoker,
|
||||
"jdbc:mysql://127.0.0.1:4000/TMS",
|
||||
"root",
|
||||
"secret"
|
||||
);
|
||||
Path sqlFile = tempDir.resolve("task/staging/payload/db/TMS.sql");
|
||||
Files.createDirectories(sqlFile.getParent());
|
||||
Files.writeString(sqlFile, "CREATE TABLE t_demo(id bigint);");
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260418-000003",
|
||||
"RESTORE_DATABASES",
|
||||
4,
|
||||
tempDir.resolve("task"),
|
||||
tempDir.resolve("task/handoff.json"),
|
||||
tempDir.resolve("task/restore-state.json")
|
||||
))
|
||||
);
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("exitCode=7"));
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ 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.DatabaseBackupCollector;
|
||||
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;
|
||||
@ -28,14 +29,16 @@ class PayloadArchiveRunnerStepExecutorTest {
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExtractPayloadArchiveIntoStagingDirectory() throws Exception {
|
||||
void shouldExtractPayloadArchiveIntoStagingDirectoryAndRestoreCriticalFiles() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourcePackageService packageService = new ResourcePackageServiceImpl(
|
||||
backupProperties(),
|
||||
new ResourceEnvelopeCryptoServiceImpl(mockCrypto(), backupProperties()),
|
||||
mockDatabaseBackupCollector(),
|
||||
objectMapper
|
||||
);
|
||||
Path sourceFile = writeFile("source/application.yml", "server.port=8088");
|
||||
Path sourceFile = writeFile("source-config/application.yml", "server.port=8088");
|
||||
Path cpConfig = writeFile("runtime/cpconfig.cfg", "cp=old");
|
||||
Path taskDir = tempDir.resolve("restore-task");
|
||||
Files.createDirectories(taskDir);
|
||||
|
||||
@ -43,10 +46,18 @@ class PayloadArchiveRunnerStepExecutorTest {
|
||||
"RBKP-TASK-20260414-000101",
|
||||
"RBKP-20260414-000101",
|
||||
sampleInitTask(),
|
||||
List.of(resource("TMS_CONFIG", sourceFile.toString())),
|
||||
List.of(
|
||||
resource("TMS_CONFIG", sourceFile.getParent().toString()),
|
||||
resource("CP_CONFIG", cpConfig.toString()),
|
||||
databaseResource("TMS_DB_DUMP", "TMS"),
|
||||
databaseResource("CMEP_DB_DUMP", "CMEP")
|
||||
),
|
||||
new CreateResourceBackupRequest()
|
||||
).getPackagePath();
|
||||
|
||||
Files.delete(sourceFile);
|
||||
Files.writeString(cpConfig, "cp=newer-runtime-content");
|
||||
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260414-000001");
|
||||
entity.setBackupId("RBKP-20260414-000101");
|
||||
@ -73,44 +84,12 @@ class PayloadArchiveRunnerStepExecutorTest {
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertTrue(result.getSummary().contains("payload extracted"));
|
||||
Assertions.assertTrue(result.getSummary().contains("restoredFileCount=2"));
|
||||
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"));
|
||||
Assertions.assertTrue(Files.exists(taskDir.resolve("staging/payload/db/TMS.sql")));
|
||||
Assertions.assertTrue(Files.exists(taskDir.resolve("staging/payload/db/CMEP.sql")));
|
||||
Assertions.assertEquals("server.port=8088", Files.readString(sourceFile));
|
||||
Assertions.assertEquals("cp=old", Files.readString(cpConfig));
|
||||
}
|
||||
|
||||
private ResourceBackupProperties backupProperties() {
|
||||
@ -171,4 +150,28 @@ class PayloadArchiveRunnerStepExecutorTest {
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem databaseResource(String code, String path) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("DB");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static DatabaseBackupCollector mockDatabaseBackupCollector() {
|
||||
DatabaseBackupCollector collector = Mockito.mock(DatabaseBackupCollector.class);
|
||||
DatabaseBackupCollector.CollectedDatabaseDump tmsDump = new DatabaseBackupCollector.CollectedDatabaseDump();
|
||||
tmsDump.setDatabaseName("TMS");
|
||||
tmsDump.setFileName("TMS.sql");
|
||||
tmsDump.setContent("CREATE TABLE t_restore(id bigint);".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
DatabaseBackupCollector.CollectedDatabaseDump cmepDump = new DatabaseBackupCollector.CollectedDatabaseDump();
|
||||
cmepDump.setDatabaseName("CMEP");
|
||||
cmepDump.setFileName("CMEP.sql");
|
||||
cmepDump.setContent("CREATE TABLE c_restore(id bigint);".getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
Mockito.when(collector.collectTmsDatabaseDump()).thenReturn(tmsDump);
|
||||
Mockito.when(collector.collectCmepDatabaseDump()).thenReturn(cmepDump);
|
||||
return collector;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
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.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ProcessResourceRestoreRunnerLauncherTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldSkipLaunchWhenAutoLaunchDisabled() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRunnerAutoLaunchEnabled(false);
|
||||
RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class);
|
||||
|
||||
ProcessResourceRestoreRunnerLauncher launcher = new ProcessResourceRestoreRunnerLauncher(properties, invoker);
|
||||
launcher.launch(tempDir.resolve("handoff.json"));
|
||||
|
||||
Mockito.verifyNoInteractions(invoker);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInvokeExternalLaunchScriptWhenAutoLaunchEnabled() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRunnerAutoLaunchEnabled(true);
|
||||
properties.setRunnerLaunchScriptPath("/tmp/start-runner.sh");
|
||||
RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class);
|
||||
Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(0);
|
||||
|
||||
ProcessResourceRestoreRunnerLauncher launcher = new ProcessResourceRestoreRunnerLauncher(properties, invoker);
|
||||
Path handoffFile = tempDir.resolve("task/handoff.json");
|
||||
launcher.launch(handoffFile);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
|
||||
ArgumentCaptor<Path> logCaptor = ArgumentCaptor.forClass(Path.class);
|
||||
ArgumentCaptor<Path> workDirCaptor = ArgumentCaptor.forClass(Path.class);
|
||||
Mockito.verify(invoker).run(commandCaptor.capture(), logCaptor.capture(), workDirCaptor.capture());
|
||||
|
||||
Assertions.assertEquals(
|
||||
List.of("/bin/bash", "/tmp/start-runner.sh", "--handoff-file", handoffFile.toAbsolutePath().normalize().toString()),
|
||||
commandCaptor.getValue()
|
||||
);
|
||||
Assertions.assertEquals(handoffFile.toAbsolutePath().normalize().getParent(), workDirCaptor.getValue());
|
||||
Assertions.assertEquals(handoffFile.toAbsolutePath().normalize().getParent().resolve("runner-launch.log"), logCaptor.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenLaunchScriptReturnsNonZeroExitCode() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRunnerAutoLaunchEnabled(true);
|
||||
properties.setRunnerLaunchScriptPath("/tmp/start-runner.sh");
|
||||
RunnerCommandInvoker invoker = Mockito.mock(RunnerCommandInvoker.class);
|
||||
Mockito.when(invoker.run(Mockito.anyList(), Mockito.any(), Mockito.any())).thenReturn(9);
|
||||
|
||||
ProcessResourceRestoreRunnerLauncher launcher = new ProcessResourceRestoreRunnerLauncher(properties, invoker);
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> launcher.launch(tempDir.resolve("task/handoff.json"))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("exitCode=9"));
|
||||
}
|
||||
}
|
||||
@ -1,111 +1,150 @@
|
||||
//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;
|
||||
// }
|
||||
//}
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
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.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceBackupTaskEntity;
|
||||
import com.cisd.tms.modules.backup.packagex.RequiredBackupResourceMissingException;
|
||||
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 shouldCreateSuccessfulBackupTaskFromLatestSuccessfulInitSnapshot() {
|
||||
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", true));
|
||||
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()
|
||||
);
|
||||
|
||||
CreateResourceBackupResponse response = service.createBackupTask(sampleRequest());
|
||||
|
||||
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("SUCCESS", saved.getStatus());
|
||||
Assertions.assertEquals(packageResult.getPackagePath(), saved.getPackagePath());
|
||||
Assertions.assertEquals(packageResult.getManifestSummaryJson(), saved.getManifestSummaryJson());
|
||||
Assertions.assertNull(saved.getErrorCode());
|
||||
Assertions.assertNull(saved.getErrorMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistFailedTaskWhenRequiredResourceIsMissing() {
|
||||
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", true));
|
||||
|
||||
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)
|
||||
)).thenThrow(new RequiredBackupResourceMissingException("required backup resource is unavailable: code=TMS_CONFIG"));
|
||||
|
||||
ResourceBackupService service = new ResourceBackupServiceImpl(
|
||||
initTaskRepository,
|
||||
resourceDiscoveryService,
|
||||
resourcePackageService,
|
||||
resourceBackupTaskRepository,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new ResourceBackupProperties()
|
||||
);
|
||||
|
||||
CreateResourceBackupResponse response = service.createBackupTask(sampleRequest());
|
||||
|
||||
Assertions.assertEquals("FAILED", response.getStatus());
|
||||
|
||||
ArgumentCaptor<ResourceBackupTaskEntity> captor = ArgumentCaptor.forClass(ResourceBackupTaskEntity.class);
|
||||
Mockito.verify(resourceBackupTaskRepository).save(captor.capture());
|
||||
ResourceBackupTaskEntity saved = captor.getValue();
|
||||
Assertions.assertEquals("FAILED", saved.getStatus());
|
||||
Assertions.assertEquals("REQUIRED_RESOURCE_MISSING", saved.getErrorCode());
|
||||
Assertions.assertTrue(saved.getErrorMessage().contains("TMS_CONFIG"));
|
||||
Assertions.assertNull(saved.getPackagePath());
|
||||
}
|
||||
|
||||
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, boolean required) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("FILE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(required);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static CreateResourceBackupRequest sampleRequest() {
|
||||
CreateResourceBackupRequest request = new CreateResourceBackupRequest();
|
||||
request.setIncludeUpgradeHistory(true);
|
||||
request.setIncludeAuditBackups(false);
|
||||
request.setRemark("before hardware replacement");
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,104 +1,103 @@
|
||||
//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());
|
||||
// }
|
||||
//}
|
||||
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.backup.runner.ResourceRestoreRunnerLauncher;
|
||||
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.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
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 shouldCreateRestoreTaskWriteHandoffFilesAndLaunchRunner() 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);
|
||||
ResourceRestoreRunnerLauncher runnerLauncher = Mockito.mock(ResourceRestoreRunnerLauncher.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("RESTORE_LOCK", "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,
|
||||
runnerLauncher
|
||||
);
|
||||
|
||||
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(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());
|
||||
|
||||
ArgumentCaptor<ResourceRestoreTaskEntity> captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class);
|
||||
Mockito.verify(repository).save(captor.capture());
|
||||
ResourceRestoreTaskEntity saved = captor.getValue();
|
||||
Assertions.assertEquals(packagePath.toString(), saved.getPackagePath());
|
||||
Assertions.assertEquals("HANDOFF", saved.getStatus());
|
||||
|
||||
Mockito.verify(runnerLauncher).launch(handoffFile);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user