Building a Lab in Hyper-V with PowerShell, Part 2

Creating VMs with New-myVM.ps1 – Part 1

As we saw in Part 1 of this series, I build my labs almost entirely with Windows PowerShell scripts. In that first post, I showed how to set the MAC address range on a Hyper-V host. I use this MAC address range to explicitly set my lab VMs to a specific MAC address for their NICs. This allows me to pre-configure all the IP addresses in DHCP by using reservations. (I showed you how to install and preconfigure the DHCP Server role in my post on Configuring Windows Server 2016 core as a DHCP Server with PowerShell. And I’ll show you how to create all those reservations automatically in a later post in this series.)

For this post, and the couple, I want to share my “New-myVM.ps1” script. I use this script to create client and server VMs, from either DVD or sysprep’d VHDX files.

First, the parameters that New-myVM accepts:

Param ([Parameter(Mandatory = $True,Position = 0)][alias("Name")][string]$VmName,
       [Parameter(Mandatory = $True,Position = 1)][alias("Final")][string]$MacFinal,
       [Parameter(Mandatory = $False)][alias("Base")][string]$MacBase = "00-15-5D-C8-0A-",
       [Parameter(Mandatory = $False)][string]$199MacBase = "00-15-5D-C8-CE-",
       [Parameter(Mandatory=$False)][Boolean]$Client=($myInvocation.myCommand.Name -match "Client"),
       [Parameter(Mandatory=$False)][alias("Target")][string]$Path = "V:\",
       [Parameter(Mandatory=$False)][alias("VHDSource","DVDBase")][string]$Source = "V:\Source",
       [Parameter(Mandatory=$False)][alias("LocalSwitch","Network")][string]$vmSwitch = "10 Network",

I have only two required parameters, the VMName and final two digits of the MAC addresses that the VM will use. Everything else defaults to some reasonable value for my labs. You’ll notice that by default, I don’t install from DVD, and I’m not installing Server 2012R2, but Server 2016. And, finally, a bit you might not have seen before, my default to determine if this is a client build or a server build.

[Parameter(Mandatory=$False)][Boolean]$Client=($myInvocation.myCommand.Name -match "Client")

The $Client parameter is a Boolean. I can specify it on the command line with -Client $True/$False, or I can allow it to accept the default value. The interesting thing is that the default value is dynamic. I use an NTFS hard link for New-myVM.ps1 to give it a second name, New-myClientVM.ps1.

Sidebar: NTFS Hard Links 

NTFS filesystem hard links take a single file and give it multiple names. The file is stored only once on the filesystem, but it has multiple names that can access the file. Each name for the file is completely equal. Even if you delete the original filename, all the linked versions are still present and completely unaffected by the deletion. You create a hard link in Server 2016 or Windows 10 with New-Item or in earlier versions of Windows with the built-in CMD command mklink. So, for example:

New-Item -Type HardLink `
         -Path 'C:\Build\New-myClientVM.ps1','C:\Build\New-myServerVM.ps1' `
         -Value 'C:\Build\New-myVM.ps1'

#Older OS Version:
cmd /c mklink /h C:\Build\New-myClientVM.ps1 C:\Build\New-myVM.ps1
cmd /c mklink /h C:\Build\New-myServerVM.ps1 C:\Build\New-myVM.ps1

I then use ($myInvocation.myCommand.Name -match “Client”) to see if the filename I started the script with was New-myClientVM.ps1 , or one of the other names I have for this script.  ($myInvocation.myCommand.Name -match “Client”) returns $True for New-myClientVM.ps1, but $False for filenames that don’t include ‘Client’ in the filename.


Next, I set some basic variables used throughout the script. These are periodically tweaked as new builds become my default. Currently, they’re set at:

$VMBase     = "$Path\$VMName"
$VHDSource  = $Source
$DVDBase    = $Source
$VHDBase    = "$VMBase\Virtual Hard Disks"
$SysVHD     = "$VMBase\Virtual Hard Disks\$VmName-System.vhdx"
$MachineBase= "$VMBase\Virtual Machines"
$ServerISO  = "$DVDBase\en_windows_server_2016_x64_dvd_9718492.iso"
$ClientISO  = "$DVDBase\en_windows_10_enterprise_version_1607_updated_jan_2017_x64_dvd_9714415.iso"
$ClientVHD  = "$Source\Generalized-client.vhdx"
if ($2012R2) { 
   $ServerVHD = "$Source\Generalized-2012R2.vhdx"
} else {
   $ServerVHD  = "$Source\Generalized-System.vhdx"

Nothing special there. Next, three functions to verify paths, etc. These are pretty basic Test-Path statements. If I need to create directories, I do. But if I don’t find my source files where I expect them, or I find an already existing .vhdx where I’m not expecting one, then I use simple Throw statements to get me out and report the problem, since either of these failures will cause the script to fail, usually in an ugly way. ;)

Function Test-SourcePath () {
   if ($Client) {
      if ($dvd) {
         if (Test-Path $ClientISO) {
            Write-Verbose "Install ISO found at $ClientISO"
         } else {
            Throw "Client ISO not found at $ClientISO" 
      } elseif (Test-Path $ClientVHD) { 
         Write-Verbose "Source VHD found at $ClientVHD"
   } else {
      if ($dvd) {
         if (Test-Path $ServerISO) {
            Write-Verbose "Install ISO found at $ServerISO"
         } else {
            Throw "Server ISO not found at $ServerISO" 
      } elseif (Test-Path $ServerVHD) { 
         Write-Verbose "Source VHD found at $ServerVHD"

if (! (Test-Path $VHDBase ) ) { 
   mkdir $VHDBase
if (! (Test-Path $MachineBase ) ) { 
   mkdir $MachineBase

Function Test-Clean () {
   If (Test-Path $VHDBase\*.vhdx ) {
      Throw "Found an existing VHD. Please clean up the target path and try again."

You’ll notice with these functions, and the ones that follow, everything builds on those original variables created at the top of the script, or as part of the script parameters. Yes, I need to keep those up to date. But there’s only one place to make the changes.

Now, assuming I’m most likely going to be building from a sysprep’d VHD, I  copy that .vhdx file into my “Virtual Hard Disks” folder for this VM, changing the name as I do to reflect the new VM’s name.

function Copy-myVhd () {
      if ( $DVD ) {
         Write-Verbose "DVD specified. Not copying source VHD to $SysVHD"
      } else { 
         if ( $Client ) { 
            Write-Verbose "Creating VM from Sysprep'd VHD base $ClientVHD"
            cp $ClientVHD $SysVHD 
         } else { 
         Write-Verbose "Creating VM from Sysprep'd VHD base $ServerVHD"
            cp $ServerVHD $SysVHD

Now that we have all that setup work done, let’s actually create the VM. We have to do this in two separate steps because Hyper-V doesn’t give us a way to set the number of CPUs or configure some other stuff as part of the initial creation of the VM. We have to modify the VM after we first create it. Silly, but not all that hard to deal with in a script, though a real nuisance at the interactive command line.

Create-myVM {
if ($DVD ) { 
  New-VM -Name $VmName `
       -MemoryStartupBytes 1024MB `
       -BootDevice VHD `
       -Generation 2 `
       -SwitchName $vmSwitch `
       -NewVHDPath $SysVHD `
       -NewVHDSize 200GB `
       -Path $MachineBase
} else { 
  New-VM -Name $VmName `
       -MemoryStartupBytes 1024MB `
       -BootDevice VHD `
       -Generation 2 `
       -SwitchName $vmSwitch `
       -VHDPath $SysVHD `
       -Path $MachineBase

And, we now have a VM. It’s not quite what we want and need, yet, but we have a VM. One problem, I can’t set the boot device to DVD, regardless of what the PowerShell help pages say, because I’m building Generation 2 virtual machines, and they don’t allow you to specify ‘CD’ as the boot device during initial setup. So, we’ll have to configure that, along with the other tweaks we need, as part of the next stage of the whole process. And for that, you’ll have to wait until tomorrow, when I do Part 2 of New-myVM. :)

Configuring Windows Server 2016 Core with and for PowerShell

I know I owe you more on creating a lab with PowerShell, and I’ll get to that in a few days. But having just set up a new domain controller running Server 2016 Core, I thought I’d include a couple of tricks I worked through to make your life a little easier if you choose to go with core.

First: Display Resolution — the default window for a virtual machine connected with a Basic Session in VMConnect is 1024×768. Which is just too small. So, we need to change that. Now in the full Desktop Experience, you’d right click and select Display Resolution, but that won’t work in Server Core, obviously. Instead we have PowerShell. Of course. The command to set the display resolution to 1600×900 is:

Set-DisplayResolution -Width 1600 -Height 900

This will accept a -Force parameter if you don’t like being prompted. A key point, however, is that it ONLY accepts supported resolutions. For a Hyper-V VM, that means one of the following resolutions:

1920x1080     1600x1050     1600x1200
1600x900      1440x900      1366x768
1280x1024     1280x800      1280x720
1152x864      1024x768       800x600

Now that we have a large enough window to get something done, start PowerShell with the Start PowerShell (that space is on purpose, remember we’re still in a cmd window.)  But don’t worry, we’ll get rid of that cmd window shortly.

Now that we have a PowerShell window, you can set various properties of that window by using any of the tricks I’ve shown before, such as Starting PowerShell Automatically which sets the Run key to start PowerShell for the current user on Login with:

 New-ItemProperty HKCU:\Software\Microsoft\Windows\CurrentVersion\Run `
                       -Name  "Windows PowerShell" `
                       -Value "C:\Windows\system32\WindowsPowerShell\v1.0\PowerShell.exe"


I also showed you how to set the PowerShell window size, font, etc in Starting PowerShell Automatically Revisited. And, of course, you can set the PowerShell window colour and syntax highlighting colours as described in Setting Console Colours. Of course, all my $Profile tricks work as well, so check those out.


So, now that we’ve configured the basics of our PowerShell windows, let’s set PowerShell to replace cmd as the default console window. To do that, use the Set-ItemProperty cmdlet to change the WinLogon registry key:

Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon' `
                 -Name Shell `
                 -Value 'PowerShell.exe -NoExit'

Viola! Now, when we log on to our Server Core machine, it will automatically open a pair of PowerShell windows, one from WinLogon registry key and one from the Run registry key.

PowerShell v5.1 Released

Microsoft has released the Windows Management Framework (WMF) 5.1, including Windows PowerShell 5.1,  to the web. You can download it here. This is the final version that released with Windows Server 2016, though it doesn’t include all the features of PowerShell 5.1 that are on Server 2016 because some are not supported on earlier versions of Windows. WMF 5.1 is available for Windows Server 2012 R2, Windows Server 2012, Windows 2008 R2 SP1, Windows 8.1, and Windows 7 SP1. (Note, this does NOT include Windows 8.0!)


Installation on Windows 7 and Windows Server 2008 R2 has updated installation requirements. Please carefully read the Release Notes before installing.


All that being said, I’m updating all my computers to the latest version. My Windows 10 and Server 2016 computers are already at the WMF 5.1 level, of course, but I still have some legacy servers that need updates. They’ll be getting them over the next couple of weeks, and my lab image templates are getting updates as well.