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:
Thales Maciel 2026-04-19 13:06:51 -03:00
parent 78ff482bfa
commit 21b74639d8
No known key found for this signature in database
GPG key ID: 33112E6833C34679
10 changed files with 594 additions and 56 deletions

View file

@ -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