feat: nvme disk on m6

This commit is contained in:
Harivansh Rathi 2026-04-10 04:04:08 +00:00
parent 54a4c423a6
commit eb9d2a76df
9 changed files with 240 additions and 22 deletions

View file

@ -159,9 +159,11 @@ func (d *Daemon) buildMachineSpec(machineID contracthost.MachineID, artifact *mo
drives := make([]firecracker.DriveSpec, 0, len(userVolumes))
for i, volume := range userVolumes {
drives = append(drives, firecracker.DriveSpec{
ID: fmt.Sprintf("user-%d", i),
Path: volume.Path,
ReadOnly: false,
ID: fmt.Sprintf("user-%d", i),
Path: volume.Path,
ReadOnly: false,
CacheType: firecracker.DriveCacheTypeUnsafe,
IOEngine: d.config.DriveIOEngine,
})
}
@ -179,9 +181,9 @@ func (d *Daemon) buildMachineSpec(machineID contracthost.MachineID, artifact *mo
ID: "root_drive",
Path: systemVolumePath,
CacheType: firecracker.DriveCacheTypeUnsafe,
IOEngine: firecracker.DriveIOEngineSync,
IOEngine: d.config.DriveIOEngine,
},
KernelArgs: defaultGuestKernelArgs,
KernelArgs: guestKernelArgs(d.config.EnablePCI),
Drives: drives,
MMDS: mmds,
Vsock: guestVsockSpec(machineID),

View file

@ -8,21 +8,22 @@ import (
"sync"
"time"
contracthost "github.com/getcompanion-ai/computer-host/contract"
appconfig "github.com/getcompanion-ai/computer-host/internal/config"
"github.com/getcompanion-ai/computer-host/internal/firecracker"
"github.com/getcompanion-ai/computer-host/internal/model"
"github.com/getcompanion-ai/computer-host/internal/store"
contracthost "github.com/getcompanion-ai/computer-host/contract"
)
const (
defaultGuestKernelArgs = "console=ttyS0 reboot=k panic=1 pci=off"
defaultGuestMemoryMiB = int64(3072)
defaultGuestVCPUs = int64(2)
defaultSSHPort = uint16(2222)
defaultVNCPort = uint16(6080)
defaultCopyBufferSize = 1024 * 1024
defaultGuestDialTimeout = 500 * time.Millisecond
defaultGuestKernelArgs = "console=ttyS0 reboot=k panic=1"
defaultGuestKernelArgsNoPCI = defaultGuestKernelArgs + " pci=off"
defaultGuestMemoryMiB = int64(3072)
defaultGuestVCPUs = int64(2)
defaultSSHPort = uint16(2222)
defaultVNCPort = uint16(6080)
defaultCopyBufferSize = 1024 * 1024
defaultGuestDialTimeout = 500 * time.Millisecond
)
type Runtime interface {
@ -73,6 +74,9 @@ func New(cfg appconfig.Config, store store.Store, runtime Runtime) (*Daemon, err
return nil, fmt.Errorf("create daemon dir %q: %w", dir, err)
}
}
if err := validateDiskCloneBackend(cfg); err != nil {
return nil, err
}
daemon := &Daemon{
config: cfg,
store: store,
@ -127,3 +131,10 @@ func (d *Daemon) lockArtifact(key string) func() {
lock.Lock()
return lock.Unlock
}
func guestKernelArgs(enablePCI bool) string {
if enablePCI {
return defaultGuestKernelArgs
}
return defaultGuestKernelArgsNoPCI
}

View file

@ -758,6 +758,7 @@ func testConfig(root string) appconfig.Config {
SnapshotsDir: filepath.Join(root, "snapshots"),
RuntimeDir: filepath.Join(root, "runtime"),
DiskCloneMode: appconfig.DiskCloneModeCopy,
DriveIOEngine: firecracker.DriveIOEngineSync,
SocketPath: filepath.Join(root, "firecracker-host.sock"),
EgressInterface: "eth0",
FirecrackerBinaryPath: "/usr/bin/firecracker",
@ -765,6 +766,22 @@ func testConfig(root string) appconfig.Config {
}
}
func TestGuestKernelArgsDisablesPCIByDefault(t *testing.T) {
t.Parallel()
if got := guestKernelArgs(false); !strings.Contains(got, "pci=off") {
t.Fatalf("guestKernelArgs(false) = %q, want pci=off", got)
}
}
func TestGuestKernelArgsRemovesPCIOffWhenPCIEnabled(t *testing.T) {
t.Parallel()
if got := guestKernelArgs(true); strings.Contains(got, "pci=off") {
t.Fatalf("guestKernelArgs(true) = %q, want no pci=off", got)
}
}
func stubGuestSSHPublicKeyReader(hostDaemon *Daemon) {
hostDaemon.readGuestSSHPublicKey = func(context.Context, string) (string, error) {
return "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO0j1AyW0mQm9a1G2rY0R4fP2G5+4Qx2V3FJ9P2mA6N3", nil

View file

@ -97,7 +97,10 @@ func cloneFile(source string, target string) error {
func cloneDiskFile(source string, target string, mode appconfig.DiskCloneMode) error {
switch mode {
case appconfig.DiskCloneModeReflink:
return reflinkFile(source, target)
if err := reflinkFile(source, target); err != nil {
return reflinkRequiredError(err)
}
return nil
case appconfig.DiskCloneModeCopy:
return cloneFile(source, target)
default:
@ -105,6 +108,42 @@ func cloneDiskFile(source string, target string, mode appconfig.DiskCloneMode) e
}
}
func validateDiskCloneBackend(cfg appconfig.Config) error {
if cfg.DiskCloneMode != appconfig.DiskCloneModeReflink {
return nil
}
sourceFile, err := os.CreateTemp(cfg.ArtifactsDir, ".reflink-probe-source-*")
if err != nil {
return fmt.Errorf("create reflink probe source in %q: %w", cfg.ArtifactsDir, err)
}
sourcePath := sourceFile.Name()
defer func() {
_ = os.Remove(sourcePath)
}()
if _, err := sourceFile.WriteString("reflink-probe"); err != nil {
_ = sourceFile.Close()
return fmt.Errorf("write reflink probe source %q: %w", sourcePath, err)
}
if err := sourceFile.Close(); err != nil {
return fmt.Errorf("close reflink probe source %q: %w", sourcePath, err)
}
targetPath := filepath.Join(cfg.MachineDisksDir, "."+filepath.Base(sourcePath)+".target")
defer func() {
_ = os.Remove(targetPath)
}()
if err := cloneDiskFile(sourcePath, targetPath, cfg.DiskCloneMode); err != nil {
return fmt.Errorf("validate disk clone backend from artifacts dir %q to machine disks dir %q: %w", cfg.ArtifactsDir, cfg.MachineDisksDir, err)
}
return nil
}
func reflinkRequiredError(err error) error {
return fmt.Errorf("FIRECRACKER_HOST_DISK_CLONE_MODE=reflink requires a CoW filesystem with reflink support across artifacts and machine-disks; mount FIRECRACKER_HOST_ROOT_DIR on XFS with reflink=1 or Btrfs, preferably on local NVMe, or set FIRECRACKER_HOST_DISK_CLONE_MODE=copy for the slow full-copy fallback: %w", err)
}
func reflinkFile(source string, target string) error {
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return fmt.Errorf("create target dir for %q: %w", target, err)