Automating my way out of ancillary tasks with PowerShell

After creating a handful of virtual machines for ramping up on the different Linux distros, certain configuration patterns started to emerge. Repeating those same settings over and over again in Hyper-V Manager or even PowerShell’s command line interface is kind of tedious. Good thing is scripts are a great way to automate those repetitive configuration tasks away.

In my case, the settings I kept copying over and over again were:

  • Two cores;
  • Network adapter bound to a virtual switch with external connectivity;
  • Automatic Checkpoints turned off;

And then there were other settings that although varied between distros and installation options within those distros, had to be configured each and every time:

  • Virtual machine name;
  • RAM size;
  • Hard drive size;
  • Path to installation ISO;
  • Secure Boot;

So after learning and getting comfortable with the different PowerShell cmdlets, I started prototyping a few scripts. At first, the scripts were kind of lame and buggy, but as I learned more about the features of Hyper-V and how those features are exposed through PowerShell, the scripts were improved little by little much in the spirit of Kaizen – continuous improvement.

The important thing here is realizing this as a learning tool and as such starting small and improving as you go and as new requirements make themselves known.

Important as well is knowing when to stop. The purpose of this exercise isn’t creating a production-ready script, but to create a script that will allow me to get back as soon as possible to what I settled out to do in the first place: Create virtual machines so I could learn something else.

If you are curious enough, you can find the source code for one of the early versions of the scripts as a gist over on GitHub. If you are even more curious, you can see the entire history of how the code evolved also on GitHub.

Since that early prototype, the code has evolved quite a bit from a collection of discrete scripts to the current version which is implemented as a PowerShell module and incorporates all the learnings of the last few weeks – including a best practice for creating virtual hard disks for use with Linux file systems. Good luck trying remember that one every time you create a new VM for Linux!

Certainly there are a lot of opportunities for improvement (documentation, error handling and resilience in general just to name a few), but those are left as an exercise for future self. Meanwhile, let me get back to playing with those VMs.

My first steps on managing Hyper-V through PowerShell

I’ve dodged Linux for way too long, but let’s face it: Linux’s kernel and it’s derivatives have won the war over Windows everywhere but the desktop (and laptop).

And while I have been dabbling with Ubuntu over WSL for over a year now, the reality is the current version of WSL has some serious limitations. Good thing is WSL 2 is on it’s way with support for a “real” Linux kernel in it.

Unfortunately, WSL 2 is still in preview so a couple of months ago when I’ve decided to up my game on Linux and it’s different distros, I choose to do so running VMs on top of Windows 10 1809’s Hyper-V.

So I’m back to fiddling with virtual machines after several years of basically ignoring them. Back then, I had a quite elaborate setup that allowed me to spin up multiple VMs at once on a laptop. I remember using differential disks heavily to conserve disk space on the host’s hard drive. For some reason I really don’t recall now, the VM’s configuration files and VHDs where stored at non-default locations. Maybe they were being copied to different hosts, which had different defaults.

Well, my current usage of virtual machines doesn’t justify the use of differential disks and I’m not moving those VMs around so, as a long time K.I.S.S. proponent, I’m sticking to the defaults for the time being.

During the first couple of days playing around, I had setup a handful of VMs using Hyper-V Manager, but that is kind of tedious and error-prone, so again, in the spirit of “Always Be Automating”, I started using PowerShell where possible so I could learn the commands and eventually codify the tasks involved in a script.

So here are the guest virtual machines I had setup:

PS C:\WINDOWS\system32> Get-VM | Select-Object Name
 Name
 CentoOS20190725
 CentOS
 Suse
 Ubuntu
 Ubuntu1804
 Windows 10 dev environment

Unfortunately, all those virtual machines came at the cost of helping to exhaust the free space on the host’s local hard drive.

PS C:\Users> Get-PSDrive -PSProvider FileSystem
 Name           Used (GB)     Free (GB) Provider      Root
 ----           ---------     --------- --------      ----
 C                 879.83         36.93 FileSystem    C:\
 D                                      FileSystem    D:\
 E                                      FileSystem    E:\

