Virtualization plan: capability-gated per machine, never policy-gated; Install or report

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-10-09 12:56:18 -07:00
co-authored by Claude Fable 5.1
parent 16e85e1e42
commit 0fa6ab98e2
2 changed files with 135 additions and 78 deletions
+8 -5
View File
@@ -20,6 +20,7 @@ stub tab or a roadmap item.
| Repo | Gitea `mitchell/hamlab`, private, `main` | Created 2026-09-17. | | Repo | Gitea `mitchell/hamlab`, private, `main` | Created 2026-09-17. |
| Guest logins | A **guest** is an account like any other, with `guest: true` and an expiry: `login`, sessions and prefs need no special case, and **it may do everything an account may do**. The expiry is the whole feature — checked on every request and swept once a minute — with revoking as the manual version of it. | Built 2026-09-27. A read-only version was built first and **cut the same day**: Mitchell hands these to people he trusts and revokes them otherwise, so a console with half its tabs missing was solving a problem he doesn't have. Consequences, stated once and accepted: a guest has the Terminal tab, which is a root-capable shell on every host, and can make an account or token of its own that outlives the guest login. The one thing a guest is not is *an account*: it is left out of the Users list, out of "the last account can't be removed", and out of the single account an empty user name resolves to. | | Guest logins | A **guest** is an account like any other, with `guest: true` and an expiry: `login`, sessions and prefs need no special case, and **it may do everything an account may do**. The expiry is the whole feature — checked on every request and swept once a minute — with revoking as the manual version of it. | Built 2026-09-27. A read-only version was built first and **cut the same day**: Mitchell hands these to people he trusts and revokes them otherwise, so a console with half its tabs missing was solving a problem he doesn't have. Consequences, stated once and accepted: a guest has the Terminal tab, which is a root-capable shell on every host, and can make an account or token of its own that outlives the guest login. The one thing a guest is not is *an account*: it is left out of the Users list, out of "the last account can't be removed", and out of the single account an empty user name resolves to. |
| Machines | **A view shows one machine: the one in the switcher.** No hub-side page lists or aggregates what its nodes are doing. To see or change anything on another machine, switch to it. | Mitchell, 2026-09-25, cutting a fleet-wide updates list out of the Docker tab. The switcher is the only place a machine is chosen, so there is exactly one answer to "which machine am I looking at" — a page that quietly reaches across breaks that, and it is never obvious which machine an action would land on. The hub still *proxies* for a node, and hub-only admin (Machines, Users, Settings, Alerts) is unaffected. | | Machines | **A view shows one machine: the one in the switcher.** No hub-side page lists or aggregates what its nodes are doing. To see or change anything on another machine, switch to it. | Mitchell, 2026-09-25, cutting a fleet-wide updates list out of the Docker tab. The switcher is the only place a machine is chosen, so there is exactly one answer to "which machine am I looking at" — a page that quietly reaches across breaks that, and it is never obvious which machine an action would land on. The hub still *proxies* for a node, and hub-only admin (Machines, Users, Settings, Alerts) is unaffected. |
| Capability, not policy | **A tab offers what the selected machine can do, detected live — never what a rule says should run there.** Where the software is missing but installable, Ham Lab installs it; where it cannot be installed or the hardware lacks it, the tab says so in a sentence and offers what does work. Every action lands on the machine in the switcher. | Mitchell, 2026-10-09, after the Virtualization plan's first draft excluded the Mac mini and `hamtv` on the strength of `hamlet/CLAUDE.md`'s deployment rules: *"I want the option to do it anywhere I want, unless the hardware / software literally doesn't support it. Ham Lab needs to be built in a general-purpose way that can run on any Linux system."* Those rules bind the agent's unasked deployments, not the product. |
## Layout ## Layout
@@ -786,11 +787,13 @@ what it has moved.
free. free.
- **A Virtualization tab** — asked for 2026-09-23; **scoped 2026-10-09 in `VIRTUALIZATION.md`**, - **A Virtualization tab** — asked for 2026-09-23; **scoped 2026-10-09 in `VIRTUALIZATION.md`**,
not started. Incus 6.0 LTS on `grid` (the only sensible VM host), containers and VMs in one not started. Incus 6.0 LTS as the engine; the tab detects per machine whether Incus is
list with console/shell through the terminal machinery, snapshots, exports, and the old running, installable, or unavailable, and whether KVM is there, and acts on the machine in
Proxmox estate on `tank` as a later import. That file has the host probe, the decisions the switcher — never a default host. Containers and VMs in one list, console/shell through
(NVMe pool, `192.168.200.0/24` bridge because the LAN is a /8, the Docker `FORWARD` fix, the terminal machinery, snapshots, exports, and the old Proxmox estate on `tank` as a later
ARC cap) and the phased build. import. That file has the probe of every host, the per-machine layout Ham Lab computes
(pool driver, a bridge subnet that avoids the LAN's /8 and Docker's pools, the Docker
`FORWARD` fix) and the phased build.
## Open questions ## Open questions
+127 -73
View File
@@ -1,63 +1,109 @@
# Virtualization tab — plan (2026-10-09, not started) # Virtualization tab — plan (2026-10-09, revised the same day; not started)
A tab for creating, watching, consoling into and deleting **LXC containers and KVM virtual 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 page. machines**, the way Proxmox does with `pct` and `qm` — **per machine, like every other
This is the scoping PLAN.md asked for. Nothing here is built. Live state of the hosts page.** This is the scoping PLAN.md asked for. Nothing here is built. Live state of the
stays in `hamlet/docs/hosts.md`. hosts stays in `hamlet/docs/hosts.md`.
## Decision: Incus 6.0 LTS, on `grid` first ## The governing rule: capability, not policy
Stated by Mitchell 2026-10-09, after the first draft of this plan excluded hosts on the
strength of the homelab's deployment rules:
> Anything that's done when a different machine is selected SHOULD GO ON THAT machine. I
> want the option to do it anywhere I want, unless the hardware / software literally
> doesn't support it. Ham Lab needs to be built in a general-purpose way that can run on
> any Linux system. If certain parts / libraries aren't available on that machine, those
> tabs should just report that.
So:
- **Every action lands on the machine in the switcher.** There is no "VM host", no default
machine, no cross-machine placement. `grid` is where he will most likely click first;
the tab does not know or care.
- **The tab is gated by what the machine can do, detected live** — never by a rule about
what *should* run there. The `hamlet/CLAUDE.md` do-not-break rules (Mac mini for
single-core workloads, `hamtv` not spare hardware) bind the *agent's* unasked
deployments. They are not feature gates for the product, and the first draft was wrong
to treat them as such.
- **Ham Lab owns the install and the data layout.** It detects what is missing, offers to
install it where it knows how, chooses and records where instance storage lives and which
subnet the bridge takes, and runs the backups. Where it cannot install or the hardware
cannot do the job, it says so in one sentence and offers what *does* work (containers
without KVM, for instance).
## Decision: Incus 6.0 LTS as the engine
| Option | Verdict | | Option | Verdict |
|---|---| |---|---|
| **Incus** (LXD fork, linuxcontainers.org) | **Chosen.** One daemon, one CLI and one JSON API for both containers and VMs. In Debian 13 as `incus` 6.0.4 LTS — `apt install incus` pulls `qemu-system-x86`, `swtpm`, `virtiofsd`, `incus-agent`, so VMs work out of the box. Fedora 42 has 6.23 for the Mac mini if that is ever wanted. Native ZFS storage driver (instant snapshots and clones), built-in image server (`images:debian/13`, `images:ubuntu/24.04`, Alpine, Fedora, Arch… as container *or* VM), snapshot schedules with expiry, `incus export` backups, per-instance CPU/memory limits, cloud-init, text and VGA console over websocket. Everything is reachable as `incus … --format json` or `incus query`, which is exactly how Ham Lab already drives Docker: `exec.Command`, no new Go dependency. | | **Incus** (LXD fork, linuxcontainers.org) | **Chosen.** One daemon, one CLI and one JSON API for both containers and VMs. Packaged by Debian 13 (`incus` 6.0.4 LTS — `apt install incus` pulls `qemu-system-x86`, `swtpm`, `virtiofsd`, `incus-agent`, so VMs work out of the box), Fedora (6.23), Arch, Alpine, Ubuntu (Zabbly repo); not by OpenWrt. Storage drivers for ZFS, btrfs, LVM and plain directories, so it fits a machine with or without ZFS. Built-in image server (`images:debian/13`, `images:ubuntu/24.04`, Alpine, Fedora, Arch… as container *or* VM, per architecture), snapshot schedules with expiry, `incus export` backups, per-instance CPU/memory limits, cloud-init, text and VGA console over websocket. Everything is reachable as `incus … --format json` or `incus query`, which is exactly how Ham Lab already drives Docker: `exec.Command`, no new Go dependency, no cgo. |
| libvirt/KVM + `virsh`, plus LXC separately | Two daemons, two models, XML. libvirt's official Go binding needs cgo, which the static binary rules out (the pure-Go `go-libvirt` exists but then containers are a second project). Rejected. | | libvirt/KVM + `virsh`, plus LXC separately | Two daemons, two models, XML. libvirt's official Go binding needs cgo, which the static binary rules out (the pure-Go `go-libvirt` exists but then containers are a second project). Rejected. |
| Proxmox itself | Means reinstalling `grid`'s OS. No. | | Proxmox | An OS, not a library. Rejected. |
## Where it can run ## What each machine can do today
Probed 2026-10-09 (`/dev/kvm`, cpu flags, `kvm_intel`/`kvm` modules, RAM, disk). Probed 2026-10-09 (`/dev/kvm`, cpu flags, kvm modules, RAM, disk, package availability).
These are **facts the tab will detect at runtime**, not a list of where instances are
allowed. The last column is what the tab would show the day it ships.
| Host | KVM | RAM | Verdict | | Machine | Arch | KVM | RAM (free) | Incus from the package manager | Pool driver available | Tab state on first open |
|---|---|---|---| |---|---|---|---|---|---|---|
| `grid` | yes — i7-11700T, VT-x, `kvm_intel` loaded, nested on | 62 GiB; ZFS ARC is at 23 GiB and allowed 40 | **The VM host.** 790 GiB free on the NVMe root, 4.5 TiB free on `tank`. Cap the ARC at 16 GiB and ~30 GiB is free for guests with headroom for Immich and the encode jobs. | | `grid` | x86-64 | yes — VT-x, `kvm_intel` loaded, nested on | 62 GiB (~25; ARC holds 23 of the rest, max 40) | apt: `incus` 6.0.4 | **zfs** (2.3.9, `tank`) | **Install** — containers and VMs |
| `core` | yes — N100, `kvm_intel` loaded | 7.5 GiB, ~4 GiB available | Small LXC containers only, never a VM. Second host, later. | | `core` | x86-64 | yes — N100, `kvm_intel` loaded | 7.5 GiB (~4) | apt: `incus` 6.0.4 | btrfs or dir (ZFS was purged 2026-09-23) | **Install** — containers and VMs; the create form shows free memory so a VM's size is an informed choice |
| Mac mini | yes — Asahi KVM, `/dev/kvm` present, 16K pages | 15 GiB, 8 GiB available | Technically workable (Fedora ships Incus 6.23 and `qemu-system-aarch64`; arm64 guests only, no x86 VMs; 4K-page guests on the 16K host are what `muvm` already does). **Excluded by the rule: single-core-performance workloads only.** | | Mac mini | aarch64 | yes — Asahi KVM, 16K pages | 15 GiB (~8) | dnf: `incus` 6.23, `qemu-system-aarch64-core`, `edk2-aarch64` | btrfs or dir | **Install** — containers and VMs, **arm64 images only** (the picker filters by host arch). Whether a stock 4K-page guest kernel boots under the 16K host is untested; `muvm` does exactly that, so it is expected to. |
| `hamtv` | yes | 3.7 GiB | In service for HamTV. Excluded. | | `hamtv` | x86-64 | yes | 3.7 GiB (~2.9) | apt: `incus` 6.0.4 | btrfs or dir | **Install** — containers and small VMs |
| `gate` | **no** `/dev/kvm` (OpenWrt) | 8 GiB | Excluded. The tab says "no virtualisation on this machine". | | `gate` | x86-64 | **no** `/dev/kvm` | 8 GiB (~6) | **none** — OpenWrt's opkg has no Incus | — | **Unavailable**: "Incus isn't packaged for OpenWrt, and this kernel has no KVM." Nothing to install. |
Nothing is installed anywhere today: no qemu, libvirt, lxc or incus on any host. Nothing is installed anywhere today: no qemu, libvirt, lxc or incus on any host.
### Detection the tab runs, in order
1. `incus` on `PATH` and its socket answering → **ready**; read pools, networks, instances.
2. Else: a package manager Ham Lab knows (apt, dnf, pacman, apk) that has an `incus`
candidate → **installable**, with the Install dialog pre-filled (below).
3. Else → **unavailable**, with the reason in one sentence (no package; no root; OpenWrt).
4. Independently: `/dev/kvm` present and openable → VMs offered. Absent → **containers
only**, said once on the page, and the create form has no VM option.
5. Architecture from `uname -m`, so the image picker only shows images that can run here.
6. Pool driver: `zfs` if the module and tools are present, else `btrfs` (installing
`btrfs-progs` as part of Install), else `dir`. The dialog says which it picked and why.
## What the tab does (v1) ## What the tab does (v1)
Mirrors the Docker tab's shape: a list, a detail page, actions that are followed tasks, Mirrors the Docker tab's shape: a list, a detail page, actions that are followed tasks,
destructive ones confirmed by typing the name. destructive ones confirmed by typing the name.
- **Instances** — containers and VMs in one list: state, IPv4, CPU and memory live, disk, - **Install** — one task that runs *on the selected machine*: install the package(s),
snapshots count, uptime. Badge for type. Click for detail. `incus admin init --preseed` with the layout from the dialog, create the pool and bridge,
- **Create** — pick container or VM; image from the `images:` remote (searchable, with the add Ham Lab's user to `incus-admin` (socket access, the `docker`-group shape) and add it
handful most people want pinned: Debian 13, Ubuntu 24.04, Alpine, Fedora); name, CPUs, to the unit's `SupplementaryGroups`, apply the Docker fix where Docker is present (below),
memory, root disk size; network (NAT bridge, or LAN once `br0` exists — below); optional then restart Ham Lab. Idempotent, so it doubles as "repair". The dialog shows and lets him
cloud-init user-data (SSH key, packages, a first command); profile. Create, or create and edit: pool driver (detected), pool size (proposed from free space), pool path, bridge
start. Collisions (name taken) named before Create. subnet (computed), whether to apply the Docker rules.
- **Instances** — containers and VMs in one list: type, state, IPv4, CPU and memory live,
disk, snapshot count, uptime. Click for detail.
- **Create** — container or VM (VM only where KVM is); image from the `images:` remote,
filtered to this architecture, searchable, with the common ones pinned (Debian 13, Ubuntu
24.04, Alpine, Fedora); name, CPUs, memory (with this machine's free memory beside it),
root disk size; network (the NAT bridge, or a LAN bridge if this machine has one);
optional cloud-init user-data (SSH key, packages, a first command); profile. Create, or
create and start. Collisions (name taken) named before Create.
- **Lifecycle** — start, stop, restart, force stop, rename, delete (type the name; the - **Lifecycle** — start, stop, restart, force stop, rename, delete (type the name; the
instance's snapshots go with it, and the dialog says so). instance's snapshots go with it, and the dialog says so).
- **Console** — two buttons, both open as tabs in the Terminal page through the existing - **Console** — two buttons, both open as tabs in the Terminal page through the existing PTY
PTY manager (`term.CreateCmd`, the `docker exec` path): **Console** (`incus console`, manager (`term.CreateCmd`, the `docker exec` path): **Console** (`incus console`, the
the serial console — works before the network is up, shows boot output) and **Shell** serial console — works before the network is up, shows boot output) and **Shell**
(`incus exec <n> -- login shell`). Works for containers and, via `incus-agent`, VMs. (`incus exec <n> -- login shell`). Containers and, via `incus-agent`, VMs.
- **Snapshots** — take, restore (confirmed), delete; a **schedule** per instance with an - **Snapshots** — take, restore (confirmed), delete; a **schedule** per instance with an
expiry, which is Incus's own `snapshots.schedule`/`snapshots.expiry`, so no cron file of expiry, which is Incus's own `snapshots.schedule`/`snapshots.expiry`, so no cron file of
ours this time. ours this time.
- **Resources** — edit CPU and memory limits (live for containers; VMs take CPU hotplug and - **Resources** — edit CPU and memory limits (live for containers; VMs take CPU hotplug and
memory balloon live with the agent, else on restart), grow the root disk, add a bind memory balloon live with the agent, else on restart), grow the root disk, add a bind mount
mount (a `tank` path into a container; virtiofs into a VM). (a host path into a container; virtiofs into a VM).
- **Images** — what is cached locally, delete, aliases; **publish** a stopped instance as an - **Images** — what is cached locally, delete, aliases; **publish** a stopped instance as an
image (Proxmox's "convert to template"). image (Proxmox's "convert to template").
- **Storage and networks** — the pools with their fill, the bridges with their subnets. - **Storage and networks** — the pools with their fill, the bridges with their subnets.
Read-only in v1 beyond what Create needs. Read-only in v1 beyond what Create and Install need.
- **Not installed here** — a host with KVM but no Incus gets an **Install** button that
runs the whole phase-0 recipe below as a task (the smartmontools-install precedent). A
host with no KVM gets one sentence.
- **Sensors** — `instance.<name>.cpu`, `.memory`, `.running` from Incus's state, like the - **Sensors** — `instance.<name>.cpu`, `.memory`, `.running` from Incus's state, like the
`container.*` sensors, so Alerts gets an "instance down" default and widgets can show one. `container.*` sensors, so Alerts gets an "instance down" default and widgets can show one.
@@ -67,41 +113,48 @@ destructive ones confirmed by typing the name.
|---|---| |---|---|
| **VGA console in the browser** | Incus hands the VM's SPICE socket over a websocket; drawing it needs `spice-html5` in the UI. Doable (the Incus UI does it), but text console + shell cover every Linux guest. Phase 3. | | **VGA console in the browser** | Incus hands the VM's SPICE socket over a websocket; drawing it needs `spice-html5` in the UI. Doable (the Incus UI does it), but text console + shell cover every Linux guest. Phase 3. |
| **Windows VMs** | Incus runs them (ISO repacked with virtio drivers by `distrobuilder`), but without a VGA console in the browser they are unusable from the tab. After the above. | | **Windows VMs** | Incus runs them (ISO repacked with virtio drivers by `distrobuilder`), but without a VGA console in the browser they are unusable from the tab. After the above. |
| **GPU passthrough to a VM** | Takes the Arc A380 from the host, and from Jellyfin. No. Sharing the GPU into a *container* (`gpu` device) is fine and is how `r720-jellyfin` used to run. Phase 3. | | **GPU passthrough to a VM** | Removes the card from the host and from whatever uses it there (Jellyfin on `grid`). Offered only with that warning, later. Sharing a GPU into a *container* (`gpu` device) leaves the host its card and is how `r720-jellyfin` used to run. Phase 3. |
| **Clustering / live migration** | One real host; the others are different sizes and one is arm64; no shared storage. Not planned. | | **Clustering / live migration** | Incus clusters, but these machines differ in size and one is arm64, with no shared storage. Not planned. |
| **A fleet-wide instance list** | One machine per view. The switcher is the machine. | | **A fleet-wide instance list** | One machine per view. The switcher is the machine. |
| **Mac mini, `hamtv`, `gate`** | Above. | | **A second engine (bare LXC on OpenWrt, libvirt-only hosts)** | Would double the backend for one router that has no KVM. The tab reports Incus unavailable there and stops. |
## Decisions baked in (say if any is wrong) ## Layout decisions Ham Lab makes per machine (overridable in the Install dialog)
1. **Storage: instances on `grid`'s NVMe, data on `tank`.** A ZFS pool for Incus made as a 1. **Storage.** A pool on the machine's own disk, driver by the detection above, as a
300 GiB loop file on the NVMe root (`incus storage create default zfs size=300GiB`) — fast loop-backed image where the driver needs one (`incus storage create default zfs
enough for VM root disks where raidz2 spinners are not, and still ZFS so snapshots and size=…`) — sized from free space, shown and editable. Bulk data is **bind-mounted in**,
clones are instant. Big data is bind-mounted in from `tank`, the same split Docker already not stored in the pool: on `grid` that means `/tank/…` paths into instances, the same
has (apps on `core`, data on `grid`). A second pool on `tank/incus` can be added later for split Docker already has. A second pool on an existing dataset (`tank/incus`) is one
a guest that genuinely wants terabytes. **Consequence:** `grid`'s root NVMe is not backed extra command if a guest wants terabytes. The pool's path and driver are recorded in
up anywhere (backups land *on* `grid`), so instances need the nightly export below. Ham Lab's state so the page can say where the data is.
2. **Network: NAT bridge first, pinned to `192.168.200.1/24`.** Incus's default picks a 2. **Bridge subnet, computed.** Incus's `auto` picks a random `10.x.y.0/24`, and **this
random `10.x.y.0/24`, and **the LAN here is `10.0.0.0/8`** — every host has a /8 on its LAN is `10.0.0.0/8`** — every host has a /8 on its NIC, so any 10.x bridge collides.
NIC, so any 10.x bridge collides. `192.168.200.0/24` is clear of Docker's pools on both Ham Lab reads the machine's routes and picks the first free /24 from `192.168.0.0/16`,
hosts (`172.28.0.0/14` on `grid`; `172.19`–`172.29` and `192.168.80`–`143` on `core`). then `172.16.0.0/12`, that is neither on-link nor routed nor inside Docker's configured
Instances get `name.incus` DNS from Incus's dnsmasq. **LAN addresses in phase 2:** a pool (`172.28.0.0/14` on `grid`; `172.19`–`172.29` and `192.168.80`–`143` live on
`br0` on `grid`'s `enp10s0` (the 10 GbE uplink) so an instance can take a lease from `core`). Instances get `name.incus` DNS from Incus's dnsmasq.
`gate` and a Pi-hole name. That rewrites `/etc/network/interfaces` and drops `grid` off 3. **LAN addresses are a later, explicit step.** A `br0` on the machine's uplink lets an
the network for seconds — its own step, with the NanoKVM standing by. macvlan was instance take a lease from `gate` and a Pi-hole name, but it rewrites the host's network
considered and rejected: no host network change, but host and guests cannot talk, so config and drops it off the network for seconds — never part of Install, always its own
Immich/Jellyfin/backups on `grid` could never reach an instance. confirmed action (with the NanoKVM standing by on `grid`). macvlan was considered and
3. **Docker and Incus on one host:** Docker sets the `FORWARD` policy to `DROP`, which rejected: no host change, but host and guests cannot talk, so nothing on the host could
silently kills the Incus bridge's traffic. Fix is two `DOCKER-USER` accept rules for reach an instance.
`incusbr0`, persisted — part of the install recipe, on `grid` and later `core`. 4. **Docker and Incus on one host.** Docker sets the `FORWARD` policy to `DROP`, which
4. **ARC cap on `grid`:** `zfs_arc_max` from 40 GiB to 16 GiB so guest RAM is deterministic. silently kills the Incus bridge's traffic. Fix is two `DOCKER-USER` accept rules for the
5. **Privilege:** `core` joins `incus-admin` (socket access, the `docker`-group shape) and bridge, persisted — applied by Install wherever Docker is detected (`grid`, `core`, the
`hamlab.service` gets `SupplementaryGroups=incus-admin`; no sudo in the hot path. Mac, `hamtv` all run it).
6. **Backups:** nightly `incus export --instance-only` of every instance to 5. **Memory.** A VM's memory is a hard reservation; the create form shows the machine's
`/tank/backups/incus/` with a retention, as a cron line like the ZFS schedules free memory and warns past it, and never refuses. On a ZFS host Install offers to cap
(`hamlab incus-export`), from phase 2. Plus the instance's own snapshot schedule. the ARC (on `grid`, 40 GiB → 16 GiB) so guest RAM is deterministic; his choice.
6. **Privilege.** Ham Lab's user joins `incus-admin`; no sudo in the hot path. Install
itself goes through `priv` like the other installers.
7. **Backups.** Nightly `incus export --instance-only` of every instance to a per-machine
export directory with a retention, as a cron line like the ZFS schedules
(`hamlab incus-export`), from phase 2. Default is under Ham Lab's data directory (so on
`core` the nightly rsync already carries it); on `grid` he will point it at
`/tank/backups/incus/`, since `grid`'s own root disk is backed up nowhere.
## The old Proxmox estate (restorable, phase 3) ## The old Proxmox estate (importable, phase 3)
`/tank/backups/proxmox/` holds the retired `r720` node: `/etc/pve` (12 LXC confs, 2 VM `/tank/backups/proxmox/` holds the retired `r720` node: `/etc/pve` (12 LXC confs, 2 VM
confs), 118 GiB of `vzdump` archives, and three raw VM disks. Latest of each: confs), 118 GiB of `vzdump` archives, and three raw VM disks. Latest of each:
@@ -125,8 +178,9 @@ confs), 118 GiB of `vzdump` archives, and three raw VM disks. Latest of each:
- **LXC:** feasible and scriptable. Unpack the rootfs (`tar --zstd -xpf … --numeric-owner`), - **LXC:** feasible and scriptable. Unpack the rootfs (`tar --zstd -xpf … --numeric-owner`),
read `etc/vzdump/pct.conf` for arch/ostype, drop it, write `metadata.yaml`, pack, `incus read `etc/vzdump/pct.conf` for arch/ostype, drop it, write `metadata.yaml`, pack, `incus
image import`, launch. Unprivileged uid shifting is Incus's job after that. A wizard in the image import`, launch. Unprivileged uid shifting is Incus's job after that. An import
tab is plausible once the script works by hand. action in the tab (pick an archive on this machine) is plausible once the script works by
hand. x86-64 archives only run on x86-64 machines; the picker says so.
- **VMs:** the raw disks import directly (`incus-migrate`, or an empty VM with the disk - **VMs:** the raw disks import directly (`incus-migrate`, or an empty VM with the disk
imported into its root volume). The `.vma.zst` archives need Proxmox's `vma` extractor (or imported into its root volume). The `.vma.zst` archives need Proxmox's `vma` extractor (or
the Python port) to become raw disks first. Most of these are the same `powerpanel` box the Python port) to become raw disks first. Most of these are the same `powerpanel` box
@@ -137,17 +191,17 @@ confs), 118 GiB of `vzdump` archives, and three raw VM disks. Latest of each:
| Phase | What | Effort | | Phase | What | Effort |
|---|---|---| |---|---|---|
| **0 — host prep on `grid`** (by hand, documented) | `apt install incus`; `incus admin init --preseed` with the pool and bridge above; `DOCKER-USER` rules; ARC cap; `incus-admin` group + unit edit; launch one Debian 13 container and one Debian 13 VM; verify console, shell, network, snapshot, export. Write it up in `hamlet/docs/hosts.md` and the preseed into `hamlet/scripts/`. | 1 session | | **1 — detection, Install, the tab** | `internal/incus` (detection, CLI wrapper, JSON types, `incus monitor` for live state, the preseed builder and subnet picker), `internal/api/virt.go` (routes below), `Virtualization.tsx` (the three page states; list, detail, create dialog, snapshot panel), rail entry `virt`, terminal integration, sensors. **Tested by clicking Install on `grid` first, then `core`** — two different pool drivers, both with Docker present. Then the Mac for the arm64 path. `gate` for the unavailable state. | 2–3 sessions |
| **1 — the tab** | `internal/incus` (CLI wrapper, JSON types, `incus monitor` for live state), `internal/api/virt.go` (routes below), `Virtualization.tsx` (list, detail, create dialog, snapshot panel), rail entry `virt`, terminal integration, Install button, the no-KVM state. | 2 sessions | | **2 — operations** | Snapshot schedules UI, nightly exports with retention and the per-machine export directory, limits editor, bind mounts, the LAN-bridge step, instance alert default, cloud-init presets. | 1–2 sessions |
| **2 — operations** | Snapshot schedules UI, nightly exports with retention, limits editor, bind mounts, `br0` on `grid` and the LAN network option, instance sensors + alert default, cloud-init presets. | 1–2 sessions | | **3 — the rest** | Proxmox import, publish-as-image, GPU device for containers, VGA console (`spice-html5`), GPU passthrough to a VM with its warning. | as wanted |
| **3 — the rest** | Proxmox import wizard, publish-as-image, GPU device for containers, VGA console (`spice-html5`), `core` as a second (containers-only) host. | as wanted |
### API sketch ### API sketch
``` ```
GET /api/virt {installed, kvm, pools[], networks[], instances[]} GET /api/virt {state: ready|installable|unavailable, reason, kvm, arch,
POST /api/virt/install task: the phase-0 recipe pool_drivers[], pools[], networks[], instances[], free_memory}
GET /api/virt/images?remote=images&q= local cache + remote search POST /api/virt/install {driver, size, path, subnet, docker_rules, arc_max?} task
GET /api/virt/images?remote=images&q= local cache + remote search, filtered to this arch
DELETE /api/virt/images/{fingerprint} DELETE /api/virt/images/{fingerprint}
POST /api/virt/instances {name, type, image, cpu, memory, disk, network, cloudinit, profile, start} task POST /api/virt/instances {name, type, image, cpu, memory, disk, network, cloudinit, profile, start} task
GET /api/virt/instances/{name} config, state, devices, snapshots GET /api/virt/instances/{name} config, state, devices, snapshots