Save time on Hyper-V using PowerShell

INTRODUCTION

Unless your datacenter is in a cave you have certainly heard of Windows PowerShell. Perhaps you have even opened up a PowerShell session and tried running a few basic commands. Although if you haven't you are probably not alone. I still encounter many IT Pros just getting started with PowerShell even though it has been out for 10 years and we're now on version 5.0.

PowerShell is more than scripting language or a set of commands. It is a management paradigm that can fundamentally change (hopefully for the better) the way you do your job. If you have Hyper-V management duties, PowerShell should be in your toolbox.

I've written a great deal about managing Hyper-V with PowerShell in the past. You can find many of those articles on the Altaro blog. This eBook intends to take a slightly different approach. My goal is to demonstrate that PowerShell is worth the investment to learn.

I hear from people all the time at conferences or classes I teach that they wish they had learned PowerShell earlier or don't know why they put it off.

Once you understand the fundamentals you'll realize that managing "things" with PowerShell isn't that difficult.

It doesn't matter if you are managing a bunch of Active Directory user accounts or Hyper-V virtual machines. The fundamental principles don't change and the rewards can be significant.

PowerShell doesn't mean you have to write a script to do everything. A great PowerShell feature is that there is really no difference between running PowerShell commands interactively at a prompt or running a PowerShell script. The benefit of a script is that you only have to type the commands once, it can be documented and it will run the same way every time.

The concept of this eBook is to demonstrate how PowerShell automation can help you save time when it comes to managing Hyper-V. Even though my PowerShell "answers" will be shown as PowerShell scripts, you could also run many of the commands interactively in the console. Actually, I hope you'll take my code examples and build your own Hyper-V management tools and scripts.

This eBook will go through a list of common Hyper-V management tasks and demonstrate how and why you might consider using PowerShell instead of manually accomplishing the task through the Hyper-V management console. I hope to show you how much time you can save.

Some of the time savings may not seem like much, but over the course of a year, or even a week, they can really add up. Plus, if you employ re-usable scripts and functions, anyone can run them even if they don't know PowerShell. They simply need access to the script files, the necessary admin permissions, and some basic training in how to run a PowerShell script or command.

My approach for all of this follows 2 principles:

  • First, everything is done from the desktop. There is no need to logon to a server. There is no need to open up a remote desktop connection. If that is the way you are used to managing servers, you need to break yourself of that habit.
  • The 2nd principle is "If you can do it for one you can do it for many. PowerShell makes it very easy to manage things at scale. By that I mean instead of managing one thing at a time, like a virtual machine, it is just as easy to manage 10, 100 or 1000 of them. When I am creating PowerShell tools I'm always thinking about working remotely and managing multiple items at once.

REQUIREMENTS

For many of my examples I will be using PowerShell 5.0 on Windows 10, although PowerShell 4.0 should suffice. I am also using version 2.0 of the PowerShell Hyper-V module. More on that in a moment.

I am also logged in with an account that has administrator privileges in my domain.

In fact, all of my Hyper-V hosts and most of my virtual machines are in the domain. Depending on your available version you may or may not be able to run some of my examples with alternate credentials.

You should always read full help and examples for any PowerShell command that you don't recognize like this:

Help get-vmhost -full

I have a few Hyper-V hosts running Window Server 2012 R2 and Windows Server 2016 Technical Preview 5. As long as you are running Windows Server 2012 or later I think you should be fine.

As I mentioned, I am going to be demonstrating from the client, which is a practice you should be following. This means you need the PowerShell module for managing Hyper-V. I'm assuming you have either Windows 8.1 or Windows 10. It is possible to set up implicit remoting and use the PowerShell commands from a Hyper-V host but that is an advanced topic that is out of scope for this eBook.

You do not need to install the hypervisor on the client desktop. But you may need to enable the management tools. To do so, open Control Panel - Programs - Turn Windows Features On or Off and expand Hyper-V. Check the boxes for the Hyper-V management tools.

You can always compare my versions with yours:

MANAGING MULTIPLE VERSIONS

As I write this, Microsoft has 2 versions of their PowerShell commands for managing Hyper-V. Which version you have depends the version of your operating system and PowerShell. Today, there are potential issues using the commands, especially if you have a mix of Hyper-V servers. In my test environment I have Hyper-V servers running Windows Server 2016 Technology Preview 5 and Window Hyper-V Server 2012. If you try to run commands from a newer system to an older system, you might see an error like this:

Although the command works just fine against a newer server.

I am going to try and avoid doing anything that has to cross this type of "version boundary", and hopefully all your servers will be running the same version of Hyper-V. If not, you might have to take a few extra steps to load a compatible version. Look at my Windows 10 client:

I have two versions of the Hyper-V module. By default, PowerShell uses the commands from the newest version. But instead I want to load the older version as it will be able to manage both older and newer servers. There are few new cmdlets in version 2 of the module as well as a few new parameters, but for what we will be looking at I don't think it will make much difference.

First, because I've already loaded v2 of the Hyper-V module, I'll unload it.

Remove-Module Hyper-V

Now I can explicitly load the required version using either of these commands:

Import-Module -FullyQualifiedName @{ModuleName="Hyper-V";
RequiredVersion="l.l"} Import-Module Hyper-V -maximumversion 1.1

This version works with the older Hyper-V server. I'm hoping this won't be an issue for you. Finally, to see all of the available commands in the module, use Get-Command:

