diff --git a/Makefile b/Makefile index 411c8cc490..1473a2541b 100644 --- a/Makefile +++ b/Makefile @@ -663,7 +663,7 @@ localunit: test/goecho/goecho test/version/version UNIT=1 $(GINKGO) \ -r \ $(TESTFLAGS) \ - --skip-package test/e2e,pkg/bindings,hack,pkg/machine/e2e,pkg/machine/wsl \ + --skip-package test/e2e,pkg/bindings,hack,pkg/machine/e2e,pkg/machine/wsl,pkg/machine/hyperv \ --cover \ --covermode atomic \ --coverprofile coverprofile \ diff --git a/cmd/podman/system/hyperv_prep_windows.go b/cmd/podman/system/hyperv_prep_windows.go new file mode 100644 index 0000000000..c5a3f7e91b --- /dev/null +++ b/cmd/podman/system/hyperv_prep_windows.go @@ -0,0 +1,267 @@ +//go:build windows && remote + +package system + +import ( + "bufio" + "errors" + "fmt" + "os" + "os/user" + "strings" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.podman.io/common/pkg/completion" + "go.podman.io/podman/v6/cmd/podman/registry" + "go.podman.io/podman/v6/cmd/podman/validate" + "go.podman.io/podman/v6/pkg/machine/hyperv" + "go.podman.io/podman/v6/pkg/machine/hyperv/vsock" + "go.podman.io/podman/v6/pkg/machine/windows" +) + +const ( + mountsFlag = "mounts" + defaultMounts = 2 +) + +var ( + hypervPrepDescription = `Command for Windows administrators who need to configure an host for running Hyper-V-based Podman machines. + + This command creates the required registry entries in HKEY_LOCAL_MACHINE for Hyper-V vsock communication and adds the current user to the Hyper-V Administrators group. + + After this command execution, a non-admin user can manage Hyper-V-based Podman machines (create,start,stop,delete). + + This command requires administrator privileges except when using the flag --status.` + + hypervPrepCommand = &cobra.Command{ + Use: "hyperv-prep [options]", + Args: validate.NoArgs, + Short: "Prepare the host to run Hyper-V-based Podman machines", + Long: hypervPrepDescription, + PersistentPreRunE: validate.NoOp, + RunE: hypervPrep, + ValidArgsFunction: completion.AutocompleteNone, + Example: `podman system hyperv-prep +podman system hyperv-prep --status +podman system hyperv-prep --reset --force`, + } + showStatus bool + resetEntries bool + mounts int +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Command: hypervPrepCommand, + Parent: systemCmd, + }) + flags := hypervPrepCommand.Flags() + flags.BoolVar(&showStatus, "status", false, "Show vsock registry entries and Hyper-V group membership status") + flags.BoolVar(&resetEntries, "reset", false, "Remove all Podman vsock registry entries and optionally remove user from Hyper-V Administrators group") + flags.BoolVarP(&force, "force", "f", false, "Don't ask for confirmation during reset. Valid only when used with --reset.") + flags.IntVar(&mounts, mountsFlag, defaultMounts, "Number of vsock entries to create for mount purpose") + hypervPrepCommand.MarkFlagsMutuallyExclusive("status", "reset", mountsFlag) + hypervPrepCommand.MarkFlagsMutuallyExclusive("status", "force") + _ = hypervPrepCommand.RegisterFlagCompletionFunc(mountsFlag, completion.AutocompleteNone) +} + +func hypervPrep(_ *cobra.Command, _ []string) error { + // --status can run without administrator privileges + if showStatus { + if err := doStatusForRegistries(); err != nil { + return err + } + doStatusForGroupMembership() + return nil + } + + if !windows.HasAdminRights() { + return fmt.Errorf("this command requires administrator privileges, please run in an elevated terminal") + } + + if resetEntries { + if err := doResetForRegistries(); err != nil { + return err + } + return doResetForGroupMembership() + } + + if err := doPreparationForRegistries(); err != nil { + return err + } + return doPreparationForGroupMembership() +} + +func doPreparationForRegistries() error { + // Every VSock has a purpose (Network, Events or Fileserver) and the + // VSocks with purpose Fileserver need as many as the machine mounts. + entriesPerPurpose := map[vsock.HVSockPurpose]int{ + vsock.Network: 1, + vsock.Events: 1, + vsock.Fileserver: mounts, + } + + var created []string + for purpose, entries := range entriesPerPurpose { + for range entries { + if entry, err := vsock.NewHVSockRegistryEntry(purpose, true); err != nil { + if errors.Is(err, vsock.ErrVSockRegistryEntryExists) { + logrus.Infof("Registry entry for %s already exists, skipping", purpose) + continue + } + return err + } else { + created = append(created, fmt.Sprintf("%s (port %d)", purpose.String(), entry.Port)) + } + } + } + + if len(created) == 0 { + fmt.Println("All required registry entries already exist") + } else { + fmt.Println("Successfully created registry entries for:") + for _, entry := range created { + fmt.Printf(" - %s\n", entry) + } + fmt.Println("These entries will persist even when all machines are removed.") + } + + return nil +} + +func doPreparationForGroupMembership() error { + if hyperv.IsHyperVAdminsGroupMember() { + fmt.Println("User is already a member of the Hyper-V Administrators group") + return nil + } + u, err := user.Current() + if err != nil { + return fmt.Errorf("failed to get current user: %w", err) + } + if err := hyperv.AddUserToHyperVAdminGroup(u.Username); err != nil { + return fmt.Errorf("failed to add user to Hyper-V Administrators group: %w", err) + } + fmt.Printf("\nAdded user %s to the Hyper-V Administrators group\n", u.Username) + fmt.Println("Note: You may need to log out and log back in for the new group membership to take effect.") + return nil +} + +func doStatusForRegistries() error { + purposes := []vsock.HVSockPurpose{vsock.Network, vsock.Events, vsock.Fileserver} + fmt.Printf("Hyper-V vsock registry entries:\n") + foundAny := false + for _, purpose := range purposes { + entries, err := vsock.LoadAllHVSockRegistryEntriesByPurpose(purpose) + if err != nil { + logrus.Debugf("Error loading registry entries for %s: %v", purpose.String(), err) + continue + } + + if len(entries) > 0 { + foundAny = true + for _, entry := range entries { + fmt.Print(entry) + } + } + } + if !foundAny { + fmt.Println(" No vsock registry entries found.") + } + return nil +} + +func doStatusForGroupMembership() { + fmt.Println("Hyper-V Administrators group membership:") + if hyperv.IsHyperVAdminsGroupMember() { + fmt.Println(" Current user is a member") + } else { + fmt.Println(" Current user is NOT a member") + } +} + +func doResetForRegistries() error { + purposes := []vsock.HVSockPurpose{vsock.Network, vsock.Events, vsock.Fileserver} + var allEntries []*vsock.HVSockRegistryEntry + // Load all entries for each purpose + for _, purpose := range purposes { + entries, err := vsock.LoadAllHVSockRegistryEntriesByPurpose(purpose) + if err != nil { + logrus.Debugf("Error loading registry entries for %s: %v", purpose.String(), err) + continue + } + allEntries = append(allEntries, entries...) + } + if len(allEntries) == 0 { + fmt.Println("No vsock registry entries found to remove.") + return nil + } + if !force { + fmt.Println("Existing VSock registry entries for Podman:") + for _, entry := range allEntries { + fmt.Print(entry) + } + reader := bufio.NewReader(os.Stdin) + fmt.Print("Do you want to delete these entries from the Windows registry? [y/N] ") + answer, err := reader.ReadString('\n') + if err != nil { + return err + } + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + var removed []string + var removalErrors []error + for _, entry := range allEntries { + if err := entry.Remove(); err != nil { + logrus.Errorf("Failed to remove registry entry %s: %v", entry.KeyName, err) + removalErrors = append(removalErrors, fmt.Errorf("failed to remove %s: %w", entry.KeyName, err)) + } else { + removed = append(removed, fmt.Sprintf("%s (port %d)", entry.Purpose.String(), entry.Port)) + } + } + if len(removed) > 0 { + fmt.Println("Successfully removed registry entries for:") + for _, entry := range removed { + fmt.Printf(" - %s\n", entry) + } + } + if len(removalErrors) > 0 { + fmt.Println("\nSome entries could not be removed. See logs for details.") + return errors.Join(removalErrors...) + } + return nil +} + +func doResetForGroupMembership() error { + inGroup, err := hyperv.IsCurrentUserInHyperVAdminGroup() + if err != nil { + return fmt.Errorf("failed to check Hyper-V Administrators group membership: %w", err) + } + if !inGroup { + fmt.Println("Current user isn't a member of the Hyper-V Administrators group, it won't be removed.") + return nil + } + if !force { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Do you want to remove the current user from the Hyper-V Administrators group? [y/N] ") + answer, err := reader.ReadString('\n') + if err != nil { + return err + } + + if strings.ToLower(answer)[0] != 'y' { + return nil + } + } + u, err := user.Current() + if err != nil { + return fmt.Errorf("failed to get current user: %w", err) + } + if err := hyperv.RemoveUserFromHyperVAdminGroup(u.Username); err != nil { + return fmt.Errorf("failed to remove user from Hyper-V Administrators group: %w", err) + } + fmt.Printf("Removed user %s from the Hyper-V Administrators group\n", u.Username) + return nil +} diff --git a/contrib/cirrus/win-unit-main.ps1 b/contrib/cirrus/win-unit-main.ps1 index b945f4a155..5f0151a5e2 100644 --- a/contrib/cirrus/win-unit-main.ps1 +++ b/contrib/cirrus/win-unit-main.ps1 @@ -10,4 +10,10 @@ if ($Env:CI -eq "true") { Run-Command ".\winmake.ps1 localunit" +Run-Command ".\winmake.ps1 ginkgo-run" # non-machine e2e integration + # test. Currently it's only + # checking the hyperv-prep + # command and it doesn't deserve + # a specific task. + Pop-Location diff --git a/docs/source/markdown/podman-system-hyperv-prep.1.md b/docs/source/markdown/podman-system-hyperv-prep.1.md new file mode 100644 index 0000000000..3813b2aa89 --- /dev/null +++ b/docs/source/markdown/podman-system-hyperv-prep.1.md @@ -0,0 +1,71 @@ +% podman-system-hyperv-prep 1 + +## NAME +podman\-system\-hyperv\-prep - A Windows administrator command to prepare a host that is going to run Hyper-V based Podman machines + +## SYNOPSIS +**podman system hyperv-prep** [*options*] + +## DESCRIPTION +Prepare a Windows host to run Hyper-V based Podman machines by creating the +required registry entries in HKEY_LOCAL_MACHINE for Hyper-V VSock communication +and adding the current user to the Hyper-V Administrators group. + +The registry entries are marked to persist even when all machines are removed, +and the group membership allows the user to manage Hyper-V virtual machines. +Together, these avoid the need for administrator privileges during normal machine +operations. + +This command requires administrator privileges and is only available on Windows. +The **--status** option can be used without administrator privileges. + +## OPTIONS + +#### **--mounts**=*number* + +Number of VSock entries for mount purpose. Every mounted host folder of a running machine needs a dedicated VSock. There should be enough VSock to satisfy the needs of running Podman machines on the host. The default is **2**. + +#### **--force**, **-f** + +Skip confirmation prompts during reset. Only valid with **--reset**. + +#### **--reset** + +Remove all Podman VSock registry entries and remove the current user from the +Hyper-V Administrators group. Prompts for confirmation before each action unless +**--force** is specified. + +#### **--status** + +Show the list of VSock registry entries and the current user's Hyper-V +Administrators group membership status. + +## EXAMPLE + +Create the required registry entries and add the current user to the Hyper-V +Administrators group: +``` +podman system hyperv-prep +``` + +Show existing registry entries and group membership status: +``` +podman system hyperv-prep --status +``` + +Remove all Podman VSock registry entries and the current user from the Hyper-V +Administrators group (with confirmation prompts): +``` +podman system hyperv-prep --reset +``` + +Reset without confirmation prompts: +``` +podman system hyperv-prep --reset --force +``` + +## SEE ALSO +**[podman(1)](podman.1.md)**, **[podman-system(1)](podman-system.1.md)** + +## HISTORY +April 2026 diff --git a/docs/source/markdown/podman-system.1.md b/docs/source/markdown/podman-system.1.md index 0f20ced2c4..7cc40b4286 100644 --- a/docs/source/markdown/podman-system.1.md +++ b/docs/source/markdown/podman-system.1.md @@ -15,6 +15,7 @@ The system command allows management of the podman systems | ------- | ------------------------------------------------------------ | ------------------------------------------------------------------------ | | check | [podman-system-check(1)](podman-system-check.1.md) | Perform consistency checks on image and container storage. | connection | [podman-system-connection(1)](podman-system-connection.1.md) | Manage the destination(s) for Podman service(s) | +| hyperv-prep| [podman-system-hyperv-prep(1)](podman-system-hyperv-prep.1.md) | A Windows administrator command to prepare a host that is going to run Hyper-V based Podman machines | | df | [podman-system-df(1)](podman-system-df.1.md) | Show podman disk usage. | | events | [podman-events(1)](podman-events.1.md) | Monitor Podman events | | info | [podman-info(1)](podman-info.1.md) | Display Podman related system information. | diff --git a/docs/tutorials/podman-for-windows.md b/docs/tutorials/podman-for-windows.md index 41ed4809f5..ab825d3f86 100644 --- a/docs/tutorials/podman-for-windows.md +++ b/docs/tutorials/podman-for-windows.md @@ -55,9 +55,13 @@ a VM, you must have a VM that supports nested virtualization. Hyper-V is only available on Windows Enterprise, Pro, or Education editions (not Home). The command to initialize the first Hyper-V Podman machine, and the command -to remove the last one, both require administrator privileges. Other commands for -machine management (start, stop, etc...) require that the current user is a member -of the Hyper-V administrators group. +to remove the last one, both require administrator privileges because they create +and delete machine-scope registry keys. Additionally, all Hyper-V machine commands +require that the current user is a member of the Hyper-V Administrators group. To +avoid these requirements, an administrator can run `podman system hyperv-prep` +once to pre-create the registry entries and add the current user to the Hyper-V +Administrators group, after which no machine operation requires administrator +privileges. It is also recommended to install the modern "Windows Terminal," which provides a superior user experience to the standard PowerShell and CMD @@ -176,10 +180,14 @@ PS C:\Users\User> podman machine init --provider hyperv **Note:** Hyper-V requires administrator privileges to initialize the first podman machine. Similarly it requires administrator privileges to remove the -last machine. That's because these operation create and delete -machine-scope registry keys, required to support the communication between the -guest OS and the host. Other commands such as machine start and stop require -that the current user is a member of the Hyper-V Administrator group. +last machine. That's because these operations create and delete machine-scope +registry keys, required to support the communication between the guest OS and +the host. Additionally, all Hyper-V machine commands require that the current +user is a member of the Hyper-V Administrators group. To avoid these +requirements, run `podman system hyperv-prep` once from an administrator +terminal to pre-create persistent registry entries and add the current user +to the Hyper-V Administrators group, so that all subsequent machine operations +can run without administrator privileges. Starting Machine ---------------- diff --git a/hack/man-page-checker b/hack/man-page-checker index 748da6b7a0..04c41139e9 100755 --- a/hack/man-page-checker +++ b/hack/man-page-checker @@ -53,6 +53,11 @@ for md in $(ls -1 *-*.1.md | grep -v remote);do # dashes. parent="podman.1.md" fi + if [[ $parent =~ "podman-system-hyperv" ]]; then + # podman-system-hyperv-prep.1.md: "hyperv-prep" is a single + # hyphenated subcommand of "podman system", not a sub-subcommand. + parent="podman-system.1.md" + fi x=3 if expr -- "$parent" : ".*-" >/dev/null; then x=4 @@ -79,7 +84,8 @@ function compare_usage() { local from_man="$2" # Special case: this is an external call to docker - if [[ $cmd = "podman compose" ]] || [[ $cmd = "podmansh" ]]; then + # or a Windows-only command not available on Linux + if [[ $cmd = "podman compose" ]] || [[ $cmd = "podmansh" ]] || [[ $cmd = "podman system hyperv-prep" ]]; then return fi @@ -158,6 +164,10 @@ for md in *.1.md;do # special case: the command is "auto-update", with a hyphen md_nodash='podman auto-update' fi + if [[ $md_nodash = 'podman system hyperv prep' ]]; then + # special case: the command is "hyperv-prep", with a hyphen + md_nodash='podman system hyperv-prep' + fi if [[ "$cmd" != "$md_nodash" ]] && [[ "$cmd" != "podman-remote" ]]; then echo printf "Inconsistent program name in SYNOPSIS in %s:\n" $md diff --git a/pkg/machine/hyperv/hutil.go b/pkg/machine/hyperv/hutil.go index 16b4c9dbee..1cd9db6535 100644 --- a/pkg/machine/hyperv/hutil.go +++ b/pkg/machine/hyperv/hutil.go @@ -24,6 +24,7 @@ var ( // Lazily load the NetAPI32 DLL and the function we need modnetapi32 = syswindows.NewLazySystemDLL("netapi32.dll") procNetLocalGroupAddMembers = modnetapi32.NewProc("NetLocalGroupAddMembers") + procNetLocalGroupDelMembers = modnetapi32.NewProc("NetLocalGroupDelMembers") procNetLocalGroupGetMembers = modnetapi32.NewProc("NetLocalGroupGetMembers") procNetApiBufferFree = modnetapi32.NewProc("NetApiBufferFree") ) @@ -90,6 +91,40 @@ func AddUserToHyperVAdminGroup(username string) error { return nil } +// RemoveUserFromHyperVAdminGroup removes the specified user from the local Hyper-V Administrators group. +func RemoveUserFromHyperVAdminGroup(username string) error { + groupPtr, err := getHyperVAdminGroupNamePtr() + if err != nil { + return err + } + + userSid, _, _, err := syswindows.LookupSID("", username) + if err != nil { + return fmt.Errorf("failed to look up SID for user %q: %w", username, err) + } + + info := localGroupMembersInfo0{ + Sid: userSid, + } + + // C Signature: NET_API_STATUS NetLocalGroupDelMembers(LPCWSTR servername, LPCWSTR groupname, DWORD level, LPBYTE buf, DWORD totalentries) + ret, _, _ := procNetLocalGroupDelMembers.Call( + 0, // ServerName (0 = local machine) + uintptr(unsafe.Pointer(groupPtr)), // GroupName + 0, // Level 0 (we are passing a SID) + uintptr(unsafe.Pointer(&info)), // Buffer containing our struct + 1, // Total Entries being removed + ) + + errno := syswindows.Errno(ret) + // ERROR_MEMBER_NOT_IN_ALIAS = "The specified account name is not a member of the group." + if errno != syswindows.NO_ERROR && errno != syswindows.ERROR_MEMBER_NOT_IN_ALIAS { + return fmt.Errorf("NetLocalGroupDelMembers failed with error: %w", errno) + } + + return nil +} + func getHyperVAdminGroupNamePtr() (*uint16, error) { hypervAdminsSid, err := syswindows.CreateWellKnownSid(syswindows.WinBuiltinHyperVAdminsSid) if err != nil { @@ -175,6 +210,24 @@ func isUserInGroup(groupPtr *uint16, userSid *syswindows.SID) (bool, error) { return false, nil } +// IsCurrentUserInHyperVAdminGroup checks the group database directly +// (via NetLocalGroupGetMembers) rather than the process token, so it +// reflects membership changes that haven't taken effect in the current +// logon session yet. +func IsCurrentUserInHyperVAdminGroup() (bool, error) { + userSid, err := getCurrentUserSID() + if err != nil { + return false, fmt.Errorf("failed to look up SID for current user: %w", err) + } + + groupPtr, err := getHyperVAdminGroupNamePtr() + if err != nil { + return false, fmt.Errorf("failed to get localized Hyper-V Admin group name: %w", err) + } + + return isUserInGroup(groupPtr, userSid) +} + // VerifyHyperVPermissions returns `nil` if the user has the privileges to manage // Hyper-V VMs. This can be either because the command is run // as an administrator or because the user is a member of the Hyper-V Admins group. diff --git a/pkg/machine/hyperv/stubber.go b/pkg/machine/hyperv/stubber.go index 15aed69ffd..3dc638919a 100644 --- a/pkg/machine/hyperv/stubber.go +++ b/pkg/machine/hyperv/stubber.go @@ -31,6 +31,22 @@ type HyperVStubber struct { vmconfigs.HyperVConfig } +type permissionChecks struct { + isElevatedProcess func() bool + isHyperVAdminMember func() bool + vsockEntriesExist func(int) bool + existingMachinesNum func() (int, error) +} + +func (h HyperVStubber) defaultPermissionChecks() permissionChecks { + return permissionChecks{ + isElevatedProcess: windows.HasAdminRights, + isHyperVAdminMember: IsHyperVAdminsGroupMember, + vsockEntriesExist: vsock.CheckIfHVSockRegistryEntriesExist, + existingMachinesNum: h.countMachinesWithToolname, + } +} + func (h HyperVStubber) UserModeNetworkEnabled(_ *vmconfigs.MachineConfig) bool { return true } @@ -59,15 +75,17 @@ func (h HyperVStubber) CreateVM(_ define.CreateVMOpts, mc *vmconfigs.MachineConf // Allow creation in these two cases: // 1. if the user is Admin - // 2. if the user has Hyper-V admin rights and there is at least one *NEW* machine. + // 2. if the user has Hyper-V admin rights and a vsock registry entry exists // *NEW* machines are those created with the vsock entry having the ToolName field. // - // This is to prevent a non-admin user from creating the first machine - // which would require adding vsock entries into the Windows Registry. - if err := h.canCreate(); err != nil { + // This is to prevent to prevent an error for trying to create a vsock entry + // in the Windows registry if the user doesn't have the privileges. + if err := h.canCreate(len(mc.Mounts)); err != nil { // If it returns ErrHypervRegistryInitRequiresElevation and we're not already re-executing, // offer to elevate automatically if user is in admin group - if err == ErrHypervRegistryInitRequiresElevation && !windows.IsReExecuting() && windows.IsInAdministratorsGroup() { + if errors.Is(err, ErrHypervRegistryInitRequiresElevation) && + !windows.IsReExecuting() && + windows.IsInAdministratorsGroup() { message := fmt.Sprintf("%s.\n\n%s", ErrHypervPrepareHostForHyperV.Error(), windows.UACConfirmationPrompt) return launchElevate(message) } @@ -121,7 +139,7 @@ func (h HyperVStubber) CreateVM(_ define.CreateVMOpts, mc *vmconfigs.MachineConf if !windows.HasAdminRights() { return ErrHypervRegistryInitRequiresElevation } - networkHVSock, err = vsock.NewHVSockRegistryEntry(vsock.Network) + networkHVSock, err = vsock.NewHVSockRegistryEntry(vsock.Network, false) if err != nil { return err } @@ -210,7 +228,9 @@ func (h HyperVStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() err if err := h.canRemove(mc); err != nil { // If we get ErrHypervRegistryRemoveRequiresElevation and we're not already re-executing, // and the user has admin rights (is in admin group), offer to elevate automatically - if err == ErrHypervRegistryRemoveRequiresElevation && !windows.IsReExecuting() && windows.IsInAdministratorsGroup() { + if errors.Is(err, ErrHypervRegistryRemoveRequiresElevation) && + !windows.IsReExecuting() && + windows.IsInAdministratorsGroup() { message := "Removing this Hyper-V machine requires admin rights to clean up the Windows Registry.\n\n" + windows.UACConfirmationPrompt return nil, nil, launchElevate(message) @@ -250,24 +270,36 @@ func (h HyperVStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() err return []string{}, rmFunc, nil } -func (h HyperVStubber) canCreate() error { - // if admin, can always create - if windows.HasAdminRights() { +// canCreate checks if the user can create an Hyper-V machine. +// It returns `nil` if the user can create it. The conditions are: +// - the command is running as Administrator (elevated mode) +// OR +// - vsock entries in the Windows registry already exist and +// the user is a member of the Hyper-V Administrators group +// +// The `mounts` parameter is the number of Mounts of the machine. A +// specific Windows registry entry is necessary for every single mount. +// +// It returns `ErrHypervRegistryInitRequiresElevation` if the vsock +// entries in the Windows registry don't exist. +// +// It returns `ErrHypervUserNotInAdminGroup` if the user doesn't +// belong to the Hyper-V administrators group. +func (h HyperVStubber) canCreate(mounts int) error { + return checkCanCreate(h.defaultPermissionChecks(), mounts) +} + +func checkCanCreate(checks permissionChecks, mounts int) error { + if checks.isElevatedProcess() { return nil } - // not admin, check if there is at least one existing machine - // if so, user could create more machines just by being member of the hyperv admin group - machines, err := h.countMachinesWithToolname() - if err != nil { - return err - } - // no existing machines, require to be admin - if machines == 0 { + if !checks.vsockEntriesExist(mounts) { return ErrHypervRegistryInitRequiresElevation } - // at least 1 machine exists - // if user is member of the hyperv admin group, allow creation - return VerifyHyperVPermissions() + if !checks.isHyperVAdminMember() { + return ErrHypervUserNotInAdminGroup + } + return nil } // launchElevate attempts to automatically re-run the command as administrator @@ -303,31 +335,59 @@ func isLegacyMachine(mc *vmconfigs.MachineConfig) bool { return mc.HyperVHypervisor != nil && mc.HyperVHypervisor.ReadyVsock.MachineName != "" } +// canRemove checks if the user can remove an Hyper-V machine. +// It returns `nil` when the user can remove it. The conditions are: +// - the command is running as Administrator (elevated mode) +// OR +// - the machine isn't a legacy one (one that doesn't share the vsock) AND +// the user is a member of the Hyper-V Administrators group AND +// it's not the last machine OR the vsock entries have `KeepAfterMachineRemove=true` +// +// It returns `ErrHypervRegistryRemoveRequiresElevation` if the machine is legacy one OR +// if the vsock entry in the Windows registry doesn't have `KeepAfterMachineRemove=true`. +// +// It returns `ErrHypervUserNotInAdminGroup` if the user doesn't +// belogn to the Hyper-V administrators group. func (h HyperVStubber) canRemove(mc *vmconfigs.MachineConfig) error { - // if admin, can always remove - if windows.HasAdminRights() { + return checkCanRemove(mc, h.defaultPermissionChecks()) +} + +func checkCanRemove(mc *vmconfigs.MachineConfig, checks permissionChecks) error { + if checks.isElevatedProcess() { return nil } - - // if machine is legacy (machineName field), require admin rights to remove if isLegacyMachine(mc) { - return ErrHypervRegistryRemoveRequiresElevation + return ErrHypervLegacyMachineRequiresElevation } - - // not admin, check if there are multiple machines - // if so, user could remove the machine just by being member of the hyperv admin group - // (only the last machine removal requires Registry changes) - machines, err := h.countMachinesWithToolname() + if !checks.isHyperVAdminMember() { + return ErrHypervUserNotInAdminGroup + } + machines, err := checks.existingMachinesNum() if err != nil { return err } - // more than 1 machine exists, allow removal if user has hyperv admin rights - if machines > 1 { - return VerifyHyperVPermissions() + if canSkipVSockEntriesRemoval(machines, *mc.HyperVHypervisor) { + return nil } return ErrHypervRegistryRemoveRequiresElevation } +func canSkipVSockEntriesRemoval(machines int, hv vmconfigs.HyperVConfig) bool { + if machines > 1 { + return true + } + if !hv.NetworkVSock.KeepAfterMachineRemove || + !hv.ReadyVsock.KeepAfterMachineRemove { + return false + } + for _, m := range hv.FileserverVSocks { + if !m.KeepAfterMachineRemove { + return false + } + } + return true +} + // canStartOrStop checks if the machine can be started or stopped. // Legacy machines require admin rights to start or stop. func (h HyperVStubber) canStartOrStop(mc *vmconfigs.MachineConfig) error { @@ -630,7 +690,7 @@ func (h HyperVStubber) PrepareIgnition(mc *vmconfigs.MachineConfig, _ *ignition. } return nil, ErrHypervRegistryInitRequiresElevation } - readySock, err = vsock.NewHVSockRegistryEntry(vsock.Events) + readySock, err = vsock.NewHVSockRegistryEntry(vsock.Events, false) if err != nil { return nil, err } diff --git a/pkg/machine/hyperv/stubber_test.go b/pkg/machine/hyperv/stubber_test.go new file mode 100644 index 0000000000..2b91d29ca4 --- /dev/null +++ b/pkg/machine/hyperv/stubber_test.go @@ -0,0 +1,180 @@ +//go:build windows + +package hyperv + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.podman.io/podman/v6/pkg/machine/hyperv/vsock" + "go.podman.io/podman/v6/pkg/machine/vmconfigs" +) + +func TestCanCreate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + isElevated bool + vsockEntriesExist bool + isHyperVAdmin bool + mounts int + expectedErr error + }{ + { + name: "in elevated process can always create", + isElevated: true, + vsockEntriesExist: false, + isHyperVAdmin: false, + mounts: 2, + expectedErr: nil, + }, + { + name: "not elevated, no vsock entries", + isElevated: false, + vsockEntriesExist: false, + isHyperVAdmin: true, + expectedErr: ErrHypervRegistryInitRequiresElevation, + }, + { + name: "not elevated, vsock entries exist, not hyperv admin", + isElevated: false, + vsockEntriesExist: true, + isHyperVAdmin: false, + expectedErr: ErrHypervUserNotInAdminGroup, + }, + { + name: "not elevated, vsock entries exist, hyperv admin", + isElevated: false, + vsockEntriesExist: true, + isHyperVAdmin: true, + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + checks := permissionChecks{ + isElevatedProcess: func() bool { return tt.isElevated }, + isHyperVAdminMember: func() bool { return tt.isHyperVAdmin }, + vsockEntriesExist: func(int) bool { return tt.vsockEntriesExist }, + } + + err := checkCanCreate(checks, tt.mounts) + assert.ErrorIs(t, err, tt.expectedErr) + }) + } +} + +func TestCanRemove(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + isElevatedProcess bool + isHyperVAdminMember bool + isLegacyMachine bool + skipVsockEntriesRemoval bool + isLastMachine bool + expectedErr error + }{ + { + name: "elevated process can always remove", + isElevatedProcess: true, + isHyperVAdminMember: false, + isLegacyMachine: false, + skipVsockEntriesRemoval: false, + isLastMachine: true, + expectedErr: nil, + }, + { + name: "not elevated, legacy machine requires elevation", + isElevatedProcess: false, + isHyperVAdminMember: false, + isLegacyMachine: true, + skipVsockEntriesRemoval: false, + isLastMachine: true, + expectedErr: ErrHypervLegacyMachineRequiresElevation, + }, + { + name: "not elevated, not hyperv admins member", + isElevatedProcess: false, + isHyperVAdminMember: false, + isLegacyMachine: false, + skipVsockEntriesRemoval: false, + isLastMachine: true, + expectedErr: ErrHypervUserNotInAdminGroup, + }, + { + name: "not elevated, hyperv admins member, not last machine", + isElevatedProcess: false, + isHyperVAdminMember: true, + isLegacyMachine: false, + skipVsockEntriesRemoval: false, + isLastMachine: false, + expectedErr: nil, + }, + { + name: "not elevated, hyperv admins member, last machine, skip vsock entries removal", + isElevatedProcess: false, + isHyperVAdminMember: true, + isLegacyMachine: false, + skipVsockEntriesRemoval: true, + isLastMachine: true, + expectedErr: nil, + }, + { + name: "not elevated, hyperv admins member, last machine, don't skip vsock entries removal", + isElevatedProcess: false, + isHyperVAdminMember: true, + isLegacyMachine: false, + skipVsockEntriesRemoval: false, + isLastMachine: true, + expectedErr: ErrHypervRegistryRemoveRequiresElevation, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + checks := permissionChecks{ + isElevatedProcess: func() bool { return tt.isElevatedProcess }, + isHyperVAdminMember: func() bool { return tt.isHyperVAdminMember }, + existingMachinesNum: func() (int, error) { + if tt.isLastMachine { + return 1, nil + } else { + return 2, nil + } + }, + } + + vsockMachineName := "" + if tt.isLegacyMachine { + vsockMachineName = "podmanmachine" + } + + mc := &vmconfigs.MachineConfig{ + HyperVHypervisor: &vmconfigs.HyperVConfig{ + ReadyVsock: vsock.HVSockRegistryEntry{ + MachineName: vsockMachineName, + KeepAfterMachineRemove: tt.skipVsockEntriesRemoval, + }, + NetworkVSock: vsock.HVSockRegistryEntry{ + KeepAfterMachineRemove: tt.skipVsockEntriesRemoval, + }, + }, + } + + err := checkCanRemove(mc, checks) + if tt.expectedErr != nil { + assert.EqualError(t, err, tt.expectedErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/machine/hyperv/volumes.go b/pkg/machine/hyperv/volumes.go index a353e1cb39..bc3adb804e 100644 --- a/pkg/machine/hyperv/volumes.go +++ b/pkg/machine/hyperv/volumes.go @@ -66,13 +66,14 @@ func createShares(mc *vmconfigs.MachineConfig) (err error) { } return ErrHypervRegistryUpdateRequiresElevation } - testVsock, err = vsock.NewHVSockRegistryEntry(vsock.Fileserver) + testVsock, err = vsock.NewHVSockRegistryEntry(vsock.Fileserver, false) if err != nil { return err } } mount.VSockNumber = &testVsock.Port + mc.HyperVHypervisor.FileserverVSocks = append(mc.HyperVHypervisor.FileserverVSocks, *testVsock) logrus.Debugf("Going to share directory %s via 9p on vsock %d", mount.Source, testVsock.Port) } return nil diff --git a/pkg/machine/hyperv/vsock/vsock.go b/pkg/machine/hyperv/vsock/vsock.go index cec3300562..7b9f085b34 100644 --- a/pkg/machine/hyperv/vsock/vsock.go +++ b/pkg/machine/hyperv/vsock/vsock.go @@ -27,6 +27,11 @@ const ( PodmanToolName = "podman" // HvsockPurpose is the string identifier for the sock purpose in a registry entry HvsockPurpose = "Purpose" + // HvsockKeepAfterMachineRemove is a flag to avoid that the entry gets deleted when + // the last machine gets deleted. This is important for use cases where an Administrator + // wants to pre-create the registry key so that the user can create and delete machines + // without requiring admin privileges. + HvsockKeepAfterMachineRemove = "KeepAfterMachineRemove" // VsockRegistryPath describes the registry path to where the hvsock registry entries live VsockRegistryPath = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices` // LinuxVM is the default guid for a Linux VM on Windows @@ -45,7 +50,7 @@ const ( Fileserver ) -func (hv HVSockPurpose) string() string { +func (hv HVSockPurpose) String() string { switch hv { case Network: return "Network" @@ -58,7 +63,7 @@ func (hv HVSockPurpose) string() string { } func (hv HVSockPurpose) Equal(purpose string) bool { - return hv.string() == purpose + return hv.String() == purpose } func toHVSockPurpose(p string) (HVSockPurpose, error) { @@ -90,8 +95,25 @@ type HVSockRegistryEntry struct { // ToolName identifies the application that created this registry entry (e.g., Podman). // This provides information about the entry's origin and can be used for filter entries // if purpose is not enough. - ToolName string `json:"creator_tool,omitempty"` - Key registry.Key `json:"key,omitempty"` + ToolName string `json:"creator_tool,omitempty"` + + // KeepAfterMachineRemove is set to true if the registry entry should not be deleted + // when the user deletes or reset the Podman machines. + // This is useful to let unprivileged users create and delete machines without + // requiring privileges to edit the registry. + KeepAfterMachineRemove bool `json:"keep_after_machine_remove,omitempty"` +} + +func (hv *HVSockRegistryEntry) String() string { + keepStatus := "" + if hv.KeepAfterMachineRemove { + keepStatus = " (persistent)" + } + return fmt.Sprintf("- Key: %s\n Purpose: %s\n Port: %d%s\n", + hv.KeyName, + hv.Purpose, + hv.Port, + keepStatus) } // Add creates a new Windows registry entry with string values from the @@ -105,31 +127,41 @@ func (hv *HVSockRegistryEntry) Add() error { return err } if exists { - return fmt.Errorf("%q: %s", ErrVSockRegistryEntryExists, hv.KeyName) + return fmt.Errorf("%w: %s", ErrVSockRegistryEntryExists, hv.KeyName) } parentKey, err := registry.OpenKey(registry.LOCAL_MACHINE, VsockRegistryPath, registry.QUERY_VALUE) + if err != nil { + return err + } defer func() { if err := parentKey.Close(); err != nil { logrus.Error(err) } }() + newKey, _, err := registry.CreateKey(parentKey, hv.KeyName, registry.WRITE) if err != nil { return err } - newKey, _, err := registry.CreateKey(parentKey, hv.KeyName, registry.WRITE) defer func() { if err := newKey.Close(); err != nil { logrus.Error(err) } }() - if err != nil { - return err - } - if err := newKey.SetStringValue(HvsockPurpose, hv.Purpose.string()); err != nil { + if err := newKey.SetStringValue(HvsockPurpose, hv.Purpose.String()); err != nil { return err } - return newKey.SetStringValue(HvsockToolName, hv.ToolName) + if err := newKey.SetStringValue(HvsockToolName, hv.ToolName); err != nil { + return err + } + // Save KeepAfterMachineRemove flag if set + if hv.KeepAfterMachineRemove { + var value uint32 = 1 + if err := newKey.SetDWordValue(HvsockKeepAfterMachineRemove, value); err != nil { + return err + } + } + return nil } // Remove deletes the registry key and its string values @@ -145,7 +177,7 @@ func (hv *HVSockRegistryEntry) validate() error { if hv.Port < 1 { return errors.New("port must be larger than 1") } - if len(hv.Purpose.string()) < 1 { + if len(hv.Purpose.String()) < 1 { return errors.New("required field purpose is empty") } if len(hv.ToolName) < 1 { @@ -170,9 +202,9 @@ func (hv *HVSockRegistryEntry) exists() (bool, error) { return false, err } -// findOpenHVSockPort looks for an open random port. it verifies the port is not +// FindOpenHVSockPort looks for an open random port. it verifies the port is not // already being used by another hvsock in the Windows registry. -func findOpenHVSockPort() (uint64, error) { +func FindOpenHVSockPort() (uint64, error) { // If we cannot find a free port in 10 attempts, something is wrong for range 10 { port, err := utils.GetRandomPort() @@ -190,29 +222,27 @@ func findOpenHVSockPort() (uint64, error) { // the port is good to go return uint64(port), nil } - if err != nil { - // something went wrong - return 0, err - } + return 0, err } return 0, errors.New("unable to find a free port for hvsock use") } // NewHVSockRegistryEntry is a constructor to make a new registry entry in Windows. After making the new // object, you must call the add() method to *actually* add it to the Windows registry. -func NewHVSockRegistryEntry(purpose HVSockPurpose) (*HVSockRegistryEntry, error) { +func NewHVSockRegistryEntry(purpose HVSockPurpose, keep bool) (*HVSockRegistryEntry, error) { // a so-called wildcard entry ... everything from FACB -> 6D3 is MS special sauce // for a " linux vm". this first segment is hexi for the hvsock port number // 00000400-FACB-11E6-BD58-64006A7986D3 - port, err := findOpenHVSockPort() + port, err := FindOpenHVSockPort() if err != nil { return nil, err } r := HVSockRegistryEntry{ - KeyName: portToKeyName(port), - Purpose: purpose, - Port: port, - ToolName: PodmanToolName, + KeyName: PortToKeyName(port), + Purpose: purpose, + Port: port, + ToolName: PodmanToolName, + KeepAfterMachineRemove: keep, } if err := r.Add(); err != nil { return nil, err @@ -220,7 +250,7 @@ func NewHVSockRegistryEntry(purpose HVSockPurpose) (*HVSockRegistryEntry, error) return &r, nil } -func portToKeyName(port uint64) string { +func PortToKeyName(port uint64) string { // this could be flattened but given the complexity, I thought it might // be more difficult to read hexi := strings.ToUpper(fmt.Sprintf("%08x", port)) @@ -228,12 +258,17 @@ func portToKeyName(port uint64) string { } func LoadHVSockRegistryEntry(port uint64) (*HVSockRegistryEntry, error) { - keyName := portToKeyName(port) + keyName := PortToKeyName(port) fqPath := fmt.Sprintf("%s\\%s", VsockRegistryPath, keyName) k, err := openVSockRegistryEntry(fqPath) if err != nil { return nil, err } + defer func(key registry.Key) { + if err := key.Close(); err != nil { + logrus.Errorf("failed to close registry key: %v", err) + } + }(k) p, _, err := k.GetStringValue(HvsockPurpose) if err != nil { return nil, err @@ -253,7 +288,6 @@ func LoadHVSockRegistryEntry(port uint64) (*HVSockRegistryEntry, error) { KeyName: keyName, Purpose: purpose, Port: port, - Key: k, MachineName: m, }, nil } @@ -290,8 +324,6 @@ func (hv *HVSockRegistryEntry) ListenSetupWait() (func() error, io.Closer, error // loadAllHVSockRegistryEntries loads HVSock registry entries, filtered by purpose and optionally limited by size. // If limit is -1, it returns all matching entries. Otherwise, it returns up to 'limit' entries. -// The caller is responsible for closing the registry.Key in each returned HVSockRegistryEntry. -// Non-matching or excess keys are closed within this function. func loadHVSockRegistryEntries(purpose HVSockPurpose, limit int) ([]*HVSockRegistryEntry, error) { parentKey, err := registry.OpenKey(registry.LOCAL_MACHINE, VsockRegistryPath, registry.ENUMERATE_SUB_KEYS) if err != nil { @@ -322,22 +354,29 @@ func loadHVSockRegistryEntries(purpose HVSockPurpose, limit int) ([]*HVSockRegis logrus.Debugf("Could not open registry entry %s: %v", fqPath, err) continue } + defer func(key registry.Key) { + if err := key.Close(); err != nil { + logrus.Errorf("failed to close registry key: %v", err) + } + }(k) p, _, err := k.GetStringValue(HvsockPurpose) if err != nil { logrus.Debugf("Could not read purpose from registry entry %s: %v", fqPath, err) - k.Close() continue } toolName, _, err := k.GetStringValue(HvsockToolName) if err != nil { logrus.Debugf("Could not read tool name from registry entry %s: %v", fqPath, err) - k.Close() continue } - k.Close() + keep, _, err := k.GetIntegerValue(HvsockKeepAfterMachineRemove) + keepBool := false + if err == nil { + keepBool = keep != 0 + } entryPurpose, err := toHVSockPurpose(p) if err != nil { @@ -345,7 +384,7 @@ func loadHVSockRegistryEntries(purpose HVSockPurpose, limit int) ([]*HVSockRegis continue } - if !entryPurpose.Equal(purpose.string()) { + if !entryPurpose.Equal(purpose.String()) { continue } @@ -367,24 +406,41 @@ func loadHVSockRegistryEntries(purpose HVSockPurpose, limit int) ([]*HVSockRegis } allEntries = append(allEntries, &HVSockRegistryEntry{ - KeyName: subKeyName, - Purpose: entryPurpose, - Port: port, - Key: k, - ToolName: PodmanToolName, + KeyName: subKeyName, + Purpose: entryPurpose, + Port: port, + ToolName: PodmanToolName, + KeepAfterMachineRemove: keepBool, }) } return allEntries, nil } +func CheckIfHVSockRegistryEntriesExist(mountsNum int) bool { + // The number or required HVSock registry entries + // depends on the purpose + requiredEntries := map[HVSockPurpose]int{ + Network: 1, + Events: 1, + Fileserver: mountsNum, + } + for p, i := range requiredEntries { + entries, err := loadHVSockRegistryEntries(p, i) + if len(entries) < i || err != nil { + return false + } + } + return true +} + func LoadHVSockRegistryEntryByPurpose(purpose HVSockPurpose) (*HVSockRegistryEntry, error) { entries, err := loadHVSockRegistryEntries(purpose, 1) if err != nil { return nil, err } if len(entries) != 1 { - return nil, fmt.Errorf("no hvsock registry entry found for purpose: %s", purpose.string()) + return nil, fmt.Errorf("no hvsock registry entry found for purpose: %s", purpose.String()) } return entries[0], nil @@ -429,6 +485,10 @@ func RemoveAllHVSockRegistryEntries() error { var removalErrors []error for _, sock := range allSocks { + // Don't remove registry entries if KeepAfterMachineRemove is set to true + if sock.KeepAfterMachineRemove { + continue + } if err := sock.Remove(); err != nil { logrus.Errorf("unable to remove registry entry for %s: %q", sock.KeyName, err) removalErrors = append(removalErrors, fmt.Errorf("failed to remove sock %s: %w", sock.KeyName, err)) diff --git a/pkg/machine/vmconfigs/config_windows.go b/pkg/machine/vmconfigs/config_windows.go index 163d4bcb97..a287835156 100644 --- a/pkg/machine/vmconfigs/config_windows.go +++ b/pkg/machine/vmconfigs/config_windows.go @@ -12,6 +12,8 @@ type HyperVConfig struct { ReadyVsock vsock.HVSockRegistryEntry // NetworkVSock is for the user networking NetworkVSock vsock.HVSockRegistryEntry + // FileserverVSocks are for machine mounts (one entry per mount) + FileserverVSocks []vsock.HVSockRegistryEntry } type WSLConfig struct { diff --git a/test/e2e/common_windows_test.go b/test/e2e/common_windows_test.go new file mode 100644 index 0000000000..9de67d0e52 --- /dev/null +++ b/test/e2e/common_windows_test.go @@ -0,0 +1,86 @@ +//go:build windows + +package integration + +import ( + "os" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.podman.io/podman/v6/pkg/machine/windows" + . "go.podman.io/podman/v6/test/utils" +) + +type PodmanTestIntegration struct { + PodmanTest + TempDir string +} + +var podmanTest *PodmanTestIntegration + +type PodmanSessionIntegration struct { + *PodmanSession +} + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "E2E Suite - Windows") +} + +var _ = BeforeEach(func() { + tempDir, err := os.MkdirTemp("", "podman_test") + Expect(err).ToNot(HaveOccurred()) + podmanTest = PodmanTestCreate(tempDir) +}) + +var _ = AfterEach(func() { + if podmanTest != nil && podmanTest.TempDir != "" { + os.RemoveAll(podmanTest.TempDir) + } +}) + +func PodmanTestCreate(tempDir string) *PodmanTestIntegration { + cwd, _ := os.Getwd() + podmanBinary := filepath.Join(cwd, "../../bin/windows/podman.exe") + if envBinary := os.Getenv("PODMAN_BINARY"); envBinary != "" { + podmanBinary = envBinary + } + + p := &PodmanTestIntegration{ + PodmanTest: PodmanTest{ + PodmanBinary: podmanBinary, + }, + TempDir: tempDir, + } + p.PodmanMakeOptions = p.makeOptions + return p +} + +func skipIfNotAdmin(reason string) { + if !windows.HasAdminRights() { + Skip(reason) + } +} + +func (p *PodmanTestIntegration) makeOptions(args []string, _ PodmanExecOptions) []string { + return args +} + +func (p *PodmanTestIntegration) Podman(args []string) *PodmanSessionIntegration { + podmanSession := p.PodmanExecBaseWithOptions(args, PodmanExecOptions{}) + return &PodmanSessionIntegration{podmanSession} +} + +func (p *PodmanTestIntegration) PodmanExitCleanly(args ...string) *PodmanSessionIntegration { + GinkgoHelper() + session := p.Podman(args) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + return session +} diff --git a/test/e2e/system_hyperv_prep_test.go b/test/e2e/system_hyperv_prep_test.go new file mode 100644 index 0000000000..a03f5a6106 --- /dev/null +++ b/test/e2e/system_hyperv_prep_test.go @@ -0,0 +1,96 @@ +//go:build windows + +package integration + +import ( + "os/exec" + "os/user" + "strings" + + "go.podman.io/podman/v6/pkg/machine/hyperv/vsock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "go.podman.io/podman/v6/test/utils" +) + +var _ = Describe("podman system hyperv-prep", func() { + It("rejects incompatible flags together", func() { + session := podmanTest.Podman([]string{"system", "hyperv-prep", "--status", "--reset"}) + session.WaitWithDefaultTimeout() + Expect(session).To(ExitWithError(125, "none of the others can be")) + session = podmanTest.Podman([]string{"system", "hyperv-prep", "--status", "--mounts", "1"}) + session.WaitWithDefaultTimeout() + Expect(session).To(ExitWithError(125, "none of the others can be")) + session = podmanTest.Podman([]string{"system", "hyperv-prep", "--reset", "--mounts", "1"}) + session.WaitWithDefaultTimeout() + Expect(session).To(ExitWithError(125, "none of the others can be")) + }) + + It("creates registry entries and resets them", func() { + skipIfNotAdmin("test requires an elevated (admin) terminal") + + // Preconditions: no existing registry entries and user not in Hyper-V Administrators group + Expect(vsock.CheckIfHVSockRegistryEntriesExist(1)).To(BeFalse(), + "vsock registry entries already exist, cannot run test") + Expect(isCurrentUserHyperVAdmin()).To(BeFalse(), + "user is already a member of the Hyper-V Administrators group, cannot run test") + + // Run `--status` a first time to check that it reports correctly that: + // - No vsock registry keys exist + // - User isn't a member of the Hyper-V Administrators group + session := podmanTest.Podman([]string{"system", "hyperv-prep", "--status"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToString()).To(ContainSubstring("No vsock registry entries found")) + Expect(session.OutputToString()).To(ContainSubstring("Current user is NOT a member")) + + // Run hyperv-prep to create registry entries + session = podmanTest.Podman([]string{"system", "hyperv-prep"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToString()).To(ContainSubstring("Network")) + Expect(session.OutputToString()).To(ContainSubstring("Events")) + Expect(session.OutputToString()).To(ContainSubstring("Fileserver")) + + // Check that running hyperv-prep worked: + // - The registry entries for vsock have been created (for 2 mounts) + // - The user is a member of the Hyper-V Administrators group + Expect(vsock.CheckIfHVSockRegistryEntriesExist(2)).To(BeTrue(), + "vsock registry entries don't exist after running hyperv-prep") + Expect(isCurrentUserHyperVAdmin()).To(BeTrue(), + "user isn't a member of the Hyper-V Administrators group after running hyperv-prep") + + // Run --status a second time to shows the created entries + session = podmanTest.Podman([]string{"system", "hyperv-prep", "--status"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToString()).To(ContainSubstring("Network")) + Expect(session.OutputToString()).To(ContainSubstring("Events")) + Expect(session.OutputToString()).To(ContainSubstring("Fileserver")) + + // Reset the entries to clean up + session = podmanTest.Podman([]string{"system", "hyperv-prep", "--reset", "--force"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToString()).To(ContainSubstring("Successfully removed")) + }) +}) + +func isCurrentUserHyperVAdmin() bool { + u, err := user.Current() + if err != nil { + return false + } + out, err := exec.Command("powershell", "-NoProfile", "-Command", + `Get-LocalGroupMember -Name "Hyper-V Administrators"`).Output() + if err != nil { + return false + } + for _, line := range strings.Split(string(out), "\n") { + if strings.Contains(line, u.Username) { + return true + } + } + return false +} diff --git a/winmake.ps1 b/winmake.ps1 index ae3b7467db..c0820b7c9b 100644 --- a/winmake.ps1 +++ b/winmake.ps1 @@ -78,6 +78,15 @@ function Local-Machine { param ( [string]$files ); + Ginkgo-Run -files $files -testPath 'pkg/machine/e2e/.' -defaultTimeout '50m' +} + +function Ginkgo-Run { + param ( + [string]$files, + [string]$testPath = 'test/e2e/.', + [string]$defaultTimeout = '5m' + ); Build-Ginkgo if ($files) { $files = "--focus-file ""$files""" @@ -89,8 +98,8 @@ function Local-Machine { $focus = "--focus ""$FOCUS"" --silence-skips" } - if ($null -eq $ENV:GINKGOTIMEOUT) { $ENV:GINKGOTIMEOUT = '--timeout=50m' } - Run-Command "./bin/ginkgo.exe -vv --tags `"$remotetags`" ${ENV:GINKGOTIMEOUT} --trace --no-color $focus $files pkg/machine/e2e/." + if ($null -eq $ENV:GINKGOTIMEOUT) { $ENV:GINKGOTIMEOUT = "--timeout=$defaultTimeout" } + Run-Command "./bin/ginkgo.exe -vv --tags `"$remotetags`" ${ENV:GINKGOTIMEOUT} --trace --no-color $focus $files $testPath" } # Expect starting directory to be /podman @@ -379,6 +388,12 @@ switch ($target) { } Local-Machine -files $files } + 'ginkgo-run' { + if ($params.Count -gt 1) { + $files = $params[1] + } + Ginkgo-Run -files $files + } 'clean' { Make-Clean } @@ -431,6 +446,12 @@ switch ($target) { Write-Host 'Example: Run specific machine tests ' Write-Host ' .\winmake localmachine 'basic_test.go"" Write-Host + Write-Host 'Example: Run all e2e tests ' + Write-Host ' .\winmake ginkgo-run' + Write-Host + Write-Host 'Example: Run specific e2e tests ' + Write-Host ' .\winmake ginkgo-run "system_hyperv_prep_test.go"' + Write-Host Write-Host 'Example: Download win-gvproxy and win-sshproxy helpers' Write-Host ' .\winmake win-gvproxy' Write-Host