From 5df0cb9cbfb6c77a6ad254f1e4dab41c6a6b6155 Mon Sep 17 00:00:00 2001 From: host Date: Sat, 25 Jul 2026 03:29:36 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/proxmox_client.py | 302 +--------------------------------- 1 file changed, 1 insertion(+), 301 deletions(-) diff --git a/backend/app/proxmox_client.py b/backend/app/proxmox_client.py index a57cdd1..871ff68 100644 --- a/backend/app/proxmox_client.py +++ b/backend/app/proxmox_client.py @@ -1,307 +1,7 @@ -"""Обёртка над Proxmox REST API. - -Все методы — синхронные (proxmoxer работает через requests). Используйте -внутри FastAPI через run_in_threadpool, если эндпоинт async. -""" - -import logging -import random -import re -import string -import time - -from proxmoxer import ProxmoxAPI - -from .config import settings - -logger = logging.getLogger(__name__) - - -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: - """Клонирует VM-шаблон в новую VM.""" - 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: - """Изменяет количество vCPU и RAM у VM.""" - 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_mb=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, poll: float = 2.0) -> None: - """Ждёт завершения асинхронной задачи Proxmox с экспоненциальной задержкой. - - Начинаем с poll=1s и удваиваем до 5s — снижает нагрузку на Proxmox. - """ - start = time.time() - delay = poll - 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(delay) - delay = min(delay * 1.5, 5.0) - raise TimeoutError("Превышено время ожидания задачи Proxmox") - - -_DISK_KEYS = ("scsi0", "virtio0", "ide0", "sata0") - - -def _disk_size_from_config(cfg: dict) -> int: - """Извлекает размер диска (ГБ) из конфигурации VM, иначе 10.""" - for k in _DISK_KEYS: - v = cfg.get(k, "") - if ",size=" in v: - m = re.search(r"size=(\d+)G", v) - if m: - return int(m.group(1)) - return 10 - - -def list_vm_templates(node: str = None) -> list: - """Возвращает список VM-шаблонов (template=1) с параметрами.""" - 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() - 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_size_from_config(cfg), - }) - return result - - -def list_lxc_templates(node: str = None) -> list: - """Возвращает список LXC-шаблонов (.tar.zst/.tar.gz/.tar.xz) из всех хранилищ.""" - node = node or settings.pve_node - px = _client() - result = [] - for st in px.nodes(node).storage.get(): - if "vztmpl" not in st.get("content", ""): - continue - 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 Exception as exc: - logger.warning("Не удалось прочитать шаблоны с хранилища %s: %s", st["storage"], exc) - return result - - -def resize_disk(vmid: int, disk_gb: int, node: str = None) -> None: - """Увеличивает диск VM до нужного размера (только в большую сторону).""" - node = node or settings.pve_node - px = _client() - config = px.nodes(node).qemu(vmid).config.get() - for key in _DISK_KEYS: - val = config.get(key, "") - if not val: - continue - 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") - return - logger.warning("У VM %s не найден ни один диск для resize", vmid) - - -def configure_cloud_init(vmid: int, ciuser: str, cipassword: str, node: str = None) -> None: - """Задаёт cloud-init user/password и переключает сеть на DHCP.""" - 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: - """Возвращает основной IPv4 инстанса. - - Для VM используется QEMU Guest Agent. Если агент недоступен — возвращает - строку-плейсхолдер, чтобы UI мог отличить «не знаю» от «ошибка». - """ - node = node or settings.pve_node - px = _client() - # 1. QEMU Guest Agent (требует установленного 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": - continue - for addr in iface.get("ip-addresses", []) or []: - ip = addr.get("ip-address", "") - if addr.get("ip-address-type") == "ipv4" and not ip.startswith("127."): - return ip - except Exception as exc: - logger.debug("VM %s: guest-agent недоступен (%s)", vmid, exc) - - # 2. LXC — адрес можно увидеть через конфиг + интерфейсы внутри. - try: - ifaces = px.nodes(node).lxc(vmid).interfaces.get() or [] - for iface in ifaces: - for ip in iface.get("ip-addresses", []) or []: - if not ip.startswith("127."): - return ip - except Exception as exc: - logger.debug("LXC %s: интерфейсы недоступны (%s)", vmid, exc) - - # 3. Fallback: понятная заглушка вместо молчаливого «неизвестен». - return "не определён (агент недоступен)" - - -def get_live_stats(vmid: int, node: str = None) -> dict: - """Live-показатели (cpu%, mem_used, mem_total, uptime, status) для VM и LXC.""" - node = node or settings.pve_node - px = _client() - - def _shape(status: dict) -> dict: - 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"), - } - - for endpoint in (px.nodes(node).qemu(vmid).status.current, px.nodes(node).lxc(vmid).status.current): - try: - return _shape(endpoint.get()) - except Exception: - continue - return {"cpu": 0, "mem_used": 0, "mem_total": 0, "uptime": 0, "status": "unknown"} - - -# Алиас для обратной совместимости со старым кодом. -get_live_stats_lxc = get_live_stats - - -_VALID_ACTIONS = {"start", "stop", "shutdown", "reboot"} - - -def guest_action(guest_type: str, vmid: int, action: str, node: str = None) -> None: - """Выполняет действие над VM/LXC: start | stop | shutdown | reboot.""" - if action not in _VALID_ACTIONS: - raise ValueError(f"Неизвестное действие: {action}") - node = node or settings.pve_node - px = _client() - endpoint = px.nodes(node).qemu(vmid) if guest_type == "vm" else px.nodes(node).lxc(vmid) - getattr(endpoint.status, action).post() - - -def delete_guest(guest_type: str, vmid: int, node: str = None) -> None: - """Удаляет VM или LXC (синхронно — Proxmox дожидается завершения).""" - 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: """Текущий статус VM/LXC (running, stopped, …).""" 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-консоли. - - Тикет привязан к origin (схема+хост+порт), через который был сделан - запрос к Proxmox. - """ - 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, - } + return px.nodes(node).lxc(vmid).status.current.get() \ No newline at end of file