# Lability ❖ The ultimate Hyper-V lab tool (Powershell DSC)

After a somewhat lengthy haitus from Windows as my primary OS, I decided to trade in my Macbook Pro for a Windows laptop with a bit more oomph. My plan was to have something sturdy enough to handle whatever I could throw at it day-to-day, as well as being able to run a portable lab in times when I need to spin up VMs. Naturally, when I saw Lability mentioned in the Powershell slack, I knew I’d hit the jackpot.

Lability is a PowerShell module that gives us the ability to automate the provision of a full Hyper-V lab environments, with configuration written in DSC.

Excited? Me too.

I'll try and explain the technologies to the best of my ability, but you may need a basic understanding of DSC to completely follow.

## Why would we want this?

Creating test environments should be simple, quick and repeatable.

The aim is to drop into a running environment, test what we need to, and get out. Having to set up the environment shouldn’t be the thing that gets in the way… that’s what comes afterwards!

Lability simplifies this by giving us the ability to write lab configurations as code, and from here, we can create reproducable environments that can fired up from scratch in the matter of minutes. We can check these into source control, iterate and improve on them and share them. But most importantly, we can re-use them time and time again.

## How does it work?

If you want the word straight from the horse’s mouth, then the Readme on Github does a great job explaining what Lability does & how it works. I also recommend watching the European PowerShell Summit 2015 introduction if you have a few spare minutes.

But since you’re already here, I’ll break down the process Lability takes to the best of my knowledge. First things first, what are the different working parts to Lability?

## Starting with a config file

The bare minimum we need to spin up a lab is a .psd1 file that defines each VM we want to provision, defined as a ‘node’. Typically with DSC, each node we define is a server we’d be creating .MOF file for - Lability piggybacks off this idea but does things a little differently.

Within each node that we define, we specify the VM’s configuration - how much memory to allocate, the virtual processor count, which virtual switch to use, etc. - using Lability’s own properties. As per the help files, we can see properties such as Lability_StartupMemory and Lability_Media.

The following is a very basic config:

@{
AllNodes = @(
@{
NodeName = '*'
Lability_StartupMemory = 2gb
}

@{
NodeName = 'VM01'
Lability_ProcessorCount = 2
}

@{
NodeName = 'VM02'
Lability_SwitchName = 'LAB-External'
}
)
}


This would provision 2 VMs with the folllowing:

• VM01 would have 2 vprocessors assigned
• VM02 would have 2gb startup memory assigned
• And both VMs will be connected connected to the LAB-External switch.

As you can see, the syntax is just a typical PowerShell data file (.psd1) that is used for DSC but we use some of Lability’s DSL. Any missing settings that are needed will be taken from the Lab defaults which we’ll cover shortly.

## Configuring the environment outside of the VMs

When we defined the our VMs, we also connected them to the LAB-External virtual switch. Well, we don’t want to create that manually, so we have to define it within the NonNodeData hashtable.

@{
AllNodes = @(
@{
NodeName = '*'
Lability_SwitchName = 'LAB-External'
}

@{
NodeName = 'VM01'
Lability_ProcessorCount = 2
}

@{
NodeName = 'VM02'
Lability_StartupMemory = 2gb
}
)

NonNodeData = @{

Lability = @{
# Prefix all of our VMs with 'LAB-' in Hyper-V
EnvironmentPrefix         = 'LAB-'

Network = @(
@{
Name              = 'LAB-External'
Type              = 'External'
AllowManagementOS = $true } ) } } }  Also see: Get-Help about_ConfigurationData. ## Putting our VMs to work To take this a step further, we don’t just want to spin up dumb VMs (although, this is sometimes handy), we want VMs that are configured to perform their defined role. To do this, we will also provide a .MOF file, generated by DSC, for each of our nodes and place it into our ‘configurations’ folder (C:\Lability\Configurations by default). When the VMs are first booted, Lability injects this configuration into the VM and DSC works its magic to ‘make it so’. Again, as is the norm when writing DSC configurations, the .psd1 file is our configuration data file that we use to separate our environment data from our configuration data (See this or this for a better run-down on DSC configuration data). This means that we can define any number of properties relating to each node and then reference them within our configuration. Well, we already have a .psd1 file that we’re defining the VM attributes in, so we can also define the VM’s roles in this! So, to recap, Lability will consume the .psd1 file and spin up a VM for all of the entries in our AllNodes array, cherrypicking Lability-specific properties (Eg. Lability_SwitchName) to define aspects of how the VM is configured. It is still up to us to generate a .MOF for each node (Ideally feeding in the same .psd1 file as configuration data), which Lability will use to configure the OS of the VM. ## Media & Resources As I briefly mentioned, Lability uses Media as the basis of the VM (Lability_Media property). This is usually an ISO or wim file for Windows VMs, or a preconfigured VHD for Linux VMs. Resources on the other hand are files/folders we can inject into the VM, ready to be utilised by DSC, For example, we can define a resource item for the SQL Server 2012 install media (An ISO file), and then use DSC to install a SQL Server 2012 instance from this. Both Media and Resources can be supplied directly on the filesystem, or we can supply a UNC path/URL to download the file from. This means that, even if we don’t have some media required for a VM to begin with, Lability can automatically download it for us prior to setting up our VMs. At the time of writing, Lability comes with 24 items of Windows media baked in (Ready to download on first use), ranging from Windows 8.1 to Server 2016 Technical Preview 5! That should be more than plenty to get us started. Here I have updated our earlier example configuration, and you can see that I’ve told VM01 to use media 2012R2_x64_Standard_Core_EN_Eval and VM02 to use WIN81_x64_Enterprise_EN_V5_Eval. This is leading us nicely into creating a simple server-client lab. @{ AllNodes = @( @{ NodeName = '*' Lability_SwitchName = 'LAB-External' } @{ NodeName = 'VM01' Lability_ProcessorCount = 2 Lability_Media = '2012R2_x64_Standard_EN_V5_Eval' } @{ NodeName = 'VM02' Lability_StartupMemory = 2gb Lability_Media = 'WIN81_x64_Enterprise_EN_V5_Eval' } ) NonNodeData = @{ Lability = @{ # Prefix all of our VMs with 'LAB-' in Hyper-V EnvironmentPrefix = 'LAB-' Network = @( @{ Name = 'LAB-External' Type = 'External' NetadapterName = 'Ethernet' AllowManagementOS =$true
}
)
}

}
}


Also see: Get-Help about_Media, Get-Help about_CustomResources, Register-LabMedia.

## VHDs & differencing disks

So, now that we know our lab VMs are based upon ‘media’, how do we go from nothing to having the underlying VHDs created for our VMs?

In a normal scenario, you might create a VHD, boot the VM from an ISO or PXE server, and install the OS onto the VHD. Or, you might create a single VM, sysprep it and copy that VM and its VHDs. Repeat this process for each VM and you’ll soon realise that this requires a lot of storage space and a lot of time.

To make this process more efficient, Lability starts by creating a ‘master’ VHD the first time we reference a specific piece of media. This is our base image that includes only the bare essentials (eg. recommended hotfixes, WMF 5.0 if not natively installed) and any VMs that reference this piece of media will create a differencing disk from the master.

The differencing disks store only the changes from the master VHD, so instead of having 10 VMs, each with an operating system VHD attached at around 20GB (99% of which may be exactly the same on each VM), each disk will only be a few GB and simply refers back to the master VHD as its ‘base’. This also means that instead of spinning up a VM, adding a VHD and then installing the OS onto this disk - which is around a 20 minute process on average - we just create a differencing disk from our existing master image and we’re up and running in a matter of mere minutes.

To recap, when you try to spin up a VM using media you’ve not used before…

