package firecracker import ( "context" "io" "os" "os/exec" "strings" "sync" 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 { client *sdk.Client } func NewMachine(ctx context.Context, cfg MachineConfig) (*Machine, error) { logFile, err := openLogFile(cfg.LogPath) if err != nil { return nil, 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() } return nil, err } 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 } go func() { _ = m.machine.Wait(context.Background()) m.closeLog() }() return 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() } }) }