pve9-repo-fix/pbs_allinone.sh
Claude 86e34d6916 Fix PBS API namespace: Add ns=PQ parameter to all API calls
모든 PBS API 호출에 ns=PQ 파라미터를 추가하여 PQ 네임스페이스의 백업만 조회하도록 수정

- groups API에 ns=PQ 추가
- snapshots API에 ns=PQ 추가 (get_snapshot_comment 함수)
- snapshots API에 ns=PQ 추가 (get_latest_snapshot 함수)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 01:35:43 +00:00

630 lines
20 KiB
Bash

#!/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