/* * SysDeck - Kata Panel (v0.0.43) * Author: Jeremy Anderson (https://dcos.net) * * v0.0.43 PRODUCTION REWRITE — PREVIOUS VERSION WAS MOCK DATA. * * The v0.0.35-v0.0.43 Kata panel shipped a pre-built React bundle * from the upstream cockpit-kata sub-project. That bundle displayed * HARDCODED MOCK DATA: * - 5 fake sandboxes (web-frontend-prod, api-gateway-staging, etc.) * with synthetic UUIDs and createdAt:"2026-07-15..." timestamps * - fake per-sandbox metrics (cpuUsagePercent, memoryUsageMB, * historyCpu/historyMemory arrays) * - a fake QCrows bundle catalog * - a fake PXE status (always dnsmasqRunning:true) * The only real features were the QCrows kernel-bundle extraction * (qcrows-export / qcrows-initrd-regen via cockpit.spawn) and the * kata-runtime check call. * * v0.0.43 deletes the React bundle and ships this vanilla-JS panel * backed by bridge/kata.py. Every value displayed is REAL: * - Sandbox list comes from kata-monitor /sandboxes + filesystem * enumeration of /run/vc/sbs/ (Go shim) + /run/kata/ (Rust shim). * - Per-sandbox metrics come from kata-monitor /metrics?sandbox= * (Prometheus text, parsed). * - Runtime version comes from `kata-runtime version` + `kata-runtime * env --json`. * - Host capability comes from `kata-runtime check` (exit code). * - PXE status comes from `systemctl is-active dnsmasq` + real * filesystem probes of /srv/tftp/. * - QCrows bundle list comes from real filesystem enumeration of * /usr/share/sysdeck/kata/qcrows/. * * When no sandboxes are running, the panel shows an EMPTY STATE * (not mock data). When kata-runtime is not installed, the panel * shows an install hint. When kata-monitor is not running, the * metrics card shows a hint to start it. * * Security hardening (v0.0.36 + v0.0.43): * - Sandbox IDs validated with ^[0-9a-f]{64}$ in the bridge before * any subprocess or HTTP call. CVE-2024-2947 lesson. * - All bridge output rendered with escapeHtml() / textContent. * CVE-2022-36446 lesson. * - No innerHTML on bridge data. * - HTTP to kata-monitor is 127.0.0.1-only, no redirects (SSRF * defense). CVE-2020-35850 lesson. */ export async function mount(panel, { bridge, EventBus }) { panel.innerHTML = renderSkeleton(); // Load summary + pxe-status + qcrows-list in parallel. const [summary, pxeStatus, qcrowsList] = await Promise.all([ safe(bridge.kata.summary(), { total_sandboxes: 0, running_sandboxes: 0, sandboxes: [], kata_monitor: { running: false }, kata_runtime: { installed: false }, host_capable: false, check_message: 'unknown', }), safe(bridge.kata.pxeStatus(), { dnsmasq_running: false, tftp_dir_exists: false, tftp_dir_writable: false, pxelinux_entries: [], }), safe(bridge.kata.qcrowsList(), []), ]); panel.innerHTML = `

SysDeck Kata

Kata Containers — hardware-virtualized OCI sandboxes · ${summary.host_capable ? 'host capable' : 'host not capable'} · ${summary.total_sandboxes} sandbox${summary.total_sandboxes === 1 ? '' : 'es'} ${summary.running_sandboxes !== summary.total_sandboxes ? ` (${summary.running_sandboxes} running)` : ''} ${summary.kata_runtime?.installed ? ` · kata-runtime ${escapeHtml(summary.kata_runtime.version || '?')}` : ' · kata-runtime not installed'} ${summary.kata_monitor?.running ? ' · kata-monitor running' : ' · kata-monitor not running'}

${renderRuntimeCard(summary)} ${renderSandboxList(summary.sandboxes, summary.kata_monitor)} ${renderPxeCard(pxeStatus)} ${renderQcrowsCard(qcrowsList)} `; wireEvents(panel, { bridge, EventBus }); EventBus.emit('kata.loaded', { totalSandboxes: summary.total_sandboxes, runningSandboxes: summary.running_sandboxes, hostCapable: summary.host_capable, }); } // ── Render helpers ────────────────────────────────────────────────── function renderRuntimeCard(summary) { const rt = summary.kata_runtime || {}; if (!rt.installed) { return `

Runtime

kata-runtime is not installed. Install Kata Containers 3.x:

# Arch (AUR):
yay -S kata-runtime kata-containers-image

# Debian/Ubuntu (official repo):
sudo apt install kata-runtime kata-containers-image

# Or build from source (you mentioned compiling yesterday):
# https://github.com/kata-containers/kata-containers/blob/main/docs/install/

After install, run kata-runtime check to verify host capability (nested virt, KVM, etc.).

`; } const env = rt.env || {}; const host = env.Host || {}; const hypervisor = env.Hypervisor || {}; return `

Runtime

${host.Kernel ? `` : ''} ${host.Architecture ? `` : ''} ${hypervisor.Path ? `` : ''} ${hypervisor.MachineType ? `` : ''}
kata-runtime version${escapeHtml(rt.version || '?')}
commit${escapeHtml(rt.commit || '?')}
OCI specs${escapeHtml(rt.oci || '?')}
host capable${summary.host_capable ? 'yes' : 'no'}
check message${escapeHtml(summary.check_message || '')}
host kernel${escapeHtml(host.Kernel)}
architecture${escapeHtml(host.Architecture)}
hypervisor${escapeHtml(hypervisor.Path)}
machine type${escapeHtml(hypervisor.MachineType)}
`; } function renderSandboxList(sandboxes, kataMonitor) { if (!sandboxes || !sandboxes.length) { return `

Sandboxes (0)

No kata sandboxes running. This is the real empty state — not mock data. Sandboxes are enumerated from:

  • kata-monitor /sandboxes (HTTP, port 8090)${kataMonitor?.running ? ' ✓ running' : ' — not running'}
  • /run/vc/sbs/<id>/ (Go shim filesystem)
  • /run/kata/<id>/ (Rust shim filesystem)

To create a sandbox, use ctr, crictl, or kubectl with RuntimeClass kata.

`; } const rows = sandboxes.map((sb) => { const idShort = sb.id.substring(0, 12); const isRunning = sb.agent_url || sb.shim_socket; return ` ${escapeHtml(idShort)}… ${isRunning ? 'running' : 'unknown'} ${escapeHtml(sb.source || '?')} ${sb.agent_url ? escapeHtml(sb.agent_url) : '—'} `; }).join(''); return `

Sandboxes (${sandboxes.length})

${rows}
IDStatusSourceAgent URLActions
`; } function renderPxeCard(pxe) { const dnsmasqBadge = pxe.dnsmasq_running ? 'running' : 'not running'; const tftpExistsBadge = pxe.tftp_dir_exists ? 'exists' : 'missing'; const tftpWritableBadge = pxe.tftp_dir_writable ? 'writable' : 'not writable'; const entries = (pxe.pxelinux_entries || []).length ? pxe.pxelinux_entries.map((e) => `${escapeHtml(e)}`).join('') : '(no entries)'; return `

PXE / TFTP Boot

dnsmasq${dnsmasqBadge}
/srv/tftp${tftpExistsBadge} ${tftpWritableBadge}
pxelinux.cfg/ entries${entries}

PXE boot configuration for network-booting kata sandboxes. dnsmasq serves DHCP + TFTP; pxelinux.cfg/ holds per-host boot configs (named by MAC address or "default").

`; } function renderQcrowsCard(qcrows) { if (!qcrows || !qcrows.length) { return `

QCrows VM Container Images (0)

No QCrows images found at /usr/share/sysdeck/kata/qcrows/. This is the real empty state — not mock data.

QCrows (.qcrows, spec v0.2) is the self-describing VM container image format — rootfs + initrd + kernel + Cockpit menu metadata in one verifiable archive. Build one with:

qcrows-pack --rootfs rootfs.tar.gz --kernel vmlinuz --kernel-config .config \\
  --name alpine-3.20-kata -o alpine-3.20-kata.qcrows

ai-lsc can export the active tool stack directly as a .qcrows image (Container Stacks → Export → QCrows).

`; } const totalSize = qcrows.reduce((sum, q) => sum + (q.size_bytes || 0), 0); const totalMb = (totalSize / (1024 * 1024)).toFixed(1); const rows = qcrows.map((q) => { // Format-aware rows (spec v0.2 metadata); legacy non-QCrows // tarballs degrade to the stat-only columns. const isQcrows = q.qcrows === true; const image = q.image || {}; const kernel = q.kernel || {}; const displayName = isQcrows ? `${image.name || '?'} ${image.version || ''}`.trim() : '(not a QCrows archive)'; const kernelCell = isQcrows ? `${kernel.version || '?'} · ${kernel.format || '?'}${q.kernel_binary_present === false ? ' ⚠ missing' : ''}` : '—'; const hyperCell = isQcrows && Array.isArray(q.hypervisors) ? q.hypervisors.join(', ') : '—'; return `
${escapeHtml(displayName)}
${escapeHtml(q.filename)}
${q.size_mb} MB ${escapeHtml(kernelCell)} ${escapeHtml(image.arch || '—')} ${escapeHtml(hyperCell)} ${new Date(q.mtime * 1000).toISOString().split('T')[0]} `; }).join(''); return `

QCrows VM Container Images (${qcrows.length}, ${totalMb} MB total)

${rows}
ImageSizeKernelArchHypervisorsModifiedActions

Verify runs the cockpit-kata master checks in memory (hashes, kernel magic + config, metadata); Inspect shows the full archive detail. Non-QCrows tarballs in the directory are listed but not verifiable.

`; } // ── Event wiring ──────────────────────────────────────────────────── function wireEvents(panel, { bridge, EventBus }) { const output = (msg, isError = false) => { const card = panel.querySelector('#kata-output'); const pre = panel.querySelector('#kata-output-pre'); if (!card || !pre) return; card.style.display = 'block'; pre.textContent = msg; pre.style.color = isError ? 'var(--sysdeck-accent-danger)' : 'var(--sysdeck-fg)'; }; panel.querySelector('#btn-kata-output-close')?.addEventListener('click', () => { const card = panel.querySelector('#kata-output'); if (card) card.style.display = 'none'; }); // Refresh buttons (multiple — runtime card + sandbox card). panel.querySelectorAll('#btn-kata-refresh, #btn-kata-pxe-refresh').forEach((btn) => { btn.addEventListener('click', () => mount(panel, { bridge, EventBus })); }); // Inspect buttons. panel.querySelectorAll('.btn-kata-inspect').forEach((btn) => { btn.addEventListener('click', async () => { const sid = btn.dataset.id; const card = panel.querySelector('#kata-inspect-card'); const pre = panel.querySelector('#kata-inspect-pre'); if (!card || !pre) return; card.style.display = 'block'; pre.textContent = `Inspecting ${sid.substring(0, 12)}…`; try { const r = await bridge.kata.inspect(sid); pre.textContent = JSON.stringify(r, null, 2); } catch (err) { pre.textContent = `Inspect error: ${err.message || err}`; } }); }); panel.querySelector('#btn-kata-inspect-close')?.addEventListener('click', () => { const card = panel.querySelector('#kata-inspect-card'); if (card) card.style.display = 'none'; }); // Metrics buttons. panel.querySelectorAll('.btn-kata-metrics').forEach((btn) => { btn.addEventListener('click', async () => { const sid = btn.dataset.id; const card = panel.querySelector('#kata-metrics-card'); const pre = panel.querySelector('#kata-metrics-pre'); if (!card || !pre) return; card.style.display = 'block'; pre.textContent = `Fetching metrics for ${sid.substring(0, 12)}…`; try { const r = await bridge.kata.metrics(sid); if (r.error) { pre.textContent = `Metrics error: ${r.error}`; } else if (r.parsed && r.summary) { const s = r.summary; pre.textContent = [ `Sandbox: ${r.id}`, `CPU: ${s.cpu_usage_percent ?? 'n/a'}%`, `Memory: ${s.memory_usage_bytes != null ? (s.memory_usage_bytes / 1048576).toFixed(1) + ' MB' : 'n/a'}`, `Network RX: ${s.network_rx_bytes ?? 'n/a'} bytes`, `Network TX: ${s.network_tx_bytes ?? 'n/a'} bytes`, `Uptime: ${s.uptime_seconds ?? 'n/a'} s`, '', '--- Raw metric families ---', JSON.stringify(r.families, null, 2), ].join('\n'); } else { pre.textContent = r.raw || '(no metrics — kata-monitor not running or sandbox not found)'; } } catch (err) { pre.textContent = `Metrics error: ${err.message || err}`; } }); }); panel.querySelector('#btn-kata-metrics-close')?.addEventListener('click', () => { const card = panel.querySelector('#kata-metrics-card'); if (card) card.style.display = 'none'; }); // QCrows image actions — Verify (in-memory master checks) and // Inspect (full archive detail). Output goes to the shared // operation-output card; every bridge value is rendered as text. panel.querySelectorAll('.btn-qcrows-verify').forEach((btn) => { btn.addEventListener('click', async () => { const filename = btn.dataset.filename; output(`Verifying ${filename}…`); try { const r = await bridge.kata.qcrowsVerify(filename); if (r && r.error) { output(`Verify error: ${r.error}`, true); return; } const lines = r.checks.map((c) => ` ${c.passed ? 'PASS' : 'FAIL'}: ${c.name} — ${c.detail}`, ); const warns = (r.warnings || []).map((w) => ` WARN: ${w}`); output([ `${r.ok ? '✔ VERIFIED' : '✘ FAILED'} — ${filename} (${r.passed} passed, ${r.failed} failed)`, ...lines, ...warns, ].join('\n'), !r.ok); } catch (err) { output(`Verify error: ${err.message || err}`, true); } }); }); panel.querySelectorAll('.btn-qcrows-inspect').forEach((btn) => { btn.addEventListener('click', async () => { const filename = btn.dataset.filename; output(`Inspecting ${filename}…`); try { const r = await bridge.kata.qcrowsInspect(filename); output(JSON.stringify(r, null, 2), Boolean(r && r.error)); } catch (err) { output(`Inspect error: ${err.message || err}`, true); } }); }); } // ── Utilities ─────────────────────────────────────────────────────── async function safe(p, fallback) { try { const v = await p; return v ?? fallback; } catch { return fallback; } } function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderSkeleton() { return `
`; }