Virtualization phase 2: ISOs, New VM from ISO, the VGA console in the browser, nightly exports, mounts, instance sensors

- Library: an ISO shelf in virt/isos: upload (the Files uploader), fetch by URL with an
  optional sha256, import a file from the machine by copying it in, rename, delete (refused
  while attached), attach to a VM as a CD-ROM live or stopped with boot-first, eject, and
  New VM from ISO: an empty VM with the ISO as first boot device, Secure Boot on or off.
- The VGA console: Ham Lab asks Incus for the VM's SPICE console over its unix socket and
  bridges the display websocket to the browser, where spice-html5 (vendored from upstream
  b372aab, LGPL, loaded on demand) draws it; Ctrl+Alt+Del, fit-to-window.
- Exports: every instance as a tarball in virt/exports/<name>/, kept to a count, as a cron
  line in /etc/cron.d/hamlab-virt calling hamlab virt-export, settings and the last run
  as files beside the data; Export now per instance or for all.
- Disks tab per instance: the root disk, attached ISOs with Eject, bind mounts in and out
  (shift for containers, virtiofs for VMs).
- instance.* sensors (running, down, state, cpu, mem) and the VM-or-container-down alert
  default, offered once to installs seeded before it existed.
- cloud-init: Use my SSH keys writes a cloud-config from the account's public keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-10-09 22:21:57 -07:00
co-authored by Claude Fable 5.1
parent 66498a58c5
commit 43ebacdd92
46 changed files with 14493 additions and 25 deletions
+21
View File
@@ -30,6 +30,7 @@ import (
"hamlab/internal/config"
"hamlab/internal/cron"
"hamlab/internal/files"
"hamlab/internal/incus"
"hamlab/internal/metrics"
"hamlab/internal/nodes"
"hamlab/internal/priv"
@@ -62,6 +63,7 @@ func main() {
passwd [user] set a user's password (makes the account if it is new; reads stdin when piped)
users list the accounts
zfs-snapshot <id> run one ZFS snapshot schedule (what its cron line calls)
virt-export [name…] export Incus instances to the library (what its cron line calls)
docker-update <id> run one image-update schedule (what its cron line calls)
user-rm <user> remove an account and sign it out everywhere
check verify the config and state load, then exit
@@ -143,6 +145,10 @@ func main() {
if err := zfsSnapshot(cfg, flag.Arg(1)); err != nil {
log.Fatal(err)
}
case "virt-export": // the nightly cron line from the Virtualization tab; optional instance names
if err := virtExport(cfg, flag.Args()[1:]); err != nil {
log.Fatal(err)
}
case "users":
migrateUsers(cfg)
if len(cfg.Users) == 0 {
@@ -293,6 +299,21 @@ func zfsSnapshot(cfg *config.Config, id string) error {
return nil
}
// virtExport runs the instance exports the Virtualization tab scheduled, as the account Ham
// Lab runs as (which is in incus-admin), and records the run beside the data.
func virtExport(cfg *config.Config, names []string) error {
who := os.Getenv("USER")
if u, err := user.Current(); err == nil && u.Username != "" {
who = u.Username
}
run := incus.New(cfg.DataDir(), who, version).RunExports(context.Background(), names, func(l string) { fmt.Println(l) })
fmt.Printf("exported %d, pruned %d, %.0f s\n", len(run.Exported), len(run.Pruned), run.Seconds)
if len(run.Errors) > 0 {
return errors.New(strings.Join(run.Errors, "; "))
}
return nil
}
func migrateUsers(cfg *config.Config) {
name := os.Getenv("USER")
if u, err := user.Current(); err == nil && u.Username != "" {