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:
co-authored by
Claude Fable 5.1
parent
a9b4d7969e
commit
ae72e45bf5
+66
-6
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user