Get-Command -module Hyper-V

CAVEATS

If you are also running System Center Virtual Machine Manager or have those PowerShell cmdlets installed, be careful. There are a number of cmdlets that share the same name between the SCVMM and Hyper-V modules. Even though the name is the same, the commands are different and if the SCVMM version gets picked instead you might run into problems. If this becomes a problem, you can modify my code examples to explicitly reference the Hyper-V module:

hyper-v\get-vm -computername chi-hvr2

I will also be running commands from the traditional PowerShell console. You can certainly load my script files into the PowerShell ISE and run them from there but that is not required. And speaking of the scripts, you can download a zip file with sample scripts here:

The script files are provided for educational and demonstration purposes only. They are not necessarily production ready and I can't guarantee they will work in your environment. You should test everything and build your own PowerShell solutions in a non-production environment.

TASK # 1 CONFIGURE A HYPER-V HOST

Let's get started with managing Hyper-V at the host itself. Typically, I don't expect you to have to make many changes to a Hyper-V host. Generally, once it is set up I think most admins leave it alone. On one hand since you only have to do it once, using the graphical Hyper-V Manager isn't necessarily a bad choice. But it can be time consuming and certainly doesn't scale.

THE TRADITIONAL WAY

There are a number of settings you might want to configure on a per host basis from default locations, to NUMA Spanning to virtual machine migration. Not to mention tasks such as creating new virtual switches. Generally, I'm talking about the settings you see when viewing server properties in the Hyper-V Manager.

Depending on what you need to configure this could take as long as 5 to 20 minutes per host. That seems like a long time to me. Plus, anything you do manually is prone to human error.

THE POWERSHELL WAY

You can the Get-VMHost cmdlet to view the current configuration.

Remember, in PowerShell everything is an object and what you see by default isn't necessarily everything. Use Select-Object to view all properties.

Once you know the property names, you can select them. In fact, you can select them for multiple servers at the same time.

get-vmhost chi-hvr1,chi-hvr3,chi-p50 | Select Computername,LogicalProcessorCount, NumaSpanningEnabledVirtualMachinePath, EnableEnhancedSessionMode,VirtualMachineMigrationEnabled

Let's say that the company standard is for Hyper-V servers to have enhanced session mode and virtual machine migration enabled and the virtual machine path to be C:\ VMs. Using the Hyper-V Manager, which was already open, it took me about 45 seconds per server to complete.

Instead let's use Set-VMHost to make the same changes. I've already created and verified C:\VMs separately, although you could certainly incorporate it into your PowerShell solution. Remember, read full help and examples for Set-VMHost.

Set-VMHost -ComputerName chi-hvr1,chi-hvr3,chi-p50-VirtualMachinePath C:\VMs -EnableEnhancedSessionMode $True -UseAnyNetworkForMigration $True -MaximumVirtualMachineMigrations 5 -VirtualMachineMigrationAuthenticationType Kerberos -VirtualMachineMigrationPerformanceOption Compression

With this one-line command, which took about 1 second to run to configure 3 servers configured the servers the way I wanted. Although, there is actually one more step:

Enable-VMMigration -ComputerName chi-hvr1,chi-hvr3,chi-p50

I needed to run a command to tick the check box.

You can run the command interactively, or put it in PowerShell script file and run it that way. The benefit is that anyone can run it at any time to ensure Hyper-V hosts are properly configured. The script also serves as a record of what was done. Once you gain experience creating PowerShell tools you can even turn it into a re-usable function that takes computer names as parameters, logs activity to a file or database, and perhaps even allows for different migration settings.

VERDICT

It is pretty hard to argue that taking a few seconds to configure multiple servers with a single command is not a compelling argument for PowerShell. Sure, you'll take a little time up front to learn the commands and syntax but the more experience you gain, the shorter amount of time you will require. And when you are finished, if you save your command to a script file you have a re-usable tool which is much more effective than giving someone a manual with a bunch of screen shots and click instructions. Unless you like that sort of work!

TASK # 2 MANAGE VIRTUAL MACHINE STORAGE

Another task that probably should be done more often but probably isn't is managing a virtual machine's storage. By that I mean the VHD or VHDX files associated with a given virtual machine. I'm betting this isn't done often because it is a tedious process to do manually.

REPORTING

Sure, it isn't too difficult to check a single VHD file. In the GUI this might take 20-30 seconds per disk. Even longer if you need to prepare some type of report for customers or managers. Instead, PowerShell makes this a snap.

Get-VMHardDiskDrive -VMName CHI-SQL01 -ComputerName chi-p50

A command like this takes less than half a seconds and can at least see the disk files. However, if we want to examine the VHDX file directly that will take an extra step or two. There is a cmdlet called Get-VHD which will be very useful but it needs access to the files. In my example above I am running the command from my client desktop but the file paths are relative to the server. This means I need to run Get-VHD on the Hyper-V host. Fortunately, this is pretty easy to do using PowerShell remoting.

Invoke-Command {Get-VMHardDiskDrive -VMName CHI-SQL01 | Get-VHD } -ComputerName chi-p50

This doesn't take much longer and provides some useful information. In fact, since I can do this for a single virtual machine, I can do it for all of them.

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | Get-VHD | Select PathjVHDFormatjFileSizejSizejParentPathjFragmentationPercentage, @{Name="VM";Expression={$pv.VMName}} } -ComputerName chi-p50

