2 Commits
Author SHA1 Message Date
Mitchell-JohnsonandClaude Fable 5.1 66498a58c5 VIRTUALIZATION.md: phase 1 deployed as v0.1.0-184, Install verified live on grid
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 15:31:06 -07:00
Mitchell-JohnsonandClaude Fable 5.1 b790b0c31c virt: the description is an instance field, not a config key — set it with a PATCH
Found on the first live test on grid: incus config set refused 'description' and the whole
limits save with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-09 15:30:10 -07:00
2 changed files with 29 additions and 9 deletions
+15 -7
View File
@@ -1,12 +1,20 @@
# Virtualization tab — plan (2026-10-09; phase 1 built the same day)
**Status:** phase 1 is written, tested and committed as **v0.1.0-180** (`internal/incus`,
`internal/api/virt.go`, `Virtualization.tsx`), **not yet deployed** — `make deploy` was
refused by the session's auto-mode classifier, so it is his to run. Nothing is installed on
any machine yet; Install has not been exercised against a live host. First real test is
Install on `grid` (zfs pool on a file, Docker present), then `core` (btrfs loop, Docker
present), then the Mac mini (dnf, arm64). Not built yet from the phase-1 list: the
`instance.*` sensors and the Alerts default — next session.
**Status:** phase 1 is **deployed to all five machines as v0.1.0-184** (2026-10-09) and
**Install has run for real on `grid`**: Incus 6.0.4, a zfs pool on a 197 GiB sparse file in
`~/apps/hamlab/virt/`, `incusbr0` at `172.16.100.1/24`, the two `DOCKER-USER` rules, the
`incus-admin` group and Ham Lab's restart — one task, 90 seconds, exit 0. Verified
afterwards: a Debian 13 container and a Debian 13 VM created from the hub, both got a lease,
DNS and the internet through Docker's `FORWARD DROP`, the VM's agent answered `exec`;
snapshot create/restore/delete, limits and a snapshot schedule, a shell as a Terminal tab
through the hub, the catalogue (98 x86_64 container images, 1.1 s then cached), and the
refusals (bad names, wrong delete confirmation, a size that isn't one). Both test instances
were deleted again; the two Debian images stay in the store. One bug found and fixed on the
way: `description` is an instance field, not a config key (v0.1.0-184). Detection on the
other four: `core`, the Mac mini and `hamtv` show **Install**; `gate` shows **unavailable**
(OpenWrt's apk has no incus, no KVM). Not built yet from the phase-1 list: the `instance.*`
sensors and the Alerts default — next session. The write-back for `grid` is in
`hamlet/docs/hosts.md`, `docker.md` and `gotchas.md`.
A tab for creating, watching, consoling into and deleting **LXC containers and KVM virtual
machines**, the way Proxmox does with `pct` and `qm` — **per machine, like every other
+14 -2
View File
@@ -388,6 +388,12 @@ func (s *Server) virtConfig(w http.ResponseWriter, r *http.Request) {
return
}
var set, unset []string
desc, hasDesc := body["description"]
delete(body, "description")
if hasDesc && (len(desc) > 200 || strings.ContainsAny(desc, "\n\r")) {
jsonErr(w, 400, "bad value for description")
return
}
for k, v := range body {
v = strings.TrimSpace(v)
ok := false
@@ -400,8 +406,6 @@ func (s *Server) virtConfig(w http.ResponseWriter, r *http.Request) {
ok = incus.ValidCron(v)
case "snapshots.expiry":
ok = incus.ValidExpiry(v)
case "description":
ok = len(v) <= 200 && !strings.ContainsAny(v, "\n\r")
case "boot.autostart":
ok = v == "" || v == "true" || v == "false"
}
@@ -429,6 +433,14 @@ func (s *Server) virtConfig(w http.ResponseWriter, r *http.Request) {
for _, k := range unset {
_, _ = s.virt.Cmd(ctx, "config", "unset", name, k).CombinedOutput() // unsetting what isn't set is fine
}
if hasDesc {
// the description is a field of the instance, not a config key, so it goes in through the API
patch, _ := json.Marshal(map[string]string{"description": strings.TrimSpace(desc)})
if out, err := s.virt.Cmd(ctx, "query", "-X", "PATCH", "-d", string(patch), "/1.0/instances/"+name).CombinedOutput(); err != nil {
jsonErr(w, 409, strings.TrimPrefix(strings.TrimSpace(string(out)), "Error: "))
return
}
}
log.Printf("virt: config %s set %v unset %v (from %s)", name, set, unset, clientIP(r))
w.WriteHeader(204)
}