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:
parent
ea72ea26fe
commit
2539800f5c
6 changed files with 1393 additions and 179 deletions
33
go.mod
33
go.mod
|
|
@ -6,31 +6,64 @@ require (
|
||||||
github.com/charmbracelet/bubbles v0.14.0
|
github.com/charmbracelet/bubbles v0.14.0
|
||||||
github.com/charmbracelet/bubbletea v0.21.1-0.20220623121936-ca32c4c62873
|
github.com/charmbracelet/bubbletea v0.21.1-0.20220623121936-ca32c4c62873
|
||||||
github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43
|
github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43
|
||||||
|
github.com/firecracker-microvm/firecracker-go-sdk v1.0.0
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/pelletier/go-toml v1.9.5
|
github.com/pelletier/go-toml v1.9.5
|
||||||
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
modernc.org/sqlite v1.38.2
|
modernc.org/sqlite v1.38.2
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/containerd/console v1.0.3 // indirect
|
github.com/containerd/console v1.0.3 // indirect
|
||||||
|
github.com/containerd/fifo v1.0.0 // indirect
|
||||||
|
github.com/containernetworking/cni v1.0.1 // indirect
|
||||||
|
github.com/containernetworking/plugins v1.0.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/go-openapi/analysis v0.21.2 // indirect
|
||||||
|
github.com/go-openapi/errors v0.20.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
|
github.com/go-openapi/loads v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/runtime v0.24.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
|
github.com/go-openapi/strfmt v0.21.2 // indirect
|
||||||
|
github.com/go-openapi/swag v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/validate v0.22.0 // indirect
|
||||||
|
github.com/go-stack/stack v1.8.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||||
github.com/muesli/cancelreader v0.2.1 // indirect
|
github.com/muesli/cancelreader v0.2.1 // indirect
|
||||||
github.com/muesli/reflow v0.3.0 // indirect
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
|
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.8.3 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
modernc.org/libc v1.66.3 // indirect
|
modernc.org/libc v1.66.3 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"banger/internal/api"
|
"banger/internal/api"
|
||||||
|
|
@ -197,69 +195,32 @@ func (d *Daemon) startVMLocked(ctx context.Context, vm model.VMRecord, image mod
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cleanupOnErr(err)
|
return cleanupOnErr(err)
|
||||||
}
|
}
|
||||||
pid, err := d.startFirecrackerProcess(ctx, fcPath, apiSock, vm.Runtime.LogPath)
|
machine, err := firecracker.NewMachine(ctx, firecracker.MachineConfig{
|
||||||
|
BinaryPath: fcPath,
|
||||||
|
VMID: vm.ID,
|
||||||
|
SocketPath: apiSock,
|
||||||
|
LogPath: vm.Runtime.LogPath,
|
||||||
|
MetricsPath: vm.Runtime.MetricsPath,
|
||||||
|
KernelImagePath: image.KernelPath,
|
||||||
|
InitrdPath: image.InitrdPath,
|
||||||
|
KernelArgs: system.BuildBootArgs(vm.Name, vm.Runtime.GuestIP, d.config.BridgeIP, d.config.DefaultDNS),
|
||||||
|
RootDrivePath: vm.Runtime.DMDev,
|
||||||
|
WorkDrivePath: vm.Runtime.WorkDiskPath,
|
||||||
|
TapDevice: tap,
|
||||||
|
VCPUCount: vm.Spec.VCPUCount,
|
||||||
|
MemoryMiB: vm.Spec.MemoryMiB,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cleanupOnErr(err)
|
return cleanupOnErr(err)
|
||||||
}
|
}
|
||||||
vm.Runtime.PID = pid
|
if err := machine.Start(ctx); err != nil {
|
||||||
|
vm.Runtime.PID = d.resolveFirecrackerPID(ctx, machine, apiSock)
|
||||||
if err := d.waitForSocket(ctx, apiSock); err != nil {
|
|
||||||
return cleanupOnErr(err)
|
return cleanupOnErr(err)
|
||||||
}
|
}
|
||||||
if actualPID, err := d.findFirecrackerPID(ctx, apiSock); err == nil && actualPID > 0 {
|
vm.Runtime.PID = d.resolveFirecrackerPID(ctx, machine, apiSock)
|
||||||
vm.Runtime.PID = actualPID
|
if err := d.ensureSocketAccess(ctx, apiSock); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
client := firecracker.New(apiSock)
|
|
||||||
if err := client.Put(ctx, "/machine-config", map[string]any{
|
|
||||||
"vcpu_count": vm.Spec.VCPUCount,
|
|
||||||
"mem_size_mib": vm.Spec.MemoryMiB,
|
|
||||||
"smt": false,
|
|
||||||
}); err != nil {
|
|
||||||
return cleanupOnErr(err)
|
return cleanupOnErr(err)
|
||||||
}
|
}
|
||||||
if err := client.Put(ctx, "/metrics", map[string]any{
|
|
||||||
"metrics_path": vm.Runtime.MetricsPath,
|
|
||||||
}); err != nil {
|
|
||||||
return cleanupOnErr(err)
|
|
||||||
}
|
|
||||||
boot := map[string]any{
|
|
||||||
"kernel_image_path": image.KernelPath,
|
|
||||||
"boot_args": system.BuildBootArgs(vm.Name, vm.Runtime.GuestIP, d.config.BridgeIP, d.config.DefaultDNS),
|
|
||||||
}
|
|
||||||
if image.InitrdPath != "" {
|
|
||||||
boot["initrd_path"] = image.InitrdPath
|
|
||||||
}
|
|
||||||
if err := client.Put(ctx, "/boot-source", boot); err != nil {
|
|
||||||
return cleanupOnErr(err)
|
|
||||||
}
|
|
||||||
if err := client.Put(ctx, "/drives/rootfs", map[string]any{
|
|
||||||
"drive_id": "rootfs",
|
|
||||||
"path_on_host": vm.Runtime.DMDev,
|
|
||||||
"is_root_device": true,
|
|
||||||
"is_read_only": false,
|
|
||||||
}); err != nil {
|
|
||||||
return cleanupOnErr(err)
|
|
||||||
}
|
|
||||||
if err := client.Put(ctx, "/drives/work", map[string]any{
|
|
||||||
"drive_id": "work",
|
|
||||||
"path_on_host": vm.Runtime.WorkDiskPath,
|
|
||||||
"is_root_device": false,
|
|
||||||
"is_read_only": false,
|
|
||||||
}); err != nil {
|
|
||||||
return cleanupOnErr(err)
|
|
||||||
}
|
|
||||||
if err := client.Put(ctx, "/network-interfaces/eth0", map[string]any{
|
|
||||||
"iface_id": "eth0",
|
|
||||||
"host_dev_name": tap,
|
|
||||||
}); err != nil {
|
|
||||||
return cleanupOnErr(err)
|
|
||||||
}
|
|
||||||
if err := client.Put(ctx, "/actions", map[string]any{"action_type": "InstanceStart"}); err != nil {
|
|
||||||
return cleanupOnErr(err)
|
|
||||||
}
|
|
||||||
fcConfig, _ := client.GetConfig(ctx)
|
|
||||||
vm.Runtime.FirecrackerState = fcConfig
|
|
||||||
if err := d.setDNS(ctx, vm.Name, vm.Runtime.GuestIP); err != nil {
|
if err := d.setDNS(ctx, vm.Name, vm.Runtime.GuestIP); err != nil {
|
||||||
return cleanupOnErr(err)
|
return cleanupOnErr(err)
|
||||||
}
|
}
|
||||||
|
|
@ -615,59 +576,6 @@ func (d *Daemon) firecrackerBinary() (string, error) {
|
||||||
return path, nil
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) startFirecrackerProcess(ctx context.Context, fcBin, apiSock, logPath string) (int, error) {
|
|
||||||
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-n", fcBin, "--api-sock", apiSock)
|
|
||||||
cmd.Stdout = logFile
|
|
||||||
cmd.Stderr = logFile
|
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
_ = logFile.Close()
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
_ = cmd.Wait()
|
|
||||||
_ = logFile.Close()
|
|
||||||
}()
|
|
||||||
return cmd.Process.Pid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) waitForSocket(ctx context.Context, apiSock string) error {
|
|
||||||
deadline := time.Now().Add(15 * time.Second)
|
|
||||||
var lastErr error
|
|
||||||
for {
|
|
||||||
if _, err := os.Stat(apiSock); err == nil {
|
|
||||||
if err := d.ensureSocketAccess(ctx, apiSock); err != nil {
|
|
||||||
lastErr = err
|
|
||||||
} else {
|
|
||||||
conn, dialErr := net.DialTimeout("unix", apiSock, 200*time.Millisecond)
|
|
||||||
if dialErr == nil {
|
|
||||||
_ = conn.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
lastErr = dialErr
|
|
||||||
}
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
if time.Now().After(deadline) {
|
|
||||||
if lastErr != nil {
|
|
||||||
return fmt.Errorf("firecracker api socket not ready: %s: %w", apiSock, lastErr)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("firecracker api socket not ready: %s", apiSock)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-time.After(20 * time.Millisecond):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Daemon) ensureSocketAccess(ctx context.Context, apiSock string) error {
|
func (d *Daemon) ensureSocketAccess(ctx context.Context, apiSock string) error {
|
||||||
if _, err := d.runner.RunSudo(ctx, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), apiSock); err != nil {
|
if _, err := d.runner.RunSudo(ctx, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), apiSock); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -684,12 +592,24 @@ func (d *Daemon) findFirecrackerPID(ctx context.Context, apiSock string) (int, e
|
||||||
return strconv.Atoi(strings.TrimSpace(string(out)))
|
return strconv.Atoi(strings.TrimSpace(string(out)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) resolveFirecrackerPID(ctx context.Context, machine *firecracker.Machine, apiSock string) int {
|
||||||
|
if pid, err := d.findFirecrackerPID(ctx, apiSock); err == nil && pid > 0 {
|
||||||
|
return pid
|
||||||
|
}
|
||||||
|
if machine != nil {
|
||||||
|
if pid, err := machine.PID(); err == nil && pid > 0 {
|
||||||
|
return pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Daemon) sendCtrlAltDel(ctx context.Context, vm model.VMRecord) error {
|
func (d *Daemon) sendCtrlAltDel(ctx context.Context, vm model.VMRecord) error {
|
||||||
if err := d.ensureSocketAccess(ctx, vm.Runtime.APISockPath); err != nil {
|
if err := d.ensureSocketAccess(ctx, vm.Runtime.APISockPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
client := firecracker.New(vm.Runtime.APISockPath)
|
client := firecracker.New(vm.Runtime.APISockPath)
|
||||||
return client.Put(ctx, "/actions", map[string]any{"action_type": "SendCtrlAltDel"})
|
return client.SendCtrlAltDel(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) waitForExit(ctx context.Context, pid int, apiSock string, timeout time.Duration) error {
|
func (d *Daemon) waitForExit(ctx context.Context, pid int, apiSock string, timeout time.Duration) error {
|
||||||
|
|
@ -748,7 +668,6 @@ func clearRuntimeHandles(vm *model.VMRecord) {
|
||||||
vm.Runtime.COWLoop = ""
|
vm.Runtime.COWLoop = ""
|
||||||
vm.Runtime.DMName = ""
|
vm.Runtime.DMName = ""
|
||||||
vm.Runtime.DMDev = ""
|
vm.Runtime.DMDev = ""
|
||||||
vm.Runtime.FirecrackerState = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Daemon) setDNS(ctx context.Context, vmName, guestIP string) error {
|
func (d *Daemon) setDNS(ctx context.Context, vmName, guestIP string) error {
|
||||||
|
|
@ -794,8 +713,6 @@ func (d *Daemon) requireStartPrereqs(ctx context.Context) error {
|
||||||
ctx,
|
ctx,
|
||||||
"sudo",
|
"sudo",
|
||||||
"ip",
|
"ip",
|
||||||
"curl",
|
|
||||||
"jq",
|
|
||||||
"dmsetup",
|
"dmsetup",
|
||||||
"losetup",
|
"losetup",
|
||||||
"blockdev",
|
"blockdev",
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,165 @@
|
||||||
package firecracker
|
package firecracker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"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 {
|
type Client struct {
|
||||||
http *http.Client
|
client *sdk.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(apiSock string) *Client {
|
func NewMachine(ctx context.Context, cfg MachineConfig) (*Machine, error) {
|
||||||
return &Client{http: rpc.NewUnixHTTPClient(apiSock)}
|
logFile, err := openLogFile(cfg.LogPath)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Put(ctx context.Context, path string, body any) error {
|
cmd := buildProcessRunner(ctx, cfg, logFile)
|
||||||
var payload io.Reader = http.NoBody
|
machine, err := sdk.NewMachine(
|
||||||
if body != nil {
|
ctx,
|
||||||
data, err := json.Marshal(body)
|
buildConfig(cfg),
|
||||||
if err != nil {
|
sdk.WithProcessRunner(cmd),
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
resp, err := c.http.Do(req)
|
go func() {
|
||||||
if err != nil {
|
_ = m.machine.Wait(context.Background())
|
||||||
return err
|
m.closeLog()
|
||||||
}
|
}()
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode >= 300 {
|
|
||||||
data, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("firecracker %s failed: %s", path, bytes.TrimSpace(data))
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetConfig(ctx context.Context) (map[string]any, error) {
|
func (m *Machine) PID() (int, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost/vm/config", nil)
|
return m.machine.PID()
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
func New(apiSock string) *Client {
|
||||||
resp, err := c.http.Do(req)
|
return &Client{client: sdk.NewClient(apiSock, newLogger(), false)}
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
func (c *Client) SendCtrlAltDel(ctx context.Context) error {
|
||||||
defer resp.Body.Close()
|
action := models.InstanceActionInfoActionTypeSendCtrlAltDel
|
||||||
if resp.StatusCode >= 300 {
|
_, err := c.client.CreateSyncAction(ctx, &models.InstanceActionInfo{
|
||||||
data, _ := io.ReadAll(resp.Body)
|
ActionType: &action,
|
||||||
return nil, fmt.Errorf("firecracker config failed: %s", bytes.TrimSpace(data))
|
})
|
||||||
}
|
return err
|
||||||
var out map[string]any
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
||||||
return nil, err
|
func openLogFile(path string) (*os.File, error) {
|
||||||
}
|
if path == "" {
|
||||||
return out, nil
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
78
internal/firecracker/client_test.go
Normal file
78
internal/firecracker/client_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
package firecracker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildConfig(t *testing.T) {
|
||||||
|
cfg := buildConfig(MachineConfig{
|
||||||
|
VMID: "vm-1",
|
||||||
|
SocketPath: "/tmp/fc.sock",
|
||||||
|
LogPath: "/tmp/fc.log",
|
||||||
|
MetricsPath: "/tmp/fc.metrics",
|
||||||
|
KernelImagePath: "/kernel",
|
||||||
|
InitrdPath: "/initrd",
|
||||||
|
KernelArgs: "console=ttyS0",
|
||||||
|
RootDrivePath: "/dev/mapper/root",
|
||||||
|
WorkDrivePath: "/var/lib/banger/root.ext4",
|
||||||
|
TapDevice: "tap-fc-1",
|
||||||
|
VCPUCount: 4,
|
||||||
|
MemoryMiB: 2048,
|
||||||
|
})
|
||||||
|
|
||||||
|
if cfg.SocketPath != "/tmp/fc.sock" {
|
||||||
|
t.Fatalf("socket path = %q", cfg.SocketPath)
|
||||||
|
}
|
||||||
|
if cfg.LogPath != "/tmp/fc.log" || cfg.MetricsPath != "/tmp/fc.metrics" {
|
||||||
|
t.Fatalf("unexpected log or metrics path: %+v", cfg)
|
||||||
|
}
|
||||||
|
if cfg.KernelImagePath != "/kernel" || cfg.InitrdPath != "/initrd" {
|
||||||
|
t.Fatalf("unexpected kernel paths: %+v", cfg)
|
||||||
|
}
|
||||||
|
if len(cfg.Drives) != 2 {
|
||||||
|
t.Fatalf("drive count = %d, want 2", len(cfg.Drives))
|
||||||
|
}
|
||||||
|
if cfg.Drives[0].DriveID == nil || *cfg.Drives[0].DriveID != "work" {
|
||||||
|
t.Fatalf("work drive id = %v", cfg.Drives[0].DriveID)
|
||||||
|
}
|
||||||
|
if cfg.Drives[1].DriveID == nil || *cfg.Drives[1].DriveID != "rootfs" {
|
||||||
|
t.Fatalf("root drive id = %v", cfg.Drives[1].DriveID)
|
||||||
|
}
|
||||||
|
if len(cfg.NetworkInterfaces) != 1 {
|
||||||
|
t.Fatalf("interface count = %d, want 1", len(cfg.NetworkInterfaces))
|
||||||
|
}
|
||||||
|
if got := cfg.NetworkInterfaces[0].StaticConfiguration.HostDevName; got != "tap-fc-1" {
|
||||||
|
t.Fatalf("host dev name = %q", got)
|
||||||
|
}
|
||||||
|
if cfg.MachineCfg.VcpuCount == nil || *cfg.MachineCfg.VcpuCount != 4 {
|
||||||
|
t.Fatalf("vcpu = %v", cfg.MachineCfg.VcpuCount)
|
||||||
|
}
|
||||||
|
if cfg.MachineCfg.MemSizeMib == nil || *cfg.MachineCfg.MemSizeMib != 2048 {
|
||||||
|
t.Fatalf("memory = %v", cfg.MachineCfg.MemSizeMib)
|
||||||
|
}
|
||||||
|
if cfg.MachineCfg.Smt == nil || *cfg.MachineCfg.Smt {
|
||||||
|
t.Fatalf("smt = %v, want false", cfg.MachineCfg.Smt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildProcessRunnerUsesSudoWrapper(t *testing.T) {
|
||||||
|
cmd := buildProcessRunner(context.Background(), MachineConfig{
|
||||||
|
BinaryPath: "/repo/firecracker",
|
||||||
|
SocketPath: "/tmp/fc.sock",
|
||||||
|
VMID: "vm-1",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
if cmd.Path != "/usr/bin/sudo" && cmd.Path != "sudo" {
|
||||||
|
t.Fatalf("command path = %q", cmd.Path)
|
||||||
|
}
|
||||||
|
if len(cmd.Args) != 5 {
|
||||||
|
t.Fatalf("args = %v", cmd.Args)
|
||||||
|
}
|
||||||
|
if cmd.Args[1] != "-n" || cmd.Args[2] != "sh" || cmd.Args[3] != "-c" {
|
||||||
|
t.Fatalf("args = %v", cmd.Args)
|
||||||
|
}
|
||||||
|
if want := "umask 000 && exec '/repo/firecracker' --api-sock '/tmp/fc.sock' --id 'vm-1'"; cmd.Args[4] != want {
|
||||||
|
t.Fatalf("script = %q, want %q", cmd.Args[4], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -76,23 +76,22 @@ type VMSpec struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type VMRuntime struct {
|
type VMRuntime struct {
|
||||||
State VMState `json:"state"`
|
State VMState `json:"state"`
|
||||||
PID int `json:"pid,omitempty"`
|
PID int `json:"pid,omitempty"`
|
||||||
GuestIP string `json:"guest_ip"`
|
GuestIP string `json:"guest_ip"`
|
||||||
TapDevice string `json:"tap_device,omitempty"`
|
TapDevice string `json:"tap_device,omitempty"`
|
||||||
APISockPath string `json:"api_sock_path,omitempty"`
|
APISockPath string `json:"api_sock_path,omitempty"`
|
||||||
LogPath string `json:"log_path,omitempty"`
|
LogPath string `json:"log_path,omitempty"`
|
||||||
MetricsPath string `json:"metrics_path,omitempty"`
|
MetricsPath string `json:"metrics_path,omitempty"`
|
||||||
DNSName string `json:"dns_name,omitempty"`
|
DNSName string `json:"dns_name,omitempty"`
|
||||||
VMDir string `json:"vm_dir"`
|
VMDir string `json:"vm_dir"`
|
||||||
SystemOverlay string `json:"system_overlay_path"`
|
SystemOverlay string `json:"system_overlay_path"`
|
||||||
WorkDiskPath string `json:"work_disk_path"`
|
WorkDiskPath string `json:"work_disk_path"`
|
||||||
BaseLoop string `json:"base_loop,omitempty"`
|
BaseLoop string `json:"base_loop,omitempty"`
|
||||||
COWLoop string `json:"cow_loop,omitempty"`
|
COWLoop string `json:"cow_loop,omitempty"`
|
||||||
DMName string `json:"dm_name,omitempty"`
|
DMName string `json:"dm_name,omitempty"`
|
||||||
DMDev string `json:"dm_dev,omitempty"`
|
DMDev string `json:"dm_dev,omitempty"`
|
||||||
LastError string `json:"last_error,omitempty"`
|
LastError string `json:"last_error,omitempty"`
|
||||||
FirecrackerState map[string]any `json:"firecracker_state,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VMStats struct {
|
type VMStats struct {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue