diff --git a/cmd/lima-driver-krunkit/main.go b/cmd/lima-driver-krunkit/main.go new file mode 100644 index 00000000000..1fa75ba64b8 --- /dev/null +++ b/cmd/lima-driver-krunkit/main.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + "github.com/lima-vm/lima/v2/pkg/driver/external/server" + "github.com/lima-vm/lima/v2/pkg/driver/krunkit" +) + +// To be used as an external driver for Lima. +func main() { + server.Serve(context.Background(), krunkit.New()) +} diff --git a/pkg/driver/krunkit/krunkit_darwin.go b/pkg/driver/krunkit/krunkit_darwin.go new file mode 100644 index 00000000000..e028854904d --- /dev/null +++ b/pkg/driver/krunkit/krunkit_darwin.go @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package krunkit + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + + "github.com/docker/go-units" + "github.com/lima-vm/lima/v2/pkg/driver/vz" + "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" + "github.com/lima-vm/lima/v2/pkg/iso9660util" + "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/lima-vm/lima/v2/pkg/networks" + "github.com/lima-vm/lima/v2/pkg/networks/usernet" + "github.com/sirupsen/logrus" +) + +const logLevelInfo = "3" + +// Cmdline constructs the command line arguments for krunkit based on the instance configuration. +func Cmdline(inst *limatype.Instance) (*exec.Cmd, error) { + var args = []string{ + "--memory", strconv.Itoa(2048), + "--cpus", fmt.Sprintf("%d", *inst.Config.CPUs), + "--device", fmt.Sprintf("virtio-serial,logFilePath=%s", filepath.Join(inst.Dir, filenames.SerialLog)), + "--krun-log-level", logLevelInfo, + "--restful-uri", "none://", + + // First virtio-blk device is the boot disk + "--device", fmt.Sprintf("virtio-blk,path=%s,format=raw", filepath.Join(inst.Dir, filenames.DiffDisk)), + "--device", fmt.Sprintf("virtio-blk,path=%s", filepath.Join(inst.Dir, filenames.CIDataISO)), + } + + networkArgs, err := buildNetworkArgs(inst) + if err != nil { + return nil, fmt.Errorf("failed to build network arguments: %w", err) + } + + if *inst.Config.MountType == limatype.VIRTIOFS { + for i, mount := range inst.Config.Mounts { + logrus.Infof("Mount: %+v", mount) + if _, err := os.Stat(mount.Location); errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(mount.Location, 0o750); err != nil { + return nil, err + } + } + tag := fmt.Sprintf("mount%d", i) + mountArg := fmt.Sprintf("virtio-fs,sharedDir=%s,mountTag=%s", mount.Location, tag) + args = append(args, "--device", mountArg) + } + } + + args = append(args, networkArgs...) + cmd := exec.CommandContext(context.Background(), "krunkit", args...) + + return cmd, nil +} + +func buildNetworkArgs(inst *limatype.Instance) ([]string, error) { + var args []string + + // Configure default usernetwork with limayaml.MACAddress(inst.Dir) for eth0 interface + firstUsernetIndex := limayaml.FirstUsernetIndex(inst.Config) + if firstUsernetIndex == -1 { + // slirp network using gvisor netstack + krunkitSock, err := usernet.SockWithDirectory(inst.Dir, "", usernet.FDSock) + if err != nil { + return nil, err + } + client, err := vz.PassFDToUnix(krunkitSock) + if err != nil { + return nil, err + } + + args = append(args, "--device", fmt.Sprintf("virtio-net,type=unixgram,fd=%d,mac=%s", client.Fd(), limayaml.MACAddress(inst.Dir))) + } + + for _, nw := range inst.Networks { + var sock string + var mac string + if nw.Lima != "" { + nwCfg, err := networks.LoadConfig() + if err != nil { + return nil, err + } + switch nw.Lima { + case networks.ModeUserV2: + sock, err = usernet.Sock(nw.Lima, usernet.QEMUSock) + if err != nil { + return nil, err + } + mac = limayaml.MACAddress(inst.Dir) + case networks.ModeShared, networks.ModeBridged: + socketVMNetInstalled, err := nwCfg.IsDaemonInstalled(networks.SocketVMNet) + if err != nil { + return nil, err + } + if !socketVMNetInstalled { + return nil, errors.New("socket_vmnet is not installed") + } + sock, err = networks.Sock(nw.Lima) + if err != nil { + return nil, err + } + mac = nw.MACAddress + default: + return nil, fmt.Errorf("invalid network spec %+v", nw) + } + } else if nw.Socket != "" { + sock = nw.Socket + mac = nw.MACAddress + } else { + return nil, fmt.Errorf("invalid network spec %+v", nw) + } + + device := fmt.Sprintf("virtio-net,type=unixstream,path=%s,mac=%s", sock, mac) + args = append(args, "--device", device) + } + + if len(args) == 0 { + return args, fmt.Errorf("no socket_vment networks defined") + } + + return args, nil +} + +func EnsureDisk(ctx context.Context, inst *limatype.Instance) error { + diffDisk := filepath.Join(inst.Dir, filenames.DiffDisk) + if _, err := os.Stat(diffDisk); err == nil || !errors.Is(err, os.ErrNotExist) { + // disk is already ensured + return err + } + + diskUtil := proxyimgutil.NewDiskUtil(ctx) + + baseDisk := filepath.Join(inst.Dir, filenames.BaseDisk) + + diskSize, _ := units.RAMInBytes(*inst.Config.Disk) + if diskSize == 0 { + return nil + } + isBaseDiskISO, err := iso9660util.IsISO9660(baseDisk) + if err != nil { + return err + } + if isBaseDiskISO { + // Create an empty data volume (sparse) + diffDiskF, err := os.Create(diffDisk) + if err != nil { + return err + } + + err = diskUtil.MakeSparse(ctx, diffDiskF, 0) + if err != nil { + diffDiskF.Close() + return fmt.Errorf("failed to create sparse diff disk %q: %w", diffDisk, err) + } + return diffDiskF.Close() + } + + // Krunkit also supports qcow2 disks but raw is faster to create and use. + if err = diskUtil.ConvertToRaw(ctx, baseDisk, diffDisk, &diskSize, false); err != nil { + return fmt.Errorf("failed to convert %q to a raw disk %q: %w", baseDisk, diffDisk, err) + } + return err +} + +func startUsernet(ctx context.Context, inst *limatype.Instance) (*usernet.Client, context.CancelFunc, error) { + if firstUsernetIndex := limayaml.FirstUsernetIndex(inst.Config); firstUsernetIndex != -1 { + return usernet.NewClientByName(inst.Config.Networks[firstUsernetIndex].Lima), nil, nil + } + // Start a in-process gvisor-tap-vsock + endpointSock, err := usernet.SockWithDirectory(inst.Dir, "", usernet.EndpointSock) + if err != nil { + return nil, nil, err + } + krunkitSock, err := usernet.SockWithDirectory(inst.Dir, "", usernet.FDSock) + if err != nil { + return nil, nil, err + } + os.RemoveAll(endpointSock) + os.RemoveAll(krunkitSock) + ctx, cancel := context.WithCancel(ctx) + err = usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{ + MTU: 1500, + Endpoint: endpointSock, + FdSocket: krunkitSock, + Async: true, + DefaultLeases: map[string]string{ + networks.SlirpIPAddress: limayaml.MACAddress(inst.Dir), + }, + Subnet: networks.SlirpNetwork, + }) + if err != nil { + defer cancel() + return nil, nil, err + } + subnetIP, _, err := net.ParseCIDR(networks.SlirpNetwork) + return usernet.NewClient(endpointSock, subnetIP), cancel, err +} diff --git a/pkg/driver/krunkit/krunkit_driver_darwin.go b/pkg/driver/krunkit/krunkit_driver_darwin.go new file mode 100644 index 00000000000..0d42da7b20a --- /dev/null +++ b/pkg/driver/krunkit/krunkit_driver_darwin.go @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package krunkit + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "syscall" + "time" + + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/driver" + "github.com/lima-vm/lima/v2/pkg/executil" + "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/lima-vm/lima/v2/pkg/networks/usernet" + "github.com/lima-vm/lima/v2/pkg/ptr" +) + +type LimaKrunkitDriver struct { + Instance *limatype.Instance + SSHLocalPort int + + usernetClient *usernet.Client + stopUsernet context.CancelFunc + krunkitCmd *exec.Cmd + krunkitWaitCh chan error +} + +var _ driver.Driver = (*LimaKrunkitDriver)(nil) + +func New() *LimaKrunkitDriver { + return &LimaKrunkitDriver{} +} + +func (l *LimaKrunkitDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriver { + l.Instance = inst + l.SSHLocalPort = inst.SSHLocalPort + + return &driver.ConfiguredDriver{ + Driver: l, + } +} + +func (l *LimaKrunkitDriver) CreateDisk(ctx context.Context) error { + return EnsureDisk(ctx, l.Instance) +} + +func (l *LimaKrunkitDriver) Start(ctx context.Context) (chan error, error) { + var err error + l.usernetClient, l.stopUsernet, err = startUsernet(ctx, l.Instance) + if err != nil { + return nil, fmt.Errorf("failed to start usernet: %w", err) + } + + krunkitCmd, err := Cmdline(l.Instance) + if err != nil { + return nil, fmt.Errorf("failed to construct krunkit command line: %w", err) + } + // Detach krunkit process from parent Lima process + krunkitCmd.SysProcAttr = executil.BackgroundSysProcAttr + + logPath := filepath.Join(l.Instance.Dir, "krunkit.log") + logfile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return nil, fmt.Errorf("failed to open krunkit logfile: %w", err) + } + krunkitCmd.Stderr = logfile + + logrus.Infof("Starting krun VM (hint: to watch the progress, see %q)", logPath) + logrus.Debugf("krunkitCmd.Args: %v", krunkitCmd.Args) + + if err := krunkitCmd.Start(); err != nil { + logfile.Close() + return nil, fmt.Errorf("failed to start krunkitCmd") + } + + pidPath := filepath.Join(l.Instance.Dir, filenames.PIDFile(*l.Instance.Config.VMType)) + if err := os.WriteFile(pidPath, []byte(fmt.Sprintf("%d\n", krunkitCmd.Process.Pid)), 0644); err != nil { + logrus.WithError(err).Warn("Failed to write PID file") + } + + l.krunkitCmd = krunkitCmd + l.krunkitWaitCh = make(chan error, 1) + go func() { + defer func() { + logfile.Close() + os.RemoveAll(pidPath) + close(l.krunkitWaitCh) + }() + l.krunkitWaitCh <- krunkitCmd.Wait() + }() + + err = l.usernetClient.ConfigureDriver(ctx, l.Instance, l.SSHLocalPort) + if err != nil { + l.krunkitWaitCh <- fmt.Errorf("failed to configure usernet: %w", err) + } + + return l.krunkitWaitCh, nil +} + +func (l *LimaKrunkitDriver) Stop(ctx context.Context) error { + if l.krunkitCmd == nil { + return nil + } + + if err := l.krunkitCmd.Process.Signal(syscall.SIGTERM); err != nil { + logrus.WithError(err).Warn("Failed to send interrupt signal") + } + + go func() { + if l.usernetClient != nil { + _ = l.usernetClient.UnExposeSSH(l.Instance.SSHLocalPort) + } + if l.stopUsernet != nil { + l.stopUsernet() + } + }() + + timeout := time.After(30 * time.Second) + select { + case <-l.krunkitWaitCh: + return nil + case <-timeout: + if err := l.krunkitCmd.Process.Kill(); err != nil { + return err + } + + <-l.krunkitWaitCh + return nil + } +} + +func (l *LimaKrunkitDriver) Delete(ctx context.Context) error { + return nil +} + +func (l *LimaKrunkitDriver) InspectStatus(ctx context.Context, inst *limatype.Instance) string { + return "" +} + +func (l *LimaKrunkitDriver) RunGUI() error { + return nil +} + +func (l *LimaKrunkitDriver) ChangeDisplayPassword(ctx context.Context, password string) error { + return fmt.Errorf("display password change not supported by krun driver") +} + +func (l *LimaKrunkitDriver) DisplayConnection(ctx context.Context) (string, error) { + return "", fmt.Errorf("display connection not supported by krun driver") +} + +func (l *LimaKrunkitDriver) CreateSnapshot(ctx context.Context, tag string) error { + return fmt.Errorf("snapshots not supported by krun driver") +} + +func (l *LimaKrunkitDriver) ApplySnapshot(ctx context.Context, tag string) error { + return fmt.Errorf("snapshots not supported by krun driver") +} + +func (l *LimaKrunkitDriver) DeleteSnapshot(ctx context.Context, tag string) error { + return fmt.Errorf("snapshots not supported by krun driver") +} + +func (l *LimaKrunkitDriver) ListSnapshots(ctx context.Context) (string, error) { + return "", fmt.Errorf("snapshots not supported by krun driver") +} + +func (l *LimaKrunkitDriver) Register(ctx context.Context) error { + return nil +} + +func (l *LimaKrunkitDriver) Unregister(ctx context.Context) error { + return nil +} + +func (l *LimaKrunkitDriver) ForwardGuestAgent() bool { + return true +} + +func (l *LimaKrunkitDriver) GuestAgentConn(ctx context.Context) (net.Conn, string, error) { + return nil, "unix", nil +} + +func (l *LimaKrunkitDriver) Validate(ctx context.Context) error { + return nil +} + +func (l *LimaKrunkitDriver) FillConfig(ctx context.Context, cfg *limatype.LimaYAML, filePath string) error { + if cfg.MountType == nil { + cfg.MountType = ptr.Of(limatype.VIRTIOFS) + } else { + *cfg.MountType = limatype.VIRTIOFS + } + + cfg.VMType = ptr.Of("krunkit") + + return nil +} + +func (l *LimaKrunkitDriver) BootScripts() (map[string][]byte, error) { + return nil, nil +} + +func (l *LimaKrunkitDriver) Create(ctx context.Context) error { + return nil +} + +func (l *LimaKrunkitDriver) Info() driver.Info { + var info driver.Info + info.Name = "krunkit" + if l.Instance != nil && l.Instance.Dir != "" { + info.InstanceDir = l.Instance.Dir + } + + info.Features = driver.DriverFeatures{ + DynamicSSHAddress: false, + SkipSocketForwarding: false, + CanRunGUI: false, + } + return info +} + +func (l *LimaKrunkitDriver) SSHAddress(ctx context.Context) (string, error) { + return "127.0.0.1", nil +} diff --git a/templates/experimental/krunkit.yaml b/templates/experimental/krunkit.yaml new file mode 100644 index 00000000000..c3e8f06ea85 --- /dev/null +++ b/templates/experimental/krunkit.yaml @@ -0,0 +1,18 @@ +vmType: krunkit + +images: +- location: "https://cloud-images.ubuntu.com/releases/plucky/release-20250701/ubuntu-25.04-server-cloudimg-arm64.img" + arch: "aarch64" + digest: "sha256:26d0ac2236f12954923eb35ddfee8fa9fff3eab6111ba84786b98ab3b972c6d8" +- location: https://cloud-images.ubuntu.com/releases/plucky/release/ubuntu-25.04-server-cloudimg-arm64.img + arch: aarch64 + +portForwards: +- static: true + guestPort: 5201 + proto: tcp + +mounts: +- location: "~" + +mountType: virtiofs \ No newline at end of file