Remind users when a VM is still running after hanger vm ssh exits instead of silently dropping them back to the host shell.\n\nAttach a Firecracker vsock device to each VM, persist the host vsock path/CID,\nadd a new guest-side banger-vsock-pingd responder to the runtime bundle and both\nimage-build paths, and expose a vm.ping RPC that the CLI and TUI call after SSH\nreturns. Doctor and start/build preflight now validate the helper plus\n/dev/vhost-vsock so the feature fails early and clearly.\n\nValidated with go mod tidy, bash -n customize.sh, git diff --check, make build,\nand GOCACHE=/tmp/banger-gocache go test ./... outside the sandbox because the\ndaemon tests need real Unix/UDP sockets. Rebuild the image/rootfs used for new\nVMs so the guest ping service is present.
101 lines
3.2 KiB
Go
101 lines
3.2 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"banger/internal/config"
|
|
"banger/internal/model"
|
|
"banger/internal/paths"
|
|
"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(),
|
|
}
|
|
return d.doctorReport(ctx), nil
|
|
}
|
|
|
|
func (d *Daemon) doctorReport(ctx context.Context) system.Report {
|
|
report := system.Report{}
|
|
|
|
report.AddPreflight("runtime bundle", d.runtimeBundleChecks(), runtimeBundleStatus(d.config))
|
|
report.AddPreflight("core vm lifecycle", d.coreVMLifecycleChecks(), "required host tools available")
|
|
report.AddPreflight("vsock ssh reminder", d.vsockChecks(), "vsock reminder prerequisites available")
|
|
d.addCapabilityDoctorChecks(ctx, &report)
|
|
report.AddPreflight("image build", d.imageBuildChecks(ctx), "image build prerequisites available")
|
|
|
|
return report
|
|
}
|
|
|
|
func (d *Daemon) runtimeBundleChecks() *system.Preflight {
|
|
checks := system.NewPreflight()
|
|
hint := paths.RuntimeBundleHint()
|
|
checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", hint)
|
|
checks.RequireFile(d.config.SSHKeyPath, "ssh private key", `set "ssh_key_path" or refresh the runtime bundle`)
|
|
checks.RequireExecutable(d.config.VSockPingHelperPath, "vsock ping helper", `run 'make build' or refresh the runtime bundle`)
|
|
checks.RequireFile(d.config.DefaultRootfs, "default rootfs image", `set "default_rootfs" or refresh the runtime bundle`)
|
|
checks.RequireFile(d.config.DefaultKernel, "kernel image", `set "default_kernel" or refresh the runtime bundle`)
|
|
if strings.TrimSpace(d.config.DefaultInitrd) != "" {
|
|
checks.RequireFile(d.config.DefaultInitrd, "initrd image", `set "default_initrd" or refresh the runtime bundle`)
|
|
}
|
|
if strings.TrimSpace(d.config.DefaultPackagesFile) != "" {
|
|
checks.RequireFile(d.config.DefaultPackagesFile, "package manifest", `set "default_packages_file" or refresh the runtime bundle`)
|
|
}
|
|
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()
|
|
d.addImageBuildPrereqs(
|
|
ctx,
|
|
checks,
|
|
firstNonEmpty(d.config.DefaultBaseRootfs, d.config.DefaultRootfs),
|
|
d.config.DefaultKernel,
|
|
d.config.DefaultInitrd,
|
|
d.config.DefaultModulesDir,
|
|
"",
|
|
)
|
|
return checks
|
|
}
|
|
|
|
func (d *Daemon) vsockChecks() *system.Preflight {
|
|
checks := system.NewPreflight()
|
|
checks.RequireExecutable(d.config.VSockPingHelperPath, "vsock ping helper", `run 'make build' or refresh the runtime bundle`)
|
|
checks.RequireFile(vsockHostDevicePath, "vsock host device", "load the vhost_vsock kernel module on the host")
|
|
return checks
|
|
}
|
|
|
|
func runtimeBundleStatus(cfg model.DaemonConfig) string {
|
|
if strings.TrimSpace(cfg.RuntimeDir) == "" {
|
|
return "runtime dir not configured"
|
|
}
|
|
return fmt.Sprintf("runtime dir %s", cfg.RuntimeDir)
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|