From 5e1c2f8d7d807dab77dba589f543e181778314b2 Mon Sep 17 00:00:00 2001 From: Brent Baude Date: Fri, 3 Oct 2025 14:01:49 -0500 Subject: [PATCH] Machine init --provider Add the ability for users to override the default provider when creating mahcines. The new flag is `--provider` and allows you to specifiy a valid vmtype for the platform. This PR also removes the previous list test where we tested listing all providers. I added a PR for testing --provider which includes a standard `machine ls` which defaults now to showing all providers. Signed-off-by: Brent Baude --- cmd/podman/machine/info.go | 6 +- cmd/podman/machine/init.go | 25 ++++++- cmd/podman/machine/inspect.go | 2 +- cmd/podman/machine/machine.go | 15 +++-- .../markdown/podman-machine-init.1.md.in | 11 ++++ docs/source/markdown/podman-machine.1.md | 11 ++++ pkg/machine/e2e/config_darwin_test.go | 11 ---- pkg/machine/e2e/config_freebsd_test.go | 4 -- pkg/machine/e2e/config_init_test.go | 24 +++---- pkg/machine/e2e/config_linux_test.go | 4 -- pkg/machine/e2e/config_list_test.go | 5 -- pkg/machine/e2e/config_windows_test.go | 11 ---- pkg/machine/e2e/init_test.go | 65 +++++++++++++++++++ pkg/machine/e2e/list_test.go | 47 -------------- pkg/machine/provider/platform_darwin.go | 8 ++- pkg/machine/provider/platform_unix.go | 18 +++-- pkg/machine/provider/platform_windows.go | 14 ++-- 17 files changed, 162 insertions(+), 119 deletions(-) diff --git a/cmd/podman/machine/info.go b/cmd/podman/machine/info.go index 237e9b2c9c..973bc0e10d 100644 --- a/cmd/podman/machine/info.go +++ b/cmd/podman/machine/info.go @@ -100,7 +100,7 @@ func hostInfo() (*entities.MachineHostInfo, error) { host.Arch = runtime.GOARCH host.OS = runtime.GOOS - dirs, err := env.GetMachineDirs(provider.VMType()) + dirs, err := env.GetMachineDirs(machineProvider.VMType()) if err != nil { return nil, err } @@ -126,7 +126,7 @@ func hostInfo() (*entities.MachineHostInfo, error) { host.DefaultMachine = vm.Name } // If machine is running or starting, it is automatically the current machine - state, err := provider.State(vm, false) + state, err := machineProvider.State(vm, false) if err != nil { return nil, err } @@ -149,7 +149,7 @@ func hostInfo() (*entities.MachineHostInfo, error) { } } - host.VMType = provider.VMType().String() + host.VMType = machineProvider.VMType().String() host.MachineImageDir = dirs.DataDir.GetPath() host.MachineConfigDir = dirs.ConfigDir.GetPath() diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 6bf03d9eaf..eeef6aa838 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -6,12 +6,15 @@ import ( "errors" "fmt" "os" + "slices" "github.com/containers/podman/v6/cmd/podman/registry" ldefine "github.com/containers/podman/v6/libpod/define" "github.com/containers/podman/v6/libpod/events" "github.com/containers/podman/v6/pkg/machine/define" + "github.com/containers/podman/v6/pkg/machine/provider" "github.com/containers/podman/v6/pkg/machine/shim" + "github.com/containers/podman/v6/pkg/machine/vmconfigs" "github.com/shirou/gopsutil/v4/mem" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -36,6 +39,7 @@ var ( initOptionalFlags = InitOptionalFlags{} defaultMachineName = define.DefaultMachineName now bool + providerOverride string ) // Flags which have a meaning when unspecified that differs from the flag default @@ -158,6 +162,10 @@ func init() { flags.BoolVar(&initOptionalFlags.tlsVerify, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries") + + providerFlagName := "provider" + flags.StringVar(&providerOverride, providerFlagName, "", "Override the default machine provider") + _ = initCmd.RegisterFlagCompletionFunc(providerFlagName, autocompleteMachineProvider) } func initMachine(cmd *cobra.Command, args []string) error { @@ -173,6 +181,21 @@ func initMachine(cmd *cobra.Command, args []string) error { } } + // If the provider option was given, we need to override the + // current provider + if cmd.Flags().Changed("provider") { + // var found bool + indexFunc := func(s vmconfigs.VMProvider) bool { + return s.VMType().String() == providerOverride + } + providers := provider.GetAll() + indexVal := slices.IndexFunc(providers, indexFunc) + if indexVal == -1 { + return fmt.Errorf("unsupported provider %q", providerOverride) + } + machineProvider = providers[indexVal] + } + // The vmtype names need to be reserved and cannot be used for podman machine names if _, err := define.ParseVMType(initOpts.Name, define.UnknownVirt); err == nil { return fmt.Errorf("cannot use %q for a machine name", initOpts.Name) @@ -251,7 +274,7 @@ func initMachine(cmd *cobra.Command, args []string) error { // return err // } - err = shim.Init(initOpts, provider) + err = shim.Init(initOpts, machineProvider) if err != nil { // The installation is partially complete and podman should // exit gracefully with no error and no success message. diff --git a/cmd/podman/machine/inspect.go b/cmd/podman/machine/inspect.go index b83b26ba27..a87d43ddb8 100644 --- a/cmd/podman/machine/inspect.go +++ b/cmd/podman/machine/inspect.go @@ -60,7 +60,7 @@ func inspect(cmd *cobra.Command, args []string) error { continue } - dirs, err := env.GetMachineDirs(provider.VMType()) + dirs, err := env.GetMachineDirs(machineProvider.VMType()) if err != nil { return err } diff --git a/cmd/podman/machine/machine.go b/cmd/podman/machine/machine.go index bf96602d70..8c8376bc8d 100644 --- a/cmd/podman/machine/machine.go +++ b/cmd/podman/machine/machine.go @@ -41,10 +41,7 @@ var ( } ) -var ( - // TODO This needs to be deleted! - provider vmconfigs.VMProvider -) +var machineProvider vmconfigs.VMProvider func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ @@ -54,7 +51,7 @@ func init() { func machinePreRunE(c *cobra.Command, args []string) error { var err error - provider, err = provider2.Get() + machineProvider, err = provider2.Get() if err != nil { return err } @@ -104,6 +101,14 @@ func autocompleteMachine(_ *cobra.Command, args []string, toComplete string) ([] return nil, cobra.ShellCompDirectiveNoFileComp } +func autocompleteMachineProvider(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + suggestions := make([]string, 0) + for _, p := range provider2.GetAll() { + suggestions = append(suggestions, p.VMType().String()) + } + return suggestions, cobra.ShellCompDirectiveNoFileComp +} + func getMachines(toComplete string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} provider, err := provider2.Get() diff --git a/docs/source/markdown/podman-machine-init.1.md.in b/docs/source/markdown/podman-machine-init.1.md.in index 672d2b8557..fe15107af1 100644 --- a/docs/source/markdown/podman-machine-init.1.md.in +++ b/docs/source/markdown/podman-machine-init.1.md.in @@ -96,6 +96,12 @@ Add the provided Ansible playbook to the machine and execute it after the first Note: The playbook will be executed with the same privileges given to the user in the virtual machine. The playbook provided cannot include other files from the host system, as they will not be copied. Use of the `--playbook` flag will require the image to include Ansible. The default image provided will have Ansible included. + +#### **--provider** + +Specify the provider for the machine to be created. This allows users to override the default provider on platforms +that have multiple providers. + #### **--rootful** Whether this machine prefers rootful (`true`) or rootless (`false`) @@ -237,6 +243,11 @@ Initialize the default Podman machine with a usb device passthrough with specifi $ podman machine init --usb bus=1,devnum=3 ``` +Initialize a machine on different provider than the default +``` +$ podman machine init --provider applehv +``` + ## SEE ALSO **[podman(1)](podman.1.md)**, **[podman-machine(1)](podman-machine.1.md)**, **containers.conf(5)** diff --git a/docs/source/markdown/podman-machine.1.md b/docs/source/markdown/podman-machine.1.md index 5e15f6db8d..0e25b156f9 100644 --- a/docs/source/markdown/podman-machine.1.md +++ b/docs/source/markdown/podman-machine.1.md @@ -22,6 +22,17 @@ environment variable while the machines are running can lead to unexpected behav Podman machine behaviour can be modified via the [machine] section in the containers.conf(5) file. +Podman is based on virtual machine providers. The following table describes which providers are +supported by platform. The asterisk denotes the default provider for the platform. + +| Platform | Provider | +| -------- |----------| +| Linux | qemu* | +| MacOS | libkrun* | +| MacOS | applehv | +| Windows | wsl* | +| Windows | hyperv | + ## SUBCOMMANDS | Command | Man Page | Description | diff --git a/pkg/machine/e2e/config_darwin_test.go b/pkg/machine/e2e/config_darwin_test.go index 6759e6a472..a04695c32c 100644 --- a/pkg/machine/e2e/config_darwin_test.go +++ b/pkg/machine/e2e/config_darwin_test.go @@ -1,14 +1,3 @@ package e2e_test -import "github.com/containers/podman/v6/pkg/machine/define" - const podmanBinary = "../../../bin/darwin/podman" - -func getOtherProvider() string { - if isVmtype(define.AppleHvVirt) { - return "libkrun" - } else if isVmtype(define.LibKrun) { - return "applehv" - } - return "" -} diff --git a/pkg/machine/e2e/config_freebsd_test.go b/pkg/machine/e2e/config_freebsd_test.go index 46107a10dd..f07121a90f 100644 --- a/pkg/machine/e2e/config_freebsd_test.go +++ b/pkg/machine/e2e/config_freebsd_test.go @@ -1,7 +1,3 @@ package e2e_test const podmanBinary = "../../../bin/podman-remote" - -func getOtherProvider() string { - return "" -} diff --git a/pkg/machine/e2e/config_init_test.go b/pkg/machine/e2e/config_init_test.go index 0934c872d6..33aafb6dcd 100644 --- a/pkg/machine/e2e/config_init_test.go +++ b/pkg/machine/e2e/config_init_test.go @@ -10,22 +10,8 @@ import ( ) type initMachine struct { - /* - --cpus uint Number of CPUs (default 1) - --disk-size uint Disk size in GiB (default 100) - --ignition-path string Path to ignition file - --username string Username of the remote user (default "core" for FCOS, "user" for Fedora) - --image-path string Path to bootable image (default "testing") - -m, --memory uint Memory in MiB (default 2048) - --now Start machine now - --rootful Whether this machine should prefer rootful container execution - --playbook string Run an ansible playbook after first boot - --tls-verify Require HTTPS and verify certificates when contacting registries - --timezone string Set timezone (default "local") - -v, --volume stringArray Volumes to mount, source:target - --volume-driver string Optional volume driver - */ playbook string + provider string cpus *uint diskSize *uint swap *uint @@ -80,6 +66,9 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string { if l := len(i.playbook); l > 0 { cmd = append(cmd, "--playbook", i.playbook) } + if l := len(i.provider); l > 0 { + cmd = append(cmd, "--provider", i.provider) + } if i.userModeNetworking { cmd = append(cmd, "--user-mode-networking") } @@ -176,6 +165,11 @@ func (i *initMachine) withRunPlaybook(p string) *initMachine { return i } +func (i *initMachine) withProvider(p string) *initMachine { + i.provider = p + return i +} + func (i *initMachine) withTlsVerify(tlsVerify *bool) *initMachine { i.tlsVerify = tlsVerify return i diff --git a/pkg/machine/e2e/config_linux_test.go b/pkg/machine/e2e/config_linux_test.go index 46107a10dd..f07121a90f 100644 --- a/pkg/machine/e2e/config_linux_test.go +++ b/pkg/machine/e2e/config_linux_test.go @@ -1,7 +1,3 @@ package e2e_test const podmanBinary = "../../../bin/podman-remote" - -func getOtherProvider() string { - return "" -} diff --git a/pkg/machine/e2e/config_list_test.go b/pkg/machine/e2e/config_list_test.go index 2ce0c03f46..e7d877124b 100644 --- a/pkg/machine/e2e/config_list_test.go +++ b/pkg/machine/e2e/config_list_test.go @@ -48,8 +48,3 @@ func (i *listMachine) withFormat(format string) *listMachine { i.format = format return i } - -func (i *listMachine) withAllProviders() *listMachine { - i.allProviders = true - return i -} diff --git a/pkg/machine/e2e/config_windows_test.go b/pkg/machine/e2e/config_windows_test.go index c208a0f8ec..091ac1b6e5 100644 --- a/pkg/machine/e2e/config_windows_test.go +++ b/pkg/machine/e2e/config_windows_test.go @@ -7,8 +7,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega/gexec" - - "github.com/containers/podman/v6/pkg/machine/define" ) const podmanBinary = "../../../bin/windows/podman.exe" @@ -29,15 +27,6 @@ func pgrep(n string) (string, error) { return strOut, nil } -func getOtherProvider() string { - if isVmtype(define.WSLVirt) { - return "hyperv" - } else if isVmtype(define.HyperVVirt) { - return "wsl" - } - return "" -} - func runWslCommand(cmdArgs []string) (*machineSession, error) { binary := "wsl" GinkgoWriter.Println(binary + " " + strings.Join(cmdArgs, " ")) diff --git a/pkg/machine/e2e/init_test.go b/pkg/machine/e2e/init_test.go index bc5afe8b94..4c6c1fe884 100644 --- a/pkg/machine/e2e/init_test.go +++ b/pkg/machine/e2e/init_test.go @@ -9,8 +9,11 @@ import ( "strings" "time" + "github.com/containers/podman/v6/pkg/domain/entities" "github.com/containers/podman/v6/pkg/machine/define" + "github.com/containers/podman/v6/pkg/machine/provider" "github.com/containers/podman/v6/utils" + jsoniter "github.com/json-iterator/go" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" @@ -635,6 +638,68 @@ var _ = Describe("podman machine init", func() { Expect(err).ToNot(HaveOccurred()) Expect(proc2Session.outputToString()).To(ContainSubstring("/proc/sys/fs/binfmt_misc/qemu-x86_64")) }) + + It("init machine with invalid --provider for platform", func() { + var providerOverride string + switch testProvider.VMType() { + case define.QemuVirt, define.AppleHvVirt, define.LibKrun: + providerOverride = "wsl" + case define.WSLVirt, define.HyperVVirt: + providerOverride = "applehv" + default: + Fail("unsupported provider in tests") + } + + i := initMachine{} + machineName := randomString() + session, err := mb.setName(machineName).setCmd(i.withImage(mb.imagePath).withProvider(providerOverride)).run() + Expect(err).ToNot(HaveOccurred()) + Expect(session.errorToString()).To(ContainSubstring(fmt.Sprintf("unsupported provider %q", providerOverride))) + + }) + + It("machine init --provider", func() { + skipIfVmtype(define.WSLVirt, "Skip to avoid long tests or image pulls on Windows") + skipIfVmtype(define.HyperVVirt, "Skip to avoid long tests or image pulls Windows") + verify := make(map[string]string) + + // Loop all providers and create a map with + // machine_name -> provider + for _, p := range provider.GetAll() { + machineName := randomString() + verify[machineName] = p.VMType().String() + } + + // Loop verify and create a podman machine with the name and + // --provider + for name, p := range verify { + i := initMachine{} + session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withProvider(p)).run() + Expect(err).ToNot(HaveOccurred()) + Expect(session.ExitCode()).To(Equal(0)) + } + + // List machines and marshall them + list := new(listMachine) + list = list.withFormat("json") + listSession, err := mb.setCmd(list).run() + Expect(err).NotTo(HaveOccurred()) + var listResponse []*entities.ListReporter + err = jsoniter.Unmarshal(listSession.Bytes(), &listResponse) + Expect(err).NotTo(HaveOccurred()) + Expect(listResponse).To(HaveLen(len(verify))) + + // Loop our machine list and make sure we have a name -> provider + // match + for _, l := range listResponse { + p, ok := verify[l.Name] + if !ok { + Fail(fmt.Sprintf("%s not found in list", l.Name)) + } + Expect(p).To(Equal(l.VMType)) + } + }) + }) var p4Config = []byte(`{ diff --git a/pkg/machine/e2e/list_test.go b/pkg/machine/e2e/list_test.go index c967d9fa4f..15a56279c4 100644 --- a/pkg/machine/e2e/list_test.go +++ b/pkg/machine/e2e/list_test.go @@ -1,14 +1,12 @@ package e2e_test import ( - "os" "slices" "strconv" "strings" "time" "github.com/containers/podman/v6/pkg/domain/entities" - "github.com/containers/podman/v6/pkg/machine/define" jsoniter "github.com/json-iterator/go" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -184,51 +182,6 @@ var _ = Describe("podman machine list", func() { Expect(listSession).To(Exit(0)) Expect(listSession.outputToString()).To(Equal("2GiB 11GiB")) }) - It("list machine from all providers", func() { - Skip("This test will be changed for when --provider is added to init") - - // create machine on other provider - currprovider := os.Getenv("CONTAINERS_MACHINE_PROVIDER") - os.Setenv("CONTAINERS_MACHINE_PROVIDER", getOtherProvider()) - defer os.Setenv("CONTAINERS_MACHINE_PROVIDER", currprovider) - - othermach := new(initMachine) - if !isWSL() && !isVmtype(define.HyperVVirt) { - // This would need to fetch a new image as we cannot use the image from the other provider, - // to avoid big pulls which are slow and flaky use /dev/null which works on macos and qemu - // as we never run the image if we do not start it. - othermach.withImage(os.DevNull) - } - session, err := mb.setName("otherprovider").setCmd(othermach).run() - // make sure to remove machine from other provider later - defer func() { - os.Setenv("CONTAINERS_MACHINE_PROVIDER", getOtherProvider()) - defer os.Setenv("CONTAINERS_MACHINE_PROVIDER", currprovider) - rm := new(rmMachine) - removed, err := mb.setName("otherprovider").setCmd(rm.withForce()).run() - Expect(err).ToNot(HaveOccurred()) - Expect(removed).To(Exit(0)) - }() - Expect(err).ToNot(HaveOccurred()) - Expect(session).To(Exit(0)) - - // change back to current provider - os.Setenv("CONTAINERS_MACHINE_PROVIDER", currprovider) - name := randomString() - i := new(initMachine) - session, err = mb.setName(name).setCmd(i.withImage(mb.imagePath)).run() - Expect(err).ToNot(HaveOccurred()) - Expect(session).To(Exit(0)) - - list := new(listMachine) - listSession, err := mb.setCmd(list.withAllProviders().withFormat("{{.Name}}")).run() - Expect(err).NotTo(HaveOccurred()) - Expect(listSession).To(Exit(0)) - listNames := listSession.outputToStringSlice() - stripAsterisk(listNames) - Expect(listNames).To(HaveLen(2)) - Expect(listNames).To(ContainElements("otherprovider", name)) - }) }) func stripAsterisk(sl []string) { diff --git a/pkg/machine/provider/platform_darwin.go b/pkg/machine/provider/platform_darwin.go index fc068d7411..c6586a92ce 100644 --- a/pkg/machine/provider/platform_darwin.go +++ b/pkg/machine/provider/platform_darwin.go @@ -33,6 +33,12 @@ func Get() (vmconfigs.VMProvider, error) { } logrus.Debugf("Using Podman machine with `%s` virtualization provider", resolvedVMType.String()) + return GetByVMType(resolvedVMType) +} + +// GetByVMType takes a VMType (presumably from ParseVMType) and returns the correlating +// VMProvider +func GetByVMType(resolvedVMType define.VMType) (vmconfigs.VMProvider, error) { switch resolvedVMType { case define.AppleHvVirt: return new(applehv.AppleHVStubber), nil @@ -42,8 +48,8 @@ func Get() (vmconfigs.VMProvider, error) { } return new(libkrun.LibKrunStubber), nil default: - return nil, fmt.Errorf("unsupported virtualization provider: `%s`", resolvedVMType.String()) } + return nil, fmt.Errorf("unsupported virtualization provider: `%s`", resolvedVMType.String()) } func GetAll() []vmconfigs.VMProvider { diff --git a/pkg/machine/provider/platform_unix.go b/pkg/machine/provider/platform_unix.go index f0126c0b25..6beda870ed 100644 --- a/pkg/machine/provider/platform_unix.go +++ b/pkg/machine/provider/platform_unix.go @@ -30,18 +30,24 @@ func Get() (vmconfigs.VMProvider, error) { } logrus.Debugf("Using Podman machine with `%s` virtualization provider", resolvedVMType.String()) - switch resolvedVMType { - case define.QemuVirt: - return qemu.NewStubber() - default: - return nil, fmt.Errorf("unsupported virtualization provider: `%s`", resolvedVMType.String()) - } + return GetByVMType(resolvedVMType) } func GetAll() []vmconfigs.VMProvider { return []vmconfigs.VMProvider{new(qemu.QEMUStubber)} } +// GetByVMType takes a VMType (presumably from ParseVMType) and returns the correlating +// VMProvider +func GetByVMType(resolvedVMType define.VMType) (vmconfigs.VMProvider, error) { + switch resolvedVMType { + case define.QemuVirt: + return qemu.NewStubber() + default: + } + return nil, fmt.Errorf("unsupported virtualization provider: `%s`", resolvedVMType.String()) +} + // SupportedProviders returns the providers that are supported on the host operating system func SupportedProviders() []define.VMType { return []define.VMType{define.QemuVirt} diff --git a/pkg/machine/provider/platform_windows.go b/pkg/machine/provider/platform_windows.go index 8bbb67fc4e..da6aebcd07 100644 --- a/pkg/machine/provider/platform_windows.go +++ b/pkg/machine/provider/platform_windows.go @@ -5,12 +5,11 @@ import ( "os" "github.com/containers/libhvee/pkg/hypervctl" + "github.com/containers/podman/v6/pkg/machine/define" + "github.com/containers/podman/v6/pkg/machine/hyperv" "github.com/containers/podman/v6/pkg/machine/vmconfigs" "github.com/containers/podman/v6/pkg/machine/wsl" "github.com/containers/podman/v6/pkg/machine/wsl/wutil" - - "github.com/containers/podman/v6/pkg/machine/define" - "github.com/containers/podman/v6/pkg/machine/hyperv" "github.com/sirupsen/logrus" "go.podman.io/common/pkg/config" ) @@ -28,8 +27,13 @@ func Get() (vmconfigs.VMProvider, error) { if err != nil { return nil, err } - logrus.Debugf("Using Podman machine with `%s` virtualization provider", resolvedVMType.String()) + return GetByVMType(resolvedVMType) +} + +// GetByVMType takes a VMType (presumably from ParseVMType) and returns the correlating +// VMProvider +func GetByVMType(resolvedVMType define.VMType) (vmconfigs.VMProvider, error) { switch resolvedVMType { case define.WSLVirt: return new(wsl.WSLStubber), nil @@ -39,8 +43,8 @@ func Get() (vmconfigs.VMProvider, error) { } return new(hyperv.HyperVStubber), nil default: - return nil, fmt.Errorf("unsupported virtualization provider: `%s`", resolvedVMType.String()) } + return nil, fmt.Errorf("unsupported virtualization provider: `%s`", resolvedVMType.String()) } func GetAll() []vmconfigs.VMProvider {