banger/internal/model/vm_defaults.go
Thales Maciel 21b74639d8
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.
2026-04-19 13:06:51 -03:00

134 lines
4 KiB
Go

package model
import "fmt"
// VMDefaults captures the baseline sizing applied to a new VM when the
// user omits the corresponding --vcpu / --memory / --disk-size flags.
// Each field carries a Source tag explaining where the number came
// from so UI layers can surface provenance ("auto" vs "config" vs
// "built-in default").
type VMDefaults struct {
VCPUCount int
MemoryMiB int
WorkDiskSizeBytes int64
SystemOverlaySizeByte int64
// Source describes which layer won for each field, one of:
// "config" — user set it in config.toml
// "auto" — computed from host resources
// "builtin"— hardcoded fallback
VCPUSource string
MemorySource string
WorkDiskSource string
SystemOverlaySource string
}
// VMDefaultsOverride is the optional block users can place in
// config.toml's [vm_defaults]. Zero-value fields mean "not set, let
// banger decide."
type VMDefaultsOverride struct {
VCPUCount int
MemoryMiB int
WorkDiskSizeBytes int64
SystemOverlaySizeByte int64
}
// ResolveVMDefaults picks effective VM defaults from (in order) the
// user's config overrides, then host-derived heuristics, then baked-in
// constants. hostCPUs and hostMemoryBytes are what `system.ReadHost
// Resources` reports; 0 on either is treated as "unknown" and skipped,
// which pushes that field down to the builtin fallback.
func ResolveVMDefaults(override VMDefaultsOverride, hostCPUs int, hostMemoryBytes int64) VMDefaults {
d := VMDefaults{
VCPUCount: DefaultVCPUCount,
MemoryMiB: DefaultMemoryMiB,
WorkDiskSizeBytes: DefaultWorkDiskSize,
SystemOverlaySizeByte: DefaultSystemOverlaySize,
VCPUSource: "builtin",
MemorySource: "builtin",
WorkDiskSource: "builtin",
SystemOverlaySource: "builtin",
}
// vCPU: config > auto > builtin.
switch {
case override.VCPUCount > 0:
d.VCPUCount = override.VCPUCount
d.VCPUSource = "config"
case hostCPUs > 0:
d.VCPUCount = autoVCPU(hostCPUs)
d.VCPUSource = "auto"
}
// Memory MiB: config > auto > builtin.
switch {
case override.MemoryMiB > 0:
d.MemoryMiB = override.MemoryMiB
d.MemorySource = "config"
case hostMemoryBytes > 0:
d.MemoryMiB = autoMemoryMiB(hostMemoryBytes)
d.MemorySource = "auto"
}
// Work disk: config > builtin. Disk is a COW overlay — growing
// the allocation with host RAM gives nothing useful, so no auto.
if override.WorkDiskSizeBytes > 0 {
d.WorkDiskSizeBytes = override.WorkDiskSizeBytes
d.WorkDiskSource = "config"
}
// System overlay: config > builtin.
if override.SystemOverlaySizeByte > 0 {
d.SystemOverlaySizeByte = override.SystemOverlaySizeByte
d.SystemOverlaySource = "config"
}
return d
}
// autoVCPU clamps cpus/4 into [1, 4]. A 2-vcpu sandbox is the sweet
// spot for most dev loops; going higher rarely helps interactive use
// and starves the host of cores.
func autoVCPU(hostCPUs int) int {
candidate := hostCPUs / 4
if candidate < 1 {
candidate = 1
}
if candidate > 4 {
candidate = 4
}
return candidate
}
// autoMemoryMiB caps at host/8, floor 1 GiB, ceiling 8 GiB. 1/8 leaves
// plenty of headroom for the host even if several VMs run
// concurrently; 8 GiB is enough for most language toolchains without
// being hostile on 32 GiB laptops.
func autoMemoryMiB(hostMemoryBytes int64) int {
const (
mib = int64(1024 * 1024)
gib = 1024 * mib
floorMiB = 1024 // 1 GiB
cappedMiB = 8 * 1024 // 8 GiB
)
candidate := hostMemoryBytes / 8 / mib
if candidate < floorMiB {
candidate = floorMiB
}
if candidate > cappedMiB {
candidate = cappedMiB
}
// Round down to 256 MiB multiples for tidier output.
candidate -= candidate % 256
if candidate < floorMiB {
candidate = floorMiB
}
return int(candidate)
}
// FormatSpecLine renders a one-line summary of VM sizing suitable for
// progress output or doctor display.
func (d VMDefaults) FormatSpecLine() string {
return fmt.Sprintf("%d vcpu · %d MiB · %s disk",
d.VCPUCount, d.MemoryMiB, FormatSizeBytes(d.WorkDiskSizeBytes))
}