Hyper-V

Nested Hyper-V Networking

As I was trying to configure a new lab setup that takes advantage of nested Hyper-V so that I can build a lab to do Hyper-V host clustering, I ran into a problem with networking. Everything looked good on the "host1" virtual machine, but the domain controller I created for TreyResearch.net that runs as a nested VM on host1 couldn't connect to anything outside of host1. Which would end up being a pain fairly quickly. But after a good bit of poking around, I found the solution - either enable MAC Address Spoofing on host1, or configure a NAT switch on host1. For most of us, the MAC Address Spoofing is the simplest solution and works just fine. But if you're in a public cloud scenario, you'll likely have to go the NAT route.

To enable Nested Hyper-V, shutdown host1 and then run the following command on the top level host:

Set-VMProcessor -VMName host1 -ExposeVirtualizationExtensions $True

Start host1 and install the Hyper-V role with:

Install-WindowsFeature -Name Hyper-V -IncludeAllSubFeature -IncludeManagementTools

Once the reboots finish on host1, enable MAC Address Spoofing on the network adapter(s) of  host1:

Get-VMNetworkAdapter -VMName host1 | Set-VMNetworkAdapter -MacAddressSpoofing On

And you're done.

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:

[CmdletBinding()]
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)][alias("ISO")][Switch]$DVD,
       [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",
       [Parameter(Mandatory=$False)][switch]$2012R2
       )

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. :)

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

Setting the MAC Address Range on a Hyper-V Host

 

Today I want to start a new series of posts on building a lab environment in Hyper-V, primarily using Windows PowerShell to do initial configuration. For some things, you'll need to use a non-PowerShell tool, such as SysPrep or even the Hyper-V Manager. But the vast majority of the process is going to be pure PowerShell. And, to be clear, this is exactly how I build labs. I do very little checkpointing (snapshotting), preferring to rebuild to a known point rather than assuming I've gotten the right snapshot. And with a good set of scripts, and properly configured .VHDXs for source files, that's not really hard.

 

To understand how I build the base environment, you'll need to understand some assumptions I make and why. The first thing I do is configure the MAC address range for the lab host in Hyper-V. This avoids potential conflicts with other machines that might be on the network, especially since I'm going to be manually configuring all the MAC addresses for the VMs I create to allow me to easily configure DHCP reservations for all VMs. All of which makes keeping track of things much, much easier.

 

When Hyper-V hands out MAC addresses dynamically, it creates a range that begins "00-15-5D". This is the Microsoft IEEE Organizationally Unique Identifier, and is used for all Hyper-V generated MAC addresses unless we do something to change that prefix. Don't, you risk conflicting with some other company's range of addresses.

 

The next two pairs in the MAC address range are based on the current IPv4 network address(es) on the host itself if we don't manually configure them.  For lab environments, I usually set the first pair (fourth pair overall) to C8, C9, or CA, depending on which host machine I'm on, and the next pair to a number related to the IPv4 network that will be the primary network for core VMs in the lab. So, when I use a simple 192.168.10/24 network, I set that 5th pair to 0A. This gives me a MAC address range from 00-15-5D-C8-0A-00 to 00-15-5D-C8-0A-FF unless I need a larger range. If I'm going to have multiple networks and multiple NICs on lab VMs, I'll set a larger MAC address range.

 

To set the MAC address range on a Hyper-V server, you could use the GUI, but where's the fun in that. Instead, use the Set-VMHost cmdlet, thus:

Set-VMHost -MacAddressMinimum 00155DC80A00 `
           -MacAddressMaximum 00155DC80AFF `
           -PassThru

Now, I set my new VM script to default to this range as well, and I edit the CSV file that creates DHCP addresses to the same range. But more on setting fixed or reserved IP addresses and configuring DHCP later in the series.  Next up, we'll start building the parts of New-myVM.ps1.

Param() Tricks

One of the new features of PowerShell v5 is support for creating hard links, symbolic links and junctions. This is long overdue, and much appreciated. Before, I'd been forced to the workaround of using "cmd /c mklink" to create links, and I'm always glad to find a way to get rid of any vestige of cmd. Plus, having it as a part of PowerShell gives me way more flexibility in creating scripts.

 

As I was looking at some of my existing scripts, it occurred to me that I should be taking advantage of hard links in more scripts. I already use hard links for my various RDP connects, using a long switch statement. (I'll write that up one of these days, it's actually pretty cool.) But what caught my eye today was the script I wrote to create virtual machines for my labs -- New-myVM.ps1. I have a -Client parameter to the script that is a Boolean, defaulted to $False:

[CmdletBinding()]
Param([Parameter(Mandatory = $True,Position = 0)]
      [alias("Name")]
      [string]
      $VmName,
      [Parameter(Mandatory=$False)]
      [Boolean]
      $Client=$False
      )

Which is OK, but it occurred to me that I could do better. So, first, I created a new, hard-linked file with:

New-Item -Type HardLink -Name New-myClientVM.ps1 -Path .\New-myVM.ps1

Now I have one file with two names. Cool. So, let's take that a step further. I can tell which version of it I called from the command line by taking advantage of the automatic PowerShell variable $myInvocation:

$myInvocation.mycommand.name

This returns the filename (".name") of the command (".mycommand") that was executed ($myInvocation). So now, I can use:

$client =  ($myInvocation.mycommand.name -match "client")

I put that near the top of the script, and now I could branch depending on whether I was creating a server VM or a client VM. Which was definitely better, but still left me thinking it could be improved.

 

So, how about making the whole thing a lot cleaner by getting rid of that extra line? After all, I'm creating a variable and defaulting its value to $false, but why not default its value more intelligently, controlled by which file I executed to create the VM? I can still override it with the parameter (so no scripts that call this script will break), but now, I can set it automatically without using a parameter at all.

[CmdletBinding()]
Param([Parameter(Mandatory = $True,Position = 0)]
      [alias("Name")]
      [string]
      $VmName,
      [Parameter(Mandatory=$False)]
      [Boolean]
      $Client=($myInvocation.myCommand.Name -match "Client")
      )

Now that pleases me. It feels "cleaner", it's clear what I'm doing, and it doesn't take any longer to evaluate than it would as a standalone line.

Shutting Down Running VMs – Revisited

A couple of years ago, I posted a perfectly good snippet for shutting down the running VMs on a machine. But the code there is very much the "old syntax" and not terribly elegant.  For shutting down all the running RODCs, I used:

Get-VM -Name *rodc* | Where-Object {$_.State -eq "Running" } | Foreach-Object { Stop-VM $_.Name }

Which is a nuisance to type, frankly. So, here's the PowerShell v5 version. Much slicker, much easier to remember, and a lot quicker to type.

Get-VM -Name *rodc* | Where State -eq "Running" | Stop-VM

Simpler and easier to follow. It still requires a two-pipe solution, but that's only because we only wanted to stop the running VMs without any warning messages. If we didn't care about spurious warning messages, the answer gets even simpler:

Stop-VM *rodc*

All things considered, I'm in favour of simpler. And the odd warning message doesn't concern me, so I'll go with the last solution 90% of the time. Note that this accepts a -ComputerName parameter for running against a remote computer, a -PassThru parameter for echoing out which VMs it's shutting down, and a -TurnOff parameter to just kill the VMs, rather than use an orderly shutdown.

 

Historical note:  If you want to see a really complicated way to do this, written before we had "Stop-VM" as a cmdlet, take a look at this post from 4 years ago! I'm so glad PowerShell is doing all the heavy lifting now!

 

 

Getting the IP addresses of running VMs

 

Ben Armstrong posted a great little tip on his blog the other day. He has a little one-line PowerShell command that gives you a listing of all the running VMs on a host, and the IP addresses being used by each of them.

Get-VM | ?{$_.State -eq "Running"}   Get-VMNetworkAdapter | Select VMName, IPAddresses

Add the -ComputerName parameter, with the name of your Hyper-V server in the above, and you've got a really useful little script to figure out which VM has an address it shouldn't. Of course, I just had to tweak it a bit, by changing that last Select to a simple format-table, which allows me to get rid of the unnecessary whitespace with a -auto parameter. Thus:

