WebSocket 1006 오류로 인해 브라우저에서 VNC 연결 실패 - 서버 환경에서는 연결 가능하나 브라우저 보안 정책으로 차단 - 역방향 프록시 솔루션 문서화 완료 - 추후 nginx 프록시 구현 필요 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
194 lines
7.0 KiB
Python
194 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Proxmox VE API 클라이언트
|
|
"""
|
|
|
|
import requests
|
|
import json
|
|
import urllib3
|
|
from urllib.parse import quote_plus
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
# SSL 경고 무시
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
class ProxmoxClient:
|
|
def __init__(self, host: str, username: str = "root@pam", password: str = "", api_token: str = ""):
|
|
self.host = host
|
|
self.username = username
|
|
self.password = password
|
|
self.api_token = api_token
|
|
self.base_url = f"https://{host}:443/api2/json"
|
|
self.session = requests.Session()
|
|
self.session.verify = False
|
|
self.ticket = None
|
|
self.csrf_token = None
|
|
|
|
def login(self) -> bool:
|
|
"""세션 쿠키 방식으로 로그인"""
|
|
if self.api_token:
|
|
# API Token 방식
|
|
self.session.headers.update({
|
|
'Authorization': f'PVEAPIToken={self.api_token}'
|
|
})
|
|
return self._test_connection()
|
|
else:
|
|
# 패스워드 방식
|
|
login_data = {
|
|
'username': self.username,
|
|
'password': self.password
|
|
}
|
|
|
|
try:
|
|
response = self.session.post(
|
|
f"{self.base_url}/access/ticket",
|
|
data=login_data,
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()['data']
|
|
self.ticket = data['ticket']
|
|
self.csrf_token = data['CSRFPreventionToken']
|
|
|
|
# 쿠키와 헤더 설정
|
|
self.session.cookies.update({'PVEAuthCookie': self.ticket})
|
|
self.session.headers.update({'CSRFPreventionToken': self.csrf_token})
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"로그인 실패: {e}")
|
|
return False
|
|
|
|
return False
|
|
|
|
def _test_connection(self) -> bool:
|
|
"""연결 테스트"""
|
|
try:
|
|
response = self.session.get(f"{self.base_url}/version", timeout=10)
|
|
return response.status_code == 200
|
|
except:
|
|
return False
|
|
|
|
def get_vm_list(self) -> List[Dict]:
|
|
"""VM 목록 조회"""
|
|
try:
|
|
response = self.session.get(
|
|
f"{self.base_url}/cluster/resources?type=vm",
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()['data']
|
|
|
|
except Exception as e:
|
|
print(f"VM 목록 조회 실패: {e}")
|
|
|
|
return []
|
|
|
|
def get_vm_status(self, node: str, vmid: int) -> Dict:
|
|
"""특정 VM 상태 확인"""
|
|
try:
|
|
response = self.session.get(
|
|
f"{self.base_url}/nodes/{node}/qemu/{vmid}/status/current",
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return response.json()['data']
|
|
|
|
except Exception as e:
|
|
print(f"VM {vmid} 상태 조회 실패: {e}")
|
|
|
|
return {}
|
|
|
|
def get_vnc_ticket(self, node: str, vmid: int) -> Optional[Dict]:
|
|
"""VNC 접속 티켓 생성"""
|
|
try:
|
|
# 먼저 VM 상태 확인
|
|
vm_status = self.get_vm_status(node, vmid)
|
|
print(f"🔍 VM {vmid} 상태: {vm_status}")
|
|
|
|
if vm_status.get('status') != 'running':
|
|
print(f"❌ VM {vmid}이 실행중이 아닙니다. 현재 상태: {vm_status.get('status', 'unknown')}")
|
|
return None
|
|
|
|
data = {
|
|
'websocket': '1',
|
|
'generate-password': '1' # 패스워드 생성 활성화
|
|
}
|
|
|
|
print(f"🔄 VNC 티켓 요청: {self.base_url}/nodes/{node}/qemu/{vmid}/vncproxy")
|
|
|
|
response = self.session.post(
|
|
f"{self.base_url}/nodes/{node}/qemu/{vmid}/vncproxy",
|
|
data=data,
|
|
timeout=10
|
|
)
|
|
|
|
print(f"📡 VNC 티켓 응답 상태: {response.status_code}")
|
|
print(f"📄 VNC 티켓 응답 내용: {response.text}")
|
|
|
|
if response.status_code == 200:
|
|
vnc_data = response.json()['data']
|
|
print(f"✅ VNC 티켓 생성 성공: {vnc_data}")
|
|
|
|
# WebSocket URL 생성 (인증 토큰 포함)
|
|
encoded_ticket = quote_plus(vnc_data['ticket'])
|
|
# Proxmox 세션 쿠키도 함께 포함 (CSRFPreventionToken도 필요할 수 있음)
|
|
csrf_token = getattr(self, 'csrf_token', None)
|
|
if csrf_token:
|
|
vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}&CSRFPreventionToken={csrf_token}"
|
|
else:
|
|
vnc_data['websocket_url'] = f"wss://{self.host}:443/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}"
|
|
|
|
# 디버깅 정보 추가
|
|
print(f"🔗 WebSocket URL: {vnc_data['websocket_url']}")
|
|
print(f"🔑 VNC Password: {vnc_data.get('password', 'N/A')}")
|
|
return vnc_data
|
|
else:
|
|
print(f"❌ VNC 티켓 생성 HTTP 오류: {response.status_code}")
|
|
print(f"Response: {response.text}")
|
|
|
|
except Exception as e:
|
|
print(f"❌ VNC 티켓 생성 실패: {e}")
|
|
|
|
return None
|
|
|
|
def start_vm(self, node: str, vmid: int) -> bool:
|
|
"""VM 시작"""
|
|
try:
|
|
response = self.session.post(
|
|
f"{self.base_url}/nodes/{node}/qemu/{vmid}/status/start",
|
|
timeout=30
|
|
)
|
|
return response.status_code == 200
|
|
|
|
except Exception as e:
|
|
print(f"VM {vmid} 시작 실패: {e}")
|
|
return False
|
|
|
|
def stop_vm(self, node: str, vmid: int) -> bool:
|
|
"""VM 정지"""
|
|
try:
|
|
response = self.session.post(
|
|
f"{self.base_url}/nodes/{node}/qemu/{vmid}/status/stop",
|
|
timeout=30
|
|
)
|
|
return response.status_code == 200
|
|
|
|
except Exception as e:
|
|
print(f"VM {vmid} 정지 실패: {e}")
|
|
return False
|
|
|
|
def get_nodes(self) -> List[Dict]:
|
|
"""노드 목록 조회"""
|
|
try:
|
|
response = self.session.get(f"{self.base_url}/nodes", timeout=10)
|
|
if response.status_code == 200:
|
|
return response.json()['data']
|
|
except Exception as e:
|
|
print(f"노드 목록 조회 실패: {e}")
|
|
|
|
return [] |