computer-host/internal/firecracker/launch.go
Hari e2f9e54970 host daemon (#2)
* feat: host daemon api scaffold

* fix: use sparse writes

* fix: unix socket length (<108 bytes)
2026-04-08 11:23:19 -04:00

217 lines
6.1 KiB
Go

package firecracker
import (
"context"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"time"
)
const (
defaultCgroupVersion = "2"
defaultFirecrackerInitTimeout = 10 * 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()
var lastStatErr error
var lastPingErr error
for {
select {
case <-waitContext.Done():
switch {
case lastPingErr != nil:
return fmt.Errorf("%w (socket=%q last_ping_err=%v)", waitContext.Err(), socketPath, lastPingErr)
case lastStatErr != nil:
return fmt.Errorf("%w (socket=%q last_stat_err=%v)", waitContext.Err(), socketPath, lastStatErr)
default:
return fmt.Errorf("%w (socket=%q)", waitContext.Err(), socketPath)
}
case <-ticker.C:
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
lastStatErr = err
continue
}
return fmt.Errorf("stat socket %q: %w", socketPath, err)
}
lastStatErr = nil
if err := client.Ping(waitContext); err != nil {
lastPingErr = err
continue
}
lastPingErr = nil
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
}