133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
|
|
"""Proxmox VE: API через proxmoxer, ноды, VM/LXC, управление питанием."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import List, Optional
|
||
|
|
|
||
|
|
from app.db.database import get_session
|
||
|
|
from app.db.models import Setting
|
||
|
|
|
||
|
|
|
||
|
|
def _get(key: str) -> str:
|
||
|
|
with get_session() as s:
|
||
|
|
row = s.get(Setting, key)
|
||
|
|
return row.value if row else ""
|
||
|
|
|
||
|
|
|
||
|
|
def _set(key: str, value: str) -> None:
|
||
|
|
with get_session() as s:
|
||
|
|
row = s.get(Setting, key) or Setting(key=key)
|
||
|
|
row.value = value
|
||
|
|
s.add(row)
|
||
|
|
s.commit()
|
||
|
|
|
||
|
|
|
||
|
|
def get_config() -> dict:
|
||
|
|
return {
|
||
|
|
"host": _get("pve_host"),
|
||
|
|
"user": _get("pve_user") or "root@pam",
|
||
|
|
"token_name": _get("pve_token_name"),
|
||
|
|
"token_value": _get("pve_token_value"),
|
||
|
|
"verify_ssl": _get("pve_verify_ssl") == "1",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def save_config(host: str, user: str, token_name: str, token_value: str,
|
||
|
|
verify_ssl: bool) -> None:
|
||
|
|
_set("pve_host", host.rstrip("/"))
|
||
|
|
_set("pve_user", user)
|
||
|
|
_set("pve_token_name", token_name)
|
||
|
|
_set("pve_token_value", token_value)
|
||
|
|
_set("pve_verify_ssl", "1" if verify_ssl else "0")
|
||
|
|
|
||
|
|
|
||
|
|
def _client():
|
||
|
|
"""Создать proxmoxer-клиент. Бросает если не настроено."""
|
||
|
|
from proxmoxer import ProxmoxAPI
|
||
|
|
cfg = get_config()
|
||
|
|
if not cfg["host"] or not cfg["token_name"] or not cfg["token_value"]:
|
||
|
|
raise RuntimeError("Proxmox не настроен — заполните хост и API-токен")
|
||
|
|
# убрать http:// или https:// если пользователь ввёл
|
||
|
|
host = cfg["host"].replace("http://", "").replace("https://", "").rstrip("/")
|
||
|
|
return ProxmoxAPI(
|
||
|
|
host, user=cfg["user"],
|
||
|
|
token_name=cfg["token_name"], token_value=cfg["token_value"],
|
||
|
|
verify_ssl=cfg["verify_ssl"], timeout=15,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_connection() -> tuple[bool, str]:
|
||
|
|
try:
|
||
|
|
pve = _client()
|
||
|
|
version = pve.version.get()
|
||
|
|
return True, f"Proxmox VE {version.get('version', '?')}"
|
||
|
|
except Exception as e:
|
||
|
|
return False, str(e)
|
||
|
|
|
||
|
|
|
||
|
|
def nodes() -> List[dict]:
|
||
|
|
return _client().nodes.get()
|
||
|
|
|
||
|
|
|
||
|
|
def node_status(node: str) -> dict:
|
||
|
|
return _client().nodes(node).status.get()
|
||
|
|
|
||
|
|
|
||
|
|
def vms(node: str) -> List[dict]:
|
||
|
|
"""Все VM и LXC на ноде."""
|
||
|
|
pve = _client()
|
||
|
|
qemu = [{"type": "qemu", **x} for x in pve.nodes(node).qemu.get()]
|
||
|
|
lxc = [{"type": "lxc", **x} for x in pve.nodes(node).lxc.get()]
|
||
|
|
return qemu + lxc
|
||
|
|
|
||
|
|
|
||
|
|
def vm_status(node: str, vmid: int, vm_type: str) -> dict:
|
||
|
|
pve = _client()
|
||
|
|
if vm_type == "qemu":
|
||
|
|
return pve.nodes(node).qemu(vmid).status.current.get()
|
||
|
|
return pve.nodes(node).lxc(vmid).status.current.get()
|
||
|
|
|
||
|
|
|
||
|
|
def vm_action(node: str, vmid: int, vm_type: str, action: str) -> str:
|
||
|
|
"""start/stop/shutdown/reboot/reset. Возвращает UPID задачи."""
|
||
|
|
assert action in ("start", "stop", "shutdown", "reboot", "reset")
|
||
|
|
pve = _client()
|
||
|
|
api = pve.nodes(node).qemu(vmid) if vm_type == "qemu" else pve.nodes(node).lxc(vmid)
|
||
|
|
return getattr(api.status, action).post()
|
||
|
|
|
||
|
|
|
||
|
|
def vm_config(node: str, vmid: int, vm_type: str) -> dict:
|
||
|
|
pve = _client()
|
||
|
|
if vm_type == "qemu":
|
||
|
|
return pve.nodes(node).qemu(vmid).config.get()
|
||
|
|
return pve.nodes(node).lxc(vmid).config.get()
|
||
|
|
|
||
|
|
|
||
|
|
def vm_rrd(node: str, vmid: int, vm_type: str, timeframe: str = "hour") -> list:
|
||
|
|
"""Метрики из RRD (cpu, mem, netin, netout...)."""
|
||
|
|
pve = _client()
|
||
|
|
api = pve.nodes(node).qemu(vmid) if vm_type == "qemu" else pve.nodes(node).lxc(vmid)
|
||
|
|
return api.rrd.get(timeframe=timeframe, cf="AVERAGE")
|
||
|
|
|
||
|
|
|
||
|
|
def get_vm_ip(node: str, vmid: int, vm_type: str) -> Optional[str]:
|
||
|
|
"""IP из QEMU Guest Agent или из конфига LXC."""
|
||
|
|
try:
|
||
|
|
pve = _client()
|
||
|
|
if vm_type == "qemu":
|
||
|
|
data = pve.nodes(node).qemu(vmid).agent("network-get-interfaces").get()
|
||
|
|
for iface in data.get("result", []):
|
||
|
|
for addr in iface.get("ip-addresses", []):
|
||
|
|
ip = addr.get("ip-address", "")
|
||
|
|
if addr.get("ip-address-type") == "ipv4" and not ip.startswith("127."):
|
||
|
|
return ip
|
||
|
|
else:
|
||
|
|
cfg = pve.nodes(node).lxc(vmid).config.get()
|
||
|
|
net0 = cfg.get("net0", "")
|
||
|
|
# "name=eth0,bridge=vmbr0,ip=192.168.1.10/24,gw=..."
|
||
|
|
for part in net0.split(","):
|
||
|
|
if part.startswith("ip=") and part != "ip=dhcp":
|
||
|
|
return part[3:].split("/")[0]
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
return None
|