To get a sense of how much space those VMs are taking, let’s take a look at the disks being used by them.

Just so I can save some typing, I’m going to be using some variables here and there.

First, I get the path to the directory where the virtual hard drives are located.

$vhdPath = (Get-VMHost).VirtualHardDiskPath

Then I get a collection of objects representing the files contained in there. Since I’m only interested in the files names and their sizes, I’ll be leaving out the other properties.

$files = Get-ChildItem $vhdPath | Select-Object Name, Length | Sort-Object Length -Descending
Contents of the files variable
Get-ChildItem $vhdPath | Select-Object Name, Length | Sort-Object Length -Descending

There are a handful of relatively smaller files containing GUIDs in their names. Those files are Hyper-V checkpoints and enable us to go back to the point in time when those snapshots were made.

Here’s how we list the checkpoints available on the current host.

Get-VM | Get-VMSnapshot | Select-Object VMName, Name
List of checkpoints for the virtual machines contained on current Hyper-V host
Get-VM | Get-VMSnapshot | Select-Object VMName, Name

Since I really don’t need those checkpoints right now, and they are making it hard to see what’s going on, I’ll be removing and merging them into their main VHDs.

But before that, let’s take note of the total number of files and how much disk space they consume.

PS C:\WINDOWS\system32> $baseline = $files | Measure-Object -Sum Length | Select-Object Count, Sum
 PS C:\WINDOWS\system32> $baseline
 Count         Sum
 -----         ---
    13 86536880128

To remove and merge the checkpoints of each VM:

Get-VM | Remove-VMSnapshot

An important note: Remove-VMSnapshot seems to be executed asynchronously and will return before removing the file so you want to be careful if you’re immediately subsequently issuing commands that depend on those files being deleted.

To check out the result of removing those checkpoints, we basically repeat the commands issued previously.

PS C:\WINDOWS\system32> $files = Get-ChildItem $vhdPath | Select-Object Name, Length | Sort-Object Length -Descending
 PS C:\WINDOWS\system32> $files
 Name                                 Length
 ----                                 ------
 Windows 10 dev environment.vhdx 39195770880
 Ubuntu 18.04.1 LTS (1).vhdx     10661920768
 New Virtual Machine (2).vhdx     8728346624
 New Virtual Machine.vhdx         5943328768
 CentoOS20190725.vhdx             5540675584
 Ubuntu 18.04.1 LTS.vhdx          4756340736
 Ubuntu Server 18.04.vhdx         3762290688
 New Virtual Machine (1).vhdx        4194304
 PS C:\WINDOWS\system32> $current = $files | Measure-Object -Sum Length | Select-Object Count, Sum
 PS C:\WINDOWS\system32> $current
 Count         Sum
 -----         ---
     8 78592868352
PS C:\WINDOWS\system32> $baseline.Count - $current.Count
 5
PS C:\WINDOWS\system32> $baseline.Sum - $current.Sum
 7944011776

As can be seen, five checkpoints were removed resulting in saving a little over 7GB. It isn’t that much, but at least looking at the remaining files, it’s easier to see that there are two more virtual disks than virtual machines. Given those virtual machines are configured with only one virtual hard disk each, there are two orphaned virtual hard drives that should probably be deleted.

As I’m sticking to Hyper-V’s defaults and checkpoints already have been removed, deleting those orphaned virtual disks is quite easy: First you get a list containing each virtual disk attached to a virtual machine. Then you enumerate the files in the host’s default virtual hard disk directory and remove those that aren’t on the list.

 PS C:\WINDOWS\system32> $rootedVHDs = (Get-VM).HardDrives.Path

 PS C:\WINDOWS\system32> (Get-VMHost).VirtualHardDiskPath | Where-Object { $_.FullName -notin $rootedVHDs } | Remove-Item