This is technically a one-line command that took about 5 seconds to run against 22 disk files.

I had to do a little advanced trick with the common PipelineVariable parameter so that I could temporarily capture the virtual machine name from the first part and use it as a custom property in the last part. Invoke-Command supports multiple computer names so I could run this command at the same time against all my Hyper-V hosts and export the information to a CSV file.

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | Get-VHD | Select Path,VHDFormat,FileSize,Size,ParentPath,FragmentationPercentage, @{Name="VM";Expression={$pv.VMName}} } -ComputerName chi-p50,chi-hvr1,chi-hvr2,chi-hvr3 | Select * -ExcludeProperty RunspaceID | Export-Csv -Path c:\work\VMDisks.csv -NoTypeInformation

There's no way with the Hyper-V Manager to do anything like this.

COMPACTING

Once we have a way to look at storage, we can then do something such as compacting or resizing. Again, in the GUI this will probably take you about 60-90 seconds per disk, not counting time for the actual operation. Let's find all disks on a Hyper-V server that are at least 80% fragmented and optimize them. We can re-use some of the code we just created.

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | Get-VHD | Where {$_.FragmentationPercentage -ge 80} Select Path,FragmentationPercentage,@{Name="VM";Expression={$pv.VMName}}, @{Name="VMStatus";Expression = {(Get-VM $pv.VMname).State}} } -computername chi-p50

This gives us a good idea of what needs to be addressed. The Optimize-VHD cmdlet has a number of options so you'll have to decide which is appropriate for your situation. The other important factor is the disk file must be unattached or in read-only mode. Generally, this means the virtual machine must be offline. But you can automate that as well.

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | Get-VHD | Where {$_.FragmentationPercentage -ge 80} Select Path,FragmentationPercentage,@{Name="VM";Expression={$pv.VMName}}, @{Name="VMStatus";Expression = {(Get-VM $pv.VMname).State}} | Foreach { if ($_.State -eq "running") { Stop-VM -Name $_.VM 
#save the stopped VM name 
$stopped = $_.VM } 
Optimize-VHD -Path $_.Path -Mode Full
#restart VM if it was stopped 
if ($Stopped) { Start-VM -VM $_.VM remove-variable stopped }
}
} -computername chi-p50

This is a little more complicated and makes more sense, at least to me, as a PowerShell script. I could run this script once a quarter if I wanted and it would only take me a few seconds. Or I could take advantage of PowerShell scheduled jobs and setup it up to ran like a scheduled task and never have to worry about it again!

RESIZING

Or I might need to do the opposite and expand a disk file. You've already seen that we can get a VHDs configured size and the actual file size. Personally, because I use dynamic disks most of the time, I find that if the file size is getting close to the configured size, I need to expand the VHDX file. First, let's see what disks might need this treatment.

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv |
Get-VHD | Select Path,Size,FileSize, @{Name="PctUsed";Expression = { ($_.filesize/$_.size)*l00 }},
@{Name="VM";Expression={$pv.VMName}}, @{Name="VMStatus";Expression = {(Get-VM $pv.VMname).State}} } -computername chi-p50 | Sort PctUsed

Again, I am only checking a single Hyper-V host but could easily have checked 10 of them with the same command. To manually resize disk files will take at least 30 seconds per file. I have identified 3 files that can be resized so doing this manually would take me 90-120 seconds. Or I can run a PowerShell command like this:

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | Get-VHD | where { (($_.filesize/$_.size)*100 -ge 80) -AND $_.MinimumSize -gt 0 } |Select Path,Size,FileSize, @{Name="PctUsed";Expression = { ($_.filesize/$_.size)*100 }}, @{Name="VM";Expression={$pv.VMName}}, @{Name="VMStatus";Expression = {(Get-VM $pv.VMname).State}} |
Foreach { if ($_.State -eq "running") { Stop-VM -Name $_.VM #save the stopped VMName $stopped = $_.VM }
#calculate a new size as 20% larger 
$NewSize = $s_.size + ($_.Size * .20) Resize-VHD -Path $_.Path -SizeBytes $NewSize
#restart VM if it was stopped 
if ($Stopped) { Start-VM -VM $_.VM remove-variable stopped }
}
} -computername chi-p50

This took a matter of seconds. There are some considerations when it comes to resizing files. If the disk file is being treated as a dynamic disk as opposed to a basic disk, then this type of operation will most likely fail. If the MinimumSize property is empty, this is usually a good indication that a dynamic disk is being used so my script filters those disk files out. The new size is calculated for each disk by adding 20% of the current size.

VERDICT

If you want to do any type of reporting or storage management across multiple Hyper-V hosts or virtual machines, you have no choice but to use PowerShell. But it isn't difficult if you take your time, start slow and practice in a non-production environment. Needless to say mucking about with storage is a potentially dangerous task so make sure you have good backups in place.

TASK # 3 CREATE A NEW VIRTUAL MACHINE

Perhaps no task is a better candidate for automation than creating a new virtual machine. Of course, you can create one in the Hyper-V Manager but it will most likely take 1 to 2 minutes of clicking and typing, not counting time to install any operating system. PowerShell provides a number of tools that you can mix and match that can spin up a new virtual machine in seconds. And when we think about managing at scale that matters.

ALL IN ONE

The primary cmdlets we will use are New-VM and Set-VM. The former will create a virtual machine and the latter will let you fine tune it. You can create a fully configured virtual machine in moments.

