mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-10-27 00:07:38 +00:00
Merge pull request #28522 from l0rd/avoid-regkey-creation
Allow skipping Windows registry key operations for first Hyper-V `machine init` and for last Hyper-V `machine rm`
This commit is contained in:
commit
1cae4a9d63
16 changed files with 1008 additions and 86 deletions
2
Makefile
2
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 \
|
||||
|
|
|
|||
267
cmd/podman/system/hyperv_prep_windows.go
Normal file
267
cmd/podman/system/hyperv_prep_windows.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
71
docs/source/markdown/podman-system-hyperv-prep.1.md
Normal file
71
docs/source/markdown/podman-system-hyperv-prep.1.md
Normal file
|
|
@ -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
|
||||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
----------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
180
pkg/machine/hyperv/stubber_test.go
Normal file
180
pkg/machine/hyperv/stubber_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
86
test/e2e/common_windows_test.go
Normal file
86
test/e2e/common_windows_test.go
Normal file
|
|
@ -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
|
||||
}
|
||||
96
test/e2e/system_hyperv_prep_test.go
Normal file
96
test/e2e/system_hyperv_prep_test.go
Normal file
|
|
@ -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
|
||||
}
|
||||
25
winmake.ps1
25
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue