Virtualization: a disk-location picker on both create forms, locations by place, add or remove a location

Pools are shown by where their bytes land (plain files on this disk, a ZFS pool inside one
file, a dataset…) rather than by driver name, which read as the NAS pool. A location can be
added as a plain-files pool at any folder on the machine, and removed while nothing uses it.
The driver order at Install is as it was; the labels say what each one really is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-10-11 14:33:10 -07:00
co-authored by Claude Fable 5.1
parent a9b4d7969e
commit ae72e45bf5
8 changed files with 191 additions and 27 deletions
+66 -6
View File
@@ -28,7 +28,7 @@ import (
)
const (
PoolName = "default"
PoolName = "local" // plain files on the machine's own disk, in Ham Lab's folder
BridgeName = "incusbr0"
ZPoolName = "hamlab-virt" // the ZFS pool made on the backing file; Incus is handed it by name
AdminGroup = "incus-admin"
@@ -615,11 +615,38 @@ type Pool struct {
Name string `json:"name"`
Driver string `json:"driver"`
Source string `json:"source"`
Where string `json:"where"` // in words: what disk this is, so a pool is picked by place, not by driver
Used uint64 `json:"used"`
Total uint64 `json:"total"`
UsedBy int `json:"used_by"`
}
// poolWhere describes a pool by where its bytes land.
func (m *Manager) poolWhere(driver, source string) string {
inHam := strings.HasPrefix(source, m.dir+"/") || source == m.dir
switch driver {
case "dir":
if inHam {
return "this machine's own disk — plain files, each disk sized on its own"
}
return "plain files at " + source
case "zfs":
if strings.HasSuffix(source, ".img") || source == ZPoolName {
return "a ZFS pool inside one file on this machine's disk"
}
if strings.Contains(source, "/") {
return "ZFS dataset " + source
}
return "ZFS pool " + source
case "btrfs":
if inHam {
return "btrfs inside one file on this machine's disk"
}
return "btrfs at " + source
}
return driver + " at " + source
}
type Network struct {
Name string `json:"name"`
Type string `json:"type"`
@@ -647,7 +674,7 @@ func (m *Manager) Pools(ctx context.Context) []Pool {
}
var out2 []Pool
for _, p := range raw {
pool := Pool{Name: p.Name, Driver: p.Driver, Source: p.Config["source"], UsedBy: len(p.UsedBy)}
pool := Pool{Name: p.Name, Driver: p.Driver, Source: p.Config["source"], Where: m.poolWhere(p.Driver, p.Config["source"]), UsedBy: len(p.UsedBy)}
if b, err := m.run(c, "query", "/1.0/storage-pools/"+p.Name+"/resources"); err == nil {
var res struct {
Space struct {
@@ -664,6 +691,24 @@ func (m *Manager) Pools(ctx context.Context) []Pool {
return out2
}
// DefaultPool is where a new instance's disk goes unless the form says otherwise: the pool
// on the default profile's root device.
func (m *Manager) DefaultPool(ctx context.Context) string {
c, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
out, err := m.run(c, "query", "/1.0/profiles/default")
if err != nil {
return ""
}
var p struct {
Devices map[string]map[string]string `json:"devices"`
}
if json.Unmarshal(out, &p) != nil {
return ""
}
return p.Devices["root"]["pool"]
}
func (m *Manager) Networks(ctx context.Context) []Network {
c, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
@@ -869,6 +914,7 @@ type CreateSpec struct {
CPU string `json:"cpu"`
Memory string `json:"memory"`
Disk string `json:"disk"`
Pool string `json:"pool"` // where the root disk goes; blank is the default profile's
Network string `json:"network"`
CloudInit string `json:"cloud_init"`
Profiles []string `json:"profiles"`
@@ -891,6 +937,8 @@ func CreateArgs(sp CreateSpec, kvm bool) ([]string, error) {
return nil, errors.New("memory: a size, like 2GiB")
case !ValidSize(sp.Disk):
return nil, errors.New("disk: a size, like 20GiB")
case sp.Pool != "" && !ValidName(sp.Pool):
return nil, errors.New("pool: a storage pool's name")
case !ValidNetwork(sp.Network):
return nil, errors.New("network: a bridge name")
case len(sp.CloudInit) > 256<<10:
@@ -912,8 +960,16 @@ func CreateArgs(sp CreateSpec, kvm bool) ([]string, error) {
if sp.Memory != "" {
args = append(args, "-c", "limits.memory="+sp.Memory)
}
// the root disk: the profile's, with the pool and size overridden where given
var root []string
if sp.Pool != "" {
root = append(root, "pool="+sp.Pool)
}
if sp.Disk != "" {
args = append(args, "-d", "root,size="+sp.Disk)
root = append(root, "size="+sp.Disk)
}
if len(root) > 0 {
args = append(args, "-d", "root,"+strings.Join(root, ","))
}
if sp.Network != "" {
args = append(args, "-n", sp.Network)
@@ -1104,6 +1160,7 @@ type FromISOSpec struct {
CPU string `json:"cpu"`
Memory string `json:"memory"`
Disk string `json:"disk"`
Pool string `json:"pool"`
Network string `json:"network"`
SecureBoot bool `json:"secureboot"`
Start bool `json:"start"`
@@ -1121,6 +1178,8 @@ func FromISOScript(sp FromISOSpec, kvm bool) (script string, params []string, er
return "", nil, errors.New("iso: pick one from the library")
case !ValidCPU(sp.CPU) || !ValidSize(sp.Memory) || !ValidSize(sp.Disk) || !ValidNetwork(sp.Network):
return "", nil, errors.New("cpu, memory, disk: a count and two sizes, like 2, 2GiB, 20GiB")
case sp.Pool != "" && !ValidName(sp.Pool):
return "", nil, errors.New("pool: a storage pool's name")
}
cpu, mem, disk := sp.CPU, sp.Memory, sp.Disk
if cpu == "" {
@@ -1140,13 +1199,14 @@ func FromISOScript(sp FromISOSpec, kvm bool) (script string, params []string, er
if sp.Start {
start = "1"
}
// $1 name $2 iso path $3 cpu $4 memory $5 disk $6 network $7 secureboot $8 start
// $1 name $2 iso path $3 cpu $4 memory $5 disk $6 network $7 secureboot $8 start $9 pool
script = `set -e
NET=""; [ -n "$6" ] && NET="-n $6"
incus init --empty --vm "$1" -c limits.cpu="$3" -c limits.memory="$4" -c security.secureboot="$7" -d root,size="$5" $NET
ROOT="root,size=$5"; [ -n "$9" ] && ROOT="root,pool=$9,size=$5"
incus init --empty --vm "$1" -c limits.cpu="$3" -c limits.memory="$4" -c security.secureboot="$7" -d "$ROOT" $NET
incus config device add "$1" iso0 disk source="$2" boot.priority=10
echo "made $1 with $2 as its first boot device"
[ "$8" = 1 ] && incus start "$1" && echo "started; open the VGA console to run the installer"
exit 0`
return script, []string{sp.Name, sp.ISO, cpu, mem, disk, sp.Network, sb, start}, nil
return script, []string{sp.Name, sp.ISO, cpu, mem, disk, sp.Network, sb, start, sp.Pool}, nil
}