$vm = New-VM -Name WinVM -MemoryStartupBytes 512MB -SwitchName Domain -NewVHDPath WinVM-C.vhdx -NewVHDSizeBytes 25GB -Generation 2 -ComputerName chi-hvr1 Set-VM -VM $vm -ProcessorCount 2 -DynamicMemory -MemoryMinimumBytes 512MB -MemoryMaximumBytes 2GB -AutomaticStartAction StartIfRunning -AutomaticStopAction Save -passthru

These commands took about 3.5 seconds to run and created a fully configured virtual machine, although an operating system still needs to be installed.

DISK FIRST

Another approach is to create the disk separately. This too takes only a few seconds.

$Path = Join-Path -Path (Get-VMHost -ComputerName CHI-HVR1).VirtualHardDiskPath -ChildPath WinVM2.vhdx $vhdx = New-VHD -Path $Path -ParentPath D:\2K12R2Corex64.vhdx -Differencing -SizeBytes 40GB -ComputerName CHI-HVR1

I just created a new 40GB differencing disk. Then I can create a new virtual machine using this disk.

$vm = New-VM -Name WinVM2 -MemoryStartupBytes 512MB -SwitchName Domain -VHDPath $Path -Generation 1 -ComputerName chi-hvr1 Set-VM -VM $vm -ProcessorCount 2 -DynamicMemory -MemoryMinimumBytes 512MB -MemoryMaximumBytes 2GB -AutomaticStartAction StartIfRunning -AutomaticStopAction Save -passthru Start-vm $vm

The parent disk is from an older version so I have to create a generation 1 virtual machine. But within literally seconds I have a virtual machine up and running. I can use PowerShell remoting to connect to the guest operating system and reconfigure as necessary.

VERDICT

There's no question that using PowerShell automation to create new virtual machines is the smart and only choice. I can just as easily create 10 new virtual machines in not much more time than it takes to bring up 1. Most likely this is something you will create some PowerShell tooling around so that other admins can run the commands. Or you might integrate the commands into some automated monitoring system that starts up new virtual machines when circumstances warrant.

TASK # 4 WORKING WITH VIRTUAL SWITCHES

While there are cmdlets for creating new virtual switches, this isn't a task I think you do often and you aren't likely to create more than a few at a time. In these cases, using the graphical manager may be just as easy. However, managing virtual switches, especially when configuring virtual machines is definitely a candidate for automation.

Let's say you have created a new virtual switch, or have an existing one, and you want to configure a virtual machine to start using it. In the Hyper-V Server Manager, changing a VM's switch can take 20-30 seconds, depending on the number of virtual machines you have to wade through. If you had to make the change on 10 virtual machines that would take you at least 5 minutes of pretty tedious work. And sure, you could assign the task to an intern, hoping he wouldn't make a mistake along the way.

THE POWERSHELL WAY

Or you can assign a new virtual switch with PowerShell in less than half a second.

Get-VMNetworkAdapter -vmname VMDemo01 -computername chi-hvr1 | Connect-VMNetworkAdapter -SwitchName Internal

Yes, this might take a few seconds to type, but you could easily turn it into a PowerShell function or script. In fact, I needed to automate some changes in my Hyper-V environment. As I added Hyper-V servers, I wasn't very careful about naming my virtual switches. This meant that I needed to take extra steps when it came time to move or live migrate virtual machines between hosts.

My solution was to rename all of the switches bound to the host's external adapter so that they all shared the same name. Then I needed to make sure that all my virtual machines that started with CHI (I was at least consistent there) were connected to this switch. If the virtual machine was already connected to the switch under the old name, it would automatically use the new name. But just in case I have some odd machines that are misconfigured, this should catch those as well.

I could have typed the commands out interactively, but I wrote a short script primarily to include support for -WhatIf.

#requires -version 4.0 
#requires -module Hyper-V
[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Mandatory,HelpMessage="Enter the new name for the switch")]
[ValidateNotNullorEmpty()] [string]$NewName,
[ValidateNotNullorEmpty()] [string[]]$VMHosts = @("chi-hvr1","chi-hvr2","chi-hvr3","chi-p50")
)
#rename switch
Get-VMSwitch -SwitchType External -ComputerName $VMHosts | Where {$_.NetAdapterInterfaceDescription -notmatch "loopback"}
foreach { #whatIf code If ($PSCmdlet.ShouldProcess("$($_.name) [$($_. computername)]","Rename to $NewName")) { Rename-VMSwitch -Name $_.Name -NewName $NewName -ComputerName $_.Computername }
}
#change virtual machines 
#I added my own code for -WhatIf because Connect-VMNetworkAdapter complains 
#if there is no existing switch 
If ($PSCmdlet.ShouldProcess("virtual machines CHI-*","Set VMSwitch to $NewName")) {
Get-VMNetworkAdapter -VMName CHI-* -ComputerName $VMHosts | foreach { Connect-VMNetworkAdapter -SwitchName $NewName -VMName $_.VMName }
}

This lets me safely test the process.

Now I can run it for real without -WhatIf and in 6 seconds I reconfigure switches on 4 servers and 14 virtual machines.

I can also use PowerShell to easily report on all of my virtual switches. Something that is impossible in the management console.

Get-VMSwitch -ComputerName chi-hvr1,chi-hvr3,chi-p50 | sort SwitchType | Format-Table -GroupBy SwitchType -Property Name,NetAdapterInterfaceDescription,AllowManagementOS,Computername

