From 600467061c361a4f45f4ea1987b47ecff641dce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 17 Oct 2025 16:06:34 +0200 Subject: [PATCH] vm management --- app.py | 15 +++++++- static/main.js | 89 +++++++++++++++++++++++++++++++++++++++++--- templates/index.html | 7 ++-- 3 files changed, 102 insertions(+), 9 deletions(-) diff --git a/app.py b/app.py index 29b6fff..bdbcf53 100644 --- a/app.py +++ b/app.py @@ -488,7 +488,10 @@ def api_vm_action(): base = f"/nodes/{node}/{typ}/{vmid}/migrate" cmd = ["pvesh", "create", base, "-target", target, "-online", "0"] res = post_json(cmd) - return jsonify(ok=True, result=res or {}) + upid = None + if isinstance(res, dict): + upid = res.get("data") or res.get("upid") + return jsonify(ok=True, result=res or {}, upid=upid, source_node=node) else: return jsonify(ok=False, error="unknown action"), 400 @@ -497,6 +500,16 @@ def api_vm_action(): return jsonify(ok=False, error=str(e)), 500 + +@app.get("/api/task-status") +def api_task_status(): + upid = request.args.get("upid", "").strip() + node = request.args.get("node", "").strip() + if not upid or not node: + return jsonify(ok=False, error="upid and node required"), 400 + st = get_json(["pvesh", "get", f"/nodes/{node}/tasks/{upid}/status"]) or {} + return jsonify(ok=True, status=st) + if __name__ == "__main__": import argparse p = argparse.ArgumentParser() diff --git a/static/main.js b/static/main.js index 4939ac9..ace0ca5 100644 --- a/static/main.js +++ b/static/main.js @@ -673,12 +673,13 @@ async function vmAction(sid, action, target) { if (!d.ok) throw new Error(d.error || 'action failed'); return d; } + async function renderVMAdmin() { const data = await fetchAllVmct(); const arr = ensureArr(data.all); const nodes = ensureArr(data.nodes); const tbody = document.querySelector('#vm-admin tbody'); - if (!arr.length) { setRows(tbody, [rowHTML(['No VMs/CTs'])]); return; } + if (!arr.length) { setRows(tbody, [rowHTML(['Brak VM/CT'])]); return; } const rows = arr.map(x => { const sid = safe(x.sid), type = safe(x.type), name = safe(x.name), node = safe(x.node); @@ -689,12 +690,12 @@ async function renderVMAdmin() { - `; const sel = ``; - return rowHTML([sid, type.toUpperCase(), name, node, st, actions, sel], `data-sid="${sid}"`); + const migrateBtn = ``; + return rowHTML([sid, type.toUpperCase(), name, node, st, actions, sel, migrateBtn], `data-sid="${sid}"`); }); setRows(tbody, rows); @@ -702,14 +703,51 @@ async function renderVMAdmin() { Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => { const sid = tr.getAttribute('data-sid'); const getTarget = () => tr.querySelector('.target-node')?.value || ''; + const colSpan = tr.children.length; // for status row width const bind = (sel, action, needsTarget = false) => { const btn = tr.querySelector(sel); if (!btn) return; btn.onclick = async () => { btn.disabled = true; try { - await vmAction(sid, action, needsTarget ? getTarget() : undefined); - await doRefresh(); + if (action === 'migrate') { + const target = getTarget(); + const resp = await vmAction(sid, action, target); + // show expandable status row + const row = ensureMigRow(tr, colSpan); + setMigRowVisible(row, true); + const log = row.querySelector('.mig-log'); + const srcNode = resp.source_node; + const upid = resp.upid; + log.textContent = `Starting offline migrate to ${target} (UPID: ${upid || '—'})...`; + // update badge immediately + const badgeCell = tr.children[4]; + if (badgeCell) badgeCell.innerHTML = badge('migrating', 'info'); + // start background polling; update log on each tick + await pollTask(upid, srcNode, (st) => { + const lines = []; + for (const k of ['type', 'status', 'pid', 'starttime', 'user', 'node', 'endtime', 'exitstatus']) { + if (st[k] !== undefined) lines.push(`${k}: ${st[k]}`); + } + log.textContent = lines.join(' +') || '—'; + }, async (finalSt) => { + const exit = (finalSt && finalSt.exitstatus) ? String(finalSt.exitstatus) : ''; + const ok = exit.toUpperCase() === 'OK'; + const badgeCell2 = tr.children[4]; + if (badgeCell2) badgeCell2.innerHTML = ok ? badge('running', 'ok') : badge('migrate error', 'err'); + log.textContent += (log.textContent ? '\n' : '') + (ok ? 'Migration finished successfully.' : ('Migration failed: ' + (exit || 'unknown error'))); + await doRefresh(); + }); + } else { + await vmAction(sid, action, needsTarget ? getTarget() : undefined); + const exit = (finalSt && finalSt.exitstatus) ? String(finalSt.exitstatus) : ''; + const ok = exit.toUpperCase() === 'OK'; + const badgeCell2 = tr.children[4]; + if (badgeCell2) badgeCell2.innerHTML = ok ? badge('running', 'ok') : badge('migrate error', 'err'); + log.textContent += (log.textContent ? '\n' : '') + (ok ? 'Migration finished successfully.' : ('Migration failed: ' + (exit || 'unknown error'))); + await doRefresh(); + } } catch (e) { alert('ERROR: ' + e.message); } btn.disabled = false; }; @@ -720,4 +758,45 @@ async function renderVMAdmin() { bind('.act-shutdown', 'shutdown'); bind('.act-migrate', 'migrate', true); }); +} + + + +async function pollTask(upid, node, onUpdate, onDone) { + let lastSt = null; + if (!upid || !node) return; + const started = Date.now(); + const maxMs = 5 * 60 * 1000; + const delay = (ms) => new Promise(r => setTimeout(r, ms)); + while (Date.now() - started < maxMs) { + try { + const r = await fetch('/api/task-status?upid=' + encodeURIComponent(upid) + '&node=' + encodeURIComponent(node)); + const d = await r.json(); + if (d && d.ok) { + const st = d.status || {}; lastSt = st; if (onUpdate) { try { onUpdate(st); } catch { } } + const s = (st.status || '').toLowerCase(); + if (s === 'stopped' || st.exitstatus) break; + } + } catch (e) { } + await delay(2000); + } + try { if (onDone) onDone(lastSt); } catch { } +} + + +function ensureMigRow(mainTr, colSpan) { + let nxt = mainTr.nextElementSibling; + if (!nxt || !nxt.classList.contains('mig-row')) { + nxt = document.createElement('tr'); + nxt.className = 'mig-row d-none'; + const td = document.createElement('td'); + td.colSpan = colSpan; + td.innerHTML = '
Migration status
'; + nxt.appendChild(td); + mainTr.parentNode.insertBefore(nxt, mainTr.nextSibling); + } + return nxt; +} +function setMigRowVisible(row, vis) { + row.classList.toggle('d-none', !vis); } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index eeff526..03a5ea2 100644 --- a/templates/index.html +++ b/templates/index.html @@ -243,12 +243,13 @@ Node Status Actions - Target (migrate) + Target + Migrate - Loading… + Loading… @@ -275,7 +276,7 @@ - Loading… + Loading…