import random import string import time from proxmoxer import ProxmoxAPI from .config import settings def _client() -> ProxmoxAPI: """Создаёт клиент Proxmox API, аутентифицированный по API-токену.""" host = settings.pve_host.replace("https://", "").replace("http://", "").split(":")[0] return ProxmoxAPI( host, user=settings.pve_token_name.split("!")[0], token_name=settings.pve_token_name.split("!")[1], token_value=settings.pve_token_value, verify_ssl=settings.pve_verify_ssl, ) def gen_password(length: int = 14) -> str: alphabet = string.ascii_letters + string.digits return "".join(random.choice(alphabet) for _ in range(length)) def get_next_vmid() -> int: """Берёт свободный VMID у самого Proxmox (гарантированно не занят).""" px = _client() return int(px.cluster.nextid.get()) def clone_vm(source_vmid: int, new_vmid: int, name: str, node: str = None, storage: str = None) -> None: node = node or settings.pve_node px = _client() upid = px.nodes(node).qemu(source_vmid).clone.post( newid=new_vmid, name=name, full=1, ) _wait_task(px, node, upid) def resize_vm(vmid: int, cores: int, memory_mb: int, node: str = None) -> None: node = node or settings.pve_node px = _client() px.nodes(node).qemu(vmid).config.put(cores=cores, memory=memory_mb) def create_lxc( new_vmid: int, name: str, template_volid: str, cores: int, memory_mb: int, disk_gb: int, storage: str = "local-lvm", node: str = None, ) -> str: """Создаёт LXC-контейнер из шаблона. Возвращает сгенерированный root-пароль.""" node = node or settings.pve_node px = _client() password = gen_password() upid = px.nodes(node).lxc.post( vmid=new_vmid, hostname=name, ostemplate=template_volid, cores=cores, memory=memory_mb, swap=memory_mb, rootfs=f"{storage}:{disk_gb}", password=password, net0="name=eth0,bridge=vmbr0,ip=dhcp", unprivileged=1, ) _wait_task(px, node, upid) return password def _wait_task(px: ProxmoxAPI, node: str, upid: str, timeout: int = 1200) -> None: """Ждёт завершения асинхронной задачи Proxmox (клонирование, создание и т.п.).""" start = time.time() while time.time() - start < timeout: status = px.nodes(node).tasks(upid).status.get() if status.get("status") == "stopped": if status.get("exitstatus") != "OK": raise RuntimeError(f"Задача Proxmox завершилась с ошибкой: {status}") return time.sleep(2) raise TimeoutError("Превышено время ожидания задачи Proxmox") def list_vm_templates(node=None): node = node or settings.pve_node px = _client() result = [] for vm in px.nodes(node).qemu.get(): if vm.get("template") == 1: cfg = px.nodes(node).qemu(vm["vmid"]).config.get() disk = 10 for k in ("scsi0","virtio0","ide0","sata0"): v = cfg.get(k,"") if ",size=" in v: import re as _re m = _re.search(r"size=(\d+)G", v) if m: disk = int(m.group(1)); break result.append({ "vmid": vm["vmid"], "name": vm.get("name",""), "cores": int(cfg.get("cores",1)), "memory_mb": int(cfg.get("memory",1024)), "disk_gb": disk, }) return result def list_lxc_templates(node=None): node = node or settings.pve_node px = _client() result = [] for st in px.nodes(node).storage.get(): if "vztmpl" in st.get("content",""): try: for item in px.nodes(node).storage(st["storage"]).content.get(): if item.get("content") == "vztmpl": name = item["volid"].split("/")[-1] for ext in (".tar.zst",".tar.gz",".tar.xz"): name = name.replace(ext,"") result.append({"volid": item["volid"], "name": name, "storage": st["storage"], "size_mb": round(item.get("size",0)/(1024**2),1)}) except: pass return result def resize_disk(vmid: int, disk_gb: int, node: str = None) -> None: node = node or settings.pve_node px = _client() config = px.nodes(node).qemu(vmid).config.get() for key in ("scsi0","virtio0","ide0","sata0"): val = config.get(key,"") if val: import re as _re m = _re.search(r"size=(\d+)G", val) current = int(m.group(1)) if m else 0 if disk_gb > current: px.nodes(node).qemu(vmid).resize.put(disk=key, size=f"+{disk_gb - current}G") break def configure_cloud_init(vmid: int, ciuser: str, cipassword: str, node: str = None) -> None: node = node or settings.pve_node _client().nodes(node).qemu(vmid).config.put(ciuser=ciuser, cipassword=cipassword, ipconfig0="ip=dhcp") def get_instance_ip(vmid: int, node: str = None) -> str: """Возвращает IP-адрес VM/LXC из гостевого агента или сетевой конфигурации.""" node = node or settings.pve_node px = _client() # Пробуем через гостевой агент (QEMU Guest Agent) try: ifaces = px.nodes(node).qemu(vmid).agent.get("network-get-interfaces").get("result", []) for iface in ifaces: if iface.get("name") != "lo" and iface.get("ip-addresses"): for addr in iface["ip-addresses"]: if addr.get("ip-address-type") == "ipv4" and not addr.get("ip-address", "").startswith("127."): return addr["ip-address"] except Exception: pass # Пробуем через LXC (если контейнер) try: config = px.nodes(node).lxc(vmid).config.get() # Просто пробуем разные пути для LXC except Exception: pass # Пробуем через DHCP-лиз Proxmox try: config = px.nodes(node).qemu(vmid).config.get() net = config.get("net0", "") if "dhcp" in net.lower(): return "dhcp (агент не установлен)" except Exception: pass return "неизвестен" def get_instance_ip(vmid: int, node: str = None) -> str: """Возвращает IP-адрес VM/LXC из гостевого агента или сетевой конфигурации.""" node = node or settings.pve_node px = _client() # Пробуем через гостевой агент (QEMU Guest Agent) try: ifaces = px.nodes(node).qemu(vmid).agent.get("network-get-interfaces").get("result", []) for iface in ifaces: if iface.get("name") != "lo" and iface.get("ip-addresses"): for addr in iface["ip-addresses"]: if addr.get("ip-address-type") == "ipv4" and not addr.get("ip-address", "").startswith("127."): return addr["ip-address"] except Exception: pass # Пробуем через LXC (если контейнер) try: config = px.nodes(node).lxc(vmid).config.get() # Просто пробуем разные пути для LXC except Exception: pass # Пробуем через DHCP-лиз Proxmox try: config = px.nodes(node).qemu(vmid).config.get() net = config.get("net0", "") if "dhcp" in net.lower(): return "dhcp (агент не установлен)" except Exception: pass return "неизвестен" def get_instance_ip(vmid: int, node: str = None) -> str: """Возвращает IP-адрес VM/LXC из гостевого агента или сетевой конфигурации.""" node = node or settings.pve_node px = _client() # Пробуем через гостевой агент (QEMU Guest Agent) try: ifaces = px.nodes(node).qemu(vmid).agent.get("network-get-interfaces").get("result", []) for iface in ifaces: if iface.get("name") != "lo" and iface.get("ip-addresses"): for addr in iface["ip-addresses"]: if addr.get("ip-address-type") == "ipv4" and not addr.get("ip-address", "").startswith("127."): return addr["ip-address"] except Exception: pass # Пробуем через LXC (если контейнер) try: config = px.nodes(node).lxc(vmid).config.get() # Просто пробуем разные пути для LXC except Exception: pass # Пробуем через DHCP-лиз Proxmox try: config = px.nodes(node).qemu(vmid).config.get() net = config.get("net0", "") if "dhcp" in net.lower(): return "dhcp (агент не установлен)" except Exception: pass return "неизвестен" def get_live_stats(vmid: int, node: str = None) -> dict: """Возвращает live-показатели: cpu%, ram_used, ram_total, uptime.""" node = node or settings.pve_node px = _client() try: status = px.nodes(node).qemu(vmid).status.current.get() return { "cpu": round(status.get("cpu", 0) * 100, 1), "mem_used": status.get("mem", 0), "mem_total": status.get("maxmem", 0), "uptime": status.get("uptime", 0), "status": status.get("status", "unknown"), } except Exception: try: status = px.nodes(node).lxc(vmid).status.current.get() return { "cpu": round(status.get("cpu", 0) * 100, 1), "mem_used": status.get("mem", 0), "mem_total": status.get("maxmem", 0), "uptime": status.get("uptime", 0), "status": status.get("status", "unknown"), } except Exception: return {"cpu": 0, "mem_used": 0, "mem_total": 0, "uptime": 0, "status": "unknown"} def get_live_stats_lxc(vmid: int, node: str = None) -> dict: """Live-показатели LXC контейнера.""" return get_live_stats(vmid, node) def guest_action(guest_type: str, vmid: int, action: str, node: str = None) -> None: """action: start | stop | shutdown | reboot""" node = node or settings.pve_node px = _client() endpoint = px.nodes(node).qemu(vmid) if guest_type == "vm" else px.nodes(node).lxc(vmid) if action == "start": endpoint.status.start.post() elif action == "stop": endpoint.status.stop.post() elif action == "shutdown": endpoint.status.shutdown.post() elif action == "reboot": endpoint.status.reboot.post() else: raise ValueError(f"Неизвестное действие: {action}") def delete_guest(guest_type: str, vmid: int, node: str = None) -> None: node = node or settings.pve_node px = _client() if guest_type == "vm": px.nodes(node).qemu(vmid).delete() else: px.nodes(node).lxc(vmid).delete() def get_status(guest_type: str, vmid: int, node: str = None) -> dict: node = node or settings.pve_node px = _client() if guest_type == "vm": return px.nodes(node).qemu(vmid).status.current.get() return px.nodes(node).lxc(vmid).status.current.get() def get_vnc_ticket(guest_type: str, vmid: int, node: str = None) -> dict: """Запрашивает у Proxmox тикет для VNC/websocket-консоли.""" node = node or settings.pve_node px = _client() endpoint = px.nodes(node).qemu(vmid) if guest_type == "vm" else px.nodes(node).lxc(vmid) result = endpoint.vncproxy.post(websocket=1) return { "ticket": result["ticket"], "port": result["port"], "node": node, "vmid": vmid, "guest_type": guest_type, "pve_host": settings.pve_host, }