The Ultimate Guide to Hyper-V Remote Management with PowerShell

The most common complaints that I see regarding Hyper-V are somehow related to management. When it comes to GUI tools, our options are all just barely on the high side of awful. Hyper-V Manager is capable, but lacking in scope for anything more than a handful of hosts, and it’s version is locked to its Windows version. Failover Cluster Manager can fill in many of the gaps missing from Hyper-V Manager, but its focus is failover clusters, not Hyper-V, and it is also locked to its Windows version. System Center Virtual Machine Manager’s sole redeeming quality is that it is not named “Microsoft Bob” — a fact which does not help VMM users in any way, but for which Microsoft Bob will be eternally grateful.

The Windows Azure Pack is too complicated for most regular administrators to have time to learn how to use, with a feature breadth to match. Azure Stack will require specialty hardware with more horsepower necessary to run itself than half of the world’s businesses need to run their entire operation, and it’s not yet released for production use anyway. Most egregiously, I cannot use any one of these tools by itself even for a typical day of Hyper-V Management.

What we do have available to us is PowerShell. Whereas Hyper-V is embarrassingly lacking in GUI management options, it is the undisputed king in command-line control. The more that I use PowerShell with Hyper-V, the better I feel about Microsoft’s virtualization stack overall. No matter how powerful a GUI is, it locks you into the mindset of the person(s) that developed it. With a well-designed command-line interface, you can routinely push a product to do things that even its engineers didn’t even conceive. So, while I can certainly agree that there is great value in a well-designed GUI and will continue to fight alongside everyone else attempting to push Microsoft toward doing the right thing, PowerShell is where serious administrators go to do serious work.

Most of us started out using PowerShell on our local systems. That’s just the beginner’s level. PowerShell works just as well against a remote system as it does locally. It is truly masterful at controlling multiple systems from a single console. What I like best about this ability is that using PowerShell to control remote machines is so simple that most people don’t even realize that the technology is involved enough to have its own name: PowerShell Remoting. There is quite a lot going on behind the scenes that we never see or need to be involved with.

Benefits of PowerShell Remoting

There are several positives about PowerShell Remoting:

  • Inherently secure
    • Source and remote machines validate each other as well as the initiating user
    • Traffic is always encrypted
    • Uses a single well-known port (5985 for standard PSRemoting, 5986 for SSL PSRemoting)
    • Custom endpoints can be created to limit what any given remote user has access to
  • Any supported version of Windows can use PowerShell Remoting on any other supported version of Windows
  • PowerShell is not locked to its Windows version
  • PowerShell Remoting works perfectly well when your local machine does not have a required PowerShell module but the target machine does
  • Many PowerShell modules work perfectly well when your local machine does have a required PowerShell module but the target machine does not
  • PowerShell Remoting allows for one machine to command several remote computers simultaneously (one-to-many)

PowerShell Remoting is Secure

I’m fortunate enough to work with a security department that is both practical and well-studied, so I’ve never had to make the case for PowerShell Remoting. From what I’ve read of the experiences of others, I am apparently in a minority. If you need to convince someone else, here’s all of the resources that you’ll ever need. If I had to pick a favorite part of that article, it would be the bit that points out that sites that avoid PowerShell Remoting over security concerns will joyfully leave open a number of other attack vectors that are far easier to exploit. These are usually the same sites that run Hyper-V in workgroup mode.

What’s not mentioned in the article that really helps change a lot of people’s minds is that PowerShell Remoting is built on top of WSMan, which is a non-Microsoft technology. Even though Microsoft has made unbelievable progress in making security into a top priority in their products, they have not done nearly so well at repairing their poor reputation (how’s that Secure Boot thing coming along?). So, if you’re facing resistance simply due to Microsoft being the owner of PowerShell, it might help to point out that the “W” in “WSMan” is for “web”, not “Windows”. WSMan is a standardized, vendor-neutral transport and management mechanism governed by the Distributed Management Task Force.

PowerShell Enables Any Version of Windows to Remotely Manage Any Other Version of Windows (and Hyper-V)

A very common complaint, and sometimes an outright problem, is that Hyper-V Manager can only fully control versions of Hyper-V that are running on the same code base. Hyper-V Manager in Windows 7 can’t control anything after the version of Hyper-V that released with the Windows 7/Windows Server 2008 R2 code base. Windows 8 or later was required. Starting in Windows 8/Server 2012, Hyper-V Manager can usually manage down-level hosts, but some people have troubles even with that.

With PowerShell, there’s no problem. PowerShell Remoting was introduced in PowerShell 2.0, and since then, PowerShell Remoting has worked perfectly well both up-level and down-level. The following is a screenshot of a Windows 7 installation with native PowerShell 2.0 remotely controlling a Hyper-V 2012 R2 server with native PowerShell 4.0:

PSRemoting Different Versions

PSRemoting Different Versions

PowerShell is Not Locked to Its Windows Version

A great deal of pain has been created by the locking of Remote Server Administration Tools (which is where we find Hyper-V Manager and Failover Cluster Manager) to a specific version of Windows. That locking is the reason that Windows 7 can’t manage anything after Hyper-V Server 2008 R2. Windows 10 has reduced the pain somewhat, but we still had to suffer through several years where some version of Windows 8 was necessary if we wanted a dedicated management workstation to manage the newer releases of Hyper-V.

