From 58f95324f4c55bc86c1c4ca8186815e46882e88b Mon Sep 17 00:00:00 2001 From: Hari <73809867+harivansh-afk@users.noreply.github.com> Date: Tue, 7 Apr 2026 20:56:37 -0400 Subject: [PATCH] feat: remove go sdk and write thin firecracker wrapper (#1) * feat: remove go sdk and write thin firecracker wrapper * chore: gitignore --- internal/firecracker/api.go | 177 ++++++++++++++++++++++++++++ internal/firecracker/doc.go | 4 +- internal/firecracker/launch.go | 203 ++++++++++++++++++++++++++++++++ internal/firecracker/paths.go | 30 ++--- internal/firecracker/runtime.go | 162 ++++++++++++------------- internal/firecracker/sdk.go | 92 --------------- 6 files changed, 478 insertions(+), 190 deletions(-) create mode 100644 internal/firecracker/api.go create mode 100644 internal/firecracker/launch.go delete mode 100644 internal/firecracker/sdk.go diff --git a/internal/firecracker/api.go b/internal/firecracker/api.go new file mode 100644 index 0000000..25c9bcb --- /dev/null +++ b/internal/firecracker/api.go @@ -0,0 +1,177 @@ +package firecracker + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" +) + +type actionRequest struct { + ActionType string `json:"action_type"` +} + +type apiClient struct { + httpClient *http.Client + socketPath string +} + +type bootSourceRequest struct { + BootArgs string `json:"boot_args,omitempty"` + KernelImagePath string `json:"kernel_image_path"` +} + +type driveRequest struct { + DriveID string `json:"drive_id"` + IsReadOnly bool `json:"is_read_only"` + IsRootDevice bool `json:"is_root_device"` + PathOnHost string `json:"path_on_host"` +} + +type faultResponse struct { + FaultMessage string `json:"fault_message"` +} + +type instanceInfo struct { + AppName string `json:"app_name,omitempty"` + ID string `json:"id,omitempty"` + State string `json:"state,omitempty"` + VMMVersion string `json:"vmm_version,omitempty"` +} + +type machineConfigRequest struct { + MemSizeMib int64 `json:"mem_size_mib"` + Smt bool `json:"smt,omitempty"` + VcpuCount int64 `json:"vcpu_count"` +} + +type networkInterfaceRequest struct { + GuestMAC string `json:"guest_mac,omitempty"` + HostDevName string `json:"host_dev_name"` + IfaceID string `json:"iface_id"` +} + +type vsockRequest struct { + GuestCID int64 `json:"guest_cid"` + UDSPath string `json:"uds_path"` +} + +const defaultStartAction = "InstanceStart" + +func newAPIClient(socketPath string) *apiClient { + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", socketPath) + }, + } + + return &apiClient{ + httpClient: &http.Client{Transport: transport}, + socketPath: socketPath, + } +} + +func (c *apiClient) Ping(ctx context.Context) error { + var info instanceInfo + return c.do(ctx, http.MethodGet, "/", nil, &info, http.StatusOK) +} + +func (c *apiClient) PutAction(ctx context.Context, action string) error { + return c.do(ctx, http.MethodPut, "/actions", actionRequest{ActionType: action}, nil, http.StatusNoContent) +} + +func (c *apiClient) PutBootSource(ctx context.Context, spec MachineSpec) error { + body := bootSourceRequest{KernelImagePath: spec.KernelImagePath} + if value := strings.TrimSpace(spec.KernelArgs); value != "" { + body.BootArgs = value + } + return c.do(ctx, http.MethodPut, "/boot-source", body, nil, http.StatusNoContent) +} + +func (c *apiClient) PutDrive(ctx context.Context, drive driveRequest) error { + endpoint := "/drives/" + url.PathEscape(drive.DriveID) + return c.do(ctx, http.MethodPut, endpoint, drive, nil, http.StatusNoContent) +} + +func (c *apiClient) PutMachineConfig(ctx context.Context, spec MachineSpec) error { + body := machineConfigRequest{ + MemSizeMib: spec.MemoryMiB, + Smt: false, + VcpuCount: spec.VCPUs, + } + return c.do(ctx, http.MethodPut, "/machine-config", body, nil, http.StatusNoContent) +} + +func (c *apiClient) PutNetworkInterface(ctx context.Context, network NetworkAllocation) error { + body := networkInterfaceRequest{ + GuestMAC: network.GuestMAC, + HostDevName: network.TapName, + IfaceID: network.InterfaceID, + } + endpoint := "/network-interfaces/" + url.PathEscape(network.InterfaceID) + return c.do(ctx, http.MethodPut, endpoint, body, nil, http.StatusNoContent) +} + +func (c *apiClient) PutVsock(ctx context.Context, spec VsockSpec) error { + body := vsockRequest{ + GuestCID: int64(spec.CID), + UDSPath: spec.Path, + } + return c.do(ctx, http.MethodPut, "/vsock", body, nil, http.StatusNoContent) +} + +func (c *apiClient) do(ctx context.Context, method string, endpoint string, input any, output any, wantStatus int) error { + var body io.Reader + if input != nil { + payload, err := json.Marshal(input) + if err != nil { + return fmt.Errorf("marshal %s %s request: %w", method, endpoint, err) + } + body = bytes.NewReader(payload) + } + + request, err := http.NewRequestWithContext(ctx, method, "http://firecracker"+endpoint, body) + if err != nil { + return fmt.Errorf("build %s %s request: %w", method, endpoint, err) + } + if input != nil { + request.Header.Set("Content-Type", "application/json") + } + + response, err := c.httpClient.Do(request) + if err != nil { + return fmt.Errorf("do %s %s via %q: %w", method, endpoint, c.socketPath, err) + } + defer response.Body.Close() + + if response.StatusCode != wantStatus { + return decodeFirecrackerError(method, endpoint, response) + } + if output == nil { + _, _ = io.Copy(io.Discard, response.Body) + return nil + } + if err := json.NewDecoder(response.Body).Decode(output); err != nil { + return fmt.Errorf("decode %s %s response: %w", method, endpoint, err) + } + return nil +} + +func decodeFirecrackerError(method string, endpoint string, response *http.Response) error { + payload, _ := io.ReadAll(response.Body) + var fault faultResponse + if err := json.Unmarshal(payload, &fault); err == nil && strings.TrimSpace(fault.FaultMessage) != "" { + return fmt.Errorf("%s %s: status %d: %s", method, endpoint, response.StatusCode, strings.TrimSpace(fault.FaultMessage)) + } + + message := strings.TrimSpace(string(payload)) + if message == "" { + message = response.Status + } + return fmt.Errorf("%s %s: status %d: %s", method, endpoint, response.StatusCode, message) +} diff --git a/internal/firecracker/doc.go b/internal/firecracker/doc.go index 0d411cc..c7fce26 100644 --- a/internal/firecracker/doc.go +++ b/internal/firecracker/doc.go @@ -1,4 +1,2 @@ -// the Firecracker Go SDK for microagentcomputer. -// its supposed to be very thin and the main point is to abstract jailer away and build new -// primitives for agentcomputer +// Package firecracker contains the thin host-local Firecracker integration for microagentcomputer. package firecracker diff --git a/internal/firecracker/launch.go b/internal/firecracker/launch.go new file mode 100644 index 0000000..040a14b --- /dev/null +++ b/internal/firecracker/launch.go @@ -0,0 +1,203 @@ +package firecracker + +import ( + "context" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +const ( + defaultCgroupVersion = "2" + defaultFirecrackerInitTimeout = 3 * time.Second + defaultFirecrackerPollInterval = 10 * time.Millisecond + defaultRootDriveID = "root_drive" + defaultVSockRunDir = "/run" +) + +func configureMachine(ctx context.Context, client *apiClient, spec MachineSpec, network NetworkAllocation) error { + if err := client.PutMachineConfig(ctx, spec); err != nil { + return fmt.Errorf("put machine config: %w", err) + } + if err := client.PutBootSource(ctx, spec); err != nil { + return fmt.Errorf("put boot source: %w", err) + } + for _, drive := range additionalDriveRequests(spec) { + if err := client.PutDrive(ctx, drive); err != nil { + return fmt.Errorf("put drive %q: %w", drive.DriveID, err) + } + } + if err := client.PutDrive(ctx, rootDriveRequest(spec)); err != nil { + return fmt.Errorf("put root drive: %w", err) + } + if err := client.PutNetworkInterface(ctx, network); err != nil { + return fmt.Errorf("put network interface: %w", err) + } + if spec.Vsock != nil { + if err := client.PutVsock(ctx, *spec.Vsock); err != nil { + return fmt.Errorf("put vsock: %w", err) + } + } + if err := client.PutAction(ctx, defaultStartAction); err != nil { + return fmt.Errorf("start instance: %w", err) + } + return nil +} + +func launchJailedFirecracker(paths machinePaths, machineID MachineID, firecrackerBinaryPath string, jailerBinaryPath string) (*exec.Cmd, error) { + command := exec.Command( + jailerBinaryPath, + "--id", string(machineID), + "--uid", strconv.Itoa(os.Getuid()), + "--gid", strconv.Itoa(os.Getgid()), + "--exec-file", firecrackerBinaryPath, + "--cgroup-version", defaultCgroupVersion, + "--chroot-base-dir", paths.JailerBaseDir, + "--", + "--api-sock", defaultFirecrackerSocketPath, + ) + command.Stdout = os.Stderr + command.Stderr = os.Stderr + if err := command.Start(); err != nil { + return nil, fmt.Errorf("start jailer: %w", err) + } + return command, nil +} + +func stageMachineFiles(spec MachineSpec, paths machinePaths) (MachineSpec, error) { + staged := spec + + kernelImagePath, err := stagedFileName(spec.KernelImagePath) + if err != nil { + return MachineSpec{}, fmt.Errorf("kernel image path: %w", err) + } + if err := linkMachineFile(spec.KernelImagePath, filepath.Join(paths.ChrootRootDir, kernelImagePath)); err != nil { + return MachineSpec{}, fmt.Errorf("link kernel image into jail: %w", err) + } + staged.KernelImagePath = kernelImagePath + + rootFSPath, err := stagedFileName(spec.RootFSPath) + if err != nil { + return MachineSpec{}, fmt.Errorf("rootfs path: %w", err) + } + if err := linkMachineFile(spec.RootFSPath, filepath.Join(paths.ChrootRootDir, rootFSPath)); err != nil { + return MachineSpec{}, fmt.Errorf("link rootfs into jail: %w", err) + } + staged.RootFSPath = rootFSPath + + staged.Drives = make([]DriveSpec, len(spec.Drives)) + for i, drive := range spec.Drives { + stagedDrive := drive + stagedDrivePath, err := stagedFileName(drive.Path) + if err != nil { + return MachineSpec{}, fmt.Errorf("drive %q path: %w", drive.ID, err) + } + if err := linkMachineFile(drive.Path, filepath.Join(paths.ChrootRootDir, stagedDrivePath)); err != nil { + return MachineSpec{}, fmt.Errorf("link drive %q into jail: %w", drive.ID, err) + } + stagedDrive.Path = stagedDrivePath + staged.Drives[i] = stagedDrive + } + + if spec.Vsock != nil { + vsock := *spec.Vsock + vsock.Path = jailedVSockPath(spec) + staged.Vsock = &vsock + } + + return staged, nil +} + +func waitForSocket(ctx context.Context, client *apiClient, socketPath string) error { + waitContext, cancel := context.WithTimeout(ctx, defaultFirecrackerInitTimeout) + defer cancel() + + ticker := time.NewTicker(defaultFirecrackerPollInterval) + defer ticker.Stop() + + for { + select { + case <-waitContext.Done(): + return waitContext.Err() + case <-ticker.C: + if _, err := os.Stat(socketPath); err != nil { + if os.IsNotExist(err) { + continue + } + return fmt.Errorf("stat socket %q: %w", socketPath, err) + } + if err := client.Ping(waitContext); err != nil { + continue + } + return nil + } + } +} + +func additionalDriveRequests(spec MachineSpec) []driveRequest { + requests := make([]driveRequest, 0, len(spec.Drives)) + for _, drive := range spec.Drives { + requests = append(requests, driveRequest{ + DriveID: drive.ID, + IsReadOnly: drive.ReadOnly, + IsRootDevice: false, + PathOnHost: drive.Path, + }) + } + return requests +} + +func cleanupStartedProcess(command *exec.Cmd) { + if command == nil || command.Process == nil { + return + } + _ = command.Process.Kill() + _ = command.Wait() +} + +func hostVSockPath(paths machinePaths, spec MachineSpec) string { + if spec.Vsock == nil { + return "" + } + return filepath.Join(paths.ChrootRootDir, defaultFirecrackerSocketDir, filepath.Base(strings.TrimSpace(spec.Vsock.Path))) +} + +func jailedVSockPath(spec MachineSpec) string { + if spec.Vsock == nil { + return "" + } + return path.Join(defaultVSockRunDir, filepath.Base(strings.TrimSpace(spec.Vsock.Path))) +} + +func linkMachineFile(source string, target string) error { + resolvedSource, err := filepath.EvalSymlinks(source) + if err != nil { + return err + } + if err := os.Link(resolvedSource, target); err != nil { + return err + } + return nil +} + +func rootDriveRequest(spec MachineSpec) driveRequest { + return driveRequest{ + DriveID: defaultRootDriveID, + IsReadOnly: false, + IsRootDevice: true, + PathOnHost: spec.RootFSPath, + } +} + +func stagedFileName(filePath string) (string, error) { + name := filepath.Base(strings.TrimSpace(filePath)) + if name == "" || name == "." || name == string(filepath.Separator) { + return "", fmt.Errorf("file path is required") + } + return name, nil +} diff --git a/internal/firecracker/paths.go b/internal/firecracker/paths.go index 3d1bd98..643600b 100644 --- a/internal/firecracker/paths.go +++ b/internal/firecracker/paths.go @@ -6,15 +6,18 @@ import ( "strings" ) -const defaultSocketName = "api.socket" +const ( + defaultChrootRootDirName = "root" + defaultFirecrackerSocketDir = "run" + defaultFirecrackerSocketName = "firecracker.socket" + defaultFirecrackerSocketPath = "/run/firecracker.socket" +) type machinePaths struct { - BaseDir string - JailerBaseDir string - ChrootRootDir string - SocketName string - SocketPath string - FirecrackerBin string + BaseDir string + ChrootRootDir string + JailerBaseDir string + SocketPath string } func buildMachinePaths(rootDir string, id MachineID, firecrackerBinaryPath string) (machinePaths, error) { @@ -25,6 +28,7 @@ func buildMachinePaths(rootDir string, id MachineID, firecrackerBinaryPath strin if strings.TrimSpace(string(id)) == "" { return machinePaths{}, fmt.Errorf("machine id is required") } + binName := filepath.Base(strings.TrimSpace(firecrackerBinaryPath)) if binName == "." || binName == string(filepath.Separator) || binName == "" { return machinePaths{}, fmt.Errorf("firecracker binary path is required") @@ -32,14 +36,12 @@ func buildMachinePaths(rootDir string, id MachineID, firecrackerBinaryPath strin baseDir := filepath.Join(rootDir, "machines", string(id)) jailerBaseDir := filepath.Join(baseDir, "jailer") - chrootRootDir := filepath.Join(jailerBaseDir, binName, string(id), "root") + chrootRootDir := filepath.Join(jailerBaseDir, binName, string(id), defaultChrootRootDirName) return machinePaths{ - BaseDir: baseDir, - JailerBaseDir: jailerBaseDir, - ChrootRootDir: chrootRootDir, - SocketName: defaultSocketName, - SocketPath: filepath.Join(chrootRootDir, defaultSocketName), - FirecrackerBin: binName, + BaseDir: baseDir, + ChrootRootDir: chrootRootDir, + JailerBaseDir: jailerBaseDir, + SocketPath: filepath.Join(chrootRootDir, defaultFirecrackerSocketDir, defaultFirecrackerSocketName), }, nil } diff --git a/internal/firecracker/runtime.go b/internal/firecracker/runtime.go index b7e1f22..05a09a9 100644 --- a/internal/firecracker/runtime.go +++ b/internal/firecracker/runtime.go @@ -5,25 +5,22 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "strings" "sync" + "syscall" "time" - - sdk "github.com/firecracker-microvm/firecracker-go-sdk" ) -// ErrMachineNotFound is returned when the runtime does not know a machine ID. var ErrMachineNotFound = errors.New("machine not found") -// RuntimeConfig configures the host-local Firecracker runtime wrapper. type RuntimeConfig struct { RootDir string FirecrackerBinaryPath string JailerBinaryPath string } -// Runtime manages local Firecracker machines on a single host. type Runtime struct { rootDir string firecrackerBinaryPath string @@ -36,20 +33,21 @@ type Runtime struct { } type managedMachine struct { - spec MachineSpec - network NetworkAllocation - paths machinePaths - machine *sdk.Machine - state MachineState + cmd *exec.Cmd + entered bool + exited chan struct{} + network NetworkAllocation + paths machinePaths + spec MachineSpec + state MachineState + stopping bool } const ( defaultVSockCIDStart = uint32(3) - defaultVSockDirName = "vsock" defaultVSockID = "vsock0" ) -// NewRuntime creates a new host-local Firecracker runtime wrapper. func NewRuntime(cfg RuntimeConfig) (*Runtime, error) { rootDir := filepath.Clean(strings.TrimSpace(cfg.RootDir)) if rootDir == "." || rootDir == "" { @@ -69,9 +67,6 @@ func NewRuntime(cfg RuntimeConfig) (*Runtime, error) { if err := os.MkdirAll(rootDir, 0o755); err != nil { return nil, fmt.Errorf("create runtime root dir %q: %w", rootDir, err) } - if err := os.MkdirAll(filepath.Join(rootDir, defaultVSockDirName), 0o755); err != nil { - return nil, fmt.Errorf("create runtime vsock dir: %w", err) - } allocator, err := NewNetworkAllocator(defaultNetworkCIDR) if err != nil { @@ -88,7 +83,6 @@ func NewRuntime(cfg RuntimeConfig) (*Runtime, error) { }, nil } -// Boot provisions host resources and starts a new jailed Firecracker process. func (r *Runtime) Boot(ctx context.Context, spec MachineSpec) (*MachineState, error) { if err := spec.Validate(); err != nil { return nil, err @@ -124,12 +118,14 @@ func (r *Runtime) Boot(ctx context.Context, spec MachineSpec) (*MachineState, er ID: spec.ID, Phase: PhaseProvisioning, }, + entered: true, } r.mu.Unlock() - cleanup := func(network NetworkAllocation, paths machinePaths) { + cleanup := func(network NetworkAllocation, paths machinePaths, command *exec.Cmd) { + cleanupStartedProcess(command) _ = r.networkProvisioner.Remove(context.Background(), network) - _ = removeIfExists(vsockPath(spec)) + _ = removeIfExists(hostVSockPath(paths, spec)) if paths.BaseDir != "" { _ = os.RemoveAll(paths.BaseDir) } @@ -140,67 +136,77 @@ func (r *Runtime) Boot(ctx context.Context, spec MachineSpec) (*MachineState, er network, err := r.networkAllocator.Allocate(usedNetworks) if err != nil { - cleanup(NetworkAllocation{}, machinePaths{}) + cleanup(NetworkAllocation{}, machinePaths{}, nil) return nil, err } paths, err := buildMachinePaths(r.rootDir, spec.ID, r.firecrackerBinaryPath) if err != nil { - cleanup(network, machinePaths{}) + cleanup(network, machinePaths{}, nil) return nil, err } if err := os.MkdirAll(paths.JailerBaseDir, 0o755); err != nil { - cleanup(network, paths) + cleanup(network, paths, nil) return nil, fmt.Errorf("create machine jailer dir %q: %w", paths.JailerBaseDir, err) } if err := r.networkProvisioner.Ensure(ctx, network); err != nil { - cleanup(network, paths) + cleanup(network, paths, nil) return nil, err } - cfg, err := buildSDKConfig(spec, paths, network, r.firecrackerBinaryPath, r.jailerBinaryPath) + command, err := launchJailedFirecracker(paths, spec.ID, r.firecrackerBinaryPath, r.jailerBinaryPath) if err != nil { - cleanup(network, paths) + cleanup(network, paths, nil) return nil, err } - machine, err := sdk.NewMachine(ctx, cfg) - if err != nil { - cleanup(network, paths) - return nil, fmt.Errorf("create firecracker machine: %w", err) - } - if err := machine.Start(ctx); err != nil { - cleanup(network, paths) - return nil, fmt.Errorf("start firecracker machine: %w", err) + client := newAPIClient(paths.SocketPath) + if err := waitForSocket(ctx, client, paths.SocketPath); err != nil { + cleanup(network, paths, command) + return nil, fmt.Errorf("wait for firecracker socket: %w", err) + } + + jailedSpec, err := stageMachineFiles(spec, paths) + if err != nil { + cleanup(network, paths, command) + return nil, err + } + if err := configureMachine(ctx, client, jailedSpec, network); err != nil { + cleanup(network, paths, command) + return nil, err + } + + pid := 0 + if command.Process != nil { + pid = command.Process.Pid } - pid, _ := machine.PID() now := time.Now().UTC() state := MachineState{ ID: spec.ID, Phase: PhaseRunning, PID: pid, RuntimeHost: network.GuestIP().String(), - SocketPath: machine.Cfg.SocketPath, + SocketPath: paths.SocketPath, TapName: network.TapName, StartedAt: &now, } r.mu.Lock() entry := r.machines[spec.ID] + entry.cmd = command + entry.exited = make(chan struct{}) entry.network = network entry.paths = paths - entry.machine = machine entry.state = state r.mu.Unlock() - go r.watchMachine(spec.ID, machine) + go r.watchMachine(spec.ID, command, entry.exited) out := state return &out, nil } -// Inspect returns the currently known state for a machine. func (r *Runtime) Inspect(id MachineID) (*MachineState, error) { r.mu.RLock() entry, ok := r.machines[id] @@ -210,22 +216,13 @@ func (r *Runtime) Inspect(id MachineID) (*MachineState, error) { } state := entry.state - if entry.machine != nil { - pid, err := entry.machine.PID() - if err != nil { - if state.Phase == PhaseRunning { - state.Phase = PhaseStopped - state.PID = 0 - } - } else { - state.PID = pid - } + if state.PID > 0 && !processExists(state.PID) { + state.Phase = PhaseStopped + state.PID = 0 } - return &state, nil } -// Stop terminates a running Firecracker process and updates local state. func (r *Runtime) Stop(ctx context.Context, id MachineID) error { r.mu.RLock() entry, ok := r.machines[id] @@ -233,34 +230,31 @@ func (r *Runtime) Stop(ctx context.Context, id MachineID) error { if !ok || entry == nil { return ErrMachineNotFound } - if entry.machine == nil { + if entry.cmd == nil || entry.cmd.Process == nil { return fmt.Errorf("machine %q has no firecracker process", id) } - - errCh := make(chan error, 1) - go func() { - errCh <- entry.machine.StopVMM() - }() - - select { - case err := <-errCh: - if err != nil { - return fmt.Errorf("stop machine %q: %w", id, err) - } - case <-ctx.Done(): - return ctx.Err() + if entry.state.Phase == PhaseStopped { + return nil } r.mu.Lock() - entry.state.Phase = PhaseStopped - entry.state.PID = 0 - entry.state.Error = "" + entry.stopping = true + process := entry.cmd.Process + exited := entry.exited r.mu.Unlock() - return nil + if err := process.Signal(syscall.SIGTERM); err != nil && !errors.Is(err, os.ErrProcessDone) { + return fmt.Errorf("stop machine %q: %w", id, err) + } + + select { + case <-exited: + return nil + case <-ctx.Done(): + return ctx.Err() + } } -// Delete stops a machine if necessary and removes its local resources. func (r *Runtime) Delete(ctx context.Context, id MachineID) error { r.mu.RLock() entry, ok := r.machines[id] @@ -269,7 +263,7 @@ func (r *Runtime) Delete(ctx context.Context, id MachineID) error { return ErrMachineNotFound } - if entry.machine != nil { + if entry.state.Phase == PhaseRunning { if err := r.Stop(ctx, id); err != nil && !errors.Is(err, context.Canceled) { return err } @@ -277,7 +271,7 @@ func (r *Runtime) Delete(ctx context.Context, id MachineID) error { if err := r.networkProvisioner.Remove(ctx, entry.network); err != nil { return err } - if err := removeIfExists(vsockPath(entry.spec)); err != nil { + if err := removeIfExists(hostVSockPath(entry.paths, entry.spec)); err != nil { return err } if err := os.RemoveAll(entry.paths.BaseDir); err != nil { @@ -290,27 +284,32 @@ func (r *Runtime) Delete(ctx context.Context, id MachineID) error { return nil } -func (r *Runtime) watchMachine(id MachineID, machine *sdk.Machine) { - err := machine.Wait(context.Background()) +func (r *Runtime) watchMachine(id MachineID, command *exec.Cmd, exited chan struct{}) { + err := command.Wait() + close(exited) r.mu.Lock() defer r.mu.Unlock() entry, ok := r.machines[id] - if !ok || entry == nil || entry.machine != machine { + if !ok || entry == nil || entry.cmd != command { return } entry.state.PID = 0 + if entry.stopping { + entry.state.Phase = PhaseStopped + entry.state.Error = "" + entry.stopping = false + return + } if err != nil { entry.state.Phase = PhaseError entry.state.Error = err.Error() return } - if entry.state.Phase != PhaseStopped { - entry.state.Phase = PhaseStopped - } + entry.state.Phase = PhaseStopped entry.state.Error = "" } @@ -330,7 +329,7 @@ func (r *Runtime) resolveVSock(spec MachineSpec, used map[uint32]struct{}) (Mach spec.Vsock = &VsockSpec{ ID: defaultVSockID, CID: cid, - Path: filepath.Join(r.rootDir, defaultVSockDirName, string(spec.ID)+".sock"), + Path: string(spec.ID) + ".sock", } return spec, nil } @@ -344,11 +343,12 @@ func nextVSockCID(used map[uint32]struct{}) (uint32, error) { return 0, fmt.Errorf("vsock cid space exhausted") } -func vsockPath(spec MachineSpec) string { - if spec.Vsock == nil { - return "" +func processExists(pid int) bool { + if pid < 1 { + return false } - return strings.TrimSpace(spec.Vsock.Path) + err := syscall.Kill(pid, 0) + return err == nil || err == syscall.EPERM } func removeIfExists(path string) error { diff --git a/internal/firecracker/sdk.go b/internal/firecracker/sdk.go deleted file mode 100644 index 0d49471..0000000 --- a/internal/firecracker/sdk.go +++ /dev/null @@ -1,92 +0,0 @@ -package firecracker - -import ( - "fmt" - "net" - "net/netip" - "os" - "path/filepath" - "strings" - - sdk "github.com/firecracker-microvm/firecracker-go-sdk" - models "github.com/firecracker-microvm/firecracker-go-sdk/client/models" -) - -const defaultNumaNode = 0 - -func buildSDKConfig(spec MachineSpec, paths machinePaths, network NetworkAllocation, firecrackerBinaryPath string, jailerBinaryPath string) (sdk.Config, error) { - if err := spec.Validate(); err != nil { - return sdk.Config{}, err - } - - firecrackerBinaryPath = strings.TrimSpace(firecrackerBinaryPath) - if firecrackerBinaryPath == "" { - return sdk.Config{}, fmt.Errorf("firecracker binary path is required") - } - - jailerBinaryPath = strings.TrimSpace(jailerBinaryPath) - if jailerBinaryPath == "" { - return sdk.Config{}, fmt.Errorf("jailer binary path is required") - } - - drives := sdk.NewDrivesBuilder(spec.RootFSPath) - for _, drive := range spec.Drives { - drives = drives.AddDrive( - drive.Path, - drive.ReadOnly, - sdk.WithDriveID(strings.TrimSpace(drive.ID)), - ) - } - - cfg := sdk.Config{ - KernelImagePath: spec.KernelImagePath, - KernelArgs: strings.TrimSpace(spec.KernelArgs), - Drives: drives.Build(), - NetworkInterfaces: sdk.NetworkInterfaces{{ - StaticConfiguration: &sdk.StaticNetworkConfiguration{ - HostDevName: network.TapName, - MacAddress: network.GuestMAC, - IPConfiguration: &sdk.IPConfiguration{ - IPAddr: toIPNet(network.GuestCIDR), - Gateway: net.ParseIP(network.GatewayIP.String()), - Nameservers: nil, - }, - }, - }}, - MachineCfg: models.MachineConfiguration{ - VcpuCount: sdk.Int64(spec.VCPUs), - MemSizeMib: sdk.Int64(spec.MemoryMiB), - Smt: sdk.Bool(false), - }, - JailerCfg: &sdk.JailerConfig{ - GID: sdk.Int(os.Getgid()), - UID: sdk.Int(os.Getuid()), - ID: string(spec.ID), - NumaNode: sdk.Int(defaultNumaNode), - ExecFile: firecrackerBinaryPath, - JailerBinary: jailerBinaryPath, - ChrootBaseDir: paths.JailerBaseDir, - ChrootStrategy: sdk.NewNaiveChrootStrategy(filepath.Clean(spec.KernelImagePath)), - CgroupVersion: "2", - }, - VMID: string(spec.ID), - } - - if spec.Vsock != nil { - cfg.VsockDevices = []sdk.VsockDevice{{ - ID: spec.Vsock.ID, - Path: spec.Vsock.Path, - CID: spec.Vsock.CID, - }} - } - - return cfg, nil -} - -func toIPNet(prefix netip.Prefix) net.IPNet { - bits := prefix.Bits() - return net.IPNet{ - IP: net.ParseIP(prefix.Addr().String()), - Mask: net.CIDRMask(bits, 32), - } -}