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.
130 lines
4.3 KiB
Go
130 lines
4.3 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"banger/internal/config"
|
|
"banger/internal/imagecat"
|
|
"banger/internal/model"
|
|
"banger/internal/paths"
|
|
"banger/internal/store"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
func Doctor(ctx context.Context) (system.Report, error) {
|
|
layout, err := paths.Resolve()
|
|
if err != nil {
|
|
return system.Report{}, err
|
|
}
|
|
cfg, err := config.Load(layout)
|
|
if err != nil {
|
|
return system.Report{}, err
|
|
}
|
|
d := &Daemon{
|
|
layout: layout,
|
|
config: cfg,
|
|
runner: system.NewRunner(),
|
|
}
|
|
db, err := store.Open(layout.DBPath)
|
|
if err == nil {
|
|
defer db.Close()
|
|
d.store = db
|
|
}
|
|
return d.doctorReport(ctx), nil
|
|
}
|
|
|
|
func (d *Daemon) doctorReport(ctx context.Context) system.Report {
|
|
report := system.Report{}
|
|
|
|
report.AddPreflight("host runtime", d.runtimeChecks(), runtimeStatus(d.config))
|
|
report.AddPreflight("core vm lifecycle", d.coreVMLifecycleChecks(), "required host tools available")
|
|
report.AddPreflight("vsock guest agent", d.vsockChecks(), "vsock guest agent prerequisites available")
|
|
d.addVMDefaultsCheck(&report)
|
|
d.addCapabilityDoctorChecks(ctx, &report)
|
|
|
|
return report
|
|
}
|
|
|
|
// addVMDefaultsCheck surfaces the effective VM sizing that `vm run` /
|
|
// `vm create` will apply when the user omits the flags. Shown as a
|
|
// PASS check so it always renders, with per-field provenance
|
|
// (config|auto|builtin) so users can tell what's driving each number.
|
|
func (d *Daemon) addVMDefaultsCheck(report *system.Report) {
|
|
host, err := system.ReadHostResources()
|
|
var cpus int
|
|
var memBytes int64
|
|
if err == nil {
|
|
cpus = host.CPUCount
|
|
memBytes = host.TotalMemoryBytes
|
|
}
|
|
defaults := model.ResolveVMDefaults(d.config.VMDefaults, cpus, memBytes)
|
|
details := []string{
|
|
fmt.Sprintf("vcpu: %d (%s)", defaults.VCPUCount, defaults.VCPUSource),
|
|
fmt.Sprintf("memory: %d MiB (%s)", defaults.MemoryMiB, defaults.MemorySource),
|
|
fmt.Sprintf("disk: %s (%s)", model.FormatSizeBytes(defaults.WorkDiskSizeBytes), defaults.WorkDiskSource),
|
|
"override any of these in ~/.config/banger/config.toml under [vm_defaults]",
|
|
}
|
|
report.AddPass("vm defaults", details...)
|
|
}
|
|
|
|
func (d *Daemon) runtimeChecks() *system.Preflight {
|
|
checks := system.NewPreflight()
|
|
checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", `install firecracker or set "firecracker_bin"`)
|
|
checks.RequireFile(d.config.SSHKeyPath, "ssh private key", `set "ssh_key_path" or let banger create its default key`)
|
|
if helper, err := d.vsockAgentBinary(); err == nil {
|
|
checks.RequireExecutable(helper, "vsock agent helper", `run 'make build' or reinstall banger`)
|
|
} else {
|
|
checks.Addf("%v", err)
|
|
}
|
|
if d.store != nil && strings.TrimSpace(d.config.DefaultImageName) != "" {
|
|
name := d.config.DefaultImageName
|
|
image, err := d.store.GetImageByName(context.Background(), name)
|
|
if err == nil {
|
|
checks.RequireFile(image.RootfsPath, "default image rootfs", `re-register or rebuild the default image`)
|
|
checks.RequireFile(image.KernelPath, "default image kernel", `re-register or rebuild the default image`)
|
|
if strings.TrimSpace(image.InitrdPath) != "" {
|
|
checks.RequireFile(image.InitrdPath, "default image initrd", `re-register or rebuild the default image`)
|
|
}
|
|
} else if !defaultImageInCatalog(name) {
|
|
checks.Addf("default image %q is not registered and not in the imagecat catalog", name)
|
|
}
|
|
// If the default image isn't local but is cataloged, vm create
|
|
// will auto-pull it on first use — no error to surface.
|
|
}
|
|
return checks
|
|
}
|
|
|
|
func defaultImageInCatalog(name string) bool {
|
|
catalog, err := imagecat.LoadEmbedded()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
_, err = catalog.Lookup(name)
|
|
return err == nil
|
|
}
|
|
|
|
func (d *Daemon) coreVMLifecycleChecks() *system.Preflight {
|
|
checks := system.NewPreflight()
|
|
d.addBaseStartCommandPrereqs(checks)
|
|
return checks
|
|
}
|
|
|
|
func (d *Daemon) vsockChecks() *system.Preflight {
|
|
checks := system.NewPreflight()
|
|
if helper, err := d.vsockAgentBinary(); err == nil {
|
|
checks.RequireExecutable(helper, "vsock agent helper", `run 'make build' or reinstall banger`)
|
|
} else {
|
|
checks.Addf("%v", err)
|
|
}
|
|
checks.RequireFile(vsockHostDevicePath, "vsock host device", "load the vhost_vsock kernel module on the host")
|
|
return checks
|
|
}
|
|
|
|
func runtimeStatus(cfg model.DaemonConfig) string {
|
|
if strings.TrimSpace(cfg.FirecrackerBin) == "" {
|
|
return "firecracker not configured"
|
|
}
|
|
return "firecracker and ssh key resolved"
|
|
}
|