refator_comm1
This commit is contained in:
54
app.py
54
app.py
@@ -586,21 +586,22 @@ def ws_task(ws):
|
|||||||
# ---------------- WebSocket: broadcast observe per sid ----------------
|
# ---------------- WebSocket: broadcast observe per sid ----------------
|
||||||
@sock.route("/ws/observe")
|
@sock.route("/ws/observe")
|
||||||
def ws_observe(ws):
|
def ws_observe(ws):
|
||||||
"""
|
|
||||||
Streamuje zmiany stanu VM/CT i aktywne taski dla danego SID.
|
|
||||||
Query: sid=vm:101 (albo ct:123)
|
|
||||||
"""
|
|
||||||
q = ws.environ.get("QUERY_STRING", "")
|
q = ws.environ.get("QUERY_STRING", "")
|
||||||
params = {}
|
params = {}
|
||||||
for part in q.split("&"):
|
for part in q.split("&"):
|
||||||
if not part: continue
|
if not part: continue
|
||||||
k, _, v = part.partition("="); params[k] = v
|
k, _, v = part.partition("="); params[k] = v
|
||||||
sid = (params.get("sid") or "").strip()
|
sid_raw = (params.get("sid") or "").strip()
|
||||||
|
sid = norm_sid(sid_raw)
|
||||||
if not sid:
|
if not sid:
|
||||||
ws.send(json.dumps({"type":"error","error":"sid required"})); return
|
ws.send(json.dumps({"type":"error","error":"sid required"})); return
|
||||||
|
|
||||||
|
# Resolve tuple + node
|
||||||
|
def resolve_tuple() -> Optional[Tuple[str,int,str]]:
|
||||||
meta = cluster_vmct_meta()
|
meta = cluster_vmct_meta()
|
||||||
tup = sid_to_tuple(sid, meta)
|
return sid_to_tuple(sid, meta)
|
||||||
|
|
||||||
|
tup = resolve_tuple()
|
||||||
if not tup:
|
if not tup:
|
||||||
ws.send(json.dumps({"type":"error","error":"unknown sid"})); return
|
ws.send(json.dumps({"type":"error","error":"unknown sid"})); return
|
||||||
typ, vmid, node = tup
|
typ, vmid, node = tup
|
||||||
@@ -609,35 +610,44 @@ def ws_observe(ws):
|
|||||||
|
|
||||||
last_hash = None
|
last_hash = None
|
||||||
seen_upids = set()
|
seen_upids = set()
|
||||||
|
prev_node = node
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
ntup = resolve_tuple()
|
||||||
|
if ntup:
|
||||||
|
_, _, cur_node = ntup
|
||||||
|
if cur_node and cur_node != node:
|
||||||
|
ws.send(json.dumps({"type":"moved","old_node":node,"new_node":cur_node,"meta":{"sid":sid,"vmid":vmid,"typ":typ}}))
|
||||||
|
prev_node, node = node, cur_node
|
||||||
|
|
||||||
base = f"/nodes/{node}/{typ}/{vmid}"
|
base = f"/nodes/{node}/{typ}/{vmid}"
|
||||||
cur = get_json(["pvesh", "get", f"{base}/status/current"]) or {}
|
cur = get_json(["pvesh", "get", f"{base}/status/current"]) or {}
|
||||||
# wyślij tylko gdy zmiana
|
|
||||||
cur_hash = json.dumps(cur, sort_keys=True)
|
cur_hash = json.dumps(cur, sort_keys=True)
|
||||||
if cur_hash != last_hash:
|
if cur_hash != last_hash:
|
||||||
last_hash = cur_hash
|
last_hash = cur_hash
|
||||||
ws.send(json.dumps({"type":"vm","current":cur,"meta":{"sid":norm_sid(sid),"node":node,"typ":typ,"vmid":vmid}}))
|
ws.send(json.dumps({"type":"vm","current":cur,"meta":{"sid":sid,"node":node,"typ":typ,"vmid":vmid}}))
|
||||||
|
|
||||||
tasks = get_json(["pvesh","get",f"/nodes/{node}/tasks","-limit","50"]) or []
|
nodes_to_scan = [node] + ([prev_node] if prev_node and prev_node != node else [])
|
||||||
|
for nX in nodes_to_scan:
|
||||||
|
tasks = get_json(["pvesh","get",f"/nodes/{nX}/tasks","-limit","50"]) or []
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
upid = t.get("upid") or t.get("pstart") # upid musi być stringiem
|
upid = t.get("upid") if isinstance(t, dict) else None
|
||||||
tid = (t.get("id") or "") # np. "qemu/101" lub "lxc/123"
|
tid = (t.get("id") or "") if isinstance(t, dict) else ""
|
||||||
if not isinstance(upid, str): continue
|
if not upid or not isinstance(upid, str): continue
|
||||||
|
# dopasuj po vmid lub ciągu qemu/<vmid>|lxc/<vmid>
|
||||||
if (str(vmid) in tid) or (f"{'qemu' if typ=='qemu' else 'lxc'}/{vmid}" in tid):
|
if (str(vmid) in tid) or (f"{'qemu' if typ=='qemu' else 'lxc'}/{vmid}" in tid):
|
||||||
# status szczegółowy
|
st = get_json(["pvesh","get",f"/nodes/{nX}/tasks/{upid}/status"]) or {}
|
||||||
st = get_json(["pvesh","get",f"/nodes/{node}/tasks/{upid}/status"]) or {}
|
ws.send(json.dumps({"type":"task","upid":upid,"status":st.get("status"),"exitstatus":st.get("exitstatus"),"node":nX}))
|
||||||
ev = {"type":"task","upid":upid,"status":st.get("status"),"exitstatus":st.get("exitstatus"),"node":node}
|
# nowy running
|
||||||
ws.send(json.dumps(ev))
|
if upid not in seen_upids and str(st.get("status","")).lower() != "stopped":
|
||||||
# nowy "running" task → ogłoś raz
|
|
||||||
if upid not in seen_upids and (str(st.get("status","")).lower() != "stopped"):
|
|
||||||
seen_upids.add(upid)
|
seen_upids.add(upid)
|
||||||
ws.send(json.dumps({"type":"task-start","upid":upid,"node":node}))
|
ws.send(json.dumps({"type":"task-start","upid":upid,"node":nX}))
|
||||||
# zakończenie
|
# zakończone
|
||||||
if str(st.get("status","")).lower() == "stopped" or st.get("exitstatus"):
|
if str(st.get("status","")).lower() == "stopped" or st.get("exitstatus"):
|
||||||
ws.send(json.dumps({"type":"done","upid":upid,"ok":str(st.get('exitstatus','')).upper()=='OK'}))
|
ws.send(json.dumps({"type":"done","upid":upid,"ok":str(st.get('exitstatus','')).upper()=='OK',"node":nX}))
|
||||||
|
|
||||||
time.sleep(2.0)
|
time.sleep(1.8)
|
||||||
except Exception:
|
except Exception:
|
||||||
try: ws.close()
|
try: ws.close()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
@@ -34,7 +34,8 @@ export async function renderVMAdmin() {
|
|||||||
Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => {
|
Array.from(tbody.querySelectorAll('tr[data-sid]')).forEach(tr => {
|
||||||
const sid = tr.getAttribute('data-sid');
|
const sid = tr.getAttribute('data-sid');
|
||||||
const colSpan = tr.children.length;
|
const colSpan = tr.children.length;
|
||||||
const badgeCell = tr.children[4];
|
const nodeCell = tr.children[3]; // Node
|
||||||
|
const badgeCell = tr.children[4]; // Status
|
||||||
|
|
||||||
// subpanel (log)
|
// subpanel (log)
|
||||||
let sub = tr.nextElementSibling;
|
let sub = tr.nextElementSibling;
|
||||||
@@ -50,7 +51,11 @@ export async function renderVMAdmin() {
|
|||||||
let wsObs = null; // observe websocket
|
let wsObs = null; // observe websocket
|
||||||
let wsTask = null; // tail websocket (auto z observe)
|
let wsTask = null; // tail websocket (auto z observe)
|
||||||
|
|
||||||
const closeWS = () => { try { wsObs && wsObs.close(); } catch {} try { wsTask && wsTask.close(); } catch {} wsObs = wsTask = null; };
|
const closeWS = () => {
|
||||||
|
try { wsObs && wsObs.close(); } catch {}
|
||||||
|
try { wsTask && wsTask.close(); } catch {}
|
||||||
|
wsObs = wsTask = null;
|
||||||
|
};
|
||||||
|
|
||||||
const setRowBusy = (busy) => {
|
const setRowBusy = (busy) => {
|
||||||
const nameCell = tr.children[2];
|
const nameCell = tr.children[2];
|
||||||
@@ -100,30 +105,41 @@ export async function renderVMAdmin() {
|
|||||||
wsObs.onmessage = (ev) => {
|
wsObs.onmessage = (ev) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(ev.data);
|
const msg = JSON.parse(ev.data);
|
||||||
|
|
||||||
if (msg.type === 'vm' && msg.current) {
|
if (msg.type === 'vm' && msg.current) {
|
||||||
// aktualizuj badge na podstawie bieżącego statusu
|
|
||||||
const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase();
|
const st = String(msg.current.status || msg.current.qmpstatus || '').toLowerCase();
|
||||||
const ok = /running|online|started/.test(st);
|
const ok = /running|online|started/.test(st);
|
||||||
|
if (badgeCell) {
|
||||||
badgeCell.innerHTML = ok ? badge('running','ok') :
|
badgeCell.innerHTML = ok ? badge('running','ok') :
|
||||||
(/stopp|shutdown|offline/.test(st) ? badge('stopped','dark') : badge(st||'—','info'));
|
(/stopp|shutdown|offline/.test(st) ? badge('stopped','dark') : badge(st||'—','info'));
|
||||||
} else if (msg.type === 'task-start' && msg.upid && msg.node) {
|
}
|
||||||
// automatycznie podłącz tail do nowo wykrytego taska
|
}
|
||||||
|
|
||||||
|
else if (msg.type === 'task-start' && msg.upid && msg.node) {
|
||||||
openTaskWS(msg.upid, msg.node);
|
openTaskWS(msg.upid, msg.node);
|
||||||
} else if (msg.type === 'task' && msg.upid && msg.status) {
|
}
|
||||||
// szybkie mrugnięcie statusem
|
|
||||||
|
else if (msg.type === 'task' && msg.upid && msg.status) {
|
||||||
const stopped = String(msg.status||'').toLowerCase() === 'stopped';
|
const stopped = String(msg.status||'').toLowerCase() === 'stopped';
|
||||||
if (stopped && typeof msg.exitstatus !== 'undefined') {
|
if (stopped && typeof msg.exitstatus !== 'undefined') {
|
||||||
const ok = String(msg.exitstatus||'').toUpperCase() === 'OK';
|
const ok = String(msg.exitstatus||'').toUpperCase() === 'OK';
|
||||||
badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err');
|
if (badgeCell) badgeCell.innerHTML = ok ? badge('running','ok') : badge('error','err');
|
||||||
} else {
|
} else {
|
||||||
badgeCell.innerHTML = badge('working','info');
|
if (badgeCell) badgeCell.innerHTML = badge('working','info');
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'done' && msg.upid) {
|
}
|
||||||
// koniec zewnętrznego zadania (bez naszego taila)
|
|
||||||
if (msg.ok) badgeCell.innerHTML = badge('running','ok');
|
else if (msg.type === 'moved' && msg.new_node) {
|
||||||
else badgeCell.innerHTML = badge('error','err');
|
if (nodeCell) nodeCell.textContent = msg.new_node;
|
||||||
try { document.getElementById('btnRefresh').click(); } catch {}
|
try { document.getElementById('btnRefresh').click(); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (msg.type === 'done' && msg.upid) {
|
||||||
|
if (typeof msg.ok === 'boolean') {
|
||||||
|
if (badgeCell) badgeCell.innerHTML = msg.ok ? badge('running','ok') : badge('error','err');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
wsObs.onclose = () => { wsObs = null; };
|
wsObs.onclose = () => { wsObs = null; };
|
||||||
@@ -157,10 +173,8 @@ export async function renderVMAdmin() {
|
|||||||
tr.querySelector('.act-shutdown')?.addEventListener('click', () => { toggleSub(true); doAction('shutdown'); });
|
tr.querySelector('.act-shutdown')?.addEventListener('click', () => { toggleSub(true); doAction('shutdown'); });
|
||||||
tr.querySelector('.act-migrate')?.addEventListener('click', () => { toggleSub(true); doAction('migrate', true); });
|
tr.querySelector('.act-migrate')?.addEventListener('click', () => { toggleSub(true); doAction('migrate', true); });
|
||||||
|
|
||||||
// Status – pokaz/ukryj subpanel (bez WS)
|
|
||||||
tr.querySelector('.act-status')?.addEventListener('click', () => toggleSub(sub.classList.contains('d-none')));
|
tr.querySelector('.act-status')?.addEventListener('click', () => toggleSub(sub.classList.contains('d-none')));
|
||||||
|
|
||||||
// NEW: Watch 🔔 – włącz/wyłącz broadcast observe
|
|
||||||
const watchBtn = tr.querySelector('.act-watch');
|
const watchBtn = tr.querySelector('.act-watch');
|
||||||
if (watchBtn) {
|
if (watchBtn) {
|
||||||
watchBtn.addEventListener('click', () => {
|
watchBtn.addEventListener('click', () => {
|
||||||
@@ -175,6 +189,5 @@ export async function renderVMAdmin() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user