PowerShell has never had that problem. Just go to Microsoft’s download page and search for Windows Management Framework. The version of the Management Framework indicates the version of PowerShell that it will install. Upgrade your clients as desired. I am somewhat more hesitant to recommend that Server systems be updated, but it is usually OK. I know that upgrading PowerShell caused a fair amount of havoc on Exchange systems in the past, so Exchange admins need to do a bit of research first.

PowerShell Modules Can Be Different Between Client and Server

If your target system has a module that you need, you can open a remote PowerShell session to it and call it almost as if it were local. A remote module can’t run against your local system, though. I will demonstrate that later in this article.

If your remote system has a module that you want to operate against the target, you might be able to use it. The module must support implicit remoting. Check your cmdlets for a ComputerName parameter. If it’s there and the module follows the standard, then it will work.

PowerShell Remoting Allows for High-Powered One-to-Many Control

All of the GUI tools mentioned in the beginning of this article can display several hosts and/or several virtual machines simultaneously, but their ability to manage several hosts and/or several virtual machines simultaneously is limited to basic features. If I want to shut down several virtual machines at once, that’s easy for all of the GUI tools. If I want to know if the Hyper-V Integration Services are up-to-date in many virtual machines, only PowerShell can help me find out in a timely fashion.

How to Enable PowerShell Remoting for Hyper-V

Both the local and remote systems must be set up properly for PowerShell Remoting to work. The first thing that you must do on both sides is:

Enable-PSRemoting -Force

On non-domain-joined systems, I received an “Access Denied” error unless I used the real Administrator account; just using an account in the local administrators group wasn’t enough. This seems to be at odds with the normal Windows authentication model and was just plain annoying for Windows 7, which disables the Administrator account by default. You can try the SkipProfileCheck parameter… it might help.

Security Configuration for PowerShell Remoting

Configuring security on your systems for PowerShell Remoting is either ridiculously simple or horrifically complicated. The differences lie in the fact that the user and both of the machines are authenticated. If the source and the target computer are in the same domain or domains with the proper trust relationship, Active Directory will transparently handle machine authentication. Domain membership should always be your first choice. If you’re covered there and you are not interested in further locking it down with SSL, just skip on to the next section.

If the local and remote computers cannot be validated using Active Directory, typically because one or both are in a workgroup, you have two choices. The preferred choice is to use SSL certificates so that the machines can trust each other. The second option is by adding names to the remote system’s TrustedHosts list so that it can be compromised by remote attackers . If you want to do the latter, that’s all on you to research and carry out; I will no longer have any part in it.

Configuring PowerShell Remoting with SSL

If you’ve already got a PKI (public key infrastructure) in place for your domain or you have third-party SSL certificates, this is relatively easy. The simplified directions (meaning, I expect that if you want to use PKI to solve this problem that you already know how to use PKI) are:

  1. Configure each host to be connected to with a certificate that has the Server Authentication usage. For most domains, this is just the regular Computer certificate template. This can be challenging if the remote host isn’t in a domain at all, as is the case with a perimeter (DMZ) Hyper-V host. The process that you must go through is not intuitive. Fortunately, Ricky Gao has documented one possible method for us. Essentially, you have to use a server running IIS within your domain to request the certificate using the friendly name used by the perimeter host. You then import that certificate into the perimeter host’s Personal store, the domain CA’s certificate into the Trusted Root Certification Authorities store, and import the domain’s CRL. It’s a good idea to then go back and delete the certificate from the IIS server. That action won’t cause the CA to revoke the certificate and it prevents accidental impersonation attempts.
  2. On each remote host with a certificate, run winrm qc -transport:https
  3. On each remote host, open TCP port 5986 in the firewall.
  4. If the source system isn’t in the same domain as the CA that issued the certificate in step 1, import that domain’s CA’s certificate into the Trusted Root Certification Authorities store, and import the domain’s CRL.
  5. In order to use PSRemoting, include the -UseSSL parameter, ex: Enter-PSSession -ComputerName hypervindmz -Credential (Get-Credential) -UseSSL

I gave much more detailed instructions in the Hyper-V Security book that I co-authored with Andy Syrewicze. I’m not linking it here because I’m trying to get your money (seriously, I make very little money from my books) but because it was much harder for me to research and write those directions than it would be for anyone to follow them and I don’t want to go through that again. Oh, and plagiarizing yourself is not only a thing, it is also an immoral thing that your publisher can sue you over.

It is critical to understand that the usage of SSL does NOT meaningfully improve the security of PSRemoting. WSMan communications are already encrypted and the same system performing WSMan encryption performs the SSL encryption. The purpose here is to allow the SSL certificate of the host to positively identify itself to guests that can’t use Kerberos authentication. If you or your security team need to verify that, then I recommend using Wireshark or something similar to inspect the packets for yourself and make your own decision.

Standard PowerShell Remoting/WSMan operates on port 5585. Encrypted PowerShell Remoting/WSMan operates on port 5586.

Installing the PowerShell Hyper-V Module

Not surprisingly, the easiest way to install the Hyper-V PowerShell module is with PowerShell. This works on Windows 8 and later, Windows Server 2012 and later, and Hyper-V Server 2012 and later:

Install-WindowsFeature Hyper-V-PowerShell

If you’d like to install Hyper-V Manager along with the PowerShell module:

Install-WindowsFeature RSAT-Hyper-V-Tools

If you’d like to install both of these tools along with Hyper-V:

Install-WindowsFeature Hyper-V -IncludeManagementTools

