#!/usr/bin/env bash # 192.168.0.188 主機層 momo PostgreSQL 備份控制器。 # # 由 Ansible 部署到: # /home/ollama/momo-pro/scripts/pg_backup.sh # # PostgreSQL 憑證只從 momo-db 容器環境讀取;禁止輸出或落地憑證值。 set -Eeuo pipefail BACKUP_DIR="${BACKUP_DIR:-/home/ollama/momo_backups}" DB_CONTAINER="${DB_CONTAINER:-momo-db}" DB_USER="${DB_USER:-momo}" DB_NAME="${DB_NAME:-momo_analytics}" KEEP_DAYS="${KEEP_DAYS:-7}" MIN_SIZE_BYTES="${MIN_SIZE_BYTES:-1048576}" LOG_FILE="${LOG_FILE:-${BACKUP_DIR}/backup.log}" TIMESTAMP="$(date +%Y%m%d_%H%M%S)" FILENAME="momo_analytics_${TIMESTAMP}.sql.gz" FILEPATH="${BACKUP_DIR}/${FILENAME}" TMP_FILE="${FILEPATH}.tmp" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" START_TS="$(date +%s)" log() { local line line="$(printf '[%s] %s' "$(date '+%Y-%m-%d %H:%M:%S')" "$*")" printf '%s\n' "$line" >> "${LOG_FILE}" if [[ "${AWOOI_BACKUP_LOG_STDOUT:-0}" == "1" || -t 1 ]]; then printf '%s\n' "$line" fi } cleanup_tmp() { rm -f "${TMP_FILE}" } elapsed_seconds() { local now now="$(date +%s)" echo "$((now - START_TS))" } notify_awoooi_ops() { local status="$1" local summary="$2" local detail="$3" local helper="${SCRIPT_DIR}/notify-awoooi-ops.sh" if [[ "${AWOOI_BACKUP_NOTIFY_ENABLED:-1}" != "1" ]]; then return 0 fi [[ -x "$helper" ]] || return 1 AWOOI_OPS_ALERTNAME="Backup.MomoPostgres" \ AWOOI_OPS_JOB_NAME="MOMO PostgreSQL 備份" \ AWOOI_OPS_STATUS="$status" \ AWOOI_OPS_SEVERITY="info" \ AWOOI_OPS_SOURCE="momo-pg-backup" \ AWOOI_OPS_COMPONENT="momo-postgres-backup" \ AWOOI_OPS_SUMMARY="$summary" \ AWOOI_OPS_DETAIL="$detail" \ AWOOI_OPS_DURATION_SECONDS="$(elapsed_seconds)" \ "$helper" >/dev/null } notify_best_effort() { local status="$1" local summary="$2" local detail="$3" if [[ "$status" == "success" && "${AWOOI_BACKUP_NOTIFY_SUCCESS:-0}" != "1" ]]; then log "略過 AwoooP 成功通知;backup-health exporter 作為健康狀態來源" return 0 fi notify_awoooi_ops "$status" "$summary" "$detail" || log "WARN AwoooP notification failed" } on_error() { local exit_code="$?" local line_no="${1:-unknown}" set +e notify_awoooi_ops \ "failed" \ "MOMO PostgreSQL 備份失敗" \ "line=${line_no}; container=${DB_CONTAINER}; db=${DB_NAME}; file=${FILENAME}; log=${LOG_FILE}" \ || true exit "$exit_code" } fail_backup() { local message="$1" log "ERROR ${message}" notify_awoooi_ops \ "failed" \ "MOMO PostgreSQL 備份失敗" \ "${message}; container=${DB_CONTAINER}; db=${DB_NAME}; file=${FILENAME}; log=${LOG_FILE}" \ || true exit 1 } container_running() { docker inspect -f '{{.State.Running}}' "${DB_CONTAINER}" 2>/dev/null | grep -qx true } run_pg_dump() { docker exec "${DB_CONTAINER}" sh -eu -c ' : "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD missing in container env}" PGPASSWORD="${POSTGRES_PASSWORD}" exec pg_dump \ -U "${POSTGRES_USER:-momo}" \ -d "${POSTGRES_DB:-momo_analytics}" \ --no-password \ --no-owner \ --no-acl ' } insert_backup_log() { local size_bytes="$1" local duration="$2" docker exec \ -e BACKUP_FILENAME="${FILENAME}" \ -e BACKUP_SIZE_BYTES="${size_bytes}" \ -e BACKUP_DURATION_SECONDS="${duration}" \ -e BACKUP_HOST="$(hostname)" \ -e BACKUP_STORAGE_PATH="${FILEPATH}" \ "${DB_CONTAINER}" sh -eu -c ' : "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD missing in container env}" PGPASSWORD="${POSTGRES_PASSWORD}" psql \ -U "${POSTGRES_USER:-momo}" \ -d "${POSTGRES_DB:-momo_analytics}" \ --no-password \ -v ON_ERROR_STOP=1 \ -c "INSERT INTO backup_log (filename, file_size_bytes, duration_seconds, status, host, storage_path, completed_at) VALUES (E'\''${BACKUP_FILENAME}'\'', ${BACKUP_SIZE_BYTES}, ${BACKUP_DURATION_SECONDS}, '\''success'\'', E'\''${BACKUP_HOST}'\'', E'\''${BACKUP_STORAGE_PATH}'\'', CURRENT_TIMESTAMP);" ' >/dev/null 2>&1 } main() { mkdir -p "${BACKUP_DIR}" trap cleanup_tmp EXIT trap 'on_error ${LINENO}' ERR log "===== momo PostgreSQL backup start =====" if ! container_running; then fail_backup "${DB_CONTAINER} is not running" fi local start_ts end_ts duration size_bytes size_human deleted backup_log_status start_ts="$(date +%s)" if run_pg_dump | gzip >"${TMP_FILE}"; then size_bytes="$(stat -c%s "${TMP_FILE}" 2>/dev/null || stat -f%z "${TMP_FILE}" 2>/dev/null || echo 0)" if [ "${size_bytes}" -lt "${MIN_SIZE_BYTES}" ]; then fail_backup "backup file too small: ${size_bytes} bytes" fi mv "${TMP_FILE}" "${FILEPATH}" chmod 0640 "${FILEPATH}" else fail_backup "pg_dump failed" fi end_ts="$(date +%s)" duration="$((end_ts - start_ts))" size_human="$(du -h "${FILEPATH}" | awk '{print $1}')" log "Backup success: ${FILENAME} (${size_human}, ${duration}s)" backup_log_status="success" if insert_backup_log "${size_bytes}" "${duration}"; then log "backup_log insert success" else backup_log_status="warning" log "WARN backup_log insert failed; backup file is still valid" fi deleted="$(find "${BACKUP_DIR}" -name 'momo_analytics_*.sql.gz' -mtime "+${KEEP_DAYS}" -print -delete | wc -l | tr -d ' ')" log "Deleted old backups: ${deleted}" log "===== momo PostgreSQL backup complete =====" notify_best_effort \ "success" \ "MOMO PostgreSQL 備份完成" \ "file=${FILENAME}; size=${size_human}; duration=${duration}s; backup_log=${backup_log_status}; deleted_old=${deleted}" } main "$@"