PSH> Get-VM -computername cpr-asus-lap | ?{$_.State -eq "Running"} | Get-VMNetworkAdapter | ft -auto VMName, IPAddresses
VMName      IPAddresses
------      -----------
trey-cdc-05 {192.168.10.5, fe80::812e:a888:ac40:666b, 2001:db8:0:10::5}
trey-dc-02  {192.168.10.2, fe80::312c:a27c:c87e:3f98, 2001:db8:0:10::2}
trey-wds-11 {192.168.10.11, fe80::4520:ea29:54bb:9b41, 2001:db8:0:10::b}

Thanks, Ben. That's a useful one!

Stopping All Running Virtual Machines (Hyper-V)

So, a good friend and fellow MVP asked me for a script to shut down all running virtual machines on a server so she could do cold backups of them. This seemed like a perfectly reasonable request, and my first thought was “Well, this gets really obvious and easy in Windows Server 8” since we have a full set of Hyper-V cmdlets there. But then I sort of remembered doing something like this before, and hunted around and found this old TechNet Wiki article I wrote over a year ago. It wasn’t a full fledged script, but had all the pieces I needed to put together a simple script to stop all the VMs on the local Hyper-V host:

# This is a simple script to stop all the currently running VMs on the local
# Hyper-V host. It could easily be extended to accept a command line
# argument of the name of a remote yper-V hosts or a list of hosts into an array

$VMs = Get-WmiObject MSVM_ComputerSystem -computer "." -namespace "root\virtualization"
ForEach ($vm in $VMs) {
   if ( $vm.name -ne $vm.elementname ) {   # skip the parent's name
      if ( $vm.EnabledState -eq 2 ) {      # If the VM is running
         $shutdown = Get-WmiObject MSVM_ComputerSystem -namespace "root\virtualization" –query “Associators of {$vm} where ResultClass=Msvm_ShutdownComponent”

         $shutdown.iniateShutdown($true,”System Maintenance”)

         sleep 5
      }
   }
}

So, what’s happening in that script? Well, Get-WmiObject grags a list of all the VMs on the local Hyper-V Host (-computer “.”), then we simply loop through the list (skipping host itself ($vm.name -ne $vm.elementname), and for each VM that is running ($vm.EnabledState -eq 2), we get a shutdown object for that specific VM and then call the initiateShutdown method on that object. 

 

Note that this is a “forced shutdown”, so is equivalent to “shutdown –s –f” at the command line. Some processes may not get politely shutdown. Too bad, so sad. Since we need this to work regardless of what else is happening, that’s a necessary risk.

Charlie.

Uninstalling Windows Server 2008 R2 SP1 Beta and RC

Before you can install Windows Server 2008 R2 SP1 (or Windows 7 SP1), you need to uninstall the beta or RC version that might be installed. On a GUI install of Windows Server, no problem, run AppWiz.cpl and click on Updates. But on Hyper-V Server 2008 R2 (or Server Core) there is no obvious way to uninstall the beta or RC versions of the Service Pack. You need to use the command line version of the Windows Update Installer: wusa.exe.

wusa.exe /uninstall /kb:976932

It’ll take a couple of reboots before it’s completely uninstalled, and then you can install the SP1 you just downloaded.

Update: Note, if it wasn't clear. This command line works equally well from GUI installs, including Windows 7.

PowerShell, Hyper-V and WMI

I’ve started an article over on the PowerShell Survival Guide Wiki to drop in quick hits how to do “stuff” with Hyper-V, using PowerShell and the native WMI interface of Hyper-V. The WMI namespace for Hyper-V is  “root\virtualization”. Turns out managing Hyper-V isn’t as hard as I thought, at least in no small part because working with WMI in PowerShell is actually pretty straightforward. I’m still learning and poking around, but this stuff is all over the net if your bingle skills are good. Today I added the simple steps needed to create a VHD, either dynamic or fixed, to the Wiki page. Any one who wants to join in is more than welcome – that’s what a Wiki is all about after all.

Charlie.