Virtualization tab, phase 1: Incus per machine — detect, install, instances, consoles, snapshots, images

The tab reports one of four states for the selected machine (ready, repair, installable,
unavailable) from what it finds — /dev/kvm, the package manager's incus candidate, the
pool drivers the kernel has — and acts on that machine only. Install is one followed task:
packages, a pool whose backing file sits in Ham Lab's own virt/ folder (zfs pool on a file,
btrfs on a loop mount, or a plain dir), a bridge on a subnet picked clear of the machine's
routes and Docker's pools (never 10.x on a /8 LAN), the DOCKER-USER rules that keep
Docker's FORWARD drop off the bridge, the incus-admin group for Ham Lab's account, and a
restart. Instances list with live CPU and memory, create from the images: catalogue or a
local image (container or VM, limits, cloud-init), start/stop/restart/force-stop/rename/
delete, console and shell as terminal tabs, snapshots with Incus's own schedule and expiry,
limits editing. Images: local store, catalogue search and download (exported into the
library too), import a tarball or a VM disk from a path, publish an instance.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-10-09 14:09:56 -07:00
co-authored by Claude Fable 5.1
parent af53b45b67
commit 240129c83f
9 changed files with 2311 additions and 3 deletions
+17
View File
@@ -34,6 +34,7 @@ import (
"hamlab/internal/cron"
"hamlab/internal/files"
"hamlab/internal/hardware"
"hamlab/internal/incus"
"hamlab/internal/metrics"
"hamlab/internal/network"
"hamlab/internal/nodes"
@@ -75,6 +76,7 @@ type Server struct {
update updateState
events eventLog
kvms kvmProxies
virt *incus.Manager
fsLocal files.FS
fsSudo files.FS // nil without passwordless sudo
nodes *nodes.Manager
@@ -104,6 +106,7 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
if s.user == "" {
s.user = "?"
}
s.virt = incus.New(cfg.DataDir(), s.user, version)
s.migrateUsers()
// The speed test and the outside view. Its history outlives a restart, and its last result
// is published as sensors, so the line is graphable and alertable like anything else.
@@ -239,6 +242,20 @@ func (s *Server) routes() *http.ServeMux {
m.HandleFunc("GET /api/docker/containers/{id}/events", s.auth(s.dockerEvents))
m.HandleFunc("POST /api/docker/containers/{id}/exec", s.auth(s.dockerExec))
m.HandleFunc("POST /api/docker/containers/{id}/limits", s.auth(s.dockerUpdate))
m.HandleFunc("GET /api/virt", s.auth(s.virtOverview))
m.HandleFunc("POST /api/virt/install", s.auth(s.virtInstall))
m.HandleFunc("GET /api/virt/images/remote", s.auth(s.virtRemoteImages))
m.HandleFunc("POST /api/virt/images/download", s.auth(s.virtImageDownload))
m.HandleFunc("POST /api/virt/images/import", s.auth(s.virtImageImport))
m.HandleFunc("POST /api/virt/images/publish", s.auth(s.virtImagePublish))
m.HandleFunc("POST /api/virt/images/delete", s.auth(s.virtImageDelete))
m.HandleFunc("POST /api/virt/instances", s.auth(s.virtCreate))
m.HandleFunc("GET /api/virt/instances/{name}", s.auth(s.virtInstance))
m.HandleFunc("POST /api/virt/instances/{name}/rename", s.auth(s.virtRename))
m.HandleFunc("PUT /api/virt/instances/{name}/config", s.auth(s.virtConfig))
m.HandleFunc("POST /api/virt/instances/{name}/snapshots", s.auth(s.virtSnapshotCreate))
m.HandleFunc("POST /api/virt/instances/{name}/snapshots/{snap}/{action}", s.auth(s.virtSnapshotAction))
m.HandleFunc("POST /api/virt/instances/{name}/{action}", s.auth(s.virtAction))
m.HandleFunc("GET /api/alerts", s.auth(s.alertsGet))
m.HandleFunc("PUT /api/alerts", s.auth(s.alertsPut))
m.HandleFunc("POST /api/alerts/test", s.auth(s.alertsTest))