And of course I can just as easily modify the switches, something that takes about 20 seconds per switch to accomplish.

Let's work with switch extensions.

Get-VMSwitchExtension -Name "Microsoft NDIS Capture" -VMSwitchName DomainNet -comp chi-p50,chi-hvr3,chi-hvr2,chi-hvr1 | Select Name,Enabled,SwitchName,Computername

I want to enable this extension.

Get-VMSwitchExtension -Name "Microsoft NDIS Capture" -VMSwitchName DomainNet -comp chi-p50,chi-hvr3,chi-hvr2,chi-hvr1 | Where {-Not $_.Enabled} | foreach {Enable-VMSwitchExtension -VMSwitchName $_.SwitchName -ComputerName $_.ComputerName -Name $_.Name }

This is technically a one line command that took 2.5 seconds to complete. It finds the NDIS Capture extension on Hyper-V hosts where it is not enable, and enables it.

VERDICT

I will be the first to admit that some of the commands in the Hyper-V module don't behave that way I would expect based on my previous experience with PowerShell. I wish some commands worked better in a pipelined expression without having to resort to ForEach-Object as I did above. That is why it is important that you take the time to read cmdlet and examples.

However, even with this learning curves, managing virtual switches and virtual machines is an order of magnitude faster than using the Hyper-V Manager.

TASK # 5 MANAGING VIRTUAL MACHINE CHECKPOINTS

The last management task we'll look at is working with virtual machine snapshots, which Microsoft now refers to as checkpoints. In fact, in starting with version 2.0 of the Hyper-V module, all of the cmdlets that use VMSnapshot as the noun have aliases using VMCheckpoint.

But for consistency I'll use the VMSnapshot commands.

If you needed to manually create a checkpoint it isn't that difficult. In fact, you can even select multiple virtual machines and create a checkpoint for all of them. Although removing checkpoints for multiple virtual machines is bit more complicated and takes about 30-60 seconds per virtual machine, not counting disk operations.

THE POWERSHELL WAY

First, let's look at reporting on the current state of checkpoints, primarily because PowerShell brings some added value that would take you a long time to duplicate manually. Let's look at snapshots for a single virtual machine.

Get-VMSnapshot -VMName CHI-Test02 -ComputerName chi-p50

If you pipe this to Get-Member you'll discover there are many more useful properties.

Get-VMSnapshot -VMName CHI-Test02 -ComputerName chi-p50 | Select *Name,Path,Size*,ID,SnapshotType,CreationTime

One thing I especially like is that SizeOfSystemFiles property. This indicates how much disk space the snapshot is consuming. Although I should note that in earlier versions of the Hyper-V module, this value does not appear to be accurate. With a little tweaking and some custom properties, we can even get a more meaningful display in practically no time.

Get-VMSnapshot -VMName CHI-Test02 -ComputerName chi-p50 | Select *Name,@{Name="SizeMB";Expression={$_.SizeofSystemFiles/1MB}}, CreationTime,@{Name="Age";Expression={(Get-Date) - $_.CreationTime}}

If we can do it for one virtual machine, we can do it for all of them.

Get-VMSnapshot -VMName * -ComputerName chi-p50 | Select VMName,Name,Parent*,@{Name="SizeMB";Expression={$_.SizeofSystemFiles/lMB}}, CreationTime,@{Name="Age";Expression={(Get-Date) - $_.CreationTime}}
Out-Gridview -Title "CHI-P50 Snapshots"

In this version I revised the Select-Object part of the command to better control the property display order.

Finally, let's scale this out and check for snapshots on multiple Hyper-V hosts.

$vmhosts = "chi-hvr1","chi-hvr3","chi-p50" Get-VMSnapshot -VMName * -ComputerName $VMHosts | Select Computername,VMName,Name,ParentSnapshotID,ParentSnapshotName, @{Name="SizeMB";Expression={$_.SizeofSystemFiles/1MB}}, CreationTime,@{Name="Age";Expression={(Get-Date) - $_.CreationTime}}
Sort SizeMB -Descending | Out-Gridview -Title "All Snapshots"

The changes to the command were minor.

This is all but impossible to do in the server manager GUI. In fact, you could create some nice tooling to find old snapshots.

Function Get-OldVMSnapShot { [cmdletbinding()] Param( [string]$VMName = "*", [Parameter(Mandatory)] [string]$Computername, [int]$Days = 30)
Get-VMSnapshot -VMName $VMName -ComputerName $Computername | Where {((Get-Date) - $_.CreationTime).TotalDays -ge $Days}}

Within milliseconds I discovered all snapshots on the given Hyper-V server that were created over 60 days ago.

Why is this useful? How about cleaning them up.

Get-OldVMSnapShot -Computername chi-p50 -Days 60 | foreach { Remove-VMSnapshot -VMSnapshot $_ -confirm}

As I mentioned earlier, some of these cmdlets don't behave the way they probably should so I've had to resort to using ForEach-Object. But it works.

Finally, let's look at the opposite situation.

Suppose you are about to undertake a process and want to have a snapshot handy as a fallback. With a single command I can create a checkpoint on all virtual machines that start with CHI.

Checkpoint-VM -Name CHI* -ComputerName chi-p50,chi-hvr1,chi-hvr3 -SnapshotName "FlashSnap" -AsJob

