Add multi-host Proxmox support with SSL certificate handling
- Added support for multiple Proxmox hosts (pve7.0bin.in:443, Healthport PVE:8006) - Enhanced VM management APIs to accept host parameter - Fixed WebSocket URL generation bug (dynamic port handling) - Added comprehensive SSL certificate trust help system - Implemented host selection dropdown in UI - Added VNC connection failure detection and automatic SSL help redirection - Updated session management to store host_key information - Enhanced error handling for different Proxmox configurations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,19 @@ from typing import Dict, List, Optional, Tuple
|
||||
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
|
||||
def __init__(self, host: str, username: str = "root@pam", password: str = "", api_token: str = "", port: int = 8006):
|
||||
# 호스트에서 포트가 포함된 경우 분리
|
||||
if ':' in host:
|
||||
self.host, port_str = host.split(':')
|
||||
self.port = int(port_str)
|
||||
else:
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.api_token = api_token
|
||||
self.base_url = f"https://{host}:443/api2/json"
|
||||
self.base_url = f"https://{self.host}:{self.port}/api2/json"
|
||||
self.session = requests.Session()
|
||||
self.session.verify = False
|
||||
self.ticket = None
|
||||
@@ -134,14 +141,14 @@ class ProxmoxClient:
|
||||
vnc_data = response.json()['data']
|
||||
print(f"✅ VNC 티켓 생성 성공: {vnc_data}")
|
||||
|
||||
# WebSocket URL 생성 (인증 토큰 포함)
|
||||
# WebSocket URL 생성 (동적 포트 및 CSRF 토큰 포함)
|
||||
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}"
|
||||
vnc_data['websocket_url'] = f"wss://{self.host}:{self.port}/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}"
|
||||
vnc_data['websocket_url'] = f"wss://{self.host}:{self.port}/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={vnc_data['port']}&vncticket={encoded_ticket}"
|
||||
|
||||
# 디버깅 정보 추가
|
||||
print(f"🔗 WebSocket URL: {vnc_data['websocket_url']}")
|
||||
|
||||
168
farmq-admin/utils/vnc_proxy.py
Normal file
168
farmq-admin/utils/vnc_proxy.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
VNC WebSocket 프록시
|
||||
브라우저와 Proxmox VNC 서버 간 WebSocket 연결을 중계하며
|
||||
PVE 인증을 자동으로 처리합니다.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import websockets
|
||||
import ssl
|
||||
import logging
|
||||
from utils.proxmox_client import ProxmoxClient
|
||||
|
||||
# 로깅 설정
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class VNCWebSocketProxy:
|
||||
def __init__(self, proxmox_host, proxmox_username, proxmox_password):
|
||||
self.proxmox_host = proxmox_host
|
||||
self.proxmox_username = proxmox_username
|
||||
self.proxmox_password = proxmox_password
|
||||
self.proxmox_client = None
|
||||
|
||||
async def create_proxmox_client(self):
|
||||
"""Proxmox 클라이언트 생성 및 로그인"""
|
||||
if not self.proxmox_client:
|
||||
self.proxmox_client = ProxmoxClient(
|
||||
self.proxmox_host,
|
||||
self.proxmox_username,
|
||||
self.proxmox_password
|
||||
)
|
||||
|
||||
if not self.proxmox_client.login():
|
||||
logger.error("Proxmox 로그인 실패")
|
||||
return False
|
||||
|
||||
logger.info("Proxmox 로그인 성공")
|
||||
return True
|
||||
|
||||
async def get_vnc_connection_info(self, node, vm_id):
|
||||
"""VNC 연결 정보 생성"""
|
||||
if not await self.create_proxmox_client():
|
||||
return None
|
||||
|
||||
# VM 상태 확인
|
||||
vm_status = self.proxmox_client.get_vm_status(node, vm_id)
|
||||
if vm_status.get('status') != 'running':
|
||||
logger.error(f"VM {vm_id}가 실행 중이 아님: {vm_status.get('status')}")
|
||||
return None
|
||||
|
||||
# VNC 티켓 생성
|
||||
vnc_data = self.proxmox_client.get_vnc_ticket(node, vm_id)
|
||||
if not vnc_data:
|
||||
logger.error(f"VM {vm_id} VNC 티켓 생성 실패")
|
||||
return None
|
||||
|
||||
# WebSocket 연결 정보 준비
|
||||
connection_info = {
|
||||
'websocket_url': vnc_data['websocket_url'],
|
||||
'password': vnc_data['password'],
|
||||
'auth_headers': {
|
||||
'Cookie': f'PVEAuthCookie={self.proxmox_client.ticket}'
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"VM {vm_id} VNC 연결 정보 생성 완료")
|
||||
return connection_info
|
||||
|
||||
async def create_proxmox_websocket(self, connection_info):
|
||||
"""Proxmox VNC WebSocket 연결 생성"""
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
try:
|
||||
websocket = await websockets.connect(
|
||||
connection_info['websocket_url'],
|
||||
ssl=ssl_context,
|
||||
additional_headers=connection_info['auth_headers']
|
||||
)
|
||||
logger.info("Proxmox VNC WebSocket 연결 성공")
|
||||
return websocket
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Proxmox VNC WebSocket 연결 실패: {e}")
|
||||
return None
|
||||
|
||||
async def proxy_data(self, browser_ws, proxmox_ws):
|
||||
"""브라우저와 Proxmox 간 WebSocket 데이터 양방향 중계"""
|
||||
async def forward_browser_to_proxmox():
|
||||
"""브라우저 → Proxmox 데이터 전달"""
|
||||
try:
|
||||
async for message in browser_ws:
|
||||
await proxmox_ws.send(message)
|
||||
logger.debug(f"브라우저 → Proxmox: {len(message)} bytes")
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.info("브라우저 연결 종료")
|
||||
except Exception as e:
|
||||
logger.error(f"브라우저 → Proxmox 전달 오류: {e}")
|
||||
|
||||
async def forward_proxmox_to_browser():
|
||||
"""Proxmox → 브라우저 데이터 전달"""
|
||||
try:
|
||||
async for message in proxmox_ws:
|
||||
await browser_ws.send(message)
|
||||
logger.debug(f"Proxmox → 브라우저: {len(message)} bytes")
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.info("Proxmox 연결 종료")
|
||||
except Exception as e:
|
||||
logger.error(f"Proxmox → 브라우저 전달 오류: {e}")
|
||||
|
||||
# 양방향 데이터 전달을 병렬로 실행
|
||||
await asyncio.gather(
|
||||
forward_browser_to_proxmox(),
|
||||
forward_proxmox_to_browser(),
|
||||
return_exceptions=True
|
||||
)
|
||||
|
||||
async def handle_vnc_proxy(self, browser_websocket, node, vm_id):
|
||||
"""VNC 프록시 메인 핸들러"""
|
||||
logger.info(f"VNC 프록시 시작: VM {vm_id}")
|
||||
|
||||
try:
|
||||
# 1. Proxmox VNC 연결 정보 생성
|
||||
connection_info = await self.get_vnc_connection_info(node, vm_id)
|
||||
if not connection_info:
|
||||
await browser_websocket.send("ERROR: VNC 연결 정보 생성 실패")
|
||||
return False
|
||||
|
||||
# 2. Proxmox VNC WebSocket 연결
|
||||
proxmox_websocket = await self.create_proxmox_websocket(connection_info)
|
||||
if not proxmox_websocket:
|
||||
await browser_websocket.send("ERROR: Proxmox VNC 연결 실패")
|
||||
return False
|
||||
|
||||
# 3. 연결 성공 알림
|
||||
logger.info(f"VM {vm_id} VNC 프록시 연결 완료")
|
||||
|
||||
# 4. 데이터 중계 시작
|
||||
await self.proxy_data(browser_websocket, proxmox_websocket)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"VNC 프록시 처리 오류: {e}")
|
||||
try:
|
||||
await browser_websocket.send(f"ERROR: {str(e)}")
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
finally:
|
||||
logger.info(f"VNC 프록시 종료: VM {vm_id}")
|
||||
|
||||
|
||||
# 전역 VNC 프록시 인스턴스 (설정값은 app.py에서 주입)
|
||||
vnc_proxy_instance = None
|
||||
|
||||
def init_vnc_proxy(proxmox_host, proxmox_username, proxmox_password):
|
||||
"""VNC 프록시 인스턴스 초기화"""
|
||||
global vnc_proxy_instance
|
||||
vnc_proxy_instance = VNCWebSocketProxy(proxmox_host, proxmox_username, proxmox_password)
|
||||
logger.info("VNC WebSocket 프록시 초기화 완료")
|
||||
|
||||
def get_vnc_proxy():
|
||||
"""VNC 프록시 인스턴스 반환"""
|
||||
global vnc_proxy_instance
|
||||
return vnc_proxy_instance
|
||||
Reference in New Issue
Block a user