diff --git a/cmd/lima-driver-krun/main.go b/cmd/lima-driver-krun/main.go new file mode 100644 index 00000000000..aab85b0e45f --- /dev/null +++ b/cmd/lima-driver-krun/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/krun" +) + +// To be used as an external driver for Lima. +func main() { + server.Serve(context.Background(), krun.New()) +} diff --git a/pkg/driver/krun/krun_darwin.go b/pkg/driver/krun/krun_darwin.go new file mode 100644 index 00000000000..de6c06bc0a3 --- /dev/null +++ b/pkg/driver/krun/krun_darwin.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package krun + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + + "github.com/docker/go-units" + "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" +) + +const ( + restfulURI = "krun-restful.sock" // This is for setting/checking the state of the VM + logLevelInfo = "3" + KrunEfi = "krun-efi" // efi variable store +) + +// 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", fmt.Sprintf("unix://%s", restfulSocketPath(inst)), + "--bootloader", fmt.Sprintf("efi,variable-store=%s,create", filepath.Join(inst.Dir, KrunEfi)), + "--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)), + } + + // TODO: socket_vmnet and ssh not working + networkArgs, err := buildNetworkArgs(inst) + if err != nil { + return nil, fmt.Errorf("failed to build network arguments: %w", err) + } + args = append(args, networkArgs...) + + return exec.CommandContext(context.Background(), "krunkit", args...), nil +} + +func buildNetworkArgs(inst *limatype.Instance) ([]string, error) { + var args []string + nwCfg, err := networks.LoadConfig() + if err != nil { + return nil, err + } + socketVMNetOk, err := nwCfg.IsDaemonInstalled(networks.SocketVMNet) + if err != nil { + return nil, err + } + if socketVMNetOk { + sock, err := networks.Sock("shared") + if err != nil { + return nil, err + } + networkArg := fmt.Sprintf("virtio-net,type=unixstream,path=%s,mac=%s,offloading=true", + sock, + limayaml.MACAddress(inst.Dir), + ) + args = append(args, "--device", networkArg) + + return args, nil + } + + return args, errors.New("socket_vmnet is not installed") +} + +func restfulSocketPath(inst *limatype.Instance) string { + return filepath.Join(inst.Dir, restfulURI) +} + +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() + } + 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 +} diff --git a/pkg/driver/krun/krun_driver_darwin.go b/pkg/driver/krun/krun_driver_darwin.go new file mode 100644 index 00000000000..9a4d890f1f6 --- /dev/null +++ b/pkg/driver/krun/krun_driver_darwin.go @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package krun + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "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/ptr" +) + +type LimaKrunDriver struct { + Instance *limatype.Instance + SSHLocalPort int + + krunCmd *exec.Cmd + krunWaitCh chan error +} + +var _ driver.Driver = (*LimaKrunDriver)(nil) + +func New() *LimaKrunDriver { + return &LimaKrunDriver{} +} + +func (l *LimaKrunDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriver { + l.Instance = inst + l.SSHLocalPort = inst.SSHLocalPort + + return &driver.ConfiguredDriver{ + Driver: l, + } +} + +func (l *LimaKrunDriver) CreateDisk(ctx context.Context) error { + return EnsureDisk(ctx, l.Instance) +} + +func (l *LimaKrunDriver) Start(ctx context.Context) (chan error, error) { + krunCmd, err := Cmdline(l.Instance) + if err != nil { + return nil, fmt.Errorf("failed to construct krunkit command line: %w", err) + } + krunCmd.SysProcAttr = executil.BackgroundSysProcAttr + + logPath := filepath.Join(l.Instance.Dir, "krun.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) + } + krunCmd.Stderr = logfile + + logrus.Infof("Starting krun VM (hint: to watch the progress, see %q)", logPath) + logrus.Debugf("krunCmd.Args: %v", krunCmd.Args) + + if err := krunCmd.Start(); err != nil { + logfile.Close() + return nil, err + } + + pidPath := filepath.Join(l.Instance.Dir, filenames.PIDFile(*l.Instance.Config.VMType)) + if err := os.WriteFile(pidPath, []byte(fmt.Sprintf("%d\n", krunCmd.Process.Pid)), 0644); err != nil { + logrus.WithError(err).Warn("Failed to write PID file") + } + + l.krunCmd = krunCmd + l.krunWaitCh = make(chan error, 1) + go func() { + defer func() { + logfile.Close() + os.RemoveAll(pidPath) + close(l.krunWaitCh) + }() + l.krunWaitCh <- krunCmd.Wait() + }() + + return l.krunWaitCh, nil +} + +func (l *LimaKrunDriver) Stop(ctx context.Context) error { + if l.krunCmd == nil { + return nil + } + + if err := l.krunCmd.Process.Signal(os.Interrupt); err != nil { + logrus.WithError(err).Warn("Failed to send interrupt signal") + } + + timeout := time.After(30 * time.Second) + select { + case <-l.krunWaitCh: + return nil + case <-timeout: + return l.krunCmd.Process.Kill() + } +} + +func (l *LimaKrunDriver) Delete(ctx context.Context) error { + return nil +} + +func (l *LimaKrunDriver) InspectStatus(ctx context.Context, inst *limatype.Instance) string { + return "" +} + +func (l *LimaKrunDriver) RunGUI() error { + return nil +} + +func (l *LimaKrunDriver) ChangeDisplayPassword(ctx context.Context, password string) error { + return fmt.Errorf("display password change not supported by krun driver") +} + +func (l *LimaKrunDriver) DisplayConnection(ctx context.Context) (string, error) { + return "", fmt.Errorf("display connection not supported by krun driver") +} + +func (l *LimaKrunDriver) CreateSnapshot(ctx context.Context, tag string) error { + return fmt.Errorf("snapshots not supported by krun driver") +} + +func (l *LimaKrunDriver) ApplySnapshot(ctx context.Context, tag string) error { + return fmt.Errorf("snapshots not supported by krun driver") +} + +func (l *LimaKrunDriver) DeleteSnapshot(ctx context.Context, tag string) error { + return fmt.Errorf("snapshots not supported by krun driver") +} + +func (l *LimaKrunDriver) ListSnapshots(ctx context.Context) (string, error) { + return "", fmt.Errorf("snapshots not supported by krun driver") +} + +func (l *LimaKrunDriver) Register(ctx context.Context) error { + return nil +} + +func (l *LimaKrunDriver) Unregister(ctx context.Context) error { + return nil +} + +func (l *LimaKrunDriver) ForwardGuestAgent() bool { + return true +} + +func (l *LimaKrunDriver) GuestAgentConn(ctx context.Context) (net.Conn, string, error) { + return nil, "", fmt.Errorf("guest agent connection not implemented for krun driver") +} + +func (l *LimaKrunDriver) Validate(ctx context.Context) error { + return nil +} + +func (l *LimaKrunDriver) 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("krun") + + return nil +} + +func (l *LimaKrunDriver) BootScripts() (map[string][]byte, error) { + return nil, nil +} + +func (l *LimaKrunDriver) Create(ctx context.Context) error { + return nil +} + +func (l *LimaKrunDriver) Info() driver.Info { + var info driver.Info + info.Name = "krun" + 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 *LimaKrunDriver) SSHAddress(ctx context.Context) (string, error) { + return "127.0.0.1", nil +} diff --git a/pkg/driver/krun/register.go b/pkg/driver/krun/register.go new file mode 100644 index 00000000000..5b96c8b2adc --- /dev/null +++ b/pkg/driver/krun/register.go @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package krun + +import "github.com/lima-vm/lima/v2/pkg/registry" + +func init() { + registry.Register(New()) +}