All of the checkpoints will have the same name. I am also taking advantage of PowerShell's background job feature. The checkpoints will be created in the background and I can get my PowerShell prompt back immediately to do other things. I can check later to see that the snapshots are complete.

That's 12 snapshots created probably in seconds where as I would have spent at least 15-20 seconds per virtual machine creating the snapshot and renaming it in the Hyper-V Manager. That's only 4-5 minutes. But my Hyper-V environment is really quite small.

Suppose you had 100 virtual machines on multiple hosts. More than likely it would take close to a half hour just to generate the snapshot.

I don't know about you, but that's not a task I would enjoy.

And as you've already seen, cleanup is a snap!

Remove-VMSnapshot -VMName * -Name FlashSnap -ComputerName chi-p50,chi-hvr1,chi-hvr3

This too you could run with the -AsJob parameter.

Finally, let's get rid of all snapshots on a Hyper-V host.

$all = Get-VMSnapshot -VMName * -ComputerName chi-hvr3 Remove-VMSnapshot -VMSnapshot $all

I only had to remove 4 small snapshots but it could just as easily been 40 or 400. The commands are the same.

VERDICT

If your organization relies on VM snapshots, and you have more than a few virtual machines, I don't see you can you effectively manage them without some form of automation. As with many of the other tasks we've looked at, what can take 20-30 seconds per virtual machine can be accomplished in milliseconds from a PowerShell prompt.

SUMMARY

If you've made your way through the entire eBook then I'm assuming you recognize the value of automation when it comes to managing Hyper-V. I also hope that you realized PowerShell can play a critical role in your daily and weekly tasks and save you a great deal of time. It may be in small increments, but these add up over the course of a year. Not to mention the benefits of consistency, documentation and sheer fun.

ADDITIONAL RESOURCES

If you are looking for additional resources on Hyper-V as well as managing Hyper-V with PowerShell, you should of course keep an eye on the Altaro blog at http://www.altaro.com/ hyper-v. I also maintain a pretty active blog at http://blog.jdhitsolutions.com

If you are a visual learner, you'll find a number of video training courses at Pluralsight. com. Or catch me at a conference like TechMentor or IT/Dev Connections where I am most likely presenting something PowerShell related.

