Create-TestVMs.ps1


<#PSScriptInfo
 
.VERSION 2.3
 
.GUID f35cd072-b739-4542-8fbf-3976b8daa444
 
.AUTHOR Jeff Gilbert (@JeffGilb)
 
.COMPANYNAME Microsoft
 
.COPYRIGHT
 
.TAGS
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
Version 1.0: Original published version.
Version 1.1: Minor bug fixes.
Version 1.2: Minor bug fixes.
Version 1.3: Minor bug fixes.
Version 2.0: Added version to PowerShell console title.
             Added capability to accept or change Hyper-V host save locations.
             Set file and folder browse dialogs to always open on top.
             Added final summary confirmation prompt before creating VMs with options to exit.
             Added a virtual network configuration selector dialog. No virtual network selected by default.
             Added differencing disk support:
             - Allows selection of a master (sysprepped) VHDX to be used as the master disk for new VMs
             - Creates an OOBE snapshot while turned off for VMs using differencing disks
             - Creates a master directory with a copy of the selected VHDX.
             - Creates a readme file in the master disk directory listing VMs using the master disk
             - Puts the master VHDX and SN in the VM notes in Hyper-V
Version 2.1: Bug fix to address concatonating paths and virtual network names.
Version 2.2: Fixed network switch assignment issue.
Version 2.2: Fixed location for non-differencing disk hard disk files to be stored.
   
.DESCRIPTION
 Script to automate the creation of test Hyper-V VMs.
#>


#################################### DEFAULT PARAMETERS #######################################

# Edit these to set defaults for the script to use:

param(
    [Parameter(Mandatory=$False)] [String] $VMPath = "C:\ProgramData\Microsoft\Windows\Hyper-V\",
    [Parameter(Mandatory=$False)] [String] $HDPath = "C:\Users\Public\Documents\Hyper-V\Virtual Hard Disks\",
    [Parameter(Mandatory=$False)] [String] $vhdx = "",
    [Parameter(Mandatory=$False)] [Int]    $RAM = 2, # 2GB = 2*1073741824
    [Parameter(Mandatory=$False)] [String] $VMSwitch = "Default Switch",
    [Parameter(Mandatory=$False)] [String] $ISO = "",
    [Parameter(Mandatory=$False)] [String] $VMPrefix = "Autopilot",
    [Parameter(Mandatory=$False)] [String] $VMnumber = "1"
)

#region ############################## FOLDER AND FILE BROWSER FUNCTIONS ###############################

Function Get-Folder 
{

    Add-Type -AssemblyName System.Windows.Forms
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")|Out-Null
    $folderbrowser = New-Object System.Windows.Forms.FolderBrowserDialog -Property @{
        description = "Select a folder"
        rootfolder = "Desktop"
        SelectedPath = $initialDirectory
        }
    $result = $folderbrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true }))
    if ($result -eq [Windows.Forms.DialogResult]::OK){
        $folder = $FolderBrowser.SelectedPath
    }
    else {
        return
    }
    return $folder
}    

Function Get-File
{

    Add-Type -AssemblyName System.Windows.Forms
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")|Out-Null
    $fileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
        Title = "Select the ISO file to mount."
        InitialDirectory = [Environment]::GetFolderPath('Desktop')
        Multiselect = $false # Multiple files cannot be chosen
        Filter = 'ISO Files (*.ISO)|*.ISO' # Specified file types
        }
    $result = $fileBrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true }))
    if ($result -eq [Windows.Forms.DialogResult]::OK){
        $file = $fileBrowser.SelectedPath
    }
    else {
        return
    }
    
    $file = $fileBrowser.FileName;
    return $file
}

Function Get-VHDX
{

    Add-Type -AssemblyName System.Windows.Forms
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")|Out-Null
    $fileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property @{
        Title = "Select the parent VHDX to use when creating differencing disks."
        InitialDirectory = [Environment]::GetFolderPath('Desktop')
        Multiselect = $false # Multiple files cannot be chosen
        Filter = 'VHDX Files (*.VHDX)|*.VHDX' # Specified file types
        }
    $result = $fileBrowser.ShowDialog((New-Object System.Windows.Forms.Form -Property @{TopMost = $true }))
    if ($result -eq [Windows.Forms.DialogResult]::OK){
        $vhdx = $fileBrowser.SelectedPath
    }
    else {
        return
    }
    
    $vhdx = $fileBrowser.FileName;
    return $vhdx
}

$Version = "Create-TestVMs v2.3"
$host.ui.RawUI.WindowTitle = $Version

#endregion ********************************************************************************************

#region ############################# VIRTUAL MACHINE STORAGE LOCATIONS ###############################

CLS 
write-host `n
write-host " Virtual Machine Storage Locations "`n -ForegroundColor White -BackgroundColor Black 

$vmpath = ""
$vmpath = get-vmhost | Select-Object -Property VirtualMachinePath
$vmpath = """?($vmpath)"""
$vmpath = $vmpath.remove(0,24)
$vmpath = $vmpath.trimStart("=")
$vmpath = $vmpath.trimend("\})""")

$hdpath = ""
$hdpath = get-vmhost | Select-Object -Property VirtualHardDiskPath 
$hdpath = """?($hdpath)"""
$hdpath = $hdpath.remove(0,24)
$hdpath = $hdpath.trimStart("=")
$hdpath = $hdpath.trimend("\})""")

