163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
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()
|