mirror of
https://github.com/getcompanion-ai/computer-host.git
synced 2026-04-15 03:00:42 +00:00
297 lines
8.3 KiB
Go
297 lines
8.3 KiB
Go
package firecracker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultCgroupVersion = "2"
|
|
defaultFirecrackerInitTimeout = 10 * time.Second
|
|
defaultFirecrackerLogLevel = "Warning"
|
|
defaultFirecrackerPollInterval = 10 * time.Millisecond
|
|
defaultRootDriveID = "root_drive"
|
|
defaultVSockRunDir = "/run"
|
|
)
|
|
|
|
func configureMachine(ctx context.Context, client *apiClient, paths machinePaths, 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.MMDS != nil {
|
|
if err := client.PutMMDSConfig(ctx, *spec.MMDS); err != nil {
|
|
return fmt.Errorf("put mmds config: %w", err)
|
|
}
|
|
if spec.MMDS.Data != nil {
|
|
if err := client.PutMMDS(ctx, spec.MMDS.Data); err != nil {
|
|
return fmt.Errorf("put mmds payload: %w", err)
|
|
}
|
|
}
|
|
}
|
|
if err := client.PutEntropy(ctx); err != nil {
|
|
return fmt.Errorf("put entropy device: %w", err)
|
|
}
|
|
if err := client.PutSerial(ctx, paths.JailedSerialLogPath); err != nil {
|
|
return fmt.Errorf("put serial device: %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, enablePCI bool, configFilePath string) (*exec.Cmd, error) {
|
|
args := []string{
|
|
"--id", string(machineID),
|
|
"--uid", strconv.Itoa(os.Getuid()),
|
|
"--gid", strconv.Itoa(os.Getgid()),
|
|
"--exec-file", firecrackerBinaryPath,
|
|
"--cgroup-version", defaultCgroupVersion,
|
|
"--chroot-base-dir", paths.JailerBaseDir,
|
|
"--daemonize",
|
|
"--new-pid-ns",
|
|
"--",
|
|
"--api-sock", defaultFirecrackerSocketPath,
|
|
}
|
|
if configFilePath != "" {
|
|
args = append(args, "--config-file", configFilePath)
|
|
} else {
|
|
args = append(args,
|
|
"--log-path", paths.JailedFirecrackerLogPath,
|
|
"--level", defaultFirecrackerLogLevel,
|
|
"--show-level",
|
|
"--show-log-origin",
|
|
)
|
|
}
|
|
if enablePCI {
|
|
args = append(args, "--enable-pci")
|
|
}
|
|
command := exec.Command(jailerBinaryPath, args...)
|
|
if err := command.Start(); err != nil {
|
|
return nil, fmt.Errorf("start jailer: %w", err)
|
|
}
|
|
go func() {
|
|
_ = command.Wait()
|
|
}()
|
|
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("root drive path: %w", err)
|
|
}
|
|
if err := linkMachineFile(spec.RootFSPath, filepath.Join(paths.ChrootRootDir, rootFSPath)); err != nil {
|
|
return MachineSpec{}, fmt.Errorf("link root drive into jail: %w", err)
|
|
}
|
|
staged.RootFSPath = rootFSPath
|
|
staged.RootDrive = spec.rootDrive()
|
|
staged.RootDrive.Path = 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 = jailedVSockDevicePath(*spec.Vsock)
|
|
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
|
|
}
|
|
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,
|
|
CacheType: drive.CacheType,
|
|
IOEngine: drive.IOEngine,
|
|
})
|
|
}
|
|
return requests
|
|
}
|
|
|
|
func cleanupStartedProcess(command *exec.Cmd) {
|
|
if command == nil || command.Process == nil {
|
|
return
|
|
}
|
|
_ = command.Process.Kill()
|
|
}
|
|
|
|
func readPIDFile(pidFilePath string) (int, error) {
|
|
payload, err := os.ReadFile(pidFilePath)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
pid, err := strconv.Atoi(strings.TrimSpace(string(payload)))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parse pid file %q: %w", pidFilePath, err)
|
|
}
|
|
if pid < 1 {
|
|
return 0, fmt.Errorf("pid file %q must contain a positive pid", pidFilePath)
|
|
}
|
|
return pid, nil
|
|
}
|
|
|
|
func waitForPIDFile(ctx context.Context, pidFilePath string) (int, error) {
|
|
waitContext, cancel := context.WithTimeout(ctx, defaultFirecrackerInitTimeout)
|
|
defer cancel()
|
|
|
|
ticker := time.NewTicker(defaultFirecrackerPollInterval)
|
|
defer ticker.Stop()
|
|
|
|
var lastErr error
|
|
for {
|
|
select {
|
|
case <-waitContext.Done():
|
|
if lastErr != nil {
|
|
return 0, fmt.Errorf("%w (pid_file=%q last_err=%v)", waitContext.Err(), pidFilePath, lastErr)
|
|
}
|
|
return 0, fmt.Errorf("%w (pid_file=%q)", waitContext.Err(), pidFilePath)
|
|
case <-ticker.C:
|
|
pid, err := readPIDFile(pidFilePath)
|
|
if err == nil {
|
|
return pid, nil
|
|
}
|
|
lastErr = err
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func jailedVSockDevicePath(spec VsockSpec) string {
|
|
return path.Join(defaultVSockRunDir, filepath.Base(strings.TrimSpace(spec.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 {
|
|
root := spec.rootDrive()
|
|
return driveRequest{
|
|
DriveID: root.ID,
|
|
IsReadOnly: root.ReadOnly,
|
|
IsRootDevice: true,
|
|
PathOnHost: root.Path,
|
|
CacheType: root.CacheType,
|
|
IOEngine: root.IOEngine,
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func stageSnapshotFile(sourcePath string, chrootRootDir string, name string) (string, error) {
|
|
target := filepath.Join(chrootRootDir, name)
|
|
if err := linkMachineFile(sourcePath, target); err != nil {
|
|
return "", err
|
|
}
|
|
return name, nil
|
|
}
|