diff --git a/cmd/limactl/clone.go b/cmd/limactl/clone.go index 96cf9865dec..87a7a980449 100644 --- a/cmd/limactl/clone.go +++ b/cmd/limactl/clone.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/cmd/limactl/editflags" + "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" "github.com/lima-vm/lima/v2/pkg/limayaml" @@ -79,6 +80,9 @@ func cloneAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + if err := driverutil.ResolveVMType(y, filePath); err != nil { + return fmt.Errorf("failed to accept config for %q: %w", filePath, err) + } if err := limayaml.Validate(y, true); err != nil { return saveRejectedYAML(yBytes, err) } diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go index baa2a983c88..8685f17b1b4 100644 --- a/cmd/limactl/edit.go +++ b/cmd/limactl/edit.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/cmd/limactl/editflags" + "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/editutil" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/limatype" @@ -120,6 +121,9 @@ func editAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + if err := driverutil.ResolveVMType(y, filePath); err != nil { + return fmt.Errorf("failed to accept config for %q: %w", filePath, err) + } if err := limayaml.Validate(y, true); err != nil { return saveRejectedYAML(yBytes, err) } diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go index eba0042e15c..7a0f5a07599 100644 --- a/cmd/limactl/editflags/editflags.go +++ b/cmd/limactl/editflags/editflags.go @@ -15,6 +15,8 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" flag "github.com/spf13/pflag" + + "github.com/lima-vm/lima/v2/pkg/registry" ) // RegisterEdit registers flags related to in-place YAML modification, for `limactl edit`. @@ -75,6 +77,15 @@ func RegisterEdit(cmd *cobra.Command, commentPrefix string) { _ = cmd.RegisterFlagCompletionFunc("disk", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return []string{"10", "30", "50", "100", "200"}, cobra.ShellCompDirectiveNoFileComp }) + + flags.String("vm-type", "", commentPrefix+"Virtual machine type") + _ = cmd.RegisterFlagCompletionFunc("vm-type", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + var drivers []string + for k := range registry.List() { + drivers = append(drivers, k) + } + return drivers, cobra.ShellCompDirectiveNoFileComp + }) } // RegisterCreate registers flags related to in-place YAML modification, for `limactl create`. @@ -92,11 +103,6 @@ func RegisterCreate(cmd *cobra.Command, commentPrefix string) { return []string{"user", "system", "user+system", "none"}, cobra.ShellCompDirectiveNoFileComp }) - flags.String("vm-type", "", commentPrefix+"Virtual machine type (qemu, vz)") // colima-compatible - _ = cmd.RegisterFlagCompletionFunc("vm-type", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return []string{"qemu", "vz"}, cobra.ShellCompDirectiveNoFileComp - }) - flags.Bool("plain", false, commentPrefix+"Plain mode. Disables mounts, port forwarding, containerd, etc.") } @@ -177,6 +183,7 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { false, }, {"mount-type", d(".mountType = %q"), false, false}, + {"vm-type", d(".vmType = %q"), false, false}, {"mount-inotify", d(".mountInotify = %s"), false, true}, {"mount-writable", d(".mounts[].writable = %s"), false, false}, { @@ -262,7 +269,6 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) { false, }, {"disk", d(".disk= \"%sGiB\""), false, false}, - {"vm-type", d(".vmType = %q"), true, false}, {"plain", d(".plain = %s"), true, false}, } var exprs []string diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go index 8b5c5a51c22..d5a51ea40c8 100644 --- a/cmd/limactl/main.go +++ b/cmd/limactl/main.go @@ -44,6 +44,7 @@ func main() { } rootCmd := newApp() if err := executeWithPluginSupport(rootCmd, os.Args[1:]); err != nil { + server.StopAllExternalDrivers() handleExitCoder(err) logrus.Fatal(err) } diff --git a/cmd/limactl/prune.go b/cmd/limactl/prune.go index df186da63b9..c7ec78f00b9 100644 --- a/cmd/limactl/prune.go +++ b/cmd/limactl/prune.go @@ -5,6 +5,7 @@ package main import ( "context" + "fmt" "maps" "os" @@ -12,6 +13,7 @@ import ( "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/pkg/downloader" + "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/store" @@ -95,6 +97,9 @@ func knownLocations(ctx context.Context) (map[string]limatype.File, error) { if err != nil { return nil, err } + if err := driverutil.ResolveVMType(y, t.Name); err != nil { + return nil, fmt.Errorf("failed to accept config for %q: %w", t.Name, err) + } maps.Copy(locations, locationsFromLimaYAML(y)) } return locations, nil diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go index 358674723d0..c3dd8261f80 100644 --- a/cmd/limactl/start.go +++ b/cmd/limactl/start.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/pflag" "github.com/lima-vm/lima/v2/cmd/limactl/editflags" + "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/editutil" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/limatmpl" @@ -362,6 +363,9 @@ func applyYQExpressionToExistingInstance(ctx context.Context, inst *limatype.Ins if err != nil { return nil, err } + if err := driverutil.ResolveVMType(y, filePath); err != nil { + return nil, fmt.Errorf("failed to accept config for %q: %w", filePath, err) + } if err := limayaml.Validate(y, true); err != nil { rejectedYAML := "lima.REJECTED.yaml" if writeErr := os.WriteFile(rejectedYAML, yBytes, 0o644); writeErr != nil { diff --git a/cmd/limactl/template.go b/cmd/limactl/template.go index dfb1b3f7e42..7913bf4e2bd 100644 --- a/cmd/limactl/template.go +++ b/cmd/limactl/template.go @@ -13,6 +13,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/limatmpl" "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" "github.com/lima-vm/lima/v2/pkg/limayaml" @@ -87,6 +88,10 @@ func fillDefaults(ctx context.Context, tmpl *limatmpl.Template) error { if err == nil { tmpl.Bytes, err = limayaml.Marshal(tmpl.Config, false) } + if err := driverutil.ResolveVMType(tmpl.Config, filePath); err != nil { + logrus.Warnf("failed to resolve VM type for %q: %v", filePath, err) + return nil + } return err } @@ -246,6 +251,10 @@ func templateValidateAction(cmd *cobra.Command, args []string) error { if err != nil { return err } + if err := driverutil.ResolveVMType(y, filePath); err != nil { + logrus.Warnf("failed to resolve VM type for %q: %v", filePath, err) + return nil + } if err := limayaml.Validate(y, false); err != nil { return fmt.Errorf("failed to validate YAML file %q: %w", arg, err) } diff --git a/hack/test-upgrade.sh b/hack/test-upgrade.sh index 696295f6098..e9be6c15636 100755 --- a/hack/test-upgrade.sh +++ b/hack/test-upgrade.sh @@ -95,8 +95,11 @@ INFO "========================================================================== INFO "Installing the new Lima ${NEWVER}" install_lima "${NEWVER}" +INFO "Editing the instance to specify vm-type as qemu explicitly" +limactl edit --vm-type=qemu "${LIMA_INSTANCE}" + INFO "Restarting the instance" -limactl start --tty=false "${LIMA_INSTANCE}" || show_lima_log +limactl start --tty=false --vm-type=qemu "${LIMA_INSTANCE}" || show_lima_log lima nerdctl info INFO "Confirming that the host filesystem is still mounted" diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index ec5432f39ee..7e633018d0e 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -24,6 +24,7 @@ import ( "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/debugutil" + "github.com/lima-vm/lima/v2/pkg/driver" "github.com/lima-vm/lima/v2/pkg/instance/hostname" "github.com/lima-vm/lima/v2/pkg/iso9660util" "github.com/lima-vm/lima/v2/pkg/limatype" @@ -137,14 +138,19 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i Containerd: Containerd{System: *instConfig.Containerd.System, User: *instConfig.Containerd.User, Archive: archive}, SlirpNICName: networks.SlirpNICName, - RosettaEnabled: *instConfig.Rosetta.Enabled, - RosettaBinFmt: *instConfig.Rosetta.BinFmt, - VMType: *instConfig.VMType, - VSockPort: vsockPort, - VirtioPort: virtioPort, - Plain: *instConfig.Plain, - TimeZone: *instConfig.TimeZone, - Param: instConfig.Param, + VMType: *instConfig.VMType, + VSockPort: vsockPort, + VirtioPort: virtioPort, + Plain: *instConfig.Plain, + TimeZone: *instConfig.TimeZone, + Param: instConfig.Param, + } + + if instConfig.VMOpts.VZ.Rosetta.Enabled != nil { + args.RosettaEnabled = *instConfig.VMOpts.VZ.Rosetta.Enabled + } + if instConfig.VMOpts.VZ.Rosetta.BinFmt != nil { + args.RosettaEnabled = *instConfig.VMOpts.VZ.Rosetta.BinFmt } firstUsernetIndex := limayaml.FirstUsernetIndex(instConfig) @@ -357,7 +363,7 @@ func GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig * return os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444) } -func GenerateISO9660(ctx context.Context, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string) error { +func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string) error { args, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort) if err != nil { return err @@ -372,6 +378,18 @@ func GenerateISO9660(ctx context.Context, instDir, name string, instConfig *lima return err } + driverScripts, err := drv.BootScripts() + if err != nil { + return fmt.Errorf("failed to get boot scripts: %w", err) + } + + for filename, content := range driverScripts { + layout = append(layout, iso9660util.Entry{ + Path: fmt.Sprintf("boot/%s", filename), + Reader: strings.NewReader(string(content)), + }) + } + for i, f := range instConfig.Provision { switch f.Mode { case limatype.ProvisionModeSystem, limatype.ProvisionModeUser, limatype.ProvisionModeDependency: diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 9fd0e0b2fd3..63820fd80d5 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -15,13 +15,13 @@ type Lifecycle interface { // Validate returns error if the current driver isn't support for given config Validate(_ context.Context) error - // Initialize is called on creating the instance for initialization. + // Create is called on creating the instance for the first time. // (e.g., creating "vz-identifier" file) // - // Initialize MUST return nil when it is called against an existing instance. + // Create MUST return nil when it is called against an existing instance. // - // Initialize does not create the disks. - Initialize(_ context.Context) error + // Create does not create the disks. + Create(_ context.Context) error // CreateDisk returns error if the current driver fails in creating disk CreateDisk(_ context.Context) error @@ -34,6 +34,12 @@ type Lifecycle interface { // Stop will terminate the running vm instance. // It returns error if there are any errors during Stop Stop(_ context.Context) error + + Delete(_ context.Context) error + + InspectStatus(_ context.Context, inst *limatype.Instance) string + + BootScripts() (map[string][]byte, error) } // GUI defines GUI-related operations. @@ -54,12 +60,6 @@ type SnapshotManager interface { ListSnapshots(ctx context.Context) (string, error) } -// Registration defines operations for registering and unregistering the driver instance. -type Registration interface { - Register(ctx context.Context) error - Unregister(ctx context.Context) error -} - // GuestAgent defines operations for the guest agent. type GuestAgent interface { // ForwardGuestAgent returns if the guest agent sock needs forwarding by host agent. @@ -74,13 +74,17 @@ type Driver interface { Lifecycle GUI SnapshotManager - Registration GuestAgent Info() Info - // SetConfig sets the configuration for the instance. + // Configure sets the configuration for the instance. Configure(inst *limatype.Instance) *ConfiguredDriver + + AcceptConfig(cfg *limatype.LimaYAML, filePath string) error + FillConfig(cfg *limatype.LimaYAML, filePath string) error + + SSHAddress(ctx context.Context) (string, error) } type ConfiguredDriver struct { @@ -88,9 +92,15 @@ type ConfiguredDriver struct { } type Info struct { - DriverName string `json:"driverName"` - CanRunGUI bool `json:"canRunGui,omitempty"` - VsockPort int `json:"vsockPort"` - VirtioPort string `json:"virtioPort"` - InstanceDir string `json:"instanceDir,omitempty"` + DriverName string `json:"driverName"` + CanRunGUI bool `json:"canRunGui,omitempty"` + VsockPort int `json:"vsockPort"` + VirtioPort string `json:"virtioPort"` + InstanceDir string `json:"instanceDir,omitempty"` + Features DriverFeatures `json:"features"` +} + +type DriverFeatures struct { + DynamicSSHAddress bool `json:"dynamicSSHAddress"` + SkipSocketForwarding bool `json:"skipSocketForwarding"` } diff --git a/pkg/driver/external/client/methods.go b/pkg/driver/external/client/methods.go index 4793cf84e6e..cb251d12cb9 100644 --- a/pkg/driver/external/client/methods.go +++ b/pkg/driver/external/client/methods.go @@ -32,17 +32,8 @@ func (d *DriverClient) Validate(ctx context.Context) error { return nil } -func (d *DriverClient) Initialize(ctx context.Context) error { - d.logger.Debug("Initializing driver instance") - - _, err := d.DriverSvc.Initialize(ctx, &emptypb.Empty{}) - if err != nil { - d.logger.Errorf("Initialization failed: %v", err) - return err - } - - d.logger.Debug("Driver instance initialized successfully") - return nil +func (d *DriverClient) Create(_ context.Context) error { + return errors.New("create not implemented for external drivers") } func (d *DriverClient) CreateDisk(ctx context.Context) error { @@ -102,6 +93,18 @@ func (d *DriverClient) Stop(ctx context.Context) error { return nil } +func (d *DriverClient) Delete(_ context.Context) error { + return errors.New("delete not implemented for external drivers") +} + +func (d *DriverClient) AcceptConfig(_ *limatype.LimaYAML, _ string) error { + return errors.New("pre-configured driver action not implemented in client driver") +} + +func (d *DriverClient) FillConfig(_ *limatype.LimaYAML, _ string) error { + return errors.New("pre-configured driver action not implemented in client driver") +} + func (d *DriverClient) RunGUI() error { d.logger.Debug("Running GUI for the driver instance") @@ -204,32 +207,6 @@ func (d *DriverClient) ListSnapshots(ctx context.Context) (string, error) { return resp.Snapshots, nil } -func (d *DriverClient) Register(ctx context.Context) error { - d.logger.Debug("Registering driver instance") - - _, err := d.DriverSvc.Register(ctx, &emptypb.Empty{}) - if err != nil { - d.logger.Errorf("Failed to register driver instance: %v", err) - return err - } - - d.logger.Debug("Driver instance registered successfully") - return nil -} - -func (d *DriverClient) Unregister(ctx context.Context) error { - d.logger.Debug("Unregistering driver instance") - - _, err := d.DriverSvc.Unregister(ctx, &emptypb.Empty{}) - if err != nil { - d.logger.Errorf("Failed to unregister driver instance: %v", err) - return err - } - - d.logger.Debug("Driver instance unregistered successfully") - return nil -} - func (d *DriverClient) ForwardGuestAgent() bool { d.logger.Debug("Checking if guest agent needs to be forwarded") @@ -303,3 +280,15 @@ func (d *DriverClient) Configure(inst *limatype.Instance) *driver.ConfiguredDriv Driver: d, } } + +func (d *DriverClient) InspectStatus(_ context.Context, _ *limatype.Instance) string { + return "" +} + +func (d *DriverClient) SSHAddress(_ context.Context) (string, error) { + return "", errors.New("sshAddress not implemented for external drivers") +} + +func (d *DriverClient) BootScripts() (map[string][]byte, error) { + return nil, errors.New("bootScripts not implemented for external drivers") +} diff --git a/pkg/driver/external/server/methods.go b/pkg/driver/external/server/methods.go index 33c49c1f680..176b5c9cc11 100644 --- a/pkg/driver/external/server/methods.go +++ b/pkg/driver/external/server/methods.go @@ -130,7 +130,7 @@ func (s *DriverServer) Validate(ctx context.Context, empty *emptypb.Empty) (*emp func (s *DriverServer) Initialize(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { s.logger.Debug("Received Initialize request") - err := s.driver.Initialize(ctx) + err := s.driver.Create(ctx) if err != nil { s.logger.Errorf("Initialization failed: %v", err) return empty, err @@ -238,28 +238,6 @@ func (s *DriverServer) ListSnapshots(ctx context.Context, _ *emptypb.Empty) (*pb return &pb.ListSnapshotsResponse{Snapshots: snapshots}, nil } -func (s *DriverServer) Register(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { - s.logger.Debug("Received Register request") - err := s.driver.Register(ctx) - if err != nil { - s.logger.Errorf("Register failed: %v", err) - return empty, err - } - s.logger.Debug("Register succeeded") - return empty, nil -} - -func (s *DriverServer) Unregister(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) { - s.logger.Debug("Received Unregister request") - err := s.driver.Unregister(ctx) - if err != nil { - s.logger.Errorf("Unregister failed: %v", err) - return empty, err - } - s.logger.Debug("Unregister succeeded") - return empty, nil -} - func (s *DriverServer) ForwardGuestAgent(_ context.Context, _ *emptypb.Empty) (*pb.ForwardGuestAgentResponse, error) { s.logger.Debug("Received ForwardGuestAgent request") return &pb.ForwardGuestAgentResponse{ShouldForward: s.driver.ForwardGuestAgent()}, nil diff --git a/pkg/driver/qemu/qemu_driver.go b/pkg/driver/qemu/qemu_driver.go index 685885b991e..2310ad6df18 100644 --- a/pkg/driver/qemu/qemu_driver.go +++ b/pkg/driver/qemu/qemu_driver.go @@ -16,6 +16,7 @@ import ( "os/exec" "path/filepath" "runtime" + "slices" "strconv" "strings" "text/template" @@ -34,6 +35,8 @@ import ( "github.com/lima-vm/lima/v2/pkg/limayaml" "github.com/lima-vm/lima/v2/pkg/networks/usernet" "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/lima-vm/lima/v2/pkg/ptr" + "github.com/lima-vm/lima/v2/pkg/version/versionutil" ) type LimaQemuDriver struct { @@ -79,14 +82,155 @@ func (l *LimaQemuDriver) Validate(ctx context.Context) error { return err } } + if err := l.validateMountType(); err != nil { + return err + } + + return nil +} - if *l.Instance.Config.MountType == limatype.VIRTIOFS && runtime.GOOS != "linux" { +// Helper method for mount type validation. +func (l *LimaQemuDriver) validateMountType() error { + if l.Instance == nil || l.Instance.Config == nil { + return errors.New("instance configuration is not set") + } + + cfg := l.Instance.Config + + if cfg.MountType != nil && *cfg.MountType == limatype.VIRTIOFS && runtime.GOOS != "linux" { return fmt.Errorf("field `mountType` must be %q or %q for QEMU driver on non-Linux, got %q", - limatype.REVSSHFS, limatype.NINEP, *l.Instance.Config.MountType) + limatype.REVSSHFS, limatype.NINEP, *cfg.MountType) + } + if cfg.MountTypesUnsupported != nil && cfg.MountType != nil && slices.Contains(cfg.MountTypesUnsupported, *cfg.MountType) { + return fmt.Errorf("mount type %q is explicitly unsupported", *cfg.MountType) } + if runtime.GOOS == "windows" && cfg.MountType != nil && *cfg.MountType == limatype.NINEP { + return fmt.Errorf("mount type %q is not supported on Windows", limatype.NINEP) + } + return nil } +func (l *LimaQemuDriver) FillConfig(cfg *limatype.LimaYAML, filePath string) error { + if cfg.VMType == nil { + cfg.VMType = ptr.Of(limatype.QEMU) + } + + instDir := filepath.Dir(filePath) + + if cfg.Video.VNC.Display == nil || *cfg.Video.VNC.Display == "" { + cfg.Video.VNC.Display = ptr.Of("127.0.0.1:0,to=9") + } + + if cfg.VMOpts.QEMU.CPUType == nil { + cfg.VMOpts.QEMU.CPUType = limatype.CPUType{} + } + + //nolint:staticcheck // Migration of top-level CPUTYPE if specified + if len(cfg.CPUType) > 0 { + logrus.Warn("The top-level `cpuType` field is deprecated and will be removed in a future release. Please migrate to `vmOpts.qemu.cpuType`.") + for arch, v := range cfg.CPUType { + if v == "" { + continue + } + if existing, ok := cfg.VMOpts.QEMU.CPUType[arch]; ok && existing != "" && existing != v { + logrus.Warnf("Conflicting cpuType for arch %q: top-level=%q, vmOpts.qemu=%q; using vmOpts.qemu value", arch, v, existing) + continue + } + cfg.VMOpts.QEMU.CPUType[arch] = v + } + cfg.CPUType = nil + } + + mountTypesUnsupported := make(map[string]struct{}) + for _, f := range cfg.MountTypesUnsupported { + mountTypesUnsupported[f] = struct{}{} + } + + if runtime.GOOS == "windows" { + // QEMU for Windows does not support 9p + mountTypesUnsupported[limatype.NINEP] = struct{}{} + } + + if cfg.MountType == nil || *cfg.MountType == "" || *cfg.MountType == "default" { + cfg.MountType = ptr.Of(limatype.NINEP) + if _, ok := mountTypesUnsupported[limatype.NINEP]; ok { + // Use REVSSHFS if the instance does not support 9p + cfg.MountType = ptr.Of(limatype.REVSSHFS) + } else if limayaml.IsExistingInstanceDir(instDir) && !versionutil.GreaterEqual(limayaml.ExistingLimaVersion(instDir), "1.0.0") { + // Use REVSSHFS if the instance was created with Lima prior to v1.0 + cfg.MountType = ptr.Of(limatype.REVSSHFS) + } + } + + for i := range cfg.Mounts { + mount := &cfg.Mounts[i] + if mount.Virtiofs.QueueSize == nil && *cfg.MountType == limatype.VIRTIOFS { + cfg.Mounts[i].Virtiofs.QueueSize = ptr.Of(limayaml.DefaultVirtiofsQueueSize) + } + } + + if _, ok := mountTypesUnsupported[*cfg.MountType]; ok { + return fmt.Errorf("mount type %q is explicitly unsupported", *cfg.MountType) + } + + return nil +} + +func (l *LimaQemuDriver) AcceptConfig(cfg *limatype.LimaYAML, _ string) error { + if l.Instance == nil { + l.Instance = &limatype.Instance{} + } + l.Instance.Config = cfg + + if err := l.Validate(context.Background()); err != nil { + return fmt.Errorf("config not supported by the QEMU driver: %w", err) + } + + if cfg.VMOpts.QEMU.MinimumVersion != nil { + if _, err := semver.NewVersion(*cfg.VMOpts.QEMU.MinimumVersion); err != nil { + return fmt.Errorf("field `vmOpts.qemu.minimumVersion` must be a semvar value, got %q: %w", *cfg.VMOpts.QEMU.MinimumVersion, err) + } + } + + if runtime.GOOS == "darwin" { + if cfg.Arch != nil && limayaml.IsNativeArch(*cfg.Arch) { + logrus.Debugf("ResolveVMType: resolved VMType %q (non-native arch=%q is specified in []*LimaYAML{o,y,d})", "qemu", *cfg.Arch) + return nil + } + if limayaml.ResolveArch(cfg.Arch) == limatype.X8664 && cfg.Firmware.LegacyBIOS != nil && *cfg.Firmware.LegacyBIOS { + logrus.Debugf("ResolveVMType: resolved VMType %q (firmware.legacyBIOS is specified in []*LimaYAML{o,y,d} on x86_64)", "qemu") + return nil + } + if cfg.MountType != nil && *cfg.MountType == limatype.NINEP { + logrus.Debugf("ResolveVMType: resolved VMType %q (mountType=%q is specified in []*LimaYAML{o,y,d})", "qemu", limatype.NINEP) + return nil + } + if cfg.Audio.Device != nil { + switch *cfg.Audio.Device { + case "", "none", "default", "vz": + // NOP + default: + logrus.Debugf("ResolveVMType: resolved VMType %q (audio.device=%q is specified in []*LimaYAML{o,y,d})", "qemu", *cfg.Audio.Device) + return nil + } + } + if cfg.Video.Display != nil { + display := *cfg.Video.Display + if display != "" && display != "none" && display != "default" && display != "vz" { + logrus.Debugf("ResolveVMType: resolved VMType %q (video.display=%q is specified in []*LimaYAML{o,y,d})", "qemu", display) + return nil + } + } + } + + return nil +} + +func (l *LimaQemuDriver) BootScripts() (map[string][]byte, error) { + return nil, nil +} + func (l *LimaQemuDriver) CreateDisk(ctx context.Context) error { qCfg := Config{ Name: l.Instance.Name, @@ -276,7 +420,7 @@ func (l *LimaQemuDriver) checkBinarySignature(ctx context.Context) error { return err } // The codesign --xml option is only available on macOS Monterey and later - if !macOSProductVersion.LessThan(*semver.New("12.0.0")) { + if !macOSProductVersion.LessThan(*semver.New("12.0.0")) && l.Instance.Arch != "" { qExe, _, err := Exe(l.Instance.Arch) if err != nil { return fmt.Errorf("failed to find the QEMU binary for the architecture %q: %w", l.Instance.Arch, err) @@ -537,10 +681,27 @@ func (l *LimaQemuDriver) Info() driver.Info { info.CanRunGUI = false info.VirtioPort = l.virtioPort info.VsockPort = l.vSockPort + + info.Features = driver.DriverFeatures{ + DynamicSSHAddress: false, + SkipSocketForwarding: false, + } return info } -func (l *LimaQemuDriver) Initialize(_ context.Context) error { +func (l *LimaQemuDriver) SSHAddress(_ context.Context) (string, error) { + return "127.0.0.1", nil +} + +func (l *LimaQemuDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string { + return "" +} + +func (l *LimaQemuDriver) Create(_ context.Context) error { + return nil +} + +func (l *LimaQemuDriver) Delete(_ context.Context) error { return nil } diff --git a/pkg/driver/vz/boot/05-rosetta-volume.sh b/pkg/driver/vz/boot/05-rosetta-volume.sh new file mode 100755 index 00000000000..4a43ff3fc69 --- /dev/null +++ b/pkg/driver/vz/boot/05-rosetta-volume.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + +set -eux -o pipefail + +if [ "$LIMA_CIDATA_ROSETTA_ENABLED" != "true" ]; then + exit 0 +fi + +if [ -f /etc/alpine-release ]; then + rc-service procfs start --ifnotstarted + rc-service qemu-binfmt stop --ifexists --ifstarted +fi + +binfmt_entry=/proc/sys/fs/binfmt_misc/rosetta +binfmtd_conf=/usr/lib/binfmt.d/rosetta.conf +if [ "$LIMA_CIDATA_ROSETTA_BINFMT" = "true" ]; then + rosetta_binfmt=":rosetta:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00:\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/mnt/lima-rosetta/rosetta:OCF" + + # If rosetta is not registered in binfmt_misc, register it. + [ -f "$binfmt_entry" ] || echo "$rosetta_binfmt" >/proc/sys/fs/binfmt_misc/register + + # Create binfmt.d(5) configuration to prioritize rosetta even if qemu-user-static is installed on systemd based systems. + # If the binfmt.d directory exists, consider systemd-binfmt.service(8) to be enabled and create the configuration file. + [ ! -d "$(dirname "$binfmtd_conf")" ] || [ -f "$binfmtd_conf" ] || echo "$rosetta_binfmt" >"$binfmtd_conf" +else + # unregister rosetta from binfmt_misc if it exists + [ ! -f "$binfmt_entry" ] || echo -1 | "$binfmt_entry" + # remove binfmt.d(5) configuration if it exists + [ ! -f "$binfmtd_conf" ] || rm "$binfmtd_conf" +fi diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index d6cc08e746a..342f73fec19 100644 --- a/pkg/driver/vz/vm_darwin.go +++ b/pkg/driver/vz/vm_darwin.go @@ -578,7 +578,7 @@ func attachFolderMounts(inst *limatype.Instance, vmConfig *vz.VirtualMachineConf } } - if *inst.Config.Rosetta.Enabled { + if *inst.Config.VMOpts.VZ.Rosetta.Enabled { logrus.Info("Setting up Rosetta share") directorySharingDeviceConfig, err := createRosettaDirectoryShareConfiguration() if err != nil { diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go index d0b6487ce53..d21870da9f4 100644 --- a/pkg/driver/vz/vz_driver_darwin.go +++ b/pkg/driver/vz/vz_driver_darwin.go @@ -7,9 +7,11 @@ package vz import ( "context" + "embed" "errors" "fmt" "net" + "os" "path/filepath" "runtime" "time" @@ -20,8 +22,10 @@ import ( "github.com/lima-vm/lima/v2/pkg/driver" "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/osutil" + "github.com/lima-vm/lima/v2/pkg/ptr" "github.com/lima-vm/lima/v2/pkg/reflectutil" ) @@ -64,6 +68,7 @@ var knownYamlProperties = []string{ "User", "Video", "VMType", + "VMOpts", } const Enabled = true @@ -91,11 +96,122 @@ func (l *LimaVzDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriv l.Instance = inst l.SSHLocalPort = inst.SSHLocalPort + if l.Instance.Config.MountType != nil { + mountTypesUnsupported := make(map[string]struct{}) + for _, f := range l.Instance.Config.MountTypesUnsupported { + mountTypesUnsupported[f] = struct{}{} + } + + if _, ok := mountTypesUnsupported[*l.Instance.Config.MountType]; ok { + // We cannot return an error here, but Validate() will return it. + logrus.Warnf("Unsupported mount type: %q", *l.Instance.Config.MountType) + } + } + return &driver.ConfiguredDriver{ Driver: l, } } +func (l *LimaVzDriver) FillConfig(cfg *limatype.LimaYAML, _ string) error { + if cfg.VMType == nil { + cfg.VMType = ptr.Of(limatype.VZ) + } + + if cfg.MountType == nil { + cfg.MountType = ptr.Of(limatype.VIRTIOFS) + } + + //nolint:staticcheck // Migration of top-level Rosetta if specified + if (cfg.VMOpts.VZ.Rosetta.Enabled == nil && cfg.VMOpts.VZ.Rosetta.BinFmt == nil) && (!isEmpty(cfg.Rosetta)) { + logrus.Debug("Migrating top-level Rosetta configuration to vmOpts.vz.rosetta") + cfg.VMOpts.VZ.Rosetta = cfg.Rosetta + } + //nolint:staticcheck // Warning about both top-level and vmOpts.vz.Rosetta being set + if (cfg.VMOpts.VZ.Rosetta.Enabled != nil && cfg.VMOpts.VZ.Rosetta.BinFmt != nil) && (!isEmpty(cfg.Rosetta)) { + logrus.Warn("Both top-level 'rosetta' and 'vmOpts.vz.rosetta' are configured. Using vmOpts.vz.rosetta. Top-level 'rosetta' is deprecated.") + } + + if cfg.VMOpts.VZ.Rosetta.Enabled == nil { + cfg.VMOpts.VZ.Rosetta.Enabled = ptr.Of(false) + } + if cfg.VMOpts.VZ.Rosetta.BinFmt == nil { + cfg.VMOpts.VZ.Rosetta.BinFmt = ptr.Of(false) + } + + return nil +} + +func isEmpty(r limatype.Rosetta) bool { + return r.Enabled == nil && r.BinFmt == nil +} + +//go:embed boot/*.sh +var bootFS embed.FS + +func (l *LimaVzDriver) BootScripts() (map[string][]byte, error) { + scripts := make(map[string][]byte) + + entries, err := bootFS.ReadDir("boot") + if err == nil { + for _, entry := range entries { + if entry.IsDir() { + continue + } + + content, err := bootFS.ReadFile("boot/" + entry.Name()) + if err != nil { + return nil, err + } + + scripts[entry.Name()] = content + } + } + + return scripts, nil +} + +func (l *LimaVzDriver) AcceptConfig(cfg *limatype.LimaYAML, filePath string) error { + if dir, basename := filepath.Split(filePath); dir != "" && basename == filenames.LimaYAML && limayaml.IsExistingInstanceDir(dir) { + vzIdentifier := filepath.Join(dir, filenames.VzIdentifier) // since Lima v0.14 + if _, err := os.Lstat(vzIdentifier); !errors.Is(err, os.ErrNotExist) { + logrus.Debugf("ResolveVMType: resolved VMType %q (existing instance, with %q)", "vz", vzIdentifier) + } + } + + for i, nw := range cfg.Networks { + field := fmt.Sprintf("networks[%d]", i) + switch { + case nw.Lima != "": + if nw.VZNAT != nil && *nw.VZNAT { + return fmt.Errorf("field `%s.lima` and field `%s.vzNAT` are mutually exclusive", field, field) + } + case nw.Socket != "": + if nw.VZNAT != nil && *nw.VZNAT { + return fmt.Errorf("field `%s.socket` and field `%s.vzNAT` are mutually exclusive", field, field) + } + case nw.VZNAT != nil && *nw.VZNAT: + if nw.Lima != "" { + return fmt.Errorf("field `%s.vzNAT` and field `%s.lima` are mutually exclusive", field, field) + } + if nw.Socket != "" { + return fmt.Errorf("field `%s.vzNAT` and field `%s.socket` are mutually exclusive", field, field) + } + } + } + + if l.Instance == nil { + l.Instance = &limatype.Instance{} + } + l.Instance.Config = cfg + + if err := l.Validate(context.Background()); err != nil { + return fmt.Errorf("config not supported by the VZ driver: %w", err) + } + + return nil +} + func (l *LimaVzDriver) Validate(_ context.Context) error { macOSProductVersion, err := osutil.ProductVersion() if err != nil { @@ -109,7 +225,7 @@ func (l *LimaVzDriver) Validate(_ context.Context) error { "Update macOS, or change vmType to \"qemu\" if the VM does not start up. (https://github.com/lima-vm/lima/issues/3334)", *l.Instance.Config.VMType) } - if *l.Instance.Config.MountType == limatype.NINEP { + if l.Instance.Config.MountType != nil && *l.Instance.Config.MountType == limatype.NINEP { return fmt.Errorf("field `mountType` must be %q or %q for VZ driver , got %q", limatype.REVSSHFS, limatype.VIRTIOFS, *l.Instance.Config.MountType) } if *l.Instance.Config.Firmware.LegacyBIOS { @@ -123,7 +239,7 @@ func (l *LimaVzDriver) Validate(_ context.Context) error { } } } - if unknown := reflectutil.UnknownNonEmptyFields(l.Instance.Config, knownYamlProperties...); len(unknown) > 0 { + if unknown := reflectutil.UnknownNonEmptyFields(l.Instance.Config, knownYamlProperties...); l.Instance.Config.VMType != nil && len(unknown) > 0 { logrus.Warnf("vmType %s: ignoring %+v", *l.Instance.Config.VMType, unknown) } @@ -175,7 +291,7 @@ func (l *LimaVzDriver) Validate(_ context.Context) error { return nil } -func (l *LimaVzDriver) Initialize(_ context.Context) error { +func (l *LimaVzDriver) Create(_ context.Context) error { _, err := getMachineIdentifier(l.Instance) return err } @@ -265,9 +381,26 @@ func (l *LimaVzDriver) Info() driver.Info { if l.Instance != nil { info.InstanceDir = l.Instance.Dir } + + info.Features = driver.DriverFeatures{ + DynamicSSHAddress: false, + SkipSocketForwarding: false, + } return info } +func (l *LimaVzDriver) SSHAddress(_ context.Context) (string, error) { + return "127.0.0.1", nil +} + +func (l *LimaVzDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string { + return "" +} + +func (l *LimaVzDriver) Delete(_ context.Context) error { + return nil +} + func (l *LimaVzDriver) Register(_ context.Context) error { return nil } diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh b/pkg/driver/wsl2/boot/02-wsl2-setup.sh similarity index 100% rename from pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh rename to pkg/driver/wsl2/boot/02-wsl2-setup.sh diff --git a/pkg/driver/wsl2/vm_windows.go b/pkg/driver/wsl2/vm_windows.go index 2ae3af698e8..6360127083f 100644 --- a/pkg/driver/wsl2/vm_windows.go +++ b/pkg/driver/wsl2/vm_windows.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" @@ -18,7 +19,6 @@ import ( "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/store" "github.com/lima-vm/lima/v2/pkg/textutil" ) @@ -134,7 +134,7 @@ func provisionVM(ctx context.Context, instanceDir, instanceName, distroName stri for { <-ctx.Done() logrus.Info("Context closed, stopping vm") - if status, err := store.GetWslStatus(instanceName); err == nil && + if status, err := getWslStatus(instanceName); err == nil && status == limatype.StatusRunning { _ = stopVM(ctx, distroName) } @@ -178,3 +178,54 @@ func unregisterVM(ctx context.Context, distroName string) error { } return nil } + +func getWslStatus(instName string) (string, error) { + distroName := "lima-" + instName + out, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--list", + "--verbose", + }) + if err != nil { + return "", fmt.Errorf("failed to run `wsl --list --verbose`, err: %w (out=%q)", err, out) + } + + if out == "" { + return limatype.StatusBroken, fmt.Errorf("failed to read instance state for instance %q, try running `wsl --list --verbose` to debug, err: %w", instName, err) + } + + // Check for edge cases first + if strings.Contains(out, "Windows Subsystem for Linux has no installed distributions.") { + if strings.Contains(out, "Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND") { + return limatype.StatusBroken, fmt.Errorf( + "failed to read instance state for instance %q because no distro is installed,"+ + "try running `wsl --install -d Ubuntu` and then re-running Lima", instName) + } + return limatype.StatusBroken, fmt.Errorf( + "failed to read instance state for instance %q because there is no WSL kernel installed,"+ + "this usually happens when WSL was installed for another user, but never for your user."+ + "Try running `wsl --install -d Ubuntu` and `wsl --update`, and then re-running Lima", instName) + } + + var instState string + wslListColsRegex := regexp.MustCompile(`\s+`) + // wsl --list --verbose may have different headers depending on localization, just split by line + for _, rows := range strings.Split(strings.ReplaceAll(out, "\r\n", "\n"), "\n") { + cols := wslListColsRegex.Split(strings.TrimSpace(rows), -1) + nameIdx := 0 + // '*' indicates default instance + if cols[0] == "*" { + nameIdx = 1 + } + if cols[nameIdx] == distroName { + instState = cols[nameIdx+1] + break + } + } + + if instState == "" { + return limatype.StatusUninitialized, nil + } + + return instState, nil +} diff --git a/pkg/driver/wsl2/wsl_driver_windows.go b/pkg/driver/wsl2/wsl_driver_windows.go index 9ae85b7a1a1..5d433897a98 100644 --- a/pkg/driver/wsl2/wsl_driver_windows.go +++ b/pkg/driver/wsl2/wsl_driver_windows.go @@ -5,6 +5,7 @@ package wsl2 import ( "context" + "embed" "fmt" "net" "regexp" @@ -17,8 +18,8 @@ import ( "github.com/lima-vm/lima/v2/pkg/freeport" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/lima-vm/lima/v2/pkg/ptr" "github.com/lima-vm/lima/v2/pkg/reflectutil" - "github.com/lima-vm/lima/v2/pkg/store" "github.com/lima-vm/lima/v2/pkg/windows" ) @@ -78,54 +79,155 @@ func (l *LimaWslDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDri } } +func (l *LimaWslDriver) AcceptConfig(cfg *limatype.LimaYAML, _ string) error { + if l.Instance == nil { + l.Instance = &limatype.Instance{} + } + l.Instance.Config = cfg + + if err := l.Validate(context.Background()); err != nil { + return fmt.Errorf("config not supported by the WSL2 driver: %w", err) + } + + return nil +} + +func (l *LimaWslDriver) FillConfig(cfg *limatype.LimaYAML, _ string) error { + if cfg.VMType == nil { + cfg.VMType = ptr.Of(limatype.WSL2) + } + if cfg.MountType == nil { + cfg.MountType = ptr.Of(limatype.WSLMount) + } + + return nil +} + func (l *LimaWslDriver) Validate(_ context.Context) error { - if *l.Instance.Config.MountType != limatype.WSLMount { + if l.Instance.Config.MountType != nil && *l.Instance.Config.MountType != limatype.WSLMount { return fmt.Errorf("field `mountType` must be %q for WSL2 driver, got %q", limatype.WSLMount, *l.Instance.Config.MountType) } // TODO: revise this list for WSL2 - if unknown := reflectutil.UnknownNonEmptyFields(l.Instance.Config, knownYamlProperties...); len(unknown) > 0 { - logrus.Warnf("Ignoring: vmType %s: %+v", *l.Instance.Config.VMType, unknown) + if l.Instance.Config.VMType != nil { + 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) + if l.Instance.Config.VMType != nil { + if l.Instance.Config.Images != nil && l.Instance.Config.Arch != nil { + // 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) + } + } + } + + if l.Instance.Config.Mounts != nil { + 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) + } + } } - 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) + + if l.Instance.Config.Networks != nil { + 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) + } + } + } + + if l.Instance.Config.Audio.Device != nil { + audioDevice := *l.Instance.Config.Audio.Device + if audioDevice != "" { + logrus.Warnf("Ignoring: vmType %s: `audio.device`: %+v", *l.Instance.Config.VMType, audioDevice) + } } } - 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) + return nil +} + +//go:embed boot/*.sh +var bootFS embed.FS + +func (l *LimaWslDriver) BootScripts() (map[string][]byte, error) { + scripts := make(map[string][]byte) + + entries, err := bootFS.ReadDir("boot") + if err != nil { + return scripts, err + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + content, err := bootFS.ReadFile("boot/" + entry.Name()) + if err != nil { + return nil, err } + + scripts[entry.Name()] = content } - 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) + return scripts, nil +} + +func (l *LimaWslDriver) InspectStatus(ctx context.Context, inst *limatype.Instance) string { + status, err := getWslStatus(inst.Name) + if err != nil { + inst.Status = limatype.StatusBroken + inst.Errors = append(inst.Errors, err) + } else { + inst.Status = status + } + + inst.SSHLocalPort = 22 + + if inst.Status == limatype.StatusRunning { + sshAddr, err := l.SSHAddress(ctx) + if err == nil { + inst.SSHAddress = sshAddr + } else { + inst.Errors = append(inst.Errors, err) } } - audioDevice := *l.Instance.Config.Audio.Device - if audioDevice != "" { - logrus.Warnf("Ignoring: vmType %s: `audio.device`: %+v", *l.Instance.Config.VMType, audioDevice) + return inst.Status +} + +func (l *LimaWslDriver) Delete(ctx context.Context) error { + distroName := "lima-" + l.Instance.Name + status, err := getWslStatus(l.Instance.Name) + if err != nil { + return err + } + switch status { + case limatype.StatusRunning, limatype.StatusStopped, limatype.StatusBroken, limatype.StatusInstalling: + return unregisterVM(ctx, distroName) } + logrus.Info("WSL VM is not running or does not exist, skipping deletion") return nil } func (l *LimaWslDriver) Start(ctx context.Context) (chan error, error) { logrus.Infof("Starting WSL VM") - status, err := store.GetWslStatus(l.Instance.Name) + status, err := getWslStatus(l.Instance.Name) if err != nil { return nil, err } @@ -178,21 +280,6 @@ func (l *LimaWslDriver) Stop(ctx context.Context) error { return stopVM(ctx, distroName) } -func (l *LimaWslDriver) Unregister(ctx context.Context) error { - distroName := "lima-" + l.Instance.Name - status, err := store.GetWslStatus(l.Instance.Name) - if err != nil { - return err - } - switch status { - case limatype.StatusRunning, limatype.StatusStopped, limatype.StatusBroken, limatype.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). // As of 08-01-2024, github.com/mdlayher/vsock does not natively support vsock on // Windows, so use the winio library to create the connection. @@ -226,18 +313,23 @@ func (l *LimaWslDriver) Info() driver.Info { info.CanRunGUI = l.canRunGUI() info.VirtioPort = l.virtioPort info.VsockPort = l.vSockPort + + info.Features = driver.DriverFeatures{ + DynamicSSHAddress: true, + SkipSocketForwarding: true, + } return info } -func (l *LimaWslDriver) Initialize(_ context.Context) error { - return nil +func (l *LimaWslDriver) SSHAddress(_ context.Context) (string, error) { + return "127.0.0.1", nil } -func (l *LimaWslDriver) CreateDisk(_ context.Context) error { +func (l *LimaWslDriver) Create(_ context.Context) error { return nil } -func (l *LimaWslDriver) Register(_ context.Context) error { +func (l *LimaWslDriver) CreateDisk(_ context.Context) error { return nil } diff --git a/pkg/driverutil/vm.go b/pkg/driverutil/vm.go new file mode 100644 index 00000000000..c1cc5af9e49 --- /dev/null +++ b/pkg/driverutil/vm.go @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package driverutil + +import ( + "context" + "errors" + "fmt" + + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/lima-vm/lima/v2/pkg/registry" +) + +func ResolveVMType(y *limatype.LimaYAML, filePath string) error { + if y.VMType != nil && *y.VMType != "" { + if err := validateConfigAgainstDriver(y, filePath, *y.VMType); err != nil { + return err + } + logrus.Debugf("Using specified vmType %q for %q", *y.VMType, filePath) + return nil + } + + // If VMType is not specified, we go with the default platform driver. + vmType := limatype.DefaultDriver() + return validateConfigAgainstDriver(y, filePath, vmType) +} + +func validateConfigAgainstDriver(y *limatype.LimaYAML, filePath, vmType string) error { + extDriver, intDriver, exists := registry.Get(vmType) + if !exists { + return fmt.Errorf("vmType %q is not a registered driver", vmType) + } + + if extDriver != nil { + return errors.New("not supported for external drivers") + } + + if err := intDriver.AcceptConfig(y, filePath); err != nil { + return err + } + if err := intDriver.FillConfig(y, filePath); err != nil { + return err + } + + return nil +} + +func InspectStatus(ctx context.Context, inst *limatype.Instance) (string, error) { + if inst == nil || inst.Config == nil || inst.Config.VMType == nil { + return "", errors.New("instance or its configuration is not properly initialized") + } + + extDriver, intDriver, exists := registry.Get(*inst.Config.VMType) + if !exists { + return "", fmt.Errorf("unknown or unsupported VM type: %s", *inst.Config.VMType) + } + + if extDriver != nil { + return "", errors.New("InspectStatus is not supported for external drivers") + } + + return intDriver.InspectStatus(ctx, inst), nil +} diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index d6730f56c78..bdc71e2d3e0 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -163,7 +163,7 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o if err := cidata.GenerateCloudConfig(ctx, inst.Dir, instName, inst.Config); err != nil { return nil, err } - if err := cidata.GenerateISO9660(ctx, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort); err != nil { + if err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort); err != nil { return nil, err } @@ -311,6 +311,9 @@ func (a *HostAgent) Run(ctx context.Context) error { if limayaml.FirstUsernetIndex(a.instConfig) == -1 && *a.instConfig.HostResolver.Enabled { hosts := a.instConfig.HostResolver.Hosts + if hosts == nil { + hosts = make(map[string]string) + } hosts["host.lima.internal"] = networks.SlirpGateway name := hostname.FromInstName(a.instName) // TODO: support customization hosts[name] = networks.SlirpIPAddress @@ -336,8 +339,8 @@ func (a *HostAgent) Run(ctx context.Context) error { } // WSL instance SSH address isn't known until after VM start - if *a.instConfig.VMType == limatype.WSL2 { - sshAddr, err := store.GetSSHAddress(ctx, a.instName) + if a.driver.Info().Features.DynamicSSHAddress { + sshAddr, err := a.driver.SSHAddress(ctx) if err != nil { return err } @@ -572,7 +575,7 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { // TODO: use vSock (when QEMU for macOS gets support for vSock) // Setup all socket forwards and defer their teardown - if *a.instConfig.VMType != limatype.WSL2 { + if !(a.driver.Info().Features.DynamicSSHAddress) { logrus.Debugf("Forwarding unix sockets") for _, rule := range a.instConfig.PortForwards { if rule.GuestSocket != "" { diff --git a/pkg/instance/create.go b/pkg/instance/create.go index 02cb5a9fb3a..8f6ef76e0eb 100644 --- a/pkg/instance/create.go +++ b/pkg/instance/create.go @@ -49,6 +49,9 @@ func Create(ctx context.Context, instName string, instConfig []byte, saveBrokenY if err != nil { return nil, err } + if err := driverutil.ResolveVMType(loadedInstConfig, filePath); err != nil { + return nil, fmt.Errorf("failed to accept config for %q: %w", filePath, err) + } if err := limayaml.Validate(loadedInstConfig, true); err != nil { if !saveBrokenYAML { return nil, err @@ -82,7 +85,7 @@ func Create(ctx context.Context, instName string, instConfig []byte, saveBrokenY return nil, fmt.Errorf("failed to create driver instance: %w", err) } - if err := limaDriver.Register(ctx); err != nil { + if err := limaDriver.Create(ctx); err != nil { return nil, err } diff --git a/pkg/instance/delete.go b/pkg/instance/delete.go index e8b27b4fdcb..df0954db5e6 100644 --- a/pkg/instance/delete.go +++ b/pkg/instance/delete.go @@ -41,5 +41,5 @@ func unregister(ctx context.Context, inst *limatype.Instance) error { return fmt.Errorf("failed to create driver instance: %w", err) } - return limaDriver.Unregister(ctx) + return limaDriver.Delete(ctx) } diff --git a/pkg/instance/start.go b/pkg/instance/start.go index b6348fe0433..a4357e7fe2d 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -97,7 +97,7 @@ func Prepare(ctx context.Context, inst *limatype.Instance) (*Prepared, error) { return nil, err } - if err := limaDriver.Initialize(ctx); err != nil { + if err := limaDriver.Create(ctx); err != nil { return nil, err } diff --git a/pkg/limatype/lima_instance.go b/pkg/limatype/lima_instance.go index 62af54313bb..308dd21ea51 100644 --- a/pkg/limatype/lima_instance.go +++ b/pkg/limatype/lima_instance.go @@ -31,7 +31,6 @@ type Instance struct { Dir string `json:"dir"` VMType VMType `json:"vmType"` Arch Arch `json:"arch"` - CPUType string `json:"cpuType"` CPUs int `json:"cpus,omitempty"` Memory int64 `json:"memory,omitempty"` // bytes Disk int64 `json:"disk,omitempty"` // bytes diff --git a/pkg/limatype/lima_yaml.go b/pkg/limatype/lima_yaml.go index 830d0bc615c..93008dfee1a 100644 --- a/pkg/limatype/lima_yaml.go +++ b/pkg/limatype/lima_yaml.go @@ -49,13 +49,14 @@ type LimaYAML struct { DNS []net.IP `yaml:"dns,omitempty" json:"dns,omitempty"` HostResolver HostResolver `yaml:"hostResolver,omitempty" json:"hostResolver,omitempty"` // `useHostResolver` was deprecated in Lima v0.8.1, removed in Lima v0.14.0. Use `hostResolver.enabled` instead. - PropagateProxyEnv *bool `yaml:"propagateProxyEnv,omitempty" json:"propagateProxyEnv,omitempty" jsonschema:"nullable"` - CACertificates CACertificates `yaml:"caCerts,omitempty" json:"caCerts,omitempty"` - Rosetta Rosetta `yaml:"rosetta,omitempty" json:"rosetta,omitempty"` - Plain *bool `yaml:"plain,omitempty" json:"plain,omitempty" jsonschema:"nullable"` - TimeZone *string `yaml:"timezone,omitempty" json:"timezone,omitempty" jsonschema:"nullable"` - NestedVirtualization *bool `yaml:"nestedVirtualization,omitempty" json:"nestedVirtualization,omitempty" jsonschema:"nullable"` - User User `yaml:"user,omitempty" json:"user,omitempty"` + PropagateProxyEnv *bool `yaml:"propagateProxyEnv,omitempty" json:"propagateProxyEnv,omitempty" jsonschema:"nullable"` + CACertificates CACertificates `yaml:"caCerts,omitempty" json:"caCerts,omitempty"` + // Deprecated: Use VMOpts.VZ.Rosetta instead. + Rosetta Rosetta `yaml:"rosetta,omitempty" json:"rosetta,omitempty"` + Plain *bool `yaml:"plain,omitempty" json:"plain,omitempty" jsonschema:"nullable"` + TimeZone *string `yaml:"timezone,omitempty" json:"timezone,omitempty" jsonschema:"nullable"` + NestedVirtualization *bool `yaml:"nestedVirtualization,omitempty" json:"nestedVirtualization,omitempty" jsonschema:"nullable"` + User User `yaml:"user,omitempty" json:"user,omitempty"` } type BaseTemplates []LocatorWithDigest @@ -111,6 +112,7 @@ type User struct { type VMOpts struct { QEMU QEMUOpts `yaml:"qemu,omitempty" json:"qemu,omitempty"` + VZ VZOpts `yaml:"vz,omitempty" json:"vz,omitempty"` } type QEMUOpts struct { @@ -118,6 +120,10 @@ type QEMUOpts struct { CPUType CPUType `yaml:"cpuType,omitempty" json:"cpuType,omitempty" jsonschema:"nullable"` } +type VZOpts struct { + Rosetta Rosetta `yaml:"rosetta,omitempty" json:"rosetta,omitempty"` +} + type Rosetta struct { Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty" jsonschema:"nullable"` BinFmt *bool `yaml:"binfmt,omitempty" json:"binfmt,omitempty" jsonschema:"nullable"` @@ -323,6 +329,16 @@ type CACertificates struct { Certs []string `yaml:"certs,omitempty" json:"certs,omitempty" jsonschema:"nullable"` } +type PreConfiguredDriverPayload struct { + Config LimaYAML `json:"config"` + FilePath string `json:"filePath"` +} + +type PreConfiguredDriverResponse struct { + Config LimaYAML `json:"config"` + Error string `json:"error,omitempty"` +} + func NewOS(osname string) OS { switch osname { case "linux": @@ -373,3 +389,14 @@ func NewArch(arch string) Arch { return arch } } + +func DefaultDriver() VMType { + switch runtime.GOOS { + case "darwin": + return VZ + case "windows": + return WSL2 + default: + return QEMU + } +} diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 9ad8bd6e4f7..104a85b1320 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -22,12 +22,10 @@ import ( "sync" "text/template" - "github.com/coreos/go-semver/semver" "github.com/docker/go-units" "github.com/goccy/go-yaml" "github.com/pbnjay/memory" "github.com/sirupsen/logrus" - "golang.org/x/sys/cpu" "github.com/lima-vm/lima/v2/pkg/instance/hostname" "github.com/lima-vm/lima/v2/pkg/ioutilx" @@ -40,7 +38,6 @@ import ( "github.com/lima-vm/lima/v2/pkg/osutil" "github.com/lima-vm/lima/v2/pkg/ptr" "github.com/lima-vm/lima/v2/pkg/version" - "github.com/lima-vm/lima/v2/pkg/version/versionutil" ) const ( @@ -142,18 +139,7 @@ func defaultGuestInstallPrefix() string { func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath string, warn bool) { instDir := filepath.Dir(filePath) - // existingLimaVersion can be empty if the instance was created with Lima prior to v0.20, - var existingLimaVersion string - if !isExistingInstanceDir(instDir) { - existingLimaVersion = version.Version - } else { - limaVersionFile := filepath.Join(instDir, filenames.LimaVersion) - if b, err := os.ReadFile(limaVersionFile); err == nil { - existingLimaVersion = strings.TrimSpace(string(b)) - } else if !errors.Is(err, os.ErrNotExist) { - logrus.WithError(err).Warnf("Failed to read %q", limaVersionFile) - } - } + existingLimaVersion := ExistingLimaVersion(instDir) if y.User.Name == nil { y.User.Name = d.User.Name @@ -223,7 +209,7 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin if o.VMType != nil { y.VMType = o.VMType } - y.VMType = ptr.Of(ResolveVMType(y, d, o, filePath)) + if y.OS == nil { y.OS = d.OS } @@ -253,28 +239,6 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin } } - if y.VMOpts.QEMU.CPUType == nil { - y.VMOpts.QEMU.CPUType = limatype.CPUType{} - } - - //nolint:staticcheck // Migration of top-level CPUTYPE if specified - if len(y.CPUType) > 0 { - if warn { - logrus.Warn("The top-level `cpuType` field is deprecated and will be removed in a future release. Please migrate to `vmOpts.qemu.cpuType`.") - } - for arch, v := range y.CPUType { - if v == "" { - continue - } - if existing, ok := y.VMOpts.QEMU.CPUType[arch]; ok && existing != "" && existing != v { - logrus.Warnf("Conflicting cpuType for arch %q: top-level=%q, vmOpts.qemu=%q; using vmOpts.qemu value", arch, v, existing) - continue - } - y.VMOpts.QEMU.CPUType[arch] = v - } - y.CPUType = nil - } - if y.CPUs == nil { y.CPUs = d.CPUs } @@ -333,9 +297,6 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin if o.Video.VNC.Display != nil { y.Video.VNC.Display = o.Video.VNC.Display } - if (y.Video.VNC.Display == nil || *y.Video.VNC.Display == "") && *y.VMType == limatype.QEMU { - y.Video.VNC.Display = ptr.Of("127.0.0.1:0,to=9") - } if y.Firmware.LegacyBIOS == nil { y.Firmware.LegacyBIOS = d.Firmware.LegacyBIOS @@ -634,15 +595,6 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin } y.MountTypesUnsupported = slices.Concat(o.MountTypesUnsupported, y.MountTypesUnsupported, d.MountTypesUnsupported) - mountTypesUnsupported := make(map[string]struct{}) - for _, f := range y.MountTypesUnsupported { - mountTypesUnsupported[f] = struct{}{} - } - - if runtime.GOOS == "windows" { - // QEMU for Windows does not support 9p - mountTypesUnsupported[limatype.NINEP] = struct{}{} - } // MountType has to be resolved before resolving Mounts if y.MountType == nil { @@ -651,28 +603,6 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin if o.MountType != nil { y.MountType = o.MountType } - if y.MountType == nil || *y.MountType == "" || *y.MountType == "default" { - switch *y.VMType { - case limatype.VZ: - y.MountType = ptr.Of(limatype.VIRTIOFS) - case limatype.QEMU: - y.MountType = ptr.Of(limatype.NINEP) - if _, ok := mountTypesUnsupported[limatype.NINEP]; ok { - // Use REVSSHFS if the instance does not support 9p - y.MountType = ptr.Of(limatype.REVSSHFS) - } else if isExistingInstanceDir(instDir) && !versionutil.GreaterEqual(existingLimaVersion, "1.0.0") { - // Use REVSSHFS if the instance was created with Lima prior to v1.0 - y.MountType = ptr.Of(limatype.REVSSHFS) - } - default: - y.MountType = ptr.Of(limatype.REVSSHFS) - } - } - - if _, ok := mountTypesUnsupported[*y.MountType]; ok { - // We cannot return an error here, but Validate() will return it. - logrus.Warnf("Unsupported mount type: %q", *y.MountType) - } if y.MountInotify == nil { y.MountInotify = d.MountInotify @@ -759,9 +689,6 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin if mount.NineP.Msize == nil { mounts[i].NineP.Msize = ptr.Of(Default9pMsize) } - if mount.Virtiofs.QueueSize == nil && *y.VMType == limatype.QEMU && *y.MountType == limatype.VIRTIOFS { - mounts[i].Virtiofs.QueueSize = ptr.Of(DefaultVirtiofsQueueSize) - } if mount.Writable == nil { mount.Writable = ptr.Of(false) } @@ -824,27 +751,19 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin y.CACertificates.Certs = unique(slices.Concat(d.CACertificates.Certs, y.CACertificates.Certs, o.CACertificates.Certs)) if runtime.GOOS == "darwin" && IsNativeArch(limatype.AARCH64) { - if y.Rosetta.Enabled == nil { - y.Rosetta.Enabled = d.Rosetta.Enabled + if y.VMOpts.VZ.Rosetta.Enabled == nil { + y.VMOpts.VZ.Rosetta.Enabled = d.VMOpts.VZ.Rosetta.Enabled } - if o.Rosetta.Enabled != nil { - y.Rosetta.Enabled = o.Rosetta.Enabled + if o.VMOpts.VZ.Rosetta.Enabled != nil { + y.VMOpts.VZ.Rosetta.Enabled = o.VMOpts.VZ.Rosetta.Enabled } - if y.Rosetta.Enabled == nil { - y.Rosetta.Enabled = ptr.Of(false) - } - } else { - y.Rosetta.Enabled = ptr.Of(false) } - if y.Rosetta.BinFmt == nil { - y.Rosetta.BinFmt = d.Rosetta.BinFmt + if y.VMOpts.VZ.Rosetta.BinFmt == nil { + y.VMOpts.VZ.Rosetta.BinFmt = d.VMOpts.VZ.Rosetta.BinFmt } - if o.Rosetta.BinFmt != nil { - y.Rosetta.BinFmt = o.Rosetta.BinFmt - } - if y.Rosetta.BinFmt == nil { - y.Rosetta.BinFmt = ptr.Of(false) + if o.VMOpts.VZ.Rosetta.BinFmt != nil { + y.VMOpts.VZ.Rosetta.BinFmt = o.VMOpts.VZ.Rosetta.BinFmt } if y.NestedVirtualization == nil { @@ -870,6 +789,22 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin fixUpForPlainMode(y) } +// ExistingLimaVersion returns empty if the instance was created with Lima prior to v0.20. +func ExistingLimaVersion(instDir string) string { + if !IsExistingInstanceDir(instDir) { + return version.Version + } + + limaVersionFile := filepath.Join(instDir, filenames.LimaVersion) + if b, err := os.ReadFile(limaVersionFile); err == nil { + return strings.TrimSpace(string(b)) + } else if !errors.Is(err, os.ErrNotExist) { + logrus.WithError(err).Warnf("Failed to read %q", limaVersionFile) + } + + return version.Version +} + func fixUpForPlainMode(y *limatype.LimaYAML) { if !*y.Plain { return @@ -878,8 +813,8 @@ func fixUpForPlainMode(y *limatype.LimaYAML) { y.PortForwards = nil y.Containerd.System = ptr.Of(false) y.Containerd.User = ptr.Of(false) - y.Rosetta.BinFmt = ptr.Of(false) - y.Rosetta.Enabled = ptr.Of(false) + y.VMOpts.VZ.Rosetta.BinFmt = ptr.Of(false) + y.VMOpts.VZ.Rosetta.Enabled = ptr.Of(false) y.TimeZone = ptr.Of("") } @@ -1002,72 +937,7 @@ func FillCopyToHostDefaults(rule *limatype.CopyToHost, instDir string, user lima } } -func NewOS(osname string) limatype.OS { - switch osname { - case "linux": - return limatype.LINUX - default: - logrus.Warnf("Unknown os: %s", osname) - return osname - } -} - -func goarm() int { - if runtime.GOOS != "linux" { - return 0 - } - if runtime.GOARCH != "arm" { - return 0 - } - if cpu.ARM.HasVFPv3 { - return 7 - } - if cpu.ARM.HasVFP { - return 6 - } - return 5 // default -} - -func NewArch(arch string) limatype.Arch { - switch arch { - case "amd64": - return limatype.X8664 - case "arm64": - return limatype.AARCH64 - case "arm": - arm := goarm() - if arm == 7 { - return limatype.ARMV7L - } - logrus.Warnf("Unknown arm: %d", arm) - return arch - case "ppc64le": - return limatype.PPC64LE - case "riscv64": - return limatype.RISCV64 - case "s390x": - return limatype.S390X - default: - logrus.Warnf("Unknown arch: %s", arch) - return arch - } -} - -func NewVMType(driver string) limatype.VMType { - switch driver { - case "vz": - return limatype.VZ - case "qemu": - return limatype.QEMU - case "wsl2": - return limatype.WSL2 - default: - logrus.Warnf("Unknown driver: %s", driver) - return driver - } -} - -func isExistingInstanceDir(dir string) bool { +func IsExistingInstanceDir(dir string) bool { // existence of "lima.yaml" does not signify existence of the instance, // because the file is created during the initialization of the instance. for _, f := range []string{ @@ -1082,97 +952,16 @@ func isExistingInstanceDir(dir string) bool { return false } -func ResolveVMType(y, d, o *limatype.LimaYAML, filePath string) limatype.VMType { - // Check if the VMType is explicitly specified - for i, f := range []*limatype.LimaYAML{o, y, d} { - if f.VMType != nil && *f.VMType != "" && *f.VMType != "default" { - logrus.Debugf("ResolveVMType: resolved VMType %q (explicitly specified in []*LimaYAML{o,y,d}[%d])", *f.VMType, i) - return NewVMType(*f.VMType) - } - } - - // If this is an existing instance, guess the VMType from the contents of the instance directory. - if dir, basename := filepath.Split(filePath); dir != "" && basename == filenames.LimaYAML && isExistingInstanceDir(dir) { - if runtime.GOOS == "darwin" { - vzIdentifier := filepath.Join(dir, filenames.VzIdentifier) // since Lima v0.14 - if _, err := os.Lstat(vzIdentifier); !errors.Is(err, os.ErrNotExist) { - logrus.Debugf("ResolveVMType: resolved VMType %q (existing instance, with %q)", limatype.VZ, vzIdentifier) - return limatype.VZ - } - logrus.Debugf("ResolveVMType: resolved VMType %q (existing instance, without %q)", limatype.QEMU, vzIdentifier) - return limatype.QEMU - } - logrus.Debugf("ResolveVMType: resolved VMType %q (existing instance)", limatype.QEMU) - return limatype.QEMU - } - - // Resolve the best type, depending on GOOS - switch runtime.GOOS { - case "darwin": - macOSProductVersion, err := osutil.ProductVersion() - if err != nil { - logrus.WithError(err).Warn("Failed to get macOS product version") - logrus.Debugf("ResolveVMType: resolved VMType %q (default for unknown version of macOS)", limatype.QEMU) - return limatype.QEMU - } - // Virtualization.framework in macOS prior to 13.5 could not boot Linux kernel v6.2 on Intel - // https://github.com/lima-vm/lima/issues/1577 - if macOSProductVersion.LessThan(*semver.New("13.5.0")) { - logrus.Debugf("ResolveVMType: resolved VMType %q (default for macOS prior to 13.5)", limatype.QEMU) - return limatype.QEMU - } - // Use QEMU if the config depends on QEMU - for i, f := range []*limatype.LimaYAML{o, y, d} { - if f.Arch != nil && !IsNativeArch(*f.Arch) { - logrus.Debugf("ResolveVMType: resolved VMType %q (non-native arch=%q is specified in []*LimaYAML{o,y,d}[%d])", limatype.QEMU, *f.Arch, i) - return limatype.QEMU - } - if ResolveArch(f.Arch) == limatype.X8664 && f.Firmware.LegacyBIOS != nil && *f.Firmware.LegacyBIOS { - logrus.Debugf("ResolveVMType: resolved VMType %q (firmware.legacyBIOS is specified in []*LimaYAML{o,y,d}[%d], on x86_64)", limatype.QEMU, i) - return limatype.QEMU - } - if f.MountType != nil && *f.MountType == limatype.NINEP { - logrus.Debugf("ResolveVMType: resolved VMType %q (mountType=%q is specified in []*LimaYAML{o,y,d}[%d])", limatype.QEMU, limatype.NINEP, i) - return limatype.QEMU - } - if f.Audio.Device != nil { - switch *f.Audio.Device { - case "", "none", "default", "vz": - // NOP - default: - logrus.Debugf("ResolveVMType: resolved VMType %q (audio.device=%q is specified in []*LimaYAML{o,y,d}[%d])", limatype.QEMU, *f.Audio.Device, i) - return limatype.QEMU - } - } - if f.Video.Display != nil { - switch *f.Video.Display { - case "", "none", "default", "vz": - // NOP - default: - logrus.Debugf("ResolveVMType: resolved VMType %q (video.display=%q is specified in []*LimaYAML{o,y,d}[%d])", limatype.QEMU, *f.Video.Display, i) - return limatype.QEMU - } - } - } - // Use VZ if the config is compatible with VZ - logrus.Debugf("ResolveVMType: resolved VMType %q (default for macOS 13.5 and later)", limatype.VZ) - return limatype.VZ - default: - logrus.Debugf("ResolveVMType: resolved VMType %q (default for GOOS=%q)", limatype.QEMU, runtime.GOOS) - return limatype.QEMU - } -} - func ResolveOS(s *string) limatype.OS { if s == nil || *s == "" || *s == "default" { - return NewOS("linux") + return limatype.NewOS("linux") } return *s } func ResolveArch(s *string) limatype.Arch { if s == nil || *s == "" || *s == "default" { - return NewArch(runtime.GOARCH) + return limatype.NewArch(runtime.GOARCH) } return *s } @@ -1230,7 +1019,7 @@ func HasHostCPU() bool { func IsNativeArch(arch limatype.Arch) bool { nativeX8664 := arch == limatype.X8664 && runtime.GOARCH == "amd64" nativeAARCH64 := arch == limatype.AARCH64 && runtime.GOARCH == "arm64" - nativeARMV7L := arch == limatype.ARMV7L && runtime.GOARCH == "arm" && goarm() == 7 + nativeARMV7L := arch == limatype.ARMV7L && runtime.GOARCH == "arm" && limatype.Goarm() == 7 nativePPC64LE := arch == limatype.PPC64LE && runtime.GOARCH == "ppc64le" nativeRISCV64 := arch == limatype.RISCV64 && runtime.GOARCH == "riscv64" nativeS390X := arch == limatype.S390X && runtime.GOARCH == "s390x" diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go index b43ce23e652..f0b2c6444f9 100644 --- a/pkg/limayaml/defaults_test.go +++ b/pkg/limayaml/defaults_test.go @@ -31,8 +31,6 @@ func TestFillDefault(t *testing.T) { logrus.SetLevel(logrus.DebugLevel) var d, y, o limatype.LimaYAML - defaultVMType := ResolveVMType(&y, &d, &o, "") - opts := []cmp.Option{ // Consider nil slices and empty slices to be identical cmpopts.EquateEmpty(), @@ -77,7 +75,6 @@ func TestFillDefault(t *testing.T) { // Builtin default values builtin := limatype.LimaYAML{ - VMType: &defaultVMType, OS: ptr.Of(limatype.LINUX), Arch: ptr.Of(arch), CPUs: ptr.Of(defaultCPUs()), @@ -106,9 +103,6 @@ func TestFillDefault(t *testing.T) { }, Video: limatype.Video{ Display: ptr.Of("none"), - VNC: limatype.VNCOptions{ - Display: ptr.Of("127.0.0.1:0,to=9"), - }, }, HostResolver: limatype.HostResolver{ Enabled: ptr.Of(true), @@ -195,31 +189,11 @@ func TestFillDefault(t *testing.T) { }, }, TimeZone: ptr.Of("Antarctica/Troll"), - Firmware: limatype.Firmware{ - LegacyBIOS: ptr.Of(false), - Images: []limatype.FileWithVMType{ - { - File: limatype.File{ - Location: "https://gitlab.com/kraxel/qemu/-/raw/704f7cad5105246822686f65765ab92045f71a3b/pc-bios/edk2-aarch64-code.fd.bz2", - Arch: limatype.AARCH64, - Digest: "sha256:a5fc228623891297f2d82e22ea56ec57cde93fea5ec01abf543e4ed5cacaf277", - }, - VMType: limatype.QEMU, - }, - { - File: limatype.File{ - Location: "https://github.com/AkihiroSuda/qemu/raw/704f7cad5105246822686f65765ab92045f71a3b/pc-bios/edk2-aarch64-code.fd.bz2", - Arch: limatype.AARCH64, - Digest: "sha256:a5fc228623891297f2d82e22ea56ec57cde93fea5ec01abf543e4ed5cacaf277", - }, - VMType: limatype.QEMU, - }, - }, - }, } expect := builtin - expect.VMType = ptr.Of(limatype.QEMU) // due to NINEP + // VMType should remain nil when not explicitly set (will be resolved by ValidateVMType later) + expect.VMType = nil expect.HostResolver.Hosts = map[string]string{ "MY.Host": "host.lima.internal", } @@ -313,12 +287,11 @@ func TestFillDefault(t *testing.T) { } expect.TimeZone = y.TimeZone - expect.Firmware = y.Firmware - expect.Firmware.Images = slices.Clone(y.Firmware.Images) - - expect.Rosetta = limatype.Rosetta{ - Enabled: ptr.Of(false), - BinFmt: ptr.Of(false), + // Set firmware expectations to match what FillDefault actually does + // FillDefault uses the builtin default values, which include LegacyBIOS: ptr.Of(false) + expect.Firmware = limatype.Firmware{ + LegacyBIOS: ptr.Of(false), // This matches what FillDefault actually sets + Images: nil, } expect.NestedVirtualization = ptr.Of(false) @@ -336,7 +309,7 @@ func TestFillDefault(t *testing.T) { // Calling filepath.Abs() to add a drive letter on Windows varLog, _ := filepath.Abs("/var/log") d = limatype.LimaYAML{ - VMType: ptr.Of("vz"), + // Remove driver-specific VMType from defaults test OS: ptr.Of("unknown"), Arch: ptr.Of("unknown"), CPUs: ptr.Of(7), @@ -364,23 +337,14 @@ func TestFillDefault(t *testing.T) { TimeZone: ptr.Of("Zulu"), Firmware: limatype.Firmware{ LegacyBIOS: ptr.Of(true), - Images: []limatype.FileWithVMType{ - { - File: limatype.File{ - Location: "/dummy", - Arch: limatype.X8664, - }, - }, - }, + // Remove driver-specific firmware images from defaults }, Audio: limatype.Audio{ Device: ptr.Of("coreaudio"), }, Video: limatype.Video{ Display: ptr.Of("cocoa"), - VNC: limatype.VNCOptions{ - Display: ptr.Of("none"), - }, + // Remove driver-specific VNC configuration }, HostResolver: limatype.HostResolver{ Enabled: ptr.Of(false), @@ -444,10 +408,6 @@ func TestFillDefault(t *testing.T) { "-----BEGIN CERTIFICATE-----\nYOUR-ORGS-TRUSTED-CA-CERT\n-----END CERTIFICATE-----\n", }, }, - Rosetta: limatype.Rosetta{ - Enabled: ptr.Of(true), - BinFmt: ptr.Of(true), - }, NestedVirtualization: ptr.Of(true), User: limatype.User{ Name: ptr.Of("xxx"), @@ -459,6 +419,8 @@ func TestFillDefault(t *testing.T) { } expect = d + // VMType should remain nil when not explicitly set + expect.VMType = nil // Also verify that archive arch is filled in expect.Containerd.Archives = slices.Clone(d.Containerd.Archives) expect.Containerd.Archives[0].Arch = *d.Arch @@ -481,31 +443,21 @@ func TestFillDefault(t *testing.T) { expect.HostResolver.Hosts = map[string]string{ "default": d.HostResolver.Hosts["default"], } - expect.MountType = ptr.Of(limatype.VIRTIOFS) + // Remove driver-specific mount type from defaults test + expect.MountType = nil expect.MountInotify = ptr.Of(false) expect.CACertificates.RemoveDefaults = ptr.Of(true) expect.CACertificates.Certs = []string{ "-----BEGIN CERTIFICATE-----\nYOUR-ORGS-TRUSTED-CA-CERT\n-----END CERTIFICATE-----\n", } - if runtime.GOOS == "darwin" && IsNativeArch(limatype.AARCH64) { - expect.Rosetta = limatype.Rosetta{ - Enabled: ptr.Of(true), - BinFmt: ptr.Of(true), - } - } else { - expect.Rosetta = limatype.Rosetta{ - Enabled: ptr.Of(false), - BinFmt: ptr.Of(true), - } - } expect.Plain = ptr.Of(false) y = limatype.LimaYAML{} FillDefault(t.Context(), &y, &d, &limatype.LimaYAML{}, filePath, false) assert.DeepEqual(t, &y, &expect, opts...) - dExpect := expect + dExpected := expect // ------------------------------------------------------------------------------------ // User-provided defaults should not override user-provided config values @@ -517,29 +469,29 @@ func TestFillDefault(t *testing.T) { expect = y - expect.Provision = slices.Concat(y.Provision, dExpect.Provision) - expect.Probes = slices.Concat(y.Probes, dExpect.Probes) - expect.PortForwards = slices.Concat(y.PortForwards, dExpect.PortForwards) - expect.CopyToHost = slices.Concat(y.CopyToHost, dExpect.CopyToHost) - expect.Containerd.Archives = slices.Concat(y.Containerd.Archives, dExpect.Containerd.Archives) + expect.Provision = slices.Concat(y.Provision, dExpected.Provision) + expect.Probes = slices.Concat(y.Probes, dExpected.Probes) + expect.PortForwards = slices.Concat(y.PortForwards, dExpected.PortForwards) + expect.CopyToHost = slices.Concat(y.CopyToHost, dExpected.CopyToHost) + expect.Containerd.Archives = slices.Concat(y.Containerd.Archives, dExpected.Containerd.Archives) expect.Containerd.Archives[2].Arch = *expect.Arch - expect.AdditionalDisks = slices.Concat(y.AdditionalDisks, dExpect.AdditionalDisks) - expect.Firmware.Images = slices.Concat(y.Firmware.Images, dExpect.Firmware.Images) + expect.AdditionalDisks = slices.Concat(y.AdditionalDisks, dExpected.AdditionalDisks) + expect.Firmware.Images = slices.Concat(y.Firmware.Images, dExpected.Firmware.Images) // Mounts and Networks start with lowest priority first, so higher priority entries can overwrite - expect.Mounts = slices.Concat(dExpect.Mounts, y.Mounts) - expect.Networks = slices.Concat(dExpect.Networks, y.Networks) + expect.Mounts = slices.Concat(dExpected.Mounts, y.Mounts) + expect.Networks = slices.Concat(dExpected.Networks, y.Networks) - expect.HostResolver.Hosts["default"] = dExpect.HostResolver.Hosts["default"] + expect.HostResolver.Hosts["default"] = dExpected.HostResolver.Hosts["default"] - // dExpect.DNS will be ignored, and not appended to y.DNS + // dExpected.DNS will be ignored, and not appended to y.DNS - // "TWO" does not exist in filledDefaults.Env, so is set from dExpect.Env - expect.Env["TWO"] = dExpect.Env["TWO"] + // "TWO" does not exist in filledDefaults.Env, so is set from dExpected.Env + expect.Env["TWO"] = dExpected.Env["TWO"] - expect.Param["TWO"] = dExpect.Param["TWO"] + expect.Param["TWO"] = dExpected.Param["TWO"] - t.Logf("d.vmType=%q, y.vmType=%q, expect.vmType=%q", *d.VMType, *y.VMType, *expect.VMType) + t.Logf("d.vmType=%v, y.vmType=%v, expect.vmType=%v", d.VMType, y.VMType, expect.VMType) FillDefault(t.Context(), &y, &d, &limatype.LimaYAML{}, filePath, false) assert.DeepEqual(t, &y, &expect, opts...) @@ -548,7 +500,7 @@ func TestFillDefault(t *testing.T) { // User-provided overrides should override user-provided config settings o = limatype.LimaYAML{ - VMType: ptr.Of("qemu"), + // Remove driver-specific VMType from override test OS: ptr.Of(limatype.LINUX), Arch: ptr.Of(arch), CPUs: ptr.Of(12), @@ -586,9 +538,7 @@ func TestFillDefault(t *testing.T) { }, Video: limatype.Video{ Display: ptr.Of("cocoa"), - VNC: limatype.VNCOptions{ - Display: ptr.Of("none"), - }, + // Remove driver-specific VNC configuration }, HostResolver: limatype.HostResolver{ Enabled: ptr.Of(false), @@ -668,10 +618,6 @@ func TestFillDefault(t *testing.T) { CACertificates: limatype.CACertificates{ RemoveDefaults: ptr.Of(true), }, - Rosetta: limatype.Rosetta{ - Enabled: ptr.Of(false), - BinFmt: ptr.Of(false), - }, NestedVirtualization: ptr.Of(false), User: limatype.User{ Name: ptr.Of("foo"), @@ -686,20 +632,20 @@ func TestFillDefault(t *testing.T) { expect = o - expect.Provision = slices.Concat(o.Provision, y.Provision, dExpect.Provision) - expect.Probes = slices.Concat(o.Probes, y.Probes, dExpect.Probes) - expect.PortForwards = slices.Concat(o.PortForwards, y.PortForwards, dExpect.PortForwards) - expect.CopyToHost = slices.Concat(o.CopyToHost, y.CopyToHost, dExpect.CopyToHost) - expect.Containerd.Archives = slices.Concat(o.Containerd.Archives, y.Containerd.Archives, dExpect.Containerd.Archives) + expect.Provision = slices.Concat(o.Provision, y.Provision, dExpected.Provision) + expect.Probes = slices.Concat(o.Probes, y.Probes, dExpected.Probes) + expect.PortForwards = slices.Concat(o.PortForwards, y.PortForwards, dExpected.PortForwards) + expect.CopyToHost = slices.Concat(o.CopyToHost, y.CopyToHost, dExpected.CopyToHost) + expect.Containerd.Archives = slices.Concat(o.Containerd.Archives, y.Containerd.Archives, dExpected.Containerd.Archives) expect.Containerd.Archives[3].Arch = *expect.Arch - expect.AdditionalDisks = slices.Concat(o.AdditionalDisks, y.AdditionalDisks, dExpect.AdditionalDisks) - expect.Firmware.Images = slices.Concat(o.Firmware.Images, y.Firmware.Images, dExpect.Firmware.Images) + expect.AdditionalDisks = slices.Concat(o.AdditionalDisks, y.AdditionalDisks, dExpected.AdditionalDisks) + expect.Firmware.Images = slices.Concat(o.Firmware.Images, y.Firmware.Images, dExpected.Firmware.Images) - expect.HostResolver.Hosts["default"] = dExpect.HostResolver.Hosts["default"] - expect.HostResolver.Hosts["MY.Host"] = dExpect.HostResolver.Hosts["host.lima.internal"] + expect.HostResolver.Hosts["default"] = dExpected.HostResolver.Hosts["default"] + expect.HostResolver.Hosts["MY.Host"] = dExpected.HostResolver.Hosts["host.lima.internal"] - // o.Mounts just makes dExpect.Mounts[0] writable because the Location matches - expect.Mounts = slices.Concat(dExpect.Mounts, y.Mounts) + // o.Mounts just makes dExpected.Mounts[0] writable because the Location matches + expect.Mounts = slices.Concat(dExpected.Mounts, y.Mounts) expect.Mounts[0].Writable = ptr.Of(true) expect.Mounts[0].SSHFS.Cache = ptr.Of(false) expect.Mounts[0].SSHFS.FollowSymlinks = ptr.Of(true) @@ -712,8 +658,8 @@ func TestFillDefault(t *testing.T) { expect.MountType = ptr.Of(limatype.NINEP) expect.MountInotify = ptr.Of(true) - // o.Networks[1] is overriding the dExpect.Networks[0].Lima entry for the "def0" interface - expect.Networks = slices.Concat(dExpect.Networks, y.Networks, []limatype.Network{o.Networks[0]}) + // o.Networks[1] is overriding the dExpected.Networks[0].Lima entry for the "def0" interface + expect.Networks = slices.Concat(dExpected.Networks, y.Networks, []limatype.Network{o.Networks[0]}) expect.Networks[0].Lima = o.Networks[1].Lima // Only highest prio DNS are retained @@ -730,10 +676,6 @@ func TestFillDefault(t *testing.T) { "-----BEGIN CERTIFICATE-----\nYOUR-ORGS-TRUSTED-CA-CERT\n-----END CERTIFICATE-----\n", } - expect.Rosetta = limatype.Rosetta{ - Enabled: ptr.Of(false), - BinFmt: ptr.Of(false), - } expect.Plain = ptr.Of(false) expect.NestedVirtualization = ptr.Of(false) diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 161dffdecd5..cd869ec6d9c 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -18,10 +18,10 @@ import ( "strings" "unicode" - "github.com/coreos/go-semver/semver" "github.com/docker/go-units" "github.com/sirupsen/logrus" + "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/identifiers" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/localpathutil" @@ -48,11 +48,7 @@ func Validate(y *limatype.LimaYAML, warn bool) error { errs = errors.Join(errs, fmt.Errorf("template requires Lima version %q; this is only %q", *y.MinimumLimaVersion, version.Version)) } } - if y.VMOpts.QEMU.MinimumVersion != nil { - if _, err := semver.NewVersion(*y.VMOpts.QEMU.MinimumVersion); err != nil { - errs = errors.Join(errs, fmt.Errorf("field `vmOpts.qemu.minimumVersion` must be a semvar value, got %q: %w", *y.VMOpts.QEMU.MinimumVersion, err)) - } - } + switch *y.OS { case limatype.LINUX: default: @@ -446,28 +442,12 @@ func validateNetwork(y *limatype.LimaYAML) error { if nw.Socket != "" { errs = errors.Join(errs, fmt.Errorf("field `%s.lima` and field `%s.socket` are mutually exclusive", field, field)) } - if nw.VZNAT != nil && *nw.VZNAT { - errs = errors.Join(errs, fmt.Errorf("field `%s.lima` and field `%s.vzNAT` are mutually exclusive", field, field)) - } case nw.Socket != "": - if nw.VZNAT != nil && *nw.VZNAT { - errs = errors.Join(errs, fmt.Errorf("field `%s.socket` and field `%s.vzNAT` are mutually exclusive", field, field)) - } if fi, err := os.Stat(nw.Socket); err != nil && !errors.Is(err, os.ErrNotExist) { errs = errors.Join(errs, err) } else if err == nil && fi.Mode()&os.ModeSocket == 0 { errs = errors.Join(errs, fmt.Errorf("field `%s.socket` %q points to a non-socket file", field, nw.Socket)) } - case nw.VZNAT != nil && *nw.VZNAT: - if y.VMType == nil || *y.VMType != limatype.VZ { - errs = errors.Join(errs, fmt.Errorf("field `%s.vzNAT` requires `vmType` to be %q", field, limatype.VZ)) - } - if nw.Lima != "" { - errs = errors.Join(errs, fmt.Errorf("field `%s.vzNAT` and field `%s.lima` are mutually exclusive", field, field)) - } - if nw.Socket != "" { - errs = errors.Join(errs, fmt.Errorf("field `%s.vzNAT` and field `%s.socket` are mutually exclusive", field, field)) - } default: errs = errors.Join(errs, fmt.Errorf("field `%s.lima` or field `%s.socket must be set", field, field)) } @@ -600,35 +580,39 @@ func warnExperimental(y *limatype.LimaYAML) { // This validates configuration rules that disallow certain changes, such as shrinking the disk. func ValidateAgainstLatestConfig(ctx context.Context, yNew, yLatest []byte) error { var n limatype.LimaYAML + var errs error // Load the latest YAML and fill in defaults l, err := LoadWithWarnings(ctx, yLatest, "") if err != nil { - return err + errs = errors.Join(errs, err) + } + if err := driverutil.ResolveVMType(l, ""); err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to accept config for %q: %w", "", err)) } if err := Unmarshal(yNew, &n, "Unmarshal new YAML bytes"); err != nil { - return err + errs = errors.Join(errs, err) } // Handle editing the template without a disk value if n.Disk == nil || l.Disk == nil { - return nil + return errs } // Disk value must be provided, as it is required when creating an instance. nDisk, err := units.RAMInBytes(*n.Disk) if err != nil { - return err + errs = errors.Join(errs, err) } lDisk, err := units.RAMInBytes(*l.Disk) if err != nil { - return err + errs = errors.Join(errs, err) } // Reject shrinking disk if nDisk < lDisk { - return fmt.Errorf("field `disk`: shrinking the disk (%v --> %v) is not supported", *l.Disk, *n.Disk) + errs = errors.Join(errs, fmt.Errorf("field `disk`: shrinking the disk (%v --> %v) is not supported", *l.Disk, *n.Disk)) } - return nil + return errs } diff --git a/pkg/limayaml/validate_test.go b/pkg/limayaml/validate_test.go index 0138c55ddaf..943786b6c72 100644 --- a/pkg/limayaml/validate_test.go +++ b/pkg/limayaml/validate_test.go @@ -4,11 +4,12 @@ package limayaml import ( - "errors" + "fmt" "testing" "gotest.tools/v3/assert" + "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/version" ) @@ -341,49 +342,51 @@ func TestValidateAgainstLatestConfig(t *testing.T) { name string yNew string yLatest string - wantErr error + wantErr string }{ { name: "Valid disk size unchanged", yNew: `disk: 100GiB`, yLatest: `disk: 100GiB`, + wantErr: fmt.Sprintf("failed to accept config for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()), }, { name: "Valid disk size increased", yNew: `disk: 200GiB`, yLatest: `disk: 100GiB`, + wantErr: fmt.Sprintf("failed to accept config for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()), }, { name: "No disk field in both YAMLs", yNew: ``, yLatest: ``, + wantErr: fmt.Sprintf("failed to accept config for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()), }, { name: "No disk field in new YAMLs", yNew: ``, yLatest: `disk: 100GiB`, + wantErr: fmt.Sprintf("failed to accept config for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()), }, { name: "No disk field in latest YAMLs", yNew: `disk: 100GiB`, yLatest: ``, + wantErr: fmt.Sprintf("failed to accept config for \"\": vmType %q is not a registered driver", limatype.DefaultDriver()), }, { name: "Disk size shrunk", yNew: `disk: 50GiB`, yLatest: `disk: 100GiB`, - wantErr: errors.New("field `disk`: shrinking the disk (100GiB --> 50GiB) is not supported"), + wantErr: fmt.Sprintf("failed to accept config for \"\": vmType %q is not a registered driver\n", limatype.DefaultDriver()) + + "field `disk`: shrinking the disk (100GiB --> 50GiB) is not supported", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateAgainstLatestConfig(t.Context(), []byte(tt.yNew), []byte(tt.yLatest)) - if tt.wantErr == nil { - assert.NilError(t, err) - } else { - assert.Error(t, err, tt.wantErr.Error()) - } + assert.Error(t, err, tt.wantErr) }) } } diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index e08b16a4e97..c672e971940 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -27,24 +27,30 @@ func newMockDriver(name string) *mockDriver { var _ driver.Driver = (*mockDriver)(nil) -func (m *mockDriver) Validate(_ context.Context) error { return nil } -func (m *mockDriver) Initialize(_ context.Context) error { return nil } -func (m *mockDriver) CreateDisk(_ context.Context) error { return nil } -func (m *mockDriver) Start(_ context.Context) (chan error, error) { return nil, nil } -func (m *mockDriver) Stop(_ context.Context) error { return nil } -func (m *mockDriver) RunGUI() error { return nil } -func (m *mockDriver) ChangeDisplayPassword(_ context.Context, _ string) error { return nil } -func (m *mockDriver) DisplayConnection(_ context.Context) (string, error) { return "", nil } -func (m *mockDriver) CreateSnapshot(_ context.Context, _ string) error { return nil } -func (m *mockDriver) ApplySnapshot(_ context.Context, _ string) error { return nil } -func (m *mockDriver) DeleteSnapshot(_ context.Context, _ string) error { return nil } -func (m *mockDriver) ListSnapshots(_ context.Context) (string, error) { return "", nil } -func (m *mockDriver) Register(_ context.Context) error { return nil } -func (m *mockDriver) Unregister(_ context.Context) error { return nil } -func (m *mockDriver) ForwardGuestAgent() bool { return false } -func (m *mockDriver) GuestAgentConn(_ context.Context) (net.Conn, string, error) { return nil, "", nil } -func (m *mockDriver) Info() driver.Info { return driver.Info{DriverName: m.Name} } -func (m *mockDriver) Configure(_ *limatype.Instance) *driver.ConfiguredDriver { return nil } +func (m *mockDriver) Validate(_ context.Context) error { return nil } +func (m *mockDriver) Create(_ context.Context) error { return nil } +func (m *mockDriver) Delete(_ context.Context) error { return nil } +func (m *mockDriver) CreateDisk(_ context.Context) error { return nil } +func (m *mockDriver) Start(_ context.Context) (chan error, error) { return nil, nil } +func (m *mockDriver) Stop(_ context.Context) error { return nil } +func (m *mockDriver) RunGUI() error { return nil } +func (m *mockDriver) ChangeDisplayPassword(_ context.Context, _ string) error { return nil } +func (m *mockDriver) DisplayConnection(_ context.Context) (string, error) { return "", nil } +func (m *mockDriver) CreateSnapshot(_ context.Context, _ string) error { return nil } +func (m *mockDriver) ApplySnapshot(_ context.Context, _ string) error { return nil } +func (m *mockDriver) DeleteSnapshot(_ context.Context, _ string) error { return nil } +func (m *mockDriver) ListSnapshots(_ context.Context) (string, error) { return "", nil } +func (m *mockDriver) Register(_ context.Context) error { return nil } +func (m *mockDriver) Unregister(_ context.Context) error { return nil } +func (m *mockDriver) ForwardGuestAgent() bool { return false } +func (m *mockDriver) GuestAgentConn(_ context.Context) (net.Conn, string, error) { return nil, "", nil } +func (m *mockDriver) Info() driver.Info { return driver.Info{DriverName: m.Name} } +func (m *mockDriver) Configure(_ *limatype.Instance) *driver.ConfiguredDriver { return nil } +func (m *mockDriver) AcceptConfig(_ *limatype.LimaYAML, _ string) error { return nil } +func (m *mockDriver) FillConfig(_ *limatype.LimaYAML, _ string) error { return nil } +func (m *mockDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string { return "" } +func (m *mockDriver) SSHAddress(_ context.Context) (string, error) { return "", nil } +func (m *mockDriver) BootScripts() (map[string][]byte, error) { return nil, nil } func TestRegister(t *testing.T) { BackupRegistry(t) diff --git a/pkg/store/instance.go b/pkg/store/instance.go index 00f195add10..038bb16bf34 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -21,6 +21,7 @@ import ( "github.com/docker/go-units" "github.com/sirupsen/logrus" + "github.com/lima-vm/lima/v2/pkg/driverutil" hostagentclient "github.com/lima-vm/lima/v2/pkg/hostagent/api/client" "github.com/lima-vm/lima/v2/pkg/instance/hostname" "github.com/lima-vm/lima/v2/pkg/limatype" @@ -58,7 +59,6 @@ func Inspect(ctx context.Context, instName string) (*limatype.Instance, error) { inst.Config = y inst.Arch = *y.Arch inst.VMType = *y.VMType - inst.CPUType = y.VMOpts.QEMU.CPUType[*y.Arch] inst.SSHAddress = "127.0.0.1" inst.SSHLocalPort = *y.SSH.LocalPort // maybe 0 inst.SSHConfigFile = filepath.Join(instDir, filenames.SSHConfig) @@ -147,6 +147,22 @@ func Inspect(ctx context.Context, instName string) (*limatype.Instance, error) { return inst, nil } +func inspectStatus(ctx context.Context, instDir string, inst *limatype.Instance, y *limatype.LimaYAML) { + status, err := driverutil.InspectStatus(ctx, inst) + if err != nil { + inst.Status = limatype.StatusBroken + inst.Errors = append(inst.Errors, fmt.Errorf("failed to inspect status: %w", err)) + return + } + + if status == "" { + inspectStatusWithPIDFiles(instDir, inst, y) + return + } + + inst.Status = status +} + func inspectStatusWithPIDFiles(instDir string, inst *limatype.Instance, y *limatype.LimaYAML) { var err error inst.DriverPID, err = ReadPIDFile(filepath.Join(instDir, filenames.PIDFile(*y.VMType))) diff --git a/pkg/store/instance_unix.go b/pkg/store/instance_unix.go deleted file mode 100644 index 1b2a0711fb4..00000000000 --- a/pkg/store/instance_unix.go +++ /dev/null @@ -1,20 +0,0 @@ -//go:build !windows - -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -package store - -import ( - "context" - - "github.com/lima-vm/lima/v2/pkg/limatype" -) - -func inspectStatus(_ context.Context, instDir string, inst *limatype.Instance, y *limatype.LimaYAML) { - inspectStatusWithPIDFiles(instDir, inst, y) -} - -func GetSSHAddress(_ context.Context, _ string) (string, error) { - return "127.0.0.1", nil -} diff --git a/pkg/store/instance_windows.go b/pkg/store/instance_windows.go deleted file mode 100644 index 53e551a3d99..00000000000 --- a/pkg/store/instance_windows.go +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -package store - -import ( - "context" - "fmt" - "os/exec" - "regexp" - "strings" - - "github.com/lima-vm/lima/v2/pkg/executil" - "github.com/lima-vm/lima/v2/pkg/limatype" -) - -func inspectStatus(ctx context.Context, instDir string, inst *limatype.Instance, y *limatype.LimaYAML) { - if inst.VMType == limatype.WSL2 { - status, err := GetWslStatus(inst.Name) - if err != nil { - inst.Status = limatype.StatusBroken - inst.Errors = append(inst.Errors, err) - } else { - inst.Status = status - } - - inst.SSHLocalPort = 22 - - if inst.Status == limatype.StatusRunning { - sshAddr, err := GetSSHAddress(ctx, inst.Name) - if err == nil { - inst.SSHAddress = sshAddr - } else { - inst.Errors = append(inst.Errors, err) - } - } - } else { - inspectStatusWithPIDFiles(instDir, inst, y) - } -} - -// GetWslStatus runs `wsl --list --verbose` and parses its output. -// There are several possible outputs, all listed with their whitespace preserved output below. -// -// (1) Expected output if at least one distro is installed: -// PS > wsl --list --verbose -// -// NAME STATE VERSION -// -// * Ubuntu Stopped 2 -// -// (2) Expected output when no distros are installed, but WSL is configured properly: -// PS > wsl --list --verbose -// Windows Subsystem for Linux has no installed distributions. -// -// Use 'wsl.exe --list --online' to list available distributions -// and 'wsl.exe --install ' to install. -// -// Distributions can also be installed by visiting the Microsoft Store: -// https://aka.ms/wslstore -// Error code: Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND -// -// (3) Expected output when no distros are installed, and WSL2 has no kernel installed: -// -// PS > wsl --list --verbose -// Windows Subsystem for Linux has no installed distributions. -// Distributions can be installed by visiting the Microsoft Store: -// https://aka.ms/wslstore -func GetWslStatus(instName string) (string, error) { - distroName := "lima-" + instName - out, err := executil.RunUTF16leCommand([]string{ - "wsl.exe", - "--list", - "--verbose", - }) - if err != nil { - return "", fmt.Errorf("failed to run `wsl --list --verbose`, err: %w (out=%q)", err, out) - } - - if out == "" { - return limatype.StatusBroken, fmt.Errorf("failed to read instance state for instance %q, try running `wsl --list --verbose` to debug, err: %w", instName, err) - } - - // Check for edge cases first - if strings.Contains(out, "Windows Subsystem for Linux has no installed distributions.") { - if strings.Contains(out, "Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND") { - return limatype.StatusBroken, fmt.Errorf( - "failed to read instance state for instance %q because no distro is installed,"+ - "try running `wsl --install -d Ubuntu` and then re-running Lima", instName) - } - return limatype.StatusBroken, fmt.Errorf( - "failed to read instance state for instance %q because there is no WSL kernel installed,"+ - "this usually happens when WSL was installed for another user, but never for your user."+ - "Try running `wsl --install -d Ubuntu` and `wsl --update`, and then re-running Lima", instName) - } - - var instState string - wslListColsRegex := regexp.MustCompile(`\s+`) - // wsl --list --verbose may have different headers depending on localization, just split by line - for _, rows := range strings.Split(strings.ReplaceAll(out, "\r\n", "\n"), "\n") { - cols := wslListColsRegex.Split(strings.TrimSpace(rows), -1) - nameIdx := 0 - // '*' indicates default instance - if cols[0] == "*" { - nameIdx = 1 - } - if cols[nameIdx] == distroName { - instState = cols[nameIdx+1] - break - } - } - - if instState == "" { - return limatype.StatusUninitialized, nil - } - - return instState, nil -} - -// GetSSHAddress runs a hostname command to get the IP from inside of a wsl2 VM. -// -// Expected output (whitespace preserved, [] for optional): -// PS > wsl -d bash -c hostname -I | cut -d' ' -f1 -// 168.1.1.1 [10.0.0.1] -// But busybox hostname does not implement --all-ip-addresses: -// hostname: unrecognized option: I -func GetSSHAddress(ctx context.Context, instName string) (string, error) { - distroName := "lima-" + instName - // Ubuntu - cmd := exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "bash", "-c", `hostname -I | cut -d ' ' -f1`) - out, err := cmd.CombinedOutput() - if err == nil { - return strings.TrimSpace(string(out)), nil - } - // Alpine - cmd = exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "sh", "-c", `ip route get 1 | awk '{gsub("^.*src ",""); print $1; exit}'`) - out, err = cmd.CombinedOutput() - if err == nil { - return strings.TrimSpace(string(out)), nil - } - // fallback - cmd = exec.CommandContext(ctx, "wsl.exe", "-d", distroName, "hostname", "-i") - out, err = cmd.CombinedOutput() - if err != nil || strings.HasPrefix(string(out), "127.") { - return "", fmt.Errorf("failed to get hostname for instance %q, err: %w (out=%q)", instName, err, string(out)) - } - - return strings.TrimSpace(string(out)), nil -} diff --git a/pkg/store/store.go b/pkg/store/store.go index 7661cd20545..af8646da540 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -6,10 +6,12 @@ package store import ( "context" "errors" + "fmt" "os" "path/filepath" "strings" + "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/identifiers" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" @@ -119,6 +121,9 @@ func LoadYAMLByFilePath(ctx context.Context, filePath string) (*limatype.LimaYAM if err != nil { return nil, err } + if err := driverutil.ResolveVMType(y, filePath); err != nil { + return nil, fmt.Errorf("failed to accept config for %q: %w", filePath, err) + } if err := limayaml.Validate(y, false); err != nil { return nil, err } diff --git a/templates/default.yaml b/templates/default.yaml index 2aa5c4077d1..2b9196e7f40 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -329,6 +329,15 @@ vmOpts: # armv7l: "max" # (or "host" when running on armv7l host) # riscv64: "max" # (or "host" when running on riscv64 host) # x86_64: "max" # (or "host" when running on x86_64 host; additional options are appended on Intel Mac) + vz: + rosetta: + # Enable Rosetta inside the VM; needs `vmType: vz` + # Hint: try `softwareupdate --install-rosetta` if Lima gets stuck at `Installing rosetta...` + # 🟢 Builtin default: false + enabled: null + # Register rosetta to /proc/sys/fs/binfmt_misc + # 🟢 Builtin default: false + binfmt: null # OS: "Linux". # 🟢 Builtin default: "Linux" @@ -337,13 +346,9 @@ os: null # DEPRECATED: Use vmOpts.qemu.cpuType instead. See the vmOpts.qemu.cpuType section above for configuration. cpuType: +# DEPRECATED: Use vmOpts.vz.rosetta instead. See the vmOpts.qemu.cpuType section above for configuration. rosetta: - # Enable Rosetta inside the VM; needs `vmType: vz` - # Hint: try `softwareupdate --install-rosetta` if Lima gets stuck at `Installing rosetta...` - # 🟢 Builtin default: false enabled: null - # Register rosetta to /proc/sys/fs/binfmt_misc - # 🟢 Builtin default: false binfmt: null # Specify the timezone name (as used by the zoneinfo database). Specify the empty string