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

@ -750,13 +750,14 @@ func newVMCommand() *cobra.Command {
}
func newVMRunCommand() *cobra.Command {
defaults := effectiveVMDefaults()
var (
name string
imageName string
vcpu = model.DefaultVCPUCount
memory = model.DefaultMemoryMiB
systemOverlaySize = model.FormatSizeBytes(model.DefaultSystemOverlaySize)
workDiskSize = model.FormatSizeBytes(model.DefaultWorkDiskSize)
vcpu = defaults.VCPUCount
memory = defaults.MemoryMiB
systemOverlaySize = model.FormatSizeBytes(defaults.SystemOverlaySizeByte)
workDiskSize = model.FormatSizeBytes(defaults.WorkDiskSizeBytes)
natEnabled bool
branchName string
fromRef = "HEAD"
@ -842,10 +843,10 @@ Three modes:
}
cmd.Flags().StringVar(&name, "name", "", "vm name")
cmd.Flags().StringVar(&imageName, "image", "", "image name or id (defaults to config's default_image_name; auto-pulled from imagecat if missing)")
cmd.Flags().IntVar(&vcpu, "vcpu", model.DefaultVCPUCount, "vcpu count")
cmd.Flags().IntVar(&memory, "memory", model.DefaultMemoryMiB, "memory in MiB")
cmd.Flags().StringVar(&systemOverlaySize, "system-overlay-size", model.FormatSizeBytes(model.DefaultSystemOverlaySize), "system overlay size")
cmd.Flags().StringVar(&workDiskSize, "disk-size", model.FormatSizeBytes(model.DefaultWorkDiskSize), "work disk size")
cmd.Flags().IntVar(&vcpu, "vcpu", defaults.VCPUCount, "vcpu count")
cmd.Flags().IntVar(&memory, "memory", defaults.MemoryMiB, "memory in MiB")
cmd.Flags().StringVar(&systemOverlaySize, "system-overlay-size", model.FormatSizeBytes(defaults.SystemOverlaySizeByte), "system overlay size")
cmd.Flags().StringVar(&workDiskSize, "disk-size", model.FormatSizeBytes(defaults.WorkDiskSizeBytes), "work disk size")
cmd.Flags().BoolVar(&natEnabled, "nat", false, "enable NAT")
cmd.Flags().StringVar(&branchName, "branch", "", "create and switch to a new guest branch")
cmd.Flags().StringVar(&fromRef, "from", "HEAD", "base ref for --branch")
@ -998,13 +999,14 @@ func promptYesNo(in io.Reader, out io.Writer, prompt string) (bool, error) {
}
func newVMCreateCommand() *cobra.Command {
defaults := effectiveVMDefaults()
var (
name string
imageName string
vcpu = model.DefaultVCPUCount
memory = model.DefaultMemoryMiB
systemOverlaySize = model.FormatSizeBytes(model.DefaultSystemOverlaySize)
workDiskSize = model.FormatSizeBytes(model.DefaultWorkDiskSize)
vcpu = defaults.VCPUCount
memory = defaults.MemoryMiB
systemOverlaySize = model.FormatSizeBytes(defaults.SystemOverlaySizeByte)
workDiskSize = model.FormatSizeBytes(defaults.WorkDiskSizeBytes)
natEnabled bool
noStart bool
)
@ -1033,10 +1035,10 @@ func newVMCreateCommand() *cobra.Command {
}
cmd.Flags().StringVar(&name, "name", "", "vm name")
cmd.Flags().StringVar(&imageName, "image", "", "image name or id (defaults to config's default_image_name; auto-pulled from imagecat if missing)")
cmd.Flags().IntVar(&vcpu, "vcpu", model.DefaultVCPUCount, "vcpu count")
cmd.Flags().IntVar(&memory, "memory", model.DefaultMemoryMiB, "memory in MiB")
cmd.Flags().StringVar(&systemOverlaySize, "system-overlay-size", model.FormatSizeBytes(model.DefaultSystemOverlaySize), "system overlay size")
cmd.Flags().StringVar(&workDiskSize, "disk-size", model.FormatSizeBytes(model.DefaultWorkDiskSize), "work disk size")
cmd.Flags().IntVar(&vcpu, "vcpu", defaults.VCPUCount, "vcpu count")
cmd.Flags().IntVar(&memory, "memory", defaults.MemoryMiB, "memory in MiB")
cmd.Flags().StringVar(&systemOverlaySize, "system-overlay-size", model.FormatSizeBytes(defaults.SystemOverlaySizeByte), "system overlay size")
cmd.Flags().StringVar(&workDiskSize, "disk-size", model.FormatSizeBytes(defaults.WorkDiskSizeBytes), "work disk size")
cmd.Flags().BoolVar(&natEnabled, "nat", false, "enable NAT")
cmd.Flags().BoolVar(&noStart, "no-start", false, "create without starting")
_ = cmd.RegisterFlagCompletionFunc("image", completeImageNames)
@ -2563,33 +2565,73 @@ func vmSetParamsFromFlags(idOrName string, vcpu, memory int, diskSize string, na
}
func vmCreateParamsFromFlags(cmd *cobra.Command, name, imageName string, vcpu, memory int, systemOverlaySize, workDiskSize string, natEnabled, noStart bool) (api.VMCreateParams, error) {
// The flag defaults are already resolved from config + host
// heuristics at command-build time, so we always forward the flag
// values to the daemon. This makes the CLI the single source of
// truth for effective defaults and lets the progress renderer show
// exactly what the VM will be sized at.
if err := validatePositiveSetting("vcpu", vcpu); err != nil {
return api.VMCreateParams{}, err
}
if err := validatePositiveSetting("memory", memory); err != nil {
return api.VMCreateParams{}, err
}
params := api.VMCreateParams{
Name: name,
ImageName: imageName,
NATEnabled: natEnabled,
NoStart: noStart,
}
if cmd.Flags().Changed("vcpu") {
if err := validatePositiveSetting("vcpu", vcpu); err != nil {
return api.VMCreateParams{}, err
}
params.VCPUCount = &vcpu
}
if cmd.Flags().Changed("memory") {
if err := validatePositiveSetting("memory", memory); err != nil {
return api.VMCreateParams{}, err
}
params.MemoryMiB = &memory
}
if cmd.Flags().Changed("system-overlay-size") {
params.SystemOverlaySize = systemOverlaySize
}
if cmd.Flags().Changed("disk-size") {
params.WorkDiskSize = workDiskSize
Name: name,
ImageName: imageName,
NATEnabled: natEnabled,
NoStart: noStart,
VCPUCount: &vcpu,
MemoryMiB: &memory,
SystemOverlaySize: systemOverlaySize,
WorkDiskSize: workDiskSize,
}
return params, nil
}
// effectiveVMDefaults resolves the default sizing applied to commands
// that accept --vcpu / --memory / --disk-size flags when the user
// doesn't set them. It combines config overrides (if any) with
// host-derived heuristics, falling back to baked-in constants.
//
// Called at command-build time, which runs before any RunE. It
// reads config.toml and /proc — any read error collapses to builtin
// constants so the CLI stays usable even on a misconfigured host.
func effectiveVMDefaults() model.VMDefaults {
var override model.VMDefaultsOverride
if layout, err := paths.Resolve(); err == nil {
if cfg, err := config.Load(layout); err == nil {
override = cfg.VMDefaults
}
}
host, err := system.ReadHostResources()
if err != nil {
return model.ResolveVMDefaults(override, 0, 0)
}
return model.ResolveVMDefaults(override, host.CPUCount, host.TotalMemoryBytes)
}
// printVMSpecLine writes a one-line sizing summary to out. Always
// emitted (even non-TTY) so logs and CI output carry the numbers.
func printVMSpecLine(out io.Writer, params api.VMCreateParams) {
vcpu := model.DefaultVCPUCount
if params.VCPUCount != nil {
vcpu = *params.VCPUCount
}
memory := model.DefaultMemoryMiB
if params.MemoryMiB != nil {
memory = *params.MemoryMiB
}
diskBytes := int64(model.DefaultWorkDiskSize)
if strings.TrimSpace(params.WorkDiskSize) != "" {
if parsed, err := model.ParseSize(params.WorkDiskSize); err == nil {
diskBytes = parsed
}
}
_, _ = fmt.Fprintf(out, "spec: %d vcpu · %d MiB · %s disk\n",
vcpu, memory, model.FormatSizeBytes(diskBytes))
}
func validatePositiveSetting(label string, value int) error {
if value <= 0 {
return fmt.Errorf("%s must be a positive integer", label)
@ -3479,6 +3521,7 @@ type anyWriter interface {
}
func runVMCreate(ctx context.Context, socketPath string, stderr io.Writer, params api.VMCreateParams) (model.VMRecord, error) {
printVMSpecLine(stderr, params)
begin, err := vmCreateBeginFunc(ctx, socketPath, params)
if err != nil {
return model.VMRecord{}, err