Finally, I would also encourage you to use the forums at PowerShell.org which is part of the non-profit DevOps Collective (https://devopscollective.org). The forums are very active and cover a wide range of PowerShell-related content. You'll also find free eBooks on the site and information about the PowerShell+DevOps Global Summit (https://powershell.org/summit) which is the meeting for learning about what PowerShell, and other DevOps related technologies, can do for you.

YOUR ACTION PLAN

I'll leave you with some recommendations. First, learning how to use PowerShell to manage anything, is like learning a foreign language. The best way to learn is to immerse yourself and use it every day. If you really want to master many of the examples I've shared with you, ditch the Hyper-V Manager and begin to learn how to do everything from the PowerShell prompt. It will be a struggle at first and definitely take longer than you expect. But at some point things will click, especially when you start managing Hyper-V hosts and virtual machines at scale.

Once you have worked out some basic PowerShell expressions, you can begin building scripts and functions and before you know it you'll have a custom management suite of PowerShell tools that will make you more efficient. In fact, you'll probably become the go-to person in your organization which is not always a bad thing career-wise.

I hope you found this book informative and inspiring. If our paths cross at training class or conference, I hope you'll let me know.

Good luck and happy scripting!

ABOUT ALTARO

Altaro Software (www.altaro.com) is a fast growing developer of easy to use backup solutions used by over 30,000 customers to back up and restore both Hyper-V and VMware-based virtual machines, built specifically for Small and mid-market business with up to 50 host servers. Altaro take pride in their software and their high level of personal customer service and support, and it shows; Founded in 2009, Altaro already service over 30,000 satisfied customers worldwide and are a Gold Microsoft Partner for Application Development and Technology Alliance VMware Partner.

ABOUT ALTARO VM BACKUP

Altaro VM Backup is an easy to use backup software solution used by over 30,000 Small and mid-market business customers to back up and restore both Hyper-V and VMware-based virtual machines. Eliminate hassle and headaches with an easy-to-use interface, straightforward setup and a backup solution that gets the job done every time.

Altaro VM Backup is intuitive, feature-rich and you get outstanding support as part of the package. Demonstrating Altaro's dedication to Hyper-V, they were the first backup provider for Hyper-V to support Windows Server 2012 and 2012 R2 and also continues support Windows Server 2008 R2.

Altaro VM Backup is an easy to use backup software solution for Hyper-V and VMware. It's built specifically for SMBs with up to 50 host servers and is trusted by over 30,000 customers to back up and restore their virtual machines. Eliminate hassle and headaches with easy-to-use interface, straightforward setup and a backup solution that gets the job done every time, without budget drain.

Altaro VM Backup is reliable, intuitive and feature-rich, plus you get outstanding support as part of the package.

ABOUT JEFF HICKS

Jeffery Hicks is an IT veteran with over 25 years of experience, much of it spent as an IT infrastructure consultant specializing in Microsoft server technologies with an emphasis in automation and efficiency. He is a multi-year recipient of the Microsoft MVP Award. He works today as an independent author, trainer and consultant. Jeff has taught and presented on PowerShell and the benefits of automation to IT Pros worldwide. Jeff has authored and co-authored a number of books, writes for numerous online sites and print publications, is a contributing editor at Petri.com, a Pluralsight author, and a frequent speaker at technology conferences and user groups.


Hyper-VAutomationSampleScripts

All of these script files are intended as demonstrations and educational exercises. None of these files are intended to be run as scripts in a production environment. Everything is offered AS-IS with no guarantees or warranties. You should test everything in secure, non-production environment.

Demo-Checkpoints.ps1

Return "Do not run this as a script."

#get a single checkpoint
Get-VMSnapshot -VMName CHI-Test02 -ComputerName chi-p50 |
Select *Name,Path,Size*,ID,SnapshotType,CreationTime

#something more meaningful
Get-VMSnapshot -VMName CHI-Test02 -ComputerName chi-p50 |
Select *Name,@{Name="SizeMB";Expression={$_.SizeofSystemFiles/1MB}},
CreationTime,@{Name="Age";Expression={(Get-Date) - $_.CreationTime}}


#get all checkpoints
Get-VMSnapshot -VMName * -ComputerName chi-p50 |
Select VMName,Name,Parent*,@{Name="SizeMB";Expression={$_.SizeofSystemFiles/1MB}},
CreationTime,@{Name="Age";Expression={(Get-Date) - $_.CreationTime}} |
Out-Gridview -Title "CHI-P50 Snapshots"


#do it for all
$vmhosts = "chi-hvr1","chi-hvr3","chi-p50"
Get-VMSnapshot -VMName * -ComputerName $VMHosts |
Select Computername,VMName,Name,ParentSnapshotID,ParentSnapShotName,
@{Name="SizeMB";Expression={$_.SizeofSystemFiles/1MB}},
CreationTime,@{Name="Age";Expression={(Get-Date) - $_.CreationTime}} |
Sort SizeMB -Descending | 
Out-Gridview -Title "All Snapshots"

#define a function to list old snapshots
Function Get-OldVMSnapShot {
[cmdletbinding()]
Param(
[string]$VMName = "*",
[Parameter(Mandatory)]
[string]$Computername,
[int]$Days = 30
)

Get-VMSnapshot -VMName $VMName -ComputerName $Computername | 
Where {((Get-Date) - $_.CreationTime).TotalDays -ge $Days}

}

#remove old snapshots
Get-OldVMSnapShot -Computername chi-p50 -Days 60 |
foreach { Remove-VMSnapshot -VMSnapshot $_ -confirm}

#create checkpoints
Checkpoint-VM -Name CHI* -ComputerName chi-p50,chi-hvr1,chi-hvr3 -SnapshotName "FlashSnap" -AsJob

#then remove them
Remove-VMSnapshot -VMName * -Name FlashSnap -ComputerName chi-p50,chi-hvr1,chi-hvr3 -AsJob

#remove all snapshots from a server
$all = Get-VMSnapshot -VMName * -ComputerName chi-hvr3
Remove-VMSnapshot -VMSnapshot $all

Demo-NewVM.ps1

Return "Do not run this as a script."

#create new VM with new VHDX
$vm = New-VM -Name WinVM -MemoryStartupBytes 512MB -SwitchName Domain -NewVHDPath WinVM-C.vhdx -NewVHDSizeBytes 25GB -Generation 2 -ComputerName chi-hvr1
Set-VM -VM $vm -ProcessorCount 2 -DynamicMemory -MemoryMinimumBytes 512MB -MemoryMaximumBytes 2GB -AutomaticStartAction StartIfRunning -AutomaticStopAction Save -passthru

#create a disk first
#paths are relative to Hyper-V host
$Path = Join-Path -Path (Get-VMHost -ComputerName CHI-HVR1).VirtualHardDiskPath -ChildPath WinVM2.vhdx
$vhdx = New-VHD -Path $Path -ParentPath D:\2K12R2Corex64.vhdx -Differencing -SizeBytes 40GB -ComputerName CHI-HVR1

$vm = New-VM -Name WinVM2 -MemoryStartupBytes 512MB -SwitchName Domain -VHDPath $Path  -Generation 1 -ComputerName chi-hvr1
Set-VM -VM $vm -ProcessorCount 2 -DynamicMemory -MemoryMinimumBytes 512MB -MemoryMaximumBytes 2GB -AutomaticStartAction StartIfRunning -AutomaticStopAction Save -passthru

Start-VM $vm

Demo-Set-VMHost.ps1

Return "Do not run this as a script."

Set-VMHost -ComputerName chi-hvr1,chi-hvr3,chi-p50 -VirtualMachinePath C:\VMs -EnableEnhancedSessionMode $True -UseAnyNetworkForMigration $True -MaximumVirtualMachineMigrations 5 -VirtualMachineMigrationAuthenticationType Kerberos -VirtualMachineMigrationPerformanceOption Compression

#need to enable the checkbox
Enable-VMMigration -ComputerName chi-hvr1,chi-hvr3,chi-p50

Demo-Storage.ps1

Return "Do not run this as a script."

#region Check storage

#check a single VM
Get-VMHardDiskDrive -VMName CHI-SQL01 -ComputerName chi-p50

Invoke-Command {Get-VMHardDiskDrive -VMName CHI-SQL01 | Get-VHD } -ComputerName chi-p50

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | 
Get-VHD | 
Select Path,VHDFormat,FileSize,Size,ParentPath,FragmentationPercentage,
@{Name="VM";Expression={$pv.VMName}}
} -ComputerName chi-p50

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | 
Get-VHD | 
Select Path,VHDFormat,FileSize,Size,ParentPath,FragmentationPercentage,
@{Name="VM";Expression={$pv.VMName}}
} -ComputerName chi-p50,chi-hvr1,chi-hvr2,chi-hvr3 |
Select * -ExcludeProperty RunspaceID |
Export-Csv -Path c:\work\VMDisks.csv -NoTypeInformation


