import httpx import asyncio import logging from typing import List, Optional, Dict from app.config import settings import json logger = logging.getLogger(__name__) class ProxmoxService: """Proxmox VE API 통신 서비스""" def __init__(self): self.base_url = settings.PROXMOX_HOST self.api_token = settings.PROXMOX_API_TOKEN self.verify_ssl = settings.PROXMOX_VERIFY_SSL async def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict: """Proxmox API 요청""" url = f"{self.base_url}/api2/json{endpoint}" headers = {"Authorization": self.api_token} try: async with httpx.AsyncClient(verify=self.verify_ssl, timeout=30.0) as client: response = await client.request(method, url, headers=headers, **kwargs) response.raise_for_status() return response.json() except httpx.HTTPStatusError as e: logger.error(f"Proxmox API HTTP Error: {e.response.status_code} - {e.response.text}") raise except httpx.RequestError as e: logger.error(f"Proxmox API Request Error: {e}") raise except Exception as e: logger.error(f"Proxmox API Unexpected Error: {e}") raise async def get_nodes(self) -> List[Dict]: """노드 목록 조회""" result = await self._make_request("GET", "/nodes") return result.get("data", []) async def get_vms(self, node: str) -> List[Dict]: """특정 노드의 VM 목록 조회""" result = await self._make_request("GET", f"/nodes/{node}/qemu") return result.get("data", []) async def get_all_vms(self) -> List[Dict]: """모든 노드의 VM 및 LXC 목록 조회 (클러스터 리소스 API 사용)""" try: # /cluster/resources 호출 (type=vm은 qemu와 lxc 모두 포함) result = await self._make_request("GET", "/cluster/resources?type=vm") resources = result.get("data", []) logger.info(f"Total resources fetched: {len(resources)}") return resources except Exception as e: logger.error(f"Failed to get cluster resources: {e}") return [] async def get_vm_status(self, node: str, vm_id: int, vm_type: str = "qemu") -> Dict: """VM/LXC 상태 조회""" try: result = await self._make_request("GET", f"/nodes/{node}/{vm_type}/{vm_id}/status/current") return result.get("data", {}) except Exception as e: logger.error(f"Failed to get status for {vm_type} {vm_id}: {e}") return {} async def get_vm_ip(self, node: str, vm_id: int, vm_type: str = "qemu") -> Optional[str]: """QEMU Guest Agent를 통해 VM IP 주소 조회 (LXC는 미지원)""" if vm_type != "qemu": return None try: result = await self._make_request( "GET", f"/nodes/{node}/qemu/{vm_id}/agent/network-get-interfaces" ) interfaces = result.get("data", {}).get("result", []) for iface in interfaces: # loopback 제외 if "loopback" in iface.get("name", "").lower(): continue ip_addresses = iface.get("ip-addresses", []) for ip in ip_addresses: if ip.get("ip-address-type") == "ipv4": address = ip.get("ip-address") # 사설 IP만 반환 if self._is_private_ip(address): return address return None except Exception as e: logger.warning(f"Failed to get VM IP (Node: {node}, VMID: {vm_id}): {e}") return None async def start_vm(self, node: str, vm_id: int, vm_type: str = "qemu") -> bool: """VM/LXC 시작""" try: await self._make_request("POST", f"/nodes/{node}/{vm_type}/{vm_id}/status/start") logger.info(f"{vm_type} started: {vm_id} (Node: {node})") return True except Exception as e: logger.error(f"Failed to start {vm_type} {vm_id}: {e}") return False async def stop_vm(self, node: str, vm_id: int, vm_type: str = "qemu") -> bool: """VM/LXC 종료""" try: await self._make_request("POST", f"/nodes/{node}/{vm_type}/{vm_id}/status/stop") logger.info(f"{vm_type} stopped: {vm_id} (Node: {node})") return True except Exception as e: logger.error(f"Failed to stop {vm_type} {vm_id}: {e}") return False async def reboot_vm(self, node: str, vm_id: int, vm_type: str = "qemu") -> bool: """VM/LXC 재시작""" try: await self._make_request("POST", f"/nodes/{node}/{vm_type}/{vm_id}/status/reboot") logger.info(f"{vm_type} rebooted: {vm_id} (Node: {node})") return True except Exception as e: logger.error(f"Failed to reboot {vm_type} {vm_id}: {e}") return False def _is_private_ip(self, ip: str) -> bool: """사설 IP 주소 확인""" if not ip: return False parts = ip.split(".") if len(parts) != 4: return False try: first = int(parts[0]) second = int(parts[1]) # 10.0.0.0/8 if first == 10: return True # 172.16.0.0/12 if first == 172 and 16 <= second <= 31: return True # 192.168.0.0/16 if first == 192 and second == 168: return True return False except: return False # 싱글톤 인스턴스 proxmox_service = ProxmoxService()