import re import time from urllib.parse import urlparse from proxmoxer import ProxmoxAPI from .config import settings def _client() -> ProxmoxAPI: """Создаёт Proxmox API client по отдельному токену панели.""" parsed = urlparse(settings.pve_host) if not parsed.hostname: raise RuntimeError("PVE_HOST имеет некорректный формат") if "!" not in settings.pve_token_name: raise RuntimeError("PVE_TOKEN_NAME должен иметь формат user@realm!token") user, token_name = settings.pve_token_name.split("!", 1) return ProxmoxAPI( parsed.hostname, port=parsed.port or 8006, user=user, token_name=token_name, token_value=settings.pve_token_value.get_secret_value(), verify_ssl=settings.pve_verify_ssl, ) def _wait_task(client: ProxmoxAPI, node: str, upid: str, timeout: int = 1800) -> None: """Ожидает завершения задачи Proxmox с ограничением времени.""" deadline = time.monotonic() + timeout while time.monotonic() < deadline: task = client.nodes(node).tasks(upid).status.get() if task.get("status") == "stopped": if task.get("exitstatus") != "OK": raise RuntimeError("Задача Proxmox завершилась с ошибкой") return time.sleep(2) raise TimeoutError("Превышено время ожидания задачи Proxmox") def next_vmid() -> int: """Запрашивает следующий свободный VMID у Proxmox.""" return int(_client().cluster.nextid.get()) def provision_vm( source_vmid: int, vmid: int, name: str, cores: int, memory_mb: int, disk_gb: int, username: str, password: str, ) -> None: """Клонирует cloud-init VM, настраивает ресурсы и запускает её.""" client = _client() node = settings.pve_node params = {"newid": vmid, "name": name, "full": 1} if settings.pve_vm_storage: params["storage"] = settings.pve_vm_storage upid = client.nodes(node).qemu(source_vmid).clone.post(**params) _wait_task(client, node, upid) client.nodes(node).qemu(vmid).config.put( cores=cores, memory=memory_mb, ciuser=username, cipassword=password, ipconfig0="ip=dhcp", ) # Увеличиваем диск только если шаблон меньше заданного размера. config = client.nodes(node).qemu(vmid).config.get() for key in ("scsi0", "virtio0", "sata0", "ide0"): value = config.get(key, "") match = re.search(r"size=(\d+)G", value) if match and disk_gb > int(match.group(1)): client.nodes(node).qemu(vmid).resize.put( disk=key, size=f"+{disk_gb - int(match.group(1))}G" ) break client.nodes(node).qemu(vmid).status.start.post() def provision_lxc( source_template: str, vmid: int, name: str, cores: int, memory_mb: int, disk_gb: int, password: str, ) -> None: """Создаёт LXC из CT-шаблона и запускает его с DHCP.""" client = _client() node = settings.pve_node upid = client.nodes(node).lxc.post( vmid=vmid, hostname=name, ostemplate=source_template, cores=cores, memory=memory_mb, swap=memory_mb, rootfs=f"{settings.pve_lxc_storage}:{disk_gb}", password=password, net0=f"name=eth0,bridge={settings.pve_bridge},ip=dhcp", unprivileged=1, ) _wait_task(client, node, upid) client.nodes(node).lxc(vmid).status.start.post()