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)) }