• The ‘master’ VM will be created.
• The OS will be installed in an unattended fashion (autounattend.xml injected).
• The VM will be sysprepped and saved wherever we pointed our MasterVirtualHardDisks folder.
• The ‘master’ VM will be removed (We only need the VHD after all).
• The differencing disk will be created for the VM(s) we requested.
• For any subsequent VMs that use the same media, it will only have to create a differencing disk from the existing master VHD.

An aside on differencing disks
Whenever differencing disks are used, we have to ensure the master VHD does not change location/get deleted and the contents don't get altered. Altering the master VHD will corrupt all differencing disks in the disk chain, so they are usually marked as Read Only.

In our case, it's not the end of the world if our differencing disks did get corrupted as we have all of our config saved, ready to rapidly spin them back up again!

# Getting started

Okay, enough of how it works and let’s get it working!

First of all, install the module by either downloading it from Github and moving it into one of your $env:PSModulePath directories, or if you’re using WMF 5.0, Install-Module -Name 'Lability'. As with all good modules, a good place to start is with the help files - Get-Help about_Lab and Get-Command -Module 'Lability'. ### Lab defaults I’ve been mentioning folders that Lability uses, and some of the default settings. Well, let’s inspect these default settings and tweak accordingly. # See what the default settings are Get-LabHostDefaults Get-LabVMDefaults  As you can see, there’s a bunch of options we can tweak if needed. Let’s change these to suit us. # This only has to be done once as the settings will be saved to disk, as # C:\ProgramData\Lability\Config\CustomMedia.json and C:\ProgramData\Lability\Config\VmDefaults.json # (Some of these are the default values already) # General Lability settings.$LabHostDefaults = @{
# You may have to manually create these directories ahead of time

# Where our .MOF files willbe taken from
ConfigurationPath       = 'C:\Lability\Configurations'
# Where to create our differencing disks and snapshots are stored
DifferencingVhdPath     = 'C:\Lability\VMVirtualHardDisks'
# Any hotfixes we wish to cache and install on our VMs
HotfixPath              = 'C:\Lability\Hotfixes'
# Where the ISOs for our media are stored/taken from
IsoPath                 = 'C:\Lability\ISOs'
# Where we store the 'master' VHDs
ParentVhdPath           = 'C:\Lability\MasterVirtualHardDisks'
# Where we store any resources we want to inject into our VHDs
ResourcePath            = 'C:\Lability\Resources'

ResourceShareName       = 'Resources'
DisableLocalFileCaching = 'False'
EnableCallStackLogging  = 'False'
}

# Default settings for VMs, if not explicitly specified.
$LabVMDefaults = @{ # The amount of memory allocated on VM startup StartupMemory = 2gb # Minimum memory (Using dynamic memory) MinimumMemory = 2gb # Max memory (Using dynamic memory) MaximumMemory = 3.5gb # Amount of virtual processor cores to allocate ProcessorCount = 2 # Which Hyper-V switch(es) to assign SwitchName = 'LAB-External' # The default media to create the VM from Media = '2012R2_x64_Datacenter_EN_V5_Eval' # Settings to inject into the VM's autounattend.xml file, which # will be use to configure the base OS TimeZone = 'UTC' UILanguage = 'en-US' SystemLocale = 'en-GB' InputLocale = '0809:00000809' UserLocale = 'en-GB' RegisteredOwner = 'Lability' RegisteredOrganization = 'Virtual Engine' # Certificate to use for DSC communication. # Lability comes with default certificates, but obviously this makes it insecure... # however, this is a **LAB** environment so security is of much less importance, # as we shouldn't be using production systems & credentials ClientCertificatePath = 'C:\ProgramData\Lability\Certificates\LabClient.pfx' RootCertificatePath = 'C:\ProgramData\Lability\Certificates\LabRoot.cer' # Boot delay in seconds BootDelay = '0' # Custom boot order CustomBootstrapOrder = 'MediaFirst' # Toggle secure boot SecureBoot = 'True' # Install Hyper-V guest integration services GuestIntegrationServices = 'True' } Set-LabHostDefaults @LabHostDefaults Set-LabVMDefaults @LabVMDefaults  ### Creating our lab We’ve already built the basis of our lab - a Windows Server 2012 R2 VM and a Windows 8.1 client VM. Well, let’s shift gears a little and make our server a domain controller for a domain and join the client to it. # Data.psd1 @{ AllNodes = @( # All nodes @{ NodeName = '*' DomainName = 'Lab.local' # Networking Lability_SwitchName = 'LAB-Private' DefaultGateway = '10.0.0.254' SubnetMask = 24 AddressFamily = 'IPv4' DnsServerAddress = '10.0.0.1' DnsConnectionSuffix = 'Lab.local' # DSC related PSDscAllowPlainTextPassword =$true
PSDscAllowDomainUser      = $true # Removes 'It is not recommended to use domain credential for node X' messages } # DC01 @{ # Basic details NodeName = 'DC01' Lability_ProcessorCount = 2 Role = 'DC' Lability_Media = '2012R2_x64_Standard_EN_V5_Eval' # Networking IPAddress = '10.0.0.1' DnsServerAddress = '127.0.0.1' # Lability extras Lability_CustomBootstrap = @' '@ } # CLIENT01 @{ # Basic details NodeName = 'CLIENT01' Lability_StartupMemory = 2gb Role = 'Client' Media = 'WIN81_x64_Enterprise_EN_V5_Eval' # Lability extras Lability_CustomBootStrap = @' net user Administrator /active:yes ## Enable local administrator account Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope LocalMachine -Force Enable-PSRemoting -SkipNetworkProfileCheck -Force '@ } ) NonNodeData = @{ OrganisationName = 'Lab' Lability = @{ # Prefix all of our VMs with 'LAB-' in Hyper-V EnvironmentPrefix = 'LAB-' Network = @( @{ Name = 'LAB-External' Type = 'External' NetadapterName = 'Ethernet' AllowManagementOS =$true
}
)

DSCResource = @(
@{ Name = 'xComputerManagement'; MinimumVersion = '1.3.0.0'; Provider = 'PSGallery' }
@{ Name = 'xNetworking'; MinimumVersion = '2.7.0.0' }
@{ Name = 'xActiveDirectory'; MinimumVersion = '2.9.0.0' }
@{ Name = 'xDnsServer'; MinimumVersion = '1.5.0.0' }
@{ Name = 'xDhcpServer'; MinimumVersion = '1.3.0.0' }
)

Media = @()

}
}
}


As you can see, I have fleshed out the config data file using pieces from the ‘examples’ bundled with Lability, and renamed the nodes as appropriate. Each node has new properties which will be referenced in our .ps1 file, as well as properties that Lability will use.

For example, each node now has a role. This is how we will define what DSC resources are applied to each VM. This is handy, as it means we can simply add more nodes of the same role, and they’ll get the appropriate config.

Each node also has some IP configuration (IPaddress, DnsServerAddress) which we will use in our DSC resources to set the VM’s networking config.

As well as this, we’ve added another Lability-specific property Lability-CustomBootStrap. Here, we can add commands that will be run on first boot.

And finally, we’ve added the DSCResource array in the NonNodeData hashtable. This defines what DSC resources are needed on the host machine and will download them automatically. This means that, as long as you have Lability installed and a working config file, you should be able to spin up any VM and all of the necessary pieces will be pulled down automatically.

Moving on to our Config.ps1 file that will generate the MOFs.

# Config.ps1

# Stop the script if a fatal error occurs
$ErrorActionPreference = 'Stop' Configuration BasicServerClient { param ( [ValidateNotNull()] [PSCredential]$Credential = (Get-Credential -Credential 'Administrator')
)

# Remember, these modules are required on the host as that's where the .MOFs are compiled,
# and the modules are also copied across to our VMs as that's where they are applied
Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName xComputerManagement, xNetworking, xActiveDirectory
Import-DscResource -ModuleName xDHCPServer, xDnsServer

#
# ALL nodes
#
Write-Verbose 'Processing: All nodes'
Node $AllNodes.Where({$true }).NodeName {

Write-Verbose "Processing:   $($node.NodeName)"

# LCM settings
LocalConfigurationManager {
RebootNodeIfNeeded   = $true AllowModuleOverwrite =$true
ConfigurationMode    = 'ApplyOnly'
#CertificateID       = $node.Thumbprint } # # Networking # # If an IP address was defined in our config file, set the adapter's IP address if ($node.IPAddress) {
IPAddress      = $node.IPAddress InterfaceAlias = 'Ethernet' SubnetMask =$node.SubnetMask
AddressFamily  = $node.AddressFamily } } # If a default gateway was defined in our config file, set it if ($node.DefaultGateway) {
InterfaceAlias = 'Ethernet'
Address        = $node.DefaultGateway AddressFamily =$node.AddressFamily
}
}

# If a DNS server was defined in our config file, set it
if ($node.DnsServerAddress) { xDnsServerAddress 'PrimaryDNSClient' { Address =$node.DnsServerAddress
InterfaceAlias = 'Ethernet'
AddressFamily  = $node.AddressFamily } } # If a DCS connection suffix was defined in our config file, set it if ($node.DnsConnectionSuffix) {
xDnsConnectionSuffix 'PrimaryConnectionSuffix' {
InterfaceAlias           = 'Ethernet'
ConnectionSpecificSuffix = $node.DnsConnectionSuffix } } } #end nodes ALL # # DC nodes # Write-Verbose "Processing: DC nodes" Node$AllNodes.Where({ $_.Role -eq 'DC' }).NodeName { Write-Verbose "Processing:$($node.NodeName)" # # Roles # # Add the following roles ForEach ($Feature in @(
'GPMC',
'DHCP',
'RSAT-DHCP'
)) {
WindowsFeature $Feature.Replace('-','') { Ensure = 'Present' Name =$Feature
IncludeAllSubFeature = $true } } # # Active Directory # # Create the AD domain xADDomain 'ADDomain' { DomainName =$node.DomainName
SafemodeAdministratorPassword = $Credential DomainAdministratorCredential =$Credential
}

# Convert the domain name to the distinguished name
$DomainDN = ('DC=' +$($node.DomainName).Replace('.',',DC='))$BaseOU   = "OU=$($ConfigurationData.NonNodeData.OrganisationName),$($DomainDN)"

# Create an OU with the company's 'Organisation name'
Name = $ConfigurationData.NonNodeData.OrganisationName Path =$DomainDN
}

# Create a 'Lab Users' OU under the base OU
Name = 'Lab Users'
Path = $BaseOU } # Create a 'Lab Computers' OU under the base OU xADOrganizationalUnit 'OU_LabComputers' { Name = 'Lab Computers' Path =$BaseOU
}

# Create a generic domain user called 'LabUser1'
DomainName  = $node.DomainName UserName = 'LabUser1' Description = 'Lab User 1' Path = "OU=Lab Users,$BaseOU"
Password    = $Credential Ensure = 'Present' DependsOn = '[xADDomain]ADDomain' } # DHCP server xDhcpServerAuthorization 'DhcpServerAuthorization' { Ensure = 'Present' DependsOn = '[WindowsFeature]DHCP','[xADDomain]ADDomain' } # Create a DCHP scope from 10.0.0.100 - 10.0.0.200 xDhcpServerScope 'DhcpScope10_0_0_0' { Name = 'Lab Clients' IPStartRange = '10.0.0.100' IPEndRange = '10.0.0.200' SubnetMask = '255.255.255.0' LeaseDuration = '00:08:00' State = 'Active' AddressFamily = 'IPv4' DependsOn = '[WindowsFeature]DHCP' } # Add the 'Router' option to the DHCP scope, which defines the default gateway xDhcpServerOption 'DhcpScope10_0_0_0_Option' { ScopeID = '10.0.0.0' DnsDomain =$node.DomainName
Router             = '10.0.0.254'
DependsOn          = '[xDhcpServerScope]DhcpScope10_0_0_0'
}

} #end nodes DC

#
# Client nodes
#
Write-Verbose "Processing: Client nodes"
Node $AllNodes.Where({$_.Role -eq 'Client' }).NodeName {
Write-Verbose "Processing:   $($node.NodeName)"

# Flip credential into username@domain.com (For domain joining)
$DomainCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ("$($Credential.UserName)@$($node.DomainName)",$Credential.Password)

# Convert the domain name to the distinguished name
$DomainDN = ('DC=' +$($node.DomainName).Replace('.',',DC='))$BaseOU   = "OU=$($ConfigurationData.NonNodeData.OrganisationName),$($DomainDN)"

# Enable DCHP on client node so that it picks up an IP address from our DC node
xDhcpClient EnableDhcpClient {
State          = 'Enabled'
InterfaceAlias = 'Ethernet'
AddressFamily  = $node.AddressFamily } # Join the domain we created on our DC xComputer 'JoinDomain' { Name =$node.NodeName
DomainName = $node.DomainName JoinOU = "OU=Lab Computers,$BaseOU"
# => OU=Lab Computers,DC=Lab,DC=Local
Credential = $DomainCredential } } } # Use the Data.psd1 in the same folder as this script$ConfigData = "$(Split-Path$MyInvocation.MyCommand.Path)\Data.psd1"

# Create a new credential that we'll pass to BasicServerClient (Required when creating the domain & joining the domain) and to Start-LabConfiguration
$AdministratorCredential = [pscredential]::new('Administrator', ('Password1' | ConvertTo-SecureString -AsPlainText -Force)) # Generate the .MOF files that will be injected into our VMs and used to set them up Write-Host 'Generating MOFs' -ForegroundColor Green BasicServerClient -ConfigurationData$ConfigData -OutputPath 'C:\Lability\Configurations' -Credential $AdministratorCredential -Verbose # Verify lab configuration & see what parts of it already exist (if any) Write-Host 'Verifying lab configuration' -ForegroundColor Green Test-LabConfiguration -ConfigurationData$ConfigData  -Verbose

# Create the lab from our config
Write-Host 'Creating lab' -ForegroundColor Green
Start-LabConfiguration -ConfigurationData $ConfigData -Verbose -IgnorePendingReboot -Credential$AdministratorCredential

# And once it's created, start the lab environment
Write-Host 'Starting lab!' -ForegroundColor Green
Start-Lab -ConfigurationData \$ConfigData -Verbose


Let’s break down which resources are applying to each node.

All nodes

• Set the LCM settings

DC nodes (Just DC01 in our case)

• Create a base OU to keep things in
• Create an OU called LabUsers
• Create an OU called LabComputers
• Create a user called User1 (Placed into LabUsers OU)
• Authorize the DHCP server in AD
• Create a DHCP scope (10.0.0.100 - 10.0.0.200)
• Add the ‘router’ option to our DHCP scope (Default gateway)

Client nodes (Just CLIENT01 in our case)

• Enable DHCP on the ethernet adapter
• Join the newly created domain (Placing CLIENT01 into the LabComputers OU)

So, our next step is to simply run the Powershell script (ensuring the .psd1` file is in the same directory), pass it the credentials and Lability do its thing.

And as they say, the proof in the pudding is in the eating.

## Next steps

There are some great, fully-fledged examples under the ‘Examples’ folder of the module (In Github or under your local copy), so I suggest poking around and adapting them to your needs. Once you have a basic config, it’s super easy to start expanding upon.

Also, check out the amazingly comprehensive post by @kilasuit at Building a lab using Hyper-V and Lability - The end to end example.

I hope to have some more posts about Lability coming up, and I’ll be storing some of my lab configs in my GitHub.

## Thanks

Just as an extra note, I want to thank the maintainers of Lability - @iainbrighton and @kilasuit - for actively working on and providing such an amazing piece of kit. I’ve personally contacted @ianbrighton numerous times via. the Powershell slack, and he’s been happy to help and even push fixes there-and-then.