Generating Virtual Machines with System Center Virtual Machine Manager SP1

Posted on 2011/08/30


Today’s blog is the result recent scalability testing I performed for XenDesktop 5 on Microsoft’s System Center Virtual Machine Manager (SCVMM) SP1 platform and HP BL460 G7 blades with DAS storage. On a side note, if you have not yet had a chance to review the scalability results, they have been published on HP’s Client Virtualization website. I strongly recommend reviewing that document if you are considering Hyper-V as the hypervisor for your XenDesktops. 

Now, on to the business at hand. During that testing I had a chance to revisit my existing PowerShell scripts and improve them.  Since some of these scripts are starting to be distributed at various forums throughout the world, I figured now would be a good time to get these on my blog.

Building thousands of desktops is always a challenging task. I tend to automate as much of that process as possible. Unfortunately, sometimes my automation is not as “smooth” as I would like it to be, but it does tend to get the job done. Each time I do one of these tests, I streamline the process a little more. In this case, the PowerShell scripts in this blog are improvements on my earlier scripts. 

The Build Process

Providing an overview of the build process I use is probably the best place to start. When I arrive on-site the first focus is usually getting the infrastructure (Hyper-V, System Center Virtual Machine Manager (SCVMM), Provisioning Services (PVS), and XenDesktop) servers setup. From there we create a golden vDisk (PVS) image and verify it functions correctly. The next step is to build the desktops using the following process:

  1. Use the GenVMs.PS1 PowerShell script to create the virtual machines on multiple hosts.
  2. Use the ListVMsOnHost.PS1 PowerShell script and the CopyVHD.CMD file to copy the write-cache VHD to each virtual machine’s folder using the machine name and a suffix of _wc. (eg E:\Hyper-V\XDH1x001\XDH1x001_wc.vhd)
  3. Use the AttachExistingVHD.PS1 PowerShell script to attach the newly copied VHD to the virtual machine.
  4. Use the GenPVSFile.PS1 PowerShell script to create a CSV file with the information required to import the desktops into Provisioning Services.

This process is a tad bit more complex than I would like it to be, and I am working to simplify it; however, this approach is extremely quick when compared to other available methods. When I did the testing in Houston, we put up 2300+ desktops in just under 6 hours using this approach. The key is to copy the write cache VHD in parallel on each host using a batch file and then attach it later. Other methods leverage SCVMM which ends up doing a single-threaded network copy. Each of these primary steps are discussed below along with their associated PowerShell or CMD scripts.

Generating the Virtual Machines

I must impress upon you the need to verify unique MAC address ranges on each of your SCVMM servers before running this script on more than one SCVMM server in the environment. By default every SCVMM server generates MAC addresses from the same static pool and if the same MAC address is assigned to different virtual machines, strange (and very bad) things will occur. So, before running this script, verify that every SCVMM server in your environment has a unique pool of MAC addresses by going to the following location on each SCVMM servers:

Administration>>Networking>>Global Static MAC Address Range

Global MAC Address Assignment

SCVMM Global MAC Addresses

NOTE: The SCVMM MAC address ranges override the Hyper-V ranges also, so any changes you made on the Hyper-V hosts to guarantee unique MAC addresses will be ignored.

The previously released GenVMs.PS1 script has been updated to streamline the process and improve the usability. The latest version offers the following improvements:

  1. Added the ability to provide parameters, such as vCPUs and dynamic memory settings, for the virtual machine profile (XD5Profile) which is created automatically if it does not exist.  Unfortunately, if the profile does already exist, it will be used but not updated with values provided in the script. If you would like to change the values in the existing XD5Profile, either edit it from the SCVMM server or delete and let the script recreate it.
  2. Default values were added to the beginning of the script that will be used as examples in the command-line output and when prompting. If you customize the default variable values for your environment you will have a sample command-line in the output with no parameters and if you choose to enter the variables by prompt the default value will be used if you just press Enter.

These alteration dramatically simplify the data input process when you run this script multiple times. Here is the latest version of the GenVMs PowerShell script in its entirety:

# Purpose:    Generate up to virtual machines using the command-line parameters supplied
#             for customization of the new virtual machine.
# Date:       29 August 2011
# Version:    7
# Author:     Paul Wilson
# Notes:      The script only creates VMs on a single host. To create VMs on multiple hosts
#             run multiple instances of the script from a batch file or create an outer loop.
#             The LocalVMStorage path and networks must exist or the script fails. I have
#             not added any data validation checks to the script.
# Add the Virtual Machine Manager snapin so the commands work.
add-pssnapin *VirtualMachineManager
# Set Default Values:
    $DefVMHost = "HOST01"
    $DefVMBaseName = "XD5H1x"
    $DefNetworkName = "PVS"
    $DefNetworkName2 = "External"
    $DefVMPath = "E:\Hyper-V"
    $DefVMCount = 50
    $DefStartCount = 1
    $DefMemMin = 512
    $DefMemMax = 1536
    $DefNumCPUs = 1
    $DefMemBuffer = 10
    $DefMemWeight = 5000
    $DefDomainUser = "XD5\Administrator"
# Parse the command-line and verify the 13 required parameters are present, if not display usage info
if ($args -eq $null -or $args.Count -lt 13)
{
    write-host "Usage: GenVMs.ps1 VMTargetHost VMBaseName PVSBootNetwork SynthNetwork "
    write-host "LocalVMStoragePath NumberToCreate StartingAt MinMemory MaxMemory"
    write-host "CpuCores DynamicMemoryBuffer DynamicMemoryWeight DomainUser"
    write-host " "
    write-host "Example: .\GenVMs.ps1 ""$DefVMHost"" ""$DefVMBaseName"" ""$DefNetworkName"" ""$DefNetworkName2"" "
    write-host """$DefVMPath"" $DefVMCount $DefStartCount $DefMemMin $DefMemMax $DefNumCPUs $DefMemBuffer $DefMemWeight $DefDomainUser"
    write-host " "
    write-host "Warning! Not enough command-line parameters have been supplied!"
    $strAnswer = read-host "Would you like to manually provide the parameters (y/n)?"
    switch ($strAnswer)
    {
      N {exit 1}
      Y {
         write-host "=========================================================="
         write-host " PROVIDE PARAMETER VALUES. CONFIRM IN NEXT STEP"
         write-host " Press [Enter] to accept the value in parenthesis"
         write-host "=========================================================="
         $VMHost = read-host "Enter HyperV Host name to create the servers on (eg $DefVMHost)"
         if($VMHost.length -eq 0){ $VMHost = $DefVMHost}
         $VMBaseName = read-host "Enter base name for virtual machines (eg $DefVMBaseName)"
         if($VMBaseName.length -eq 0){ $VMBaseName = $DefVMBaseName}
         $NetworkName = read-host "Enter the Hyper-V network for the emulated adapter (eg $DefNetworkName)"
         if($NetworkName.length -eq 0){ $NetworkName = $DefNetworkName}
         $NetworkName2 = read-host "Enter the Hyper-V network for the synthetic adapter (eg $DefNetworkName2)"
         if($NetworkName2.length -eq 0){ $NetworkName2 = $DefNetworkName2}
         $VMPath = read-host "Enter the locally accessible path where the host will store`r`nthe virtual machines data (eg $DefVMPath)"
         if($VMPath.length -eq 0){ $VMPath = $DefVMPath}
         [int]$VMCount = read-host "Enter the number of virtual machines to create on the host (eg $DefVMCount)"
         if($VMCount -eq ""){ [int]$VMCount = $DefVMCount}
         [int]$StartCount = read-host "Enter the first number to start at (eg $DefStartCount)" 
         if($StartCount -eq ""){ [int]$StartCount = [int]$DefStartCount}
         $MemMin = read-host "Enter the minimum amount of dynamic memory in MB (eg $DefMemMin)"
         if($MemMin.length -eq 0){ $MemMin = $DefMemMin}
         $MemMax = read-host "Enter the maximum amount of dynamic memory to assign in MB (eg $DefMemMax)"
         if($MemMax.length -eq 0){ $MemMax = $DefMemMax}
         [int]$NumCPUs = read-host "Enter the number of CPUs to assign to the VM (eg $DefNumCPUs)"
         if($NumCPUs -eq ""){ [int]$NumCPUs = [int]$DefNumCPUs}

         $MemBuffer = read-host "Percentage of memory to use for cache (eg $DefMemBuffer)"
         if($MemBuffer.length -eq 0){ $MemBuffer = $DefMemBuffer}
         $MemWeight = read-host "Enter the memory weight for dynamic memory range is 0-10000 (eg $DefMemWeight)"
         if($MemWeight.length -eq 0){ $MemWeight = $DefMemWeight}
         $DomainUser = read-host "Enter the domain user for the hardware profile owner (eg $DefDomainUser)"
         if($DomainUser.length -eq 0){ $DomainUser = $DefDomainUser}
         write-host "Thank you..."
         }
      Default {exit 1}
    }
}
else
{
    # Place the command-line parameters into named variables for later use.
    $VMHost = $args[0]
    $VMBaseName = $args[1]
    $NetworkName = $args[2]
    $NetworkName2 = $args[3]
    $VMPath = $args[4]
    [int]$VMCount = $args[5]
    [int]$StartCount = $args[6]
    $MemMin = $args[7]
    $MemMax = $args[8]
    [int]$NumCPUs = $args[9]
    $MemBuffer = $args[10]
    $MemWeight = $args[11]
    $DomainUser = $args[12]
}
# Post back the settings to the user for confirmation
write-host "=========================================================="
write-host "CONFIRM CONFIGURED SETTINGS"
write-host "=========================================================="
write-host "HyperV Server to create VMs on: $VMHost"
write-host "Base name for VMs: $VMBaseName"
write-host "PVS boot network name (emulated nic): $NetworkName"
write-host "Normal network name (synthetic nic): $NetworkName2"
write-host "Local path for HyperV server to store VMs: $VMPath"
write-host "Number of VMs to create: $VMCount"
write-host "Base number to start VM creation at: $StartCount"
write-host "Minimum Memory to assign to VM: $MemMin MB"
write-host "Maximum Memory to assign to VM: $MemMax MB"
write-host "Number of CPUs for the VM: $NumCPUs"
write-host "Dynamic Memory buffer: $MemBuffer%"
write-host "Dynamic Memory weight value: $MemWeight"
write-host "Profile Owner: $DomainUser"
write-host "=========================================================="
$strConfirm = read-host "Please confirm these settings. Continue (YES)?"
if ($strConfirm -ne "YES")
 {
   write-host "You did not type out the word YES. Aborting!"
   exit 1
 }
# Get the name of the SCVMM server we are running this on. The VMM server could be passed as a parameter as well.
$VMMServer = Get-VMMServer -Computername "localhost"
# Create a new Hardware Profile for a XenDesktop and set the default values or use the existing profile. Updating an existing profile is not supported.
# If the profile already exists and you want to make changes, you will need to change it through PowerShell, SCVMM, or delete and recreate the profile with the script.
    $HWProfile = Get-HardwareProfile | where {$_.Name -eq "XD5Profile"}
    if ($HWProfile -eq $null)
    {
        write-output "Hardware profile not found. Creating a default profile."
        $HWProfile = New-HardwareProfile -Owner "$DomainUser" -Description "Hosted XenDesktop" -Name "XD5Profile" -CPUCount $NumCPUs -BootOrder PXEBoot,IDEHardDrive,CD,Floppy -DynamicMemoryEnabled $True -DynamicMemoryMaximumMB $MemMax -DynamicMemoryBufferPercentage $MemBuffer -MemoryWeight $MemWeight -MemoryMB $MemMin
    }
# Calculate the ending value for the VM generation loop
$EndCount = $StartCount + $VMCount - 1
# Create VMs in a loop
for ($i=$StartCount; $i -le $EndCount; $i++)
{
    # Create the Virtual Machine and assign the VM Name. Use the number after the format type to control the number of leading 0's.
    # Format types: D=Decimal, X=Hexadecimal. ie. D3=(001,002,...,099,100,...999,1000,...n) D2=(01,02...,99,100,...n) X2=(...,0A,OB,...,FF,100,...n)
    $VMName = "{1}{0:D3}" -f $i, $VMBaseName
    write-host "Creating $VMName..."
    # Create a job group id to link the items together and create them as a group with the New-VM command
    $JobGroupID = [System.Guid]::NewGuid().ToString()
    # Get a MAC Address from the pool of available MAC addresses on the server. (Alternatively a MAC address could be assigned here.)
    $PooledMACAddress = New-PhysicalAddress -Commit

    # Get a network objects for creating the network adapters. If a second network adapter (synthetic usually) comment out the $VNetwork2= line
    $VNetwork = Get-VirtualNetwork | where {$_.Name -match $NetworkName -and $_.VMHost -eq $VMHost}
    $VNetwork2 = Get-VirtualNetwork | where {$_.Name -match $NetworkName2 -and $_.VMHost -eq $VMHost}
    # Create a Virtual Legacy Network Adapter required for PXE booting with Provisioning Services

    New-VirtualNetworkAdapter -JobGroup $JobGroupID -PhysicalAddressType Static -PhysicalAddress $PooledMACAddress -VirtualNetwork $VNetwork

    # In case a second synthetic adapter is not necessary comment out the line below

    New-VirtualNetworkAdapter -JobGroup $JobGroupID -PhysicalAddressType Dynamic -Synthetic -VirtualNetwork $VNetwork2

    # Create a virtual DVD
    New-VirtualDVDDrive -JobGroup $JobGroupID -Bus 1 -LUN 0
    # Build Virtual Machine using SCVMM Powershell API.
    New-VM -VMMServer $VMMServer -Name $VMName -VMHost $VMHost -Path $VMPath -HardwareProfile $HWProfile -JobGroup $JobGroupID -RunAsynchronously -RunAsSystem -StartAction NeverAutoTurnOnVM -StopAction TurnOffVM
}

Run the GenVMs.PS1 script from the SCVMM server. You will need to run this script once for each Hyper-V host in the environment, but that can be scripted using a batch file with command-line parameters if you need it to run unattended. Just create a batch file that looks something like this:

REM This batch file calls the GenVMs powershell script once for each HyperV host. This script should be
REM run from the SCVMM server where the Powershell script GenVMs resides.
@echo off
powershell c:\GenVMs.ps1 "HOST01" "XD5H1x" "PVS" "External" "E:\Hyper-V" 50 1 512 1536 1 10 5000 XD5\Administrator
powershell c:\GenVMs.ps1 "HOST02" "XD5H2x" "PVS" "External" "E:\Hyper-V" 50 1 512 1536 1 10 5000 XD5\Administrator
powershell c:\GenVMs.ps1 "HOST03" "XD5H3x" "PVS" "External" "E:\Hyper-V" 50 1 512 1536 1 10 5000 XD5\Administrator
powershell c:\GenVMs.ps1 "HOST04" "XD5H4x" "PVS" "External" "E:\Hyper-V" 50 1 512 1536 1 10 5000 XD5\Administrator

You will also need to remove the following lines from the GenVMs.PS1 script if you don’t want to confirm the command-line options for each line of the batch file.

$strConfirm = read-host "Please confirm these settings. Continue (YES)?"
if ($strConfirm -ne "YES")
 {
   write-host "You did not type out the word YES. Aborting!"
   exit 1
 }

Once the virtual machines are created, you are ready to get the VHD files copied and attached. Prior to that point the directories will not exist to copy the VHD file into, so you should wait until that process completes before moving on to the next step.

Copying the VHD files

If you are using Provisioning Services, once all the virtual machines have been created, you will still need to attach the VHD files for the write-cache drives.  The fastest way to do that is to take a VHD and mount it on your XenDesktop golden PVS image and format  it NTFS. Then copy that same VHD file (with the disk signature) to the root of all the Hyper-V hosts where XenDesktops have been created. Once the VHD exists on the root, it can be copied as a local file to each of the virtual machine directories.

However, before doing the file copy, you need a list of all the virtual machines on the host so the directories can be accessed. Use the ListVMsOnHost.PS1 PowerShell script to generate a text file for each Hyper-V host with a list of virtual machines hosted on it.

 From there you can use a batch file like this to copy the VHD file once for each host and pass into the text file that was created using the ListVMsOnHost PowerShell script.

# Purpose:    This script outputs all the virtual machines on a given host that
#             match a specific name pattern and outputs it to a text file.
# Date:       29 August 2011
# Version:    1.00
# Author:     Paul Wilson
# Notes:      Must be run from a System Center VMM Server
# Add the Virtual Machine Manager snapin so the commands work.
add-pssnapin *VirtualMachineManager
# Parse the command-line and verify the one required parameter is present, if not display usage info
if ($args -eq $null -or $args.Count -lt 1)
{
    write-output "Usage: ListVMsOnHost.ps1 VMNameToMatch"
    write-output "Example: .\ListVMsOnHost.ps1 ""HVDesktop01"" "
    write-output "Function: Creates a text file for each host with the VMs on each host"
    exit 1
}
# Place the command-line parameters into named variables for later use.
$VMNameMatches = $args[0]
$Hosts = Get-VMHost -VMMServer localhost
foreach ($myhost in $Hosts)
{
$server = $myhost.FQDN
$OutFileName = "$server.txt"
$VMs = Get-VM -VMMServer localhost | where {($_.HostName -eq $myhost.FQDN) -and ($_.Name -match "$VMNameMatches")}
foreach ($myVM in $VMs)
{
    add-content $OutFileName $myVM.Name
}
}

Once the Text files are created you can use the CopyVHD.CMD file process the virtual machine text file from the ListVMsOnHost.PS1 PowerShell script. Here is a sample CopyVHD.CMD file.

@REM =============================================
@REM = This command file takes three parameters: 
@REM =      
@REM = (1) Input file listing the VM machines  
@REM = (2) Parent directory where VM machines exist 
@REM = (3) Write-cache VHD to be copied to the VM 
@REM =      
@REM = Usage: copyvhd VMHostList TargetLocation SourceFile
@REM = Example: copyvhd hyperv01.txt E:\Hyper-V c:\wc.vhd
@REM =============================================
For /F %%I in (%1) do copy %3 %2\%%I\%%I_wc.vhd

When executed, the command file parses the input text file with the machine names and copies the write-cache VHD into the folder of each virtual machine and names the file VirtualMachine_wc.vhd. Once all the VHDs are copied, the next step is to return to the SCVMM console and attach them.

Attaching the VHDs to the Virtual Machines

Once you have all the VHD files copied to the respective virtual machine folder directories, you can use the AttachExistingVHD.PS1 PowerShell script to attach them. This PowerShell script was written a while back and I had blogged it in October 2010 as part of my XenDesktop PowerShell series. The only changes to the original MountVHD script are the inclusion of the VMM SnapIn and its name, which I modified to make it more descriptive of what it was doing in order to alleviate some confusion. Here is the new script. 

# Purpose:    This script attaches an existing VHD to a virtual machine. Designed for deploying XenDesktop and attaching write
#             cache drives to existing VMs.
# Date:       29 August 2011
# Authors:    Loay Shbeilat and Paul Wilson (no implied or expressed warranties) with content taken from Taylor Brown's blog:
#             http://blogs.msdn.com/b/taylorb/archive/2008/10/13/pdc-teaser-attaching-a-vhd-to-a-virtual-machine.aspx
# Notes:      This script will add a New IDE Virtual disk drive to attach the VHD.
# Add the Virtual Machine Manager snapin so the commands work.
add-pssnapin *VirtualMachineManager
# Function ProcessWMIJob used to add the new Virtual Disk and VHD.
filter ProcessWMIJob
{ 
    param
    ( 
        [string]$WmiClassPath = $null,
        [string]$MethodName = $null
    )

    $errorCode = 0
    if ($_.ReturnValue -eq 4096)
    { 
        $Job = [WMI]$_.Job
        while ($Job.JobState -eq 4)
        { 
            Write-Progress $Job.Caption "% Complete" -PercentComplete $Job.PercentComplete
            Start-Sleep -seconds 1
            $Job.PSBase.Get()
        } 
        if ($Job.JobState -ne 7)
        { 
            if ($Job.ErrorDescription -ne "")
            {
                Write-Error $Job.ErrorDescription
                Throw $Job.ErrorDescription
            }
            else
            {
                $errorCode = $Job.ErrorCode
            }
        } 
        Write-Progress $Job.Caption "Completed" -Completed $TRUE
    }
    elseif($_.ReturnValue -ne 0)
    {
        $errorCode = $_.ReturnValue
    }

    if ($errorCode -ne 0)
    { 
        Write-Error "Hyper-V WMI Job Failed!"
        if ($WmiClassPath -and $MethodName)
        {
            $psWmiClass = [WmiClass]$WmiClassPath
            $psWmiClass.PSBase.Options.UseAmendedQualifiers = $TRUE
            $MethodQualifiers = $psWmiClass.PSBase.Methods[$MethodName].Qualifiers
            $indexOfError = [System.Array]::IndexOf($MethodQualifiers["ValueMap"].Value, [string]$errorCode)
            if ($indexOfError -ne "-1")
            {
                Throw "ReturnCode: ", $errorCode, " ErrorMessage: '", $MethodQualifiers["Values"].Value[$indexOfError], "' - when calling $MethodName"
            }
            else
            {
                Throw "ReturnCode: ", $errorCode, " ErrorMessage: 'MessageNotFound' - when calling $MethodName"
            }
        }
        else
        {
            Throw "ReturnCode: ", $errorCode, "When calling $MethodName - for rich error messages provide classpath and method name."
        }
    } 
    return $_
}
# Parse the command-line and verify the 3 required parameters are present, if not display usage info
if ($args -eq $null -or $args.Count -lt 3)
{
    write-output "Usage: AttachExistingVHD.ps1 LocalVMStoragePath VMNameMatch Postpend"
    write-output "Example: .\AttachExistingVHD.ps1 ""E:\Hyper-V"" ""HVDesktop01"" ""_wc"" "
    write-output "Function: Adds a IDE drive and attachs an existing VHD to the VM."
    write-output "In this example the E:\Hyper-V\HVDesktop01\HVDesktop01_wc.vhd is attached HVDesktop01"
    exit 1
}
# Place the command-line parameters into named variables for later use.
$VHDPath = $args[0]
$VMNameMatches = $args[1]
$PostPend = $args[2]
# Get the VMM server name
$VMHost = Get-VMHost -VMMServer localhost
# Get the list of VMs that match the VMNameMatch provided on the command-line
$AllVMs = Get-VM | where { $_.Name -match "$VMNameMatches" } | sort Name
# Determine how many VM's meet the VMNameMatch criteria. Save the count for later output.
if ($AllVMs -eq $null)
{
 write-output "No VMs match the pattern: $VMNameMatches"
 exit 1
}
else
{
    $LeftToGo = $AllVMs.Count
    if ($LeftToGo -eq $null)
    {
        $matchString = "Only one VM matched the pattern: {0}" -f $VMNameMatches
        $LeftToGo = 1
    }
    else
    {
    $matchString = "{0} VMs match the pattern: {1}" -f $AllVMs.Count, $VMNameMatches
 }
    write-output $matchString
}
# Process each VM and attempt to mount the VHD. The VHD needs to exist first.
foreach ($myVM in $AllVMs)
{
    $LeftToGo = $LeftToGo - 1
    $HyperVGuest = $myVM.Name
    $server = $myVM.hostname

    # Modify $vhdToMount variable to match the path to the VHD if yours is not in the VM directory.

    $vhdToMount = "{0}\{1}\{1}{2}.vhd" -f $VHDPath, $myVM.Name, $PostPend

    $Status = "Processing VM:{0} VHD:{1} VMs Left:{2}" -f $myVM.Name, $vhdToMount, $LeftToGo
    Write-output $Status

    # Try to attach, if that fails... catch error and continue
    # This bit of code is from Taylor's blog so I am not even going to attempt to explain it.

    try
    {
        $VMManagementService = Get-WmiObject -computername $server -class "Msvm_VirtualSystemManagementService" -namespace "root\virtualization"
        $Vm = Get-WmiObject -computername $server -Namespace "root\virtualization" -Query "Select * From Msvm_ComputerSystem Where ElementName='$HyperVGuest'"
        $VMSettingData = Get-WmiObject -computername $server -Namespace "root\virtualization" -Query "Associators of {$Vm} Where ResultClass=Msvm_VirtualSystemSettingData AssocClass=Msvm_SettingsDefineState"
        $VmIdeController = (Get-WmiObject -computername $server -Namespace "root\virtualization" -Query "Associators of {$VMSettingData} Where ResultClass=Msvm_ResourceAllocationSettingData AssocClass=Msvm_VirtualSystemSettingDataComponent" | where-object {$_.ResourceSubType -eq "Microsoft Emulated IDE Controller" -and $_.Address -eq 0})
        $DiskAllocationSetting = Get-WmiObject -computername $server -Namespace "root\virtualization" -Query "SELECT * FROM Msvm_AllocationCapabilities WHERE ResourceSubType = 'Microsoft Synthetic Disk Drive'"
        $DefaultDiskDrive = (Get-WmiObject -computername $server -Namespace "root\virtualization" -Query "Associators of {$DiskAllocationSetting} Where ResultClass=Msvm_ResourceAllocationSettingData AssocClass=Msvm_SettingsDefineCapabilities" | where-object {$_.InstanceID -like "*Default"})

        $DefaultDiskDrive.Parent = $VmIdeController.__Path
        $DefaultDiskDrive.Address = 0
        $NewDiskDrive = ($VMManagementService.AddVirtualSystemResources($Vm.__Path, $DefaultDiskDrive.PSBase.GetText(1)) | ProcessWMIJob $VMManagementService "AddVirtualSystemResources").NewResources
        $DiskAllocationSetting = Get-WmiObject -computername $server -Namespace "root\virtualization" -Query "SELECT * FROM Msvm_AllocationCapabilities WHERE ResourceSubType = 'Microsoft Virtual Hard Disk'"
        $DefaultHardDisk = (Get-WmiObject -computername $server -Namespace "root\virtualization" -Query "Associators of {$DiskAllocationSetting} Where ResultClass=Msvm_ResourceAllocationSettingData AssocClass=Msvm_SettingsDefineCapabilities" | where-object {$_.InstanceID -like "*Default"})

        $DefaultHardDisk.Parent = $NewDiskDrive
        $DefaultHardDisk.Connection = $vhdToMount
        $VMManagementService.AddVirtualSystemResources($Vm.__Path, $DefaultHardDisk.PSBase.GetText(1)) | ProcessWMIJob $VMManagementService "AddVirtualSystemResources"
    }
    catch { }
}

Once that is finished the next step is to get the virtual machines added to the Provisioning Services console so the correct vDisk can be assigned and streamed to the virtual machine.

Building the Provisioning Services Import File

Before creating the Provisioning Services import file, all the VMs will have to be started at least once so the dynamic MAC address will be assigned from the pool (which you should have configured to be unique for each SCVMM server in your environment) to the virtual machine. Once this is complete the PVS import file can be created using GenPVSFile.PS1 PowerShell script. This script has been updated from previous scripts to load the Virtual Machine Manager Snapin and to distinguish between VMs with one or more network cards automatically. The use of add-content now has removed the need to convert the CSV file to ANSI before importing.

# Purpose:       Create a CSV file that can be imported by Provisioning services
# Date:          29 August 2011
# Version:       1.03
# Author:        Paul Wilson
# Notes:         The CSV file may need to opened and saved in the ANSI format on some computers.
#                This script automatically appends information to any existing file in case you need to
#                run the command multiple times with different match criteria.
# Add the Virtual Machine Manager snapin so the commands work.
add-pssnapin *VirtualMachineManager
# Parse the command-line and verify the five required parameters are present, if not display usage info
if ($args -eq $null -or $args.Count -lt 5)
{
    write-output "Usage: GenPVSFile.ps1 SiteName CollectionName Description ImportFileName VMMatchCriteria"
    write-output "Example: .\GenPVSFile.ps1 ""Site"" ""Collection"" ""XD Desktop"" ""c:\PVSImport.csv"" HVDesktop "
    exit 1
}
# Pulls the VM Name Match criteria off the command-line
$VMNameMatches = $args[4]
# Connects to the local SCVMM Server
$VMMServer = Get-VMMServer -Computername "localhost"
# Finds all matching VMs and sorts by their machine name
$AllVMs = Get-VM | where { $_.Name -match "$VMNameMatches" } | sort Name
if ($AllVMs -eq $null)
{
 write-output "No VMs match the pattern: $VMNameMatches"
 exit 1
}
else
{
    $LeftToGo = $AllVMs.Count
    if ($LeftToGo -eq $null)
    {
        $matchString = "Only one VM matched the pattern: {0}" -f $VMNameMatches
        $LeftToGo = 1
    }
    else
    {
    $matchString = "{0} VMs match the pattern: {1}" -f $VMs.Count, $VMNameMatches
 }
    write-output $matchString
}

# The following loop gets the MAC address of the primary NIC then writes
# that output to the CSV file along with the other fields required for the PVS import
# most of which were supplied as parameters on the command-line.
# This code assumes the first NIC is the PVS boot NIC. If not, change the $nicDetails[0] to $nicDetails[1]
foreach ($vm in $AllVms)
{
    $LeftToGo = $LeftToGo -1
 $nicDetails = Get-VirtualNetworkAdapter -VM $vm

    # Look to see if more than one network adapter is defined, if so, grab the first one in the array
    if ($nicDetails.count -lt 1)
        # If only one network adapter is defined, this evaluation will return True because no array is created
    {
        $csvString = "{0},{1},{2},{3},{4}" -f $vm.Name, $nicDetails.PhysicalAddress, $args[0], $args[1], $args[2]
     add-content $args[3] $csvString
        $StatusString = "Processing {0}. Virtual Machines left to process: {1}" -f $vm.Name, $LeftToGo
        write-output $StatusString
    }
    else
       # If that evalution was False, more than one NIC exists and we grab the MAC of the first one in the array
    {
        $csvString = "{0},{1},{2},{3},{4}" -f $vm.Name, $nicDetails[0].PhysicalAddress, $args[0], $args[1], $args[2]
     add-content $args[3] $csvString
        $StatusString = "Processing {0}. Virtual Machines left to process: {1}" -f $vm.Name, $LeftToGo
        write-output $StatusString

    }
}

Now you have the CSV file, you can run through the rest of the process I originally documented on my Citrix blogs as part of my PowerShell scripts series Part 3, since it has not changed.

Modifying the Dynamic Memory Settings

Once you have setup virtual machines with dynamic memory, you may need to adjust the settings, such as the minimum, maximum, buffer, or weight to optimize it for your environment. The SetDynamicMemory PowerShell script below allows you to modify these four settings on a set of virtual machines.

# Purpose:    This script enables Dynamic Memory for a set of VMs that match a supplied
#             name pattern. The four key dynamic memory values are configurable.
# Date:       29 August 2011
# Version:    1.02
# Author:     Paul Wilson
# Notes:      Must be run from a System Center VMM Server with Service Pack 1
# Add the Virtual Machine Manager snapin so the commands work.
add-pssnapin *VirtualMachineManager
# Parse the command-line and verify the 5 required parameters are present, if not display usage info
if ($args -eq $null -or $args.Count -lt 5)
{
    write-output "Usage: SetDynamicMemory.ps1 VMNameToMatch MinMemory MaxMemory Buffer(5-95) Weight(1-10000)"
    write-output "Example: .\SetDynamicMemory.ps1 ""HVDesktop01"" 1024 2048 5 5000 "
    write-output "Function: Enables Dynamic Memory using provided parameters for all VMs that match the name supplied"
    exit 1
}
# Place the command-line parameters into named variables for later use.
$VMNameMatches = $args[0]
$MemMin = $args[1]
$MemMax = $args[2]
$MemBuffer = $args[3]
$MemWeight = $args[4]
# Get the VMM server name
$VMHost = Get-VMHost -VMMServer localhost
# Get the list of VMs that match the VMNameMatch provided on the command-line
$AllVMs = Get-VM | where { $_.Name -match "$VMNameMatches" } | sort Name
# Determine how many VM's meet the VMNameMatch criteria. Save the count for later output.
if ($AllVMs -eq $null)
{
 write-output "No VMs match the pattern: $VMNameMatches"
 exit 1
}
else
{
    $LeftToGo = $AllVMs.Count
    if ($LeftToGo -eq $null)
    {
        $matchString = "Only one VM matched the pattern: {0}" -f $VMNameMatches
        $LeftToGo = 1
    }
    else
    {
    $matchString = "{0} VMs match the pattern: {1}" -f $AllVMs.Count, $VMNameMatches
 }
    write-output $matchString
}
# Process each VM and attempt to set the memory parameters
foreach ($myVM in $AllVMs)
{
    $LeftToGo = $LeftToGo - 1
    $HyperVGuest = $myVM.Name

    $Status = "Processing VM:{0} VMs Left:{1}" -f $myVM.Name, $LeftToGo
    Write-output $Status

    # Try to set the VM memory, it fails, catch the error and continue.

    try
    {
      set-vm $myVM -MemoryMB $MemMin -DynamicMemoryEnabled $True -DynamicMemoryMaximumMB $MemMax -DynamicMemoryBufferPercentage $MemBuffer -MemoryWeight $MemWeight
    }
    catch { }
}

As the note at the beginning of the script states, this version requires SCVMM SP1. Earlier versions of SCVMM used different parameter names for the dynamic memory settings. If you really need the script for the earlier version, let me know and I can get it for you.

Wrap Up

Well, that is it for today. I am sure that I have given you a lot to think about. I am still working on streamlining this process, but the sheer number of variables and my limited time makes the task somewhat daunting. I know this task is still somewhat complicated, so feel free to ask questions or post comments.

If you found this information useful and would like to be notified of future blog posts, please follow me on Twitter @pwilson98.

If you want all these scripts in an easy to download document, click this link: HyperVScripts.

Advertisements