If you’d rather take the long way through the GUI for some reason, your approach depends on whether or not you’re using a desktop or a server operating system.

For a desktop, open Turn windows features on or off via the Control Panel. Open the Hyper-V tree, then the Hyper-V Management Tools subtree, and check Hyper-V Module for Windows PowerShell (along with anything else that you’d like).

Hyper-V PowerShell Module on Windows

Hyper-V PowerShell Module on Windows

For a Server operating system, start in Server Manager. Click Add roles and features. Click through all of the screens in the wizard until you reach the Features page. Expand Remote Server Administration Tools, then Hyper-V Management Tools, and check Hyper-V Module for Windows PowerShell (along with anything else that you’d like).

Server Hyper-V PowerShell Module

Server Hyper-V PowerShell Module

Once the module is installed, you can use it immediately without rebooting. You might need to import it if you’ve already got an open PowerShell session, or you could just start a new session.

Implicit PowerShell Remoting in the Hyper-V Module

The easiest way to start using PowerShell Remoting is with implicit remoting. As a general rule, cmdlets with a ComputerName parameter are making use of implicit remoting. What that means is that all of the typing is done on your local machine, but all of the action occurs on the remote machine. Everything that the Hyper-V module does is in WMI, so this means that all of the commands that you type are being sent to the VMMS service on the target host to perform. If you are carrying this out interactively, the results are then returned to your console in serialized form.

To make use of implicit remoting with the Hyper-V module, you must have it installed on your local computer. There are more limitations on implicit remoting:

  • You can only (legally) install the Hyper-V PowerShell module that matches your local Windows version
  • You are limited to the functionality exposed by your local Hyper-V PowerShell module
  • No matter the local version, it cannot be used to manage 2008 R2 or lower target hosts
  • Implicit remoting doesn’t always work if the local and remote Hyper-V versions are different

For example, I cannot install the Hyper-V PowerShell module at all on Windows 7. As another example, the Hyper-V PowerShell module in Windows 10 cannot control a 2012 R2 environment. Basically, the Hyper-V PowerShell module on your local system follows similar rules as Hyper-V Manager on your local system. I want to reiterate that these rules only apply to implicit remoting; you can still explicitly operate any cmdlet in any module that exists on the target.

For example, from my Windows 10 desktop, I run Get-VM -CompterName svhv01 against my Windows Server 2016 TP5 host:

Implicit Remote Get-VM

Implicit Remote Get-VM

Behind the scenes, it is directing WMI on SVHV01 to run “SELECT * FROM Msvm_ComputerSystem” against its “rootvirtualizationv2” namespace. It is then processing the results through a local view filter.

Implicit Remoting Against Several Hosts

I was saying how PowerShell Remoting can be used against multiple machines at once. Any time that a cmdlet’s ComputerName parameter supports a string array, you can operate it against multiple machines. Run Get-Help against the cmdlet in question and check to see if the ComputerName parameter has square brackets:

ComputerName with Array Support

ComputerName with Array Support

As you can see, Get-VM has square brackets in its ComputerName parameter (the [<String[]>]] part), so it can accept multiple hosts. Example:

Get-VM -ComputerName svhv01, svhv02

Locking Implicit Remoting to a Specific Host

I have a complex environment with several hosts, so I am content to always specify the -ComputerName parameter as necessary. If you’ve only got a single host, then you might like to avoid typing that ComputerName parameter each time. To do that, open up your PowerShell profile and enter the following:

