sots-re/tools/vmwatch-install.sh
alex 962cdb6981 tools: live screen wall + contact sheet for the five lab guests
The lab went from one Windows guest to five and there was no way to see what
they were all doing without issuing a QEMU screendump per guest by hand.

- tools/vmwatch.py: always-on HTTP service serving an auto-refreshing wall of
  live guest screens. Runs on spicy as vmwatch.service; browse it at
  http://192.168.3.201:8140/. Click a tile for that guest full size. Guests are
  discovered from /etc/pve/qemu-server by matching sots-re, so clones appear and
  vanish on their own. A stopped, paused or unreachable guest gets a labelled
  placeholder tile carrying the monitor's own error, never a broken image or a
  500. Python 3 stdlib only.
- tools/vmwatch-install.sh: install/update/uninstall the unit on the host.
- tools/vmshot.py: one-shot contact sheet, and --one <id> for a full-size grab.
  Pulls frames from the vmwatch service when it is up (0.5s) and falls back to
  ssh + qm monitor when it is not (4s).
- guides/lab-screen-wall.md: how to use both, and why.

Capture goes over each guest's QMP socket rather than forking qm: qm is a Perl
program, and one fork per guest per tick cost ~90% of a host core and a 728 MB
cgroup peak. Direct QMP is 0.33 CPU-seconds per 88s and 23 MB RSS. QEMU 11 here
dumps PNG natively; the fallback PPM encoder was verified pixel-identical to
QEMU's own on a real framebuffer.

Read-only throughout: screendump does not perturb the guest (method-rule 19),
so reading VM 140's screen is not an experiment and does not take its lock.
2026-09-08 16:53:04 -04:00

60 lines
2 KiB
Bash
Executable file

#!/usr/bin/env bash
# Install / update the vmwatch fishtank service on the Proxmox host.
#
# What this puts on spicy (and nothing else -- no packages, no pip):
# /opt/vmwatch/vmwatch.py the service (Python 3 stdlib only)
# /etc/systemd/system/vmwatch.service
#
# Usage: tools/vmwatch-install.sh [host] (default host: spicy)
# tools/vmwatch-install.sh spicy --uninstall
set -euo pipefail
HOST="${1:-spicy}"
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/vmwatch.py"
PORT="${VMWATCH_PORT:-8140}"
INTERVAL="${VMWATCH_INTERVAL:-5}"
if [[ "${2:-}" == "--uninstall" ]]; then
ssh "$HOST" 'systemctl disable --now vmwatch.service 2>/dev/null || true
rm -f /etc/systemd/system/vmwatch.service
rm -rf /opt/vmwatch
systemctl daemon-reload
echo "vmwatch removed from $(hostname)"'
exit 0
fi
echo "installing vmwatch on $HOST (port $PORT, interval ${INTERVAL}s)"
ssh "$HOST" "mkdir -p /opt/vmwatch"
scp -q "$SRC" "$HOST:/opt/vmwatch/vmwatch.py"
ssh "$HOST" "chmod 0755 /opt/vmwatch/vmwatch.py"
ssh "$HOST" "cat > /etc/systemd/system/vmwatch.service" <<UNIT
[Unit]
Description=vmwatch - live screen wall for the SOTS lab VMs
Documentation=file:///opt/vmwatch/vmwatch.py
After=network-online.target pve-guests.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/vmwatch/vmwatch.py --port ${PORT} --interval ${INTERVAL}
Restart=always
RestartSec=3
# read-only instrument: it only runs \`qm list\` and \`qm monitor ... screendump\`
NoNewPrivileges=yes
ProtectHome=yes
PrivateTmp=no
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
UNIT
# `enable --now` will not restart an already-running unit, so restart explicitly
ssh "$HOST" "systemctl daemon-reload \
&& systemctl enable vmwatch.service >/dev/null \
&& systemctl restart vmwatch.service && sleep 2 && systemctl is-active vmwatch.service"
IP=$(ssh "$HOST" "hostname -I | tr ' ' '\n' | grep -E '^192\.168\.' | head -1")
echo
echo "http://${IP}:${PORT}/"