diff --git a/pbs_allinone.sh b/pbs_allinone.sh new file mode 100644 index 0000000..2ce98b0 --- /dev/null +++ b/pbs_allinone.sh @@ -0,0 +1,582 @@ +#!/bin/bash +# +# PBS 올인원 스크립트 - Proxmox 호스트에서 실행 +# 작성일: 2025-11-04 +# 용도: PBS 등록 → 백업 조회 → 복구를 한 번에 +# + +set -e + +# 색상 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +NC='\033[0m' + +# PBS 서버 설정 (자동 입력) +PBS_SERVER="100.64.0.10" +PBS_PORT="8007" +PBS_USERNAME="0bin@pbs" +PBS_PASSWORD="@Trajet6640" +PBS_DATASTORE="PBS-DVA" +PBS_FINGERPRINT="24:42:c6:0f:a8:1b:93:32:32:44:84:be:6a:c5:71:97:e4:4d:61:fc:a4:48:12:0c:97:3b:9f:1f:cc:b2:54:e8" +PBS_STORAGE_NAME="PBS-Auto" + +# 복구 설정 +TEMPLATE_VMID="" +TARGET_VMID="" +TARGET_STORAGE="local-lvm" +BACKUP_TYPE="" +LATEST_SNAPSHOT="" + +# 로그 함수 +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_step() { echo -e "${CYAN}╰─► ${NC}$1"; } + +# 배너 +print_banner() { + clear + echo -e "${MAGENTA}" + cat << "EOF" +╔═══════════════════════════════════════════════════════════════╗ +║ ║ +║ PBS 올인원 스크립트 ║ +║ 등록 → 조회 → 복구 한 번에! ║ +║ ║ +╚═══════════════════════════════════════════════════════════════╝ +EOF + echo -e "${NC}" + echo "" +} + +# Proxmox 환경 확인 +check_proxmox() { + if [ ! -f /etc/pve/storage.cfg ]; then + log_error "Proxmox VE가 설치되어 있지 않습니다." + exit 1 + fi + log_success "Proxmox VE 환경 확인 완료" +} + +# PBS 스토리지 등록 +register_pbs() { + log_step "PBS 스토리지 등록 중..." + + # 이미 등록되어 있는지 확인 + if pvesm status | grep -q "^${PBS_STORAGE_NAME} "; then + log_warning "PBS 스토리지가 이미 등록되어 있습니다." + read -p "$(echo -e ${YELLOW}재등록하시겠습니까?${NC}) (y/N): " reregister + + if [[ "$reregister" =~ ^[Yy]$ ]]; then + log_info "기존 PBS 스토리지 제거 중..." + pvesm remove "${PBS_STORAGE_NAME}" + sleep 1 + else + log_info "기존 PBS 스토리지 사용" + return 0 + fi + fi + + # PBS 등록 + log_info "PBS 서버 등록 중..." + pvesm add pbs "${PBS_STORAGE_NAME}" \ + --server "${PBS_SERVER}" \ + --port "${PBS_PORT}" \ + --datastore "${PBS_DATASTORE}" \ + --username "${PBS_USERNAME}" \ + --password "${PBS_PASSWORD}" \ + --fingerprint "${PBS_FINGERPRINT}" 2>&1 + + if [ $? -eq 0 ]; then + log_success "PBS 스토리지 등록 완료!" + else + log_error "PBS 스토리지 등록 실패" + return 1 + fi + + # 연결 테스트 + log_info "PBS 연결 테스트 중..." + if pvesm status -storage "${PBS_STORAGE_NAME}" &>/dev/null; then + log_success "PBS 연결 성공!" + echo "" + pvesm status -storage "${PBS_STORAGE_NAME}" + echo "" + else + log_error "PBS 연결 테스트 실패" + return 1 + fi +} + +# PBS API 인증 +pbs_login() { + log_info "PBS API 인증 중..." + + curl -k -s -X POST "https://${PBS_SERVER}:${PBS_PORT}/api2/json/access/ticket" \ + -d "username=${PBS_USERNAME}" \ + -d "password=${PBS_PASSWORD}" > /tmp/pbs_auth.json + + if [ ! -s /tmp/pbs_auth.json ]; then + log_error "PBS API 응답 없음. 서버 연결을 확인하세요." + return 1 + fi + + PBS_TICKET=$(python3 -c 'import json; data=json.load(open("/tmp/pbs_auth.json")); print(data["data"]["ticket"])' 2>/dev/null) + PBS_CSRF=$(python3 -c 'import json; data=json.load(open("/tmp/pbs_auth.json")); print(data["data"]["CSRFPreventionToken"])' 2>/dev/null) + + if [ -z "$PBS_TICKET" ]; then + log_error "PBS API 인증 실패. 응답:" + cat /tmp/pbs_auth.json | head -5 + return 1 + fi + + log_success "PBS API 인증 완료" +} + +# 백업 그룹 목록 조회 및 선택 +list_and_select_backup() { + log_step "PBS에서 백업 목록 조회 중..." + echo "" + + # 응답을 파일에 저장 + curl -k -s -X GET "https://${PBS_SERVER}:${PBS_PORT}/api2/json/admin/datastore/${PBS_DATASTORE}/groups" \ + -H "Cookie: PBSAuthCookie=${PBS_TICKET}" \ + -H "CSRFPreventionToken: ${PBS_CSRF}" > /tmp/pbs_groups.json + + # 디버깅: 응답 확인 + if [ ! -s /tmp/pbs_groups.json ]; then + log_error "PBS API 응답이 비어있습니다" + log_info "PBS_TICKET: ${PBS_TICKET:0:50}..." + log_info "PBS_CSRF: ${PBS_CSRF:0:50}..." + return 1 + fi + + # 디버깅: 응답 길이 확인 + log_info "API 응답 수신 ($(stat -c%s /tmp/pbs_groups.json) bytes)" + + # PBS 인증 정보를 환경 변수로 export + export PBS_TICKET + export PBS_CSRF + + python3 << PYEOF +import sys, json, subprocess, os + +# PBS 인증 정보 가져오기 +PBS_TICKET = os.environ.get('PBS_TICKET', '') +PBS_CSRF = os.environ.get('PBS_CSRF', '') +PBS_SERVER = "${PBS_SERVER}" +PBS_PORT = "${PBS_PORT}" +PBS_DATASTORE = "${PBS_DATASTORE}" + +def get_snapshot_comment(backup_type, backup_id): + """최신 스냅샷의 comment를 가져오기""" + try: + cmd = [ + 'curl', '-k', '-s', '-X', 'GET', + f'https://{PBS_SERVER}:{PBS_PORT}/api2/json/admin/datastore/{PBS_DATASTORE}/snapshots?backup-type={backup_type}&backup-id={backup_id}', + '-H', f'Cookie: PBSAuthCookie={PBS_TICKET}', + '-H', f'CSRFPreventionToken: {PBS_CSRF}' + ] + result = subprocess.run(cmd, capture_output=True, text=True) + data = json.loads(result.stdout) + snapshots = data.get("data", []) + + if snapshots: + # 가장 최근 스냅샷 찾기 + latest = max(snapshots, key=lambda x: x.get("backup-time", 0)) + return latest.get("comment", "") + except: + pass + return "" + +try: + with open('/tmp/pbs_groups.json', 'r') as f: + data = json.load(f) + groups = data.get("data", []) + + vm_groups = [g for g in groups if g.get("backup-type") == "vm"] + ct_groups = [g for g in groups if g.get("backup-type") == "ct"] + + if vm_groups: + print("\033[1;36m=== VM 백업 ===\033[0m") + for i, group in enumerate(vm_groups, 1): + backup_id = group.get("backup-id", "N/A") + backup_count = group.get("backup-count", 0) + last_backup = group.get("last-backup", 0) + + # groups에서 comment 가져오기 (없으면 스냅샷에서 조회) + comment = group.get("comment", "") + if not comment: + comment = get_snapshot_comment("vm", backup_id) + + from datetime import datetime + if last_backup > 0: + last_str = datetime.fromtimestamp(last_backup).strftime("%Y-%m-%d %H:%M") + else: + last_str = "N/A" + + if comment: + print(f" {i}. VM {backup_id:<6} - {backup_count}개 백업 (최근: {last_str})") + print(f" \033[0;90m└─ {comment}\033[0m") + else: + print(f" {i}. VM {backup_id:<6} - {backup_count}개 백업 (최근: {last_str})") + + if ct_groups: + print("\n\033[1;36m=== CT 백업 ===\033[0m") + for i, group in enumerate(ct_groups, 1): + backup_id = group.get("backup-id", "N/A") + backup_count = group.get("backup-count", 0) + last_backup = group.get("last-backup", 0) + + # groups에서 comment 가져오기 (없으면 스냅샷에서 조회) + comment = group.get("comment", "") + if not comment: + comment = get_snapshot_comment("ct", backup_id) + + from datetime import datetime + if last_backup > 0: + last_str = datetime.fromtimestamp(last_backup).strftime("%Y-%m-%d %H:%M") + else: + last_str = "N/A" + + if comment: + print(f" {i}. CT {backup_id:<6} - {backup_count}개 백업 (최근: {last_str})") + print(f" \033[0;90m└─ {comment}\033[0m") + else: + print(f" {i}. CT {backup_id:<6} - {backup_count}개 백업 (최근: {last_str})") + + print("") + +except Exception as e: + print(f"오류: {e}") + sys.exit(1) +PYEOF + + # 백업 선택 + echo "" + while true; do + read -p "$(echo -e ${CYAN}백업 타입${NC}) (vm/ct): " BACKUP_TYPE + if [[ "$BACKUP_TYPE" =~ ^(vm|ct)$ ]]; then + break + fi + log_error "vm 또는 ct를 입력하세요" + done + + read -p "$(echo -e ${CYAN}백업 ID${NC}): " TEMPLATE_VMID + if [ -z "$TEMPLATE_VMID" ]; then + log_error "백업 ID는 필수입니다" + exit 1 + fi + + # 최신 스냅샷 찾기 + log_info "최신 백업 조회 중..." + if get_latest_snapshot "$BACKUP_TYPE" "$TEMPLATE_VMID"; then + log_success "최신 백업: ${LATEST_SNAPSHOT}" + else + log_error "${BACKUP_TYPE} ${TEMPLATE_VMID}의 백업을 찾을 수 없습니다" + exit 1 + fi +} + +# 최신 스냅샷 조회 +get_latest_snapshot() { + local backup_type="$1" + local backup_id="$2" + + curl -k -s -X GET "https://${PBS_SERVER}:${PBS_PORT}/api2/json/admin/datastore/${PBS_DATASTORE}/snapshots?backup-type=${backup_type}&backup-id=${backup_id}" \ + -H "Cookie: PBSAuthCookie=${PBS_TICKET}" \ + -H "CSRFPreventionToken: ${PBS_CSRF}" > /tmp/pbs_snapshots.json + + LATEST_SNAPSHOT=$(python3 << 'PYEOF' +import sys, json +try: + with open('/tmp/pbs_snapshots.json', 'r') as f: + data = json.load(f) + snapshots = data.get("data", []) + + if not snapshots: + print("") + sys.exit(1) + + # 가장 최근 백업 선택 + latest = max(snapshots, key=lambda x: x.get("backup-time", 0)) + backup_time = latest.get("backup-time", 0) + + from datetime import datetime + backup_time_str = datetime.utcfromtimestamp(backup_time).strftime("%Y-%m-%dT%H:%M:%SZ") + + print(backup_time_str) + +except Exception as e: + print("", file=sys.stderr) + sys.exit(1) +PYEOF +) + + if [ -z "$LATEST_SNAPSHOT" ]; then + return 1 + fi + + return 0 +} + +# 복구 설정 입력 +get_restore_config() { + echo "" + log_step "복구 설정을 입력하세요" + echo "" + + # 복구할 VM/CT ID + read -p "$(echo -e ${CYAN}복구할 VM/CT ID${NC}) [기본값: ${TEMPLATE_VMID}]: " TARGET_VMID + TARGET_VMID=${TARGET_VMID:-$TEMPLATE_VMID} + + # 스토리지 + read -p "$(echo -e ${CYAN}저장 스토리지${NC}) [기본값: local-lvm]: " input_storage + TARGET_STORAGE=${input_storage:-local-lvm} + + echo "" + log_info "복구 요약:" + echo " 백업 소스: ${BACKUP_TYPE}/${TEMPLATE_VMID}" + echo " 백업 시점: ${LATEST_SNAPSHOT}" + echo " 복구 ID: ${TARGET_VMID}" + echo " 저장 위치: ${TARGET_STORAGE}" + echo "" + + read -p "$(echo -e ${YELLOW}이 설정으로 복구하시겠습니까?${NC}) (y/N): " confirm + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + log_info "작업 취소됨" + exit 0 + fi +} + +# VM/CT 복구 +restore_backup() { + log_step "백업 복구 중... (시간이 걸릴 수 있습니다)" + echo "" + + # 백업 경로 구성 + BACKUP_PATH="${PBS_STORAGE_NAME}:backup/${BACKUP_TYPE}/${TEMPLATE_VMID}/${LATEST_SNAPSHOT}" + + log_info "백업 경로: ${BACKUP_PATH}" + + # 기존 VM/CT 확인 + if qm status ${TARGET_VMID} 2>/dev/null || pct status ${TARGET_VMID} 2>/dev/null; then + log_warning "VM/CT ${TARGET_VMID}가 이미 존재합니다" + read -p "$(echo -e ${YELLOW}삭제하고 복구하시겠습니까?${NC}) (y/N): " delete_confirm + + if [[ "$delete_confirm" =~ ^[Yy]$ ]]; then + log_info "기존 VM/CT 삭제 중..." + if [ "$BACKUP_TYPE" = "vm" ]; then + qm stop ${TARGET_VMID} --skiplock || true + sleep 2 + qm destroy ${TARGET_VMID} --purge --skiplock || true + else + pct stop ${TARGET_VMID} || true + sleep 2 + pct destroy ${TARGET_VMID} --purge || true + fi + sleep 2 + log_success "기존 VM/CT 삭제 완료" + else + log_error "복구 취소됨" + exit 1 + fi + fi + + # 복구 실행 + if [ "$BACKUP_TYPE" = "vm" ]; then + log_info "VM 복구 시작..." + echo "" + + qmrestore "${BACKUP_PATH}" ${TARGET_VMID} \ + --storage ${TARGET_STORAGE} \ + --unique 1 2>&1 | tee /tmp/pbs-restore.log + + if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo "" + log_success "VM 복구 완료!" + else + echo "" + log_error "VM 복구 실패. 로그: /tmp/pbs-restore.log" + return 1 + fi + else + log_info "CT 복구 시작..." + echo "" + + pct restore ${TARGET_VMID} "${BACKUP_PATH}" \ + --storage ${TARGET_STORAGE} \ + --unprivileged 1 2>&1 | tee /tmp/pbs-restore.log + + if [ ${PIPESTATUS[0]} -eq 0 ]; then + echo "" + log_success "CT 복구 완료!" + else + echo "" + log_error "CT 복구 실패. 로그: /tmp/pbs-restore.log" + return 1 + fi + fi +} + +# VM/CT 시작 +start_vm() { + echo "" + read -p "$(echo -e ${YELLOW}VM/CT를 바로 시작하시겠습니까?${NC}) (y/N): " start_confirm + + if [[ "$start_confirm" =~ ^[Yy]$ ]]; then + log_info "VM/CT 시작 중..." + + if [ "$BACKUP_TYPE" = "vm" ]; then + qm start ${TARGET_VMID} + else + pct start ${TARGET_VMID} + fi + + if [ $? -eq 0 ]; then + log_success "VM/CT 시작 완료!" + + sleep 3 + log_info "상태 확인 중..." + if [ "$BACKUP_TYPE" = "vm" ]; then + qm status ${TARGET_VMID} + else + pct status ${TARGET_VMID} + fi + else + log_error "시작 실패" + return 1 + fi + else + log_info "수동으로 시작하세요:" + if [ "$BACKUP_TYPE" = "vm" ]; then + echo " qm start ${TARGET_VMID}" + else + echo " pct start ${TARGET_VMID}" + fi + fi +} + +# 완료 메시지 +print_summary() { + echo "" + echo -e "${GREEN}" + cat << "EOF" +╔═══════════════════════════════════════════════════════════════╗ +║ ║ +║ ✅ 복구 완료! ║ +║ ║ +╚═══════════════════════════════════════════════════════════════╝ +EOF + echo -e "${NC}" + echo "" + + log_info "복구 정보:" + echo " 백업 소스: ${BACKUP_TYPE}/${TEMPLATE_VMID}" + echo " 백업 시점: ${LATEST_SNAPSHOT}" + echo " 복구 ID: ${TARGET_VMID}" + echo " 스토리지: ${TARGET_STORAGE}" + echo "" + + log_info "Proxmox Web UI:" + local pve_ip=$(hostname -I | awk '{print $1}') + echo " https://${pve_ip}:8006" + echo "" + + if [ "$BACKUP_TYPE" = "vm" ]; then + log_info "유용한 명령어:" + echo " VM 상태: qm status ${TARGET_VMID}" + echo " VM 시작: qm start ${TARGET_VMID}" + echo " VM 중지: qm stop ${TARGET_VMID}" + echo " VM 정보: qm config ${TARGET_VMID}" + else + log_info "유용한 명령어:" + echo " CT 상태: pct status ${TARGET_VMID}" + echo " CT 시작: pct start ${TARGET_VMID}" + echo " CT 중지: pct stop ${TARGET_VMID}" + echo " CT 접속: pct enter ${TARGET_VMID}" + fi + echo "" + + log_info "PBS 스토리지:" + echo " 이름: ${PBS_STORAGE_NAME}" + echo " 제거: pvesm remove ${PBS_STORAGE_NAME}" + echo "" +} + +# 메인 실행 +main() { + print_banner + + # Proxmox 환경 확인 + check_proxmox + echo "" + + # PBS 스토리지 등록 + register_pbs || exit 1 + echo "" + + # PBS API 인증 + pbs_login || exit 1 + echo "" + + # 백업 목록 조회 및 선택 + list_and_select_backup + echo "" + + # 복구 설정 입력 + get_restore_config + echo "" + + # 백업 복구 + restore_backup || exit 1 + echo "" + + # VM/CT 시작 + start_vm + echo "" + + # 완료 메시지 + print_summary +} + +# 도움말 +if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "PBS 올인원 스크립트 - Proxmox 호스트에서 실행" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "사용법: $0" + echo "" + echo "이 스크립트는:" + echo " 1. PBS 스토리지를 자동 등록" + echo " 2. API로 백업 목록 조회" + echo " 3. 선택한 백업을 복구" + echo "" + echo "실행 예시:" + echo " bash $0" + echo "" + echo "사전 준비:" + echo " - Proxmox VE 설치 완료" + echo " - PBS 서버 (100.64.0.10) 접근 가능" + echo " - python3 설치 (보통 기본 설치됨)" + echo "" + echo "모든 설정(IP, 비밀번호 등)은 자동 입력됩니다!" + echo "" + exit 0 +fi + +# Root 권한 확인 +if [ "$EUID" -ne 0 ]; then + log_error "이 스크립트는 root 권한이 필요합니다." + log_info "다음 명령으로 실행하세요: sudo $0" + exit 1 +fi + +# 스크립트 실행 +main