#!/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 < /dev/tty 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}" \ --namespace "PQ" 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 "" # 응답을 파일에 저장 (PQ 네임스페이스 명시) curl -k -s -X GET "https://${PBS_SERVER}:${PBS_PORT}/api2/json/admin/datastore/${PBS_DATASTORE}/groups?ns=PQ" \ -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}&ns=PQ', '-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 < /dev/tty if [[ "$BACKUP_TYPE" =~ ^(vm|ct)$ ]]; then break fi log_error "vm 또는 ct를 입력하세요" done read -p "$(echo -e ${CYAN}백업 ID${NC}): " TEMPLATE_VMID < /dev/tty 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}&ns=PQ" \ -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 } # 사용 가능한 스토리지 목록 표시 list_available_storage() { echo "" log_step "사용 가능한 스토리지 목록" echo "" # VM/CT 이미지를 저장할 수 있는 스토리지만 필터링 pvesm status -content images,rootdir 2>/dev/null | tail -n +2 | while read -r line; do storage_name=$(echo "$line" | awk '{print $1}') storage_type=$(echo "$line" | awk '{print $2}') total_mb=$(echo "$line" | awk '{print $4}') used_mb=$(echo "$line" | awk '{print $5}') avail_mb=$(echo "$line" | awk '{print $6}') usage_pct=$(echo "$line" | awk '{print $7}') # MB를 GB로 변환 total_gb=$(echo "scale=2; $total_mb / 1024 / 1024" | bc) avail_gb=$(echo "scale=2; $avail_mb / 1024 / 1024" | bc) echo -e " ${GREEN}●${NC} ${CYAN}${storage_name}${NC}" echo -e " 타입: ${storage_type} | 용량: ${total_gb}GB | 여유: ${avail_gb}GB | 사용률: ${usage_pct}" done echo "" } # 복구 설정 입력 get_restore_config() { echo "" log_step "복구 설정을 입력하세요" echo "" # 복구할 VM/CT ID read -p "$(echo -e ${CYAN}복구할 VM/CT ID${NC}) [기본값: ${TEMPLATE_VMID}]: " TARGET_VMID < /dev/tty TARGET_VMID=${TARGET_VMID:-$TEMPLATE_VMID} # 사용 가능한 스토리지 목록 표시 list_available_storage # 스토리지 선택 while true; do read -p "$(echo -e ${CYAN}저장 스토리지 이름${NC}) [기본값: local-lvm]: " input_storage < /dev/tty TARGET_STORAGE=${input_storage:-local-lvm} # 입력한 스토리지가 존재하는지 확인 if pvesm status -storage "${TARGET_STORAGE}" &>/dev/null; then # VM/CT 이미지를 저장할 수 있는지 확인 if pvesm status -storage "${TARGET_STORAGE}" -content images &>/dev/null || \ pvesm status -storage "${TARGET_STORAGE}" -content rootdir &>/dev/null; then log_success "스토리지 '${TARGET_STORAGE}' 선택됨" break else log_error "스토리지 '${TARGET_STORAGE}'는 VM/CT 이미지를 저장할 수 없습니다" fi else log_error "스토리지 '${TARGET_STORAGE}'를 찾을 수 없습니다" fi echo "" done 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 < /dev/tty 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 < /dev/tty 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 < /dev/tty 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