Get-Command –Module Hyper-V –Verb Get | foreach {

Just replace TARGETHOSTNAME with the name or IP address of your remote host. From that point onward, any time you open any PowerShell prompt using the modified profile, all cmdlets from the Hyper-V module that start with “Get” will be automatically injected with “-ComputerName TARGETHOSTNAME”.

Implicit Remoting and the Pipeline

It can take some time and practice to become accustomed to how the pipeline works with implicit remoting, not least of which because some cmdlets behave differently. As a general rule, the pipeline brings items back to your computer.

So, let’s say you did this:

Get-VM -ComputerName | Export-CSV -Path d:tempsvhv02VMList.csv -NoTypeInformation

Where do you expect to find the file? On the remote host or the local host?

Implicit Remoting with a Pipeline

Implicit Remoting with a Pipeline

The file was created on my local system (if you looked closely at my screenshot and it made you curious, the file is zero length because there are no VMs on that system, not because it failed).

So, what this behavior means to you is that if you choose to then carry out operations on the objects such as Set cmdlets, you might need to use implicit remoting again after a pipeline… but then again, you might not. Which of the following do you think is correct?

  1. Get-VM -ComputerName svhv01 | Set-VMMemory -MaximumBytes 2.5GB
  2. Get-VM -ComputerName svhv01 | Set-VMMemory -ComputerName svhv01 -MaximumBytes 2.5GB

If you said #1, then you’re right! But, do you know why? It’s actually fairly simple to tell. Just look at the object that is crossing the pipeline:

Computer Name on Object

Computer Name on Object

The object contains its computer name and the cmdlets in the Hyper-V module are smart enough to process it as an object that contains a computer name. To then specify the computer name again for the cmdlet to the right of the pipeline just confuses PowerShell and causes it to error. Not all objects have a ComputerName property and not all cmdlets know to look for it. Furthermore, if you do anything to strip away that property and then try to pipe it to another cmdlet, you will need to specify ComputerName again. For example:

Get-VM -ComputerName svhv01 | select Name | % { Set-VMMemory -ComputerName svhv01 -MaximumBytes 2.5GB -VMName $_.Name }

Explicit Remoting

If the remote host has the PowerShell module installed, you can establish a session to it and begin work immediately. Natively, this requires the target system to be 2012 or later, as there was no official Hyper-V PowerShell module in prior versions. There is an unofficial release for 2008 R2 (and maybe 2008, I never tried). Explicit remoting is “harder” than implicit remoting (requires more typing) but is much more powerful and there are no fancy rules governing it. If you can connect to the remote machine using PowerShell Remoting, then you can operate any PowerShell cmdlets there (for the PowerShell experts, just imagine that there is an asterisk right here that links to a mention of Constrained Endpoints).

There are two general ways to use explicit remoting. The first is to use Enter-PSSession. That drops you right onto the remote console as a sort of console-within-a-console. From there, you can work interactively. The second method is to encapsulate script in a block and feed it to Invoke-Command. That method is used for scripting and automatically cleans up after itself.

PowerShell Remoting in an Interactive Session

The simplest way to remotely connect to an interactive PowerShell session is:

Enter-PSSession hostname

As shown, the command only works between machines on the same domain and when the current user account has sufficient privileges. I showed this above, but for the sake of completeness, to connect when one of the computers is unjoined or untrusted and/or if the user account is not administrative:

Enter-PSSession hostname -Credential (Get-Credential)

This will securely prompt you for the credentials to use on the target. If the target is domained-joined, make sure you use the format of domainusername. If it’s a standalone system, you can use the username by itself or computernameusername.

If the remote system is using SSL:

Enter-PSSession hostname -Credential (Get-Credential) -UseSSL

Once the connection is established, it will change the prompt to reflect that you’re accessing the remote computer. You can see examples in the screenshots above. It will look like this:

[hostname]: PS C:UsersUserAccountDocuments>

One thing I generally avoid in instructional text is using positional parameters. I especially dislike mixing positional and named parameters. I’ve done both here for the sake of showing you how uncomplicated PowerShell Remoting is to use. For the purposes of delivering a proper education, be aware that the named parameter that you use to specify the host name is -ComputerName . As long as it’s the first parameter submitted to the cmdlet, you don’t have to type it out.

Once you’re connected, it’s mostly like you were sitting at a PowerShell prompt on the remote system. Be aware that any custom PowerShell profile you have on that host isn’t loaded. Also note that whatever you’re doing on that remote system stays on that system. For instance, you can’t put something into a variable and then call that variable from your system after you exit the session.

When you’re done, you can just close the PowerShell window. PowerShell will clean up for you. If you want to go back to working on your computer:


I much prefer the shorter alias:


Using PowerShell Remoting to Address the Remote Device Manager Problem

Now that you know how to connect to a remote PowerShell session, you have the ability to overcome one of the long-standing remote management challenges of both Windows and Hyper-V Server. Prior to the 2012 versions, you could remotely connect to Device Manager, but only in a read-only mode. Starting in 2012, even that is gone.

You can get driver information for a lot of devices using PowerShell. For example, Get-NetAdapter returns several Driver fields. But what about installing or upgrading drivers? That was never possible using Device Manager remotely, or even through other remote tools. Well, with PowerShell Remoting, the problem is solvable.

You’re not restricted to running PowerShell commands inside your remote session. You can run executables, batch files, and other such things. The only things you can’t do is initiate a GUI or start anything that has its own shell. Fortunately, one of the things that can be run is pnputil. This utility can be used to manage drivers on a system. So, with PowerShell Remoting, you can remotely install and upgrade drivers.

My systems use Broadcom NICs as their management adapters. I downloaded the drivers and transferred them into local drives on my hosts. Then, using PowerShell Remoting from my desktop, I connected in and used pnputil to install them. The command to install a driver is:

pnputil -i -a driverfile.inf

You can see the results for yourself:

Remote PNPUTIL Driver Installation

You can see that, as expected, my network connection was interrupted. What’s not shown is that PowerShell used a progress bar to show its automatic reconnection attempts. Once the driver was installed, the session automatically picked up right where it left off.

For verification, you can use pnputil -e :

Remote PNPUTIL Driver Enumeration

Windows replaces the original driver file name with OEM#, as you can see here, but it keeps the manufacturer name and the driver version and date. If you want further verification, you can also run Get-NetAdapter | fl Driver* .

Advanced PowerShell Remoting with Invoke-Command

Here’s where the fun begins. Where the remote session usage shown above is great for addressing immediate needs, the true power of PowerShell Remoting is in connecting to multiple machines. Invoke-Command is the tool of choice:

Invoke-Command -ComputerName svhv1, svhv2 -ScriptBlock { Get-VM }

If you run the above on systems prior to 2016, the first thing you’ll likely notice is that there’s no prettification of the output. Get-VM usually looks like this:

Demo Get-VM

That’s because there’s a defined custom formatting view being applied. When an object crosses back to a source system across Invoke-Command , no view is applied. What you get is mostly the same thing you’d see if you piped it through Format-List -Property *   (mostly seen as | fl *  ).

At this point, it might not make any sense why we’re doing this. This is the same output that we got from the implicit remoting earlier, but it required more typing, and more to remember.

If you have any VMs, the above cmdlets produce a wall of text. Let’s slim it down a bit. From PowerShell 2.0 in Windows 7, I ran Invoke-Command -Computer svhv1, svhv2 -Credential (Get-Credential) -UseSSL -ScriptBlock { Get-VM | select Name }. Here’s the output:

Get-VM through Invoke-Command

Running the same script block locally would have resulted in a table with just the name column. Here, I get three more: PSComputerName, RunspaceId, and PSShowComputerName. In PowerShell 3.0 and later, the PSShowComputerName column isn’t there anymore. The benefit here is that you can use these fields to sort the output by the system that sent it.

You can use -HideComputer with Invoke-Command  to suppress the output of all the extra fields if your source system is running PowerShell 3.0 or later. For PowerShell 2.0, RunspaceId is still shown but the others are hidden. They’re still there, so you can query against them. What’s nice about this is, if your system has the related module installed, then any custom formatting views will be applied just as if you were running inside a connected session:

Invoked Get-VM with -HideComputerName

This formatting issue is no longer a concern in Windows 10/Windows Server 2016 (which I suspect is more due to changes in PowerShell 5 than in the Hyper-V module):

PowerShell Remoting Formatting in 2016

PowerShell Remoting Formatting in 2016

Being able to format the output will always be a useful skill even with the basic formatting issues automatically addressed.

It might not make sense why we’re doing things this way. The implicit remoting method that I showed you earlier did just as well, and it required less to type (and memorize). The first reason that you’d use this method is because it doesn’t matter if the local computer and the remote computer are running the same versions of anything. The PowerShell 2.0 examples on Windows 7 that I showed you were running against a Hyper-V Server 2012 R2 environment. Neither the Windows 7 nor my Windows 10 environment can even run implicit remoting against those systems.

Even more importantly, this doesn’t begin to show the true power of PowerShell Remoting. Let’s do some interesting things. For instance, looking at VM output is fun and all, but why stop there? How about:

$RemoteVMs = Invoke-Command -Computer svhv1, svhv2 -Credential (Get-Credential) -UseSSL -ScriptBlock { Get-VM }

What I’ve done here is connect in to both hosts, retrieve their virtual machines, store them in a variable on my computer, and then disconnected the remote session. I can store them, format the output, build reports, etc. What I can’t do is make any changes to them, but that’s OK. I’ve got a couple of answers to that.

First, I can perform the modifications right on the target system by using a more complicated script block. The following example builds a script that retrieves all the VMs that aren’t set to start automatically and sets them so that they do. That entire script is assigned to a variable named “RemoteVMManipulation”. I use that as the -ScriptBlock parameter in an Invoke-Command , which I send to each of the hosts. The result of the script is saved to a variable:

$RemoteVMManipulationBlock = {
    $VMsNotStartingAutomatically = Get-VM | where { $_.AutomaticStartAction -ne "Start" }
    foreach ($VM in $VMsNotStartingAutomatically)
        Set-VM -VM $VM -AutomaticStartAction Start
$ModifiedVMs = Invoke-Command -ComputerName svhv1, svhv2 -ScriptBlock $RemoteVMManipulationBlock

This isn’t the most efficient script block, but I wrote it that way for illustration purposes. The variable “VMsNotStartingAutomatically” is created on each of the remote systems, but is destroyed as soon as the script block exits. It is not retrievable or usable on my calling system. However, I’ve placed the combined output into a variable named “ModifiedVMs”. Like a local function call, the output is populated by whatever was in the pipeline at the end of the script block’s execution. In this case, it’s the “VMsNotStartingAutomatically” array. Upon return, this array is transferred to the “ModifiedVMs” variable, which lives only on my system. In subsequent lines of the above script, I can view the VM objects that were changed even though the remote sessions are closed.

The second way to manipulate the objects that were returned is to transmit them back to the remote hosts using the -InputObject  parameter and keep track of them with the added “PSComputerName” field:

$RemoteVMManipulationBlock = {
    foreach ($VMList in $input)
        foreach($VM in $VMList)
            if($VM.PSComputerName -eq $env:COMPUTERNAME)
                Set-VM -VMName $VM.Name -AutomaticStartAction StartIfRunning
Invoke-Command -ComputerName svhv1, svhv2 -ScriptBlock $RemoteVMManipulationBlock -InputObject $ModifiedVMs

What I’ve done here is send the “ModifiedVMs” variable from my system into each of the target systems using the -InputObject parameter. Once inside the script block, you reference this variable with $input . There are a few things to note here. For one, you’ll notice that I had to unpack the “ModifiedVMs” variable two times. For another, I wasn’t able to reference the input items as VM objects. Instead, I had to point it to the names of the VMs. This is because we’re not sending in true VM objects. We’re sending in what we got. GetType() reveals them as:


Because they’re a different object type, parameters expecting an object of the type “Microsoft.HyperV.PowerShell.VirtualMachine” will not work. Objects returned from Invoke-Command are always deserialized, which is why you have to go through these extra steps to do anything other than look at them. If you’ve got decent programming experience or you just don’t care about these sorts of things, you can skip ahead to the next section.

Serialization and deserialization are the methods that the .Net Framework, which is the underpinning of PowerShell, uses to first package objects for uses other than in-memory operations, and then to unpackage them later. There are lots of definitions out there for the term “object” in computer programming, but they are basically just containers. These containers hold only two things: memory addresses and an index of those memory addresses. The contents at those memory addresses are really just plain old binary 0s and 1s. It’s the indexes that give them meaning. How exactly that’s done from the developer’s view is dependent upon the language. So, in C++, you might find a “variable” defined as “int32”. This means that the memory location referenced by the index is 32 bits in length and the contents of those 32 bits should be considered an integer and that those contents can be modified. Indexes come in two broad types: data and code. In (a perhaps overly simplistic description of) .Net, data indexes are properties and refer to constants and variables. Code indexes can be either methods or events, and refer to functions.

As long as the objects are in memory, all of this works pretty much as you’d expect. If you send a read or write operation to a data index, then the contents of memory that it points to are retrieved or changed, respectively. If you (or, for an event, the system) call on a code index, then the memory contents it refers to are processed as an instruction set.

What happens if you want to save the object, say to disk? Well, you probably don’t care about the memory locations. You just want their contents. As for the functions and events, those have no meaning once the object is stored. So, what has to happen is all the code portions need to be discarded and the names of the indexes need to be paired up with the contents of the memory that they point to. As mentioned earlier, the .Net Framework does this by a process called serialization. Once an object is serialized, it can be written directly to disk. In our case, though, the object is being transmitted back to the system that called Invoke-Command .  Once there, it is deserialized so that its new owning system can manipulate it in-memory like any other object. However, because it came from a serialized object, its structure looks different than the original because it isn’t the same object.

For comparison, here’s the structure of a VM object (2012 R2):

   TypeName: Microsoft.HyperV.PowerShell.VirtualMachine

Name                         MemberType    Definition
----                         ----------    ----------
VMId                         AliasProperty VMId = Id
VMName                       AliasProperty VMName = Name
Equals                       Method        bool Equals(System.Object obj)
GetHashCode                  Method        int GetHashCode()
GetHostOnlyKeyValuePairItems Method        string[] GetHostOnlyKeyValuePairItems()
GetType                      Method        type GetType()
RemoveKeyValuePairItem       Method        void RemoveKeyValuePairItem(string name, Microsoft.HyperV.PowerShell.KeyV...
ToString                     Method        string ToString()
VerifyClusterPath            Method        void VerifyClusterPath(string path, bool allowUnverifiedPaths)
AutomaticStartAction         Property      Microsoft.HyperV.PowerShell.StartAction AutomaticStartAction {get;}
AutomaticStartDelay          Property      int AutomaticStartDelay {get;}
AutomaticStopAction          Property      Microsoft.HyperV.PowerShell.StopAction AutomaticStopAction {get;}
ComPort1                     Property      Microsoft.HyperV.PowerShell.VMComPort ComPort1 {get;}
ComPort2                     Property      Microsoft.HyperV.PowerShell.VMComPort ComPort2 {get;}
ComputerName                 Property      string ComputerName {get;}
ConfigurationLocation        Property      string ConfigurationLocation {get;}
CPUUsage                     Property      int CPUUsage {get;}
CreationTime                 Property      datetime CreationTime {get;}
DVDDrives                    Property      Microsoft.HyperV.PowerShell.DvdDrive[] DVDDrives {get;}
DynamicMemoryEnabled         Property      bool DynamicMemoryEnabled {get;}
FibreChannelHostBusAdapters  Property      System.Collections.Generic.List[Microsoft.HyperV.PowerShell.VMFibreChanne...
FloppyDrive                  Property      Microsoft.HyperV.PowerShell.VMFloppyDiskDrive FloppyDrive {get;}
Generation                   Property      int Generation {get;}
HardDrives                   Property      Microsoft.HyperV.PowerShell.HardDiskDrive[] HardDrives {get;}
Heartbeat                    Property      System.Nullable[Microsoft.HyperV.PowerShell.VMHeartbeatStatus] Heartbeat ...
Id                           Property      guid Id {get;}
IntegrationServicesState     Property      string IntegrationServicesState {get;}
IntegrationServicesVersion   Property      version IntegrationServicesVersion {get;}
IsClustered                  Property      bool IsClustered {get;}
IsDeleted                    Property      bool IsDeleted {get;}
Key                          Property      Microsoft.HyperV.PowerShell.ObjectKey Key {get;}
MemoryAssigned               Property      long MemoryAssigned {get;}
MemoryDemand                 Property      long MemoryDemand {get;}
MemoryMaximum                Property      long MemoryMaximum {get;}
MemoryMinimum                Property      long MemoryMinimum {get;}
MemoryStartup                Property      long MemoryStartup {get;}
MemoryStatus                 Property      string MemoryStatus {get;}
Name                         Property      string Name {get;}
NetworkAdapters              Property      System.Collections.Generic.List[Microsoft.HyperV.PowerShell.VMNetworkAdap...
Notes                        Property      string Notes {get;}
NumaAligned                  Property      System.Nullable[bool] NumaAligned {get;}
NumaNodesCount               Property      int NumaNodesCount {get;}
NumaSocketCount              Property      int NumaSocketCount {get;}
OperationalStatus            Property      Microsoft.HyperV.PowerShell.VMOperationalStatus[] OperationalStatus {get;}
ParentSnapshotId             Property      System.Nullable[guid] ParentSnapshotId {get;}
ParentSnapshotName           Property      string ParentSnapshotName {get;}
Path                         Property      string Path {get;}
PrimaryOperationalStatus     Property      System.Nullable[Microsoft.HyperV.PowerShell.VMOperationalStatus] PrimaryO...
PrimaryStatusDescription     Property      string PrimaryStatusDescription {get;}
ProcessorCount               Property      long ProcessorCount {get;}
RemoteFxAdapter              Property      Microsoft.HyperV.PowerShell.VMRemoteFx3DVideoAdapter RemoteFxAdapter {get;}
ReplicationHealth            Property      Microsoft.HyperV.PowerShell.VMReplicationHealthState ReplicationHealth {g...
ReplicationMode              Property      Microsoft.HyperV.PowerShell.VMReplicationMode ReplicationMode {get;}
ReplicationState             Property      Microsoft.HyperV.PowerShell.VMReplicationState ReplicationState {get;}
ResourceMeteringEnabled      Property      bool ResourceMeteringEnabled {get;}
SecondaryOperationalStatus   Property      System.Nullable[Microsoft.HyperV.PowerShell.VMOperationalStatus] Secondar...
SecondaryStatusDescription   Property      string SecondaryStatusDescription {get;}
SizeOfSystemFiles            Property      long SizeOfSystemFiles {get;}
SmartPagingFileInUse         Property      bool SmartPagingFileInUse {get;}
SmartPagingFilePath          Property      string SmartPagingFilePath {get;}
SnapshotFileLocation         Property      string SnapshotFileLocation {get;}
State                        Property      Microsoft.HyperV.PowerShell.VMState State {get;}
Status                       Property      string Status {get;}
StatusDescriptions           Property      string[] StatusDescriptions {get;}
Uptime                       Property      timespan Uptime {get;}
Version                      Property      string Version {get;}
VMIntegrationService         Property      System.Collections.Generic.List[Microsoft.HyperV.PowerShell.VMIntegration...

The same VM when it pops out the other side of Invoke-Command:

   TypeName: Deserialized.Microsoft.HyperV.PowerShell.VirtualMachine

Name                        MemberType   Definition
----                        ----------   ----------
GetType                     Method       type GetType()
ToString                    Method       string ToString(), string ToString(string format, System.IFormatProvider fo...
PSComputerName              NoteProperty System.String PSComputerName=svhv1
PSShowComputerName          NoteProperty System.Boolean PSShowComputerName=True
RunspaceId                  NoteProperty System.Guid RunspaceId=edeb5a97-c8cc-4556-97c0-5c5fe47a2486
VMId                        NoteProperty System.Guid VMId=e3a95f29-2102-4aed-ac59-ce5651afa3a1
VMName                      NoteProperty System.String VMName=svdc1
AutomaticStartAction        Property     System.String {get;set;}
AutomaticStartDelay         Property     System.Int32 {get;set;}
AutomaticStopAction         Property     System.String {get;set;}
ComPort1                    Property     System.String {get;set;}
ComPort2                    Property     System.String {get;set;}
ComputerName                Property     System.String {get;set;}
ConfigurationLocation       Property     System.String {get;set;}
CPUUsage                    Property     System.Int32 {get;set;}
CreationTime                Property     System.DateTime {get;set;}
DVDDrives                   Property     Deserialized.Microsoft.HyperV.PowerShell.DvdDrive[] {get;set;}
DynamicMemoryEnabled        Property     System.Boolean {get;set;}
FibreChannelHostBusAdapters Property     Deserialized.System.Collections.Generic.List`1[[Microsoft.HyperV.PowerShell...
FloppyDrive                 Property      {get;set;}
Generation                  Property     System.Int32 {get;set;}
HardDrives                  Property     Deserialized.Microsoft.HyperV.PowerShell.HardDiskDrive[] {get;set;}
Heartbeat                   Property     System.String {get;set;}
Id                          Property     System.Guid {get;set;}
IntegrationServicesState    Property     System.String {get;set;}
IntegrationServicesVersion  Property     System.Version {get;set;}
IsClustered                 Property     System.Boolean {get;set;}
IsDeleted                   Property     System.Boolean {get;set;}
Key                         Property     System.String {get;set;}
MemoryAssigned              Property     System.Int64 {get;set;}
MemoryDemand                Property     System.Int64 {get;set;}
MemoryMaximum               Property     System.Int64 {get;set;}
MemoryMinimum               Property     System.Int64 {get;set;}
MemoryStartup               Property     System.Int64 {get;set;}
MemoryStatus                Property     System.String {get;set;}
Name                        Property     System.String {get;set;}
NetworkAdapters             Property     Deserialized.System.Collections.Generic.List`1[[Microsoft.HyperV.PowerShell...
Notes                       Property     System.String {get;set;}
NumaAligned                 Property     System.Boolean {get;set;}
NumaNodesCount              Property     System.Int32 {get;set;}
NumaSocketCount             Property     System.Int32 {get;set;}
OperationalStatus           Property     Deserialized.Microsoft.HyperV.PowerShell.VMOperationalStatus[] {get;set;}
ParentSnapshotId            Property      {get;set;}
ParentSnapshotName          Property      {get;set;}
Path                        Property     System.String {get;set;}
PrimaryOperationalStatus    Property     System.String {get;set;}
PrimaryStatusDescription    Property     System.String {get;set;}
ProcessorCount              Property     System.Int64 {get;set;}
RemoteFxAdapter             Property      {get;set;}
ReplicationHealth           Property     System.String {get;set;}
ReplicationMode             Property     System.String {get;set;}
ReplicationState            Property     System.String {get;set;}
ResourceMeteringEnabled     Property     System.Boolean {get;set;}
SecondaryOperationalStatus  Property      {get;set;}
SecondaryStatusDescription  Property      {get;set;}
SizeOfSystemFiles           Property     System.Int64 {get;set;}
SmartPagingFileInUse        Property     System.Boolean {get;set;}
SmartPagingFilePath         Property     System.String {get;set;}
SnapshotFileLocation        Property     System.String {get;set;}
State                       Property     System.String {get;set;}
Status                      Property     System.String {get;set;}
StatusDescriptions          Property     Deserialized.System.String[] {get;set;}
Uptime                      Property     System.TimeSpan {get;set;}
Version                     Property     System.String {get;set;}
VMIntegrationService        Property     Deserialized.System.Collections.Generic.List`1[[Microsoft.HyperV.PowerShell...

You’ll notice that all the events are gone. The only methods are GetType() and ToString(), which are part of this new object, not carried over from the original, and are here because they exist on every PowerShell object. Properties that contained complex objects have also been similarly serialized and deserialized.

Using Saved Credentials and Multiple Sessions

Of course, what puts the power into PowerShell is automation. Automation should mean you can “set it and forget it”. It’s tough to do that if you have to manually enter information into Get-Credential, isn’t it?

There’s also the problem of multiple credential sets. Hopefully, if you’ve got more than one host that sits outside your domain, they’ve each got their own credentials. I know that some people out there put Hyper-V systems in workgroup mode “to protect the domain” but then use a credential set with the same user name and password as their domain credentials. It’s no secret that I see no value in workgroup Hyper-V hosts when a domain is available except for perimeter networks, but if you’re going to do it, at least have the sense to use unique user names and passwords. Sure, it can be inconvenient, but when you actively choose the micro-management hell of workgroup-joined machines, you can’t really be surprised when you find yourself in micro-management hell. Fortunately for you, PowerShell Remoting can take a lot of the sting out of it.

The first step is to gather the necessary credentials for all of your remote machines and save them into disk files on the system where you’ll be running Invoke-Command. For that bit, I’m just going to pass the buck to Lee Holmes. He does a great job explaining both the mechanism and the safety of the process.

Once you have the credentials stored in variables, you next create individual sessions to the various hosts.

$SVHV1Session = New-PSSession -ComputerName svhv1 -Credential $SVHV1Credential
$SVHV2Session = New-PSSession -ComputerName svhv2 -Credential $SVHV2Credential

You’re all set. You use the sessions like this:

$RemoteVMs = Invoke-Command -Session $SVHV1Session, $SVHV2Session -ScriptBlock { Get-VM }

Of course, if you’ve only got one remote host but you want to use credentials retrieved from disk, you don’t need the sessions:

$RemoteVMs = Invoke-Command -ComputerName svhv1 -Credential $SVHV1Credential -ScriptBlock { Get-VM }

One thing to remember though, is that sessions created with New-PSSession will persist, even if you close your PowerShell prompt. They’ll eventually time out, but until then, they’re like a disconnected RDP session. They just sit there and chew up resources, giving an attackers an open session to attempt to compromise, all for no good reason. If you want, you can reconnect and reuse these sessions. Otherwise, get rid of them:

$SVHV1Session | Remove-PSSession
$SVHV2Session | Remove-PSSession

Or, to close all:

Get-PSSession | Remove-PSSession

For More Information

I’ve really only scratched the surface of PowerShell Remoting here. I had heard about it some time before, but I wasn’t in a hurry to use it because I was “getting by” with Hyper-V Manager and Remote Desktop connections. Ever since I spent a few minutes learning about Remoting, I have come to use it every single day. The ad hoc capabilities of Enter-PSSession allow me to knock things out quickly and the scripting powers of Invoke-Command are completely irreplaceable.

All that, and I haven’t even talked about delegated administration (allowing a junior admin to carry out a set of activities as narrowly defined as you like via a pre-defined PowerShell Remoting session) or implicit remoting or setting up “second hop” powers so you can control other computers from within your remote session. For those things, and more, you’re going to have to do some research on your own. I recommend starting with PowerShell in Depth. Most of the general information, but not all, of what you saw in this article can be found in that book. That chapter does contain all the things I teased about, and more.


Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

21 thoughts on "The Ultimate Guide to Hyper-V Remote Management with PowerShell"

  • Joce says:

    On a Windows 8.1 Enterprise where RSAT is installed the command

    Install-WindowsFeature Hyper-V-PowerShell

    gives this error

    Install-WindowsFeature : The target of the specified cmdlet cannot be a Windows client-based operating system

    Any idea why ?

  • Joce says:

    On a Windows 8.1 Enterprise where RSAT is installed the command

    Install-WindowsFeature Hyper-V-PowerShell

    gives this error

    Install-WindowsFeature : The target of the specified cmdlet cannot be a Windows client-based operating system

    Any idea why ?

  • Hgsysit says:

    very useful


  • Hgsysit says:

    very useful


  • Jason says:

    Windows 10 hyperv manager connecting to HyperV server 2016 doesnt work. Everything is on the Domain. Ran HVRemote and everything looks good. Any help would be appreciated

  • Anthony says:

    I got installed on my Hyper-V server:
    [X] Hyper-V
    [X] Hyper-V Management Tools
    [X] Hyper-V Module for Windows PowerShell
    Is there a way to launch Hyper-V manager via PowerShell under MS Hyper-V Server? I need this in order to get some GUI in virtual environment.

  • Antonio Sebastian says:

    Nicely Written article, thanks for taking the time. I learned a lot…

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published. Required fields are marked *

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.