Use Firecracker SDK in daemon

Replace the daemon's hand-rolled Firecracker process/socket client with the official firecracker-go-sdk while keeping the existing VM lifecycle and host-side disk and TAP setup intact.

Build machine configs through the SDK, launch Firecracker through a sudo process runner, resolve the real VM PID after startup, and use the SDK client for Ctrl-Alt-Del instead of raw REST calls. Drop the unused cached Firecracker state and add focused adapter tests for config and process-runner wiring.

Validated with go mod tidy, go test ./..., and make build. A live KVM/Firecracker smoke boot was not run in this environment.
This commit is contained in:
Thales Maciel 2026-03-16 13:26:41 -03:00
parent ea72ea26fe
commit 2539800f5c
No known key found for this signature in database
GPG key ID: 33112E6833C34679
6 changed files with 1393 additions and 179 deletions

View file

@ -1,67 +1,165 @@
package firecracker
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strings"
"sync"
"banger/internal/rpc"
sdk "github.com/firecracker-microvm/firecracker-go-sdk"
models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
"github.com/sirupsen/logrus"
)
type MachineConfig struct {
BinaryPath string
VMID string
SocketPath string
LogPath string
MetricsPath string
KernelImagePath string
InitrdPath string
KernelArgs string
RootDrivePath string
WorkDrivePath string
TapDevice string
VCPUCount int
MemoryMiB int
}
type Machine struct {
machine *sdk.Machine
logFile *os.File
closeOnce sync.Once
}
type Client struct {
http *http.Client
client *sdk.Client
}
func New(apiSock string) *Client {
return &Client{http: rpc.NewUnixHTTPClient(apiSock)}
}
func NewMachine(ctx context.Context, cfg MachineConfig) (*Machine, error) {
logFile, err := openLogFile(cfg.LogPath)
if err != nil {
return nil, err
}
func (c *Client) Put(ctx context.Context, path string, body any) error {
var payload io.Reader = http.NoBody
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return err
cmd := buildProcessRunner(ctx, cfg, logFile)
machine, err := sdk.NewMachine(
ctx,
buildConfig(cfg),
sdk.WithProcessRunner(cmd),
sdk.WithLogger(newLogger()),
)
if err != nil {
if logFile != nil {
_ = logFile.Close()
}
payload = bytes.NewReader(data)
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://localhost"+path, payload)
if err != nil {
return &Machine{machine: machine, logFile: logFile}, nil
}
func (m *Machine) Start(ctx context.Context) error {
if err := m.machine.Start(ctx); err != nil {
m.closeLog()
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
data, _ := io.ReadAll(resp.Body)
return fmt.Errorf("firecracker %s failed: %s", path, bytes.TrimSpace(data))
}
go func() {
_ = m.machine.Wait(context.Background())
m.closeLog()
}()
return nil
}
func (c *Client) GetConfig(ctx context.Context) (map[string]any, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost/vm/config", nil)
if err != nil {
return nil, err
}
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
data, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("firecracker config failed: %s", bytes.TrimSpace(data))
}
var out map[string]any
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
return out, nil
func (m *Machine) PID() (int, error) {
return m.machine.PID()
}
func New(apiSock string) *Client {
return &Client{client: sdk.NewClient(apiSock, newLogger(), false)}
}
func (c *Client) SendCtrlAltDel(ctx context.Context) error {
action := models.InstanceActionInfoActionTypeSendCtrlAltDel
_, err := c.client.CreateSyncAction(ctx, &models.InstanceActionInfo{
ActionType: &action,
})
return err
}
func openLogFile(path string) (*os.File, error) {
if path == "" {
return nil, nil
}
return os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
}
func buildConfig(cfg MachineConfig) sdk.Config {
drives := sdk.NewDrivesBuilder(
cfg.RootDrivePath,
).
WithRootDrive(cfg.RootDrivePath, sdk.WithDriveID("rootfs"), sdk.WithReadOnly(false)).
AddDrive(cfg.WorkDrivePath, false, sdk.WithDriveID("work")).
Build()
return sdk.Config{
SocketPath: cfg.SocketPath,
LogPath: cfg.LogPath,
MetricsPath: cfg.MetricsPath,
KernelImagePath: cfg.KernelImagePath,
InitrdPath: cfg.InitrdPath,
KernelArgs: cfg.KernelArgs,
Drives: drives,
NetworkInterfaces: sdk.NetworkInterfaces{{
StaticConfiguration: &sdk.StaticNetworkConfiguration{
HostDevName: cfg.TapDevice,
},
}},
MachineCfg: models.MachineConfiguration{
VcpuCount: sdk.Int64(int64(cfg.VCPUCount)),
MemSizeMib: sdk.Int64(int64(cfg.MemoryMiB)),
Smt: sdk.Bool(false),
},
VMID: cfg.VMID,
}
}
func buildProcessRunner(ctx context.Context, cfg MachineConfig, logFile *os.File) *exec.Cmd {
script := strings.Join([]string{
"umask 000",
"exec " + shellQuote(cfg.BinaryPath) +
" --api-sock " + shellQuote(cfg.SocketPath) +
" --id " + shellQuote(cfg.VMID),
}, " && ")
cmd := exec.CommandContext(ctx, "sudo", "-n", "sh", "-c", script)
cmd.Stdin = nil
if logFile != nil {
cmd.Stdout = logFile
cmd.Stderr = logFile
}
return cmd
}
func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}
func newLogger() *logrus.Entry {
logger := logrus.New()
logger.SetOutput(io.Discard)
return logrus.NewEntry(logger)
}
func (m *Machine) closeLog() {
m.closeOnce.Do(func() {
if m.logFile != nil {
_ = m.logFile.Close()
}
})
}