From 6747b7bbf2dfbf13d6628b479a07ee0f90df00df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Fri, 8 Aug 2025 10:26:53 +0200 Subject: [PATCH] Apple Container external driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new macOS driver similar to the WSL2 driver for Windows, creating virtual machines from container images (as rootfs). Signed-off-by: Anders F Björklund --- cmd/lima-driver-ac/main_darwin.go | 14 + .../{02-wsl2-setup.sh => 02-wsl2-ac-setup.sh} | 6 +- pkg/cidata/cidata.go | 2 +- pkg/driver/ac/ac_driver_darwin.go | 243 ++++++++++++++++++ pkg/driver/ac/errors_darwin.go | 8 + pkg/driver/ac/fs.go | 40 +++ pkg/driver/ac/lima-init.TEMPLATE | 8 + pkg/driver/ac/vm_darwin.go | 218 ++++++++++++++++ pkg/hostagent/hostagent.go | 4 +- pkg/limayaml/defaults.go | 2 + pkg/limayaml/limayaml.go | 3 +- pkg/limayaml/validate.go | 4 +- pkg/limayaml/validate_test.go | 2 +- pkg/store/instance_darwin.go | 122 +++++++++ .../{instance_unix.go => instance_others.go} | 2 +- templates/experimental/ac.yaml | 23 ++ 16 files changed, 691 insertions(+), 10 deletions(-) create mode 100644 cmd/lima-driver-ac/main_darwin.go rename pkg/cidata/cidata.TEMPLATE.d/boot/{02-wsl2-setup.sh => 02-wsl2-ac-setup.sh} (83%) create mode 100644 pkg/driver/ac/ac_driver_darwin.go create mode 100644 pkg/driver/ac/errors_darwin.go create mode 100644 pkg/driver/ac/fs.go create mode 100644 pkg/driver/ac/lima-init.TEMPLATE create mode 100644 pkg/driver/ac/vm_darwin.go create mode 100644 pkg/store/instance_darwin.go rename pkg/store/{instance_unix.go => instance_others.go} (92%) create mode 100644 templates/experimental/ac.yaml diff --git a/cmd/lima-driver-ac/main_darwin.go b/cmd/lima-driver-ac/main_darwin.go new file mode 100644 index 00000000000..7d9d8d91e6e --- /dev/null +++ b/cmd/lima-driver-ac/main_darwin.go @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "github.com/lima-vm/lima/v2/pkg/driver/ac" + "github.com/lima-vm/lima/v2/pkg/driver/external/server" +) + +// To be used as an external driver for Lima. +func main() { + server.Serve(ac.New()) +} diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-ac-setup.sh similarity index 83% rename from pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh rename to pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-ac-setup.sh index cb5639657d3..a02cac785dd 100755 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-ac-setup.sh @@ -4,8 +4,8 @@ # SPDX-License-Identifier: Apache-2.0 # This script replaces the cloud-init functionality of creating a user and setting its SSH keys -# when using a WSL2 VM. -[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0 +# when using a WSL2 or AC VM. +[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || [ "$LIMA_CIDATA_VMTYPE" = "ac" ] || exit 0 # create user # shellcheck disable=SC2153 @@ -22,4 +22,4 @@ chmod 600 "${LIMA_CIDATA_HOME}"/.ssh/authorized_keys echo "${LIMA_CIDATA_USER} ALL=(ALL) NOPASSWD:ALL" | tee -a /etc/sudoers.d/99_lima_sudoers # symlink CIDATA to the hardcoded path for requirement checks (TODO: make this not hardcoded) -ln -sfFn "${LIMA_CIDATA_MNT}" /mnt/lima-cidata +[ "$LIMA_CIDATA_MNT" = "/mnt/lima-cidata" ] || ln -sfFn "${LIMA_CIDATA_MNT}" /mnt/lima-cidata diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index 0bf39d3f9a1..f225bbbf819 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -432,7 +432,7 @@ func GenerateISO9660(instDir, name string, instConfig *limayaml.LimaYAML, udpDNS }) } - if args.VMType == limayaml.WSL2 { + if args.VMType == limayaml.WSL2 || args.VMType == limayaml.AC { layout = append(layout, iso9660util.Entry{ Path: "ssh_authorized_keys", Reader: strings.NewReader(strings.Join(args.SSHPubKeys, "\n")), diff --git a/pkg/driver/ac/ac_driver_darwin.go b/pkg/driver/ac/ac_driver_darwin.go new file mode 100644 index 00000000000..967cfdbe6dd --- /dev/null +++ b/pkg/driver/ac/ac_driver_darwin.go @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package ac + +import ( + "context" + "fmt" + "net" + "regexp" + + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/driver" + "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/lima-vm/lima/v2/pkg/reflectutil" + "github.com/lima-vm/lima/v2/pkg/store" +) + +var knownYamlProperties = []string{ + "Arch", + "Containerd", + "CopyToHost", + "CPUType", + "Disk", + "DNS", + "Env", + "HostResolver", + "Images", + "Message", + "Mounts", + "MountType", + "Param", + "Plain", + "PortForwards", + "Probes", + "PropagateProxyEnv", + "Provision", + "SSH", + "VMType", +} + +const Enabled = true + +type LimaAcDriver struct { + Instance *store.Instance + + SSHLocalPort int + vSockPort int + virtioPort string +} + +var _ driver.Driver = (*LimaAcDriver)(nil) + +func New() *LimaAcDriver { + return &LimaAcDriver{ + vSockPort: 0, + virtioPort: "", + } +} + +func (l *LimaAcDriver) Configure(inst *store.Instance) *driver.ConfiguredDriver { + l.Instance = inst + l.SSHLocalPort = inst.SSHLocalPort + + return &driver.ConfiguredDriver{ + Driver: l, + } +} + +func (l *LimaAcDriver) Validate() error { + if *l.Instance.Config.MountType != limayaml.VIRTIOFS && *l.Instance.Config.MountType != limayaml.REVSSHFS { + return fmt.Errorf("field `mountType` must be %q or %q for AC driver, got %q", limayaml.VIRTIOFS, limayaml.REVSSHFS, *l.Instance.Config.MountType) + } + // TODO: revise this list for AC + if unknown := reflectutil.UnknownNonEmptyFields(l.Instance.Config, knownYamlProperties...); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: %+v", *l.Instance.Config.VMType, unknown) + } + + if !limayaml.IsNativeArch(*l.Instance.Config.Arch) { + return fmt.Errorf("unsupported arch: %q", *l.Instance.Config.Arch) + } + + // TODO: real filetype checks + tarFileRegex := regexp.MustCompile(`.*tar\.*`) + for i, image := range l.Instance.Config.Images { + if unknown := reflectutil.UnknownNonEmptyFields(image, "File"); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: images[%d]: %+v", *l.Instance.Config.VMType, i, unknown) + } + match := tarFileRegex.MatchString(image.Location) + if image.Arch == *l.Instance.Config.Arch && !match { + return fmt.Errorf("unsupported image type for vmType: %s, tarball root file system required: %q", *l.Instance.Config.VMType, image.Location) + } + } + + for i, mount := range l.Instance.Config.Mounts { + if unknown := reflectutil.UnknownNonEmptyFields(mount); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: mounts[%d]: %+v", *l.Instance.Config.VMType, i, unknown) + } + } + + for i, network := range l.Instance.Config.Networks { + if unknown := reflectutil.UnknownNonEmptyFields(network); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: networks[%d]: %+v", *l.Instance.Config.VMType, i, unknown) + } + } + + audioDevice := *l.Instance.Config.Audio.Device + if audioDevice != "" { + logrus.Warnf("Ignoring: vmType %s: `audio.device`: %+v", *l.Instance.Config.VMType, audioDevice) + } + + return nil +} + +func (l *LimaAcDriver) Start(ctx context.Context) (chan error, error) { + logrus.Infof("Starting AC VM") + status, err := store.GetAcStatus(l.Instance.Name) + if err != nil { + return nil, err + } + + distroName := "lima-" + l.Instance.Name + + if status == store.StatusUninitialized { + if err := EnsureFs(ctx, l.Instance); err != nil { + return nil, err + } + if err := initVM(ctx, l.Instance.Dir, distroName); err != nil { + return nil, err + } + cpus := l.Instance.CPUs + memory := int(l.Instance.Memory >> 20) // MiB + if err := registerVM(ctx, distroName, cpus, memory); err != nil { + return nil, err + } + } + + errCh := make(chan error) + + if err := startVM(ctx, distroName); err != nil { + return nil, err + } + + if err := provisionVM( + ctx, + l.Instance.Dir, + l.Instance.Name, + distroName, + errCh, + ); err != nil { + return nil, err + } + + return errCh, err +} + +func (l *LimaAcDriver) canRunGUI() bool { + return false +} + +func (l *LimaAcDriver) RunGUI() error { + return fmt.Errorf("RunGUI is not supported for the given driver '%s' and display '%s'", "ac", *l.Instance.Config.Video.Display) +} + +func (l *LimaAcDriver) Stop(ctx context.Context) error { + logrus.Info("Shutting down AC VM") + distroName := "lima-" + l.Instance.Name + return stopVM(ctx, distroName) +} + +func (l *LimaAcDriver) Unregister(ctx context.Context) error { + distroName := "lima-" + l.Instance.Name + status, err := store.GetAcStatus(l.Instance.Name) + if err != nil { + return err + } + switch status { + case store.StatusRunning, store.StatusStopped, store.StatusBroken, store.StatusInstalling: + return unregisterVM(ctx, distroName) + } + + logrus.Info("VM not registered, skipping unregistration") + return nil +} + +// GuestAgentConn returns the guest agent connection, or nil (if forwarded by ssh). +func (l *LimaAcDriver) GuestAgentConn(_ context.Context) (net.Conn, string, error) { + return nil, "", nil +} + +func (l *LimaAcDriver) Info() driver.Info { + var info driver.Info + if l.Instance != nil { + info.InstanceDir = l.Instance.Dir + } + info.DriverName = "ac" + info.CanRunGUI = l.canRunGUI() + info.VirtioPort = l.virtioPort + info.VsockPort = l.vSockPort + return info +} + +func (l *LimaAcDriver) Initialize(_ context.Context) error { + return nil +} + +func (l *LimaAcDriver) CreateDisk(_ context.Context) error { + return nil +} + +func (l *LimaAcDriver) Register(_ context.Context) error { + return nil +} + +func (l *LimaAcDriver) ChangeDisplayPassword(_ context.Context, _ string) error { + return nil +} + +func (l *LimaAcDriver) DisplayConnection(_ context.Context) (string, error) { + return "", nil +} + +func (l *LimaAcDriver) CreateSnapshot(_ context.Context, _ string) error { + return errUnimplemented +} + +func (l *LimaAcDriver) ApplySnapshot(_ context.Context, _ string) error { + return errUnimplemented +} + +func (l *LimaAcDriver) DeleteSnapshot(_ context.Context, _ string) error { + return errUnimplemented +} + +func (l *LimaAcDriver) ListSnapshots(_ context.Context) (string, error) { + return "", errUnimplemented +} + +func (l *LimaAcDriver) ForwardGuestAgent() bool { + // If driver is not providing, use host agent + return l.vSockPort == 0 && l.virtioPort == "" +} diff --git a/pkg/driver/ac/errors_darwin.go b/pkg/driver/ac/errors_darwin.go new file mode 100644 index 00000000000..47454705c81 --- /dev/null +++ b/pkg/driver/ac/errors_darwin.go @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package ac + +import "errors" + +var errUnimplemented = errors.New("unimplemented") diff --git a/pkg/driver/ac/fs.go b/pkg/driver/ac/fs.go new file mode 100644 index 00000000000..b343626322c --- /dev/null +++ b/pkg/driver/ac/fs.go @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package ac + +import ( + "context" + "errors" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/fileutils" + "github.com/lima-vm/lima/v2/pkg/store" + "github.com/lima-vm/lima/v2/pkg/store/filenames" +) + +// EnsureFs downloads the root fs. +func EnsureFs(ctx context.Context, inst *store.Instance) error { + baseDisk := filepath.Join(inst.Dir, filenames.BaseDisk) + if _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) { + var ensuredBaseDisk bool + errs := make([]error, len(inst.Config.Images)) + for i, f := range inst.Config.Images { + if _, err := fileutils.DownloadFile(ctx, baseDisk, f.File, true, "the image", *inst.Config.Arch); err != nil { + errs[i] = err + continue + } + ensuredBaseDisk = true + break + } + if !ensuredBaseDisk { + return fileutils.Errors(errs) + } + } + logrus.Info("Download succeeded") + + return nil +} diff --git a/pkg/driver/ac/lima-init.TEMPLATE b/pkg/driver/ac/lima-init.TEMPLATE new file mode 100644 index 00000000000..b30c8825344 --- /dev/null +++ b/pkg/driver/ac/lima-init.TEMPLATE @@ -0,0 +1,8 @@ +#!/bin/bash +set -eu +export LOG_FILE=/var/log/lima-init.log +exec > >(tee $LOG_FILE) 2>&1 +ln -sf rtc0 /dev/rtc +chmod 666 /dev/fuse +export LIMA_CIDATA_MNT="{{.CIDataPath}}" +exec "$LIMA_CIDATA_MNT/boot.sh" diff --git a/pkg/driver/ac/vm_darwin.go b/pkg/driver/ac/vm_darwin.go new file mode 100644 index 00000000000..6fa77540dc8 --- /dev/null +++ b/pkg/driver/ac/vm_darwin.go @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package ac + +import ( + "bytes" + "context" + _ "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/store" + "github.com/lima-vm/lima/v2/pkg/store/filenames" + "github.com/lima-vm/lima/v2/pkg/textutil" +) + +// init system (pid 1) in VM. +const initSystem = "openrc" + +// registerVM calls AC to register a VM. +func registerVM(ctx context.Context, distroName string, cpus, memory int) error { + imageName := distroName + entrypoint := "/sbin/init" + // /sbin/init is normally just a symlink to systemd + // eventually we might want to look inside the image + switch initSystem { + case "systemd": + entrypoint = "/lib/systemd/systemd" + case "openrc": + entrypoint = "/sbin/openrc-init" + default: + logrus.Infof("unknown init system, running only vminitd") + } + out, err := exec.CommandContext(ctx, + "container", + "create", + "--name", + distroName, + "--cpus", + fmt.Sprintf("%d", cpus), + "--memory", + fmt.Sprintf("%dM", memory), + "--entrypoint", + entrypoint, + imageName).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run `container create %s`: %w (out=%q)", + distroName, err, out) + } + return nil +} + +// startVM calls AC to start a VM. +func startVM(ctx context.Context, distroName string) error { + out, err := exec.CommandContext(ctx, + "container", + "start", + distroName).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run `container start %s`: %w (out=%q)", + distroName, err, out) + } + return nil +} + +// initVM calls AC to import a new VM specifically for Lima. +func initVM(ctx context.Context, instanceDir, distroName string) error { + imageName := distroName + dockerFile := filepath.Join(instanceDir, "Dockerfile") + fileContents := fmt.Sprintf("FROM scratch\nADD %s /\n", filenames.BaseDisk) + err := os.WriteFile(dockerFile, []byte(fileContents), 0o644) + if err != nil { + return err + } + baseDisk := filepath.Join(instanceDir, filenames.BaseDisk) + logrus.Infof("Importing distro from %q to %q", baseDisk, instanceDir) + wd, err := os.Getwd() + if err != nil { + return err + } + err = os.Chdir(instanceDir) + if err != nil { + return err + } + defer func() { _ = os.Chdir(wd) }() + out, err := exec.CommandContext(ctx, + "container", + "build", + "-t", + imageName, + instanceDir).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run `container build -t %s %s`: %w (out=%q)", + imageName, instanceDir, err, out) + } + return nil +} + +// stopVM calls AC to stop a running VM. +func stopVM(ctx context.Context, distroName string) error { + out, err := exec.CommandContext(ctx, + "container", + "stop", + distroName).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run `container stop %s`: %w (out=%q)", + distroName, err, out) + } + return nil +} + +//go:embed lima-init.TEMPLATE +var limaBoot string + +// copyDir copies a directory. +func copyDir(ctx context.Context, distroName, src, dst string) error { + cmd := exec.CommandContext(ctx, "container", "exec", distroName, "mkdir", "-p", dst) + if err := cmd.Run(); err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + logrus.Debugf("run stderr: %s", exiterr.Stderr) + } + return fmt.Errorf("failed to run %v: %w", cmd.Args, err) + } + logrus.Infof("Copying directory from %q to \"%s:%s\"", src, distroName, dst) + tar1 := exec.CommandContext(ctx, "tar", "Cc", src, ".") + tar2 := exec.CommandContext(ctx, "container", "exec", "-i", distroName, "tar", "Cx", dst) + + p, err := tar1.StdoutPipe() + if err != nil { + return err + } + tar2.Stdin = p + if err := tar2.Start(); err != nil { + return err + } + if err := tar1.Run(); err != nil { + return err + } + if err := tar2.Wait(); err != nil { + return err + } + return nil +} + +// provisionVM starts Lima's boot process inside an already imported VM. +func provisionVM(ctx context.Context, instanceDir, instanceName, distroName string, errCh chan<- error) error { + ciDataPath := filepath.Join(instanceDir, filenames.CIDataISODir) + // can't mount the cidata, due to problems with virtiofs mounts + if err := copyDir(ctx, distroName, ciDataPath, "/mnt/lima-cidata"); err != nil { + return fmt.Errorf("failed to copy cidata directory: %w", err) + } + m := map[string]string{ + "CIDataPath": "/mnt/lima-cidata", + } + limaBootB, err := textutil.ExecuteTemplate(limaBoot, m) + if err != nil { + return fmt.Errorf("failed to construct ac boot.sh script: %w", err) + } + go func() { + cmd := exec.CommandContext( + ctx, + "container", + "exec", + "-i", + distroName, + "/bin/bash", + ) + cmd.Stdin = bytes.NewReader(limaBootB) + out, err := cmd.CombinedOutput() + logrus.Debugf("%v: %q", cmd.Args, string(out)) + if err != nil { + errCh <- fmt.Errorf( + "error running command that executes boot.sh (%v): %w, "+ + "check /var/log/lima-init.log for more details (out=%q)", cmd.Args, err, string(out)) + } + + for { + <-ctx.Done() + logrus.Info("Context closed, stopping vm") + if status, err := store.GetAcStatus(instanceName); err == nil && + status == store.StatusRunning { + _ = stopVM(ctx, distroName) + } + } + }() + + return err +} + +// unregisterVM calls AC to unregister a VM. +func unregisterVM(ctx context.Context, distroName string) error { + imageName := distroName + logrus.Info("Unregistering AC VM") + out, err := exec.CommandContext(ctx, + "container", + "rm", + "-f", + distroName).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run `container rm -f %s`: %w (out=%q)", + distroName, err, out) + } + out, err = exec.CommandContext(ctx, + "container", + "image", + "rm", + imageName).CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run `container image rm %s`: %w (out=%q)", + distroName, err, out) + } + return nil +} diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 98d0922a0bd..fa1320ec943 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -115,7 +115,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt if err != nil { return nil, err } - if *inst.Config.VMType == limayaml.WSL2 { + if *inst.Config.VMType == limayaml.WSL2 || *inst.Config.VMType == limayaml.AC { sshLocalPort = inst.SSHLocalPort } @@ -309,7 +309,7 @@ func (a *HostAgent) Run(ctx context.Context) error { } // WSL instance SSH address isn't known until after VM start - if *a.instConfig.VMType == limayaml.WSL2 { + if *a.instConfig.VMType == limayaml.WSL2 || *a.instConfig.VMType == limayaml.AC { sshAddr, err := store.GetSSHAddress(a.instName) if err != nil { return err diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 1ab059a7645..9ef7e1669e5 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -1058,6 +1058,8 @@ func NewVMType(driver string) VMType { return QEMU case "wsl2": return WSL2 + case "ac": + return AC default: logrus.Warnf("Unknown driver: %s", driver) return driver diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index a21c4aa5cf0..2cbfe9d29b9 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -89,13 +89,14 @@ const ( QEMU VMType = "qemu" VZ VMType = "vz" WSL2 VMType = "wsl2" + AC VMType = "ac" ) var ( OSTypes = []OS{LINUX} ArchTypes = []Arch{X8664, AARCH64, ARMV7L, PPC64LE, RISCV64, S390X} MountTypes = []MountType{REVSSHFS, NINEP, VIRTIOFS, WSLMount} - VMTypes = []VMType{QEMU, VZ, WSL2} + VMTypes = []VMType{QEMU, VZ, WSL2, AC} ) type User struct { diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index e09f56b3a1b..05ea397f95b 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -66,12 +66,14 @@ func Validate(y *LimaYAML, warn bool) error { // NOP case WSL2: // NOP + case AC: + // NOP case VZ: if !IsNativeArch(*y.Arch) { errs = errors.Join(errs, fmt.Errorf("field `arch` must be %q for VZ; got %q", NewArch(runtime.GOARCH), *y.Arch)) } default: - errs = errors.Join(errs, fmt.Errorf("field `vmType` must be %q, %q, %q; got %q", QEMU, VZ, WSL2, *y.VMType)) + errs = errors.Join(errs, fmt.Errorf("field `vmType` must be %q, %q, %q, %q; got %q", QEMU, VZ, WSL2, AC, *y.VMType)) } if len(y.Images) == 0 { diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go index 5c9d696a0de..cc0213e9615 100644 --- a/pkg/limayaml/validate_test.go +++ b/pkg/limayaml/validate_test.go @@ -331,7 +331,7 @@ provision: assert.Error(t, err, "field `os` must be \"Linux\"; got \"windows\"\n"+ "field `arch` must be one of [x86_64 aarch64 armv7l ppc64le riscv64 s390x]; got \"unsupported_arch\"\n"+ - "field `vmType` must be \"qemu\", \"vz\", \"wsl2\"; got \"invalid_type\"\n"+ + "field `vmType` must be \"qemu\", \"vz\", \"wsl2\", \"ac\"; got \"invalid_type\"\n"+ "field `images` must be set\n"+ "field `provision[0].mode` must one of \"system\", \"user\", \"boot\", \"data\", \"dependency\", or \"ansible\"\n"+ "field `provision[1].path` must not be empty when mode is \"data\"") diff --git a/pkg/store/instance_darwin.go b/pkg/store/instance_darwin.go new file mode 100644 index 00000000000..319e763cfea --- /dev/null +++ b/pkg/store/instance_darwin.go @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package store + +import ( + "encoding/json" + "fmt" + "os/exec" + "strings" + + "github.com/lima-vm/lima/v2/pkg/limayaml" +) + +func inspectStatus(instDir string, inst *Instance, y *limayaml.LimaYAML) { + if inst.VMType == limayaml.AC { + status, err := GetAcStatus(inst.Name) + if err != nil { + inst.Status = StatusBroken + inst.Errors = append(inst.Errors, err) + } else { + inst.Status = status + } + + inst.SSHLocalPort = 22 + + if inst.Status == StatusRunning { + sshAddr, err := GetSSHAddress(inst.Name) + if err == nil { + inst.SSHAddress = sshAddr + } else { + inst.Errors = append(inst.Errors, err) + } + } + } else { + inspectStatusWithPIDFiles(instDir, inst, y) + } +} + +type Network struct { + Address string `json:"address"` + Gateway string `json:"gateway"` + Hostname string `json:"hostname"` + Network string `json:"network"` +} + +type Container struct { + Status string `json:"status"` + Config map[string]any `json:"configuration"` + Networks []Network `json:"networks,omitempty"` +} + +// listContainers returns all containers in the system. +// +// but currently there is _no_ way to filter in the list. +// so we need to loop through all of them in the client. +func listContainers() ([]Container, error) { + out, err := exec.Command( + "container", + "list", + "--format=json", + ).CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to run `container list --format=json`, err: %w (out=%q)", err, out) + } + + var list []Container + err = json.Unmarshal(out, &list) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal json output, err: %w (out=%q)", err, out) + } + return list, nil +} + +func GetAcStatus(instName string) (string, error) { + distroName := "lima-" + instName + list, err := listContainers() + if err != nil { + return "", err + } + + instState := StatusUninitialized + for _, c := range list { + // container don't have real ID + // (any --name replaces the UUID) + if c.Config["id"] == distroName { + switch c.Status { + case "stopped": + instState = StatusStopped + case "running": + instState = StatusRunning + default: + instState = StatusUnknown + } + break + } + } + + return instState, nil +} + +func GetSSHAddress(instName string) (string, error) { + distroName := "lima-" + instName + list, err := listContainers() + if err != nil { + return "", err + } + + instAddress := "127.0.0.1" + for _, c := range list { + // container don't have real ID + // (any --name replaces the UUID) + if c.Config["id"] == distroName { + if len(c.Networks) > 0 { + instAddress = c.Networks[0].Address + } + break + } + } + + return strings.Replace(instAddress, "/24", "", 1), nil +} diff --git a/pkg/store/instance_unix.go b/pkg/store/instance_others.go similarity index 92% rename from pkg/store/instance_unix.go rename to pkg/store/instance_others.go index 41ded92c368..5c011fe5caf 100644 --- a/pkg/store/instance_unix.go +++ b/pkg/store/instance_others.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && !darwin // SPDX-FileCopyrightText: Copyright The Lima Authors // SPDX-License-Identifier: Apache-2.0 diff --git a/templates/experimental/ac.yaml b/templates/experimental/ac.yaml new file mode 100644 index 00000000000..eb6063dda25 --- /dev/null +++ b/templates/experimental/ac.yaml @@ -0,0 +1,23 @@ +vmType: ac + +arch: aarch64 +cpus: 4 +memory: 1GiB +disk: 512GiB + +images: +- location: "debian-rootfs-arm64.tar.gz" + arch: "aarch64" + +# virtiofs is broken +mountType: reverse-sshfs + +mounts: +- location: "~" +- location: "/tmp/lima" + writable: true + +# The built-in containerd installer does not support OpenRC currently. +containerd: + system: false + user: false