#endregion

#region Compress

#list
Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | 
Get-VHD | Where {$_.FragmentationPercentage -ge 80} |
Select Path,FragmentationPercentage,@{Name="VM";Expression={$pv.VMName}},
@{Name="VMStatus";Expression = {(Get-VM $pv.VMname).State}}
} -computername chi-p50

Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | 
Get-VHD | Where {$_.FragmentationPercentage -ge 80} |
Select Path,FragmentationPercentage,@{Name="VM";Expression={$pv.VMName}},
@{Name="VMStatus";Expression = {(Get-VM $pv.VMname).State}} |
Foreach {
    if ($_.State -eq "running") {
        Stop-VM -Name $_.VM
        #save the stopped VMName
        $stopped = $_.VM
    }
    Optimize-VHD -Path $_.Path -Mode Full

    #restart VM if it was stopped
    if ($Stopped) {
        Start-VM -VM $_.VM
        remove-variable stopped 
    }
}
} -computername chi-p50


#endregion

#region Expand disks

#get a report
Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | 
Get-VHD | 
Select Path,Size,FileSize
@{Name="PctUsed";Expression = { ($_.filesize/$_.size)*100 }},
@{Name="VM";Expression={$pv.VMName}},
@{Name="VMStatus";Expression = {(Get-VM $pv.VMname).State}}
} -computername chi-p50 | Sort PctUsed

#expand
Invoke-Command {Get-VMHardDiskDrive -VMName * -PipelineVariable pv | 
Get-VHD | where { (($_.filesize/$_.size)*100 -ge 70) -AND $_.MinimumSize -gt 0 } |
Select Path,Size,FileSize,
@{Name="PctUsed";Expression = { ($_.filesize/$_.size)*100 }},
@{Name="VM";Expression={$pv.VMName}},
@{Name="VMStatus";Expression = {(Get-VM $pv.VMname).State}} |
Foreach {
    if ($_.State -eq "running") {
        Stop-VM -Name $_.VM
        #save the stopped VMName
        $stopped = $_.VM
    }
    #calculate a new size as 20% larger
    $NewSize = $s_.size + ($_.Size * .20)
    Resize-VHD -Path $_.Path -SizeBytes $NewSize

    #restart VM if it was stopped
    if ($Stopped) {
        Start-VM -VM $_.VM
        remove-variable stopped 
    }
}
} -computername chi-p50



#endregion

Demo-VMSwitch.ps1

Return "Do not run this as a script."

#show vmnetwork adapters and their switches
Get-VMNetworkAdapter -vmname * -ComputerName chi-hvr1,chi-hvr2,chi-hvr3,chi-p50 | 
Sort Computername | 
Format-Table -GroupBy Computername -Property VMname,SwitchName,IPAddresses


#get VM Switches
Get-VMSwitch -ComputerName chi-hvr1,chi-hvr2,chi-hvr3,chi-p50

Get-VMSwitch -ComputerName chi-hvr1,chi-hvr2,chi-hvr3,chi-p50 | 
sort SwitchType | 
Format-Table -GroupBy SwitchType -Property Name,NetAdapterInterfaceDescription,AllowManagementOS,Computername

#extensions
Get-VMSwitchExtension -Name "Microsoft NDIS Capture" -VMSwitchName DomainNet -comp chi-p50,chi-hvr3,chi-hvr2,chi-hvr1 | 
Select Name,Enabled,SwitchName,Computername

Get-VMSwitchExtension -Name "Microsoft NDIS Capture" -VMSwitchName DomainNet -comp chi-p50,chi-hvr3,chi-hvr2,chi-hvr1 | 
Where {-Not $_.Enabled} | 
foreach {
 Enable-VMSwitchExtension -VMSwitchName $_.SwitchName -ComputerName $_.ComputerName -Name $_.Name
}

VMSwitchChange.ps1

#requires -version 4.0

[cmdletbinding(SupportsShouldProcess)]
Param(
[Parameter(Mandatory,HelpMessage="Enter the new name for the switch")]
[ValidateNotNullorEmpty()]
[string]$NewName,

[ValidateNotNullorEmpty()]
[string[]]$VMHosts = @("chi-hvr1","chi-hvr2","chi-hvr3","chi-p50")

)

#rename switch
Get-VMSwitch -SwitchType External -ComputerName $VMHosts | 
Where {$_.NetAdapterInterfaceDescription -notmatch "loopback"} | 
foreach {
    #WhatIf code
    If ($PSCmdlet.ShouldProcess("$($_.name) [$($_.computername)]","Rename to $NewName")) {
        Rename-VMSwitch -Name $_.Name -NewName $NewName -ComputerName $_.Computername
  }
}

#change virtual machines
#I added my own code for -WhatIf because Connect-VMNetworkAdapter complains
#if there is no existing switch
If ($PSCmdlet.ShouldProcess("virtual machines CHI-*","Set VMSwitch to $NewName")) {
Get-VMNetworkAdapter -VMName CHI-* -ComputerName $VMHosts | 
foreach { Connect-VMNetworkAdapter -SwitchName $NewName -VMName $_.VMName }
}