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
File diff suppressed because it is too large Load Diff
+130
View File
@@ -0,0 +1,130 @@
package incus
// The bridge subnet. Incus's own `auto` picks a random 10.x.y.0/24, and on a LAN that is
// itself 10.0.0.0/8 (this one) every such pick is on-link and collides. So Ham Lab chooses:
// the first /24 that nothing on the machine already routes, is not an address it holds, and
// is outside Docker's configured pools — tried in an order that also stays clear of Docker's
// *defaults* (172.17–172.31 and 192.168.0.0/16 carved into /20s), which a Docker that has no
// daemon.json will hand out later without asking anyone.
import (
"context"
"encoding/json"
"fmt"
"net/netip"
"os"
"os/exec"
"strings"
"time"
)
// candidates in order of preference: 172.16.0.0/16 is the one RFC 1918 block Docker never
// takes by default; then the top of 192.168/16, which Docker reaches last; then the rest.
func candidates() []netip.Prefix {
var out []netip.Prefix
for i := 100; i <= 254; i++ {
out = append(out, netip.MustParsePrefix(fmt.Sprintf("172.16.%d.0/24", i)))
}
for i := 254; i >= 200; i-- {
out = append(out, netip.MustParsePrefix(fmt.Sprintf("192.168.%d.0/24", i)))
}
for i := 1; i < 100; i++ {
out = append(out, netip.MustParsePrefix(fmt.Sprintf("172.16.%d.0/24", i)))
}
return out
}
func overlaps(a, b netip.Prefix) bool {
return a.Contains(b.Addr()) || b.Contains(a.Addr())
}
// PickSubnet returns the first candidate /24 clear of every prefix in taken.
func PickSubnet(taken []netip.Prefix) netip.Prefix {
for _, c := range candidates() {
clear := true
for _, t := range taken {
if overlaps(c, t) {
clear = false
break
}
}
if clear {
return c
}
}
return netip.MustParsePrefix("172.16.100.0/24") // every candidate taken: not a machine this will run on
}
// Gateway is the bridge's own address in a subnet: its first host, as CIDR, the way
// Incus's ipv4.address wants it.
func Gateway(p netip.Prefix) string {
return fmt.Sprintf("%s/%d", p.Masked().Addr().Next(), p.Bits())
}
// TakenPrefixes reads what the machine already uses: its routes, its addresses, and the
// pools Docker is configured to carve networks from.
func TakenPrefixes(ctx context.Context) []netip.Prefix {
var out []netip.Prefix
add := func(s string) {
if p, err := netip.ParsePrefix(s); err == nil && p.Addr().Is4() {
out = append(out, p.Masked())
} else if a, err := netip.ParseAddr(s); err == nil && a.Is4() {
out = append(out, netip.PrefixFrom(a, 32))
}
}
c, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if b, err := exec.CommandContext(c, "ip", "-j", "-4", "route", "show").Output(); err == nil {
var routes []struct{ Dst string }
if json.Unmarshal(b, &routes) == nil {
for _, r := range routes {
if r.Dst != "" && r.Dst != "default" {
add(r.Dst)
}
}
}
}
if b, err := exec.CommandContext(c, "ip", "-j", "-4", "addr", "show").Output(); err == nil {
var links []struct {
AddrInfo []struct {
Local string `json:"local"`
Prefixlen int `json:"prefixlen"`
} `json:"addr_info"`
}
if json.Unmarshal(b, &links) == nil {
for _, l := range links {
for _, a := range l.AddrInfo {
add(fmt.Sprintf("%s/%d", a.Local, a.Prefixlen))
}
}
}
}
for _, p := range dockerPools() {
add(p)
}
return out
}
// dockerPools reads default-address-pools from Docker's daemon.json, and falls back to
// Docker's built-in defaults when the file names none.
func dockerPools() []string {
var cfg struct {
Pools []struct {
Base string `json:"base"`
} `json:"default-address-pools"`
}
if b, err := os.ReadFile("/etc/docker/daemon.json"); err == nil && json.Unmarshal(b, &cfg) == nil && len(cfg.Pools) > 0 {
var out []string
for _, p := range cfg.Pools {
out = append(out, p.Base)
}
return out
}
if _, err := exec.LookPath("docker"); err != nil {
return nil
}
return []string{"172.17.0.0/12", "192.168.0.0/16"}
}
// String form used in the layout file and on the page.
func prefixString(p netip.Prefix) string { return strings.TrimSpace(p.Masked().String()) }
+41
View File
@@ -0,0 +1,41 @@
package incus
import (
"net/netip"
"testing"
)
func TestPickSubnetAvoidsTaken(t *testing.T) {
taken := []netip.Prefix{
netip.MustParsePrefix("10.0.0.0/8"), // the LAN, a /8
netip.MustParsePrefix("172.16.100.0/24"), // first choice gone
netip.MustParsePrefix("172.16.101.0/24"), // and the second
netip.MustParsePrefix("172.28.0.0/14"), // Docker's configured pool
netip.MustParsePrefix("192.168.80.0/20"), // a Docker network
}
got := PickSubnet(taken)
if got.String() != "172.16.102.0/24" {
t.Fatalf("got %s", got)
}
if Gateway(got) != "172.16.102.1/24" {
t.Fatalf("gateway %s", Gateway(got))
}
}
func TestPickSubnetWholeBlockTaken(t *testing.T) {
// 172.16.0.0/12 covers every 172.16.x candidate; the picker must move to 192.168
got := PickSubnet([]netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")})
if got.String() != "192.168.254.0/24" {
t.Fatalf("got %s", got)
}
}
func TestOverlapsEitherWay(t *testing.T) {
a := netip.MustParsePrefix("172.16.5.0/24")
if !overlaps(a, netip.MustParsePrefix("172.16.0.0/12")) || !overlaps(netip.MustParsePrefix("172.16.0.0/12"), a) {
t.Fatal("a /24 inside a /12 overlaps both ways")
}
if overlaps(a, netip.MustParsePrefix("172.16.6.0/24")) {
t.Fatal("neighbours do not overlap")
}
}