Переписан для Open Web UI: плоские функции вместо класса, префикс pve1_
This commit is contained in:
+91
-341
@@ -5,11 +5,10 @@ Description: Full toolset for Proxmox VE management (second server):
|
|||||||
- LXC (create, start, stop, delete)
|
- LXC (create, start, stop, delete)
|
||||||
- VM (clone from template, create from ISO, manage)
|
- VM (clone from template, create from ISO, manage)
|
||||||
- Monitoring (nodes, storage, resources, tasks)
|
- Monitoring (nodes, storage, resources, tasks)
|
||||||
Usage:
|
|
||||||
from proxmox_tools_pve1 import ToolsPVE1
|
Usage: Import and call functions directly.
|
||||||
tools = ToolsPVE1()
|
import proxmox_tools_pve1 as pve1
|
||||||
print(tools.connection())
|
print(pve1.pve1_connection())
|
||||||
print(tools.vm_create(vmid=200, name="ubuntu", ciuser="ubuntu", cipassword="1234567"))
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -29,9 +28,6 @@ DEFAULT_NODE = "pve1"
|
|||||||
DEFAULT_STORAGE = "local"
|
DEFAULT_STORAGE = "local"
|
||||||
DEFAULT_BRIDGE = "vmbr0"
|
DEFAULT_BRIDGE = "vmbr0"
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# INTERNAL HELPERS
|
|
||||||
# ============================================================
|
|
||||||
_base_url = PROXMOX_HOST.rstrip("/") + "/api2/json"
|
_base_url = PROXMOX_HOST.rstrip("/") + "/api2/json"
|
||||||
_auth_header = {
|
_auth_header = {
|
||||||
"Authorization": f"PVEAPIToken={PROXMOX_USER}!{PROXMOX_TOKEN_NAME}={PROXMOX_TOKEN_VALUE}"
|
"Authorization": f"PVEAPIToken={PROXMOX_USER}!{PROXMOX_TOKEN_NAME}={PROXMOX_TOKEN_VALUE}"
|
||||||
@@ -92,10 +88,7 @@ def _uptime(s):
|
|||||||
|
|
||||||
|
|
||||||
def _wait_for_task(upid, timeout=60):
|
def _wait_for_task(upid, timeout=60):
|
||||||
"""
|
"""Wait for a Proxmox task to complete by UPID."""
|
||||||
Wait for a Proxmox task to complete by UPID.
|
|
||||||
Returns (ok: bool, message: str).
|
|
||||||
"""
|
|
||||||
if not upid or not isinstance(upid, str) or not upid.startswith("UPID:"):
|
if not upid or not isinstance(upid, str) or not upid.startswith("UPID:"):
|
||||||
return True, "no UPID to wait"
|
return True, "no UPID to wait"
|
||||||
node = DEFAULT_NODE
|
node = DEFAULT_NODE
|
||||||
@@ -107,10 +100,7 @@ def _wait_for_task(upid, timeout=60):
|
|||||||
if t.get("upid") == upid:
|
if t.get("upid") == upid:
|
||||||
status = t.get("status", "")
|
status = t.get("status", "")
|
||||||
if status == "stopped":
|
if status == "stopped":
|
||||||
if t.get("exitstatus") == "OK":
|
return (True, "OK") if t.get("exitstatus") == "OK" else (False, str(t))
|
||||||
return True, "OK"
|
|
||||||
else:
|
|
||||||
return False, str(t)
|
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -119,45 +109,33 @@ def _wait_for_task(upid, timeout=60):
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# MAIN TOOLS CLASS
|
# FUNCTIONS
|
||||||
# ============================================================
|
# ============================================================
|
||||||
class ToolsPVE1:
|
|
||||||
"""Proxmox VE management toolset for PVE1 (192.168.31.4)."""
|
|
||||||
|
|
||||||
# ---- CONNECTION & NODES --------------------------------------------
|
def pve1_connection():
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def connection():
|
|
||||||
"""Check Proxmox connection."""
|
"""Check Proxmox connection."""
|
||||||
try:
|
try:
|
||||||
v = _api("GET", "/version")
|
v = _api("GET", "/version")
|
||||||
return (
|
return "OK Proxmox VE " + v.get("version", "?") + " | node: pve1 | IP: 192.168.31.4"
|
||||||
"OK Proxmox VE "
|
|
||||||
+ v.get("version", "?")
|
|
||||||
+ " | node: pve1 | IP: 192.168.31.4"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def nodes():
|
def pve1_nodes():
|
||||||
"""Show all cluster nodes."""
|
"""Show all cluster nodes."""
|
||||||
try:
|
try:
|
||||||
items = _api("GET", "/nodes")
|
items = _api("GET", "/nodes")
|
||||||
out = ["Nodes:"]
|
out = ["Nodes:"]
|
||||||
for n in items:
|
for n in items:
|
||||||
out.append(f"- {n.get('node', '?')} ({n.get('status', '?')})")
|
out.append(f"- {n.get('node', '?')} ({n.get('status', '?')})")
|
||||||
out.append(
|
out.append(f" CPU: {n.get('cpu', 0)*100:.1f}% | RAM: {_pct(n.get('mem'), n.get('maxmem'))}")
|
||||||
f" CPU: {n.get('cpu', 0)*100:.1f}% | "
|
|
||||||
f"RAM: {_pct(n.get('mem'), n.get('maxmem'))}"
|
|
||||||
)
|
|
||||||
out.append(f" Uptime: {_uptime(n.get('uptime'))}")
|
out.append(f" Uptime: {_uptime(n.get('uptime'))}")
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def node_status(node=DEFAULT_NODE):
|
def pve1_node_status(node=DEFAULT_NODE):
|
||||||
"""Show detailed node status."""
|
"""Show detailed node status."""
|
||||||
try:
|
try:
|
||||||
s = _api("GET", f"/nodes/{node}/status")
|
s = _api("GET", f"/nodes/{node}/status")
|
||||||
@@ -184,8 +162,8 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def cluster():
|
def pve1_cluster():
|
||||||
"""Show cluster status (quorum, members)."""
|
"""Show cluster status (quorum, members)."""
|
||||||
try:
|
try:
|
||||||
items = _api("GET", "/cluster/status")
|
items = _api("GET", "/cluster/status")
|
||||||
@@ -195,15 +173,9 @@ class ToolsPVE1:
|
|||||||
t = item.get("type", "")
|
t = item.get("type", "")
|
||||||
if t == "cluster":
|
if t == "cluster":
|
||||||
has_cluster_info = True
|
has_cluster_info = True
|
||||||
out.append(
|
out.append(f"Cluster: {item.get('name', '?')} | Quorum: {'OK' if item.get('quorate') else 'FAIL'}")
|
||||||
f"Cluster: {item.get('name', '?')} | "
|
|
||||||
f"Quorum: {'OK' if item.get('quorate') else 'FAIL'}"
|
|
||||||
)
|
|
||||||
elif t == "node":
|
elif t == "node":
|
||||||
out.append(
|
out.append(f" {item.get('name', '?')} - {item.get('status', '?')} ({item.get('ip', '-')})")
|
||||||
f" {item.get('name', '?')} - {item.get('status', '?')} "
|
|
||||||
f"({item.get('ip', '-')})"
|
|
||||||
)
|
|
||||||
if not has_cluster_info:
|
if not has_cluster_info:
|
||||||
node_count = sum(1 for i in items if i.get("type") == "node")
|
node_count = sum(1 for i in items if i.get("type") == "node")
|
||||||
out.append("Cluster: single-node | Quorum: OK (N/A)")
|
out.append("Cluster: single-node | Quorum: OK (N/A)")
|
||||||
@@ -212,26 +184,17 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
# ---- RESOURCES & STORAGE ------------------------------------------
|
|
||||||
|
|
||||||
@staticmethod
|
def pve1_resources(ftype=""):
|
||||||
def resources(ftype=""):
|
"""Show all cluster resources."""
|
||||||
"""Show all cluster resources (VMs, LXC, nodes, storage)."""
|
|
||||||
try:
|
try:
|
||||||
p = {}
|
p = {"type": ftype} if ftype else {}
|
||||||
if ftype:
|
|
||||||
p["type"] = ftype
|
|
||||||
items = _api("GET", "/cluster/resources", params=p)
|
items = _api("GET", "/cluster/resources", params=p)
|
||||||
out = ["Resources:"]
|
out = ["Resources:"]
|
||||||
g = {}
|
g = {}
|
||||||
for r in items:
|
for r in items:
|
||||||
g.setdefault(r.get("type", "?"), []).append(r)
|
g.setdefault(r.get("type", "?"), []).append(r)
|
||||||
labels = {
|
labels = {"node": "Nodes", "qemu": "QEMU", "lxc": "LXC", "storage": "Storage"}
|
||||||
"node": "Nodes",
|
|
||||||
"qemu": "QEMU",
|
|
||||||
"lxc": "LXC",
|
|
||||||
"storage": "Storage",
|
|
||||||
}
|
|
||||||
for t, lst in g.items():
|
for t, lst in g.items():
|
||||||
out.append(f"{labels.get(t, t)}: {len(lst)}")
|
out.append(f"{labels.get(t, t)}: {len(lst)}")
|
||||||
for r in lst[:25]:
|
for r in lst[:25]:
|
||||||
@@ -250,8 +213,8 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def storage():
|
def pve1_storage():
|
||||||
"""Show all storages."""
|
"""Show all storages."""
|
||||||
try:
|
try:
|
||||||
items = _api("GET", "/storage")
|
items = _api("GET", "/storage")
|
||||||
@@ -270,8 +233,8 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def tasks(limit=10, node=DEFAULT_NODE):
|
def pve1_tasks(limit=10, node=DEFAULT_NODE):
|
||||||
"""Show recent tasks on node."""
|
"""Show recent tasks on node."""
|
||||||
try:
|
try:
|
||||||
items = _api("GET", f"/nodes/{node}/tasks", params={"limit": limit})
|
items = _api("GET", f"/nodes/{node}/tasks", params={"limit": limit})
|
||||||
@@ -280,18 +243,13 @@ class ToolsPVE1:
|
|||||||
out = [f"Tasks on {node}:"]
|
out = [f"Tasks on {node}:"]
|
||||||
for t in items:
|
for t in items:
|
||||||
start = time.ctime(t.get("starttime", 0)) if t.get("starttime") else "?"
|
start = time.ctime(t.get("starttime", 0)) if t.get("starttime") else "?"
|
||||||
out.append(
|
out.append(f"- {t.get('type', '?')} | {t.get('status', '?')} | {t.get('user', '?')} | {start}")
|
||||||
f"- {t.get('type', '?')} | {t.get('status', '?')} | "
|
|
||||||
f"{t.get('user', '?')} | {start}"
|
|
||||||
)
|
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
# ---- LXC CONTAINERS -----------------------------------------------
|
|
||||||
|
|
||||||
@staticmethod
|
def pve1_lxc_list(node=DEFAULT_NODE):
|
||||||
def lxc_list(node=DEFAULT_NODE):
|
|
||||||
"""Show LXC container list."""
|
"""Show LXC container list."""
|
||||||
try:
|
try:
|
||||||
items = _api("GET", f"/nodes/{node}/lxc")
|
items = _api("GET", f"/nodes/{node}/lxc")
|
||||||
@@ -299,10 +257,7 @@ class ToolsPVE1:
|
|||||||
return f"No LXC on {node}"
|
return f"No LXC on {node}"
|
||||||
out = [f"LXC on {node}:"]
|
out = [f"LXC on {node}:"]
|
||||||
for c in items:
|
for c in items:
|
||||||
out.append(
|
out.append(f"- {c.get('name', '?')} (ID:{c.get('vmid', '?')}) - {c.get('status', '?')}")
|
||||||
f"- {c.get('name', '?')} (ID:{c.get('vmid', '?')}) - "
|
|
||||||
f"{c.get('status', '?')}"
|
|
||||||
)
|
|
||||||
if c.get("cpu"):
|
if c.get("cpu"):
|
||||||
out.append(f" CPU:{c['cpu']*100:.1f}%")
|
out.append(f" CPU:{c['cpu']*100:.1f}%")
|
||||||
if c.get("maxmem"):
|
if c.get("maxmem"):
|
||||||
@@ -316,18 +271,15 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def lxc_detail(vmid, node=DEFAULT_NODE):
|
def pve1_lxc_detail(vmid, node=DEFAULT_NODE):
|
||||||
"""Show detailed LXC info."""
|
"""Show detailed LXC info."""
|
||||||
try:
|
try:
|
||||||
s = _api("GET", f"/nodes/{node}/lxc/{vmid}/status/current")
|
s = _api("GET", f"/nodes/{node}/lxc/{vmid}/status/current")
|
||||||
c = _api("GET", f"/nodes/{node}/lxc/{vmid}/config")
|
c = _api("GET", f"/nodes/{node}/lxc/{vmid}/config")
|
||||||
name = s.get("name", c.get("hostname", "?"))
|
name = s.get("name", c.get("hostname", "?"))
|
||||||
out = [f"LXC #{vmid}: {name} - {s.get('status', '?')}"]
|
out = [f"LXC #{vmid}: {name} - {s.get('status', '?')}"]
|
||||||
out.append(
|
out.append(f"OS: {c.get('ostype', '?')} | CPU: {c.get('cores', '?')} | RAM: {c.get('memory', '?')}MB")
|
||||||
f"OS: {c.get('ostype', '?')} | CPU: {c.get('cores', '?')} | "
|
|
||||||
f"RAM: {c.get('memory', '?')}MB"
|
|
||||||
)
|
|
||||||
out.append(f"Disk: {c.get('rootfs', '?')} | SWAP: {c.get('swap', '?')}MB")
|
out.append(f"Disk: {c.get('rootfs', '?')} | SWAP: {c.get('swap', '?')}MB")
|
||||||
out.append(f"Hostname: {c.get('hostname', '?')}")
|
out.append(f"Hostname: {c.get('hostname', '?')}")
|
||||||
nets = [f" {k}: {v}" for k, v in c.items() if k.startswith("net")]
|
nets = [f" {k}: {v}" for k, v in c.items() if k.startswith("net")]
|
||||||
@@ -343,21 +295,12 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def lxc_action(vmid, action, node=DEFAULT_NODE):
|
def pve1_lxc_action(vmid, action, node=DEFAULT_NODE):
|
||||||
"""
|
"""Manage LXC: start, stop, shutdown, restart, suspend, resume, delete."""
|
||||||
Manage LXC: start, stop, shutdown, restart, suspend, resume, delete.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
acts = {
|
acts = {"start": "start", "stop": "stop", "shutdown": "shutdown", "restart": "reboot",
|
||||||
"start": "start",
|
"suspend": "suspend", "resume": "resume", "delete": "del"}
|
||||||
"stop": "stop",
|
|
||||||
"shutdown": "shutdown",
|
|
||||||
"restart": "reboot",
|
|
||||||
"suspend": "suspend",
|
|
||||||
"resume": "resume",
|
|
||||||
"delete": "del",
|
|
||||||
}
|
|
||||||
if action not in acts:
|
if action not in acts:
|
||||||
return f"Unknown action: {action}. Allowed: {', '.join(acts.keys())}"
|
return f"Unknown action: {action}. Allowed: {', '.join(acts.keys())}"
|
||||||
if action == "delete":
|
if action == "delete":
|
||||||
@@ -368,8 +311,8 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def templates(storage="local", node=DEFAULT_NODE):
|
def pve1_templates(storage="local", node=DEFAULT_NODE):
|
||||||
"""Show available LXC templates in storage."""
|
"""Show available LXC templates in storage."""
|
||||||
try:
|
try:
|
||||||
content = _api("GET", f"/nodes/{node}/storage/{storage}/content")
|
content = _api("GET", f"/nodes/{node}/storage/{storage}/content")
|
||||||
@@ -383,50 +326,24 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def lxc_create(
|
def pve1_lxc_create(vmid, hostname, ostemplate, password="", sshkey="", storage=DEFAULT_STORAGE,
|
||||||
vmid,
|
cores=1, memory=512, swap=0, disk="8G", bridge=DEFAULT_BRIDGE,
|
||||||
hostname,
|
ip="dhcp", netmask=24, gw="", dns="", domain="", node=DEFAULT_NODE,
|
||||||
ostemplate,
|
unpriv=True, start_now=True):
|
||||||
password="",
|
|
||||||
sshkey="",
|
|
||||||
storage=DEFAULT_STORAGE,
|
|
||||||
cores=1,
|
|
||||||
memory=512,
|
|
||||||
swap=0,
|
|
||||||
disk="8G",
|
|
||||||
bridge=DEFAULT_BRIDGE,
|
|
||||||
ip="dhcp",
|
|
||||||
netmask=24,
|
|
||||||
gw="",
|
|
||||||
dns="",
|
|
||||||
domain="",
|
|
||||||
node=DEFAULT_NODE,
|
|
||||||
unpriv=True,
|
|
||||||
start_now=True,
|
|
||||||
):
|
|
||||||
"""Create a new LXC container."""
|
"""Create a new LXC container."""
|
||||||
try:
|
try:
|
||||||
if not hostname:
|
if not hostname:
|
||||||
return "Error: hostname required"
|
return "Error: hostname required"
|
||||||
if not ostemplate:
|
|
||||||
return "Error: ostemplate required"
|
|
||||||
if not password and not sshkey:
|
if not password and not sshkey:
|
||||||
return "Error: password or sshkey required"
|
return "Error: password or sshkey required"
|
||||||
exist = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/lxc")]
|
exist = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/lxc")]
|
||||||
exist += [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/qemu")]
|
exist += [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/qemu")]
|
||||||
if str(vmid) in exist:
|
if str(vmid) in exist:
|
||||||
return f"Error: VMID {vmid} already exists"
|
return f"Error: VMID {vmid} already exists"
|
||||||
params = {
|
params = {"vmid": vmid, "hostname": hostname, "ostemplate": ostemplate,
|
||||||
"vmid": vmid,
|
"storage": storage, "cores": cores, "memory": memory, "swap": swap,
|
||||||
"hostname": hostname,
|
"unprivileged": 1 if unpriv else 0}
|
||||||
"ostemplate": ostemplate,
|
|
||||||
"storage": storage,
|
|
||||||
"cores": cores,
|
|
||||||
"memory": memory,
|
|
||||||
"swap": swap,
|
|
||||||
"unprivileged": 1 if unpriv else 0,
|
|
||||||
}
|
|
||||||
params["rootfs"] = disk if ":" in disk else f"{storage}:{disk}"
|
params["rootfs"] = disk if ":" in disk else f"{storage}:{disk}"
|
||||||
net = f"name=eth0,bridge={bridge}"
|
net = f"name=eth0,bridge={bridge}"
|
||||||
if ip and ip.lower() != "dhcp":
|
if ip and ip.lower() != "dhcp":
|
||||||
@@ -447,24 +364,17 @@ class ToolsPVE1:
|
|||||||
if start_now:
|
if start_now:
|
||||||
params["start"] = 1
|
params["start"] = 1
|
||||||
result = _api("POST", f"/nodes/{node}/lxc", data=params)
|
result = _api("POST", f"/nodes/{node}/lxc", data=params)
|
||||||
out = [f"OK LXC #{vmid} creating!"]
|
out = [f"OK LXC #{vmid} creating!", f" Name: {hostname}", f" Template: {ostemplate}",
|
||||||
out.append(f" Name: {hostname}")
|
f" CPU: {cores} cores, RAM: {memory}MB, Disk: {disk}"]
|
||||||
out.append(f" Template: {ostemplate}")
|
out.append(f" Net: {bridge}, IP: {'DHCP' if ip == 'dhcp' else f'{ip}/{netmask}'}")
|
||||||
out.append(f" CPU: {cores} cores, RAM: {memory}MB, Disk: {disk}")
|
|
||||||
if ip and ip.lower() != "dhcp":
|
|
||||||
out.append(f" Net: {bridge}, IP: {ip}/{netmask}")
|
|
||||||
else:
|
|
||||||
out.append(f" Net: {bridge}, IP: DHCP")
|
|
||||||
if isinstance(result, str) and result:
|
if isinstance(result, str) and result:
|
||||||
out.append(f"UPID: {result}")
|
out.append(f"UPID: {result}")
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
# ---- VIRTUAL MACHINES (QEMU) -------------------------------------
|
|
||||||
|
|
||||||
@staticmethod
|
def pve1_vm_list(node=DEFAULT_NODE):
|
||||||
def vm_list(node=DEFAULT_NODE):
|
|
||||||
"""Show all VM (QEMU) list."""
|
"""Show all VM (QEMU) list."""
|
||||||
try:
|
try:
|
||||||
items = _api("GET", f"/nodes/{node}/qemu")
|
items = _api("GET", f"/nodes/{node}/qemu")
|
||||||
@@ -472,10 +382,7 @@ class ToolsPVE1:
|
|||||||
return f"No VMs on {node}"
|
return f"No VMs on {node}"
|
||||||
out = [f"VMs on {node}:"]
|
out = [f"VMs on {node}:"]
|
||||||
for v in items:
|
for v in items:
|
||||||
out.append(
|
out.append(f"- {v.get('name', '?')} (ID:{v.get('vmid', '?')}) - {v.get('status', '?')}")
|
||||||
f"- {v.get('name', '?')} (ID:{v.get('vmid', '?')}) - "
|
|
||||||
f"{v.get('status', '?')}"
|
|
||||||
)
|
|
||||||
if v.get("cpu"):
|
if v.get("cpu"):
|
||||||
out.append(f" CPU:{v['cpu']*100:.1f}%")
|
out.append(f" CPU:{v['cpu']*100:.1f}%")
|
||||||
if v.get("maxmem"):
|
if v.get("maxmem"):
|
||||||
@@ -487,13 +394,12 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def vm_detail(vmid, node=DEFAULT_NODE):
|
def pve1_vm_detail(vmid, node=DEFAULT_NODE):
|
||||||
"""Show detailed VM info."""
|
"""Show detailed VM info."""
|
||||||
try:
|
try:
|
||||||
s = _api("GET", f"/nodes/{node}/qemu/{vmid}/status/current")
|
s = _api("GET", f"/nodes/{node}/qemu/{vmid}/status/current")
|
||||||
out = [f"VM #{vmid}: {s.get('name', '?')}"]
|
out = [f"VM #{vmid}: {s.get('name', '?')}", f"Status: {s.get('status', '?')}"]
|
||||||
out.append(f"Status: {s.get('status', '?')}")
|
|
||||||
if s.get("maxmem"):
|
if s.get("maxmem"):
|
||||||
out.append(f"RAM: {_pct(s.get('mem'), s.get('maxmem'))}")
|
out.append(f"RAM: {_pct(s.get('mem'), s.get('maxmem'))}")
|
||||||
if s.get("cpu"):
|
if s.get("cpu"):
|
||||||
@@ -506,109 +412,54 @@ class ToolsPVE1:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def vm_action(vmid, action, node=DEFAULT_NODE):
|
def pve1_vm_action(vmid, action, node=DEFAULT_NODE):
|
||||||
"""
|
"""Manage VM: start, stop, shutdown, restart, suspend, resume, delete."""
|
||||||
Manage VM: start, stop, shutdown, restart, suspend, resume, delete.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
acts = {
|
acts = {"start": "start", "stop": "stop", "shutdown": "shutdown", "restart": "reboot",
|
||||||
"start": "start",
|
"suspend": "suspend", "resume": "resume", "delete": "del"}
|
||||||
"stop": "stop",
|
|
||||||
"shutdown": "shutdown",
|
|
||||||
"restart": "reboot",
|
|
||||||
"suspend": "suspend",
|
|
||||||
"resume": "resume",
|
|
||||||
"delete": "del",
|
|
||||||
}
|
|
||||||
if action not in acts:
|
if action not in acts:
|
||||||
return f"Unknown action: {action}. Allowed: {', '.join(acts.keys())}"
|
return f"Unknown action: {action}. Allowed: {', '.join(acts.keys())}"
|
||||||
if action == "delete":
|
if action == "delete":
|
||||||
result = _api("DELETE", f"/nodes/{node}/qemu/{vmid}")
|
result = _api("DELETE", f"/nodes/{node}/qemu/{vmid}")
|
||||||
else:
|
else:
|
||||||
result = _api(
|
result = _api("POST", f"/nodes/{node}/qemu/{vmid}/status/{acts[action]}")
|
||||||
"POST", f"/nodes/{node}/qemu/{vmid}/status/{acts[action]}"
|
|
||||||
)
|
|
||||||
return f"OK VM #{vmid} {action}\n{result}"
|
return f"OK VM #{vmid} {action}\n{result}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def vm_create_from_template(
|
|
||||||
vmid,
|
|
||||||
name,
|
|
||||||
template_vmid=1000,
|
|
||||||
full=True,
|
|
||||||
memory=None,
|
|
||||||
cores=None,
|
|
||||||
storage=None,
|
|
||||||
target_node=None,
|
|
||||||
pool=None,
|
|
||||||
start_now=True,
|
|
||||||
ciuser="",
|
|
||||||
cipassword="",
|
|
||||||
sshkeys="",
|
|
||||||
node=DEFAULT_NODE,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Create a new VM by cloning from a template.
|
|
||||||
Default template: VM #1000 (cloud-init).
|
|
||||||
|
|
||||||
Cloud-init params (for guest OS user setup):
|
def pve1_vm_create_from_template(vmid, name, template_vmid=1000, full=True, memory=None,
|
||||||
ciuser: username in the guest (e.g. "ubuntu")
|
cores=None, storage=None, target_node=None, pool=None,
|
||||||
cipassword: password for the user
|
start_now=True, ciuser="", cipassword="", sshkeys="", node=DEFAULT_NODE):
|
||||||
sshkeys: SSH public keys (newline-separated string)
|
"""Create a new VM by cloning from template. Default: VM #1000 (cloud-init)."""
|
||||||
|
|
||||||
FIXED: "start" parameter is NOT sent to /clone -
|
|
||||||
startup is done via separate API call after clone completes.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
if not vmid:
|
if not vmid:
|
||||||
return "Error: vmid required"
|
return "Error: vmid required"
|
||||||
if not name:
|
if not name:
|
||||||
return "Error: name required"
|
return "Error: name required"
|
||||||
|
|
||||||
# Check VMID availability
|
|
||||||
exist_vm = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/qemu")]
|
exist_vm = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/qemu")]
|
||||||
exist_lxc = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/lxc")]
|
exist_lxc = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/lxc")]
|
||||||
if str(vmid) in exist_vm + exist_lxc:
|
if str(vmid) in exist_vm + exist_lxc:
|
||||||
return f"Error: VMID {vmid} already exists"
|
return f"Error: VMID {vmid} already exists"
|
||||||
|
params = {"newid": vmid, "name": name, "full": 1 if full else 0}
|
||||||
# Step 1: Clone (WITHOUT "start" parameter!)
|
|
||||||
params = {
|
|
||||||
"newid": vmid,
|
|
||||||
"name": name,
|
|
||||||
"full": 1 if full else 0,
|
|
||||||
}
|
|
||||||
if storage:
|
if storage:
|
||||||
params["storage"] = storage
|
params["storage"] = storage
|
||||||
if target_node:
|
if target_node:
|
||||||
params["target"] = target_node
|
params["target"] = target_node
|
||||||
if pool:
|
if pool:
|
||||||
params["pool"] = pool
|
params["pool"] = pool
|
||||||
|
result = _api("POST", f"/nodes/{node}/qemu/{template_vmid}/clone", data=params)
|
||||||
result = _api(
|
out = [f"OK VM #{vmid}: {name} cloning from template #{template_vmid}!",
|
||||||
"POST", f"/nodes/{node}/qemu/{template_vmid}/clone", data=params
|
f" Type: {'full' if full else 'linked'} clone"]
|
||||||
)
|
|
||||||
|
|
||||||
out = [f"OK VM #{vmid}: {name} cloning from template #{template_vmid}!"]
|
|
||||||
out.append(f" Type: {'full' if full else 'linked'} clone")
|
|
||||||
if storage:
|
if storage:
|
||||||
out.append(f" Storage: {storage}")
|
out.append(f" Storage: {storage}")
|
||||||
|
|
||||||
upid = result if isinstance(result, str) else ""
|
upid = result if isinstance(result, str) else ""
|
||||||
if upid:
|
if upid:
|
||||||
out.append(f"UPID: {upid}")
|
out.append(f"UPID: {upid}")
|
||||||
|
|
||||||
# Step 2: Wait for clone to finish
|
|
||||||
out.append("Waiting for clone to finish...")
|
out.append("Waiting for clone to finish...")
|
||||||
ok, msg = _wait_for_task(upid, timeout=120)
|
ok, msg = _wait_for_task(upid, timeout=120)
|
||||||
if not ok:
|
out.append(f" {'Clone done' if ok else f'WARN Clone may not have finished: {msg}'}")
|
||||||
out.append(f" WARN Clone may not have finished: {msg}")
|
|
||||||
else:
|
|
||||||
out.append(" Clone done")
|
|
||||||
|
|
||||||
# Step 3: Apply settings (resources + cloud-init)
|
|
||||||
config_params = {}
|
config_params = {}
|
||||||
if memory is not None:
|
if memory is not None:
|
||||||
config_params["memory"] = memory
|
config_params["memory"] = memory
|
||||||
@@ -620,15 +471,10 @@ class ToolsPVE1:
|
|||||||
config_params["cipassword"] = cipassword
|
config_params["cipassword"] = cipassword
|
||||||
if sshkeys:
|
if sshkeys:
|
||||||
config_params["sshkeys"] = sshkeys
|
config_params["sshkeys"] = sshkeys
|
||||||
|
|
||||||
if config_params:
|
if config_params:
|
||||||
out.append("Applying settings...")
|
out.append("Applying settings...")
|
||||||
try:
|
try:
|
||||||
_api(
|
_api("PUT", f"/nodes/{node}/qemu/{vmid}/config", data=config_params)
|
||||||
"PUT",
|
|
||||||
f"/nodes/{node}/qemu/{vmid}/config",
|
|
||||||
data=config_params,
|
|
||||||
)
|
|
||||||
if memory is not None:
|
if memory is not None:
|
||||||
out.append(f" RAM -> {memory} MB")
|
out.append(f" RAM -> {memory} MB")
|
||||||
if cores is not None:
|
if cores is not None:
|
||||||
@@ -642,8 +488,6 @@ class ToolsPVE1:
|
|||||||
out.append(" Settings applied")
|
out.append(" Settings applied")
|
||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
out.append(f" WARN Could not apply settings: {e2}")
|
out.append(f" WARN Could not apply settings: {e2}")
|
||||||
|
|
||||||
# Step 4: Start VM (separate call!)
|
|
||||||
if start_now:
|
if start_now:
|
||||||
out.append("Starting VM...")
|
out.append("Starting VM...")
|
||||||
try:
|
try:
|
||||||
@@ -653,119 +497,34 @@ class ToolsPVE1:
|
|||||||
out.append(f" WARN Could not start: {e3}")
|
out.append(f" WARN Could not start: {e3}")
|
||||||
else:
|
else:
|
||||||
out.append(f" VM #{vmid} created (not started)")
|
out.append(f" VM #{vmid} created (not started)")
|
||||||
|
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def vm_create(
|
|
||||||
vmid,
|
|
||||||
name,
|
|
||||||
template_vmid=1000,
|
|
||||||
memory=4096,
|
|
||||||
cores=2,
|
|
||||||
disk_size="32G",
|
|
||||||
disk_storage="local-lvm",
|
|
||||||
iso="",
|
|
||||||
bridge=DEFAULT_BRIDGE,
|
|
||||||
ip="dhcp",
|
|
||||||
netmask=24,
|
|
||||||
gw="",
|
|
||||||
ostype="l26",
|
|
||||||
agent=1,
|
|
||||||
ciuser="",
|
|
||||||
cipassword="",
|
|
||||||
sshkeys="",
|
|
||||||
node=DEFAULT_NODE,
|
|
||||||
start_now=True,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Universal VM creation.
|
|
||||||
|
|
||||||
Modes:
|
def pve1_vm_create(vmid, name, template_vmid=1000, memory=4096, cores=2,
|
||||||
1. From template (default): clones VM #1000 (cloud-init)
|
disk_size="32G", disk_storage="local-lvm", iso="",
|
||||||
2. From ISO: template_vmid=None, iso="local:iso/image.iso"
|
bridge=DEFAULT_BRIDGE, ip="dhcp", netmask=24, gw="",
|
||||||
|
ostype="l26", agent=1, ciuser="", cipassword="", sshkeys="",
|
||||||
Cloud-init params (for mode 1):
|
node=DEFAULT_NODE, start_now=True):
|
||||||
ciuser: guest OS username
|
"""Universal VM creation. Mode 1: clone from template. Mode 2: from ISO (template_vmid=None)."""
|
||||||
cipassword: guest OS password
|
|
||||||
sshkeys: SSH public keys
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Clone from template with cloud-init user:
|
|
||||||
tools.vm_create(
|
|
||||||
vmid=200,
|
|
||||||
name="web-server",
|
|
||||||
memory=8192,
|
|
||||||
cores=4,
|
|
||||||
ciuser="ubuntu",
|
|
||||||
cipassword="mypassword",
|
|
||||||
start_now=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create from ISO:
|
|
||||||
tools.vm_create(
|
|
||||||
vmid=202,
|
|
||||||
name="debian-server",
|
|
||||||
template_vmid=None,
|
|
||||||
iso="local:iso/debian-12.iso",
|
|
||||||
ip="192.168.31.60",
|
|
||||||
gw="192.168.31.1",
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Mode 1: clone from template
|
|
||||||
if template_vmid:
|
if template_vmid:
|
||||||
return ToolsPVE1.vm_create_from_template(
|
return pve1_vm_create_from_template(vmid=vmid, name=name, template_vmid=template_vmid,
|
||||||
vmid=vmid,
|
full=True, memory=memory, cores=cores, storage=disk_storage,
|
||||||
name=name,
|
start_now=start_now, ciuser=ciuser, cipassword=cipassword, sshkeys=sshkeys, node=node)
|
||||||
template_vmid=template_vmid,
|
if not name or not vmid:
|
||||||
full=True,
|
return "Error: name and vmid required"
|
||||||
memory=memory,
|
|
||||||
cores=cores,
|
|
||||||
storage=disk_storage,
|
|
||||||
start_now=start_now,
|
|
||||||
ciuser=ciuser,
|
|
||||||
cipassword=cipassword,
|
|
||||||
sshkeys=sshkeys,
|
|
||||||
node=node,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mode 2: create from scratch (ISO)
|
|
||||||
if not name:
|
|
||||||
return "Error: name required"
|
|
||||||
if not vmid:
|
|
||||||
return "Error: vmid required"
|
|
||||||
|
|
||||||
exist_vm = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/qemu")]
|
exist_vm = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/qemu")]
|
||||||
exist_lxc = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/lxc")]
|
exist_lxc = [str(x.get("vmid")) for x in _api("GET", f"/nodes/{node}/lxc")]
|
||||||
if str(vmid) in exist_vm + exist_lxc:
|
if str(vmid) in exist_vm + exist_lxc:
|
||||||
return f"Error: VMID {vmid} already exists"
|
return f"Error: VMID {vmid} already exists"
|
||||||
|
params = {"vmid": vmid, "name": name, "memory": memory, "cores": cores,
|
||||||
params = {
|
"sockets": 1, "ostype": ostype, "agent": str(agent)}
|
||||||
"vmid": vmid,
|
params["virtio0"] = disk_size if ":" in disk_size else f"{disk_storage}:{disk_size}"
|
||||||
"name": name,
|
|
||||||
"memory": memory,
|
|
||||||
"cores": cores,
|
|
||||||
"sockets": 1,
|
|
||||||
"ostype": ostype,
|
|
||||||
"agent": str(agent),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Disk
|
|
||||||
if ":" in disk_size:
|
|
||||||
params["virtio0"] = disk_size
|
|
||||||
else:
|
|
||||||
params["virtio0"] = f"{disk_storage}:{disk_size}"
|
|
||||||
|
|
||||||
# ISO
|
|
||||||
if iso:
|
if iso:
|
||||||
params["ide2"] = f"{iso},media=cdrom"
|
params["ide2"] = f"{iso},media=cdrom"
|
||||||
params["boot"] = "order=ide2;virtio0"
|
params["boot"] = "order=ide2;virtio0"
|
||||||
|
|
||||||
# Network
|
|
||||||
net = f"name=eth0,bridge={bridge}"
|
net = f"name=eth0,bridge={bridge}"
|
||||||
if ip and ip.lower() != "dhcp":
|
if ip and ip.lower() != "dhcp":
|
||||||
net += f",ip={ip}/{netmask}"
|
net += f",ip={ip}/{netmask}"
|
||||||
@@ -774,26 +533,18 @@ class ToolsPVE1:
|
|||||||
else:
|
else:
|
||||||
net += ",ip=dhcp"
|
net += ",ip=dhcp"
|
||||||
params["net0"] = net
|
params["net0"] = net
|
||||||
|
|
||||||
if start_now:
|
if start_now:
|
||||||
params["start"] = 1
|
params["start"] = 1
|
||||||
|
|
||||||
result = _api("POST", f"/nodes/{node}/qemu", data=params)
|
result = _api("POST", f"/nodes/{node}/qemu", data=params)
|
||||||
|
out = [f"OK VM #{vmid}: {name} creating!", f" CPU: {cores} cores, RAM: {memory} MB",
|
||||||
out = [f"OK VM #{vmid}: {name} creating!"]
|
f" Disk: {params['virtio0']}"]
|
||||||
out.append(f" CPU: {cores} cores, RAM: {memory} MB")
|
|
||||||
out.append(f" Disk: {params['virtio0']}")
|
|
||||||
if iso:
|
if iso:
|
||||||
out.append(f" ISO: {iso}")
|
out.append(f" ISO: {iso}")
|
||||||
out.append(
|
out.append(f" Net: {bridge}, IP: {'DHCP' if ip == 'dhcp' else f'{ip}/{netmask}'}")
|
||||||
f" Net: {bridge}, "
|
|
||||||
f"IP: {'DHCP' if ip == 'dhcp' else f'{ip}/{netmask}'}"
|
|
||||||
)
|
|
||||||
out.append(f" QEMU Agent: {'on' if agent else 'off'}")
|
out.append(f" QEMU Agent: {'on' if agent else 'off'}")
|
||||||
if isinstance(result, str) and result:
|
if isinstance(result, str) and result:
|
||||||
out.append(f"UPID: {result}")
|
out.append(f"UPID: {result}")
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "ERR " + str(e)
|
return "ERR " + str(e)
|
||||||
|
|
||||||
@@ -802,9 +553,8 @@ class ToolsPVE1:
|
|||||||
# TEST
|
# TEST
|
||||||
# ============================================================
|
# ============================================================
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
tools = ToolsPVE1()
|
print(pve1_connection())
|
||||||
print(tools.connection())
|
|
||||||
print()
|
print()
|
||||||
print(tools.vm_list())
|
print(pve1_vm_list())
|
||||||
print()
|
print()
|
||||||
print(tools.storage())
|
print(pve1_storage())
|
||||||
|
|||||||
Reference in New Issue
Block a user