feat:备份恢复功能
This commit is contained in:
@@ -227,7 +227,6 @@ Open:
|
||||
## Resource Backup / Restore
|
||||
|
||||
最小调用顺序:
|
||||
- `GET /api/v1/resource-backups/keyset/status`
|
||||
- `POST /api/v1/resource-backups`
|
||||
- `GET /api/v1/resource-backups/{taskId}`
|
||||
- `GET /api/v1/resource-backups/{taskId}/download`
|
||||
@@ -237,54 +236,48 @@ Open:
|
||||
- `GET /api/v1/resource-restores/{taskId}/steps`
|
||||
- `GET /api/v1/resource-restores/{taskId}/steps/{stepNo}/log`
|
||||
|
||||
前后端字段、状态枚举和联调流程见 `docs/openapi/resource-backup-restore-frontend-integration.md`。
|
||||
|
||||
当前实现边界:
|
||||
- 资源备份包输出为 `.tmsbak`,包内包含 `manifest.json`、`envelope.json`、`payload.enc`、`signature.sig`
|
||||
- 当前第一版备份/恢复只覆盖最小资源集:
|
||||
- `TMS_CONFIG`
|
||||
- `TMS_DB_DUMP`
|
||||
- `CMEP_DB_DUMP`
|
||||
- `TMS_DB`
|
||||
- `CMEP_DB`
|
||||
- `CP_CONFIG`
|
||||
- `RECEIVER_LICENSE`
|
||||
- `MQ_REPLAY`
|
||||
- 预检阶段只做包头解析、指纹匹配和验签,不提前解密完整 payload
|
||||
- 恢复任务创建后会生成 `handoff.json`、`restore-state.json`,并进入 `HANDOFF_READY`
|
||||
- `ResourceRestoreRunner` 当前已具备接管、步骤循环、`step-output/*.json`、心跳刷新、`RECONCILE_PENDING` 推进能力
|
||||
- runner 现已通过外部脚本启动独立进程,不再运行在 TMS Web 进程内
|
||||
- 当前已落地的恢复步骤:
|
||||
- `STOP_SERVICES`
|
||||
- `RESTORE_FILES`
|
||||
- `RESTORE_DATABASES`
|
||||
- `START_SERVICES`
|
||||
- `CONSISTENCY_VERIFY`
|
||||
- `RESTORE_FILES` 当前只回写关键配置/License 文件,不做目录级全量恢复
|
||||
- `RESTORE_DATABASES` 当前恢复:
|
||||
- 恢复任务创建时 Java 完成验签、PCIe 解密、payload 展开和恢复路径白名单校验
|
||||
- Java 会生成 shell 友好的 `restore-files.tsv`、`restore-databases.tsv`、`restore-mq.tsv`
|
||||
- 创建恢复任务后启动 `apply-resource-backup.sh --work-dir <workDir>`,由 shell 执行停服务、文件覆盖、SQL 导入、MQ replay、起服务和健康检查
|
||||
- shell 侧状态写入 `status.json`,日志写入 `restore.log`
|
||||
- `FILE` 回写 manifest 明确声明的配置/License 文件
|
||||
- `DATABASE` 恢复:
|
||||
- `db/TMS.sql`
|
||||
- `db/CMEP.sql`(包内存在时)
|
||||
- `CONSISTENCY_VERIFY` 当前只检查:
|
||||
- `RESTORE_FILES` 成功
|
||||
- `RESTORE_DATABASES` 成功
|
||||
- `START_SERVICES` 成功
|
||||
- 健康检查地址返回 `UP`
|
||||
- `MQ_REPLAY` 通过 `replay-mq.sh` 消费 `mq/mq-restore-context.json`,也可用 `MQ_REPLAY_COMMAND` 委派给现场脚本
|
||||
- Java 启动恢复脚本前会在工作目录生成 `restore-env.sh`,传递数据库连接、恢复脚本路径、MQ replay 脚本路径和健康检查配置
|
||||
|
||||
关键配置:
|
||||
- `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.restore-apply-script-path`
|
||||
- `tms.backup.allowed-restore-roots`
|
||||
- `tms.backup.mysqldump-path`
|
||||
- `tms.backup.mysql-path`
|
||||
- `tms.backup.restore-db-script-path`
|
||||
- `tms.backup.mq-replay-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` 必须已部署并可执行
|
||||
- `/home/tms/bin/resource-restore/apply-resource-backup.sh`、`restore-db.sh`、`replay-mq.sh` 必须已部署并可执行
|
||||
- `mysql` / `mysqldump` 客户端必须可用
|
||||
|
||||
当前明确不在第一版恢复范围内:
|
||||
@@ -292,8 +285,7 @@ Open:
|
||||
- `organization.json`
|
||||
- 已发布 web 静态资源
|
||||
- `/home/tms/uploads`
|
||||
- RabbitMQ / TLQ / CFMQ
|
||||
- `CMEP` / 机构库
|
||||
- 机构库
|
||||
- 在线恢复 / 增量恢复 / 回滚
|
||||
- 升级执行日志统一写入 `tms.upgrade.log-dir`
|
||||
|
||||
|
||||
@@ -874,3 +874,27 @@ bash -n /home/tms/bin/start_standard_apps.sh
|
||||
bash -n /home/tms/bin/start_standard_nginx.sh
|
||||
bash -n /home/tms/bin/check_standard_runtime.sh
|
||||
```
|
||||
|
||||
如启用当前第一版资源备份恢复能力,再额外检查:
|
||||
|
||||
```bash
|
||||
ls -l /home/tms/bin/resource-restore
|
||||
bash -n /home/tms/bin/resource-restore/apply-resource-backup.sh
|
||||
bash -n /home/tms/bin/resource-restore/restore-db.sh
|
||||
bash -n /home/tms/bin/resource-restore/replay-mq.sh
|
||||
which mysql
|
||||
which mysqldump
|
||||
```
|
||||
|
||||
当前第一版资源恢复只覆盖:
|
||||
- `TMS_CONFIG`
|
||||
- `TMS_DB`
|
||||
- `CMEP_DB`
|
||||
- `CP_CONFIG`
|
||||
- `RECEIVER_LICENSE`
|
||||
- `MQ_REPLAY`
|
||||
|
||||
当前第一版明确不覆盖:
|
||||
- nginx / web / uploads
|
||||
- RabbitMQ / TLQ / CFMQ 运行时文件
|
||||
- 机构库
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
WORK_DIR=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--work-dir)
|
||||
WORK_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] unsupported argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${WORK_DIR}" ]]; then
|
||||
echo "[ERROR] --work-dir is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PAYLOAD_DIR="${WORK_DIR}/payload"
|
||||
STATUS_FILE="${WORK_DIR}/status.json"
|
||||
LOG_FILE="${WORK_DIR}/restore.log"
|
||||
MANIFEST="${PAYLOAD_DIR}/payload-manifest.json"
|
||||
FILES_PLAN="${WORK_DIR}/restore-files.tsv"
|
||||
DATABASES_PLAN="${WORK_DIR}/restore-databases.tsv"
|
||||
MQ_PLAN="${WORK_DIR}/restore-mq.tsv"
|
||||
ENV_FILE="${WORK_DIR}/restore-env.sh"
|
||||
|
||||
json_escape() {
|
||||
local value="$1"
|
||||
value="${value//\\/\\\\}"
|
||||
value="${value//\"/\\\"}"
|
||||
value="${value//$'\n'/ }"
|
||||
printf '%s' "${value}"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
local status="$1"
|
||||
local phase="$2"
|
||||
local message="$3"
|
||||
printf '{"status":"%s","phase":"%s","message":"%s","updatedAt":"%s"}\n' \
|
||||
"$(json_escape "${status}")" \
|
||||
"$(json_escape "${phase}")" \
|
||||
"$(json_escape "${message}")" \
|
||||
"$(date '+%Y-%m-%dT%H:%M:%S%z')" > "${STATUS_FILE}"
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%F %T')" "$1" >> "${LOG_FILE}"
|
||||
}
|
||||
|
||||
fail() {
|
||||
local message="$1"
|
||||
local phase="${2:-FAILED}"
|
||||
log "[ERROR] ${message}"
|
||||
write_status "FAILED" "${phase}" "${message}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_env() {
|
||||
local name="$1"
|
||||
local value="${!name:-}"
|
||||
if [[ -z "${value}" ]]; then
|
||||
fail "required environment variable ${name} is empty" "${2:-PRECHECK}"
|
||||
fi
|
||||
}
|
||||
|
||||
restore_files() {
|
||||
[[ -f "${FILES_PLAN}" ]] || return 0
|
||||
while IFS=$'\t' read -r entry_path restore_path || [[ -n "${entry_path:-}" ]]; do
|
||||
[[ -n "${entry_path:-}" ]] || continue
|
||||
[[ -n "${restore_path:-}" ]] || fail "restore file target is empty for ${entry_path}" "FILE"
|
||||
local source_file="${PAYLOAD_DIR}/${entry_path}"
|
||||
[[ -f "${source_file}" ]] || fail "restore source file not found: ${entry_path}" "FILE"
|
||||
mkdir -p "$(dirname "${restore_path}")"
|
||||
cp -f "${source_file}" "${restore_path}"
|
||||
log "restored file: ${restore_path}"
|
||||
done < "${FILES_PLAN}"
|
||||
}
|
||||
|
||||
restore_databases() {
|
||||
[[ -f "${DATABASES_PLAN}" ]] || return 0
|
||||
[[ -s "${DATABASES_PLAN}" ]] || return 0
|
||||
local script="${DB_RESTORE_SCRIPT_PATH:-}"
|
||||
[[ -n "${script}" && -x "${script}" ]] || fail "DB_RESTORE_SCRIPT_PATH is not executable" "DATABASE"
|
||||
require_env "DB_HOST" "DATABASE"
|
||||
require_env "DB_PORT" "DATABASE"
|
||||
require_env "DB_USER" "DATABASE"
|
||||
local mysql_path="${MYSQL_PATH:-mysql}"
|
||||
local db_password="${DB_PASSWORD:-}"
|
||||
while IFS=$'\t' read -r entry_path db_name || [[ -n "${entry_path:-}" ]]; do
|
||||
[[ -n "${entry_path:-}" ]] || continue
|
||||
[[ -n "${db_name:-}" ]] || fail "database name is empty for ${entry_path}" "DATABASE"
|
||||
local sql_file="${PAYLOAD_DIR}/${entry_path}"
|
||||
[[ -f "${sql_file}" ]] || fail "sql file not found: ${entry_path}" "DATABASE"
|
||||
"${script}" \
|
||||
--mysql-path "${mysql_path}" \
|
||||
--host "${DB_HOST}" \
|
||||
--port "${DB_PORT}" \
|
||||
--username "${DB_USER}" \
|
||||
--password "${db_password}" \
|
||||
--db-name "${db_name}" \
|
||||
--sql-file "${sql_file}" >> "${LOG_FILE}" 2>&1
|
||||
log "restored database: ${db_name}"
|
||||
done < "${DATABASES_PLAN}"
|
||||
}
|
||||
|
||||
replay_mq() {
|
||||
[[ -f "${MQ_PLAN}" ]] || return 0
|
||||
[[ -s "${MQ_PLAN}" ]] || return 0
|
||||
local script="${MQ_REPLAY_SCRIPT_PATH:-}"
|
||||
[[ -n "${script}" && -x "${script}" ]] || fail "MQ_REPLAY_SCRIPT_PATH is not executable" "MQ_REPLAY"
|
||||
while IFS= read -r entry_path || [[ -n "${entry_path:-}" ]]; do
|
||||
[[ -n "${entry_path:-}" ]] || continue
|
||||
local context_file="${PAYLOAD_DIR}/${entry_path}"
|
||||
[[ -f "${context_file}" ]] || fail "mq replay context not found: ${entry_path}" "MQ_REPLAY"
|
||||
"${script}" --context-file "${context_file}" --work-dir "${WORK_DIR}" >> "${LOG_FILE}" 2>&1
|
||||
log "mq replay applied"
|
||||
done < "${MQ_PLAN}"
|
||||
}
|
||||
|
||||
health_check() {
|
||||
local url="${HEALTH_CHECK_URL:-}"
|
||||
[[ -n "${url}" ]] || {
|
||||
log "health check skipped: HEALTH_CHECK_URL is empty"
|
||||
return 0
|
||||
}
|
||||
command -v curl >/dev/null 2>&1 || fail "curl command not found for health check" "HEALTH_CHECK"
|
||||
local timeout="${HEALTH_CHECK_TIMEOUT_SECONDS:-5}"
|
||||
local response
|
||||
response="$(curl -fsS --max-time "${timeout}" "${url}")" || fail "health check request failed" "HEALTH_CHECK"
|
||||
if [[ "${response}" != *"UP"* ]]; then
|
||||
fail "health check did not return UP" "HEALTH_CHECK"
|
||||
fi
|
||||
log "health check passed"
|
||||
}
|
||||
|
||||
mkdir -p "${WORK_DIR}"
|
||||
touch "${LOG_FILE}"
|
||||
chmod 700 "${WORK_DIR}" || true
|
||||
|
||||
if [[ -f "${ENV_FILE}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${ENV_FILE}"
|
||||
fi
|
||||
|
||||
[[ -f "${MANIFEST}" ]] || fail "payload manifest not found" "PRECHECK"
|
||||
|
||||
write_status "RUNNING" "SERVICE_STOP" "stopping services"
|
||||
log "SERVICE_STOP"
|
||||
if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then
|
||||
"${TMS_SCRIPT_PATH}" stop >> "${LOG_FILE}" 2>&1 || true
|
||||
fi
|
||||
|
||||
write_status "RUNNING" "FILE" "restoring files"
|
||||
log "FILE"
|
||||
restore_files
|
||||
|
||||
write_status "RUNNING" "DATABASE" "restoring databases"
|
||||
log "DATABASE"
|
||||
restore_databases
|
||||
|
||||
write_status "RUNNING" "MQ_REPLAY" "replaying mq configuration"
|
||||
log "MQ_REPLAY"
|
||||
replay_mq
|
||||
|
||||
write_status "RUNNING" "SERVICE_START" "starting services"
|
||||
log "SERVICE_START"
|
||||
if [[ -n "${TMS_SCRIPT_PATH:-}" && -x "${TMS_SCRIPT_PATH}" ]]; then
|
||||
"${TMS_SCRIPT_PATH}" start >> "${LOG_FILE}" 2>&1 || true
|
||||
fi
|
||||
|
||||
write_status "RUNNING" "HEALTH_CHECK" "checking health"
|
||||
log "HEALTH_CHECK"
|
||||
health_check
|
||||
|
||||
write_status "SUCCESS" "DONE" "restore applied"
|
||||
log "DONE"
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTEXT_FILE=""
|
||||
WORK_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--context-file)
|
||||
CONTEXT_FILE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--work-dir)
|
||||
WORK_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "[ERROR] unsupported argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${CONTEXT_FILE}" || ! -f "${CONTEXT_FILE}" ]]; then
|
||||
echo "[ERROR] --context-file is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="${WORK_DIR:-$(dirname "${CONTEXT_FILE}")}"
|
||||
mkdir -p "${WORK_DIR}"
|
||||
LOG_FILE="${WORK_DIR}/mq-replay.log"
|
||||
touch "${LOG_FILE}"
|
||||
|
||||
log() {
|
||||
printf '[%s] %s\n' "$(date '+%F %T')" "$1" >> "${LOG_FILE}"
|
||||
}
|
||||
|
||||
json_text() {
|
||||
local key="$1"
|
||||
sed -nE 's/.*"'"${key}"'"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p' "${CONTEXT_FILE}" | head -n 1
|
||||
}
|
||||
|
||||
find_ctl() {
|
||||
local configured="${RABBITMQ_CTL_COMMAND:-}"
|
||||
if [[ -n "${configured}" ]]; then
|
||||
printf '%s' "${configured}"
|
||||
return 0
|
||||
fi
|
||||
for candidate in rabbitmqctl /usr/local/rabbitmq/sbin/rabbitmqctl /usr/local/rabbitmq-server/sbin/rabbitmqctl /usr/sbin/rabbitmqctl /usr/lib/rabbitmq/bin/rabbitmqctl; do
|
||||
if command -v "${candidate}" >/dev/null 2>&1; then
|
||||
command -v "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
if [[ -x "${candidate}" ]]; then
|
||||
printf '%s' "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "${MQ_REPLAY_COMMAND:-}" ]]; then
|
||||
log "delegating mq replay to configured command"
|
||||
"${MQ_REPLAY_COMMAND}" --context-file "${CONTEXT_FILE}" --work-dir "${WORK_DIR}" >> "${LOG_FILE}" 2>&1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MQ_TYPE="$(json_text mqType)"
|
||||
USER_NAME="$(json_text channelUsername)"
|
||||
USER_PASS="$(json_text channelPassword)"
|
||||
ORG_CODE="$(json_text orgCode)"
|
||||
VHOST="${RABBIT_VHOST:-/RQ}"
|
||||
|
||||
case "${MQ_TYPE}" in
|
||||
RABBITMQ|RABBITMQ_TLQ|RABBITMQ_CFMQ)
|
||||
;;
|
||||
*)
|
||||
log "mq replay skipped for mqType=${MQ_TYPE}"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "${USER_NAME}" || -z "${USER_PASS}" ]]; then
|
||||
echo "[ERROR] mq replay context missing channel account" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CTL="$(find_ctl)" || {
|
||||
echo "[ERROR] rabbitmqctl not found" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
"${CTL}" status >> "${LOG_FILE}" 2>&1 || log "rabbitmqctl status failed, continue replay"
|
||||
"${CTL}" add_vhost "${VHOST}" >> "${LOG_FILE}" 2>&1 || true
|
||||
"${CTL}" add_user "${USER_NAME}" "${USER_PASS}" >> "${LOG_FILE}" 2>&1 || "${CTL}" change_password "${USER_NAME}" "${USER_PASS}" >> "${LOG_FILE}" 2>&1
|
||||
"${CTL}" set_permissions -p "${VHOST}" "${USER_NAME}" ".*" ".*" ".*" >> "${LOG_FILE}" 2>&1
|
||||
log "rabbitmq user and permissions replayed"
|
||||
|
||||
if [[ -n "${RABBIT_QUEUE_REPLAY_SCRIPT_PATH:-}" && -x "${RABBIT_QUEUE_REPLAY_SCRIPT_PATH}" ]]; then
|
||||
STAGED_QUEUE_SCRIPT="${WORK_DIR}/$(basename "${RABBIT_QUEUE_REPLAY_SCRIPT_PATH}")"
|
||||
cp -f "${RABBIT_QUEUE_REPLAY_SCRIPT_PATH}" "${STAGED_QUEUE_SCRIPT}"
|
||||
if [[ -n "${ORG_CODE}" ]]; then
|
||||
sed -i.bak "s/AAAABBBBXXX/${ORG_CODE}/g" "${STAGED_QUEUE_SCRIPT}" || true
|
||||
fi
|
||||
bash "${STAGED_QUEUE_SCRIPT}" >> "${LOG_FILE}" 2>&1
|
||||
log "rabbitmq queue script replayed"
|
||||
fi
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/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 $!
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.cisd.tms.modules.backup.config;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.support.BackupResourceKind;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
|
||||
import java.util.List;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "tms.backup")
|
||||
@@ -9,18 +12,24 @@ 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 mqReplayScriptPath = "/home/tms/bin/resource-restore/replay-mq.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;
|
||||
private int authenticitySignKeyIndex = 1;
|
||||
private String restoreApplyScriptPath = "/home/tms/bin/resource-restore/apply-resource-backup.sh";
|
||||
private List<String> allowedRestoreRoots = List.of(
|
||||
"/home/tms/config",
|
||||
"/home/cmep4i",
|
||||
"/home/cmep4d",
|
||||
"/home/tms/tmp/resource-restore"
|
||||
);
|
||||
private List<ConfiguredFileBackupResource> fileResources = List.of();
|
||||
|
||||
public String getOutputDir() {
|
||||
return outputDir;
|
||||
@@ -54,14 +63,6 @@ public class ResourceBackupProperties {
|
||||
this.tmsScriptPath = tmsScriptPath;
|
||||
}
|
||||
|
||||
public String getRunnerLaunchScriptPath() {
|
||||
return runnerLaunchScriptPath;
|
||||
}
|
||||
|
||||
public void setRunnerLaunchScriptPath(String runnerLaunchScriptPath) {
|
||||
this.runnerLaunchScriptPath = runnerLaunchScriptPath;
|
||||
}
|
||||
|
||||
public String getMysqldumpPath() {
|
||||
return mysqldumpPath;
|
||||
}
|
||||
@@ -86,6 +87,14 @@ public class ResourceBackupProperties {
|
||||
this.restoreDbScriptPath = restoreDbScriptPath;
|
||||
}
|
||||
|
||||
public String getMqReplayScriptPath() {
|
||||
return mqReplayScriptPath;
|
||||
}
|
||||
|
||||
public void setMqReplayScriptPath(String mqReplayScriptPath) {
|
||||
this.mqReplayScriptPath = mqReplayScriptPath;
|
||||
}
|
||||
|
||||
public String getTmsDatabaseName() {
|
||||
return tmsDatabaseName;
|
||||
}
|
||||
@@ -118,22 +127,6 @@ public class ResourceBackupProperties {
|
||||
this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds;
|
||||
}
|
||||
|
||||
public boolean getRunnerAutoLaunchEnabled() {
|
||||
return runnerAutoLaunchEnabled;
|
||||
}
|
||||
|
||||
public void setRunnerAutoLaunchEnabled(boolean runnerAutoLaunchEnabled) {
|
||||
this.runnerAutoLaunchEnabled = runnerAutoLaunchEnabled;
|
||||
}
|
||||
|
||||
public int getRunnerHeartbeatTimeoutSeconds() {
|
||||
return runnerHeartbeatTimeoutSeconds;
|
||||
}
|
||||
|
||||
public void setRunnerHeartbeatTimeoutSeconds(int runnerHeartbeatTimeoutSeconds) {
|
||||
this.runnerHeartbeatTimeoutSeconds = runnerHeartbeatTimeoutSeconds;
|
||||
}
|
||||
|
||||
public int getStorageEncKeyIndex() {
|
||||
return storageEncKeyIndex;
|
||||
}
|
||||
@@ -149,4 +142,58 @@ public class ResourceBackupProperties {
|
||||
public void setAuthenticitySignKeyIndex(int authenticitySignKeyIndex) {
|
||||
this.authenticitySignKeyIndex = authenticitySignKeyIndex;
|
||||
}
|
||||
|
||||
public String getRestoreApplyScriptPath() {
|
||||
return restoreApplyScriptPath;
|
||||
}
|
||||
|
||||
public void setRestoreApplyScriptPath(String restoreApplyScriptPath) {
|
||||
this.restoreApplyScriptPath = restoreApplyScriptPath;
|
||||
}
|
||||
|
||||
public List<String> getAllowedRestoreRoots() {
|
||||
return allowedRestoreRoots;
|
||||
}
|
||||
|
||||
public void setAllowedRestoreRoots(List<String> allowedRestoreRoots) {
|
||||
this.allowedRestoreRoots = allowedRestoreRoots;
|
||||
}
|
||||
|
||||
public List<ConfiguredFileBackupResource> getFileResources() {
|
||||
return fileResources;
|
||||
}
|
||||
|
||||
public void setFileResources(List<ConfiguredFileBackupResource> fileResources) {
|
||||
this.fileResources = fileResources;
|
||||
}
|
||||
|
||||
public List<ConfiguredFileBackupResource> getDefaultFileResources() {
|
||||
ConfiguredFileBackupResource tmsConfig = fileResource(
|
||||
"TMS_CONFIG",
|
||||
"DIR",
|
||||
"/home/tms/config",
|
||||
"/home/tms/config",
|
||||
true
|
||||
);
|
||||
ConfiguredFileBackupResource cpConfig = fileResource("CP_CONFIG", "FILE", "", "", true);
|
||||
ConfiguredFileBackupResource receiverLicense = fileResource("RECEIVER_LICENSE", "FILE", "", "", true);
|
||||
return List.of(tmsConfig, cpConfig, receiverLicense);
|
||||
}
|
||||
|
||||
private static ConfiguredFileBackupResource fileResource(
|
||||
String code,
|
||||
String mode,
|
||||
String path,
|
||||
String restorePath,
|
||||
boolean required
|
||||
) {
|
||||
ConfiguredFileBackupResource resource = new ConfiguredFileBackupResource();
|
||||
resource.setCode(code);
|
||||
resource.setKind(BackupResourceKind.FILE.name());
|
||||
resource.setMode(mode);
|
||||
resource.setPath(path);
|
||||
resource.setRestorePath(restorePath);
|
||||
resource.setRequired(required);
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.cisd.tms.common.api.ApiResponse;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourceBackupService;
|
||||
@@ -39,12 +38,6 @@ public class ResourceBackupController {
|
||||
private final ResourceBackupService resourceBackupService;
|
||||
|
||||
|
||||
@GetMapping("/keyset/status")
|
||||
@Operation(summary = "查询资源备份密钥状态", description = "返回资源备份恢复前置密钥状态和公钥指纹。")
|
||||
public ApiResponse<ResourceKeysetStatusResponse> keysetStatus() {
|
||||
return ApiResponse.success(resourceBackupService.getKeysetStatus());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ReplayProtected
|
||||
@Operation(summary = "创建资源备份任务", description = "受理资源备份请求并返回备份任务号。")
|
||||
|
||||
+22
-2
@@ -7,8 +7,8 @@ import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
@@ -75,6 +75,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
return sealedPayload;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to seal resource payload", ex);
|
||||
} finally {
|
||||
wipe(sm4Key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +130,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
decryptRequest.setKeyIndex(resourceBackupProperties.getStorageEncKeyIndex());
|
||||
decryptRequest.setCipherBlob(wrappedDek);
|
||||
decryptRequest.setOutBufferSize(dekLength);
|
||||
byte[] sm4Key = normalize(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest));
|
||||
byte[] sm4Key = normalizeAndWipe(pcieCryptoService.sdfeInternalDecryptEcc(decryptRequest));
|
||||
byte[] iv = Base64.getDecoder().decode(String.valueOf(envelope.get("ivBase64")));
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", PROVIDER);
|
||||
@@ -136,6 +138,8 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
return cipher.doFinal(cipherBytes);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to unseal resource payload", ex);
|
||||
} finally {
|
||||
wipe(sm4Key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +169,16 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static byte[] normalizeAndWipe(BackupDataResult result) {
|
||||
try {
|
||||
return normalize(result);
|
||||
} finally {
|
||||
if (result != null) {
|
||||
wipe(result.getData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseInt(Object value, int defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
@@ -182,4 +196,10 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
|
||||
private static String buildFingerprint(String prefix, int keyIndex) {
|
||||
return prefix + ":" + keyIndex;
|
||||
}
|
||||
|
||||
private static void wipe(byte[] bytes) {
|
||||
if (bytes != null) {
|
||||
Arrays.fill(bytes, (byte) 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-24
@@ -28,24 +28,15 @@ 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));
|
||||
items.add(file("TMS_CONFIG", initExecutorProperties.getTmsConfigDir(), initExecutorProperties.getTmsConfigDir(), "DIR", true));
|
||||
items.add(file("CP_CONFIG", resolveCpConfigPath(productType), resolveCpConfigPath(productType), "FILE", true));
|
||||
items.add(file("RECEIVER_LICENSE", resolveReceiverLicensePath(productType), resolveReceiverLicensePath(productType), "FILE", true));
|
||||
items.add(database("TMS_DB", "TMS", "db/TMS.sql", 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));
|
||||
|
||||
if (mqType.startsWith("RABBITMQ")) {
|
||||
items.add(item("MQ", "RABBITMQ_DEFINITIONS", initExecutorProperties.getRabbitmqAdminCommand(), true));
|
||||
}
|
||||
if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) {
|
||||
items.add(item("FILE", "TLQ_LICENSE", resolveTlqLicensePath(productType), true));
|
||||
}
|
||||
if ("RABBITMQ_CFMQ".equals(mqType)) {
|
||||
items.add(item("FILE", "CFMQ_CONFIG", initExecutorProperties.getCfmqTargetDir(), true));
|
||||
items.add(database("CMEP_DB", "CMEP", "db/CMEP.sql", true));
|
||||
}
|
||||
items.add(mqReplay(true));
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -63,18 +54,33 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
|
||||
return initExecutorProperties.getStandardReceiverLicenseTargetPath();
|
||||
}
|
||||
|
||||
private String resolveTlqLicensePath(String productType) {
|
||||
if ("DIRECT".equals(productType)) {
|
||||
return initExecutorProperties.getDirectTlqLicenseTargetPath();
|
||||
}
|
||||
return initExecutorProperties.getStandardTlqLicenseTargetPath();
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem item(String type, String code, String path, boolean required) {
|
||||
private static DiscoveredResourceItem file(String code, String path, String restorePath, String mode, boolean required) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType(type);
|
||||
item.setResourceType("FILE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRestorePath(restorePath);
|
||||
item.setMode(mode);
|
||||
item.setRequired(required);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem database(String code, String databaseName, String entryPath, boolean required) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("DATABASE");
|
||||
item.setResourceCode(code);
|
||||
item.setDatabaseName(databaseName);
|
||||
item.setResolvedPath(databaseName);
|
||||
item.setEntryPath(entryPath);
|
||||
item.setRequired(required);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem mqReplay(boolean required) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("MQ_REPLAY");
|
||||
item.setResourceCode("MQ_REPLAY");
|
||||
item.setEntryPath("mq/mq-restore-context.json");
|
||||
item.setRequired(required);
|
||||
return item;
|
||||
}
|
||||
|
||||
+5
-5
@@ -21,7 +21,7 @@ public class ResourceRestoreTaskDetailResponse {
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String lastHeartbeatAt;
|
||||
private String runnerStatePath;
|
||||
private String statusFilePath;
|
||||
private Boolean orphaned;
|
||||
private List<String> cleanupWarnings;
|
||||
private List<String> warnings;
|
||||
@@ -155,12 +155,12 @@ public class ResourceRestoreTaskDetailResponse {
|
||||
this.lastHeartbeatAt = lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public String getRunnerStatePath() {
|
||||
return runnerStatePath;
|
||||
public String getStatusFilePath() {
|
||||
return statusFilePath;
|
||||
}
|
||||
|
||||
public void setRunnerStatePath(String runnerStatePath) {
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
public void setStatusFilePath(String statusFilePath) {
|
||||
this.statusFilePath = statusFilePath;
|
||||
}
|
||||
|
||||
public Boolean getOrphaned() {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.cisd.tms.modules.backup.dto.support;
|
||||
|
||||
public enum BackupResourceKind {
|
||||
FILE,
|
||||
DATABASE,
|
||||
MQ_REPLAY
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.cisd.tms.modules.backup.dto.support;
|
||||
|
||||
public class ConfiguredFileBackupResource {
|
||||
|
||||
private String code;
|
||||
private String kind = BackupResourceKind.FILE.name();
|
||||
private String mode = "FILE";
|
||||
private String path;
|
||||
private String restorePath;
|
||||
private boolean required = true;
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getKind() {
|
||||
return kind;
|
||||
}
|
||||
|
||||
public void setKind(String kind) {
|
||||
this.kind = kind;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getRestorePath() {
|
||||
return restorePath;
|
||||
}
|
||||
|
||||
public void setRestorePath(String restorePath) {
|
||||
this.restorePath = restorePath;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public void setRequired(boolean required) {
|
||||
this.required = required;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ public class DiscoveredResourceItem {
|
||||
private String resourceType;
|
||||
private String resourceCode;
|
||||
private String resolvedPath;
|
||||
private String restorePath;
|
||||
private String mode;
|
||||
private String databaseName;
|
||||
private String entryPath;
|
||||
private boolean required;
|
||||
|
||||
public String getResourceType() {
|
||||
@@ -31,6 +35,38 @@ public class DiscoveredResourceItem {
|
||||
this.resolvedPath = resolvedPath;
|
||||
}
|
||||
|
||||
public String getRestorePath() {
|
||||
return restorePath;
|
||||
}
|
||||
|
||||
public void setRestorePath(String restorePath) {
|
||||
this.restorePath = restorePath;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getDatabaseName() {
|
||||
return databaseName;
|
||||
}
|
||||
|
||||
public void setDatabaseName(String databaseName) {
|
||||
this.databaseName = databaseName;
|
||||
}
|
||||
|
||||
public String getEntryPath() {
|
||||
return entryPath;
|
||||
}
|
||||
|
||||
public void setEntryPath(String entryPath) {
|
||||
this.entryPath = entryPath;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ package com.cisd.tms.modules.backup.enums;
|
||||
*/
|
||||
public enum ResourceControlPhase {
|
||||
TMS_PRECHECK,
|
||||
HANDOFF_READY,
|
||||
RUNNER_OWNED,
|
||||
RECONCILE_PENDING,
|
||||
JAVA_EXTRACT,
|
||||
SHELL_APPLY,
|
||||
DONE
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ public enum ResourceTaskStatus {
|
||||
PRECHECKING,
|
||||
PRECHECK_FAILED,
|
||||
READY,
|
||||
HANDOFF,
|
||||
RUNNING,
|
||||
APPLYING,
|
||||
VERIFYING,
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MqReplayContextBuilder {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public MqReplayContextBuilder(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public byte[] build(InitTaskEntity task) {
|
||||
if (task == null || isBlank(task.getInitPlanJson())) {
|
||||
throw new IllegalArgumentException("initPlanJson is empty");
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(task.getInitPlanJson());
|
||||
JsonNode request = root.path("request");
|
||||
JsonNode nodes = request.path("nodes");
|
||||
JsonNode mq = request.path("mq");
|
||||
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("schemaVersion", "1.0");
|
||||
payload.put("productType", firstNonBlank(text(root, "productType"), task.getProductType()));
|
||||
payload.put("mqType", firstNonBlank(text(mq, "mqType"), text(root, "mqType"), task.getMqType()));
|
||||
payload.put("orgCodeType", text(request, "orgCodeType"));
|
||||
payload.put("orgCode", firstNonBlank(text(request, "orgCode"), text(root, "orgCode"), task.getOrgCode()));
|
||||
payload.put("orgNameCn", text(request, "orgNameCn"));
|
||||
payload.put("orgNameEn", text(request, "orgNameEn"));
|
||||
payload.put("deployMode", text(request, "deployMode"));
|
||||
|
||||
Map<String, Object> nodeValues = new LinkedHashMap<>();
|
||||
nodeValues.put("node01Ip", text(nodes, "node01Ip"));
|
||||
nodeValues.put("node02Ip", text(nodes, "node02Ip"));
|
||||
nodeValues.put("node03Ip", text(nodes, "node03Ip"));
|
||||
nodeValues.put("node04Ip", text(nodes, "node04Ip"));
|
||||
payload.put("nodes", nodeValues);
|
||||
|
||||
Map<String, Object> mqValues = new LinkedHashMap<>();
|
||||
mqValues.put("channelUsername", text(mq, "channelUsername"));
|
||||
mqValues.put("channelPassword", text(mq, "channelPassword"));
|
||||
mqValues.put("cipsGatewayHost", text(mq, "cipsGatewayHost"));
|
||||
mqValues.put("cfgZipResourceCode", "");
|
||||
mqValues.put("cfmqConfigResourceCode", "");
|
||||
payload.put("mq", mqValues);
|
||||
|
||||
return objectMapper.writeValueAsBytes(payload);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException("failed to build mq replay context", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(JsonNode node, String field) {
|
||||
JsonNode value = node == null ? null : node.path(field);
|
||||
return value == null || value.isMissingNode() || value.isNull() ? "" : value.asText("");
|
||||
}
|
||||
|
||||
private static String firstNonBlank(String... values) {
|
||||
for (String value : values) {
|
||||
if (!isBlank(value)) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceEnvelopeCryptoService resourceEnvelopeCryptoService;
|
||||
private final DatabaseBackupCollector databaseBackupCollector;
|
||||
private final MqReplayContextBuilder mqReplayContextBuilder;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ResourcePackageServiceImpl(
|
||||
@@ -44,10 +45,21 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
ResourceEnvelopeCryptoService resourceEnvelopeCryptoService,
|
||||
DatabaseBackupCollector databaseBackupCollector,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this(resourceBackupProperties, resourceEnvelopeCryptoService, databaseBackupCollector, new MqReplayContextBuilder(objectMapper), objectMapper);
|
||||
}
|
||||
|
||||
public ResourcePackageServiceImpl(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
ResourceEnvelopeCryptoService resourceEnvelopeCryptoService,
|
||||
DatabaseBackupCollector databaseBackupCollector,
|
||||
MqReplayContextBuilder mqReplayContextBuilder,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.resourceEnvelopeCryptoService = resourceEnvelopeCryptoService;
|
||||
this.databaseBackupCollector = databaseBackupCollector;
|
||||
this.mqReplayContextBuilder = mqReplayContextBuilder;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -225,7 +237,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
resourceEntry.put("resolvedPath", item.getResolvedPath());
|
||||
resourceEntry.put("required", item.isRequired());
|
||||
|
||||
int collectedFiles = collectResourceItem(zipOutputStream, item, resourceEntry);
|
||||
int collectedFiles = collectResourceItem(zipOutputStream, initTask, item, resourceEntry);
|
||||
if (collectedFiles > 0) {
|
||||
collectedCount++;
|
||||
}
|
||||
@@ -243,18 +255,26 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
|
||||
private int collectResourceItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
InitTaskEntity initTask,
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
if ("DB".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
if ("DATABASE".equalsIgnoreCase(trim(item.getResourceType())) || "DB".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
return collectDatabaseItem(zipOutputStream, item, resourceEntry);
|
||||
}
|
||||
if ("MQ_REPLAY".equalsIgnoreCase(trim(item.getResourceType()))) {
|
||||
return collectMqReplayItem(zipOutputStream, initTask, 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;
|
||||
}
|
||||
resourceEntry.put("kind", "FILE");
|
||||
resourceEntry.put("mode", trim(item.getMode()).isEmpty() ? "FILE" : trim(item.getMode()));
|
||||
resourceEntry.put("sourcePath", item.getResolvedPath());
|
||||
resourceEntry.put("restorePath", trim(item.getRestorePath()).isEmpty() ? item.getResolvedPath() : item.getRestorePath());
|
||||
Path resolvedPath = toResolvedPath(item.getResolvedPath());
|
||||
if (resolvedPath == null || !Files.exists(resolvedPath)) {
|
||||
assertOptionalResource(item, "PATH_NOT_FOUND");
|
||||
@@ -328,8 +348,10 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
fileEntry.put("contentSm3", sm3Hex(content));
|
||||
|
||||
resourceEntry.put("collected", Boolean.TRUE);
|
||||
resourceEntry.put("kind", "DATABASE");
|
||||
resourceEntry.put("fileCount", 1);
|
||||
resourceEntry.put("entryPath", entryPath);
|
||||
resourceEntry.put("database", databaseName);
|
||||
resourceEntry.put("databaseName", databaseName);
|
||||
resourceEntry.put("files", List.of(fileEntry));
|
||||
return 1;
|
||||
@@ -341,12 +363,28 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
|
||||
}
|
||||
}
|
||||
|
||||
private int collectMqReplayItem(
|
||||
ZipOutputStream zipOutputStream,
|
||||
InitTaskEntity initTask,
|
||||
DiscoveredResourceItem item,
|
||||
Map<String, Object> resourceEntry
|
||||
) throws IOException {
|
||||
byte[] contextBytes = mqReplayContextBuilder.build(initTask);
|
||||
String entryPath = trim(item.getEntryPath()).isEmpty() ? "mq/mq-restore-context.json" : trim(item.getEntryPath());
|
||||
addZipEntry(zipOutputStream, entryPath, contextBytes);
|
||||
resourceEntry.put("kind", "MQ_REPLAY");
|
||||
resourceEntry.put("collected", Boolean.TRUE);
|
||||
resourceEntry.put("entryPath", entryPath);
|
||||
resourceEntry.put("contentSm3", sm3Hex(contextBytes));
|
||||
return 1;
|
||||
}
|
||||
|
||||
private DatabaseBackupCollector.CollectedDatabaseDump resolveDatabaseDump(DiscoveredResourceItem item) {
|
||||
String resourceCode = trim(item.getResourceCode());
|
||||
if ("TMS_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
if ("TMS_DB".equalsIgnoreCase(resourceCode) || "TMS_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
return databaseBackupCollector.collectTmsDatabaseDump();
|
||||
}
|
||||
if ("CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
if ("CMEP_DB".equalsIgnoreCase(resourceCode) || "CMEP_DB_DUMP".equalsIgnoreCase(resourceCode)) {
|
||||
return databaseBackupCollector.collectCmepDatabaseDump();
|
||||
}
|
||||
return null;
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class ResourcePayloadExtractionResult {
|
||||
|
||||
private Path workDir;
|
||||
private Path payloadDir;
|
||||
private Path manifestPath;
|
||||
private Path restoreContextPath;
|
||||
|
||||
public Path getWorkDir() {
|
||||
return workDir;
|
||||
}
|
||||
|
||||
public void setWorkDir(Path workDir) {
|
||||
this.workDir = workDir;
|
||||
}
|
||||
|
||||
public Path getPayloadDir() {
|
||||
return payloadDir;
|
||||
}
|
||||
|
||||
public void setPayloadDir(Path payloadDir) {
|
||||
this.payloadDir = payloadDir;
|
||||
}
|
||||
|
||||
public Path getManifestPath() {
|
||||
return manifestPath;
|
||||
}
|
||||
|
||||
public void setManifestPath(Path manifestPath) {
|
||||
this.manifestPath = manifestPath;
|
||||
}
|
||||
|
||||
public Path getRestoreContextPath() {
|
||||
return restoreContextPath;
|
||||
}
|
||||
|
||||
public void setRestoreContextPath(Path restoreContextPath) {
|
||||
this.restoreContextPath = restoreContextPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourcePayloadExtractor {
|
||||
|
||||
private final ResourcePackageService resourcePackageService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ResourceBackupProperties properties;
|
||||
|
||||
public ResourcePayloadExtractor(
|
||||
ResourcePackageService resourcePackageService,
|
||||
ObjectMapper objectMapper,
|
||||
ResourceBackupProperties properties
|
||||
) {
|
||||
this.resourcePackageService = resourcePackageService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public ResourcePayloadExtractionResult extract(Path packagePath, Path workDir) {
|
||||
Path normalizedWorkDir = workDir.normalize();
|
||||
Path payloadDir = normalizedWorkDir.resolve("payload").normalize();
|
||||
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackage(packagePath);
|
||||
byte[] payloadArchiveBytes = Optional.ofNullable(inspectResult.getPayloadArchiveBytes())
|
||||
.orElseThrow(() -> new IllegalStateException("payload archive bytes missing"));
|
||||
extractPayloadArchive(payloadArchiveBytes, payloadDir);
|
||||
|
||||
Path manifestPath = payloadDir.resolve("payload-manifest.json").normalize();
|
||||
if (!Files.isRegularFile(manifestPath)) {
|
||||
throw new IllegalStateException("payload manifest missing: " + manifestPath);
|
||||
}
|
||||
Map<String, Object> manifest = readManifest(manifestPath);
|
||||
validateRestoreTargets(payloadDir, manifest);
|
||||
writeShellPlans(normalizedWorkDir, manifest);
|
||||
Path restoreContextPath = writeRestoreContext(packagePath, normalizedWorkDir, payloadDir, manifestPath, manifest);
|
||||
|
||||
ResourcePayloadExtractionResult result = new ResourcePayloadExtractionResult();
|
||||
result.setWorkDir(normalizedWorkDir);
|
||||
result.setPayloadDir(payloadDir);
|
||||
result.setManifestPath(manifestPath);
|
||||
result.setRestoreContextPath(restoreContextPath);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void extractPayloadArchive(byte[] payloadArchiveBytes, Path payloadDir) {
|
||||
try {
|
||||
Files.createDirectories(payloadDir);
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
Path target = resolveSafePayloadTarget(payloadDir, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(target);
|
||||
} else {
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.write(target, readAllBytes(zipInputStream));
|
||||
}
|
||||
zipInputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to extract payload archive", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveSafePayloadTarget(Path payloadDir, String entryName) {
|
||||
Path target = payloadDir.resolve(entryName).normalize();
|
||||
if (!target.startsWith(payloadDir)) {
|
||||
throw new IllegalStateException("payload archive entry escapes payload dir: " + entryName);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private Map<String, Object> readManifest(Path manifestPath) {
|
||||
try {
|
||||
return objectMapper.readValue(manifestPath.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read payload manifest: " + manifestPath, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRestoreTargets(Path payloadDir, Map<String, Object> manifest) {
|
||||
List<Path> allowedRoots = allowedRestoreRoots();
|
||||
Object resourcesValue = manifest.get("resources");
|
||||
if (!(resourcesValue instanceof List<?> resources)) {
|
||||
return;
|
||||
}
|
||||
for (Object resourceObject : resources) {
|
||||
if (!(resourceObject instanceof Map<?, ?> rawResource)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> resource = castMap(rawResource);
|
||||
if (!"FILE".equalsIgnoreCase(kindOf(resource))) {
|
||||
continue;
|
||||
}
|
||||
String resourceRestorePath = firstText(resource, "restorePath", "resolvedPath");
|
||||
validateAllowedRestorePath(resourceRestorePath, allowedRoots);
|
||||
validateFileEntries(payloadDir, resource, resourceRestorePath, allowedRoots);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeShellPlans(Path workDir, Map<String, Object> manifest) {
|
||||
List<String> fileLines = new ArrayList<>();
|
||||
List<String> databaseLines = new ArrayList<>();
|
||||
List<String> mqLines = new ArrayList<>();
|
||||
Object resourcesValue = manifest.get("resources");
|
||||
if (resourcesValue instanceof List<?> resources) {
|
||||
for (Object resourceObject : resources) {
|
||||
if (!(resourceObject instanceof Map<?, ?> rawResource)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> resource = castMap(rawResource);
|
||||
String kind = kindOf(resource);
|
||||
if ("FILE".equalsIgnoreCase(kind)) {
|
||||
appendFilePlanLines(fileLines, resource);
|
||||
} else if ("DATABASE".equalsIgnoreCase(kind) || "DB".equalsIgnoreCase(kind)) {
|
||||
String entryPath = firstText(resource, "entryPath");
|
||||
String databaseName = normalizeDatabaseName(firstText(resource, "databaseName", "database", "resourceCode", "code"));
|
||||
if (!entryPath.isEmpty()) {
|
||||
databaseLines.add(entryPath + "\t" + databaseName);
|
||||
}
|
||||
} else if ("MQ_REPLAY".equalsIgnoreCase(kind)) {
|
||||
String entryPath = firstText(resource, "entryPath");
|
||||
if (!entryPath.isEmpty()) {
|
||||
mqLines.add(entryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
Files.writeString(workDir.resolve("restore-files.tsv"), String.join(System.lineSeparator(), fileLines)
|
||||
+ (fileLines.isEmpty() ? "" : System.lineSeparator()));
|
||||
Files.writeString(workDir.resolve("restore-databases.tsv"), String.join(System.lineSeparator(), databaseLines)
|
||||
+ (databaseLines.isEmpty() ? "" : System.lineSeparator()));
|
||||
Files.writeString(workDir.resolve("restore-mq.tsv"), String.join(System.lineSeparator(), mqLines)
|
||||
+ (mqLines.isEmpty() ? "" : System.lineSeparator()));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write shell restore plans", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendFilePlanLines(List<String> fileLines, Map<String, Object> resource) {
|
||||
String resourceRestorePath = firstText(resource, "restorePath", "resolvedPath");
|
||||
Object filesValue = resource.get("files");
|
||||
if (!(filesValue instanceof List<?> files)) {
|
||||
return;
|
||||
}
|
||||
for (Object fileObject : files) {
|
||||
if (!(fileObject instanceof Map<?, ?> rawFile)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> fileEntry = castMap(rawFile);
|
||||
String entryPath = firstText(fileEntry, "entryPath");
|
||||
if (entryPath.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String restorePath = firstText(fileEntry, "restorePath");
|
||||
if (restorePath.isEmpty()) {
|
||||
restorePath = inferFileRestorePath(resource, resourceRestorePath, fileEntry);
|
||||
}
|
||||
if (!restorePath.isEmpty()) {
|
||||
fileLines.add(entryPath + "\t" + restorePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFileEntries(
|
||||
Path payloadDir,
|
||||
Map<String, Object> resource,
|
||||
String resourceRestorePath,
|
||||
List<Path> allowedRoots
|
||||
) {
|
||||
Object filesValue = resource.get("files");
|
||||
if (!(filesValue instanceof List<?> files)) {
|
||||
return;
|
||||
}
|
||||
for (Object fileObject : files) {
|
||||
if (!(fileObject instanceof Map<?, ?> rawFile)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> fileEntry = castMap(rawFile);
|
||||
validatePayloadEntryPath(payloadDir, firstText(fileEntry, "entryPath"));
|
||||
String fileRestorePath = firstText(fileEntry, "restorePath");
|
||||
if (fileRestorePath.isEmpty()) {
|
||||
fileRestorePath = inferFileRestorePath(resource, resourceRestorePath, fileEntry);
|
||||
}
|
||||
validateAllowedRestorePath(fileRestorePath, allowedRoots);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePayloadEntryPath(Path payloadDir, String entryPath) {
|
||||
if (entryPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Path sourceFile = payloadDir.resolve(entryPath).normalize();
|
||||
if (!sourceFile.startsWith(payloadDir)) {
|
||||
throw new IllegalStateException("payload manifest entry escapes payload dir: " + entryPath);
|
||||
}
|
||||
}
|
||||
|
||||
private String inferFileRestorePath(Map<String, Object> resource, String resourceRestorePath, Map<String, Object> fileEntry) {
|
||||
if (resourceRestorePath.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String relativeName = firstText(fileEntry, "relativeName");
|
||||
String mode = firstText(resource, "mode");
|
||||
if (!relativeName.isEmpty() && "DIR".equalsIgnoreCase(mode)) {
|
||||
return Path.of(resourceRestorePath).resolve(relativeName).normalize().toString();
|
||||
}
|
||||
return resourceRestorePath;
|
||||
}
|
||||
|
||||
private void validateAllowedRestorePath(String restorePath, List<Path> allowedRoots) {
|
||||
if (restorePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Path target = Path.of(restorePath).normalize().toAbsolutePath();
|
||||
boolean allowed = allowedRoots.stream().anyMatch(target::startsWith);
|
||||
if (!allowed) {
|
||||
throw new IllegalStateException("restore path is not allowed: " + restorePath);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Path> allowedRestoreRoots() {
|
||||
List<String> configuredRoots = properties.getAllowedRestoreRoots();
|
||||
if (configuredRoots == null || configuredRoots.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Path> roots = new ArrayList<>();
|
||||
for (String configuredRoot : configuredRoots) {
|
||||
String root = trim(configuredRoot);
|
||||
if (!root.isEmpty()) {
|
||||
roots.add(Path.of(root).normalize().toAbsolutePath());
|
||||
}
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
private Path writeRestoreContext(
|
||||
Path packagePath,
|
||||
Path workDir,
|
||||
Path payloadDir,
|
||||
Path manifestPath,
|
||||
Map<String, Object> manifest
|
||||
) {
|
||||
try {
|
||||
Files.createDirectories(workDir);
|
||||
Path restoreContextPath = workDir.resolve("restore-context.json").normalize();
|
||||
Map<String, Object> context = new LinkedHashMap<>();
|
||||
context.put("schemaVersion", "1.0");
|
||||
context.put("createdAt", Instant.now().toString());
|
||||
context.put("packagePath", packagePath.normalize().toString());
|
||||
context.put("workDir", workDir.toString());
|
||||
context.put("payloadDir", payloadDir.toString());
|
||||
context.put("manifestPath", manifestPath.toString());
|
||||
context.put("payloadManifest", manifest);
|
||||
objectMapper.writerWithDefaultPrettyPrinter().writeValue(restoreContextPath.toFile(), context);
|
||||
return restoreContextPath;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write restore context", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> castMap(Map<?, ?> rawMap) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
rawMap.forEach((key, value) -> result.put(String.valueOf(key), value));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String kindOf(Map<String, Object> resource) {
|
||||
String kind = firstText(resource, "kind");
|
||||
if (!kind.isEmpty()) {
|
||||
return kind;
|
||||
}
|
||||
return firstText(resource, "resourceType");
|
||||
}
|
||||
|
||||
private static String firstText(Map<String, Object> map, String... keys) {
|
||||
for (String key : keys) {
|
||||
Object value = map.get(key);
|
||||
if (value != null) {
|
||||
String text = trim(String.valueOf(value));
|
||||
if (!text.isEmpty() && !"null".equalsIgnoreCase(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String normalizeDatabaseName(String value) {
|
||||
if ("TMS_DB".equalsIgnoreCase(value) || "TMS_DB_DUMP".equalsIgnoreCase(value)) {
|
||||
return "TMS";
|
||||
}
|
||||
if ("CMEP_DB".equalsIgnoreCase(value) || "CMEP_DB_DUMP".equalsIgnoreCase(value)) {
|
||||
return "CMEP";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
|
||||
return inputStream.readAllBytes();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class LightweightRestoreApplierLauncher {
|
||||
|
||||
private final ResourceBackupProperties properties;
|
||||
private final RestoreCommandInvoker invoker;
|
||||
private final String datasourceUrl;
|
||||
private final String datasourceUsername;
|
||||
private final String datasourcePassword;
|
||||
|
||||
public LightweightRestoreApplierLauncher(
|
||||
ResourceBackupProperties properties,
|
||||
RestoreCommandInvoker invoker,
|
||||
@Value("${spring.datasource.url:}") String datasourceUrl,
|
||||
@Value("${spring.datasource.username:}") String datasourceUsername,
|
||||
@Value("${spring.datasource.password:}") String datasourcePassword
|
||||
) {
|
||||
this.properties = properties;
|
||||
this.invoker = invoker;
|
||||
this.datasourceUrl = datasourceUrl;
|
||||
this.datasourceUsername = datasourceUsername;
|
||||
this.datasourcePassword = datasourcePassword;
|
||||
}
|
||||
|
||||
public void launch(Path workDir) {
|
||||
Path normalizedWorkDir = workDir.toAbsolutePath().normalize();
|
||||
writeRestoreEnv(normalizedWorkDir);
|
||||
Path logFile = normalizedWorkDir.resolve("restore-launch.log");
|
||||
List<String> command = List.of(
|
||||
"/bin/bash",
|
||||
properties.getRestoreApplyScriptPath(),
|
||||
"--work-dir",
|
||||
normalizedWorkDir.toString()
|
||||
);
|
||||
int exitCode = invoker.run(command, logFile, normalizedWorkDir);
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("failed to launch lightweight restore apply script, exitCode=" + exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeRestoreEnv(Path workDir) {
|
||||
try {
|
||||
Files.createDirectories(workDir);
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
MysqlTarget target = parseMysqlTarget(datasourceUrl);
|
||||
values.put("TMS_SCRIPT_PATH", trim(properties.getTmsScriptPath()));
|
||||
values.put("DB_RESTORE_SCRIPT_PATH", trim(properties.getRestoreDbScriptPath()));
|
||||
values.put("MQ_REPLAY_SCRIPT_PATH", trim(properties.getMqReplayScriptPath()));
|
||||
values.put("MYSQL_PATH", trim(properties.getMysqlPath()).isEmpty() ? "mysql" : trim(properties.getMysqlPath()));
|
||||
values.put("DB_HOST", target.host());
|
||||
values.put("DB_PORT", target.port());
|
||||
values.put("DB_USER", trim(datasourceUsername));
|
||||
values.put("DB_PASSWORD", trim(datasourcePassword));
|
||||
values.put("HEALTH_CHECK_URL", trim(properties.getHealthCheckUrl()));
|
||||
values.put("HEALTH_CHECK_TIMEOUT_SECONDS", String.valueOf(properties.getHealthCheckTimeoutSeconds()));
|
||||
|
||||
StringBuilder builder = new StringBuilder("# generated by TMS resource restore\n");
|
||||
for (Map.Entry<String, String> entry : values.entrySet()) {
|
||||
builder.append("export ")
|
||||
.append(entry.getKey())
|
||||
.append("=")
|
||||
.append(quoteShell(entry.getValue()))
|
||||
.append(System.lineSeparator());
|
||||
}
|
||||
Files.writeString(workDir.resolve("restore-env.sh"), builder.toString());
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write lightweight restore env", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static MysqlTarget parseMysqlTarget(String datasourceUrl) {
|
||||
String url = trim(datasourceUrl);
|
||||
if (url.startsWith("jdbc:mysql://")) {
|
||||
try {
|
||||
URI uri = URI.create(url.substring("jdbc:".length()).split("\\?", 2)[0]);
|
||||
String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost();
|
||||
int port = uri.getPort() <= 0 ? 3306 : uri.getPort();
|
||||
return new MysqlTarget(host, String.valueOf(port));
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
return new MysqlTarget("", "");
|
||||
}
|
||||
}
|
||||
return new MysqlTarget("", "");
|
||||
}
|
||||
|
||||
private static String quoteShell(String value) {
|
||||
return "'" + trim(value).replace("'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private record MysqlTarget(String host, String port) {
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -7,7 +7,7 @@ import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ProcessBuilderRunnerCommandInvoker implements RunnerCommandInvoker {
|
||||
public class ProcessBuilderRestoreCommandInvoker implements RestoreCommandInvoker {
|
||||
|
||||
@Override
|
||||
public int run(List<String> command, Path logFile, Path workingDirectory) {
|
||||
@@ -27,10 +27,10 @@ public class ProcessBuilderRunnerCommandInvoker implements RunnerCommandInvoker
|
||||
Process process = pb.start();
|
||||
return process.waitFor();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to start runner command: " + String.join(" ", command), ex);
|
||||
throw new IllegalStateException("failed to start restore command: " + String.join(" ", command), ex);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("runner command interrupted: " + String.join(" ", command), ex);
|
||||
throw new IllegalStateException("restore command interrupted: " + String.join(" ", command), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public interface RunnerCommandInvoker {
|
||||
public interface RestoreCommandInvoker {
|
||||
|
||||
int run(List<String> command, Path logFile, Path workingDirectory);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RestoreStatusFileReader {
|
||||
|
||||
private static final int MAX_LOG_BYTES = 64 * 1024;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public RestoreStatusFileReader(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public RestoreStatusSnapshot read(Path workDir) {
|
||||
if (workDir == null) {
|
||||
return empty();
|
||||
}
|
||||
Path statusFile = workDir.resolve("status.json").normalize();
|
||||
Path logFile = workDir.resolve("restore.log").normalize();
|
||||
Map<String, Object> status = readStatus(statusFile);
|
||||
return new RestoreStatusSnapshot(
|
||||
text(status.get("status")),
|
||||
text(status.get("phase")),
|
||||
text(status.get("message")),
|
||||
text(status.get("updatedAt")),
|
||||
readTail(logFile)
|
||||
);
|
||||
}
|
||||
|
||||
private Map<String, Object> readStatus(Path statusFile) {
|
||||
if (!Files.isRegularFile(statusFile)) {
|
||||
return Map.of();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(statusFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read restore status file: " + statusFile, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String readTail(Path logFile) {
|
||||
if (!Files.isRegularFile(logFile)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
byte[] bytes = Files.readAllBytes(logFile);
|
||||
if (bytes.length <= MAX_LOG_BYTES) {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
return new String(bytes, bytes.length - MAX_LOG_BYTES, MAX_LOG_BYTES, StandardCharsets.UTF_8);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read restore log file: " + logFile, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static RestoreStatusSnapshot empty() {
|
||||
return new RestoreStatusSnapshot("", "", "", "", "");
|
||||
}
|
||||
|
||||
private static String text(Object value) {
|
||||
return value == null ? "" : String.valueOf(value);
|
||||
}
|
||||
|
||||
public record RestoreStatusSnapshot(
|
||||
String status,
|
||||
String phase,
|
||||
String message,
|
||||
String updatedAt,
|
||||
String log
|
||||
) {
|
||||
}
|
||||
}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
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.beans.factory.annotation.Autowired;
|
||||
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;
|
||||
|
||||
@Autowired
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
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) {
|
||||
}
|
||||
}
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.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;
|
||||
|
||||
@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;
|
||||
|
||||
public PayloadArchiveRunnerStepExecutor(
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourcePackageService resourcePackageService,
|
||||
ObjectMapper objectMapper
|
||||
) {
|
||||
this.handoffFileStore = handoffFileStore;
|
||||
this.resourcePackageService = resourcePackageService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "RESTORE_FILES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
if ("RESTORE_FILES".equals(context.stepCode())) {
|
||||
return restoreFiles(context);
|
||||
}
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
|
||||
private RunnerStepExecutionResult restoreFiles(RunnerStepContext context) {
|
||||
ResourceRestoreHandoffSnapshot handoff = readRequiredHandoff(context.handoffFile());
|
||||
Path packagePath = resolveRequiredPackagePath(handoff);
|
||||
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackage(packagePath);
|
||||
byte[] payloadArchiveBytes = Optional.ofNullable(inspectResult.getPayloadArchiveBytes())
|
||||
.orElseThrow(() -> new IllegalStateException("payload archive bytes missing"));
|
||||
Path payloadDir = context.taskDir().resolve("staging").resolve("payload").normalize();
|
||||
extractPayloadArchive(payloadArchiveBytes, payloadDir);
|
||||
|
||||
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) {
|
||||
return handoffFileStore.read(handoffFile)
|
||||
.orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile));
|
||||
}
|
||||
|
||||
private Path resolveRequiredPackagePath(ResourceRestoreHandoffSnapshot handoff) {
|
||||
String packagePath = trim(handoff.getPackagePath());
|
||||
if (packagePath.isEmpty()) {
|
||||
throw new IllegalStateException("restore handoff packagePath is empty");
|
||||
}
|
||||
Path resolved = Path.of(packagePath).normalize();
|
||||
if (!Files.isRegularFile(resolved)) {
|
||||
throw new IllegalStateException("restore package file not found: " + resolved);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void extractPayloadArchive(byte[] payloadArchiveBytes, Path payloadDir) {
|
||||
try {
|
||||
Files.createDirectories(payloadDir);
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(payloadArchiveBytes))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zipInputStream.getNextEntry()) != null) {
|
||||
Path target = resolveSafePayloadTarget(payloadDir, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(target);
|
||||
continue;
|
||||
}
|
||||
Files.createDirectories(target.getParent());
|
||||
// 备份包来自外部输入,即使已经验签,也要防御 zip-slip,确保只落到 staging/payload 下。
|
||||
Files.write(target, readAllBytes(zipInputStream));
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to extract payload archive", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveSafePayloadTarget(Path payloadDir, String entryName) {
|
||||
Path target = payloadDir.resolve(entryName).normalize();
|
||||
if (!target.startsWith(payloadDir)) {
|
||||
throw new IllegalStateException("payload archive entry escapes staging dir: " + entryName);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
private int readResourceCount(Path manifestFile) {
|
||||
try {
|
||||
Map<String, Object> manifest = objectMapper.readValue(manifestFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
Object resourcesValue = manifest.get("resources");
|
||||
if (resourcesValue instanceof List<?> resources) {
|
||||
return resources.size();
|
||||
}
|
||||
return 0;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read payload manifest: " + manifestFile, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private 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();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourceRestoreRunner {
|
||||
|
||||
private static final String INITIAL_STEP_CODE = "RESTORE_LOCK";
|
||||
private static final int INITIAL_STEP_NO = 1;
|
||||
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final List<RunnerStepExecutor> runnerStepExecutors;
|
||||
|
||||
public ResourceRestoreRunner(
|
||||
HandoffFileStore handoffFileStore,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ObjectMapper objectMapper,
|
||||
List<RunnerStepExecutor> runnerStepExecutors
|
||||
) {
|
||||
this.handoffFileStore = handoffFileStore;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.objectMapper = objectMapper;
|
||||
this.runnerStepExecutors = runnerStepExecutors;
|
||||
}
|
||||
|
||||
public void run(Path handoffFile) {
|
||||
ResourceRestoreHandoffSnapshot handoff = handoffFileStore.read(handoffFile)
|
||||
.orElseThrow(() -> new IllegalStateException("restore handoff file not found: " + handoffFile));
|
||||
Path stateFile = Path.of(handoff.getRunnerStatePath()).normalize();
|
||||
String runnerInstanceId = nextRunnerInstanceId();
|
||||
long runnerPid = ProcessHandle.current().pid();
|
||||
resourceRestoreStateFileStore.claimOwnership(stateFile, runnerInstanceId, runnerPid, INITIAL_STEP_CODE, INITIAL_STEP_NO);
|
||||
|
||||
List<String> restorePlan = readRestorePlan(handoff.getRestorePlanJson());
|
||||
try {
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
String stepCode = restorePlan.get(i);
|
||||
int stepNo = i + 1;
|
||||
if (INITIAL_STEP_CODE.equals(stepCode)) {
|
||||
continue;
|
||||
}
|
||||
resourceRestoreStateFileStore.markStepRunning(stateFile, stepCode, stepNo, runnerInstanceId);
|
||||
RunnerStepExecutor executor = resolveExecutor(stepCode);
|
||||
RunnerStepContext context = new RunnerStepContext(
|
||||
handoff.getTaskId(),
|
||||
stepCode,
|
||||
stepNo,
|
||||
handoffFile.getParent(),
|
||||
handoffFile,
|
||||
stateFile
|
||||
);
|
||||
RunnerStepExecutionResult result = executor.execute(context);
|
||||
writeStepOutput(context, result);
|
||||
resourceRestoreStateFileStore.touchHeartbeat(stateFile, runnerInstanceId);
|
||||
}
|
||||
String lastStepCode = restorePlan.isEmpty() ? INITIAL_STEP_CODE : restorePlan.get(restorePlan.size() - 1);
|
||||
int lastStepNo = restorePlan.isEmpty() ? INITIAL_STEP_NO : restorePlan.size();
|
||||
resourceRestoreStateFileStore.markReconcilePending(stateFile, "SUCCESS", null, null, lastStepCode, lastStepNo, runnerInstanceId);
|
||||
} catch (RuntimeException ex) {
|
||||
int failedStepNo = detectFailedStepNo(handoff, stateFile);
|
||||
String failedStepCode = detectFailedStepCode(handoff, stateFile);
|
||||
writeStepFailureOutput(handoffFile.getParent(), failedStepCode, failedStepNo, ex);
|
||||
resourceRestoreStateFileStore.markReconcilePending(
|
||||
stateFile,
|
||||
"FAILED",
|
||||
"RUNNER_EXECUTION_FAILED",
|
||||
ex.getMessage(),
|
||||
failedStepCode,
|
||||
failedStepNo,
|
||||
runnerInstanceId
|
||||
);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private String nextRunnerInstanceId() {
|
||||
return "RRUN-" + UUID.randomUUID();
|
||||
}
|
||||
|
||||
private List<String> readRestorePlan(String restorePlanJson) {
|
||||
if (restorePlanJson == null || restorePlanJson.isBlank()) {
|
||||
return List.of(INITIAL_STEP_CODE);
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(restorePlanJson, new TypeReference<List<String>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to parse restore plan json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private RunnerStepExecutor resolveExecutor(String stepCode) {
|
||||
return runnerStepExecutors.stream()
|
||||
.filter(executor -> executor.supports(stepCode))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("no runner step executor for step: " + stepCode));
|
||||
}
|
||||
|
||||
private int detectFailedStepNo(ResourceRestoreHandoffSnapshot handoff, Path stateFile) {
|
||||
return resourceRestoreStateFileStore.read(stateFile)
|
||||
.map(snapshot -> snapshot.getCurrentStepNo() == null ? INITIAL_STEP_NO : snapshot.getCurrentStepNo())
|
||||
.orElse(INITIAL_STEP_NO);
|
||||
}
|
||||
|
||||
private String detectFailedStepCode(ResourceRestoreHandoffSnapshot handoff, Path stateFile) {
|
||||
return resourceRestoreStateFileStore.read(stateFile)
|
||||
.map(snapshot -> snapshot.getCurrentStepCode() == null || snapshot.getCurrentStepCode().isBlank()
|
||||
? INITIAL_STEP_CODE
|
||||
: snapshot.getCurrentStepCode())
|
||||
.orElse(INITIAL_STEP_CODE);
|
||||
}
|
||||
|
||||
private void writeStepOutput(RunnerStepContext context, RunnerStepExecutionResult result) {
|
||||
try {
|
||||
Path outputDir = context.taskDir().resolve("step-output");
|
||||
Files.createDirectories(outputDir);
|
||||
Path outputFile = outputDir.resolve(String.format("%02d-%s.json", context.stepNo(), context.stepCode()));
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("stepCode", context.stepCode());
|
||||
json.put("result", result.getResult());
|
||||
json.put("summary", result.getSummary());
|
||||
objectMapper.writeValue(outputFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write step output for " + context.stepCode(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeStepFailureOutput(Path taskDir, String stepCode, int stepNo, RuntimeException ex) {
|
||||
try {
|
||||
Path outputDir = taskDir.resolve("step-output");
|
||||
Files.createDirectories(outputDir);
|
||||
Path outputFile = outputDir.resolve(String.format("%02d-%s.json", stepNo, stepCode));
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("stepCode", stepCode);
|
||||
json.put("result", "FAILED");
|
||||
json.put("summary", ex.getMessage() == null ? ex.getClass().getSimpleName() : ex.getMessage());
|
||||
objectMapper.writeValue(outputFile.toFile(), json);
|
||||
} catch (IOException ioException) {
|
||||
throw new IllegalStateException("failed to write failed step output for " + stepCode, ioException);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface ResourceRestoreRunnerLauncher {
|
||||
|
||||
void launch(Path handoffFile);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RestoreStateReconciler {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final Supplier<LocalDateTime> nowSupplier;
|
||||
|
||||
@Autowired
|
||||
public RestoreStateReconciler(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore
|
||||
) {
|
||||
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, LocalDateTime::now);
|
||||
}
|
||||
|
||||
RestoreStateReconciler(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
Supplier<LocalDateTime> nowSupplier
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.nowSupplier = nowSupplier;
|
||||
}
|
||||
|
||||
public void reconcileOnce() {
|
||||
for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) {
|
||||
if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
java.nio.file.Path stateFile = java.nio.file.Path.of(entity.getRunnerStatePath()).normalize();
|
||||
resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> {
|
||||
if (!"RECONCILE_PENDING".equals(snapshot.getControlPhase())) {
|
||||
return;
|
||||
}
|
||||
entity.setStatus(snapshot.getStatus());
|
||||
entity.setErrorCode(blankToNull(snapshot.getErrorCode()));
|
||||
entity.setErrorMessage(blankToNull(snapshot.getErrorMessage()));
|
||||
entity.setFinishTime(nowSupplier.get());
|
||||
entity.setUpdateTime(nowSupplier.get());
|
||||
resourceRestoreTaskRepository.update(entity);
|
||||
resourceRestoreStateFileStore.markDone(stateFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static String blankToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.function.Supplier;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RunnerStallDetector {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final Supplier<LocalDateTime> nowSupplier;
|
||||
|
||||
@Autowired
|
||||
public RunnerStallDetector(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties
|
||||
) {
|
||||
this(resourceRestoreTaskRepository, resourceRestoreStateFileStore, resourceBackupProperties, LocalDateTime::now);
|
||||
}
|
||||
|
||||
RunnerStallDetector(
|
||||
ResourceRestoreTaskRepository resourceRestoreTaskRepository,
|
||||
ResourceRestoreStateFileStore resourceRestoreStateFileStore,
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
Supplier<LocalDateTime> nowSupplier
|
||||
) {
|
||||
this.resourceRestoreTaskRepository = resourceRestoreTaskRepository;
|
||||
this.resourceRestoreStateFileStore = resourceRestoreStateFileStore;
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.nowSupplier = nowSupplier;
|
||||
}
|
||||
|
||||
public void detectOnce() {
|
||||
for (ResourceRestoreTaskEntity entity : resourceRestoreTaskRepository.findAll()) {
|
||||
if (entity.getRunnerStatePath() == null || entity.getRunnerStatePath().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
Path stateFile = Path.of(entity.getRunnerStatePath()).normalize();
|
||||
resourceRestoreStateFileStore.read(stateFile).ifPresent(snapshot -> {
|
||||
if (!"RUNNER_OWNED".equals(snapshot.getControlPhase())) {
|
||||
return;
|
||||
}
|
||||
if (snapshot.getLastHeartbeatAt() == null || snapshot.getLastHeartbeatAt().isBlank()) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime heartbeat = LocalDateTime.parse(snapshot.getLastHeartbeatAt());
|
||||
if (Duration.between(heartbeat, nowSupplier.get()).getSeconds() <= resourceBackupProperties.getRunnerHeartbeatTimeoutSeconds()) {
|
||||
return;
|
||||
}
|
||||
resourceRestoreStateFileStore.markStalled(stateFile, "RUNNER_STALLED", "restore runner heartbeat timed out");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record RunnerStepContext(
|
||||
String taskId,
|
||||
String stepCode,
|
||||
int stepNo,
|
||||
Path taskDir,
|
||||
Path handoffFile,
|
||||
Path stateFile
|
||||
) {
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
public class RunnerStepExecutionResult {
|
||||
|
||||
private String result;
|
||||
private String summary;
|
||||
|
||||
public static RunnerStepExecutionResult success(String summary) {
|
||||
RunnerStepExecutionResult result = new RunnerStepExecutionResult();
|
||||
result.setResult("SUCCESS");
|
||||
result.setSummary(summary);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static RunnerStepExecutionResult failed(String summary) {
|
||||
RunnerStepExecutionResult result = new RunnerStepExecutionResult();
|
||||
result.setResult("FAILED");
|
||||
result.setSummary(summary);
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setResult(String result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
public interface RunnerStepExecutor {
|
||||
|
||||
boolean supports(String stepCode);
|
||||
|
||||
RunnerStepExecutionResult execute(RunnerStepContext context);
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ServiceCommandRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final InitExecutorProperties initExecutorProperties;
|
||||
private final RunnerCommandInvoker runnerCommandInvoker;
|
||||
|
||||
public ServiceCommandRunnerStepExecutor(
|
||||
ResourceBackupProperties resourceBackupProperties,
|
||||
InitExecutorProperties initExecutorProperties,
|
||||
RunnerCommandInvoker runnerCommandInvoker
|
||||
) {
|
||||
this.resourceBackupProperties = resourceBackupProperties;
|
||||
this.initExecutorProperties = initExecutorProperties;
|
||||
this.runnerCommandInvoker = runnerCommandInvoker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "STOP_SERVICES".equals(stepCode) || "START_SERVICES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
if (!supports(context.stepCode())) {
|
||||
throw new IllegalArgumentException("unsupported runner step: " + context.stepCode());
|
||||
}
|
||||
List<List<String>> commands = resolveCommands(context.stepCode());
|
||||
int executedCount = 0;
|
||||
for (List<String> command : commands) {
|
||||
int exitCode = runnerCommandInvoker.run(command, resolveLogFile(context), context.taskDir());
|
||||
if (exitCode != 0) {
|
||||
throw new IllegalStateException("runner step " + context.stepCode() + " failed, exitCode=" + exitCode);
|
||||
}
|
||||
executedCount++;
|
||||
}
|
||||
return RunnerStepExecutionResult.success("executed " + executedCount + " command(s)");
|
||||
}
|
||||
|
||||
private List<List<String>> resolveCommands(String stepCode) {
|
||||
return switch (stepCode) {
|
||||
case "STOP_SERVICES" -> buildStopCommands();
|
||||
case "START_SERVICES" -> buildStartCommands();
|
||||
default -> throw new IllegalArgumentException("unsupported runner step: " + stepCode);
|
||||
};
|
||||
}
|
||||
|
||||
private List<List<String>> buildStopCommands() {
|
||||
List<List<String>> commands = new ArrayList<>();
|
||||
commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "stop"));
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardAppStopCommand());
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardRabbitStopCommand());
|
||||
return commands;
|
||||
}
|
||||
|
||||
private List<List<String>> buildStartCommands() {
|
||||
List<List<String>> commands = new ArrayList<>();
|
||||
appendShellCommand(commands, initExecutorProperties.getStandardAppStartCommand());
|
||||
commands.add(List.of("bash", resourceBackupProperties.getTmsScriptPath(), "start"));
|
||||
return commands;
|
||||
}
|
||||
|
||||
private void appendShellCommand(List<List<String>> commands, String shellCommand) {
|
||||
if (shellCommand == null || shellCommand.isBlank()) {
|
||||
return;
|
||||
}
|
||||
commands.add(List.of("/bin/zsh", "-lc", shellCommand.trim()));
|
||||
}
|
||||
|
||||
private Path resolveLogFile(RunnerStepContext context) {
|
||||
return context.taskDir().resolve("steps").resolve(String.format("%02d-%s.log", context.stepNo(), context.stepCode()));
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner.model;
|
||||
|
||||
public class ResourceRestoreHandoffSnapshot {
|
||||
|
||||
private String taskId;
|
||||
private String precheckId;
|
||||
private String backupId;
|
||||
private String packageFileId;
|
||||
private String packagePath;
|
||||
private String stagingDir;
|
||||
private String runnerStatePath;
|
||||
private String restorePlanJson;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getPrecheckId() {
|
||||
return precheckId;
|
||||
}
|
||||
|
||||
public void setPrecheckId(String precheckId) {
|
||||
this.precheckId = precheckId;
|
||||
}
|
||||
|
||||
public String getBackupId() {
|
||||
return backupId;
|
||||
}
|
||||
|
||||
public void setBackupId(String backupId) {
|
||||
this.backupId = backupId;
|
||||
}
|
||||
|
||||
public String getPackageFileId() {
|
||||
return packageFileId;
|
||||
}
|
||||
|
||||
public void setPackageFileId(String packageFileId) {
|
||||
this.packageFileId = packageFileId;
|
||||
}
|
||||
|
||||
public String getPackagePath() {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
public void setPackagePath(String packagePath) {
|
||||
this.packagePath = packagePath;
|
||||
}
|
||||
|
||||
public String getStagingDir() {
|
||||
return stagingDir;
|
||||
}
|
||||
|
||||
public void setStagingDir(String stagingDir) {
|
||||
this.stagingDir = stagingDir;
|
||||
}
|
||||
|
||||
public String getRunnerStatePath() {
|
||||
return runnerStatePath;
|
||||
}
|
||||
|
||||
public void setRunnerStatePath(String runnerStatePath) {
|
||||
this.runnerStatePath = runnerStatePath;
|
||||
}
|
||||
|
||||
public String getRestorePlanJson() {
|
||||
return restorePlanJson;
|
||||
}
|
||||
|
||||
public void setRestorePlanJson(String restorePlanJson) {
|
||||
this.restorePlanJson = restorePlanJson;
|
||||
}
|
||||
}
|
||||
+1
-8
@@ -57,14 +57,7 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
|
||||
response.setPayloadDecrypted(false);
|
||||
response.setWarnings(List.of());
|
||||
response.setBlockingIssues(List.of());
|
||||
response.setRestorePlan(List.of(
|
||||
"RESTORE_LOCK",
|
||||
"STOP_SERVICES",
|
||||
"RESTORE_FILES",
|
||||
"RESTORE_DATABASES",
|
||||
"START_SERVICES",
|
||||
"CONSISTENCY_VERIFY"
|
||||
));
|
||||
response.setRestorePlan(List.of("JAVA_EXTRACT", "SHELL_APPLY"));
|
||||
response.setRestoreScopeSummary(buildScopeSummary(inspectResult));
|
||||
response.setExpiresAt(LocalDateTime.now().plusMinutes(30).toString());
|
||||
resourcePrecheckStateStore.save(new ResourcePrecheckContext(request.getFileId(), response));
|
||||
|
||||
+42
-35
@@ -6,20 +6,21 @@ 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.packagex.ResourcePayloadExtractor;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher;
|
||||
import com.cisd.tms.modules.backup.service.ResourceRestoreService;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
import com.cisd.tms.modules.backup.support.ResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -32,10 +33,9 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
private final FileRecordRepository fileRecordRepository;
|
||||
private final ResourcePrecheckStateStore resourcePrecheckStateStore;
|
||||
private final ResourceTaskIdGenerator resourceTaskIdGenerator;
|
||||
private final HandoffFileStore handoffFileStore;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ResourceBackupProperties resourceBackupProperties;
|
||||
private final ResourceRestoreRunnerLauncher resourceRestoreRunnerLauncher;
|
||||
private final ResourcePayloadExtractor resourcePayloadExtractor;
|
||||
private final LightweightRestoreApplierLauncher lightweightRestoreApplierLauncher;
|
||||
|
||||
|
||||
@Override
|
||||
@@ -47,6 +47,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId())
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found"));
|
||||
ResourceRestorePrecheckResponse precheck = context.getResponse();
|
||||
validatePrecheck(precheck);
|
||||
if (!trim(request.getConfirmBackupId()).equals(trim(precheck.getBackupId()))) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "confirmed backup id does not match precheck result");
|
||||
}
|
||||
@@ -59,36 +60,31 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
|
||||
String taskId = resourceTaskIdGenerator.nextRestoreTaskId();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Path taskDir = Path.of(resourceBackupProperties.getRestoreTaskRootDir()).resolve(taskId).normalize();
|
||||
createDirectories(taskDir);
|
||||
resourcePayloadExtractor.extract(Path.of(packagePath), taskDir);
|
||||
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setPrecheckId(precheck.getPrecheckId());
|
||||
entity.setBackupId(precheck.getBackupId());
|
||||
// 恢复任务创建完成后,就应该已经进入“等待 runner 接管”的交接态,
|
||||
// 前端和运维看到的状态不能还停留在 PRECHECKED。
|
||||
entity.setStatus("HANDOFF");
|
||||
// 轻量恢复模型下,Java 已完成包验签/解密/展开,后续由 shell apply 脚本接管实际覆盖/导入。
|
||||
entity.setStatus("APPLYING");
|
||||
entity.setSourceDeviceId("UNKNOWN");
|
||||
entity.setSourceProductType(precheck.getSourceProductType());
|
||||
entity.setTargetProductType(precheck.getTargetProductType());
|
||||
entity.setPackageFileId(context.getFileId());
|
||||
Path taskDir = Path.of(resourceBackupProperties.getRestoreTaskRootDir()).resolve(taskId).normalize();
|
||||
// 恢复 runner 的工作目录和状态文件路径在任务创建时就固定下来,
|
||||
// 这样后续即使 TMS 重启,控制面路径也不会漂移。
|
||||
// 这里必须把真实备份包路径写进任务和 handoff,后续 runner 才能真正解包,
|
||||
// 否则它只能拿到 fileId,却不知道恢复时去哪里取浏览器上传/下载回来的备份文件。
|
||||
entity.setPackagePath(packagePath);
|
||||
entity.setStagingDir(taskDir.resolve("staging").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("restore-state.json").toString());
|
||||
entity.setStagingDir(taskDir.resolve("payload").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("status.json").toString());
|
||||
entity.setStorageEncKeyFingerprint("USER_KEY_ENC:" + resourceBackupProperties.getStorageEncKeyIndex());
|
||||
entity.setAuthenticityKeyFingerprint("USER_KEY_SIGN:" + resourceBackupProperties.getAuthenticitySignKeyIndex());
|
||||
entity.setRestorePlanJson(joinAsJsonArray(precheck.getRestorePlan()));
|
||||
entity.setRestorePlanJson("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]");
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(now);
|
||||
entity.setUpdateTime(now);
|
||||
resourceRestoreTaskRepository.save(entity);
|
||||
handoffFileStore.write(taskDir, entity);
|
||||
resourceRestoreStateFileStore.initialize(taskDir, entity);
|
||||
resourceRestoreStateFileStore.markHandoffReady(taskDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
resourceRestoreRunnerLauncher.launch(taskDir.resolve("handoff.json"));
|
||||
lightweightRestoreApplierLauncher.launch(taskDir);
|
||||
|
||||
CreateResourceRestoreResponse response = new CreateResourceRestoreResponse();
|
||||
response.setTaskId(taskId);
|
||||
@@ -97,23 +93,34 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String joinAsJsonArray(List<String> items) {
|
||||
if (items == null || items.isEmpty()) {
|
||||
return "[]";
|
||||
private static void createDirectories(Path taskDir) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to create restore task dir: " + taskDir, ex);
|
||||
}
|
||||
// 这里先用最轻量的 JSON 数组序列化,避免为了一个占位字段引入额外 DTO。
|
||||
StringBuilder builder = new StringBuilder("[");
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (i > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append('"').append(items.get(i)).append('"');
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private static void validatePrecheck(ResourceRestorePrecheckResponse precheck) {
|
||||
if (precheck == null) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found");
|
||||
}
|
||||
if (!Boolean.TRUE.equals(precheck.getCompatible())) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck is not compatible");
|
||||
}
|
||||
String expiresAt = trim(precheck.getExpiresAt());
|
||||
if (!expiresAt.isEmpty()) {
|
||||
try {
|
||||
if (LocalDateTime.parse(expiresAt).isBefore(LocalDateTime.now())) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck expired");
|
||||
}
|
||||
} catch (DateTimeException ex) {
|
||||
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck expiresAt is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
-83
@@ -7,17 +7,13 @@ import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader;
|
||||
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader.RestoreStatusSnapshot;
|
||||
import com.cisd.tms.modules.backup.service.ResourceTaskQueryService;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskStepLogService;
|
||||
import com.cisd.tms.modules.backup.support.file.FileSystemResourceTaskStepLogService;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateSnapshot;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -28,9 +24,8 @@ import org.springframework.stereotype.Service;
|
||||
public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
|
||||
private final ResourceRestoreTaskRepository resourceRestoreTaskRepository;
|
||||
private final ResourceRestoreStateFileStore resourceRestoreStateFileStore;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ResourceTaskStepLogService resourceTaskStepLogService;
|
||||
private final RestoreStatusFileReader restoreStatusFileReader;
|
||||
|
||||
@Override
|
||||
public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) {
|
||||
@@ -48,7 +43,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
response.setTargetProductType(entity.getTargetProductType());
|
||||
response.setPackageFileId(entity.getPackageFileId());
|
||||
response.setPackagePathVisible(Boolean.FALSE);
|
||||
response.setRunnerStatePath(entity.getRunnerStatePath());
|
||||
response.setStatusFilePath(entity.getRunnerStatePath());
|
||||
response.setRestorePlan(readStringList(entity.getRestorePlanJson()));
|
||||
response.setConsistencyReport(readObject(entity.getConsistencyReportJson()));
|
||||
response.setErrorCode(entity.getErrorCode());
|
||||
@@ -57,7 +52,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
response.setCreatedAt(entity.getCreateTime() == null ? null : entity.getCreateTime().toString());
|
||||
response.setStartedAt(entity.getStartTime() == null ? null : entity.getStartTime().toString());
|
||||
response.setFinishedAt(entity.getFinishTime() == null ? null : entity.getFinishTime().toString());
|
||||
mergeRunnerState(response, entity);
|
||||
mergeLightweightStatus(response, entity);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -66,8 +61,8 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
ResourceRestoreStateSnapshot snapshot = resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath())).orElse(null);
|
||||
Path taskDir = resolveTaskDir(entity);
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(taskDir);
|
||||
List<ResourceTaskStepItemResponse> responses = new ArrayList<>();
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
String stepCode = restorePlan.get(i);
|
||||
@@ -75,7 +70,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
|
||||
item.setStepNo(stepNo);
|
||||
item.setStepCode(stepCode);
|
||||
applyStepState(item, taskDir, snapshot, stepCode, stepNo);
|
||||
applyLightweightStepState(item, snapshot, stepCode);
|
||||
responses.add(item);
|
||||
}
|
||||
return responses;
|
||||
@@ -87,38 +82,36 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
|
||||
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
|
||||
String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : "";
|
||||
return resourceTaskStepLogService.read(resolveTaskDir(entity), trim(taskId), stepNo, stepCode);
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
|
||||
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
|
||||
response.setTaskId(trim(taskId));
|
||||
response.setStepNo(stepNo);
|
||||
response.setStepCode(stepCode);
|
||||
response.setContent(snapshot.log());
|
||||
response.setTruncated(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private void mergeRunnerState(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) {
|
||||
// 恢复任务详情以数据库记录为基线,再用 runner 状态文件覆盖实时字段。
|
||||
// 这样即使 runner 尚未接管,前端也能看到稳定的任务基本信息。
|
||||
resourceRestoreStateFileStore.read(resolveStatePath(entity.getRunnerStatePath()))
|
||||
.ifPresent(snapshot -> applySnapshot(response, snapshot));
|
||||
private void mergeLightweightStatus(ResourceRestoreTaskDetailResponse response, ResourceRestoreTaskEntity entity) {
|
||||
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
|
||||
if (trim(snapshot.status()).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
response.setStatus(defaultIfBlank(snapshot.status(), response.getStatus()));
|
||||
response.setControlPhase(defaultIfBlank(snapshot.phase(), response.getControlPhase()));
|
||||
response.setStateSource("SHELL_STATUS");
|
||||
response.setCurrentStepCode(defaultIfBlank(snapshot.phase(), response.getCurrentStepCode()));
|
||||
response.setCurrentStepNo(currentStepNo(snapshot.phase(), response.getRestorePlan()));
|
||||
response.setLastHeartbeatAt(defaultIfBlank(snapshot.updatedAt(), response.getLastHeartbeatAt()));
|
||||
response.setErrorMessage(defaultIfBlank(snapshot.message(), response.getErrorMessage()));
|
||||
}
|
||||
|
||||
private void applySnapshot(ResourceRestoreTaskDetailResponse response, ResourceRestoreStateSnapshot snapshot) {
|
||||
response.setStatus(defaultIfBlank(snapshot.getStatus(), response.getStatus()));
|
||||
response.setControlPhase(defaultIfBlank(snapshot.getControlPhase(), response.getControlPhase()));
|
||||
response.setStateSource(defaultIfBlank(snapshot.getStateSource(), response.getStateSource()));
|
||||
response.setCurrentStepCode(defaultIfBlank(snapshot.getCurrentStepCode(), response.getCurrentStepCode()));
|
||||
response.setCurrentStepNo(snapshot.getCurrentStepNo() == null ? response.getCurrentStepNo() : snapshot.getCurrentStepNo());
|
||||
response.setLastHeartbeatAt(defaultIfBlank(snapshot.getLastHeartbeatAt(), response.getLastHeartbeatAt()));
|
||||
response.setOrphaned(snapshot.getOrphaned() == null ? response.getOrphaned() : snapshot.getOrphaned());
|
||||
response.setCleanupWarnings(defaultIfNull(snapshot.getCleanupWarnings(), response.getCleanupWarnings()));
|
||||
response.setWarnings(defaultIfNull(snapshot.getWarnings(), response.getWarnings()));
|
||||
response.setBlockingIssues(defaultIfNull(snapshot.getBlockingIssues(), response.getBlockingIssues()));
|
||||
response.setConsistencyReport(snapshot.getConsistencyReport() == null ? response.getConsistencyReport() : snapshot.getConsistencyReport());
|
||||
response.setErrorCode(defaultIfBlank(snapshot.getErrorCode(), response.getErrorCode()));
|
||||
response.setErrorMessage(defaultIfBlank(snapshot.getErrorMessage(), response.getErrorMessage()));
|
||||
}
|
||||
|
||||
private Path resolveStatePath(String runnerStatePath) {
|
||||
return trim(runnerStatePath).isEmpty() ? null : Path.of(runnerStatePath).normalize();
|
||||
private Path resolveStatePath(String statusFilePath) {
|
||||
return trim(statusFilePath).isEmpty() ? null : Path.of(statusFilePath).normalize();
|
||||
}
|
||||
|
||||
private Path resolveTaskDir(ResourceRestoreTaskEntity entity) {
|
||||
@@ -126,58 +119,29 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
return statePath == null ? null : statePath.getParent();
|
||||
}
|
||||
|
||||
private void applyStepState(
|
||||
ResourceTaskStepItemResponse item,
|
||||
Path taskDir,
|
||||
ResourceRestoreStateSnapshot snapshot,
|
||||
String stepCode,
|
||||
int stepNo
|
||||
) {
|
||||
Map<String, Object> stepOutput = readStepOutput(taskDir, stepNo, stepCode);
|
||||
item.setLogAvailable(taskDir != null && Files.isRegularFile(taskDir.resolve("steps").resolve(String.format("%02d-%s.log", stepNo, stepCode))));
|
||||
if (stepOutput != null) {
|
||||
item.setStatus(stringValue(stepOutput.get("result"), "UNKNOWN"));
|
||||
item.setMessage(stringValue(stepOutput.get("summary"), ""));
|
||||
private void applyLightweightStepState(ResourceTaskStepItemResponse item, RestoreStatusSnapshot snapshot, String stepCode) {
|
||||
item.setLogAvailable(!trim(snapshot.log()).isEmpty());
|
||||
if (trim(snapshot.status()).isEmpty()) {
|
||||
item.setStatus("PENDING");
|
||||
return;
|
||||
}
|
||||
if (snapshot != null && stepNo == safeInt(snapshot.getCurrentStepNo())) {
|
||||
if ("RUNNER_OWNED".equals(snapshot.getControlPhase())) {
|
||||
item.setStatus("RUNNING");
|
||||
} else if ("RECONCILE_PENDING".equals(snapshot.getControlPhase()) && "FAILED".equals(snapshot.getStatus())) {
|
||||
item.setStatus("FAILED");
|
||||
item.setMessage(defaultIfBlank(snapshot.getErrorMessage(), ""));
|
||||
} else {
|
||||
item.setStatus("PENDING");
|
||||
}
|
||||
if ("JAVA_EXTRACT".equals(stepCode)) {
|
||||
item.setStatus("SUCCESS");
|
||||
return;
|
||||
}
|
||||
if ("SHELL_APPLY".equals(stepCode)) {
|
||||
item.setStatus(snapshot.status());
|
||||
item.setMessage(snapshot.message());
|
||||
return;
|
||||
}
|
||||
if (stepCode.equals(snapshot.phase())) {
|
||||
item.setStatus(snapshot.status());
|
||||
item.setMessage(snapshot.message());
|
||||
return;
|
||||
}
|
||||
item.setStatus("PENDING");
|
||||
}
|
||||
|
||||
private Map<String, Object> readStepOutput(Path taskDir, int stepNo, String stepCode) {
|
||||
if (taskDir == null) {
|
||||
return null;
|
||||
}
|
||||
Path outputFile = taskDir.resolve("step-output").resolve(String.format("%02d-%s.json", stepNo, stepCode));
|
||||
if (!Files.isRegularFile(outputFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(outputFile.toFile(), new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to parse resource task step output", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static int safeInt(Integer value) {
|
||||
return value == null ? -1 : value;
|
||||
}
|
||||
|
||||
private static String stringValue(Object value, String defaultValue) {
|
||||
return value == null ? defaultValue : String.valueOf(value);
|
||||
}
|
||||
|
||||
private List<String> readStringList(String json) {
|
||||
if (trim(json).isEmpty()) {
|
||||
return List.of();
|
||||
@@ -205,7 +169,15 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
|
||||
return trim(candidate).isEmpty() ? fallback : candidate;
|
||||
}
|
||||
|
||||
private static <T> T defaultIfNull(T candidate, T fallback) {
|
||||
return candidate == null ? fallback : candidate;
|
||||
private static Integer currentStepNo(String phase, List<String> restorePlan) {
|
||||
if (restorePlan == null || restorePlan.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < restorePlan.size(); i++) {
|
||||
if (restorePlan.get(i).equals(phase)) {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return restorePlan.contains("SHELL_APPLY") ? restorePlan.indexOf("SHELL_APPLY") + 1 : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.runner.model.ResourceRestoreHandoffSnapshot;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class HandoffFileStore {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public HandoffFileStore(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void write(Path taskDir, ResourceRestoreTaskEntity entity) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
Path file = taskDir.resolve("handoff.json");
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("taskId", entity.getTaskId());
|
||||
json.put("precheckId", entity.getPrecheckId());
|
||||
json.put("backupId", entity.getBackupId());
|
||||
json.put("packageFileId", entity.getPackageFileId());
|
||||
json.put("packagePath", entity.getPackagePath());
|
||||
json.put("stagingDir", entity.getStagingDir());
|
||||
json.put("runnerStatePath", entity.getRunnerStatePath());
|
||||
json.put("restorePlanJson", entity.getRestorePlanJson());
|
||||
objectMapper.writeValue(file.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to write restore handoff file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<ResourceRestoreHandoffSnapshot> read(Path handoffFile) {
|
||||
if (handoffFile == null || !Files.isRegularFile(handoffFile)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.ofNullable(objectMapper.readValue(handoffFile.toFile(), ResourceRestoreHandoffSnapshot.class));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read restore handoff file", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ResourceRestoreStateFileStore {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ResourceRestoreStateFileStore(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public void initialize(Path taskDir, ResourceRestoreTaskEntity entity) {
|
||||
try {
|
||||
Files.createDirectories(taskDir);
|
||||
Map<String, Object> json = new LinkedHashMap<>();
|
||||
json.put("taskId", entity.getTaskId());
|
||||
json.put("status", entity.getStatus());
|
||||
json.put("controlPhase", "PRECHECKED");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("runnerInstanceId", "");
|
||||
json.put("runnerPid", 0);
|
||||
json.put("ownershipToken", UUID.randomUUID().toString());
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(taskDir.resolve("restore-state.json").toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to initialize restore state file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public java.util.Optional<ResourceRestoreStateSnapshot> read(Path stateFile) {
|
||||
if (stateFile == null || !Files.isRegularFile(stateFile)) {
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
try {
|
||||
return java.util.Optional.ofNullable(objectMapper.readValue(stateFile.toFile(), ResourceRestoreStateSnapshot.class));
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to read restore state file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markHandoffReady(Path stateFile, String currentStepCode, Integer currentStepNo) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
json.put("status", "HANDOFF");
|
||||
json.put("controlPhase", "HANDOFF_READY");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark restore state as handoff ready", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void claimOwnership(Path stateFile, String runnerInstanceId, long runnerPid, String currentStepCode, Integer currentStepNo) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
String controlPhase = String.valueOf(json.getOrDefault("controlPhase", ""));
|
||||
if (!"HANDOFF_READY".equals(controlPhase)) {
|
||||
throw new IllegalStateException("restore state is not handoff ready: " + controlPhase);
|
||||
}
|
||||
json.put("status", "RUNNING");
|
||||
json.put("controlPhase", "RUNNER_OWNED");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("runnerInstanceId", runnerInstanceId);
|
||||
json.put("runnerPid", runnerPid);
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
json.put("orphaned", false);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to claim restore runner ownership", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markStepRunning(Path stateFile, String currentStepCode, Integer currentStepNo, String runnerInstanceId) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json -> {
|
||||
json.put("status", "RUNNING");
|
||||
json.put("controlPhase", "RUNNER_OWNED");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
}, "failed to mark restore step running");
|
||||
}
|
||||
|
||||
public void touchHeartbeat(Path stateFile, String runnerInstanceId) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json ->
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString()), "failed to touch restore heartbeat");
|
||||
}
|
||||
|
||||
public void markReconcilePending(
|
||||
Path stateFile,
|
||||
String status,
|
||||
String errorCode,
|
||||
String errorMessage,
|
||||
String currentStepCode,
|
||||
Integer currentStepNo,
|
||||
String runnerInstanceId
|
||||
) {
|
||||
updateOwnedState(stateFile, runnerInstanceId, json -> {
|
||||
json.put("status", status);
|
||||
json.put("controlPhase", "RECONCILE_PENDING");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("currentStepCode", currentStepCode);
|
||||
json.put("currentStepNo", currentStepNo);
|
||||
json.put("errorCode", errorCode == null ? "" : errorCode);
|
||||
json.put("errorMessage", errorMessage == null ? "" : errorMessage);
|
||||
json.put("lastHeartbeatAt", java.time.LocalDateTime.now().toString());
|
||||
}, "failed to mark restore reconcile pending");
|
||||
}
|
||||
|
||||
private void updateOwnedState(
|
||||
Path stateFile,
|
||||
String runnerInstanceId,
|
||||
JsonMutator mutator,
|
||||
String errorMessage
|
||||
) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
String currentRunnerInstanceId = String.valueOf(json.getOrDefault("runnerInstanceId", ""));
|
||||
if (!runnerInstanceId.equals(currentRunnerInstanceId)) {
|
||||
throw new IllegalStateException("restore runner ownership changed: " + currentRunnerInstanceId);
|
||||
}
|
||||
mutator.accept(json);
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markDone(Path stateFile) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
json.put("controlPhase", "DONE");
|
||||
json.put("stateSource", "DB");
|
||||
json.put("lastHeartbeatAt", LocalDateTime.now().toString());
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark restore state done", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void markStalled(Path stateFile, String errorCode, String errorMessage) {
|
||||
try {
|
||||
Map<String, Object> json = readJson(stateFile);
|
||||
if (!"RUNNER_OWNED".equals(String.valueOf(json.getOrDefault("controlPhase", "")))) {
|
||||
return;
|
||||
}
|
||||
json.put("status", "FAILED");
|
||||
json.put("controlPhase", "RECONCILE_PENDING");
|
||||
json.put("stateSource", "RUNNER_FILE");
|
||||
json.put("orphaned", true);
|
||||
json.put("errorCode", errorCode);
|
||||
json.put("errorMessage", errorMessage);
|
||||
json.put("lastHeartbeatAt", LocalDateTime.now().toString());
|
||||
objectMapper.writeValue(stateFile.toFile(), json);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("failed to mark stalled restore state", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface JsonMutator {
|
||||
void accept(Map<String, Object> json);
|
||||
}
|
||||
|
||||
private Map<String, Object> readJson(Path stateFile) throws IOException {
|
||||
if (stateFile == null || !Files.isRegularFile(stateFile)) {
|
||||
throw new IllegalStateException("restore state file not found: " + stateFile);
|
||||
}
|
||||
return objectMapper.readValue(
|
||||
stateFile.toFile(),
|
||||
objectMapper.getTypeFactory().constructMapType(LinkedHashMap.class, String.class, Object.class)
|
||||
);
|
||||
}
|
||||
}
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ResourceRestoreStateSnapshot {
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private String controlPhase;
|
||||
private String stateSource;
|
||||
private String runnerInstanceId;
|
||||
private Long runnerPid;
|
||||
private String ownershipToken;
|
||||
private String currentStepCode;
|
||||
private Integer currentStepNo;
|
||||
private String lastHeartbeatAt;
|
||||
private Boolean orphaned;
|
||||
private List<String> cleanupWarnings;
|
||||
private List<String> warnings;
|
||||
private List<String> blockingIssues;
|
||||
private Object consistencyReport;
|
||||
private String errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getControlPhase() {
|
||||
return controlPhase;
|
||||
}
|
||||
|
||||
public void setControlPhase(String controlPhase) {
|
||||
this.controlPhase = controlPhase;
|
||||
}
|
||||
|
||||
public String getStateSource() {
|
||||
return stateSource;
|
||||
}
|
||||
|
||||
public void setStateSource(String stateSource) {
|
||||
this.stateSource = stateSource;
|
||||
}
|
||||
|
||||
public String getRunnerInstanceId() {
|
||||
return runnerInstanceId;
|
||||
}
|
||||
|
||||
public void setRunnerInstanceId(String runnerInstanceId) {
|
||||
this.runnerInstanceId = runnerInstanceId;
|
||||
}
|
||||
|
||||
public Long getRunnerPid() {
|
||||
return runnerPid;
|
||||
}
|
||||
|
||||
public void setRunnerPid(Long runnerPid) {
|
||||
this.runnerPid = runnerPid;
|
||||
}
|
||||
|
||||
public String getOwnershipToken() {
|
||||
return ownershipToken;
|
||||
}
|
||||
|
||||
public void setOwnershipToken(String ownershipToken) {
|
||||
this.ownershipToken = ownershipToken;
|
||||
}
|
||||
|
||||
public String getCurrentStepCode() {
|
||||
return currentStepCode;
|
||||
}
|
||||
|
||||
public void setCurrentStepCode(String currentStepCode) {
|
||||
this.currentStepCode = currentStepCode;
|
||||
}
|
||||
|
||||
public Integer getCurrentStepNo() {
|
||||
return currentStepNo;
|
||||
}
|
||||
|
||||
public void setCurrentStepNo(Integer currentStepNo) {
|
||||
this.currentStepNo = currentStepNo;
|
||||
}
|
||||
|
||||
public String getLastHeartbeatAt() {
|
||||
return lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public void setLastHeartbeatAt(String lastHeartbeatAt) {
|
||||
this.lastHeartbeatAt = lastHeartbeatAt;
|
||||
}
|
||||
|
||||
public Boolean getOrphaned() {
|
||||
return orphaned;
|
||||
}
|
||||
|
||||
public void setOrphaned(Boolean orphaned) {
|
||||
this.orphaned = orphaned;
|
||||
}
|
||||
|
||||
public List<String> getCleanupWarnings() {
|
||||
return cleanupWarnings;
|
||||
}
|
||||
|
||||
public void setCleanupWarnings(List<String> cleanupWarnings) {
|
||||
this.cleanupWarnings = cleanupWarnings;
|
||||
}
|
||||
|
||||
public List<String> getWarnings() {
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public void setWarnings(List<String> warnings) {
|
||||
this.warnings = warnings;
|
||||
}
|
||||
|
||||
public List<String> getBlockingIssues() {
|
||||
return blockingIssues;
|
||||
}
|
||||
|
||||
public void setBlockingIssues(List<String> blockingIssues) {
|
||||
this.blockingIssues = blockingIssues;
|
||||
}
|
||||
|
||||
public Object getConsistencyReport() {
|
||||
return consistencyReport;
|
||||
}
|
||||
|
||||
public void setConsistencyReport(Object consistencyReport) {
|
||||
this.consistencyReport = consistencyReport;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public void setErrorCode(String errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import java.util.List;
|
||||
@Data
|
||||
public class EntityCreateRequest {
|
||||
@NotBlank
|
||||
private String entityName;
|
||||
private String orgCode;
|
||||
@NotBlank
|
||||
private String orgName;
|
||||
@NotBlank
|
||||
private String algoType;
|
||||
private List<String> allowIps;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ import lombok.Data;
|
||||
public class EntityGenP10Request {
|
||||
@NotNull
|
||||
private Long id;
|
||||
@NotNull
|
||||
private String certMode;
|
||||
@Valid
|
||||
@NotNull
|
||||
private Subject subject;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ import java.util.List;
|
||||
@Data
|
||||
public class EntityItemResponse {
|
||||
private Long id;
|
||||
private String entityName;
|
||||
private String orgCode;
|
||||
private String orgName;
|
||||
private String algoType;
|
||||
private Integer keyLen;
|
||||
private Integer keyIdx;
|
||||
private String subject;
|
||||
private List<String> allowIps;
|
||||
private String status;
|
||||
private String statusName;
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode;
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class EntityListRequest extends CommonPageRequest {
|
||||
private String keyword;
|
||||
private String orgCode;
|
||||
private String algoType;
|
||||
private String status;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.util.List;
|
||||
public class EntityUpdateRequest {
|
||||
@NotNull
|
||||
private Long id;
|
||||
private String entityName;
|
||||
private String orgCode;
|
||||
private String orgName;
|
||||
private List<String> allowIps;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import lombok.experimental.Accessors;
|
||||
@TableName("t_key_entity")
|
||||
public class KeyEntity extends BaseEntity {
|
||||
|
||||
private String entityName;
|
||||
private String orgCode;
|
||||
private String orgName;
|
||||
private String algoType;
|
||||
private Integer keyLen;
|
||||
private Integer keyIdx;
|
||||
@@ -20,8 +21,9 @@ public class KeyEntity extends BaseEntity {
|
||||
private String publicKey;
|
||||
private String p10Data;
|
||||
private String subjectDn;
|
||||
private String certMode;
|
||||
private String status;
|
||||
private String allowIps;
|
||||
private String signData;
|
||||
private String createBy;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.cisd.tms.modules.cert.enums;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum EntityCertMode {
|
||||
SINGLE("SINGLE", "单证"),
|
||||
DOUBLE("DOUBLE", "双证");
|
||||
|
||||
private final String code;
|
||||
private final String displayName;
|
||||
|
||||
EntityCertMode(String code, String displayName) {
|
||||
this.code = code;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static EntityCertMode requireValid(String code) {
|
||||
return Arrays.stream(values())
|
||||
.filter(value -> value.code.equalsIgnoreCase(code))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "invalid entity cert mode"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.cisd.tms.modules.cert.enums;
|
||||
|
||||
import com.cisd.tms.common.enums.ErrorCode;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public enum EntityStatus {
|
||||
CREATED("CREATED", "已创建"),
|
||||
P10_GENERATED("P10_GENERATED", "已生成P10请求"),
|
||||
CERT_IMPORTED("CERT_IMPORTED", "已导入证书");
|
||||
|
||||
private final String code;
|
||||
private final String displayName;
|
||||
|
||||
EntityStatus(String code, String displayName) {
|
||||
this.code = code;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static EntityStatus requireValid(String code) {
|
||||
return Arrays.stream(values())
|
||||
.filter(value -> value.code.equalsIgnoreCase(code))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "invalid entity status"));
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,5 @@ public interface KeyEntityRepository {
|
||||
Optional<KeyEntity> findByKeyIdx(Integer keyIdx);
|
||||
List<Integer> findUsedKeyIdx();
|
||||
Page<KeyEntity> page(EntityListRequest request);
|
||||
void updateStatus(Long id, String status);
|
||||
}
|
||||
|
||||
|
||||
+14
-5
@@ -1,6 +1,7 @@
|
||||
package com.cisd.tms.modules.cert.repository.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.cisd.tms.modules.cert.dto.EntityListRequest;
|
||||
import com.cisd.tms.modules.cert.entity.KeyEntity;
|
||||
@@ -62,12 +63,14 @@ public class KeyEntityRepositoryImpl implements KeyEntityRepository {
|
||||
@Override
|
||||
public Page<KeyEntity> page(EntityListRequest request) {
|
||||
LambdaQueryWrapper<KeyEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
if (StringUtils.hasText(request.getKeyword())) {
|
||||
wrapper.and(w -> w.like(KeyEntity::getEntityName, request.getKeyword())
|
||||
.or().like(KeyEntity::getSubjectDn, request.getKeyword()));
|
||||
if (StringUtils.hasText(request.getOrgCode())) {
|
||||
wrapper.like(KeyEntity::getOrgCode, request.getOrgCode());
|
||||
}
|
||||
if (StringUtils.hasText(request.getAlgoType())) {
|
||||
wrapper.eq(KeyEntity::getAlgoType, request.getAlgoType());
|
||||
wrapper.eq(KeyEntity::getAlgoType, request.getAlgoType().toUpperCase());
|
||||
}
|
||||
if (StringUtils.hasText(request.getStatus())) {
|
||||
wrapper.eq(KeyEntity::getStatus, request.getStatus().toUpperCase());
|
||||
}
|
||||
wrapper.orderByDesc(KeyEntity::getCreateTime);
|
||||
long total = mapper.selectCount(wrapper);
|
||||
@@ -78,5 +81,11 @@ public class KeyEntityRepositoryImpl implements KeyEntityRepository {
|
||||
page.setRecords(records);
|
||||
return page;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatus(Long id, String status) {
|
||||
mapper.update(null, new LambdaUpdateWrapper<KeyEntity>()
|
||||
.eq(KeyEntity::getId, id)
|
||||
.set(KeyEntity::getStatus, status));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ public class CertificateService {
|
||||
entity.setCreateTime(LocalDateTime.now());
|
||||
entity.setUpdateTime(LocalDateTime.now());
|
||||
certificateRepository.save(entity);
|
||||
if (finalEntityId != null) {
|
||||
entityService.markCertificateImported(finalEntityId);
|
||||
}
|
||||
return new ImportCertificateResponse(entity.getId(), entity.getSubjectDn(), entity.getValidTo());
|
||||
} catch (BizException ex) {
|
||||
throw ex;
|
||||
|
||||
@@ -16,6 +16,8 @@ import com.cisd.tms.modules.cert.dto.EntityListRequest;
|
||||
import com.cisd.tms.modules.cert.dto.EntityUpdateRequest;
|
||||
import com.cisd.tms.modules.cert.dto.Subject;
|
||||
import com.cisd.tms.modules.cert.entity.KeyEntity;
|
||||
import com.cisd.tms.modules.cert.enums.EntityCertMode;
|
||||
import com.cisd.tms.modules.cert.enums.EntityStatus;
|
||||
import com.cisd.tms.modules.cert.repository.CertificateRepository;
|
||||
import com.cisd.tms.modules.cert.repository.KeyEntityRepository;
|
||||
import com.cisd.tms.modules.cert.support.Sm2KeySupport;
|
||||
@@ -39,11 +41,13 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@Service
|
||||
public class EntityService {
|
||||
|
||||
private static final String SM2 = "SM2";
|
||||
private static final String RSA = "RSA";
|
||||
private static final byte[] DEFAULT_SM2_USER_ID = "1234567812345678".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private final KeyEntityRepository keyEntityRepository;
|
||||
@@ -72,26 +76,33 @@ public class EntityService {
|
||||
|
||||
@Transactional
|
||||
public EntityCreateResponse create(EntityCreateRequest request) {
|
||||
if (!SM2.equalsIgnoreCase(request.getAlgoType())) {
|
||||
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "only SM2 is supported");
|
||||
}
|
||||
String algoType = normalizeAlgoType(request.getAlgoType());
|
||||
int keyIdx = resolveNextKeyIdx();
|
||||
int keyLen = SM2.equals(algoType) ? 256 : 2048;
|
||||
GenerateKeyPairRequest generateRequest = new GenerateKeyPairRequest();
|
||||
generateRequest.setKeyBits(256);
|
||||
generateRequest.setKeyBits(keyLen);
|
||||
generateRequest.setKeyIndex(keyIdx);
|
||||
generateRequest.setKeyType(PcieUserKeyType.SIGN.getCode());
|
||||
|
||||
pcieCryptoService.generateKeyPairEcc(generateRequest);
|
||||
if (SM2.equals(algoType)) {
|
||||
pcieCryptoService.generateKeyPairEcc(generateRequest);
|
||||
} else {
|
||||
pcieCryptoService.generateKeyPairRsa(generateRequest);
|
||||
}
|
||||
try {
|
||||
BackupDataResult backup = pcieCryptoService.backupUserKey(keyIdx, PcieUserKeyType.SIGN.getCode(), 4096);
|
||||
BackupDataResult publicKey = pcieCryptoService.exportSignPublicKeyEcc(keyIdx);
|
||||
BackupDataResult publicKey = SM2.equals(algoType)
|
||||
? pcieCryptoService.exportSignPublicKeyEcc(keyIdx)
|
||||
: pcieCryptoService.exportSignPublicKeyRsa(keyIdx);
|
||||
KeyEntity entity = new KeyEntity()
|
||||
.setEntityName(request.getEntityName())
|
||||
.setAlgoType(SM2)
|
||||
.setKeyLen(256)
|
||||
.setOrgCode(request.getOrgCode())
|
||||
.setOrgName(request.getOrgName())
|
||||
.setAlgoType(algoType)
|
||||
.setKeyLen(keyLen)
|
||||
.setKeyIdx(keyIdx)
|
||||
.setBackupData(Base64.getEncoder().encodeToString(backup.getData()))
|
||||
.setPublicKey(Base64.getEncoder().encodeToString(publicKey.getData()))
|
||||
.setStatus(EntityStatus.CREATED.getCode())
|
||||
.setAllowIps(writeAllowIps(request.getAllowIps()))
|
||||
.setCreateBy("system");
|
||||
entity.setCreateTime(LocalDateTime.now());
|
||||
@@ -110,8 +121,11 @@ public class EntityService {
|
||||
@Transactional
|
||||
public void update(EntityUpdateRequest request) {
|
||||
KeyEntity entity = getById(request.getId());
|
||||
if (StringUtils.hasText(request.getEntityName())) {
|
||||
entity.setEntityName(request.getEntityName());
|
||||
if (StringUtils.hasText(request.getOrgCode())) {
|
||||
entity.setOrgCode(request.getOrgCode());
|
||||
}
|
||||
if (StringUtils.hasText(request.getOrgName())) {
|
||||
entity.setOrgName(request.getOrgName());
|
||||
}
|
||||
if (request.getAllowIps() != null) {
|
||||
entity.setAllowIps(writeAllowIps(request.getAllowIps()));
|
||||
@@ -132,6 +146,10 @@ public class EntityService {
|
||||
@Transactional
|
||||
public String generateP10(EntityGenP10Request request) {
|
||||
KeyEntity entity = getById(request.getId());
|
||||
if (!SM2.equalsIgnoreCase(entity.getAlgoType())) {
|
||||
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "only SM2 P10 is supported");
|
||||
}
|
||||
EntityCertMode certMode = EntityCertMode.requireValid(request.getCertMode());
|
||||
Subject subject = request.getSubject();
|
||||
X500Name subjectName = subject.toX500Name();
|
||||
String subjectDn = subjectName.toString();
|
||||
@@ -142,6 +160,8 @@ public class EntityService {
|
||||
String pem = encodePkcs10Pem(csr);
|
||||
entity.setSubjectDn(subjectDn);
|
||||
entity.setP10Data(pem);
|
||||
entity.setCertMode(certMode.getCode());
|
||||
entity.setStatus(EntityStatus.P10_GENERATED.getCode());
|
||||
keyEntityRepository.save(entity);
|
||||
return pem;
|
||||
}
|
||||
@@ -156,19 +176,40 @@ public class EntityService {
|
||||
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "entity not found"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markCertificateImported(Long id) {
|
||||
keyEntityRepository.updateStatus(id, EntityStatus.CERT_IMPORTED.getCode());
|
||||
}
|
||||
|
||||
private EntityItemResponse toItem(KeyEntity entity) {
|
||||
EntityItemResponse response = new EntityItemResponse();
|
||||
response.setId(entity.getId());
|
||||
response.setEntityName(entity.getEntityName());
|
||||
response.setOrgCode(entity.getOrgCode());
|
||||
response.setOrgName(entity.getOrgName());
|
||||
response.setAlgoType(entity.getAlgoType());
|
||||
response.setKeyLen(entity.getKeyLen());
|
||||
response.setKeyIdx(entity.getKeyIdx());
|
||||
response.setSubject(entity.getSubjectDn());
|
||||
response.setAllowIps(readAllowIps(entity.getAllowIps()));
|
||||
response.setStatus(entity.getStatus());
|
||||
if (StringUtils.hasText(entity.getStatus())) {
|
||||
response.setStatusName(EntityStatus.requireValid(entity.getStatus()).getDisplayName());
|
||||
}
|
||||
response.setCreateTime(entity.getCreateTime());
|
||||
return response;
|
||||
}
|
||||
|
||||
private String normalizeAlgoType(String algoType) {
|
||||
if (!StringUtils.hasText(algoType)) {
|
||||
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "algoType is required");
|
||||
}
|
||||
String normalized = algoType.toUpperCase(Locale.ROOT);
|
||||
if (!SM2.equals(normalized) && !RSA.equals(normalized)) {
|
||||
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "algoType must be SM2 or RSA");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private int resolveNextKeyIdx() {
|
||||
List<Integer> used = keyEntityRepository.findUsedKeyIdx();
|
||||
for (int idx = 2; idx <= 8; idx++) {
|
||||
|
||||
@@ -184,16 +184,24 @@ tms:
|
||||
backup:
|
||||
# 资源备份包输出目录;第一版先在本机生成可见的 .tmsbak 文件。
|
||||
output-dir: ${TMS_BACKUP_OUTPUT_DIR:/home/tms/tmp/resource-backup-packages}
|
||||
# 恢复 runner 启停 TMS 时使用的脚本路径。
|
||||
# 轻量恢复 apply 脚本启停 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}
|
||||
# 轻量恢复 apply 脚本路径;Java 完成验签/解密/展开后调用该脚本执行覆盖、SQL、MQ replay。
|
||||
restore-apply-script-path: ${TMS_BACKUP_RESTORE_APPLY_SCRIPT_PATH:/home/tms/bin/resource-restore/apply-resource-backup.sh}
|
||||
# 文件恢复白名单根路径;manifest 中 FILE 恢复路径必须落在这些目录下。
|
||||
allowed-restore-roots:
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_TMS_CONFIG:/home/tms/config}
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4I:/home/cmep4i}
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_CMEP4D:/home/cmep4d}
|
||||
- ${TMS_BACKUP_ALLOWED_RESTORE_ROOT_WORK:/home/tms/tmp/resource-restore}
|
||||
# 导出 TMS 库时使用的 mysqldump 命令;现场可按实际安装路径覆盖。
|
||||
mysqldump-path: ${TMS_BACKUP_MYSQLDUMP_PATH:mysqldump}
|
||||
# 恢复 TMS 库时使用的 mysql 客户端命令。
|
||||
mysql-path: ${TMS_BACKUP_MYSQL_PATH:mysql}
|
||||
# 数据库恢复脚本路径;runner 在 RESTORE_DATABASES 阶段调用。
|
||||
# 数据库恢复脚本路径;apply 脚本在 RESTORE_DATABASES 阶段调用。
|
||||
restore-db-script-path: ${TMS_BACKUP_RESTORE_DB_SCRIPT_PATH:/home/tms/bin/resource-restore/restore-db.sh}
|
||||
# MQ replay 脚本路径;apply 脚本在 MQ_REPLAY 阶段调用。
|
||||
mq-replay-script-path: ${TMS_BACKUP_MQ_REPLAY_SCRIPT_PATH:/home/tms/bin/resource-restore/replay-mq.sh}
|
||||
# 备份 TMS 库名;为空时默认从 spring.datasource.url 中解析。
|
||||
tms-database-name: ${TMS_BACKUP_TMS_DATABASE_NAME:}
|
||||
# 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。
|
||||
@@ -202,10 +210,6 @@ tms:
|
||||
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 心跳失联超时(秒)。
|
||||
runner-heartbeat-timeout-seconds: ${TMS_BACKUP_RUNNER_HEARTBEAT_TIMEOUT_SECONDS:300}
|
||||
mk:
|
||||
init-identify:
|
||||
# SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。
|
||||
@@ -217,7 +221,7 @@ tms:
|
||||
internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token}
|
||||
internal-auth:
|
||||
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
|
||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false}
|
||||
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:true}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试角色。
|
||||
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:SUPER_ADMIN}
|
||||
# 关闭内部鉴权后,注入请求上下文的调试认证等级。
|
||||
|
||||
@@ -47,16 +47,16 @@ class ResourceBackupDtoContractTest {
|
||||
void shouldSerializeRestoreTaskDetailControlFields() throws Exception {
|
||||
ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse();
|
||||
response.setTaskId("RRST-TASK-20260408-000001");
|
||||
response.setStatus("RUNNING");
|
||||
response.setControlPhase("RUNNER_OWNED");
|
||||
response.setStateSource("RUNNER_FILE");
|
||||
response.setCurrentStepCode("RESTORE_DATABASES");
|
||||
response.setStatus("APPLYING");
|
||||
response.setControlPhase("SHELL_APPLY");
|
||||
response.setStateSource("SHELL_STATUS");
|
||||
response.setCurrentStepCode("SHELL_APPLY");
|
||||
|
||||
String json = objectMapper.writeValueAsString(response);
|
||||
|
||||
Assertions.assertTrue(json.contains("\"taskId\":\"RRST-TASK-20260408-000001\""));
|
||||
Assertions.assertTrue(json.contains("\"controlPhase\":\"RUNNER_OWNED\""));
|
||||
Assertions.assertTrue(json.contains("\"stateSource\":\"RUNNER_FILE\""));
|
||||
Assertions.assertTrue(json.contains("\"currentStepCode\":\"RESTORE_DATABASES\""));
|
||||
Assertions.assertTrue(json.contains("\"controlPhase\":\"SHELL_APPLY\""));
|
||||
Assertions.assertTrue(json.contains("\"stateSource\":\"SHELL_STATUS\""));
|
||||
Assertions.assertTrue(json.contains("\"currentStepCode\":\"SHELL_APPLY\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.cisd.tms.modules.backup;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.support.BackupResourceKind;
|
||||
import com.cisd.tms.modules.backup.dto.support.ConfiguredFileBackupResource;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ResourceBackupPropertiesTest {
|
||||
|
||||
@Test
|
||||
void shouldExposeDefaultLightweightRestoreSettings() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
|
||||
Assertions.assertEquals(
|
||||
"/home/tms/bin/resource-restore/apply-resource-backup.sh",
|
||||
properties.getRestoreApplyScriptPath()
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
"/home/tms/bin/resource-restore/replay-mq.sh",
|
||||
properties.getMqReplayScriptPath()
|
||||
);
|
||||
Assertions.assertTrue(properties.getAllowedRestoreRoots().contains("/home/tms/config"));
|
||||
Assertions.assertTrue(properties.getDefaultFileResources().stream()
|
||||
.anyMatch(item -> "TMS_CONFIG".equals(item.getCode())
|
||||
&& BackupResourceKind.FILE.name().equals(item.getKind())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptAdditionalFileResources() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
ConfiguredFileBackupResource custom = new ConfiguredFileBackupResource();
|
||||
custom.setCode("CUSTOM_CERT");
|
||||
custom.setKind(BackupResourceKind.FILE.name());
|
||||
custom.setMode("FILE");
|
||||
custom.setPath("/home/tms/certs/custom.crt");
|
||||
custom.setRestorePath("/home/tms/certs/custom.crt");
|
||||
custom.setRequired(false);
|
||||
|
||||
properties.setFileResources(java.util.List.of(custom));
|
||||
|
||||
Assertions.assertEquals("CUSTOM_CERT", properties.getFileResources().get(0).getCode());
|
||||
Assertions.assertFalse(properties.getFileResources().get(0).isRequired());
|
||||
}
|
||||
}
|
||||
+5
-17
@@ -5,7 +5,6 @@ import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceBackupResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceBackupTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceKeysetStatusResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
import com.cisd.tms.modules.backup.service.ResourceBackupService;
|
||||
@@ -40,12 +39,6 @@ class ResourceBackupControllerTest {
|
||||
ResourceRestoreService restoreService = Mockito.mock(ResourceRestoreService.class);
|
||||
ResourceTaskQueryService taskQueryService = Mockito.mock(ResourceTaskQueryService.class);
|
||||
|
||||
ResourceKeysetStatusResponse keysetStatus = new ResourceKeysetStatusResponse();
|
||||
keysetStatus.setLmkReady(true);
|
||||
keysetStatus.setStorageEncKeyReady(true);
|
||||
keysetStatus.setStorageEncKeyFingerprint("SM3(KEYSET)");
|
||||
Mockito.when(backupService.getKeysetStatus()).thenReturn(keysetStatus);
|
||||
|
||||
CreateResourceBackupResponse createBackup = new CreateResourceBackupResponse();
|
||||
createBackup.setTaskId("RBKP-TASK-20260408-000001");
|
||||
createBackup.setStatus("PENDING");
|
||||
@@ -79,9 +72,9 @@ class ResourceBackupControllerTest {
|
||||
|
||||
ResourceRestoreTaskDetailResponse restoreDetail = new ResourceRestoreTaskDetailResponse();
|
||||
restoreDetail.setTaskId("RRST-TASK-20260408-000001");
|
||||
restoreDetail.setStatus("RUNNING");
|
||||
restoreDetail.setControlPhase("RUNNER_OWNED");
|
||||
restoreDetail.setStateSource("RUNNER_FILE");
|
||||
restoreDetail.setStatus("APPLYING");
|
||||
restoreDetail.setControlPhase("SHELL_APPLY");
|
||||
restoreDetail.setStateSource("SHELL_STATUS");
|
||||
Mockito.when(taskQueryService.getRestoreTask("RRST-TASK-20260408-000001")).thenReturn(restoreDetail);
|
||||
|
||||
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||
@@ -89,11 +82,6 @@ class ResourceBackupControllerTest {
|
||||
new ResourceRestoreController(precheckService, restoreService, taskQueryService)
|
||||
).build();
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-backups/keyset/status"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"lmkReady\":true")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"storageEncKeyFingerprint\":\"SM3(KEYSET)\"")));
|
||||
|
||||
mockMvc.perform(post("/api/v1/resource-backups")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"includeUpgradeHistory\":true,\"includeAuditBackups\":false,\"remark\":\"before hardware replacement\"}"))
|
||||
@@ -124,7 +112,7 @@ class ResourceBackupControllerTest {
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260408-000001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"RUNNER_OWNED\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"stateSource\":\"RUNNER_FILE\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"SHELL_APPLY\"")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"stateSource\":\"SHELL_STATUS\"")));
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -37,26 +37,26 @@ class ResourceRestoreControllerTest {
|
||||
|
||||
CreateResourceRestoreResponse create = new CreateResourceRestoreResponse();
|
||||
create.setTaskId("RRST-TASK-20260414-000001");
|
||||
create.setStatus("HANDOFF");
|
||||
create.setStatus("APPLYING");
|
||||
Mockito.when(restoreService.createRestoreTask(Mockito.any(CreateResourceRestoreRequest.class))).thenReturn(create);
|
||||
|
||||
ResourceRestoreTaskDetailResponse detail = new ResourceRestoreTaskDetailResponse();
|
||||
detail.setTaskId("RRST-TASK-20260414-000001");
|
||||
detail.setControlPhase("RECONCILE_PENDING");
|
||||
detail.setStateSource("RUNNER_FILE");
|
||||
detail.setControlPhase("SHELL_APPLY");
|
||||
detail.setStateSource("SHELL_STATUS");
|
||||
Mockito.when(taskQueryService.getRestoreTask("RRST-TASK-20260414-000001")).thenReturn(detail);
|
||||
|
||||
ResourceTaskStepItemResponse step = new ResourceTaskStepItemResponse();
|
||||
step.setStepNo(2);
|
||||
step.setStepCode("STOP_SERVICES");
|
||||
step.setStepCode("SHELL_APPLY");
|
||||
step.setStatus("SUCCESS");
|
||||
Mockito.when(taskQueryService.getRestoreTaskSteps("RRST-TASK-20260414-000001")).thenReturn(List.of(step));
|
||||
|
||||
ResourceTaskStepLogResponse log = new ResourceTaskStepLogResponse();
|
||||
log.setTaskId("RRST-TASK-20260414-000001");
|
||||
log.setStepNo(2);
|
||||
log.setStepCode("STOP_SERVICES");
|
||||
log.setContent("stop ok");
|
||||
log.setStepCode("SHELL_APPLY");
|
||||
log.setContent("apply ok");
|
||||
log.setTruncated(false);
|
||||
Mockito.when(taskQueryService.getRestoreTaskStepLog("RRST-TASK-20260414-000001", 2)).thenReturn(log);
|
||||
|
||||
@@ -78,14 +78,14 @@ class ResourceRestoreControllerTest {
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"RECONCILE_PENDING\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"controlPhase\":\"SHELL_APPLY\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001/steps"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"stepCode\":\"STOP_SERVICES\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"stepCode\":\"SHELL_APPLY\"")));
|
||||
|
||||
mockMvc.perform(get("/api/v1/resource-restores/RRST-TASK-20260414-000001/steps/2/log"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"content\":\"stop ok\"")));
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"content\":\"apply ok\"")));
|
||||
}
|
||||
}
|
||||
|
||||
+23
-1
@@ -7,6 +7,7 @@ import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -23,16 +24,23 @@ class ResourceEnvelopeCryptoServiceTest {
|
||||
properties.setAuthenticitySignKeyIndex(8);
|
||||
|
||||
AtomicReference<byte[]> capturedDek = new AtomicReference<>();
|
||||
AtomicReference<byte[]> decryptableDek = new AtomicReference<>();
|
||||
AtomicReference<byte[]> decryptResultDek = new AtomicReference<>();
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
capturedDek.set(request.getData());
|
||||
decryptableDek.set(Arrays.copyOf(request.getData(), request.getData().length));
|
||||
return backup(new byte[]{0x11, 0x22, 0x33});
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any(EccInternalSignRequest.class)))
|
||||
.thenReturn(backup(new byte[]{0x44, 0x55, 0x66}));
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> backup(capturedDek.get()));
|
||||
.thenAnswer(invocation -> {
|
||||
byte[] decryptedDek = Arrays.copyOf(decryptableDek.get(), decryptableDek.get().length);
|
||||
decryptResultDek.set(decryptedDek);
|
||||
return backup(decryptedDek);
|
||||
});
|
||||
|
||||
ResourceEnvelopeCryptoServiceImpl service = new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, properties);
|
||||
byte[] payload = "payload-data".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
@@ -60,6 +68,8 @@ class ResourceEnvelopeCryptoServiceTest {
|
||||
ArgumentCaptor<EccInternalDecryptRequest> decryptCaptor = ArgumentCaptor.forClass(EccInternalDecryptRequest.class);
|
||||
Mockito.verify(pcieCryptoService).sdfeInternalDecryptEcc(decryptCaptor.capture());
|
||||
Assertions.assertEquals(7, decryptCaptor.getValue().getKeyIndex());
|
||||
Assertions.assertTrue(isAllZero(capturedDek.get()), "generated SM4 DEK should be wiped after sealing");
|
||||
Assertions.assertTrue(isAllZero(decryptResultDek.get()), "decrypted SM4 DEK result buffer should be wiped after unsealing");
|
||||
}
|
||||
|
||||
private static BackupDataResult backup(byte[] data) {
|
||||
@@ -68,4 +78,16 @@ class ResourceEnvelopeCryptoServiceTest {
|
||||
result.setLength(data.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isAllZero(byte[] bytes) {
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
return false;
|
||||
}
|
||||
for (byte value : bytes) {
|
||||
if (value != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+15
-7
@@ -16,11 +16,18 @@ 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, "TMS_DB"));
|
||||
Assertions.assertTrue(containsCode(items, "CMEP_DB"));
|
||||
Assertions.assertTrue(containsCode(items, "CP_CONFIG"));
|
||||
Assertions.assertTrue(containsCode(items, "RECEIVER_LICENSE"));
|
||||
Assertions.assertTrue(containsCode(items, "RABBITMQ_DEFINITIONS"));
|
||||
Assertions.assertTrue(containsCode(items, "MQ_REPLAY"));
|
||||
Assertions.assertFalse(containsCode(items, "RABBITMQ_DEFINITIONS"));
|
||||
Assertions.assertFalse(containsCode(items, "TLQ_LICENSE"));
|
||||
Assertions.assertFalse(containsCode(items, "CFMQ_CONFIG"));
|
||||
Assertions.assertTrue(items.stream().allMatch(item ->
|
||||
"FILE".equals(item.getResourceType())
|
||||
|| "DATABASE".equals(item.getResourceType())
|
||||
|| "MQ_REPLAY".equals(item.getResourceType())));
|
||||
Assertions.assertFalse(containsCode(items, "NGINX_CONFIG"));
|
||||
Assertions.assertFalse(containsCode(items, "WEB_STATIC"));
|
||||
Assertions.assertFalse(containsCode(items, "ORGANIZATION_JSON"));
|
||||
@@ -35,10 +42,11 @@ class ResourceDiscoveryServiceTest {
|
||||
|
||||
Assertions.assertTrue(hasPath(items, "CP_CONFIG", "/opt/cpconfig.cfg"));
|
||||
Assertions.assertTrue(hasPath(items, "RECEIVER_LICENSE", "/home/cmep4d/cmep/license/cmep.license"));
|
||||
Assertions.assertTrue(hasPath(items, "TLQ_LICENSE", "/home/tlq/TLQ8/license.dat"));
|
||||
Assertions.assertTrue(containsCode(items, "RABBITMQ_DEFINITIONS"));
|
||||
Assertions.assertTrue(hasRequired(items, "TMS_DB_DUMP"));
|
||||
Assertions.assertFalse(containsCode(items, "CMEP_DB_DUMP"));
|
||||
Assertions.assertFalse(containsCode(items, "TLQ_LICENSE"));
|
||||
Assertions.assertFalse(containsCode(items, "RABBITMQ_DEFINITIONS"));
|
||||
Assertions.assertTrue(hasRequired(items, "TMS_DB"));
|
||||
Assertions.assertTrue(hasRequired(items, "MQ_REPLAY"));
|
||||
Assertions.assertFalse(containsCode(items, "CMEP_DB"));
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask(String productType, String mqType) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class MqReplayContextBuilderTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final MqReplayContextBuilder builder = new MqReplayContextBuilder(objectMapper);
|
||||
|
||||
@Test
|
||||
void shouldBuildMqReplayContextFromInitPlanJson() throws Exception {
|
||||
InitTaskEntity task = new InitTaskEntity();
|
||||
task.setProductType("INDIRECT");
|
||||
task.setMqType("RABBITMQ_TLQ");
|
||||
task.setOrgCode("BKCHCNBJXXX");
|
||||
task.setInitPlanJson("""
|
||||
{"productType":"INDIRECT","orgCode":"BKCHCNBJXXX","mqType":"RABBITMQ_TLQ",
|
||||
"request":{"orgCodeType":"BIC","orgCode":"BKCHCNBJXXX","orgNameCn":"中国银行",
|
||||
"orgNameEn":"Bank of China","deployMode":"DUAL",
|
||||
"nodes":{"node01Ip":"192.168.1.11","node02Ip":"192.168.1.12"},
|
||||
"mq":{"mqType":"RABBITMQ_TLQ","channelUsername":"tms","channelPassword":"Secret123!"}}}
|
||||
""");
|
||||
|
||||
byte[] json = builder.build(task);
|
||||
JsonNode root = objectMapper.readTree(json);
|
||||
|
||||
Assertions.assertEquals("INDIRECT", root.path("productType").asText());
|
||||
Assertions.assertEquals("RABBITMQ_TLQ", root.path("mqType").asText());
|
||||
Assertions.assertEquals("tms", root.path("mq").path("channelUsername").asText());
|
||||
Assertions.assertEquals("Secret123!", root.path("mq").path("channelPassword").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectMissingInitPlanJson() {
|
||||
InitTaskEntity task = new InitTaskEntity();
|
||||
task.setProductType("INDIRECT");
|
||||
|
||||
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> builder.build(task));
|
||||
|
||||
Assertions.assertTrue(ex.getMessage().contains("initPlanJson is empty"));
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,9 @@ class ResourcePackageServiceTest {
|
||||
sampleInitTask(),
|
||||
List.of(
|
||||
resource("TMS_CONFIG", payloadFile.toString()),
|
||||
databaseResource("TMS_DB_DUMP", "TMS"),
|
||||
databaseResource("CMEP_DB_DUMP", "CMEP")
|
||||
databaseResource("TMS_DB", "TMS", "db/TMS.sql"),
|
||||
databaseResource("CMEP_DB", "CMEP", "db/CMEP.sql"),
|
||||
mqReplayResource()
|
||||
),
|
||||
sampleRequest()
|
||||
);
|
||||
@@ -71,7 +72,7 @@ class ResourcePackageServiceTest {
|
||||
Assertions.assertFalse(result.getPackageHash().isBlank());
|
||||
Assertions.assertFalse(result.getStorageEncKeyFingerprint().isBlank());
|
||||
Assertions.assertFalse(result.getAuthenticityKeyFingerprint().isBlank());
|
||||
Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":3"));
|
||||
Assertions.assertTrue(result.getManifestSummaryJson().contains("\"resourceCount\":4"));
|
||||
|
||||
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
|
||||
Assertions.assertNotNull(zipFile.getEntry("manifest.json"));
|
||||
@@ -89,13 +90,18 @@ class ResourcePackageServiceTest {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.List<Map<String, Object>> resources = (java.util.List<Map<String, Object>>) inspectResult.getPayload().get("resources");
|
||||
Assertions.assertEquals(3, resources.size());
|
||||
Assertions.assertEquals(4, 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("TMS_DB", resources.get(1).get("resourceCode"));
|
||||
Assertions.assertEquals("DATABASE", resources.get(1).get("kind"));
|
||||
Assertions.assertEquals("db/TMS.sql", resources.get(1).get("entryPath"));
|
||||
Assertions.assertEquals("CMEP_DB_DUMP", resources.get(2).get("resourceCode"));
|
||||
Assertions.assertEquals("CMEP_DB", resources.get(2).get("resourceCode"));
|
||||
Assertions.assertEquals("DATABASE", resources.get(2).get("kind"));
|
||||
Assertions.assertEquals("db/CMEP.sql", resources.get(2).get("entryPath"));
|
||||
Assertions.assertEquals("MQ_REPLAY", resources.get(3).get("resourceCode"));
|
||||
Assertions.assertEquals("MQ_REPLAY", resources.get(3).get("kind"));
|
||||
Assertions.assertEquals("mq/mq-restore-context.json", resources.get(3).get("entryPath"));
|
||||
|
||||
try (ZipInputStream zipInputStream = new ZipInputStream(new java.io.ByteArrayInputStream(inspectResult.getPayloadArchiveBytes()))) {
|
||||
java.util.Map<String, String> entries = new java.util.LinkedHashMap<>();
|
||||
@@ -109,6 +115,8 @@ class ResourcePackageServiceTest {
|
||||
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"));
|
||||
Assertions.assertTrue(entries.containsKey("mq/mq-restore-context.json"));
|
||||
Assertions.assertTrue(entries.get("mq/mq-restore-context.json").contains("\"channelUsername\":\"tms\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +163,13 @@ class ResourcePackageServiceTest {
|
||||
entity.setMqType("RABBITMQ");
|
||||
entity.setOrgCode("AAAABBBBXXX");
|
||||
entity.setDeployMode("SINGLE");
|
||||
entity.setInitPlanJson("""
|
||||
{"productType":"ENTERPRISE","orgCode":"AAAABBBBXXX","mqType":"RABBITMQ",
|
||||
"request":{"orgCodeType":"BIC","orgCode":"AAAABBBBXXX","orgNameCn":"测试机构",
|
||||
"orgNameEn":"Test Bank","deployMode":"SINGLE",
|
||||
"nodes":{"node01Ip":"127.0.0.1"},
|
||||
"mq":{"mqType":"RABBITMQ","channelUsername":"tms","channelPassword":"Secret123!"}}}
|
||||
""");
|
||||
return entity;
|
||||
}
|
||||
|
||||
@@ -167,11 +182,22 @@ class ResourcePackageServiceTest {
|
||||
return item;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem databaseResource(String code, String path) {
|
||||
private static DiscoveredResourceItem databaseResource(String code, String databaseName, String entryPath) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("DB");
|
||||
item.setResourceType("DATABASE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setResolvedPath(databaseName);
|
||||
item.setDatabaseName(databaseName);
|
||||
item.setEntryPath(entryPath);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem mqReplayResource() {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("MQ_REPLAY");
|
||||
item.setResourceCode("MQ_REPLAY");
|
||||
item.setEntryPath("mq/mq-restore-context.json");
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.cisd.tms.modules.backup.packagex;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourcePayloadExtractorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExtractPayloadAndWriteRestoreContext() throws Exception {
|
||||
ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourcePackageInspectResult inspectResult = new ResourcePackageInspectResult();
|
||||
inspectResult.setPayloadArchiveBytes(payloadArchive(
|
||||
"""
|
||||
{"schemaVersion":"2.0","resources":[
|
||||
{"code":"TMS_CONFIG","kind":"FILE","restorePath":"%s/config",
|
||||
"files":[{"entryPath":"files/TMS_CONFIG/application.yml","restorePath":"%s/config/application.yml"}]},
|
||||
{"code":"TMS_DB","kind":"DATABASE","entryPath":"db/TMS.sql"},
|
||||
{"code":"MQ_REPLAY","kind":"MQ_REPLAY","entryPath":"mq/mq-restore-context.json"}
|
||||
]}
|
||||
""".formatted(tempDir, tempDir),
|
||||
"files/TMS_CONFIG/application.yml",
|
||||
"server.port=8080"
|
||||
));
|
||||
Mockito.when(packageService.inspectPackage(Mockito.any(Path.class))).thenReturn(inspectResult);
|
||||
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setAllowedRestoreRoots(List.of(tempDir.toString()));
|
||||
ResourcePayloadExtractor extractor = new ResourcePayloadExtractor(packageService, new ObjectMapper(), properties);
|
||||
Path workDir = tempDir.resolve("restore");
|
||||
|
||||
ResourcePayloadExtractionResult result = extractor.extract(tempDir.resolve("backup.tmsbak"), workDir);
|
||||
|
||||
Assertions.assertEquals(workDir.resolve("payload"), result.getPayloadDir());
|
||||
Assertions.assertTrue(Files.exists(result.getPayloadDir().resolve("payload-manifest.json")));
|
||||
Assertions.assertTrue(Files.exists(result.getPayloadDir().resolve("files/TMS_CONFIG/application.yml")));
|
||||
Assertions.assertTrue(Files.exists(workDir.resolve("restore-context.json")));
|
||||
Assertions.assertEquals(
|
||||
"files/TMS_CONFIG/application.yml\t" + tempDir.resolve("config/application.yml") + System.lineSeparator(),
|
||||
Files.readString(workDir.resolve("restore-files.tsv"))
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
"db/TMS.sql\tTMS" + System.lineSeparator(),
|
||||
Files.readString(workDir.resolve("restore-databases.tsv"))
|
||||
);
|
||||
Assertions.assertEquals(
|
||||
"mq/mq-restore-context.json" + System.lineSeparator(),
|
||||
Files.readString(workDir.resolve("restore-mq.tsv"))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPayloadEntryEscapingTargetDirectory() throws Exception {
|
||||
ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourcePackageInspectResult inspectResult = new ResourcePackageInspectResult();
|
||||
inspectResult.setPayloadArchiveBytes(payloadArchive("{}", "../escaped.txt", "bad"));
|
||||
Mockito.when(packageService.inspectPackage(Mockito.any(Path.class))).thenReturn(inspectResult);
|
||||
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setAllowedRestoreRoots(List.of(tempDir.toString()));
|
||||
ResourcePayloadExtractor extractor = new ResourcePayloadExtractor(packageService, new ObjectMapper(), properties);
|
||||
|
||||
IllegalStateException ex = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> extractor.extract(tempDir.resolve("backup.tmsbak"), tempDir.resolve("restore"))
|
||||
);
|
||||
|
||||
Assertions.assertTrue(ex.getMessage().contains("escapes payload dir"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectFileRestorePathOutsideAllowedRoots() throws Exception {
|
||||
ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourcePackageInspectResult inspectResult = new ResourcePackageInspectResult();
|
||||
inspectResult.setPayloadArchiveBytes(payloadArchive(
|
||||
"""
|
||||
{"schemaVersion":"2.0","resources":[
|
||||
{"code":"TMS_CONFIG","kind":"FILE","restorePath":"/etc",
|
||||
"files":[{"entryPath":"files/TMS_CONFIG/application.yml","restorePath":"/etc/application.yml"}]}
|
||||
]}
|
||||
""",
|
||||
"files/TMS_CONFIG/application.yml",
|
||||
"server.port=8080"
|
||||
));
|
||||
Mockito.when(packageService.inspectPackage(Mockito.any(Path.class))).thenReturn(inspectResult);
|
||||
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setAllowedRestoreRoots(List.of(tempDir.toString()));
|
||||
ResourcePayloadExtractor extractor = new ResourcePayloadExtractor(packageService, new ObjectMapper(), properties);
|
||||
|
||||
IllegalStateException ex = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> extractor.extract(tempDir.resolve("backup.tmsbak"), tempDir.resolve("restore"))
|
||||
);
|
||||
|
||||
Assertions.assertTrue(ex.getMessage().contains("restore path is not allowed"));
|
||||
}
|
||||
|
||||
private static byte[] payloadArchive(String manifestJson, String extraEntry, String extraContent) throws Exception {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
|
||||
zipOutputStream.putNextEntry(new ZipEntry("payload-manifest.json"));
|
||||
zipOutputStream.write(manifestJson.getBytes(StandardCharsets.UTF_8));
|
||||
zipOutputStream.closeEntry();
|
||||
zipOutputStream.putNextEntry(new ZipEntry(extraEntry));
|
||||
zipOutputStream.write(extraContent.getBytes(StandardCharsets.UTF_8));
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -57,8 +57,8 @@ class ResourceTaskRepositoryTest {
|
||||
entity.setBackupId("RBACKUP-001");
|
||||
entity.setStatus("READY");
|
||||
entity.setPackageFileId("FILE-001");
|
||||
entity.setStagingDir("/home/tms/tmp/resource-restore/RRST-001/staging");
|
||||
entity.setRunnerStatePath("/home/tms/tmp/resource-restore/RRST-001/restore-state.json");
|
||||
entity.setStagingDir("/home/tms/tmp/resource-restore/RRST-001/payload");
|
||||
entity.setRunnerStatePath("/home/tms/tmp/resource-restore/RRST-001/status.json");
|
||||
entity.setSourceProductType("DIRECT");
|
||||
entity.setTargetProductType("DIRECT");
|
||||
entity.setConsistencyReportJson("{\"ok\":true}");
|
||||
@@ -74,8 +74,8 @@ class ResourceTaskRepositoryTest {
|
||||
Assertions.assertEquals("RRST-001", found.getTaskId());
|
||||
Assertions.assertEquals("RPRE-001", found.getPrecheckId());
|
||||
Assertions.assertEquals("FILE-001", found.getPackageFileId());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/staging", found.getStagingDir());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/restore-state.json", found.getRunnerStatePath());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/payload", found.getStagingDir());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/status.json", found.getRunnerStatePath());
|
||||
Assertions.assertEquals("DIRECT", found.getSourceProductType());
|
||||
Assertions.assertEquals("DIRECT", found.getTargetProductType());
|
||||
Assertions.assertEquals("{\"ok\":true}", found.getConsistencyReportJson());
|
||||
@@ -90,9 +90,9 @@ class ResourceTaskRepositoryTest {
|
||||
entity.setTaskId("RRST-001");
|
||||
entity.setTaskType("RESOURCE_RESTORE");
|
||||
entity.setStepNo(1);
|
||||
entity.setStepCode("STOP_SERVICES");
|
||||
entity.setStepCode("SHELL_APPLY");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setLogPath("/home/tms/tmp/resource-restore/RRST-001/steps/01-STOP_SERVICES.log");
|
||||
entity.setLogPath("/home/tms/tmp/resource-restore/RRST-001/restore.log");
|
||||
entity.setExitCode(0);
|
||||
|
||||
repository.save(entity);
|
||||
@@ -104,8 +104,8 @@ class ResourceTaskRepositoryTest {
|
||||
|
||||
Assertions.assertEquals(1, steps.size());
|
||||
Assertions.assertEquals("RESOURCE_RESTORE", steps.get(0).getTaskType());
|
||||
Assertions.assertEquals("STOP_SERVICES", steps.get(0).getStepCode());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/steps/01-STOP_SERVICES.log", steps.get(0).getLogPath());
|
||||
Assertions.assertEquals("SHELL_APPLY", steps.get(0).getStepCode());
|
||||
Assertions.assertEquals("/home/tms/tmp/resource-restore/RRST-001/restore.log", steps.get(0).getLogPath());
|
||||
Assertions.assertEquals(0, steps.get(0).getExitCode());
|
||||
}
|
||||
|
||||
|
||||
+3
-5
@@ -62,8 +62,7 @@ class ResourceTaskSchemaContractTest {
|
||||
"PRECHECKING",
|
||||
"PRECHECK_FAILED",
|
||||
"READY",
|
||||
"HANDOFF",
|
||||
"RUNNING",
|
||||
"APPLYING",
|
||||
"VERIFYING",
|
||||
"SUCCESS",
|
||||
"FAILED",
|
||||
@@ -77,9 +76,8 @@ class ResourceTaskSchemaContractTest {
|
||||
assertEnumConstants(
|
||||
"com.cisd.tms.modules.backup.enums.ResourceControlPhase",
|
||||
"TMS_PRECHECK",
|
||||
"HANDOFF_READY",
|
||||
"RUNNER_OWNED",
|
||||
"RECONCILE_PENDING",
|
||||
"JAVA_EXTRACT",
|
||||
"SHELL_APPLY",
|
||||
"DONE"
|
||||
);
|
||||
assertEnumConstants(
|
||||
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ApplyResourceBackupScriptContractTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void scriptShouldContainRequiredPhasesAndNoSecretsEcho() throws Exception {
|
||||
String script = Files.readString(Path.of("scripts/resource-restore/apply-resource-backup.sh"));
|
||||
|
||||
Assertions.assertTrue(script.contains("FILE"));
|
||||
Assertions.assertTrue(script.contains("DATABASE"));
|
||||
Assertions.assertTrue(script.contains("MQ_REPLAY"));
|
||||
Assertions.assertTrue(script.contains("HEALTH_CHECK"));
|
||||
Assertions.assertFalse(script.contains("channelPassword"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRestoreFilesFromGeneratedPlan() throws Exception {
|
||||
Path workDir = tempDir.resolve("work");
|
||||
Path payloadDir = workDir.resolve("payload");
|
||||
Path sourceFile = payloadDir.resolve("resources/TMS_CONFIG/application.yml");
|
||||
Path targetFile = tempDir.resolve("target/application.yml");
|
||||
Files.createDirectories(sourceFile.getParent());
|
||||
Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}");
|
||||
Files.writeString(sourceFile, "server.port=8080");
|
||||
Files.writeString(workDir.resolve("restore-files.tsv"),
|
||||
"resources/TMS_CONFIG/application.yml\t" + targetFile + System.lineSeparator());
|
||||
|
||||
runScript(workDir, Map.of());
|
||||
|
||||
Assertions.assertEquals("server.port=8080", Files.readString(targetFile));
|
||||
Assertions.assertTrue(Files.readString(workDir.resolve("status.json")).contains("\"status\":\"SUCCESS\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunDatabaseAndMqReplayScriptsFromGeneratedPlans() throws Exception {
|
||||
Path workDir = tempDir.resolve("work");
|
||||
Path payloadDir = workDir.resolve("payload");
|
||||
Files.createDirectories(payloadDir.resolve("db"));
|
||||
Files.createDirectories(payloadDir.resolve("mq"));
|
||||
Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}");
|
||||
Files.writeString(payloadDir.resolve("db/TMS.sql"), "select 1;");
|
||||
Files.writeString(payloadDir.resolve("mq/mq-restore-context.json"), "{}");
|
||||
Files.writeString(workDir.resolve("restore-databases.tsv"), "db/TMS.sql\tTMS" + System.lineSeparator());
|
||||
Files.writeString(workDir.resolve("restore-mq.tsv"), "mq/mq-restore-context.json" + System.lineSeparator());
|
||||
|
||||
Path commandLog = tempDir.resolve("commands.log");
|
||||
Path dbScript = fakeScript("db-restore.sh", commandLog);
|
||||
Path mqScript = fakeScript("mq-replay.sh", commandLog);
|
||||
|
||||
runScript(workDir, Map.of(
|
||||
"DB_RESTORE_SCRIPT_PATH", dbScript.toString(),
|
||||
"MQ_REPLAY_SCRIPT_PATH", mqScript.toString(),
|
||||
"MYSQL_PATH", "mysql-test",
|
||||
"DB_HOST", "127.0.0.1",
|
||||
"DB_PORT", "4000",
|
||||
"DB_USER", "root",
|
||||
"DB_PASSWORD", "secret"
|
||||
));
|
||||
|
||||
String log = Files.readString(commandLog);
|
||||
Assertions.assertTrue(log.contains("--db-name TMS"));
|
||||
Assertions.assertTrue(log.contains("--sql-file " + payloadDir.resolve("db/TMS.sql")));
|
||||
Assertions.assertTrue(log.contains("--context-file " + payloadDir.resolve("mq/mq-restore-context.json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLoadRestoreEnvFileFromWorkDir() throws Exception {
|
||||
Path workDir = tempDir.resolve("work");
|
||||
Path payloadDir = workDir.resolve("payload");
|
||||
Files.createDirectories(payloadDir.resolve("db"));
|
||||
Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}");
|
||||
Files.writeString(payloadDir.resolve("db/TMS.sql"), "select 1;");
|
||||
Files.writeString(workDir.resolve("restore-databases.tsv"), "db/TMS.sql\tTMS" + System.lineSeparator());
|
||||
|
||||
Path commandLog = tempDir.resolve("commands.log");
|
||||
Path dbScript = fakeScript("db-restore.sh", commandLog);
|
||||
Files.writeString(workDir.resolve("restore-env.sh"), """
|
||||
export DB_RESTORE_SCRIPT_PATH='%s'
|
||||
export MYSQL_PATH='mysql-from-env'
|
||||
export DB_HOST='10.0.0.1'
|
||||
export DB_PORT='4000'
|
||||
export DB_USER='root'
|
||||
export DB_PASSWORD='secret'
|
||||
""".formatted(dbScript));
|
||||
|
||||
runScript(workDir, Map.of());
|
||||
|
||||
String log = Files.readString(commandLog);
|
||||
Assertions.assertTrue(log.contains("--mysql-path mysql-from-env"));
|
||||
Assertions.assertTrue(log.contains("--host 10.0.0.1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRunHealthCheckWhenConfigured() throws Exception {
|
||||
Path workDir = tempDir.resolve("work");
|
||||
Path payloadDir = workDir.resolve("payload");
|
||||
Files.createDirectories(payloadDir);
|
||||
Files.writeString(payloadDir.resolve("payload-manifest.json"), "{\"resources\":[]}");
|
||||
Path commandLog = tempDir.resolve("curl.log");
|
||||
Path binDir = tempDir.resolve("bin");
|
||||
Files.createDirectories(binDir);
|
||||
Path curl = binDir.resolve("curl");
|
||||
Files.writeString(curl, """
|
||||
#!/usr/bin/env bash
|
||||
printf '%%s\\n' "$*" >> "%s"
|
||||
printf '{"status":"UP"}'
|
||||
""".formatted(commandLog));
|
||||
curl.toFile().setExecutable(true);
|
||||
|
||||
runScript(workDir, Map.of(
|
||||
"HEALTH_CHECK_URL", "http://127.0.0.1:8080/actuator/health",
|
||||
"HEALTH_CHECK_TIMEOUT_SECONDS", "3",
|
||||
"PATH", binDir + ":" + System.getenv("PATH")
|
||||
));
|
||||
|
||||
String log = Files.readString(commandLog);
|
||||
Assertions.assertTrue(log.contains("--max-time 3"));
|
||||
Assertions.assertTrue(log.contains("http://127.0.0.1:8080/actuator/health"));
|
||||
}
|
||||
|
||||
private static void runScript(Path workDir, Map<String, String> environment) throws Exception {
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
"bash",
|
||||
Path.of("scripts/resource-restore/apply-resource-backup.sh").toAbsolutePath().normalize().toString(),
|
||||
"--work-dir",
|
||||
workDir.toString()
|
||||
);
|
||||
builder.directory(Path.of(".").toAbsolutePath().normalize().toFile());
|
||||
builder.environment().putAll(environment);
|
||||
Process process = builder.start();
|
||||
int exitCode = process.waitFor();
|
||||
Assertions.assertEquals(0, exitCode, new String(process.getErrorStream().readAllBytes()));
|
||||
}
|
||||
|
||||
private Path fakeScript(String name, Path commandLog) throws Exception {
|
||||
Path script = tempDir.resolve(name);
|
||||
Files.writeString(script, """
|
||||
#!/usr/bin/env bash
|
||||
printf '%%s %%s\\n' "$0" "$*" >> "%s"
|
||||
""".formatted(commandLog));
|
||||
script.toFile().setExecutable(true);
|
||||
return script;
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
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;
|
||||
|
||||
class LightweightRestoreApplierLauncherTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldLaunchApplyScriptWithWorkDir() throws Exception {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRestoreApplyScriptPath("/home/tms/bin/resource-restore/apply-resource-backup.sh");
|
||||
properties.setRestoreDbScriptPath("/home/tms/bin/resource-restore/restore-db.sh");
|
||||
properties.setMqReplayScriptPath("/home/tms/bin/resource-restore/replay-mq.sh");
|
||||
RecordingInvoker invoker = new RecordingInvoker();
|
||||
LightweightRestoreApplierLauncher launcher = new LightweightRestoreApplierLauncher(
|
||||
properties,
|
||||
invoker,
|
||||
"jdbc:mysql://db-host:4000/tms?useUnicode=true",
|
||||
"root",
|
||||
"p'ass"
|
||||
);
|
||||
|
||||
Path workDir = tempDir.resolve("RRST-001");
|
||||
launcher.launch(workDir);
|
||||
|
||||
Assertions.assertEquals(List.of(
|
||||
"/bin/bash",
|
||||
"/home/tms/bin/resource-restore/apply-resource-backup.sh",
|
||||
"--work-dir",
|
||||
workDir.toAbsolutePath().normalize().toString()
|
||||
), invoker.command);
|
||||
Assertions.assertEquals(workDir.toAbsolutePath().normalize().resolve("restore-launch.log"), invoker.logFile);
|
||||
Assertions.assertEquals(workDir.toAbsolutePath().normalize(), invoker.workingDirectory);
|
||||
|
||||
String env = Files.readString(workDir.toAbsolutePath().normalize().resolve("restore-env.sh"));
|
||||
Assertions.assertTrue(env.contains("export DB_RESTORE_SCRIPT_PATH='/home/tms/bin/resource-restore/restore-db.sh'"));
|
||||
Assertions.assertTrue(env.contains("export MQ_REPLAY_SCRIPT_PATH='/home/tms/bin/resource-restore/replay-mq.sh'"));
|
||||
Assertions.assertTrue(env.contains("export MYSQL_PATH='mysql'"));
|
||||
Assertions.assertTrue(env.contains("export DB_HOST='db-host'"));
|
||||
Assertions.assertTrue(env.contains("export DB_PORT='4000'"));
|
||||
Assertions.assertTrue(env.contains("export DB_USER='root'"));
|
||||
Assertions.assertTrue(env.contains("export DB_PASSWORD='p'\\''ass'"));
|
||||
}
|
||||
|
||||
private static final class RecordingInvoker implements RestoreCommandInvoker {
|
||||
|
||||
private List<String> command;
|
||||
private Path logFile;
|
||||
private Path workingDirectory;
|
||||
|
||||
@Override
|
||||
public int run(List<String> command, Path logFile, Path workingDirectory) {
|
||||
this.command = command;
|
||||
this.logFile = logFile;
|
||||
this.workingDirectory = workingDirectory;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
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;
|
||||
|
||||
class MqReplayScriptContractTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldReplayRabbitMqUserAndPermissionsFromContext() throws Exception {
|
||||
Path context = tempDir.resolve("mq-restore-context.json");
|
||||
Files.writeString(context, """
|
||||
{"schemaVersion":"1.0","productType":"INDIRECT","mqType":"RABBITMQ_TLQ","orgCode":"BKCHCNBJXXX",
|
||||
"mq":{"channelUsername":"tms","channelPassword":"Secret123!"}}
|
||||
""");
|
||||
Path commandLog = tempDir.resolve("commands.log");
|
||||
Path rabbitmqctl = fakeRabbitmqctl(commandLog);
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
"bash",
|
||||
Path.of("scripts/resource-restore/replay-mq.sh").toAbsolutePath().normalize().toString(),
|
||||
"--context-file",
|
||||
context.toString(),
|
||||
"--work-dir",
|
||||
tempDir.resolve("work").toString()
|
||||
);
|
||||
builder.environment().put("RABBITMQ_CTL_COMMAND", rabbitmqctl.toString());
|
||||
Process process = builder.start();
|
||||
|
||||
Assertions.assertEquals(0, process.waitFor(), new String(process.getErrorStream().readAllBytes()));
|
||||
String log = Files.readString(commandLog);
|
||||
Assertions.assertTrue(log.contains("add_vhost /RQ"));
|
||||
Assertions.assertTrue(log.contains("add_user tms Secret123!"));
|
||||
Assertions.assertTrue(log.contains("set_permissions -p /RQ tms .* .* .*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateToCustomReplayCommandWhenConfigured() throws Exception {
|
||||
Path context = tempDir.resolve("mq-restore-context.json");
|
||||
Files.writeString(context, "{}");
|
||||
Path commandLog = tempDir.resolve("commands.log");
|
||||
Path custom = fakeRabbitmqctl(commandLog);
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(
|
||||
"bash",
|
||||
Path.of("scripts/resource-restore/replay-mq.sh").toAbsolutePath().normalize().toString(),
|
||||
"--context-file",
|
||||
context.toString(),
|
||||
"--work-dir",
|
||||
tempDir.resolve("work").toString()
|
||||
);
|
||||
builder.environment().put("MQ_REPLAY_COMMAND", custom.toString());
|
||||
Process process = builder.start();
|
||||
|
||||
Assertions.assertEquals(0, process.waitFor(), new String(process.getErrorStream().readAllBytes()));
|
||||
Assertions.assertTrue(Files.readString(commandLog).contains("--context-file " + context));
|
||||
}
|
||||
|
||||
private Path fakeRabbitmqctl(Path commandLog) throws Exception {
|
||||
Path script = tempDir.resolve("rabbitmqctl");
|
||||
Files.writeString(script, """
|
||||
#!/usr/bin/env bash
|
||||
printf '%%s\\n' "$*" >> "%s"
|
||||
exit 0
|
||||
""".formatted(commandLog));
|
||||
script.toFile().setExecutable(true);
|
||||
return script;
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -7,23 +7,23 @@ import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class RunnerCommandInvokerTest {
|
||||
class RestoreCommandInvokerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExecuteCommandAndAppendLog() throws Exception {
|
||||
ProcessBuilderRunnerCommandInvoker invoker = new ProcessBuilderRunnerCommandInvoker();
|
||||
Path logFile = tempDir.resolve("steps/01-STOP_SERVICES.log");
|
||||
ProcessBuilderRestoreCommandInvoker invoker = new ProcessBuilderRestoreCommandInvoker();
|
||||
Path logFile = tempDir.resolve("restore.log");
|
||||
|
||||
int exitCode = invoker.run(
|
||||
List.of("/bin/zsh", "-lc", "printf 'hello-runner'"),
|
||||
List.of("/bin/zsh", "-lc", "printf 'hello-restore'"),
|
||||
logFile,
|
||||
tempDir
|
||||
);
|
||||
|
||||
Assertions.assertEquals(0, exitCode);
|
||||
Assertions.assertEquals("hello-runner", Files.readString(logFile));
|
||||
Assertions.assertEquals("hello-restore", Files.readString(logFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.cisd.tms.modules.backup.restore;
|
||||
|
||||
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;
|
||||
|
||||
class RestoreStatusFileReaderTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldReadStatusJsonAndLog() throws Exception {
|
||||
Path workDir = tempDir.resolve("RRST-001");
|
||||
Files.createDirectories(workDir);
|
||||
Files.writeString(workDir.resolve("status.json"),
|
||||
"{\"status\":\"RUNNING\",\"phase\":\"MQ_REPLAY\",\"message\":\"replaying mq\",\"updatedAt\":\"2026-04-29T10:00:00+08:00\"}");
|
||||
Files.writeString(workDir.resolve("restore.log"), "line1\nline2\n");
|
||||
|
||||
RestoreStatusFileReader reader = new RestoreStatusFileReader(new ObjectMapper());
|
||||
|
||||
RestoreStatusFileReader.RestoreStatusSnapshot snapshot = reader.read(workDir);
|
||||
|
||||
Assertions.assertEquals("RUNNING", snapshot.status());
|
||||
Assertions.assertEquals("MQ_REPLAY", snapshot.phase());
|
||||
Assertions.assertEquals("replaying mq", snapshot.message());
|
||||
Assertions.assertEquals("2026-04-29T10:00:00+08:00", snapshot.updatedAt());
|
||||
Assertions.assertTrue(snapshot.log().contains("line1"));
|
||||
}
|
||||
}
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
-196
@@ -1,196 +0,0 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest;
|
||||
import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.packagex.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;
|
||||
import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class PayloadArchiveRunnerStepExecutorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExtractPayloadArchiveIntoStagingDirectoryAndRestoreCriticalFiles() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourcePackageService packageService = new ResourcePackageServiceImpl(
|
||||
backupProperties(),
|
||||
new ResourceEnvelopeCryptoServiceImpl(mockCrypto(), backupProperties()),
|
||||
mockDatabaseBackupCollector(),
|
||||
objectMapper
|
||||
);
|
||||
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);
|
||||
|
||||
String packagePath = packageService.createPackage(
|
||||
"RBKP-TASK-20260414-000101",
|
||||
"RBKP-20260414-000101",
|
||||
sampleInitTask(),
|
||||
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");
|
||||
entity.setPackageFileId("FILE-20260414-000001");
|
||||
entity.setPackagePath(packagePath);
|
||||
entity.setStagingDir(taskDir.resolve("staging").toString());
|
||||
entity.setRunnerStatePath(taskDir.resolve("restore-state.json").toString());
|
||||
entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"RESTORE_FILES\",\"CONSISTENCY_VERIFY\"]");
|
||||
new HandoffFileStore(objectMapper).write(taskDir, entity);
|
||||
|
||||
PayloadArchiveRunnerStepExecutor executor = new PayloadArchiveRunnerStepExecutor(
|
||||
new HandoffFileStore(objectMapper),
|
||||
packageService,
|
||||
objectMapper
|
||||
);
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
entity.getTaskId(),
|
||||
"RESTORE_FILES",
|
||||
2,
|
||||
taskDir,
|
||||
taskDir.resolve("handoff.json"),
|
||||
taskDir.resolve("restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertTrue(result.getSummary().contains("restoredFileCount=2"));
|
||||
Assertions.assertTrue(Files.exists(taskDir.resolve("staging/payload/payload-manifest.json")));
|
||||
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() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setOutputDir(tempDir.resolve("backup-output").toString());
|
||||
return properties;
|
||||
}
|
||||
|
||||
private Path writeFile(String relativePath, String content) throws Exception {
|
||||
Path file = tempDir.resolve(relativePath);
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.writeString(file, content);
|
||||
return file;
|
||||
}
|
||||
|
||||
private static PcieCryptoService mockCrypto() {
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getData());
|
||||
});
|
||||
Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any(EccInternalDecryptRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
EccInternalDecryptRequest request = invocation.getArgument(0);
|
||||
return backup(request.getCipherBlob());
|
||||
});
|
||||
Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any()))
|
||||
.thenReturn(backup(new byte[]{0x21, 0x22, 0x23}));
|
||||
Mockito.doNothing().when(pcieCryptoService).eccInternalVerify(Mockito.any());
|
||||
return pcieCryptoService;
|
||||
}
|
||||
|
||||
private static BackupDataResult backup(byte[] data) {
|
||||
BackupDataResult result = new BackupDataResult();
|
||||
result.setData(data);
|
||||
result.setLength(data.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static InitTaskEntity sampleInitTask() {
|
||||
InitTaskEntity entity = new InitTaskEntity();
|
||||
entity.setTaskId("INIT-20260408-000001");
|
||||
entity.setTaskType("INIT");
|
||||
entity.setStatus("SUCCESS");
|
||||
entity.setProductType("ENTERPRISE");
|
||||
entity.setMqType("RABBITMQ");
|
||||
entity.setOrgCode("AAAABBBBXXX");
|
||||
entity.setDeployMode("SINGLE");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static DiscoveredResourceItem resource(String code, String path) {
|
||||
DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
item.setResourceType("FILE");
|
||||
item.setResourceCode(code);
|
||||
item.setResolvedPath(path);
|
||||
item.setRequired(true);
|
||||
return item;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
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,148 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ResourceRestoreRunnerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldTakeOwnershipFromHandoffReadyState() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper);
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
RecordingRunnerStepExecutor stepExecutor = new RecordingRunnerStepExecutor();
|
||||
ResourceRestoreRunner runner = new ResourceRestoreRunner(
|
||||
handoffFileStore,
|
||||
stateFileStore,
|
||||
objectMapper,
|
||||
List.of(stepExecutor)
|
||||
);
|
||||
|
||||
ResourceRestoreTaskEntity entity = sampleTask();
|
||||
handoffFileStore.write(tempDir, entity);
|
||||
stateFileStore.initialize(tempDir, entity);
|
||||
stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
|
||||
runner.run(tempDir.resolve("handoff.json"));
|
||||
|
||||
JsonNode state = objectMapper.readTree(Files.readString(tempDir.resolve("restore-state.json")));
|
||||
Assertions.assertEquals("SUCCESS", state.path("status").asText());
|
||||
Assertions.assertEquals("RECONCILE_PENDING", state.path("controlPhase").asText());
|
||||
Assertions.assertEquals("RUNNER_FILE", state.path("stateSource").asText());
|
||||
Assertions.assertEquals("START_SERVICES", state.path("currentStepCode").asText());
|
||||
Assertions.assertEquals(3, state.path("currentStepNo").asInt());
|
||||
Assertions.assertFalse(state.path("runnerInstanceId").asText().isBlank());
|
||||
Assertions.assertTrue(state.path("runnerPid").asLong() > 0);
|
||||
Assertions.assertFalse(state.path("lastHeartbeatAt").asText().isBlank());
|
||||
Assertions.assertEquals(List.of("STOP_SERVICES", "START_SERVICES"), stepExecutor.executedSteps);
|
||||
JsonNode stepOutput = objectMapper.readTree(Files.readString(tempDir.resolve("step-output/02-STOP_SERVICES.json")));
|
||||
Assertions.assertEquals("STOP_SERVICES", stepOutput.path("stepCode").asText());
|
||||
Assertions.assertEquals("SUCCESS", stepOutput.path("result").asText());
|
||||
Assertions.assertEquals("executed STOP_SERVICES", stepOutput.path("summary").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDuplicateOwnershipWhenStateAlreadyTaken() {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper);
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
RecordingRunnerStepExecutor stepExecutor = new RecordingRunnerStepExecutor();
|
||||
ResourceRestoreRunner runner = new ResourceRestoreRunner(
|
||||
handoffFileStore,
|
||||
stateFileStore,
|
||||
objectMapper,
|
||||
List.of(stepExecutor)
|
||||
);
|
||||
|
||||
ResourceRestoreTaskEntity entity = sampleTask();
|
||||
handoffFileStore.write(tempDir, entity);
|
||||
stateFileStore.initialize(tempDir, entity);
|
||||
stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
|
||||
runner.run(tempDir.resolve("handoff.json"));
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> runner.run(tempDir.resolve("handoff.json"))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("handoff ready"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistFailureWhenStepExecutorMissing() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
HandoffFileStore handoffFileStore = new HandoffFileStore(objectMapper);
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
ResourceRestoreRunner runner = new ResourceRestoreRunner(
|
||||
handoffFileStore,
|
||||
stateFileStore,
|
||||
objectMapper,
|
||||
List.of()
|
||||
);
|
||||
|
||||
ResourceRestoreTaskEntity entity = sampleTask();
|
||||
handoffFileStore.write(tempDir, entity);
|
||||
stateFileStore.initialize(tempDir, entity);
|
||||
stateFileStore.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> runner.run(tempDir.resolve("handoff.json"))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("no runner step executor"));
|
||||
|
||||
JsonNode state = objectMapper.readTree(Files.readString(tempDir.resolve("restore-state.json")));
|
||||
Assertions.assertEquals("FAILED", state.path("status").asText());
|
||||
Assertions.assertEquals("RECONCILE_PENDING", state.path("controlPhase").asText());
|
||||
Assertions.assertEquals("STOP_SERVICES", state.path("currentStepCode").asText());
|
||||
Assertions.assertEquals(2, state.path("currentStepNo").asInt());
|
||||
Assertions.assertEquals("RUNNER_EXECUTION_FAILED", state.path("errorCode").asText());
|
||||
Assertions.assertTrue(state.path("errorMessage").asText().contains("no runner step executor"));
|
||||
JsonNode stepOutput = objectMapper.readTree(Files.readString(tempDir.resolve("step-output/02-STOP_SERVICES.json")));
|
||||
Assertions.assertEquals("FAILED", stepOutput.path("result").asText());
|
||||
Assertions.assertTrue(stepOutput.path("summary").asText().contains("no runner step executor"));
|
||||
}
|
||||
|
||||
private ResourceRestoreTaskEntity sampleTask() {
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260413-000001");
|
||||
entity.setPrecheckId("RPRE-20260413-000001");
|
||||
entity.setBackupId("RBKP-20260413-153000-000001");
|
||||
entity.setPackageFileId("FILE-20260413-000001");
|
||||
entity.setPackagePath("/tmp/backup.tmsbak");
|
||||
entity.setStagingDir(tempDir.resolve("staging").toString());
|
||||
entity.setRunnerStatePath(tempDir.resolve("restore-state.json").toString());
|
||||
entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"STOP_SERVICES\",\"START_SERVICES\"]");
|
||||
entity.setStatus("HANDOFF");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static class RecordingRunnerStepExecutor implements RunnerStepExecutor {
|
||||
|
||||
private final List<String> executedSteps = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public boolean supports(String stepCode) {
|
||||
return "STOP_SERVICES".equals(stepCode) || "START_SERVICES".equals(stepCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RunnerStepExecutionResult execute(RunnerStepContext context) {
|
||||
executedSteps.add(context.stepCode());
|
||||
return RunnerStepExecutionResult.success("executed " + context.stepCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class RestoreStateReconcilerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldOnlyReconcileTasksInReconcilePendingPhase() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
|
||||
ResourceRestoreTaskEntity running = task("RRST-TASK-001", tempDir.resolve("running/restore-state.json"));
|
||||
Files.createDirectories(runningStateDir(running));
|
||||
Files.writeString(Path.of(running.getRunnerStatePath()), """
|
||||
{"taskId":"RRST-TASK-001","status":"RUNNING","controlPhase":"RUNNER_OWNED","stateSource":"RUNNER_FILE"}
|
||||
""");
|
||||
ResourceRestoreTaskEntity pending = task("RRST-TASK-002", tempDir.resolve("pending/restore-state.json"));
|
||||
Files.createDirectories(runningStateDir(pending));
|
||||
Files.writeString(Path.of(pending.getRunnerStatePath()), """
|
||||
{"taskId":"RRST-TASK-002","status":"SUCCESS","controlPhase":"RECONCILE_PENDING","stateSource":"RUNNER_FILE","errorCode":"","errorMessage":""}
|
||||
""");
|
||||
Mockito.when(repository.findAll()).thenReturn(List.of(running, pending));
|
||||
|
||||
RestoreStateReconciler reconciler = new RestoreStateReconciler(repository, stateFileStore, LocalDateTime::now);
|
||||
reconciler.reconcileOnce();
|
||||
|
||||
ArgumentCaptor<ResourceRestoreTaskEntity> captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class);
|
||||
Mockito.verify(repository, Mockito.times(1)).update(captor.capture());
|
||||
Assertions.assertEquals("RRST-TASK-002", captor.getValue().getTaskId());
|
||||
|
||||
String stateContent = Files.readString(Path.of(pending.getRunnerStatePath()));
|
||||
Assertions.assertTrue(stateContent.contains("\"controlPhase\":\"DONE\""));
|
||||
}
|
||||
|
||||
private ResourceRestoreTaskEntity task(String taskId, Path statePath) {
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setStatus("HANDOFF");
|
||||
entity.setRunnerStatePath(statePath.toString());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Path runningStateDir(ResourceRestoreTaskEntity entity) {
|
||||
return Path.of(entity.getRunnerStatePath()).getParent();
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class RunnerStallDetectorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldMarkRunnerOwnedTaskAsFailedWhenHeartbeatExpired() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourceRestoreStateFileStore stateFileStore = new ResourceRestoreStateFileStore(objectMapper);
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setRunnerHeartbeatTimeoutSeconds(60);
|
||||
|
||||
ResourceRestoreTaskEntity stale = task("RRST-TASK-003", tempDir.resolve("stale/restore-state.json"));
|
||||
Files.createDirectories(Path.of(stale.getRunnerStatePath()).getParent());
|
||||
Files.writeString(Path.of(stale.getRunnerStatePath()), """
|
||||
{"taskId":"RRST-TASK-003","status":"RUNNING","controlPhase":"RUNNER_OWNED","stateSource":"RUNNER_FILE","runnerInstanceId":"RRUN-001","lastHeartbeatAt":"2026-04-14T08:00:00","currentStepCode":"STOP_SERVICES","currentStepNo":2,"orphaned":false}
|
||||
""");
|
||||
Mockito.when(repository.findAll()).thenReturn(List.of(stale));
|
||||
|
||||
RunnerStallDetector detector = new RunnerStallDetector(repository, stateFileStore, properties, () -> LocalDateTime.parse("2026-04-14T08:05:01"));
|
||||
detector.detectOnce();
|
||||
|
||||
String content = Files.readString(Path.of(stale.getRunnerStatePath()));
|
||||
Assertions.assertTrue(content.contains("\"status\":\"FAILED\""));
|
||||
Assertions.assertTrue(content.contains("\"controlPhase\":\"RECONCILE_PENDING\""));
|
||||
Assertions.assertTrue(content.contains("\"orphaned\":true"));
|
||||
Assertions.assertTrue(content.contains("RUNNER_STALLED"));
|
||||
}
|
||||
|
||||
private ResourceRestoreTaskEntity task(String taskId, Path statePath) {
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId(taskId);
|
||||
entity.setRunnerStatePath(statePath.toString());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.runner;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.init.config.InitExecutorProperties;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class ServiceCommandRunnerStepExecutorTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldInvokeStopCommandsInExpectedOrder() {
|
||||
RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker();
|
||||
ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor(
|
||||
backupProperties(),
|
||||
initProperties(),
|
||||
invoker
|
||||
);
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000001",
|
||||
"STOP_SERVICES",
|
||||
2,
|
||||
tempDir,
|
||||
tempDir.resolve("handoff.json"),
|
||||
tempDir.resolve("restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertEquals("executed 3 command(s)", result.getSummary());
|
||||
Assertions.assertEquals(List.of(
|
||||
List.of("bash", "/mock/scripts/tms.sh", "stop"),
|
||||
List.of("/bin/zsh", "-lc", "/mock/bin/stop_standard_apps.sh"),
|
||||
List.of("/bin/zsh", "-lc", "/mock/bin/stop_standard_rabbitmq.sh")
|
||||
), invoker.commands);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInvokeStartCommandsInExpectedOrder() {
|
||||
RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker();
|
||||
ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor(
|
||||
backupProperties(),
|
||||
initProperties(),
|
||||
invoker
|
||||
);
|
||||
|
||||
RunnerStepExecutionResult result = executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000001",
|
||||
"START_SERVICES",
|
||||
3,
|
||||
tempDir,
|
||||
tempDir.resolve("handoff.json"),
|
||||
tempDir.resolve("restore-state.json")
|
||||
));
|
||||
|
||||
Assertions.assertEquals("SUCCESS", result.getResult());
|
||||
Assertions.assertEquals("executed 2 command(s)", result.getSummary());
|
||||
Assertions.assertEquals(List.of(
|
||||
List.of("/bin/zsh", "-lc", "/mock/bin/start_standard_apps.sh"),
|
||||
List.of("bash", "/mock/scripts/tms.sh", "start")
|
||||
), invoker.commands);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUnsupportedStep() {
|
||||
RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker();
|
||||
ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor(
|
||||
backupProperties(),
|
||||
initProperties(),
|
||||
invoker
|
||||
);
|
||||
|
||||
IllegalArgumentException exception = Assertions.assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000001",
|
||||
"RESTORE_DATABASES",
|
||||
4,
|
||||
tempDir,
|
||||
tempDir.resolve("handoff.json"),
|
||||
tempDir.resolve("restore-state.json")
|
||||
))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("unsupported runner step"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSurfaceCommandFailure() {
|
||||
RecordingRunnerCommandInvoker invoker = new RecordingRunnerCommandInvoker();
|
||||
invoker.exitCodes.add(1);
|
||||
ServiceCommandRunnerStepExecutor executor = new ServiceCommandRunnerStepExecutor(
|
||||
backupProperties(),
|
||||
initProperties(),
|
||||
invoker
|
||||
);
|
||||
|
||||
IllegalStateException exception = Assertions.assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> executor.execute(new RunnerStepContext(
|
||||
"RRST-TASK-20260414-000001",
|
||||
"STOP_SERVICES",
|
||||
2,
|
||||
tempDir,
|
||||
tempDir.resolve("handoff.json"),
|
||||
tempDir.resolve("restore-state.json")
|
||||
))
|
||||
);
|
||||
Assertions.assertTrue(exception.getMessage().contains("exitCode=1"));
|
||||
}
|
||||
|
||||
private ResourceBackupProperties backupProperties() {
|
||||
ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
properties.setTmsScriptPath("/mock/scripts/tms.sh");
|
||||
return properties;
|
||||
}
|
||||
|
||||
private InitExecutorProperties initProperties() {
|
||||
InitExecutorProperties properties = new InitExecutorProperties();
|
||||
properties.setStandardAppStartCommand("/mock/bin/start_standard_apps.sh");
|
||||
properties.setStandardAppStopCommand("/mock/bin/stop_standard_apps.sh");
|
||||
properties.setStandardRabbitStopCommand("/mock/bin/stop_standard_rabbitmq.sh");
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static class RecordingRunnerCommandInvoker implements RunnerCommandInvoker {
|
||||
|
||||
private final List<List<String>> commands = new ArrayList<>();
|
||||
private final List<Integer> exitCodes = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public int run(List<String> command, Path logFile, Path workingDirectory) {
|
||||
commands.add(List.copyOf(command));
|
||||
if (exitCodes.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return exitCodes.remove(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageInspectResult;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourcePrecheckServiceImpl;
|
||||
import com.cisd.tms.modules.backup.support.InMemoryResourcePrecheckStateStore;
|
||||
import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourcePrecheckLightweightServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldExposeLightweightRestorePlanWithoutDecryptingPayload() {
|
||||
FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
ResourcePackageService packageService = Mockito.mock(ResourcePackageService.class);
|
||||
ResourceBackupProperties backupProperties = new ResourceBackupProperties();
|
||||
backupProperties.setStorageEncKeyIndex(7);
|
||||
backupProperties.setAuthenticitySignKeyIndex(9);
|
||||
CisdPresetProperties presetProperties = new CisdPresetProperties();
|
||||
presetProperties.setProductType("ENTERPRISE");
|
||||
InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore();
|
||||
Path packagePath = tempDir.resolve("resource.tmsbak");
|
||||
|
||||
FileRecordEntity fileRecord = new FileRecordEntity();
|
||||
fileRecord.setFileId("FILE-20260429-000001");
|
||||
fileRecord.setStoragePath(packagePath.toString());
|
||||
Mockito.when(fileRecordRepository.findByFileId("FILE-20260429-000001"))
|
||||
.thenReturn(Optional.of(fileRecord));
|
||||
Mockito.when(packageService.inspectPackageHeader(packagePath)).thenReturn(inspectResult());
|
||||
|
||||
ResourcePrecheckService service = new ResourcePrecheckServiceImpl(
|
||||
fileRecordRepository,
|
||||
packageService,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
presetProperties,
|
||||
backupProperties
|
||||
);
|
||||
ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest();
|
||||
request.setFileId("FILE-20260429-000001");
|
||||
|
||||
ResourceRestorePrecheckResponse response = service.precheck(request);
|
||||
|
||||
Assertions.assertEquals(List.of("JAVA_EXTRACT", "SHELL_APPLY"), response.getRestorePlan());
|
||||
Assertions.assertFalse(response.getPayloadDecrypted());
|
||||
Assertions.assertTrue(response.getSignatureValid());
|
||||
Assertions.assertTrue(store.find(response.getPrecheckId()).isPresent());
|
||||
}
|
||||
|
||||
private static ResourcePackageInspectResult inspectResult() {
|
||||
Map<String, Object> manifest = new LinkedHashMap<>();
|
||||
manifest.put("backupId", "RBKP-20260429-000001");
|
||||
manifest.put("productType", "ENTERPRISE");
|
||||
manifest.put("mqType", "RABBITMQ");
|
||||
manifest.put("orgCode", "AAAABBBBXXX");
|
||||
manifest.put("backupTime", "2026-04-29T10:00:00");
|
||||
manifest.put("resourceCount", 3);
|
||||
manifest.put("storageEncKeyFingerprint", "USER_KEY_ENC:7");
|
||||
manifest.put("authenticityKeyFingerprint", "USER_KEY_SIGN:9");
|
||||
ResourcePackageInspectResult result = new ResourcePackageInspectResult();
|
||||
result.setManifest(manifest);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+99
-25
@@ -1,24 +1,24 @@
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
import com.cisd.tms.common.exception.BizException;
|
||||
import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.runner.ResourceRestoreRunnerLauncher;
|
||||
import com.cisd.tms.modules.backup.packagex.ResourcePayloadExtractor;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.LightweightRestoreApplierLauncher;
|
||||
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 java.time.LocalDateTime;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -31,15 +31,15 @@ class ResourceRestoreHandoffServiceTest {
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldCreateRestoreTaskWriteHandoffFilesAndLaunchRunner() throws Exception {
|
||||
void shouldCreateRestoreTaskExtractPayloadAndLaunchLightweightApplier() 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();
|
||||
ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class);
|
||||
LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class);
|
||||
Path packagePath = tempDir.resolve("download").resolve("RBKP-20260413-153000-000001.tmsbak");
|
||||
Files.createDirectories(packagePath.getParent());
|
||||
Files.writeString(packagePath, "package-bytes");
|
||||
@@ -52,7 +52,8 @@ class ResourceRestoreHandoffServiceTest {
|
||||
precheckResponse.setKeysetMatched(true);
|
||||
precheckResponse.setSignatureValid(true);
|
||||
precheckResponse.setCompatible(true);
|
||||
precheckResponse.setRestorePlan(java.util.List.of("RESTORE_LOCK", "STOP_SERVICES", "RESTORE_DATABASES", "START_SERVICES"));
|
||||
precheckResponse.setExpiresAt(LocalDateTime.now().plusMinutes(10).toString());
|
||||
precheckResponse.setRestorePlan(java.util.List.of("JAVA_EXTRACT", "SHELL_APPLY"));
|
||||
store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse));
|
||||
FileRecordEntity fileRecordEntity = new FileRecordEntity();
|
||||
fileRecordEntity.setFileId("FILE-20260413-000001");
|
||||
@@ -65,10 +66,9 @@ class ResourceRestoreHandoffServiceTest {
|
||||
fileRecordRepository,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
new HandoffFileStore(objectMapper),
|
||||
new ResourceRestoreStateFileStore(objectMapper),
|
||||
properties,
|
||||
runnerLauncher
|
||||
payloadExtractor,
|
||||
applierLauncher
|
||||
);
|
||||
|
||||
CreateResourceRestoreRequest request = new CreateResourceRestoreRequest();
|
||||
@@ -79,25 +79,99 @@ class ResourceRestoreHandoffServiceTest {
|
||||
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());
|
||||
Assertions.assertEquals("APPLYING", saved.getStatus());
|
||||
Assertions.assertEquals(taskDir.resolve("payload").toString(), saved.getStagingDir());
|
||||
Assertions.assertEquals(taskDir.resolve("status.json").toString(), saved.getRunnerStatePath());
|
||||
Assertions.assertEquals("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]", saved.getRestorePlanJson());
|
||||
|
||||
Mockito.verify(runnerLauncher).launch(handoffFile);
|
||||
Mockito.verify(payloadExtractor).extract(packagePath, taskDir);
|
||||
Mockito.verify(applierLauncher).launch(taskDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectIncompatiblePrecheckWhenCreatingRestoreTask() {
|
||||
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);
|
||||
ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class);
|
||||
LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class);
|
||||
ResourceRestorePrecheckResponse precheckResponse = validPrecheckResponse();
|
||||
precheckResponse.setCompatible(false);
|
||||
store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse));
|
||||
ResourceRestoreService service = new ResourceRestoreServiceImpl(
|
||||
repository,
|
||||
fileRecordRepository,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
properties,
|
||||
payloadExtractor,
|
||||
applierLauncher
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class,
|
||||
() -> service.createRestoreTask(validCreateRequest()));
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("not compatible"));
|
||||
Mockito.verifyNoInteractions(repository, fileRecordRepository, payloadExtractor, applierLauncher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectExpiredPrecheckWhenCreatingRestoreTask() {
|
||||
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);
|
||||
ResourcePayloadExtractor payloadExtractor = Mockito.mock(ResourcePayloadExtractor.class);
|
||||
LightweightRestoreApplierLauncher applierLauncher = Mockito.mock(LightweightRestoreApplierLauncher.class);
|
||||
ResourceRestorePrecheckResponse precheckResponse = validPrecheckResponse();
|
||||
precheckResponse.setExpiresAt(LocalDateTime.now().minusMinutes(1).toString());
|
||||
store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse));
|
||||
ResourceRestoreService service = new ResourceRestoreServiceImpl(
|
||||
repository,
|
||||
fileRecordRepository,
|
||||
store,
|
||||
new ResourceTaskIdGenerator(),
|
||||
properties,
|
||||
payloadExtractor,
|
||||
applierLauncher
|
||||
);
|
||||
|
||||
BizException exception = Assertions.assertThrows(BizException.class,
|
||||
() -> service.createRestoreTask(validCreateRequest()));
|
||||
|
||||
Assertions.assertTrue(exception.getMessage().contains("expired"));
|
||||
Mockito.verifyNoInteractions(repository, fileRecordRepository, payloadExtractor, applierLauncher);
|
||||
}
|
||||
|
||||
private static ResourceRestorePrecheckResponse validPrecheckResponse() {
|
||||
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.setExpiresAt(LocalDateTime.now().plusMinutes(10).toString());
|
||||
precheckResponse.setRestorePlan(java.util.List.of("JAVA_EXTRACT", "SHELL_APPLY"));
|
||||
return precheckResponse;
|
||||
}
|
||||
|
||||
private static CreateResourceRestoreRequest validCreateRequest() {
|
||||
CreateResourceRestoreRequest request = new CreateResourceRestoreRequest();
|
||||
request.setPrecheckId("RPRE-20260413-000001");
|
||||
request.setConfirmBackupId("RBKP-20260413-153000-000001");
|
||||
request.setConfirmRiskAccepted(true);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
-241
@@ -1,241 +0,0 @@
|
||||
//package com.cisd.tms.modules.backup.service;
|
||||
//
|
||||
//import com.cisd.tms.common.config.properties.CisdPresetProperties;
|
||||
//import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
//import com.cisd.tms.integration.crypto.pcie.model.EccInternalEncryptRequest;
|
||||
//import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
//import com.cisd.tms.modules.backup.config.ResourceBackupProperties;
|
||||
//import com.cisd.tms.modules.backup.crypto.ResourceEnvelopeCryptoServiceImpl;
|
||||
//import com.cisd.tms.modules.backup.dto.request.CreateResourceRestoreRequest;
|
||||
//import com.cisd.tms.modules.backup.dto.request.ResourceRestorePrecheckRequest;
|
||||
//import com.cisd.tms.modules.backup.dto.response.CreateResourceRestoreResponse;
|
||||
//import com.cisd.tms.modules.backup.dto.response.ResourceRestorePrecheckResponse;
|
||||
//import com.cisd.tms.modules.backup.dto.support.DiscoveredResourceItem;
|
||||
//import com.cisd.tms.modules.backup.dto.response.ResourceRestoreTaskDetailResponse;
|
||||
//import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
//import com.cisd.tms.modules.backup.packagex.ResourcePackageResult;
|
||||
//import com.cisd.tms.modules.backup.packagex.ResourcePackageService;
|
||||
//import com.cisd.tms.modules.backup.packagex.ResourcePackageServiceImpl;
|
||||
//import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
//import com.cisd.tms.modules.backup.service.impl.ResourcePrecheckServiceImpl;
|
||||
//import com.cisd.tms.modules.backup.service.impl.ResourceRestoreServiceImpl;
|
||||
//import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl;
|
||||
//import com.cisd.tms.modules.backup.support.InMemoryResourcePrecheckStateStore;
|
||||
//import com.cisd.tms.modules.backup.support.ResourcePrecheckContext;
|
||||
//import com.cisd.tms.modules.backup.support.ResourceTaskIdGenerator;
|
||||
//import com.cisd.tms.modules.backup.support.file.HandoffFileStore;
|
||||
//import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
//import com.cisd.tms.modules.file.entity.FileRecordEntity;
|
||||
//import com.cisd.tms.modules.file.repository.FileRecordRepository;
|
||||
//import com.cisd.tms.modules.init.entity.InitTaskEntity;
|
||||
//import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
//import java.nio.file.Files;
|
||||
//import java.nio.file.Path;
|
||||
//import java.util.Optional;
|
||||
//import org.junit.jupiter.api.Assertions;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.junit.jupiter.api.io.TempDir;
|
||||
//import org.mockito.ArgumentCaptor;
|
||||
//import org.mockito.Mockito;
|
||||
//
|
||||
//class ResourceRestoreSupportServiceTest {
|
||||
//
|
||||
// @TempDir
|
||||
// Path tempDir;
|
||||
//
|
||||
// @Test
|
||||
// void shouldParseRealBackupPackageByFileIdAndCachePrecheckContext() {
|
||||
// InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore();
|
||||
// FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
// CisdPresetProperties presetProperties = new CisdPresetProperties();
|
||||
// presetProperties.setProductType("ENTERPRISE");
|
||||
// ResourceBackupProperties backupProperties = new ResourceBackupProperties();
|
||||
// backupProperties.setOutputDir(tempDir.toString());
|
||||
// Path payloadFile = writePayloadFile("runtime/application.yml", "server.port=9090");
|
||||
// PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
// Mockito.when(pcieCryptoService.sdfeInternalEncryptEcc(Mockito.any(EccInternalEncryptRequest.class)))
|
||||
// .thenAnswer(invocation -> {
|
||||
// EccInternalEncryptRequest request = invocation.getArgument(0);
|
||||
// return backup(request.getData());
|
||||
// });
|
||||
// Mockito.when(pcieCryptoService.eccInternalSign(Mockito.any()))
|
||||
// .thenReturn(backup(new byte[]{0x21, 0x22, 0x23}));
|
||||
// Mockito.when(pcieCryptoService.sdfeInternalDecryptEcc(Mockito.any()))
|
||||
// .thenAnswer(invocation -> {
|
||||
// com.cisd.tms.integration.crypto.pcie.model.EccInternalDecryptRequest request = invocation.getArgument(0);
|
||||
// return backup(request.getCipherBlob());
|
||||
// });
|
||||
//
|
||||
// ResourcePackageService packageService = new ResourcePackageServiceImpl(
|
||||
// backupProperties,
|
||||
// new ResourceEnvelopeCryptoServiceImpl(pcieCryptoService, backupProperties),
|
||||
// new ObjectMapper()
|
||||
// );
|
||||
// ResourcePackageResult packageResult = packageService.createPackage(
|
||||
// "RBKP-TASK-20260413-000001",
|
||||
// "RBKP-20260413-153000-000001",
|
||||
// sampleInitTask(),
|
||||
// java.util.List.of(resource("TMS_CONFIG", payloadFile.toString())),
|
||||
// new com.cisd.tms.modules.backup.dto.request.CreateResourceBackupRequest()
|
||||
// );
|
||||
//
|
||||
// FileRecordEntity fileRecordEntity = new FileRecordEntity();
|
||||
// fileRecordEntity.setFileId("FILE-20260413-000001");
|
||||
// fileRecordEntity.setStoragePath(packageResult.getPackagePath());
|
||||
// Mockito.when(fileRecordRepository.findByFileId("FILE-20260413-000001")).thenReturn(Optional.of(fileRecordEntity));
|
||||
//
|
||||
// ResourcePrecheckService service = new ResourcePrecheckServiceImpl(
|
||||
// fileRecordRepository,
|
||||
// packageService,
|
||||
// store,
|
||||
// new ResourceTaskIdGenerator(),
|
||||
// presetProperties,
|
||||
// backupProperties
|
||||
// );
|
||||
//
|
||||
// ResourceRestorePrecheckRequest request = new ResourceRestorePrecheckRequest();
|
||||
// request.setFileId("FILE-20260413-000001");
|
||||
//
|
||||
// ResourceRestorePrecheckResponse response = service.precheck(request);
|
||||
//
|
||||
// Assertions.assertTrue(response.getPrecheckId().startsWith("RPRE-"));
|
||||
// Assertions.assertEquals("RBKP-20260413-153000-000001", response.getBackupId());
|
||||
// Assertions.assertEquals("ENTERPRISE", response.getSourceProductType());
|
||||
// Assertions.assertEquals("RABBITMQ", response.getSourceMqType());
|
||||
// Assertions.assertEquals("AAAABBBBXXX", response.getSourceOrgCode());
|
||||
// Assertions.assertEquals("ENTERPRISE", response.getTargetProductType());
|
||||
// Assertions.assertTrue(response.getKeysetMatched());
|
||||
// Assertions.assertTrue(response.getSignatureValid());
|
||||
// Assertions.assertTrue(response.getCompatible());
|
||||
// Assertions.assertFalse(response.getPayloadDecrypted());
|
||||
// Assertions.assertEquals("RESTORE_LOCK", response.getRestorePlan().get(0));
|
||||
//
|
||||
// ResourcePrecheckContext context = store.find(response.getPrecheckId()).orElseThrow();
|
||||
// Assertions.assertEquals("FILE-20260413-000001", context.getFileId());
|
||||
// Assertions.assertEquals(response.getBackupId(), context.getResponse().getBackupId());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void shouldCreateAndQueryRestoreTaskFromCachedPrecheck() {
|
||||
// InMemoryResourcePrecheckStateStore store = new InMemoryResourcePrecheckStateStore();
|
||||
// ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
// FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class);
|
||||
// Path packagePath = tempDir.resolve("download").resolve("RBKP-20260413-153000-000001.tmsbak");
|
||||
// try {
|
||||
// Files.createDirectories(packagePath.getParent());
|
||||
// Files.writeString(packagePath, "backup-package");
|
||||
// } catch (Exception ex) {
|
||||
// throw new IllegalStateException(ex);
|
||||
// }
|
||||
//
|
||||
// ResourceRestorePrecheckResponse precheckResponse = new ResourceRestorePrecheckResponse();
|
||||
// precheckResponse.setPrecheckId("RPRE-20260413-000001");
|
||||
// precheckResponse.setBackupId("RBKP-20260413-153000-000001");
|
||||
// precheckResponse.setSourceProductType("ENTERPRISE");
|
||||
// precheckResponse.setTargetProductType("ENTERPRISE");
|
||||
// precheckResponse.setKeysetMatched(true);
|
||||
// precheckResponse.setSignatureValid(true);
|
||||
// precheckResponse.setCompatible(true);
|
||||
// store.save(new ResourcePrecheckContext("FILE-20260413-000001", precheckResponse));
|
||||
// FileRecordEntity fileRecordEntity = new FileRecordEntity();
|
||||
// fileRecordEntity.setFileId("FILE-20260413-000001");
|
||||
// fileRecordEntity.setStoragePath(packagePath.toString());
|
||||
// Mockito.when(fileRecordRepository.findByFileId("FILE-20260413-000001"))
|
||||
// .thenReturn(Optional.of(fileRecordEntity));
|
||||
//
|
||||
// ResourceRestoreService restoreService = new ResourceRestoreServiceImpl(
|
||||
// repository,
|
||||
// fileRecordRepository,
|
||||
// store,
|
||||
// new ResourceTaskIdGenerator(),
|
||||
// new HandoffFileStore(new ObjectMapper()),
|
||||
// new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
// restoreProperties()
|
||||
// );
|
||||
//
|
||||
// CreateResourceRestoreRequest request = new CreateResourceRestoreRequest();
|
||||
// request.setPrecheckId("RPRE-20260413-000001");
|
||||
// request.setConfirmBackupId("RBKP-20260413-153000-000001");
|
||||
// request.setConfirmRiskAccepted(true);
|
||||
//
|
||||
// CreateResourceRestoreResponse response = restoreService.createRestoreTask(request);
|
||||
//
|
||||
// Assertions.assertTrue(response.getTaskId().startsWith("RRST-TASK-"));
|
||||
// Assertions.assertEquals("HANDOFF", response.getStatus());
|
||||
//
|
||||
// ArgumentCaptor<ResourceRestoreTaskEntity> captor = ArgumentCaptor.forClass(ResourceRestoreTaskEntity.class);
|
||||
// Mockito.verify(repository).save(captor.capture());
|
||||
// ResourceRestoreTaskEntity saved = captor.getValue();
|
||||
// Assertions.assertEquals(response.getTaskId(), saved.getTaskId());
|
||||
// Assertions.assertEquals("RPRE-20260413-000001", saved.getPrecheckId());
|
||||
// Assertions.assertEquals("RBKP-20260413-153000-000001", saved.getBackupId());
|
||||
// Assertions.assertEquals("FILE-20260413-000001", saved.getPackageFileId());
|
||||
// Assertions.assertEquals(packagePath.toString(), saved.getPackagePath());
|
||||
// Assertions.assertEquals("ENTERPRISE", saved.getSourceProductType());
|
||||
// Assertions.assertEquals("ENTERPRISE", saved.getTargetProductType());
|
||||
// Assertions.assertTrue(saved.getStagingDir().contains(response.getTaskId()));
|
||||
// Assertions.assertTrue(saved.getRunnerStatePath().contains(response.getTaskId()));
|
||||
//
|
||||
// Mockito.when(repository.findByTaskId(response.getTaskId())).thenReturn(Optional.of(saved));
|
||||
// ResourceTaskQueryService queryService = new ResourceTaskQueryServiceImpl(
|
||||
// repository,
|
||||
// new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
// new ObjectMapper()
|
||||
// );
|
||||
//
|
||||
// ResourceRestoreTaskDetailResponse detail = queryService.getRestoreTask(response.getTaskId());
|
||||
//
|
||||
// Assertions.assertEquals(response.getTaskId(), detail.getTaskId());
|
||||
// Assertions.assertEquals("HANDOFF", detail.getStatus());
|
||||
// Assertions.assertEquals("DB", detail.getStateSource());
|
||||
// Assertions.assertEquals("HANDOFF_READY", detail.getControlPhase());
|
||||
// Assertions.assertEquals("FILE-20260413-000001", detail.getPackageFileId());
|
||||
// Assertions.assertEquals("RESTORE_LOCK", detail.getCurrentStepCode());
|
||||
// Assertions.assertEquals(1, detail.getCurrentStepNo());
|
||||
// }
|
||||
//
|
||||
// private static InitTaskEntity sampleInitTask() {
|
||||
// InitTaskEntity entity = new InitTaskEntity();
|
||||
// entity.setTaskId("INIT-20260408-000001");
|
||||
// entity.setTaskType("INIT");
|
||||
// entity.setStatus("SUCCESS");
|
||||
// entity.setProductType("ENTERPRISE");
|
||||
// entity.setMqType("RABBITMQ");
|
||||
// entity.setOrgCode("AAAABBBBXXX");
|
||||
// entity.setDeployMode("SINGLE");
|
||||
// return entity;
|
||||
// }
|
||||
//
|
||||
// private static DiscoveredResourceItem resource(String code, String path) {
|
||||
// DiscoveredResourceItem item = new DiscoveredResourceItem();
|
||||
// item.setResourceType("FILE");
|
||||
// item.setResourceCode(code);
|
||||
// item.setResolvedPath(path);
|
||||
// item.setRequired(true);
|
||||
// return item;
|
||||
// }
|
||||
//
|
||||
// private Path writePayloadFile(String relativePath, String content) {
|
||||
// try {
|
||||
// Path file = tempDir.resolve(relativePath);
|
||||
// Files.createDirectories(file.getParent());
|
||||
// Files.writeString(file, content);
|
||||
// return file;
|
||||
// } catch (Exception ex) {
|
||||
// throw new IllegalStateException(ex);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private static BackupDataResult backup(byte[] data) {
|
||||
// BackupDataResult result = new BackupDataResult();
|
||||
// result.setData(data);
|
||||
// result.setLength(data.length);
|
||||
// return result;
|
||||
// }
|
||||
//
|
||||
// private ResourceBackupProperties restoreProperties() {
|
||||
// ResourceBackupProperties properties = new ResourceBackupProperties();
|
||||
// properties.setRestoreTaskRootDir(tempDir.resolve("resource-restore").toString());
|
||||
// return properties;
|
||||
// }
|
||||
//}
|
||||
+111
-158
@@ -1,158 +1,111 @@
|
||||
//package com.cisd.tms.modules.backup.service;
|
||||
//
|
||||
//import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
//import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
//import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
//import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
//import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl;
|
||||
//import com.cisd.tms.modules.backup.support.file.ResourceRestoreStateFileStore;
|
||||
//import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
//import java.nio.file.Files;
|
||||
//import java.nio.file.Path;
|
||||
//import java.time.LocalDateTime;
|
||||
//import java.util.List;
|
||||
//import java.util.Optional;
|
||||
//import org.junit.jupiter.api.Assertions;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.junit.jupiter.api.io.TempDir;
|
||||
//import org.mockito.Mockito;
|
||||
//
|
||||
//class ResourceTaskQueryServiceTest {
|
||||
//
|
||||
// @TempDir
|
||||
// Path tempDir;
|
||||
//
|
||||
// @Test
|
||||
// void shouldPreferRunnerStateFileWhenPresent() throws Exception {
|
||||
// Path taskDir = tempDir.resolve("RRST-TASK-20260413-000001");
|
||||
// Files.createDirectories(taskDir);
|
||||
// Path stateFile = taskDir.resolve("restore-state.json");
|
||||
// Files.writeString(stateFile, """
|
||||
// {
|
||||
// "taskId":"RRST-TASK-20260413-000001",
|
||||
// "status":"RUNNING",
|
||||
// "controlPhase":"RUNNER_OWNED",
|
||||
// "stateSource":"RUNNER_FILE",
|
||||
// "runnerInstanceId":"RRUN-20260413-000001",
|
||||
// "runnerPid":1234,
|
||||
// "ownershipToken":"TOKEN-001",
|
||||
// "lastHeartbeatAt":"2026-04-13T17:10:00",
|
||||
// "orphaned":false,
|
||||
// "cleanupWarnings":["cleanup skipped"]
|
||||
// }
|
||||
// """);
|
||||
//
|
||||
// ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
// entity.setTaskId("RRST-TASK-20260413-000001");
|
||||
// entity.setPrecheckId("RPRE-20260413-000001");
|
||||
// entity.setBackupId("RBKP-20260413-153000-000001");
|
||||
// entity.setStatus("PENDING");
|
||||
// entity.setSourceProductType("ENTERPRISE");
|
||||
// entity.setTargetProductType("ENTERPRISE");
|
||||
// entity.setPackageFileId("FILE-20260413-000001");
|
||||
// entity.setRunnerStatePath(stateFile.toString());
|
||||
// entity.setCreateBy("SYSTEM");
|
||||
// entity.setCreateTime(LocalDateTime.parse("2026-04-13T17:00:00"));
|
||||
//
|
||||
// ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
// Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000001")).thenReturn(Optional.of(entity));
|
||||
//
|
||||
// ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
// repository,
|
||||
// new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
// new ObjectMapper()
|
||||
// );
|
||||
//
|
||||
// var detail = service.getRestoreTask("RRST-TASK-20260413-000001");
|
||||
//
|
||||
// Assertions.assertEquals("RUNNING", detail.getStatus());
|
||||
// Assertions.assertEquals("RUNNER_OWNED", detail.getControlPhase());
|
||||
// Assertions.assertEquals("RUNNER_FILE", detail.getStateSource());
|
||||
// Assertions.assertEquals("2026-04-13T17:10:00", detail.getLastHeartbeatAt());
|
||||
// Assertions.assertEquals(Boolean.FALSE, detail.getOrphaned());
|
||||
// Assertions.assertEquals(1, detail.getCleanupWarnings().size());
|
||||
// Assertions.assertEquals("cleanup skipped", detail.getCleanupWarnings().get(0));
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void shouldFallbackToDatabaseStateWhenRunnerStateFileMissing() {
|
||||
// ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
// entity.setTaskId("RRST-TASK-20260413-000002");
|
||||
// entity.setPrecheckId("RPRE-20260413-000002");
|
||||
// entity.setBackupId("RBKP-20260413-153500-000002");
|
||||
// entity.setStatus("PENDING");
|
||||
// entity.setSourceProductType("ENTERPRISE");
|
||||
// entity.setTargetProductType("ENTERPRISE");
|
||||
// entity.setPackageFileId("FILE-20260413-000002");
|
||||
// entity.setRunnerStatePath(tempDir.resolve("missing.json").toString());
|
||||
//
|
||||
// ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
// Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000002")).thenReturn(Optional.of(entity));
|
||||
//
|
||||
// ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
// repository,
|
||||
// new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
// new ObjectMapper()
|
||||
// );
|
||||
//
|
||||
// var detail = service.getRestoreTask("RRST-TASK-20260413-000002");
|
||||
//
|
||||
// Assertions.assertEquals("PENDING", detail.getStatus());
|
||||
// Assertions.assertEquals("PRECHECKED", detail.getControlPhase());
|
||||
// Assertions.assertEquals("DB", detail.getStateSource());
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void shouldExposeRestoreStepsAndLogsFromRunnerArtifacts() throws Exception {
|
||||
// Path taskDir = tempDir.resolve("RRST-TASK-20260413-000003");
|
||||
// Files.createDirectories(taskDir.resolve("step-output"));
|
||||
// Files.createDirectories(taskDir.resolve("steps"));
|
||||
// Path stateFile = taskDir.resolve("restore-state.json");
|
||||
// Files.writeString(stateFile, """
|
||||
// {
|
||||
// "taskId":"RRST-TASK-20260413-000003",
|
||||
// "status":"RUNNING",
|
||||
// "controlPhase":"RUNNER_OWNED",
|
||||
// "stateSource":"RUNNER_FILE",
|
||||
// "currentStepCode":"START_SERVICES",
|
||||
// "currentStepNo":3
|
||||
// }
|
||||
// """);
|
||||
// Files.writeString(taskDir.resolve("step-output/02-STOP_SERVICES.json"), """
|
||||
// {
|
||||
// "stepCode":"STOP_SERVICES",
|
||||
// "result":"SUCCESS",
|
||||
// "summary":"stopped services"
|
||||
// }
|
||||
// """);
|
||||
// Files.writeString(taskDir.resolve("steps/02-STOP_SERVICES.log"), "stop log line");
|
||||
//
|
||||
// ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
// entity.setTaskId("RRST-TASK-20260413-000003");
|
||||
// entity.setStatus("HANDOFF");
|
||||
// entity.setRestorePlanJson("[\"RESTORE_LOCK\",\"STOP_SERVICES\",\"START_SERVICES\"]");
|
||||
// entity.setRunnerStatePath(stateFile.toString());
|
||||
//
|
||||
// ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
// Mockito.when(repository.findByTaskId("RRST-TASK-20260413-000003")).thenReturn(Optional.of(entity));
|
||||
//
|
||||
// ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
// repository,
|
||||
// new ResourceRestoreStateFileStore(new ObjectMapper()),
|
||||
// new ObjectMapper()
|
||||
// );
|
||||
//
|
||||
// List<ResourceTaskStepItemResponse> steps = service.getRestoreTaskSteps("RRST-TASK-20260413-000003");
|
||||
// Assertions.assertEquals(3, steps.size());
|
||||
// Assertions.assertEquals("PENDING", steps.get(0).getStatus());
|
||||
// Assertions.assertEquals("SUCCESS", steps.get(1).getStatus());
|
||||
// Assertions.assertEquals("stopped services", steps.get(1).getMessage());
|
||||
// Assertions.assertEquals(Boolean.TRUE, steps.get(1).getLogAvailable());
|
||||
// Assertions.assertEquals("RUNNING", steps.get(2).getStatus());
|
||||
//
|
||||
// ResourceTaskStepLogResponse log = service.getRestoreTaskStepLog("RRST-TASK-20260413-000003", 2);
|
||||
// Assertions.assertEquals("STOP_SERVICES", log.getStepCode());
|
||||
// Assertions.assertEquals("stop log line", log.getContent());
|
||||
// Assertions.assertFalse(log.getTruncated());
|
||||
// }
|
||||
//}
|
||||
package com.cisd.tms.modules.backup.service;
|
||||
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepItemResponse;
|
||||
import com.cisd.tms.modules.backup.dto.response.ResourceTaskStepLogResponse;
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
import com.cisd.tms.modules.backup.repository.ResourceRestoreTaskRepository;
|
||||
import com.cisd.tms.modules.backup.restore.RestoreStatusFileReader;
|
||||
import com.cisd.tms.modules.backup.service.impl.ResourceTaskQueryServiceImpl;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
class ResourceTaskQueryServiceTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldPreferLightweightStatusFileWhenPresent() throws Exception {
|
||||
Path taskDir = tempDir.resolve("RRST-TASK-20260429-000001");
|
||||
Files.createDirectories(taskDir);
|
||||
Files.writeString(taskDir.resolve("status.json"), """
|
||||
{
|
||||
"status":"RUNNING",
|
||||
"phase":"MQ_REPLAY",
|
||||
"message":"replaying mq",
|
||||
"updatedAt":"2026-04-29T10:00:00+0800"
|
||||
}
|
||||
""");
|
||||
Files.writeString(taskDir.resolve("restore.log"), "line1\nline2\n");
|
||||
|
||||
ResourceRestoreTaskEntity entity = restoreTask(taskDir);
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260429-000001")).thenReturn(Optional.of(entity));
|
||||
|
||||
ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
repository,
|
||||
new ObjectMapper(),
|
||||
new RestoreStatusFileReader(new ObjectMapper())
|
||||
);
|
||||
|
||||
var detail = service.getRestoreTask("RRST-TASK-20260429-000001");
|
||||
|
||||
Assertions.assertEquals("RUNNING", detail.getStatus());
|
||||
Assertions.assertEquals("MQ_REPLAY", detail.getControlPhase());
|
||||
Assertions.assertEquals("SHELL_STATUS", detail.getStateSource());
|
||||
Assertions.assertEquals("MQ_REPLAY", detail.getCurrentStepCode());
|
||||
Assertions.assertEquals("2026-04-29T10:00:00+0800", detail.getLastHeartbeatAt());
|
||||
Assertions.assertEquals("replaying mq", detail.getErrorMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeLightweightStepsAndRestoreLog() throws Exception {
|
||||
Path taskDir = tempDir.resolve("RRST-TASK-20260429-000001");
|
||||
Files.createDirectories(taskDir);
|
||||
Files.writeString(taskDir.resolve("status.json"), """
|
||||
{
|
||||
"status":"RUNNING",
|
||||
"phase":"MQ_REPLAY",
|
||||
"message":"replaying mq",
|
||||
"updatedAt":"2026-04-29T10:00:00+0800"
|
||||
}
|
||||
""");
|
||||
Files.writeString(taskDir.resolve("restore.log"), "line1\nline2\n");
|
||||
|
||||
ResourceRestoreTaskEntity entity = restoreTask(taskDir);
|
||||
ResourceRestoreTaskRepository repository = Mockito.mock(ResourceRestoreTaskRepository.class);
|
||||
Mockito.when(repository.findByTaskId("RRST-TASK-20260429-000001")).thenReturn(Optional.of(entity));
|
||||
|
||||
ResourceTaskQueryService service = new ResourceTaskQueryServiceImpl(
|
||||
repository,
|
||||
new ObjectMapper(),
|
||||
new RestoreStatusFileReader(new ObjectMapper())
|
||||
);
|
||||
|
||||
List<ResourceTaskStepItemResponse> steps = service.getRestoreTaskSteps("RRST-TASK-20260429-000001");
|
||||
Assertions.assertEquals(2, steps.size());
|
||||
Assertions.assertEquals("SUCCESS", steps.get(0).getStatus());
|
||||
Assertions.assertEquals("RUNNING", steps.get(1).getStatus());
|
||||
Assertions.assertEquals("replaying mq", steps.get(1).getMessage());
|
||||
Assertions.assertEquals(Boolean.TRUE, steps.get(1).getLogAvailable());
|
||||
|
||||
ResourceTaskStepLogResponse log = service.getRestoreTaskStepLog("RRST-TASK-20260429-000001", 2);
|
||||
Assertions.assertEquals("SHELL_APPLY", log.getStepCode());
|
||||
Assertions.assertEquals("line1\nline2\n", log.getContent());
|
||||
Assertions.assertFalse(log.getTruncated());
|
||||
}
|
||||
|
||||
private static ResourceRestoreTaskEntity restoreTask(Path taskDir) {
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260429-000001");
|
||||
entity.setPrecheckId("RPRE-20260429-000001");
|
||||
entity.setBackupId("RBKP-20260429-153000-000001");
|
||||
entity.setStatus("APPLYING");
|
||||
entity.setSourceProductType("ENTERPRISE");
|
||||
entity.setTargetProductType("ENTERPRISE");
|
||||
entity.setPackageFileId("FILE-20260429-000001");
|
||||
entity.setRunnerStatePath(taskDir.resolve("status.json").toString());
|
||||
entity.setRestorePlanJson("[\"JAVA_EXTRACT\",\"SHELL_APPLY\"]");
|
||||
entity.setCreateBy("SYSTEM");
|
||||
entity.setCreateTime(LocalDateTime.parse("2026-04-29T09:59:00"));
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
package com.cisd.tms.modules.backup.support.file;
|
||||
|
||||
import com.cisd.tms.modules.backup.entity.ResourceRestoreTaskEntity;
|
||||
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;
|
||||
|
||||
class ResourceRestoreStateFileStoreTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void shouldPromoteStateToHandoffReady() throws Exception {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ResourceRestoreStateFileStore store = new ResourceRestoreStateFileStore(objectMapper);
|
||||
ResourceRestoreTaskEntity entity = new ResourceRestoreTaskEntity();
|
||||
entity.setTaskId("RRST-TASK-20260413-000001");
|
||||
entity.setStatus("PENDING");
|
||||
|
||||
store.initialize(tempDir, entity);
|
||||
store.markHandoffReady(tempDir.resolve("restore-state.json"), "RESTORE_LOCK", 1);
|
||||
|
||||
JsonNode state = objectMapper.readTree(Files.readString(tempDir.resolve("restore-state.json")));
|
||||
Assertions.assertEquals("HANDOFF", state.path("status").asText());
|
||||
Assertions.assertEquals("HANDOFF_READY", state.path("controlPhase").asText());
|
||||
Assertions.assertEquals("DB", state.path("stateSource").asText());
|
||||
Assertions.assertEquals("RESTORE_LOCK", state.path("currentStepCode").asText());
|
||||
Assertions.assertEquals(1, state.path("currentStepNo").asInt());
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
package com.cisd.tms.modules.cert.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey;
|
||||
import com.cisd.tms.integration.crypto.pcie.jna.EccSignature;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyPairRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.UserKeySignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.model.UserKeySm2SignRequest;
|
||||
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
|
||||
import com.cisd.tms.modules.cert.dto.EntityCreateRequest;
|
||||
import com.cisd.tms.modules.cert.dto.EntityGenP10Request;
|
||||
import com.cisd.tms.modules.cert.dto.EntityItemResponse;
|
||||
import com.cisd.tms.modules.cert.dto.EntityListRequest;
|
||||
import com.cisd.tms.modules.cert.dto.EntityUpdateRequest;
|
||||
import com.cisd.tms.modules.cert.dto.Subject;
|
||||
import com.cisd.tms.modules.cert.entity.KeyEntity;
|
||||
import com.cisd.tms.modules.cert.repository.CertificateRepository;
|
||||
@@ -26,14 +32,177 @@ import java.security.Security;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
|
||||
class EntityServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldListEntityWithUiFieldsAndStatusName() {
|
||||
KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class);
|
||||
CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class);
|
||||
EntityService service = new EntityService(
|
||||
keyEntityRepository,
|
||||
certificateRepository,
|
||||
Mockito.mock(PcieCryptoService.class),
|
||||
new ObjectMapper()
|
||||
);
|
||||
KeyEntity entity = new KeyEntity()
|
||||
.setOrgCode("00000001")
|
||||
.setOrgName("中国工商银行杭州分行")
|
||||
.setAlgoType("SM2")
|
||||
.setKeyLen(256)
|
||||
.setKeyIdx(2)
|
||||
.setStatus("CERT_IMPORTED")
|
||||
.setAllowIps("[]");
|
||||
entity.setId(10L);
|
||||
Page<KeyEntity> page = new Page<>(1, 10, 1);
|
||||
page.setRecords(List.of(entity));
|
||||
EntityListRequest request = new EntityListRequest();
|
||||
request.setOrgCode("00000001");
|
||||
request.setAlgoType("SM2");
|
||||
request.setStatus("CERT_IMPORTED");
|
||||
Mockito.when(keyEntityRepository.page(request)).thenReturn(page);
|
||||
|
||||
Page<EntityItemResponse> result = service.list(request);
|
||||
|
||||
assertEquals(1, result.getRecords().size());
|
||||
EntityItemResponse item = result.getRecords().get(0);
|
||||
assertEquals("00000001", item.getOrgCode());
|
||||
assertEquals("中国工商银行杭州分行", item.getOrgName());
|
||||
assertEquals("SM2", item.getAlgoType());
|
||||
assertEquals("CERT_IMPORTED", item.getStatus());
|
||||
assertEquals("已导入证书", item.getStatusName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateEntityWithOrgFieldsAndCreatedStatus() {
|
||||
KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class);
|
||||
CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class);
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
EntityService service = new EntityService(keyEntityRepository, certificateRepository, pcieCryptoService, new ObjectMapper());
|
||||
Mockito.when(keyEntityRepository.findUsedKeyIdx()).thenReturn(List.of());
|
||||
Mockito.when(keyEntityRepository.save(any())).thenAnswer(invocation -> {
|
||||
KeyEntity saved = invocation.getArgument(0);
|
||||
saved.setId(100L);
|
||||
return saved;
|
||||
});
|
||||
Mockito.when(pcieCryptoService.backupUserKey(Mockito.eq(2), Mockito.anyInt(), Mockito.anyInt()))
|
||||
.thenReturn(binaryResult(new byte[] {0x01, 0x02}));
|
||||
Mockito.when(pcieCryptoService.exportSignPublicKeyEcc(2))
|
||||
.thenReturn(binaryResult(new byte[] {0x03, 0x04}));
|
||||
|
||||
EntityCreateRequest request = new EntityCreateRequest();
|
||||
request.setOrgCode("00000001");
|
||||
request.setOrgName("中国工商银行杭州分行");
|
||||
request.setAlgoType("SM2");
|
||||
request.setAllowIps(List.of("127.0.0.1"));
|
||||
|
||||
service.create(request);
|
||||
|
||||
ArgumentCaptor<GenerateKeyPairRequest> generateCaptor = ArgumentCaptor.forClass(GenerateKeyPairRequest.class);
|
||||
Mockito.verify(pcieCryptoService).generateKeyPairEcc(generateCaptor.capture());
|
||||
assertEquals(256, generateCaptor.getValue().getKeyBits());
|
||||
assertEquals(2, generateCaptor.getValue().getKeyIndex());
|
||||
|
||||
ArgumentCaptor<KeyEntity> entityCaptor = ArgumentCaptor.forClass(KeyEntity.class);
|
||||
Mockito.verify(keyEntityRepository).save(entityCaptor.capture());
|
||||
KeyEntity saved = entityCaptor.getValue();
|
||||
assertEquals("00000001", saved.getOrgCode());
|
||||
assertEquals("中国工商银行杭州分行", saved.getOrgName());
|
||||
assertEquals("SM2", saved.getAlgoType());
|
||||
assertEquals("CREATED", saved.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUnsupportedEntityAlgoType() {
|
||||
EntityService service = new EntityService(
|
||||
Mockito.mock(KeyEntityRepository.class),
|
||||
Mockito.mock(CertificateRepository.class),
|
||||
Mockito.mock(PcieCryptoService.class),
|
||||
new ObjectMapper()
|
||||
);
|
||||
EntityCreateRequest request = new EntityCreateRequest();
|
||||
request.setOrgCode("00000001");
|
||||
request.setOrgName("中国工商银行杭州分行");
|
||||
request.setAlgoType("DSA");
|
||||
|
||||
assertThrows(RuntimeException.class, () -> service.create(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateRsaEntityWhenAlgoTypeIsRsa() {
|
||||
KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class);
|
||||
CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class);
|
||||
PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class);
|
||||
EntityService service = new EntityService(keyEntityRepository, certificateRepository, pcieCryptoService, new ObjectMapper());
|
||||
Mockito.when(keyEntityRepository.findUsedKeyIdx()).thenReturn(List.of());
|
||||
Mockito.when(keyEntityRepository.save(any())).thenAnswer(invocation -> {
|
||||
KeyEntity saved = invocation.getArgument(0);
|
||||
saved.setId(101L);
|
||||
return saved;
|
||||
});
|
||||
Mockito.when(pcieCryptoService.backupUserKey(Mockito.eq(2), Mockito.anyInt(), Mockito.anyInt()))
|
||||
.thenReturn(binaryResult(new byte[] {0x05, 0x06}));
|
||||
Mockito.when(pcieCryptoService.exportSignPublicKeyRsa(2))
|
||||
.thenReturn(binaryResult(new byte[] {0x07, 0x08}));
|
||||
|
||||
EntityCreateRequest request = new EntityCreateRequest();
|
||||
request.setOrgCode("00000002");
|
||||
request.setOrgName("中国工商银行上海分行");
|
||||
request.setAlgoType("RSA");
|
||||
|
||||
service.create(request);
|
||||
|
||||
ArgumentCaptor<GenerateKeyPairRequest> generateCaptor = ArgumentCaptor.forClass(GenerateKeyPairRequest.class);
|
||||
Mockito.verify(pcieCryptoService).generateKeyPairRsa(generateCaptor.capture());
|
||||
assertEquals(2048, generateCaptor.getValue().getKeyBits());
|
||||
assertEquals(2, generateCaptor.getValue().getKeyIndex());
|
||||
|
||||
ArgumentCaptor<KeyEntity> entityCaptor = ArgumentCaptor.forClass(KeyEntity.class);
|
||||
Mockito.verify(keyEntityRepository).save(entityCaptor.capture());
|
||||
assertEquals("RSA", entityCaptor.getValue().getAlgoType());
|
||||
assertEquals(2048, entityCaptor.getValue().getKeyLen());
|
||||
assertEquals("CREATED", entityCaptor.getValue().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateEntityOrgFields() {
|
||||
KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class);
|
||||
CertificateRepository certificateRepository = Mockito.mock(CertificateRepository.class);
|
||||
EntityService service = new EntityService(
|
||||
keyEntityRepository,
|
||||
certificateRepository,
|
||||
Mockito.mock(PcieCryptoService.class),
|
||||
new ObjectMapper()
|
||||
);
|
||||
KeyEntity entity = new KeyEntity();
|
||||
entity.setId(10L);
|
||||
entity.setOrgCode("00000001");
|
||||
entity.setOrgName("旧机构");
|
||||
Mockito.when(keyEntityRepository.findById(10L)).thenReturn(Optional.of(entity));
|
||||
Mockito.when(keyEntityRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
EntityUpdateRequest request = new EntityUpdateRequest();
|
||||
request.setId(10L);
|
||||
request.setOrgCode("00000002");
|
||||
request.setOrgName("中国工商银行上海分行");
|
||||
request.setAllowIps(List.of("10.0.0.1"));
|
||||
|
||||
service.update(request);
|
||||
|
||||
ArgumentCaptor<KeyEntity> entityCaptor = ArgumentCaptor.forClass(KeyEntity.class);
|
||||
Mockito.verify(keyEntityRepository).save(entityCaptor.capture());
|
||||
assertEquals("00000002", entityCaptor.getValue().getOrgCode());
|
||||
assertEquals("中国工商银行上海分行", entityCaptor.getValue().getOrgName());
|
||||
assertEquals("[\"10.0.0.1\"]", entityCaptor.getValue().getAllowIps());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGenerateP10WithStandardSm2Sm3Signer() throws Exception {
|
||||
KeyEntityRepository keyEntityRepository = Mockito.mock(KeyEntityRepository.class);
|
||||
@@ -52,6 +221,11 @@ class EntityServiceTest {
|
||||
Mockito.verify(pcieCryptoService, Mockito.never()).userKeySignWithSm3(any(UserKeySignRequest.class));
|
||||
assertEquals(5, sm2Captor.getValue().getKeyIndex());
|
||||
assertEquals("1234567812345678", new String(sm2Captor.getValue().getUserId()));
|
||||
|
||||
ArgumentCaptor<KeyEntity> entityCaptor = ArgumentCaptor.forClass(KeyEntity.class);
|
||||
Mockito.verify(keyEntityRepository).save(entityCaptor.capture());
|
||||
assertEquals("SINGLE", entityCaptor.getValue().getCertMode());
|
||||
assertEquals("P10_GENERATED", entityCaptor.getValue().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,6 +256,7 @@ class EntityServiceTest {
|
||||
subject.setCountry("CN");
|
||||
EntityGenP10Request request = new EntityGenP10Request();
|
||||
request.setId(10L);
|
||||
request.setCertMode("SINGLE");
|
||||
request.setSubject(subject);
|
||||
return request;
|
||||
}
|
||||
@@ -90,10 +265,19 @@ class EntityServiceTest {
|
||||
KeyEntity entity = new KeyEntity();
|
||||
entity.setId(id);
|
||||
entity.setKeyIdx(keyIdx);
|
||||
entity.setAlgoType("SM2");
|
||||
entity.setStatus("CREATED");
|
||||
entity.setPublicKey(Base64.getEncoder().encodeToString(gm0018EccRefPublicKey(generateSm2PublicKey())));
|
||||
return entity;
|
||||
}
|
||||
|
||||
private BackupDataResult binaryResult(byte[] data) {
|
||||
BackupDataResult result = new BackupDataResult();
|
||||
result.setLength(data.length);
|
||||
result.setData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
private BackupDataResult signatureResult() {
|
||||
EccSignature signature = new EccSignature();
|
||||
signature.r[EccRefPublicKey.MAX_LEN - 1] = 0x01;
|
||||
|
||||
Reference in New Issue
Block a user