diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index 8439ca022f9..6409702bfa4 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -235,6 +235,15 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L args.MountType = "virtiofs" } + switch *instConfig.VMType { + case limayaml.QEMU: + args.DiskType = "virtio" + case limayaml.VZ: + args.DiskType = "virtio" + case limayaml.VBOX: + args.DiskType = "sata" + } + for i, d := range instConfig.AdditionalDisks { format := true if d.Format != nil { @@ -246,7 +255,7 @@ func templateArgs(bootScripts bool, instDir, name string, instConfig *limayaml.L } args.Disks = append(args.Disks, Disk{ Name: d.Name, - Device: diskDeviceNameFromOrder(i), + Device: diskDeviceNameFromOrder(args.DiskType, i), Format: format, FSType: fstype, FSArgs: d.FSArgs, @@ -472,7 +481,10 @@ func getBootCmds(p []limayaml.Provision) []BootCmds { return bootCmds } -func diskDeviceNameFromOrder(order int) string { +func diskDeviceNameFromOrder(diskType string, order int) string { + if diskType == "sata" { + return fmt.Sprintf("sd%c", int('b')+order) + } // diskType == "virtio" return fmt.Sprintf("vd%c", int('b')+order) } diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go index 80622def553..52d5ef90a76 100644 --- a/pkg/cidata/template.go +++ b/pkg/cidata/template.go @@ -79,6 +79,7 @@ type TemplateArgs struct { Mounts []Mount MountType string Disks []Disk + DiskType string GuestInstallPrefix string UpgradePackages bool Containerd Containerd diff --git a/pkg/driverutil/instance.go b/pkg/driverutil/instance.go index d7c443ff5d2..729b0433284 100644 --- a/pkg/driverutil/instance.go +++ b/pkg/driverutil/instance.go @@ -7,12 +7,16 @@ import ( "github.com/lima-vm/lima/pkg/driver" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/qemu" + "github.com/lima-vm/lima/pkg/vbox" "github.com/lima-vm/lima/pkg/vz" "github.com/lima-vm/lima/pkg/wsl2" ) func CreateTargetDriverInstance(base *driver.BaseDriver) driver.Driver { limaDriver := base.Instance.Config.VMType + if *limaDriver == limayaml.VBOX { + return vbox.New(base) + } if *limaDriver == limayaml.VZ { return vz.New(base) } diff --git a/pkg/instance/start.go b/pkg/instance/start.go index 1df00c785a3..b487f3a4332 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -402,19 +402,31 @@ func prepareDiffDisk(inst *store.Instance) error { return err } - f, err := os.Open(diffDisk) - if err != nil { - return err - } - defer f.Close() + var diskSize int64 + var format string + if *inst.Config.VMType != limayaml.VBOX { + f, err := os.Open(diffDisk) + if err != nil { + return err + } + defer f.Close() - img, err := qcow2reader.Open(f) - if err != nil { - return err - } + img, err := qcow2reader.Open(f) + if err != nil { + return err + } - diskSize := img.Size() - format := string(img.Type()) + diskSize = img.Size() + format = string(img.Type()) + } else { + info, err := imgutil.GetInfo(diffDisk) + if err != nil { + return err + } + + diskSize = info.VSize + format = info.Format + } if inst.Disk == diskSize { return nil diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index a2193a28389..c1e63ad1868 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -1110,6 +1110,8 @@ func NewArch(arch string) Arch { func NewVMType(driver string) VMType { switch driver { + case "vbox": + return VBOX case "vz": return VZ case "qemu": diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index d72e5b602c8..269d51802a9 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -86,6 +86,7 @@ const ( WSLMount MountType = "wsl2" QEMU VMType = "qemu" + VBOX VMType = "vbox" VZ VMType = "vz" WSL2 VMType = "wsl2" ) diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 750383e2c51..f169e9de06a 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -84,12 +84,16 @@ func Validate(y *LimaYAML, warn bool) error { // NOP case WSL2: // NOP + case VBOX: + if *y.Arch != X8664 { + return fmt.Errorf("field `arch` must be %q for VBox; got %q", X8664, *y.Arch) + } case VZ: if !IsNativeArch(*y.Arch) { return fmt.Errorf("field `arch` must be %q for VZ; got %q", NewArch(runtime.GOARCH), *y.Arch) } default: - return fmt.Errorf("field `vmType` must be %q, %q, %q; got %q", QEMU, VZ, WSL2, *y.VMType) + return fmt.Errorf("field `vmType` must be %q, %q, %q, or %q; got %q", QEMU, VBOX, VZ, WSL2, *y.VMType) } if len(y.Images) == 0 { diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/qemu/imgutil/imgutil.go index 18512bf0bf7..e228a6104e5 100644 --- a/pkg/qemu/imgutil/imgutil.go +++ b/pkg/qemu/imgutil/imgutil.go @@ -71,6 +71,11 @@ func (sp *InfoFormatSpecific) Vmdk() *InfoFormatSpecificDataVmdk { return &x } +func (sp *InfoFormatSpecific) Vdi() *InfoFormatSpecificDataVdi { + var x InfoFormatSpecificDataVdi + return &x +} + type InfoFormatSpecificDataQcow2 struct { Compat string `json:"compat,omitempty"` // since QEMU 1.7 LazyRefcounts bool `json:"lazy-refcounts,omitempty"` // since QEMU 1.7 @@ -94,6 +99,9 @@ type InfoFormatSpecificDataVmdkExtent struct { ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.7 } +type InfoFormatSpecificDataVdi struct { +} + // Info corresponds to the output of `qemu-img info --output=json FILE`. type Info struct { Filename string `json:"filename,omitempty"` // since QEMU 1.3 @@ -121,6 +129,18 @@ func ConvertToRaw(source, dest string) error { return nil } +func ConvertToVDI(source string, dest string) error { + var stdout, stderr bytes.Buffer + cmd := exec.Command("qemu-img", "convert", "-O", "vdi", source, dest) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w", + cmd.Args, stdout.String(), stderr.String(), err) + } + return nil +} + func ParseInfo(b []byte) (*Info, error) { var imgInfo Info if err := json.Unmarshal(b, &imgInfo); err != nil { diff --git a/pkg/vbox/disk.go b/pkg/vbox/disk.go new file mode 100644 index 00000000000..d9e210291d0 --- /dev/null +++ b/pkg/vbox/disk.go @@ -0,0 +1,93 @@ +package vbox + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + + "github.com/docker/go-units" + "github.com/lima-vm/lima/pkg/driver" + "github.com/lima-vm/lima/pkg/fileutils" + "github.com/lima-vm/lima/pkg/iso9660util" + "github.com/lima-vm/lima/pkg/qemu/imgutil" + "github.com/lima-vm/lima/pkg/store/filenames" +) + +func EnsureDisk(ctx context.Context, driver *driver.BaseDriver) error { + diffDisk := filepath.Join(driver.Instance.Dir, filenames.DiffDisk) + if _, err := os.Stat(diffDisk); err == nil || !errors.Is(err, os.ErrNotExist) { + // disk is already ensured + return err + } + + baseDisk := filepath.Join(driver.Instance.Dir, filenames.BaseDisk) + if _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) { + var ensuredBaseDisk bool + errs := make([]error, len(driver.Instance.Config.Images)) + for i, f := range driver.Instance.Config.Images { + if _, err := fileutils.DownloadFile(ctx, baseDisk, f.File, true, "the image", *driver.Instance.Config.Arch); err != nil { + errs[i] = err + continue + } + ensuredBaseDisk = true + break + } + if !ensuredBaseDisk { + return fileutils.Errors(errs) + } + } + diskSize, _ := units.RAMInBytes(*driver.Instance.Config.Disk) + if diskSize == 0 { + return nil + } + // TODO - Break qemu dependency + isBaseDiskISO, err := iso9660util.IsISO9660(baseDisk) + if err != nil { + return err + } + baseDiskInfo, err := imgutil.GetInfo(baseDisk) + if err != nil { + return fmt.Errorf("failed to get the information of base disk %q: %w", baseDisk, err) + } + if err = imgutil.AcceptableAsBasedisk(baseDiskInfo); err != nil { + return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err) + } + if baseDiskInfo.Format == "" { + return fmt.Errorf("failed to inspect the format of %q", baseDisk) + } + args := []string{"create", "-f", "qcow2"} + if !isBaseDiskISO { + args = append(args, "-F", baseDiskInfo.Format, "-b", baseDisk) + } + args = append(args, diffDisk, strconv.Itoa(int(diskSize))) + cmd := exec.Command("qemu-img", args...) + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err) + } + if isBaseDiskISO { + if err = os.Rename(baseDisk, baseDisk+".iso"); err != nil { + return err + } + if err = os.Symlink(filenames.BaseDisk+".iso", baseDisk); err != nil { + return err + } + } else { + if err = os.Rename(baseDisk, baseDisk+".img"); err != nil { + return err + } + if err = os.Symlink(filenames.BaseDisk+".img", baseDisk); err != nil { + return err + } + } + if err = imgutil.ConvertToVDI(diffDisk, diffDisk+".vdi"); err != nil { + return errors.New("cannot convert qcow2 to vdi for vbox driver") + } + if err = os.Remove(diffDisk); err != nil { + return err + } + return os.Symlink(filenames.DiffDisk+".vdi", diffDisk) +} diff --git a/pkg/vbox/vbox_driver.go b/pkg/vbox/vbox_driver.go new file mode 100644 index 00000000000..8451d383245 --- /dev/null +++ b/pkg/vbox/vbox_driver.go @@ -0,0 +1,283 @@ +package vbox + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/docker/go-units" + "github.com/lima-vm/lima/pkg/driver" + "github.com/lima-vm/lima/pkg/iso9660util" + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/lima-vm/lima/pkg/store" + "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/sirupsen/logrus" +) + +type LimaVBoxDriver struct { + *driver.BaseDriver + qCmd *exec.Cmd + qWaitCh chan error +} + +func New(driver *driver.BaseDriver) *LimaVBoxDriver { + return &LimaVBoxDriver{ + BaseDriver: driver, + } +} + +func (l *LimaVBoxDriver) Validate() error { + if *l.Instance.Config.Arch != limayaml.X8664 { + return fmt.Errorf("field `arch` must be %q for VBox driver , got %q", limayaml.X8664, *l.Instance.Config.Arch) + } + if *l.Instance.Config.MountType != limayaml.REVSSHFS { + return fmt.Errorf("field `mountType` must be %q for VBox driver , got %q", limayaml.REVSSHFS, *l.Instance.Config.MountType) + } + return nil +} + +func (l *LimaVBoxDriver) CreateDisk(ctx context.Context) error { + return EnsureDisk(ctx, l.BaseDriver) +} + +func (l *LimaVBoxDriver) create(ctx context.Context, name string) error { + qExe := "VBoxManage" + + qArgsFinal := []string{"createvm", "--basefolder", l.Instance.Dir, "--name", name, "--register"} + qCmd := exec.CommandContext(ctx, qExe, qArgsFinal...) + _, err := qCmd.StdoutPipe() + if err != nil { + return err + } + logrus.Debugf("qCmd.Args: %v", qCmd.Args) + if err := qCmd.Run(); err != nil { + return err + } + + baseDisk := filepath.Join(l.Instance.Dir, filenames.BaseDisk) + diffDisk := filepath.Join(l.Instance.Dir, filenames.DiffDisk) + extraDisks := []string{} + if len(l.Instance.AdditionalDisks) > 0 { + for _, d := range l.Instance.AdditionalDisks { + diskName := d.Name + disk, err := store.InspectDisk(diskName) + if err != nil { + logrus.Errorf("could not load disk %q: %q", diskName, err) + return err + } + + if disk.Instance != "" { + logrus.Errorf("could not attach disk %q, in use by instance %q", diskName, disk.Instance) + return err + } + logrus.Infof("Mounting disk %q on %q", diskName, disk.MountPoint) + err = disk.Lock(l.Instance.Dir) + if err != nil { + logrus.Errorf("could not lock disk %q: %q", diskName, err) + return err + } + dataDisk := filepath.Join(disk.Dir, filenames.DataDisk) + extraDisks = append(extraDisks, dataDisk) + } + } + isBaseDiskISO, err := iso9660util.IsISO9660(baseDisk) + if err != nil { + return err + } + + var firmware string + if *l.Instance.Config.Firmware.LegacyBIOS { + firmware = "bios" + } else { + firmware = "efi" + } + cpus := *l.Instance.Config.CPUs + memBytes, err := units.RAMInBytes(*l.Instance.Config.Memory) + if err != nil { + return err + } + memory := memBytes >> 20 + var boot string + if isBaseDiskISO { + boot = "dvd" + } else { + boot = "disk" + } + + modifyFlags := []string{ + "modifyvm", name, + "--firmware", firmware, + "--ostype", "Linux26_64", + "--cpus", fmt.Sprintf("%d", cpus), + "--memory", fmt.Sprintf("%d", memory), + "--boot1", boot, + } + mCmd := exec.CommandContext(ctx, qExe, modifyFlags...) + logrus.Debugf("mCmd.Args: %v", mCmd.Args) + if err := mCmd.Run(); err != nil { + return err + } + + logrus.Debugf("storage") + if err := exec.CommandContext(ctx, qExe, "storagectl", name, + "--name", "SATA", + "--add", "sata", + "--portcount", "4", + "--hostiocache", "on").Run(); err != nil { + logrus.Debugf("storagectl %v", err) + return err + } + if isBaseDiskISO { + if err := exec.CommandContext(ctx, qExe, "storageattach", name, + "--storagectl", "SATA", + "--port", "1", + "--device", "0", + "--type", "dvddrive", + "--medium", baseDisk+".iso").Run(); err != nil { + logrus.Debugf("basedisk %v", err) + return err + } + } + if err := exec.CommandContext(ctx, qExe, "storageattach", name, + "--storagectl", "SATA", + "--port", "0", + "--device", "0", + "--type", "hdd", + "--medium", diffDisk+".vdi").Run(); err != nil { + logrus.Debugf("diffdisk %v", err) + return err + } + for i, extraDisk := range extraDisks { + if err := exec.CommandContext(ctx, qExe, "storageattach", name, + "--storagectl", "SATA", + "--port", "3", + "--device", fmt.Sprintf("%d", i), + "--type", "hdd", + "--medium", extraDisk).Run(); err != nil { + logrus.Debugf("extradisk %v", err) + return err + } + } + + if err := exec.CommandContext(ctx, qExe, "storageattach", name, + "--storagectl", "SATA", + "--type", "dvddrive", + "--port", "2", + "--device", "0", + "--medium", filepath.Join(l.Instance.Dir, filenames.CIDataISO)).Run(); err != nil { + logrus.Debugf("cidata %v", err) + return err + } + + logrus.Debugf("network") + + slirpMACAddress := limayaml.MACAddress(l.Instance.Dir) + if out, err := exec.CommandContext(ctx, qExe, "modifyvm", name, + "--nic1", "nat", + "--macaddress1", strings.ReplaceAll(slirpMACAddress, ":", ""), + "--nictype1", "virtio", + "--cableconnected1", "on").CombinedOutput(); err != nil { + logrus.Debugf("modifyvm nic1 %v %s", err, out) + return err + } + + return nil +} + +func (l *LimaVBoxDriver) Start(ctx context.Context) (chan error, error) { + name := "lima-" + l.Instance.Name + qExe := "VBoxManage" + if err := exec.CommandContext(ctx, qExe, "showvminfo", name).Run(); err != nil { + err = l.create(ctx, name) + if err != nil { + return nil, err + } + } + + _ = exec.CommandContext(ctx, qExe, "modifyvm", name, + "--natpf1", "delete", "ssh").Run() + if out, err := exec.CommandContext(ctx, qExe, "modifyvm", name, + "--natpf1", fmt.Sprintf("%s,%s,127.0.0.1,%d,,%d", "ssh", "tcp", l.SSHLocalPort, 22)).CombinedOutput(); err != nil { + logrus.Debugf("modifyvm natpf1 %v %s", err, out) + return nil, err + } + if out, err := exec.CommandContext(ctx, qExe, "modifyvm", name, + "--uart1", "0x3F8", "4", // these are the "traditional values" for COM1, according to the documentation + "--uartmode1", "file", filepath.Join(l.Instance.Dir, filenames.SerialLog)).CombinedOutput(); err != nil { + logrus.Debugf("modifyvm uartmode %v %s", err, out) + return nil, err + } + + logrus.Infof("Starting VBox (hint: to watch the boot progress, see %q)", filepath.Join(l.Instance.Dir, filenames.SerialLog)) + displayType := "headless" + if l.Instance.Config.Video.Display != nil { + display := *l.Instance.Config.Video.Display + if display == "none" || display == "headless" { + displayType = "headless" + } else { // display == "default" || display == "gui" + displayType = "gui" + } + } + startFlags := []string{ + "startvm", name, + "--type", displayType, + } + qCmd := exec.CommandContext(ctx, qExe, startFlags...) + _, err := qCmd.StdoutPipe() + if err != nil { + return nil, err + } + logrus.Debugf("qCmd.Args: %v", qCmd.Args) + if err := qCmd.Start(); err != nil { + logrus.Debugf("%v", err) + return nil, err + } + + l.qCmd = qCmd + l.qWaitCh = make(chan error) + go func() { + for { + time.Sleep(1 * time.Second) + } + }() + logrus.Info("Started VBox") + + // TODO: get Pid of VM + pidFile := filepath.Join(l.Instance.Dir, filenames.PIDFile(*l.Instance.Config.VMType)) + if _, err := os.Stat(pidFile); !errors.Is(err, os.ErrNotExist) { + logrus.Errorf("pidfile %q already exists", pidFile) + } + if err := os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())+"\n"), 0o644); err != nil { + logrus.Errorf("error writing to pid file %q", pidFile) + } + + return l.qWaitCh, nil +} + +func (l *LimaVBoxDriver) Stop(ctx context.Context) error { + args := []string{"controlvm", "lima-" + l.Instance.Name, "acpipowerbutton"} + qCmd := exec.CommandContext(ctx, "VBoxManage", args...) + err := qCmd.Run() + return err +} + +func (l *LimaVBoxDriver) Register(ctx context.Context) error { + name := "lima-" + l.Instance.Name + args := []string{"registervm", filepath.Join(l.Instance.Dir, name)} + qCmd := exec.CommandContext(ctx, "VBoxManage", args...) + err := qCmd.Run() + return err +} + +func (l *LimaVBoxDriver) Unregister(ctx context.Context) error { + args := []string{"unregistervm", "lima-" + l.Instance.Name} + qCmd := exec.CommandContext(ctx, "VBoxManage", args...) + err := qCmd.Run() + return err +} diff --git a/templates/experimental/vbox.yaml b/templates/experimental/vbox.yaml new file mode 100644 index 00000000000..b9d5fe6bc2a --- /dev/null +++ b/templates/experimental/vbox.yaml @@ -0,0 +1,20 @@ +# A template to run ubuntu using vmType: vbox instead of qemu (Default) + +minimumLimaVersion: 1.1.0 + +base: +- template://_images/ubuntu-lts +- template://_default/mounts + +vmType: vbox + +# Arch: "default", "x86_64", "aarch64". +# 🟢 Builtin default: "default" (corresponds to the host architecture) +arch: "x86_64" + +video: + # VirtualBox display, e.g., "none" (or "headless"), "default" (or "gui") + # Choosing "none" will hide the video output, and not show any window. + # Choosing "default" will show a window while the instance is running. + # 🟢 Builtin default: "none" + display: null