package daemon import ( "context" "database/sql" "strings" "banger/internal/config" "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.addCapabilityDoctorChecks(ctx, &report) report.AddPreflight("image build", d.imageBuildChecks(ctx), "image build prerequisites available") return report } 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) != "" { image, err := d.store.GetImageByName(context.Background(), d.config.DefaultImageName) switch { case 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`) } case err != nil && err != sql.ErrNoRows: checks.Addf("failed to inspect default image %q: %v", d.config.DefaultImageName, err) default: checks.Addf("default image %q is not registered", d.config.DefaultImageName) } } return checks } func (d *Daemon) coreVMLifecycleChecks() *system.Preflight { checks := system.NewPreflight() d.addBaseStartCommandPrereqs(checks) return checks } func (d *Daemon) imageBuildChecks(ctx context.Context) *system.Preflight { checks := system.NewPreflight() if d.store == nil || strings.TrimSpace(d.config.DefaultImageName) == "" { checks.Addf("default image is not available for build inheritance") return checks } image, err := d.store.GetImageByName(ctx, d.config.DefaultImageName) if err != nil { checks.Addf("default image %q is not registered", d.config.DefaultImageName) return checks } d.addImageBuildPrereqs(ctx, checks, image.RootfsPath, image.KernelPath, image.InitrdPath, image.ModulesDir, "") 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" }