feat:备份恢复功能
This commit is contained in:
+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 $!
|
||||
Reference in New Issue
Block a user