vm defaults: host-aware sizing + spec line on spawn + doctor check
Replaces the static model.Default* constants that drove --vcpu / --memory / --disk-size with a three-layer resolver: 1. [vm_defaults] in ~/.config/banger/config.toml (if set) 2. host-derived heuristics (cpus/4 capped at 4; ram/8 capped at 8 GiB) 3. baked-in constants (floor) Visibility: - Every `vm run` / `vm create` prints a `spec:` line before progress begins: `spec: 4 vcpu · 8192 MiB · 8G disk`. Matches the VM that actually gets created because the CLI is now the single source of truth — it resolves, populates the flag defaults, and forwards the explicit values to the daemon. - `banger doctor` adds a "vm defaults" check showing per-field provenance (config|auto|builtin) and the config file path for overrides. - `--help` shows the resolved defaults (e.g. `--vcpu int (default 4)` on an 8-core host). No `banger config init` command, no first-run side effects, no writes to the user's filesystem behind their back. Users who want explicit control set the keys; everyone else gets sensible numbers that track their hardware.
This commit is contained in:
parent
78ff482bfa
commit
21b74639d8
10 changed files with 594 additions and 56 deletions
|
|
@ -34,6 +34,7 @@ type fileConfig struct {
|
|||
TapPoolSize int `toml:"tap_pool_size"`
|
||||
DefaultDNS string `toml:"default_dns"`
|
||||
FileSync []fileSyncEntryFile `toml:"file_sync"`
|
||||
VMDefaults *vmDefaultsFile `toml:"vm_defaults"`
|
||||
}
|
||||
|
||||
type fileSyncEntryFile struct {
|
||||
|
|
@ -42,6 +43,16 @@ type fileSyncEntryFile struct {
|
|||
Mode string `toml:"mode"`
|
||||
}
|
||||
|
||||
// vmDefaultsFile mirrors the optional `[vm_defaults]` block. All
|
||||
// fields are zero-valued when omitted; the resolver treats zero as
|
||||
// "not set, compute from host or fall back to builtin constants."
|
||||
type vmDefaultsFile struct {
|
||||
VCPUCount int `toml:"vcpu"`
|
||||
MemoryMiB int `toml:"memory_mib"`
|
||||
DiskSize string `toml:"disk_size"`
|
||||
SystemOverlaySize string `toml:"system_overlay_size"`
|
||||
}
|
||||
|
||||
func Load(layout paths.Layout) (model.DaemonConfig, error) {
|
||||
cfg := model.DaemonConfig{
|
||||
LogLevel: "info",
|
||||
|
|
@ -140,9 +151,48 @@ func Load(layout paths.Layout) (model.DaemonConfig, error) {
|
|||
}
|
||||
cfg.FileSync = append(cfg.FileSync, validated)
|
||||
}
|
||||
|
||||
if file.VMDefaults != nil {
|
||||
override, err := parseVMDefaults(*file.VMDefaults)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("vm_defaults: %w", err)
|
||||
}
|
||||
cfg.VMDefaults = override
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// parseVMDefaults validates and translates the TOML block into the
|
||||
// model-level override struct. Negative values are rejected outright;
|
||||
// zero means "not set."
|
||||
func parseVMDefaults(file vmDefaultsFile) (model.VMDefaultsOverride, error) {
|
||||
override := model.VMDefaultsOverride{
|
||||
VCPUCount: file.VCPUCount,
|
||||
MemoryMiB: file.MemoryMiB,
|
||||
}
|
||||
if override.VCPUCount < 0 {
|
||||
return model.VMDefaultsOverride{}, fmt.Errorf("vcpu must be >= 0 (got %d)", override.VCPUCount)
|
||||
}
|
||||
if override.MemoryMiB < 0 {
|
||||
return model.VMDefaultsOverride{}, fmt.Errorf("memory_mib must be >= 0 (got %d)", override.MemoryMiB)
|
||||
}
|
||||
if value := strings.TrimSpace(file.DiskSize); value != "" {
|
||||
bytes, err := model.ParseSize(value)
|
||||
if err != nil {
|
||||
return model.VMDefaultsOverride{}, fmt.Errorf("disk_size: %w", err)
|
||||
}
|
||||
override.WorkDiskSizeBytes = bytes
|
||||
}
|
||||
if value := strings.TrimSpace(file.SystemOverlaySize); value != "" {
|
||||
bytes, err := model.ParseSize(value)
|
||||
if err != nil {
|
||||
return model.VMDefaultsOverride{}, fmt.Errorf("system_overlay_size: %w", err)
|
||||
}
|
||||
override.SystemOverlaySizeByte = bytes
|
||||
}
|
||||
return override, nil
|
||||
}
|
||||
|
||||
// validateFileSyncEntry normalises a single `[[file_sync]]` entry
|
||||
// and rejects anything the operator would regret later: empty
|
||||
// paths, unsupported leading characters, path traversal, or
|
||||
|
|
|
|||
|
|
@ -205,3 +205,65 @@ func TestLoadRejectsInvalidFileSyncEntries(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAcceptsVMDefaults(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
data := []byte(`
|
||||
[vm_defaults]
|
||||
vcpu = 4
|
||||
memory_mib = 4096
|
||||
disk_size = "16G"
|
||||
system_overlay_size = "12G"
|
||||
`)
|
||||
if err := os.WriteFile(filepath.Join(configDir, "config.toml"), data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := Load(paths.Layout{ConfigDir: configDir})
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.VMDefaults.VCPUCount != 4 {
|
||||
t.Errorf("VCPUCount = %d, want 4", cfg.VMDefaults.VCPUCount)
|
||||
}
|
||||
if cfg.VMDefaults.MemoryMiB != 4096 {
|
||||
t.Errorf("MemoryMiB = %d, want 4096", cfg.VMDefaults.MemoryMiB)
|
||||
}
|
||||
if cfg.VMDefaults.WorkDiskSizeBytes != 16*1024*1024*1024 {
|
||||
t.Errorf("WorkDiskSizeBytes = %d, want 16 GiB", cfg.VMDefaults.WorkDiskSizeBytes)
|
||||
}
|
||||
if cfg.VMDefaults.SystemOverlaySizeByte != 12*1024*1024*1024 {
|
||||
t.Errorf("SystemOverlaySizeByte = %d, want 12 GiB", cfg.VMDefaults.SystemOverlaySizeByte)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEmptyVMDefaultsLeavesZeros(t *testing.T) {
|
||||
// No [vm_defaults] block → cfg.VMDefaults is the zero value,
|
||||
// which the resolver will map to auto or builtin.
|
||||
cfg, err := Load(paths.Layout{ConfigDir: t.TempDir()})
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.VMDefaults.VCPUCount != 0 || cfg.VMDefaults.MemoryMiB != 0 {
|
||||
t.Errorf("VMDefaults = %+v, want zeroed", cfg.VMDefaults)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsNegativeVMDefaults(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"vcpu": `[vm_defaults]` + "\n" + `vcpu = -1`,
|
||||
"memory": `[vm_defaults]` + "\n" + `memory_mib = -1`,
|
||||
"disk_size": `[vm_defaults]` + "\n" + `disk_size = "banana"`,
|
||||
"overlay": `[vm_defaults]` + "\n" + `system_overlay_size = "banana"`,
|
||||
}
|
||||
for name, body := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(configDir, "config.toml"), []byte(body+"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Load(paths.Layout{ConfigDir: configDir}); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue