Распаковал архив Proxmox-VPS-Panel.rar и добавил содержимое в репозиторий
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user