write-host "The Hyper-V host machine is set to store VMs at these default locations:`n" 
write-host -ForeGroundColor Green "`tVM configuration files:`t" $vmpath"`n`n`tVM hard disk files:`t $HDpath"`n""

write-host -ForeGroundColor Yellow "Do you want to change the default storage settings?"
$Readhost = Read-Host " [Y] [N] (Default is N)"  
    Switch ($ReadHost){ 
        Y {
           write-host -ForeGroundColor Yellow " Do you want to change the host defaults for where to store VMs?" 
           $VMReadhost = Read-Host " [Y] [N] (Default is N)"
           Switch ($VMReadhost){
             Y {
                $VMfolder = Get-Folder
                If ($VMfolder){$VMPath = $VMfolder}
                write-host -ForegroundColor Green "Virtual Machine path will be: $VMPath.`n"
               }
             N {} # Use default setting.
          } 
                   
           If ($VMReadhost -ne "y"){write-host -ForegroundColor Green "`nDefault VM storage setting will be used.`n"}        
           
           write-host -ForeGroundColor Yellow "Do you want to change the host defaults for where to store VM hard disks?" 
           $HDReadhost = Read-Host " [Y] [N] (Default is N)"
           Switch ($HDReadhost){
             Y {
                $HDfolder = Get-Folder
                If ($HDfolder){$HDPath = $HDfolder}
                write-host -ForegroundColor Green "`nVirtual Machine hard disk path will be: $HDPath.`n"
               }
             N {} # Use default setting.
             }
        If ($HDReadhost -ne "y"){write-host -ForegroundColor Green "`nDefault VM hard disk storage setting will be used.`n"}     
      }
    }
       
    If ($ReadHost -ne "y"){write-host -ForeGroundColor Green "`nHyper-V host default settings will be used.`n"}
        Else{
             write-host "`nThe Hyper-V host machine will be set to store VMs at these default locations:`n" 
             write-host -ForeGroundColor Green "`tVM configuration files:`t" $vmpath"`n`n`tVM hard disk files:`t $HDpath"`n""
            }
#endregion ********************************************************************************************

#region ################################## HARD DISK CONFIGURATION #####################################

write-host " VM Hard Disk Options "`n -ForegroundColor White -BackgroundColor Black 

# Prompt for master VHDX to use for differencing disks
write-host -ForeGroundColor Yellow "Do you have a sysprepped VHDX to use as a master for differencing disks?"
$VHDXReadhost = Read-Host " [Y] [N] (Default is N)"  
    Switch ($VHDXReadHost){ 
        Y { 
            write-host "`nBrowse to and select your sysprepped .VHDX. This will be used as the master image for differencing disks.`n"      
            $vhdx = get-vhdx
            If ($vhdx){
                $nameEnd = get-random -Minimum 100 -Maximum 999 
                write-host -ForegroundColor Green "A copy of "$vhdx" will be used to create the master VHDX image in the Master-"$nameEnd "directory of the VM hard disk storage location.`n"
                $masterVHDX = $HDPath+"\" + "Master-"+$nameEnd
                $masterSource = $vhdx          
            }
            Else {Write-host -ForegroundColor Green "Browse cancelled by user. Differencing disks will not be used.`n";  $vhdx = ""}    
          }
        N {}
     }
     
     If ($VHDXReadhost -ne "y"){write-host -ForegroundColor Yellow "`n"Differencing disks will not be used."`n"}  
     
#endregion ********************************************************************************************
   
#region ###################################### RAM TO ASSIGN ###########################################

write-host " Amount of RAM For New VMs "`n -ForegroundColor White -BackgroundColor Black 

$getName = Get-WmiObject -Class win32_computersystem -ComputerName . | Select-Object -Property Name
$getName = """?($getName)"""
$getName = $getName.trimStart("""?(@{Name=")
$getName = $getName.trimend("})""")

write-host "The default amount of RAM to assign to VMs is" $RAM"GB.`nYour Hyper-V host," $getName", has the following amount of RAM to assign:"
Get-WmiObject -Class win32_computersystem -ComputerName . | fl @{Name="Available memory (GB)";e={[math]::truncate($_.TotalPhysicalMemory /1GB)}}
write-host "Do you want to change the amount of RAM assigned to VMs?" -ForegroundColor Yellow 
$Readhost = Read-Host " [Y] [N] (Default is N)"  
    Switch ($ReadHost) 
     { 
       Y {
         $inputValue = ""
         do {
             $inputValid = [int]::TryParse((Read-Host "`nHow many Gigabytes of RAM do you want to assign to VMs?"), [ref]$inputValue) 
             if (-not $inputValid) {
             Write-Host "Enter a valid number..." -ForegroundColor Red
            }
         } 
         while (-not $inputValid)
         write-host  -ForegroundColor Green "`nVMs will be created with "$inputValue"GB of RAM.`n"
         $RAM = $inputValue
         }     
       N {write-host  -ForegroundColor Green "`n Default selected.`n"}  

   } 
         $RAMAssigned = $RAM * 1073741824 # "Default 2GB = 2 * 1073741824 (2,147,483,648)."
         if ($Readhost){} else{write-host -ForegroundColor Green "`n Default selected. `n"}

#endregion ********************************************************************************************

#region ##################################### NETWORK SWITCH ##########################################

write-host " Virtual Network Switch "`n -ForegroundColor White -BackgroundColor Black 

write-host "`nDo you want to put the VMs on a virtual switch?" -ForegroundColor Yellow 
$Readhost = Read-Host " [Y] [N] (Default is N)"  
    Switch ($ReadHost) 
     { 
       Y {
            Add-Type -AssemblyName System.Windows.Forms
            Add-Type -AssemblyName System.Drawing
            $form = New-Object System.Windows.Forms.Form
            $form.Text = 'Virtual Switch'
            $Form.TopMost    = $true
            $form.Size = New-Object System.Drawing.Size(300,200)
            $form.StartPosition = 'CenterScreen'
            $okButton = New-Object System.Windows.Forms.Button
            $okButton.Location = New-Object System.Drawing.Point(75,120)
            $okButton.Size = New-Object System.Drawing.Size(75,23)
            $okButton.Text = 'OK'
            $okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
            $form.AcceptButton = $okButton
            $form.Controls.Add($okButton)
            $cancelButton = New-Object System.Windows.Forms.Button
            $cancelButton.Location = New-Object System.Drawing.Point(150,120)
            $cancelButton.Size = New-Object System.Drawing.Size(75,23)
            $cancelButton.Text = 'Cancel'
            $cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
            $form.CancelButton = $cancelButton
            $form.Controls.Add($cancelButton)
            $label = New-Object System.Windows.Forms.Label
            $label.Location = New-Object System.Drawing.Point(10,20)
            $label.Size = New-Object System.Drawing.Size(280,20)
            $label.Text = 'Select a virtual switch to use:'
            $form.Controls.Add($label)
            $listBox = New-Object System.Windows.Forms.ListBox
            $listBox.Location = New-Object System.Drawing.Point(10,40)
            $listBox.Size = New-Object System.Drawing.Size(260,20)
            $listBox.Height = 80

            $swi = get-vmswitch | Select-Object -Property Name
            write-host
            ForEach ($item in $swi) {

            $item = """?($item)"""
            $item = $item.Remove(0,10);
            $item = $item.trimEnd("})""")

            [void] $listBox.Items.Add($item)
            } 

            $form.Controls.Add($listBox)
            $form.Topmost = $true
            $result = $form.ShowDialog()
            if ($result -eq [System.Windows.Forms.DialogResult]::OK)
            {
                $x = $listBox.SelectedItem
                if($x){$SwitchToUse = $x;$VMSwitch=$x;write-host -ForegroundColor Green "$VMSwitch will be used." }
                Else{$VMSwitch = "No virtual switch selected.";write-host -ForegroundColor Green $VMSwitch`n}
            }
            $VMSwitch = $SwitchToUse
            if ($result -eq [System.Windows.Forms.DialogResult]::Cancel)    
            {$VMSwitch = "No virtual switch selected.";write-host -ForegroundColor Green $VMSwitch`n}   
        }
       N {}  
     } 
     If ($readhost -ne "y"){
                            $VMSwitch = "No virtual switch selected."
                            write-host -ForeGroundColor Green "$VMSwitch`n" 
                           }

#endregion ********************************************************************************************

#region ######################################### ISO #################################################
write-host "`n" 
write-host " Operating System ISO "`n -ForegroundColor White -BackgroundColor Black  

write-host "`nDo you want to mount an Operating System .ISO?" -ForegroundColor Yellow 
$Readhost = Read-Host " [Y] [N] (Default is N)"  
    Switch ($ReadHost) 
     { 
       Y {
        $file = Get-File
        If ($file){$ISO = $file;write-host -ForegroundColor Green `n"The VM will be set to boot from: $ISO.`n"}
        Else {Write-host -ForegroundColor Green "`nBrowse cancelled by user. No .ISO will be mounted.";  $ISO = "No .ISO selected."}    
        }
       N {}  
     } 
     If ($readhost -ne "y"){
                            $ISO = "No .ISO selected."
                            write-host "`n"
                            write-host -ForegroundColor Green "An .ISO will not be mounted.`n" 
                           }

#endregion ***********************************************************************************************

#region #################################### NAMING PREFIX ############################################

write-host " Virtual Machine Naming Prefix "`n -ForegroundColor White -BackgroundColor Black   

write-host "`nThe VM name prefix will be followed by -999 where 999 is three random numbers.`n"   -ForegroundColor Yellow

$Prefix = read-host "Enter a VM naming prefix (default is $VMPrefix)"
If ($Prefix -ne ""){$VMPrefix = $Prefix}
Write-host -ForegroundColor Green "`nVM names will start with $VMPrefix.`n"

#endregion ***********************************************************************************************

#region ################################ NUMBER OF VMs TO CREATE ######################################

write-host " Number of VMs to Create "`n -ForegroundColor White -BackgroundColor Black 
 
write-host -ForegroundColor Yellow "`nHow many VMs do you want to create?`n"         
    $inputValue = ""
    do {
        $inputValid = [int]::TryParse((Read-Host "Enter the number of VMs to create"), [ref]$inputValue)
        if (-not $inputValid) {
        Write-Host "Enter a valid number..." -ForegroundColor Red
       }
       } 
       while (-not $inputValid)
       $VMNumber = $inputValue 
       write-host -ForegroundColor Green `n"$VMNumber VMs will be created.`n"

start-sleep -seconds 2;CLS # Give time to read last confirmation before clearing screen.

#endregion ********************************************************************************************

#region ###################################### SUMMARY ################################################

write-host " Summary "`n -ForegroundColor White -BackgroundColor Black  

write-host -ForegroundColor Cyan "`nVMs will be stored here: "$VMPath  
write-host -ForegroundColor Cyan "Hard disks will be stored here: "$HDPath
If ($vhdx -ne ""){write-host -ForegroundColor Cyan "Differencing disks master at: "$masterVHDX}  
write-host -ForegroundColor Cyan "VMs will have this much RAM: "$RAM"GB"
write-host -ForegroundColor Cyan "Network switch to use: "$VMSwitch
write-host -ForegroundColor Cyan "This .ISO will be mounted: "$ISO
write-host -ForegroundColor Cyan "VM names will start with: "$VMPrefix
write-host -ForegroundColor Cyan "Number of VMs to create: "$VMnumber

Write-host -ForeGroundColor Cyan "`n`nTo modify the default values used by the script, edit the default parameter`nsettings at the top of the script. If you installed the script using the `nInstall-Script Create-TestVMs PowerShell command, the script can be found at `n$env:ProgramFiles\WindowsPowerShell\Scripts.`n"


write-host -ForeGroundColor Green "`nReady to create VMs!`n"
write-host -ForeGroundColor Yellow "Continue (Y) or Cancel (N)?"
$Readhost = Read-Host " [Y] [N] (Default is Y)"  
    Switch ($ReadHost) 
     { 
       Y {}
       N {return}  
     }
     If ($readhost -ne "n"){}

#endregion ********************************************************************************************

#region ##################################### CREATE VMs ##############################################

# If alternate VM storage paths were provided, update them on the hyper-v host now:
Set-VMHost -VirtualMachinePath $vmpath
Set-VMHost -VirtualHardDiskPath $hdpath

# Using differencing disks
If($vhdx -ne "")
    {
        $masterVHDX = $HDPath+"\Master-"+$nameEnd+"\"
        # Create master directory
        new-item $masterVHDX -ItemType Directory
        # Copy master VHDX into master directory
        write-host "Copying master hard disk file...`n"
        # Copy-Item -Path $vhdx -Destination $masterVHDX
        Start-BitsTransfer -Source $vhdx -Destination $masterVHDX -Description "Copying $vhdx to $masterVHDX" -DisplayName "Copying master hard disk file."
        #Set the parent VHDX as Read-Only
        Set-ItemProperty -Path $masterVHDX"\*.vhdx" -Name IsReadOnly -Value $true
        Add-Content $masterVHDX"ReadMe.txt" "These VMs are using differencing disks based on this master disk:"
              
    for ($i=1; $i -le $VMnumber; $i++)
        {
        $VMSuffix = get-random -minimum 100 -maximum 999
        $VMname = $VMPrefix + "-" + $VMSuffix
        # Create differencing disk in HD location
        $masterVhdxName = Get-ChildItem $masterVHDX*.vhdx | Select-Object -ExpandProperty Name
        write-host "Creating differencing disk for"$VMName"...`n"
        $parentPath = $masterVHDX+$masterVhdxName  
        $vhdPath = $HDPath + "\" + $vmname + "\" + $vmname + ".vhdx"
        $vhdxDiff = New-VHD -Path $vhdPath -ParentPath $parentPath -Differencing
        
        # Create VM, but first check for network switch
        If ($VMSwitch -ne "No virtual switch selected."){
            new-vm -Name $VMName -Path $VMPath"\" -Generation 2 -VHDPath $vhdPath -BootDevice VHD -MemoryStartupBytes $RAMAssigned -SwitchName $VMSwitch
        }
        Else{   
            new-vm -Name $VMName -Path $VMPath"\" -Generation 2 -VHDPath $vhdPath -BootDevice VHD -MemoryStartupBytes $RAMAssigned     
            }    
        
        # Adding an ISO to the VM?
        If ($ISO -ne "No .ISO selected."){
            Add-VMDvdDrive -VMName $VMname -Path $ISO
            $bootorder = (Get-VMFirmware -VMName $vmname).bootorder | Sort-Object -Property Device
            Get-VM -VMName $VMname | Set-VMFirmware -BootOrder $bootorder}
        Set-VMKeyProtector -VMName $VMName -NewLocalKeyProtector
        Enable-VMTPM -VMName $VMName
        Set-VM -Name $VMName -ProcessorCount 4 -SmartPagingFilePath $HDPath -AutomaticStartAction StartIfRunning -DynamicMemory 
        
        # Add VM SN# to Hyper-V VM notes to make it easier to find for Autopilot maintenance.
        $sn = Get-WmiObject -ComputerName . -Namespace root\virtualization\v2 -class Msvm_VirtualSystemSettingData `
            | ? {$_.elementName -eq $VMName} `
            | Select -ExpandProperty BIOSSerialNumber        
        $vm = Get-VM -Name $VMName
        Set-VM -VM $vm -Notes "$($vm.Notes) VM serial number is: $sn." -Confirm:$false
        
        # Add master disk location used by differencing disk to VM notes
        Set-VM -VM $vm -Notes "$($vm.Notes) `n`nThis VM is using differencing disks. Its master disk is located at $masterVHDX." -Confirm:$false

        #Checkpoint the VM at pre-out of box experience state.
        write-host "Creating OOBE checkpoint"
        Get-VM $VMName -ComputerName . | checkpoint-vm -SnapshotName "OOBE"
        write-host "Starting "$VMName`n
        Start-VM $VMname
        Add-Content $masterVHDX"ReadMe.txt" $VMname     
        }        
    } 
# Not using differencing disks
Else{
    for ($i=1; $i -le $VMnumber; $i++)
        {
        $VMSuffix = get-random -minimum 100 -maximum 999
        $VMname = $VMPrefix + "-" + $VMSuffix
        $vhdPath = $HDPath + "\" + $vmname + "\" + $vmname + ".vhdx"

        # Create VM, but first check for network switch
        If ($VMSwitch -ne "No virtual switch selected."){
            New-VM -Name $VMname -Path $VMpath -Generation 2 -NewVHDSizeBytes 127GB -NewVHDPath $vhdPath -MemoryStartupBytes $RAMAssigned -SwitchName $VMSwitch
        }
        Else{   
            New-VM -Name $VMname -Path $VMpath -Generation 2 -NewVHDSizeBytes 127GB -NewVHDPath $vhdPath -MemoryStartupBytes $RAMAssigned     
            }  
                 
        # Adding an ISO to the VM?
        If ($ISO -ne "No .ISO selected."){
            Add-VMDvdDrive -VMName $VMname -Path $ISO
            $bootorder = (Get-VMFirmware -VMName $vmname).bootorder | Sort-Object -Property Device
            Get-VM -VMName $VMname | Set-VMFirmware -BootOrder $bootorder}
        Set-VMKeyProtector -VMName $VMName -NewLocalKeyProtector
        Enable-VMTPM -VMName $VMName
        Set-VM -Name $VMName -ProcessorCount 4 -SmartPagingFilePath $HDPath -AutomaticStartAction StartIfRunning -DynamicMemory 

        # Add VM SN# to Hyper-V VM notes to make it easier to find for Autopilot maintenance.
        $sn = Get-WmiObject -ComputerName . -Namespace root\virtualization\v2 -class Msvm_VirtualSystemSettingData `
            | ? {$_.elementName -eq $VMName} `
            | Select -ExpandProperty BIOSSerialNumber        
        $vm = Get-VM -Name $VMName
        Set-VM -VM $vm -Notes "$($vm.Notes) VM serial number is: $sn." -Confirm:$false
                 
        }        
        write-host -ForegroundColor Cyan "Created $vmname!"
        Start-VM $VMname
        start-sleep 1
    }
    
    write-host -ForegroundColor Green "`n`nComplete!`n"

#endregion ********************************************************************************************
    
exit