Add experimental Void guest workflow and vsock agent

Make iterating on a Firecracker-friendly Void guest practical without replacing the Debian default image path.

Add local Void rootfs build/register/verify plumbing, a language-agnostic dev package baseline, and guest SSH/work-disk hardening so new images use the runtime bundle key, keep a normal root bash environment, and repair stale nested /root layouts on restart.

Replace the guest PING/PONG responder with an HTTP /healthz agent over vsock, rename the runtime bundle and config surface from ping helper to agent while still accepting the legacy keys, and route the post-SSH reminder through the new vm.health path.

Validated with GOCACHE=/tmp/banger-gocache go test ./..., make build, bash -n customize.sh make-rootfs-void.sh, and git diff --check.
This commit is contained in:
Thales Maciel 2026-03-19 14:51:25 -03:00
parent c8d9a122f9
commit 3ed78fdcfc
No known key found for this signature in database
GPG key ID: 33112E6833C34679
42 changed files with 2222 additions and 388 deletions

View file

@ -24,7 +24,7 @@ import (
"banger/internal/rpc"
"banger/internal/system"
"banger/internal/vmdns"
"banger/internal/vsockping"
"banger/internal/vsockagent"
"github.com/spf13/cobra"
)
@ -42,8 +42,8 @@ var (
sshCmd.Stdin = stdin
return sshCmd.Run()
}
vmPingFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMPingResult, error) {
return rpc.Call[api.VMPingResult](ctx, socketPath, "vm.ping", api.VMRefParams{IDOrName: idOrName})
vmHealthFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) {
return rpc.Call[api.VMHealthResult](ctx, socketPath, "vm.health", api.VMRefParams{IDOrName: idOrName})
}
)
@ -550,6 +550,7 @@ func newImageCommand() *cobra.Command {
}
cmd.AddCommand(
newImageBuildCommand(),
newImageRegisterCommand(),
newImageListCommand(),
newImageShowCommand(),
newImageDeleteCommand(),
@ -591,6 +592,41 @@ func newImageBuildCommand() *cobra.Command {
return cmd
}
func newImageRegisterCommand() *cobra.Command {
var params api.ImageRegisterParams
cmd := &cobra.Command{
Use: "register",
Short: "Register or update an unmanaged image",
Args: noArgsUsage("usage: banger image register --name <name> --rootfs <path> [--work-seed <path>] [--kernel <path>] [--initrd <path>] [--modules <dir>] [--packages <path>]"),
RunE: func(cmd *cobra.Command, args []string) error {
if err := absolutizeImageRegisterPaths(&params); err != nil {
return err
}
if err := system.EnsureSudo(cmd.Context()); err != nil {
return err
}
layout, _, err := ensureDaemon(cmd.Context())
if err != nil {
return err
}
result, err := rpc.Call[api.ImageShowResult](cmd.Context(), layout.SocketPath, "image.register", params)
if err != nil {
return err
}
return printImageSummary(cmd.OutOrStdout(), result.Image)
},
}
cmd.Flags().StringVar(&params.Name, "name", "", "image name")
cmd.Flags().StringVar(&params.RootfsPath, "rootfs", "", "rootfs path")
cmd.Flags().StringVar(&params.WorkSeedPath, "work-seed", "", "work-seed path")
cmd.Flags().StringVar(&params.KernelPath, "kernel", "", "kernel path")
cmd.Flags().StringVar(&params.InitrdPath, "initrd", "", "initrd path")
cmd.Flags().StringVar(&params.ModulesDir, "modules", "", "modules dir")
cmd.Flags().StringVar(&params.PackagesPath, "packages", "", "packages manifest path")
cmd.Flags().BoolVar(&params.Docker, "docker", false, "mark image as docker-prepared")
return cmd
}
func newImageListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
@ -995,17 +1031,17 @@ func runSSHSession(ctx context.Context, socketPath, vmRef string, stdin io.Reade
}
pingCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
ping, err := vmPingFunc(pingCtx, socketPath, vmRef)
health, err := vmHealthFunc(pingCtx, socketPath, vmRef)
if err != nil {
_, _ = fmt.Fprintln(stderr, vsockping.WarningMessage(vmRef, err))
_, _ = fmt.Fprintln(stderr, vsockagent.WarningMessage(vmRef, err))
return sshErr
}
if ping.Alive {
name := ping.Name
if health.Healthy {
name := health.Name
if strings.TrimSpace(name) == "" {
name = vmRef
}
_, _ = fmt.Fprintln(stderr, vsockping.ReminderMessage(name))
_, _ = fmt.Fprintln(stderr, vsockagent.ReminderMessage(name))
}
return sshErr
}
@ -1015,7 +1051,10 @@ func shouldCheckSSHReminder(err error) bool {
return true
}
var exitErr *exec.ExitError
return errors.As(err, &exitErr)
if !errors.As(err, &exitErr) {
return false
}
return exitErr.ExitCode() != 255
}
func sshCommandArgs(cfg model.DaemonConfig, guestIP string, extra []string) ([]string, error) {
@ -1023,10 +1062,21 @@ func sshCommandArgs(cfg model.DaemonConfig, guestIP string, extra []string) ([]s
return nil, errors.New("vm has no guest IP")
}
args := []string{}
args = append(args, "-F", "/dev/null")
if cfg.SSHKeyPath != "" {
args = append(args, "-i", cfg.SSHKeyPath)
}
args = append(args, "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "root@"+guestIP)
args = append(
args,
"-o", "IdentitiesOnly=yes",
"-o", "BatchMode=yes",
"-o", "PreferredAuthentications=publickey",
"-o", "PasswordAuthentication=no",
"-o", "KbdInteractiveAuthentication=no",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"root@"+guestIP,
)
args = append(args, extra...)
return args, nil
}
@ -1035,14 +1085,29 @@ func validateSSHPrereqs(cfg model.DaemonConfig) error {
checks := system.NewPreflight()
checks.RequireCommand("ssh", "install openssh-client")
if strings.TrimSpace(cfg.SSHKeyPath) != "" {
checks.RequireFile(cfg.SSHKeyPath, "ssh private key", `set "ssh_key_path" or refresh the runtime bundle`)
checks.RequireFile(cfg.SSHKeyPath, "runtime ssh private key", `refresh the runtime bundle`)
}
return checks.Err("ssh preflight failed")
}
func absolutizeImageBuildPaths(params *api.ImageBuildParams) error {
return absolutizePaths(&params.BaseRootfs, &params.KernelPath, &params.InitrdPath, &params.ModulesDir)
}
func absolutizeImageRegisterPaths(params *api.ImageRegisterParams) error {
return absolutizePaths(
&params.RootfsPath,
&params.WorkSeedPath,
&params.KernelPath,
&params.InitrdPath,
&params.ModulesDir,
&params.PackagesPath,
)
}
func absolutizePaths(values ...*string) error {
var err error
for _, value := range []*string{&params.BaseRootfs, &params.KernelPath, &params.InitrdPath, &params.ModulesDir} {
for _, value := range values {
if *value == "" || filepath.IsAbs(*value) {
continue
}