Add Go daemon-driven VM control plane

Replace the shell-only user workflow with `banger` and `bangerd`: Cobra commands, XDG/SQLite-backed state, managed VM and image lifecycle, and a Bubble Tea TUI for browsing and operating VMs.\n\nKeep Firecracker orchestration behind the daemon so VM specs become persistent objects, and add repo entrypoints for building, installing, and documenting the new flow while still delegating rootfs customization to the existing shell tooling.\n\nHarden the control plane around real usage by reclaiming Firecracker API sockets for the user, restarting stale daemons after rebuilds, and returning the correct `vm.create` payload so the CLI and TUI creation flow work reliably.\n\nValidation: `go test ./...`, `make build`, and a host-side smoke test with `./banger vm create --name codex-smoke`.
This commit is contained in:
Thales Maciel 2026-03-16 12:52:54 -03:00
parent 3cf33d1e0a
commit ea72ea26fe
No known key found for this signature in database
GPG key ID: 33112E6833C34679
22 changed files with 5480 additions and 0 deletions

125
internal/config/config.go Normal file
View file

@ -0,0 +1,125 @@
package config
import (
"os"
"path/filepath"
"time"
toml "github.com/pelletier/go-toml"
"banger/internal/model"
"banger/internal/paths"
)
type fileConfig struct {
RepoRoot string `toml:"repo_root"`
DefaultImageName string `toml:"default_image_name"`
DefaultBaseRootfs string `toml:"default_base_rootfs"`
DefaultKernel string `toml:"default_kernel"`
DefaultInitrd string `toml:"default_initrd"`
DefaultModulesDir string `toml:"default_modules_dir"`
DefaultPackages string `toml:"default_packages_file"`
AutoStopStaleAfter string `toml:"auto_stop_stale_after"`
StatsPollInterval string `toml:"stats_poll_interval"`
MetricsPoll string `toml:"metrics_poll_interval"`
BridgeName string `toml:"bridge_name"`
BridgeIP string `toml:"bridge_ip"`
CIDR string `toml:"cidr"`
DefaultDNS string `toml:"default_dns"`
}
func Load(layout paths.Layout) (model.DaemonConfig, error) {
cfg := model.DaemonConfig{
RepoRoot: paths.DetectRepoRoot(),
AutoStopStaleAfter: 0,
StatsPollInterval: model.DefaultStatsPollInterval,
MetricsPollInterval: model.DefaultMetricsPollInterval,
BridgeName: model.DefaultBridgeName,
BridgeIP: model.DefaultBridgeIP,
CIDR: model.DefaultCIDR,
DefaultDNS: model.DefaultDNS,
DefaultImageName: "default",
}
if cfg.RepoRoot != "" {
cfg.DefaultBaseRootfs = filepath.Join(cfg.RepoRoot, "rootfs.ext4")
cfg.DefaultKernel = filepath.Join(cfg.RepoRoot, "wtf/root/boot/vmlinux-6.8.0-94-generic")
cfg.DefaultInitrd = filepath.Join(cfg.RepoRoot, "wtf/root/boot/initrd.img-6.8.0-94-generic")
cfg.DefaultModulesDir = filepath.Join(cfg.RepoRoot, "wtf/root/lib/modules/6.8.0-94-generic")
cfg.DefaultPackagesFile = filepath.Join(cfg.RepoRoot, "packages.apt")
}
path := filepath.Join(layout.ConfigDir, "config.toml")
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return cfg, nil
}
return cfg, err
}
if info.IsDir() {
return cfg, nil
}
data, err := os.ReadFile(path)
if err != nil {
return cfg, err
}
var file fileConfig
if err := toml.Unmarshal(data, &file); err != nil {
return cfg, err
}
if file.RepoRoot != "" {
cfg.RepoRoot = file.RepoRoot
}
if file.DefaultImageName != "" {
cfg.DefaultImageName = file.DefaultImageName
}
if file.DefaultBaseRootfs != "" {
cfg.DefaultBaseRootfs = file.DefaultBaseRootfs
}
if file.DefaultKernel != "" {
cfg.DefaultKernel = file.DefaultKernel
}
if file.DefaultInitrd != "" {
cfg.DefaultInitrd = file.DefaultInitrd
}
if file.DefaultModulesDir != "" {
cfg.DefaultModulesDir = file.DefaultModulesDir
}
if file.DefaultPackages != "" {
cfg.DefaultPackagesFile = file.DefaultPackages
}
if file.BridgeName != "" {
cfg.BridgeName = file.BridgeName
}
if file.BridgeIP != "" {
cfg.BridgeIP = file.BridgeIP
}
if file.CIDR != "" {
cfg.CIDR = file.CIDR
}
if file.DefaultDNS != "" {
cfg.DefaultDNS = file.DefaultDNS
}
if file.AutoStopStaleAfter != "" {
duration, err := time.ParseDuration(file.AutoStopStaleAfter)
if err != nil {
return cfg, err
}
cfg.AutoStopStaleAfter = duration
}
if file.StatsPollInterval != "" {
duration, err := time.ParseDuration(file.StatsPollInterval)
if err != nil {
return cfg, err
}
cfg.StatsPollInterval = duration
}
if file.MetricsPoll != "" {
duration, err := time.ParseDuration(file.MetricsPoll)
if err != nil {
return cfg, err
}
cfg.MetricsPollInterval = duration
}
return cfg, nil
}