文件
hyb-oa/deploy/trialrun/install-automatic-backup-timer.sh
T

228 行
8.3 KiB
Bash
可执行文件

#!/usr/bin/env bash
set -euo pipefail
REMOTE_DIR="${REMOTE_DIR:-/opt/hyb-oa-trialrun}"
REMOTE_ENV_FILE="${REMOTE_ENV_FILE:-${REMOTE_DIR}/deploy/trialrun/.env}"
COMPOSE_FILE="${COMPOSE_FILE:-deploy/trialrun/docker-compose.yml}"
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-}"
POSTGRES_DATA_DIR="${POSTGRES_DATA_DIR:-}"
STORAGE_HOST_DIR="${STORAGE_HOST_DIR:-}"
BUILD_SHA="${BUILD_SHA:-}"
VITE_BUILD_SHA="${VITE_BUILD_SHA:-}"
AUTOMATIC_BACKUP_DIR="${AUTOMATIC_BACKUP_DIR:-}"
AUTOMATIC_BACKUP_STATE_DIR="${AUTOMATIC_BACKUP_STATE_DIR:-}"
OFFHOST_TARGET="${OFFHOST_TARGET:-}"
OFFHOST_SSH_KEY="${OFFHOST_SSH_KEY:-}"
OFFHOST_KNOWN_HOSTS="${OFFHOST_KNOWN_HOSTS:-}"
ALERT_RECIPIENT_EMPLOYEE_NO="${ALERT_RECIPIENT_EMPLOYEE_NO:-HYD000006}"
BACKUP_RUN_AT="${BACKUP_RUN_AT:-02:30}"
WATCHDOG_RUN_AT="${WATCHDOG_RUN_AT:-07:10}"
SERVICE_PREFIX="${SERVICE_PREFIX:-hyb-oa-automatic-backup}"
MODE="${1:-install}"
RUNNER="${REMOTE_DIR}/scripts/run-automatic-backup.sh"
SYSTEMD_DIR="/etc/systemd/system"
fail() {
echo "[automatic-backup-installer][ERROR] $1" >&2
exit 1
}
need_root() {
[[ "$(id -u)" == "0" ]] || fail "root privileges are required"
}
validate_absolute_path() {
local name="$1"
local value="$2"
if [[ -z "$value" || "$value" != /* || ! "$value" =~ ^/[A-Za-z0-9._/-]+$ ]]; then
fail "$name must be an explicit safe absolute path"
fi
if [[ "$value" == "/" || "$value" == */ || "$value" == *"//"* \
|| "/$value/" == *"/../"* || "/$value/" == *"/./"* ]]; then
fail "$name contains an unsafe path segment"
fi
}
validate_managed_directory_path() {
local name="$1"
local value="$2"
validate_absolute_path "$name" "$value"
[[ "$value" =~ ^/[^/]+/[^/]+ ]] \
|| fail "$name must be a dedicated path below a top-level directory"
case "$value" in
/bin/*|/boot/*|/dev/*|/etc/*|/lib/*|/lib64/*|/proc/*|/run/*|/sbin/*|/sys/*|/usr/*)
fail "$name must not be inside a system-managed directory"
;;
esac
}
paths_overlap() {
local first="$1"
local second="$2"
[[ "$first" == "$second" || "$first" == "$second"/* || "$second" == "$first"/* ]]
}
validate_inputs() {
local name value
for name in REMOTE_DIR REMOTE_ENV_FILE POSTGRES_DATA_DIR STORAGE_HOST_DIR \
AUTOMATIC_BACKUP_DIR AUTOMATIC_BACKUP_STATE_DIR OFFHOST_SSH_KEY OFFHOST_KNOWN_HOSTS; do
value="${!name:-}"
validate_absolute_path "$name" "$value"
done
validate_managed_directory_path AUTOMATIC_BACKUP_DIR "$AUTOMATIC_BACKUP_DIR"
validate_managed_directory_path AUTOMATIC_BACKUP_STATE_DIR "$AUTOMATIC_BACKUP_STATE_DIR"
[[ -d "$REMOTE_DIR" && -f "$REMOTE_ENV_FILE" && -x "$RUNNER" ]] \
|| fail "deployment root, env file, or backup runner is missing"
[[ -f "$POSTGRES_DATA_DIR/PG_VERSION" && -d "$STORAGE_HOST_DIR" ]] \
|| fail "active data directories are invalid"
[[ "$COMPOSE_PROJECT_NAME" =~ ^[a-z0-9][a-z0-9_-]*$ ]] || fail "Compose project name is invalid"
[[ "$COMPOSE_FILE" =~ ^[A-Za-z0-9._/-]+$ && "$COMPOSE_FILE" != /* \
&& "/$COMPOSE_FILE/" != *"/../"* && "/$COMPOSE_FILE/" != *"/./"* ]] \
|| fail "Compose file must be a safe relative path"
[[ "$BUILD_SHA" =~ ^[0-9a-fA-F]{40,64}$ ]] || fail "full build SHA is invalid"
BUILD_SHA="${BUILD_SHA,,}"
VITE_BUILD_SHA="${VITE_BUILD_SHA,,}"
[[ "$VITE_BUILD_SHA" =~ ^[0-9a-f]{12}$ && "${BUILD_SHA:0:12}" == "$VITE_BUILD_SHA" ]] \
|| fail "frontend build SHA must match the full build prefix"
[[ "$OFFHOST_TARGET" =~ ^[a-z_][a-z0-9_-]*@[A-Za-z0-9._-]+$ ]] \
|| fail "off-host target must use a safe user@alias value"
[[ -f "$OFFHOST_SSH_KEY" && ! -L "$OFFHOST_SSH_KEY" \
&& "$(stat -c %a "$OFFHOST_SSH_KEY")" == "600" ]] \
|| fail "off-host private key must exist with mode 0600"
[[ -s "$OFFHOST_KNOWN_HOSTS" ]] || fail "pinned known-hosts file is missing"
[[ "$BACKUP_RUN_AT" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]] || fail "backup time is invalid"
[[ "$WATCHDOG_RUN_AT" =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]] || fail "watchdog time is invalid"
[[ "$SERVICE_PREFIX" =~ ^[a-z0-9][a-z0-9_-]*$ ]] || fail "service prefix is invalid"
[[ "$ALERT_RECIPIENT_EMPLOYEE_NO" =~ ^[A-Za-z0-9_-]{1,80}$ ]] || fail "alert recipient id is invalid"
local -a isolated_paths=(
"$POSTGRES_DATA_DIR" "$STORAGE_HOST_DIR" "$AUTOMATIC_BACKUP_DIR" "$AUTOMATIC_BACKUP_STATE_DIR"
)
local first_index second_index
for ((first_index = 0; first_index < ${#isolated_paths[@]}; first_index += 1)); do
for ((second_index = first_index + 1; second_index < ${#isolated_paths[@]}; second_index += 1)); do
if paths_overlap "${isolated_paths[$first_index]}" "${isolated_paths[$second_index]}"; then
fail "data, backup, and state directories must be independent"
fi
done
done
}
environment_lines() {
cat <<ENVIRONMENT
Environment=REMOTE_DIR=${REMOTE_DIR}
Environment=REMOTE_ENV_FILE=${REMOTE_ENV_FILE}
Environment=COMPOSE_FILE=${COMPOSE_FILE}
Environment=COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
Environment=POSTGRES_DATA_DIR=${POSTGRES_DATA_DIR}
Environment=STORAGE_HOST_DIR=${STORAGE_HOST_DIR}
Environment=BUILD_SHA=${BUILD_SHA}
Environment=VITE_BUILD_SHA=${VITE_BUILD_SHA}
Environment=AUTOMATIC_BACKUP_DIR=${AUTOMATIC_BACKUP_DIR}
Environment=AUTOMATIC_BACKUP_STATE_DIR=${AUTOMATIC_BACKUP_STATE_DIR}
Environment=OFFHOST_TARGET=${OFFHOST_TARGET}
Environment=OFFHOST_SSH_KEY=${OFFHOST_SSH_KEY}
Environment=OFFHOST_KNOWN_HOSTS=${OFFHOST_KNOWN_HOSTS}
Environment=ALERT_RECIPIENT_EMPLOYEE_NO=${ALERT_RECIPIENT_EMPLOYEE_NO}
ENVIRONMENT
}
write_service() {
local suffix="$1"
local mode="$2"
local description="$3"
local service_file="$SYSTEMD_DIR/${SERVICE_PREFIX}-${suffix}.service"
cat >"$service_file" <<SERVICE
[Unit]
Description=${description}
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=${REMOTE_DIR}
UMask=0077
$(environment_lines)
ExecStart=/bin/bash ${RUNNER} ${mode}
SERVICE
}
write_timer() {
local suffix="$1"
local calendar="$2"
local description="$3"
local timer_file="$SYSTEMD_DIR/${SERVICE_PREFIX}-${suffix}.timer"
cat >"$timer_file" <<TIMER
[Unit]
Description=${description}
[Timer]
OnCalendar=${calendar}
Persistent=true
Unit=${SERVICE_PREFIX}-${suffix}.service
[Install]
WantedBy=timers.target
TIMER
}
install_units() {
need_root
validate_inputs
mkdir -p "$AUTOMATIC_BACKUP_DIR" "$AUTOMATIC_BACKUP_STATE_DIR"
[[ -d "$AUTOMATIC_BACKUP_DIR" && ! -L "$AUTOMATIC_BACKUP_DIR" \
&& -d "$AUTOMATIC_BACKUP_STATE_DIR" && ! -L "$AUTOMATIC_BACKUP_STATE_DIR" ]] \
|| fail "backup and state directories must not be symbolic links"
chmod 0700 "$AUTOMATIC_BACKUP_DIR" "$AUTOMATIC_BACKUP_STATE_DIR"
write_service daily run "hyb-oa automatic paired off-host backup"
write_service watchdog check-stale "hyb-oa automatic backup freshness watchdog"
write_service drill-reminder remind-drill "hyb-oa quarterly shadow restore reminder"
write_timer daily "*-*-* ${BACKUP_RUN_AT}:00" "Run hyb-oa automatic off-host backup daily"
write_timer watchdog "*-*-* ${WATCHDOG_RUN_AT}:00" "Check hyb-oa off-host backup freshness daily"
write_timer drill-reminder "*-01,04,07,10-01 09:00:00" "Remind quarterly hyb-oa shadow restore drill"
systemctl daemon-reload
echo "[automatic-backup-installer][PASS] units installed disabled"
}
enable_units() {
install_units
systemctl enable --now \
"${SERVICE_PREFIX}-daily.timer" \
"${SERVICE_PREFIX}-watchdog.timer" \
"${SERVICE_PREFIX}-drill-reminder.timer"
echo "[automatic-backup-installer][PASS] timers enabled"
}
uninstall_units() {
need_root
[[ "$SERVICE_PREFIX" =~ ^[a-z0-9][a-z0-9_-]*$ ]] || fail "service prefix is invalid"
local suffix
for suffix in daily watchdog drill-reminder; do
systemctl disable --now "${SERVICE_PREFIX}-${suffix}.timer" >/dev/null 2>&1 || true
systemctl stop "${SERVICE_PREFIX}-${suffix}.service" >/dev/null 2>&1 || true
rm -f -- "$SYSTEMD_DIR/${SERVICE_PREFIX}-${suffix}.timer" \
"$SYSTEMD_DIR/${SERVICE_PREFIX}-${suffix}.service"
done
systemctl daemon-reload
echo "[automatic-backup-installer][PASS] units removed; backups and credentials retained"
}
case "$MODE" in
install)
install_units
;;
enable)
enable_units
;;
uninstall|--uninstall)
uninstall_units
;;
--help|-h)
echo "Usage: <explicit deployment/data/backup/SSH env> bash install-automatic-backup-timer.sh [install|enable|uninstall]"
;;
*)
fail "unsupported mode"
;;
esac