Some checks failed
CD Pipeline / select-latest-carrier (push) Successful in 49s
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m28s
CD Pipeline / revalidate-deploy-carrier (push) Successful in 27s
CD Pipeline / build-and-deploy (push) Failing after 17m18s
CD Pipeline / revalidate-post-deploy-carrier (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
E2E Health Check / e2e-health (push) Successful in 48s
AI 技術雷達監控 / ai-technology-watch (push) Successful in 1m4s
AWOOOI Harbor 110 Local Repair / workflow-shape (push) Successful in 0s
AWOOOI Harbor 110 Local Repair / harbor-110-local-repair (push) Successful in 22s
196 lines
7.0 KiB
Bash
196 lines
7.0 KiB
Bash
#\!/bin/bash
|
|
# =============================================================================
|
|
# WOOO AIOps - 備份共用函式庫
|
|
# 版本: 1.0.0
|
|
# 建立日期: 2026-03-12
|
|
# =============================================================================
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 配置區 (待 CEO 提供 B2 帳號後更新)
|
|
# -----------------------------------------------------------------------------
|
|
export BACKUP_BASE="${BACKUP_BASE:-/backup}"
|
|
export BACKUP_LOG_DIR="${BACKUP_LOG_DIR:-${BACKUP_BASE}/logs}"
|
|
export RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE:-${BACKUP_BASE}/scripts/.restic-password}"
|
|
|
|
# Backblaze B2 配置 (待填入)
|
|
export B2_ACCOUNT_ID="" # 待 CEO 提供
|
|
export B2_APPLICATION_KEY="" # 待 CEO 提供
|
|
export B2_BUCKET="wooo-aiops-backup"
|
|
|
|
# ClawBot 通知 Webhook
|
|
export CLAWBOT_WEBHOOK="http://192.168.0.188:8088/api/v1/webhook/custom"
|
|
|
|
# 保留策略 (GFS 祖父子)
|
|
export KEEP_DAILY=30 # 2026-04-05 Claude Code: 延長保留 (原7→30)
|
|
export KEEP_WEEKLY=12 # 2026-04-05 Claude Code: 延長保留 (原4→12)
|
|
export KEEP_MONTHLY=24 # 2026-04-05 Claude Code: 延長保留 (原6→24)
|
|
|
|
# Ephemeral backup containers must remain bounded even when cron has no env file.
|
|
export BACKUP_DOCKER_CPUS="${BACKUP_DOCKER_CPUS:-1.0}"
|
|
export BACKUP_DOCKER_MEMORY="${BACKUP_DOCKER_MEMORY:-1g}"
|
|
export BACKUP_DOCKER_MEMORY_SWAP="${BACKUP_DOCKER_MEMORY_SWAP:-1g}"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 日誌函式
|
|
# -----------------------------------------------------------------------------
|
|
log() {
|
|
local level="$1"
|
|
local message="$2"
|
|
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
|
|
echo "[${timestamp}] [${level}] ${message}" | tee -a "${BACKUP_LOG_DIR}/backup.log"
|
|
}
|
|
|
|
log_info() { log "INFO" "$1"; }
|
|
log_warn() { log "WARN" "$1"; }
|
|
log_error() { log "ERROR" "$1"; }
|
|
log_success() { log "SUCCESS" "$1"; }
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 通知函式
|
|
# -----------------------------------------------------------------------------
|
|
notify_clawbot() {
|
|
local status="$1"
|
|
local service="$2"
|
|
local message="$3"
|
|
local duration="${4:-0}"
|
|
|
|
# 2026-04-05 Claude Code: 正確的 /webhook/custom payload + severity 依狀態
|
|
local severity="info"
|
|
[ "$status" = "warning" ] && severity="warning"
|
|
[ "$status" = "failed" ] && severity="critical"
|
|
|
|
if command -v curl &> /dev/null; then
|
|
curl -s -X POST "${CLAWBOT_WEBHOOK}" \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"name\":\"Backup.${service}\",\"severity\":\"${severity}\",\"service\":\"${service}\",\"description\":\"[${status}] ${message} (${duration}s)\"}" \
|
|
--connect-timeout 5 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Restic 標籤函式
|
|
# -----------------------------------------------------------------------------
|
|
get_app_version() {
|
|
local service="$1"
|
|
case "$service" in
|
|
gitea)
|
|
docker exec gitea gitea --version 2>/dev/null | grep -oP "\\d+\\.\\d+\\.\\d+" | head -1 || echo "unknown"
|
|
;;
|
|
harbor)
|
|
cat /opt/harbor/harbor.yml 2>/dev/null | grep -oP "version: \\K.*" || echo "unknown"
|
|
;;
|
|
momo)
|
|
echo "1.0.0" # MOMO 版本固定或從配置讀取
|
|
;;
|
|
*)
|
|
echo "unknown"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
get_git_hash() {
|
|
local service="$1"
|
|
case "$service" in
|
|
gitea)
|
|
cd /var/lib/gitea 2>/dev/null && git rev-parse --short HEAD 2>/dev/null || echo "none"
|
|
;;
|
|
*)
|
|
echo "none"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
build_tags() {
|
|
local service="$1"
|
|
local version=$(get_app_version "$service")
|
|
local git_hash=$(get_git_hash "$service")
|
|
local timestamp=$(date "+%Y%m%d_%H%M%S")
|
|
|
|
echo "--tag service:${service} --tag version:${version} --tag git:${git_hash} --tag timestamp:${timestamp}"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 備份驗證函式
|
|
# -----------------------------------------------------------------------------
|
|
verify_backup() {
|
|
local repo="$1"
|
|
local snapshot_id="$2"
|
|
local snapshot_rows snapshot_files
|
|
|
|
if ! [[ "${snapshot_id}" =~ ^[0-9a-fA-F]{8,64}$ ]]; then
|
|
log_error "無效 snapshot id"
|
|
return 64
|
|
fi
|
|
log_info "驗證備份快照: ${snapshot_id}"
|
|
snapshot_rows="$(restic -r "${repo}" snapshots "${snapshot_id}" --json \
|
|
--password-file "${RESTIC_PASSWORD_FILE}" 2>/dev/null)" || return 1
|
|
if ! printf '%s' "${snapshot_rows}" | python3 -c \
|
|
'import json,sys; rows=json.load(sys.stdin); raise SystemExit(0 if len(rows) == 1 else 1)'; then
|
|
log_error "指定 snapshot 無法唯一讀回: ${snapshot_id}"
|
|
return 1
|
|
fi
|
|
snapshot_files="$(restic -r "${repo}" ls "${snapshot_id}" --json \
|
|
--password-file "${RESTIC_PASSWORD_FILE}" 2>/dev/null | \
|
|
python3 -c 'import json,sys
|
|
count=0
|
|
for line in sys.stdin:
|
|
try: row=json.loads(line)
|
|
except json.JSONDecodeError: continue
|
|
if row.get("type") == "file": count += 1
|
|
print(count)')" || return 1
|
|
if [ "${snapshot_files}" -le 0 ]; then
|
|
log_error "指定 snapshot 不含可還原檔案: ${snapshot_id}"
|
|
return 1
|
|
fi
|
|
restic -r "${repo}" check --read-data-subset=1% \
|
|
--password-file "${RESTIC_PASSWORD_FILE}" 2>&1
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 清理函式 (GFS 策略)
|
|
# -----------------------------------------------------------------------------
|
|
cleanup_old_backups() {
|
|
local repo="$1"
|
|
local mode="${BACKUP_RETENTION_MODE:-gfs}"
|
|
local keep_last="${BACKUP_KEEP_LAST:-1}"
|
|
|
|
if [ "${mode}" = "latest" ]; then
|
|
case "${keep_last}" in
|
|
''|*[!0-9]*|0) log_error "BACKUP_KEEP_LAST 必須是正整數"; return 64 ;;
|
|
esac
|
|
log_info "執行 latest-only 清理策略 keep-last=${keep_last}"
|
|
restic -r "${repo}" forget \
|
|
--keep-last "${keep_last}" \
|
|
--prune \
|
|
--password-file "${RESTIC_PASSWORD_FILE}" 2>&1
|
|
return $?
|
|
fi
|
|
if [ "${mode}" != "gfs" ]; then
|
|
log_error "未知 BACKUP_RETENTION_MODE: ${mode}"
|
|
return 64
|
|
fi
|
|
log_info "執行 GFS 清理策略"
|
|
restic -r "${repo}" forget \
|
|
--keep-daily "${KEEP_DAILY}" \
|
|
--keep-weekly "${KEEP_WEEKLY}" \
|
|
--keep-monthly "${KEEP_MONTHLY}" \
|
|
--prune \
|
|
--password-file "${RESTIC_PASSWORD_FILE}" 2>&1
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 檢查配置
|
|
# -----------------------------------------------------------------------------
|
|
check_b2_config() {
|
|
if [ -z "${B2_ACCOUNT_ID}" ] || [ -z "${B2_APPLICATION_KEY}" ]; then
|
|
log_warn "B2 配置未設定,僅執行本地備份"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# 初始化日誌目錄
|
|
mkdir -p "${BACKUP_LOG_DIR}"
|
|
|
|
log_info "共用函式庫載入完成 (v1.0.0)"
|