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:
parent
c8d9a122f9
commit
3ed78fdcfc
42 changed files with 2222 additions and 388 deletions
|
|
@ -13,11 +13,13 @@ import (
|
|||
|
||||
"banger/internal/api"
|
||||
"banger/internal/firecracker"
|
||||
"banger/internal/guest"
|
||||
"banger/internal/guestconfig"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/system"
|
||||
"banger/internal/vmdns"
|
||||
"banger/internal/vsockagent"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -582,11 +584,11 @@ func (d *Daemon) GetVMStats(ctx context.Context, idOrName string) (model.VMRecor
|
|||
return vm, vm.Stats, nil
|
||||
}
|
||||
|
||||
func (d *Daemon) PingVM(ctx context.Context, idOrName string) (result api.VMPingResult, err error) {
|
||||
func (d *Daemon) HealthVM(ctx context.Context, idOrName string) (result api.VMHealthResult, err error) {
|
||||
_, err = d.withVMLockByRef(ctx, idOrName, func(vm model.VMRecord) (model.VMRecord, error) {
|
||||
result.Name = vm.Name
|
||||
if vm.State != model.VMStateRunning || !system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath) {
|
||||
result.Alive = false
|
||||
result.Healthy = false
|
||||
return vm, nil
|
||||
}
|
||||
if strings.TrimSpace(vm.Runtime.VSockPath) == "" {
|
||||
|
|
@ -600,15 +602,23 @@ func (d *Daemon) PingVM(ctx context.Context, idOrName string) (result api.VMPing
|
|||
}
|
||||
pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
if err := firecracker.PingVSock(pingCtx, d.logger, vm.Runtime.VSockPath); err != nil {
|
||||
if err := vsockagent.Health(pingCtx, d.logger, vm.Runtime.VSockPath); err != nil {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
result.Alive = true
|
||||
result.Healthy = true
|
||||
return vm, nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *Daemon) PingVM(ctx context.Context, idOrName string) (result api.VMPingResult, err error) {
|
||||
health, err := d.HealthVM(ctx, idOrName)
|
||||
if err != nil {
|
||||
return api.VMPingResult{}, err
|
||||
}
|
||||
return api.VMPingResult{Name: health.Name, Alive: health.Healthy}, nil
|
||||
}
|
||||
|
||||
func (d *Daemon) getVMStatsLocked(ctx context.Context, vm model.VMRecord) (model.VMRecord, error) {
|
||||
stats, err := d.collectStats(ctx, vm)
|
||||
if err == nil {
|
||||
|
|
@ -814,6 +824,84 @@ func (d *Daemon) ensureWorkDisk(ctx context.Context, vm *model.VMRecord, image m
|
|||
return nil
|
||||
}
|
||||
|
||||
func (d *Daemon) ensureAuthorizedKeyOnWorkDisk(ctx context.Context, vm *model.VMRecord) error {
|
||||
publicKey, err := guest.AuthorizedPublicKey(d.config.SSHKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("derive authorized ssh key: %w", err)
|
||||
}
|
||||
workMount, cleanupWork, err := system.MountTempDir(ctx, d.runner, vm.Runtime.WorkDiskPath, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanupWork()
|
||||
|
||||
if err := d.flattenNestedWorkHome(ctx, workMount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sshDir := filepath.Join(workMount, ".ssh")
|
||||
if _, err := d.runner.RunSudo(ctx, "mkdir", "-p", sshDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := d.runner.RunSudo(ctx, "chmod", "700", sshDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authorizedKeysPath := filepath.Join(sshDir, "authorized_keys")
|
||||
existing, err := d.runner.RunSudo(ctx, "cat", authorizedKeysPath)
|
||||
if err != nil {
|
||||
existing = nil
|
||||
}
|
||||
merged := mergeAuthorizedKey(existing, publicKey)
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "banger-authorized-keys-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
if _, err := tmpFile.Write(merged); err != nil {
|
||||
_ = tmpFile.Close()
|
||||
_ = os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
_ = os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
if _, err := d.runner.RunSudo(ctx, "install", "-m", "600", tmpPath, authorizedKeysPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeAuthorizedKey(existing, managed []byte) []byte {
|
||||
managedLine := strings.TrimSpace(string(managed))
|
||||
if managedLine == "" {
|
||||
return append([]byte(nil), existing...)
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.ReplaceAll(string(existing), "\r\n", "\n"), "\n")
|
||||
out := make([]string, 0, len(lines)+1)
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if trimmed == managedLine {
|
||||
found = true
|
||||
}
|
||||
out = append(out, line)
|
||||
}
|
||||
if !found {
|
||||
out = append(out, managedLine)
|
||||
}
|
||||
return []byte(strings.Join(out, "\n") + "\n")
|
||||
}
|
||||
|
||||
func (d *Daemon) flattenNestedWorkHome(ctx context.Context, workMount string) error {
|
||||
nestedHome := filepath.Join(workMount, "root")
|
||||
if !exists(nestedHome) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue