vm management

This commit is contained in:
Mateusz Gruszczyński
2025-10-17 16:20:07 +02:00
parent 2e988265b4
commit b9586a3517
3 changed files with 121 additions and 12 deletions

38
app.py
View File

@@ -22,13 +22,13 @@ def run(cmd: List[str], timeout: int = 25) -> subprocess.CompletedProcess:
return subprocess.run(cmd, check=False, text=True, capture_output=True, timeout=timeout)
def get_text(cmd: List[str]) -> str:
r = run(cmd)
r = run(cmd, timeout=timeout or 25)
return r.stdout if r.returncode == 0 else ""
def get_json(cmd: List[str]) -> Any:
if cmd and cmd[0] == "pvesh" and "--output-format" not in cmd:
cmd = cmd + ["--output-format", "json"]
r = run(cmd)
r = run(cmd, timeout=timeout or 25)
if r.returncode != 0 or not r.stdout.strip():
return None
try:
@@ -36,11 +36,11 @@ def get_json(cmd: List[str]) -> Any:
except Exception:
return None
def post_json(cmd: List[str]) -> Any:
def post_json(cmd: List[str], timeout: Optional[int] = None) -> Any:
# force "create" for POST-like agent calls
if cmd and cmd[0] == "pvesh" and len(cmd) > 2 and cmd[1] != "create":
cmd = ["pvesh", "create"] + cmd[1:]
r = run(cmd)
r = run(cmd, timeout=timeout or 25)
if r.returncode != 0 or not r.stdout.strip():
return None
try:
@@ -64,7 +64,7 @@ def stop_if_running(unit: str, out: List[str]) -> None:
def ha_node_maint(enable: bool, node: str, out: List[str]) -> None:
cmd = ["ha-manager", "crm-command", "node-maintenance", "enable" if enable else "disable", node]
out.append("$ " + " ".join(shlex.quote(x) for x in cmd))
r = run(cmd)
r = run(cmd, timeout=timeout or 25)
if r.returncode != 0:
out.append(f"ERR: {r.stderr.strip()}")
@@ -441,7 +441,7 @@ def vm_locked(typ: str, node: str, vmid: int) -> bool:
def run_qm_pct(typ: str, vmid: int, subcmd: str) -> subprocess.CompletedProcess:
tool = "qm" if typ == "qemu" else "pct"
return run([tool, subcmd, str(vmid)])
return run([tool, subcmd, str(vmid)], timeout=120)
@app.get("/api/list-all-vmct")
def api_list_all_vmct():
@@ -487,7 +487,7 @@ def api_vm_action():
return jsonify(ok=False, error="target required and must differ from source"), 400
base = f"/nodes/{node}/{typ}/{vmid}/migrate"
cmd = ["pvesh", "create", base, "-target", target, "-online", "0"]
res = post_json(cmd)
res = post_json(cmd, timeout=120)
upid = None
if isinstance(res, dict):
upid = res.get("data") or res.get("upid")
@@ -510,6 +510,30 @@ def api_task_status():
st = get_json(["pvesh", "get", f"/nodes/{node}/tasks/{upid}/status"]) or {}
return jsonify(ok=True, status=st)
@app.get("/api/task-log")
def api_task_log():
upid = request.args.get("upid", "").strip()
node = request.args.get("node", "").strip()
start = request.args.get("start", "0").strip()
try:
start_i = int(start)
except Exception:
start_i = 0
if not upid or not node:
return jsonify(ok=False, error="upid and node required"), 400
# Returns a list of {n: <line_no>, t: <text>}
lines = get_json(["pvesh", "get", f"/nodes/{node}/tasks/{upid}/log", "-start", str(start_i)]) or []
# Compute next start
next_start = start_i
if isinstance(lines, list) and lines:
# find max n
try:
next_start = max((int(x.get("n", start_i)) for x in lines if isinstance(x, dict)), default=start_i) + 1
except Exception:
next_start = start_i
return jsonify(ok=True, lines=lines or [], next_start=next_start)
if __name__ == "__main__":
import argparse
p = argparse.ArgumentParser()