In my PowerShell Scripts for XenDesktop Part 2 back in April, I provided a script for copying the PVS write-cache drive and attaching them to virtual machines using the SCVMM library. Most write-cache drives are between 2GB and 4GB, which means if you had 1000 VMs with 4GB drives the SCVMM library would need to transfer 4TB worth of data. The script I provided earlier works, but the SCVMM server becomes the bottleneck because it needs to copy the write-cache drive once for each VM. If you have a slow network or SAN, the better approach is to use multiple machines to copy the files in parallel and later mount them to the VMs.

As it turns out, SCVMM doesn’t have an easy way to mount an existing VHD on a machine that is not already in the library unless you create it using the New-VM command with the -UseLocalVirtualHardDisks option. The original version of the MountVHD PowerShell script came from Taylor Brown’s blog and was initially modified by Loay Shbeilat who was helping me at the EEC during our first attempt at building a XenDesktop farm on a Microsoft cluster.

Since I already had the VMs, so the first step was to copy the write-cache VHDs into the VM folders. I created a batch file that took a list of VMs as input from a text file and copied the write-cache VHD into each folder. Here is the batch file:

CopyVHD Batch File
@echo off
echo USAGE:   CopyVHD [Textfile] [SrceFilePath] [SrceFileName] [DestPath]
echo.
echo WHERE:
echo  [Textfile] = the textfile containing list of VMs to copy the VHD <span class="code-keyword">for</span>
echo  [SrceFilePath] = the source path of the VHD to be echo copied locally then to the VMS
echo  [SrceFileName] = the name of the VHD file to copy
echo  [DestDrive] = the destination drive where the VM folders exist
echo.
echo EXAMPLE: CopyVHD c:\vms.txt \\SCVMM\MSCVMMLibrary\VHDS\ writecache.vhd f:\
echo.
echo.
echo PURPOSE:Script will copy the VHD file to the c:\ drive and then copy
echo   the file to each of the VM folders as specified in the TextFile
echo.
echo Press Ctrl+C to <span class="code-keyword">break</span> out <span class="code-keyword">if</span> you did not supply all 4 parameters.
echo.
echo %0 %1 %2 %3 %4
echo.
pause

copy %2%3 c:\

<span class="code-keyword">for</span> /F %%I in (%1) <span class="code-keyword">do</span> copy c:\%3 %4%%I\%%I.vhd

If you need a quick PowerShell script to create the textfiles for each host, here is a bit of code I have been using:

DumpVMs PowerShell Script
$AllHosts = Get-VMHost -VMMServer localhost
foreach ($myhost in $AllHosts)
{
$server = $myhost.FQDN
write-output $server
$file = New-Item -type file <span class="code-quote">"c:\$server.txt"</span>
$AllVMs = Get-VM -VMMServer localhost | where {$_.HostName -eq $myhost.FQDN}
foreach ($myVM in $AllVMs)
{
    add-content $file $myVM.Name
}
}

The MountVHD PowerShell script is used to get a list of the virtual machines that match a supplied naming pattern and then attach a write-cache VHD file to the virtual machine. The script assumes the file is in the root of the virtual machine’s folder. Since the original Gen-VMs Powershell script from PowerShell Scripts for XenDesktop Part 5 does not add a virtual IDE drive, that still needs to be done. The end of the script uses Taylor’s WMI code to both create the IDE virtual disk drive and attach the VHD.

MountVHD Syntax

Usage: MountVHD.ps1 LocalVMStoragePath VMNameMatch Postpend

Where:
LocalVMStoragePath= The drive path local to the Hyper-V host that has the virtual machine VHDs
VMNameMatch= The name pattern to match the VMs that will be processed
Postpend= A string to be appended to the end of the file name. If no string is necessary pass “”

Example:
PS C:\> .\MountVHD.ps1 “E:\Hyper-V” “HVDesktop01” “_wc”

In this example the E:\Hyper-V\HVDesktop01\HVDesktop01_wc.vhd is attached to a virtual machine named HVDesktop01

MountVHD PowerShell Script
# Purpose:    This script attaches an existing VHD to a virtual machine. Designed <span class="code-keyword">for</span> deploying XenDesktop and attaching write
#             cache drives to existing VMs.
# Date:       28 Oct 2010
# Authors:    Loay Shbeilat and Paul Wilson (no implied or expressed warranties) with content taken from Taylor Brown's blog:
#             http:<span class="code-comment">//blogs.msdn.com/b/taylorb/archive/2008/10/13/pdc-teaser-attaching-a-vhd-to-a-virtual-machine.aspx
</span>
# Function ProcessWMIJob used to add the <span class="code-keyword">new</span> Virtual Disk and VHD at the end of the script.

filter ProcessWMIJob
{
    param
    (
        [string]$WmiClassPath = $<span class="code-keyword">null</span>,
        [string]$MethodName = $<span class="code-keyword">null</span>
    )

    $errorCode = 0

    <span class="code-keyword">if</span> ($_.ReturnValue -eq 4096)
    {
        $Job = [WMI]$_.Job

        <span class="code-keyword">while</span> ($Job.JobState -eq 4)
        {
            Write-Progress $Job.Caption <span class="code-quote">"% Complete"</span> -PercentComplete $Job.PercentComplete
            Start-Sleep -seconds 1
            $Job.PSBase.Get()
        }
        <span class="code-keyword">if</span> ($Job.JobState -ne 7)
        {
            <span class="code-keyword">if</span> ($Job.ErrorDescription -ne "")
            {
                Write-Error $Job.ErrorDescription
                Throw $Job.ErrorDescription
            }
            <span class="code-keyword">else</span>
            {
                $errorCode = $Job.ErrorCode
            }
        }
        Write-Progress $Job.Caption <span class="code-quote">"Completed"</span> -Completed $TRUE
    }
    elseif($_.ReturnValue -ne 0)
    {
        $errorCode = $_.ReturnValue
    }

    <span class="code-keyword">if</span> ($errorCode -ne 0)
    {
        Write-Error <span class="code-quote">"Hyper-V WMI Job Failed!"</span>
        <span class="code-keyword">if</span> ($WmiClassPath -and $MethodName)
        {
            $psWmiClass = [WmiClass]$WmiClassPath
            $psWmiClass.PSBase.Options.UseAmendedQualifiers = $TRUE
            $MethodQualifiers = $psWmiClass.PSBase.Methods[$MethodName].Qualifiers
            $indexOfError = [<span class="code-object">System</span>.Array]::IndexOf($MethodQualifiers[<span class="code-quote">"ValueMap"</span>].Value, [string]$errorCode)
            <span class="code-keyword">if</span> ($indexOfError -ne <span class="code-quote">"-1"</span>)
            {
                Throw <span class="code-quote">"ReturnCode: "</span>, $errorCode, <span class="code-quote">" ErrorMessage: '"</span>, $MethodQualifiers[<span class="code-quote">"Values"</span>].Value[$indexOfError], <span class="code-quote">"' - when calling $MethodName"</span>
            }
            <span class="code-keyword">else</span>
            {
                Throw <span class="code-quote">"ReturnCode: "</span>, $errorCode, <span class="code-quote">" ErrorMessage: 'MessageNotFound' - when calling $MethodName"</span>
            }
        }
        <span class="code-keyword">else</span>
        {
            Throw <span class="code-quote">"ReturnCode: "</span>, $errorCode, <span class="code-quote">"When calling $MethodName - <span class="code-keyword">for</span> rich error messages provide classpath and method name."</span>
        }
    }
    <span class="code-keyword">return</span> $_
}

# Parse the command-line and verify the 3 required parameters are present, <span class="code-keyword">if</span> not display usage info
<span class="code-keyword">if</span> ($args -eq $<span class="code-keyword">null</span> -or $args.Count -lt 3)
{
    write-output <span class="code-quote">"Usage: MountVHD.ps1 LocalVMStoragePath VMNameMatch Postpend"</span>
    write-output <span class="code-quote">"Example: .\MountVHD.ps1 "</span><span class="code-quote">"E:\Hyper-V"</span><span class="code-quote">" "</span><span class="code-quote">"HVDesktop01"</span><span class="code-quote">" "</span><span class="code-quote">"_wc"</span><span class="code-quote">" "</span>
    write-output <span class="code-quote">"Function: Adds a IDE drive and attachs an existing VHD to the VM."</span>
    write-output <span class="code-quote">"In <span class="code-keyword">this</span> example the E:\Hyper-V\HVDesktop01\HVDesktop01_wc.vhd is attached HVDesktop01"</span>
    exit 1
}

# Place the command-line parameters into named variables <span class="code-keyword">for</span> 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 <span class="code-quote">"$VMNameMatches"</span> } | sort Name

# Determine how many VM's meet the VMNameMatch criteria. Save the count <span class="code-keyword">for</span> later.

<span class="code-keyword">if</span> ($AllVMs -eq $<span class="code-keyword">null</span>)
{
  write-output <span class="code-quote">"No VMs match the pattern: $VMNameMatches"</span>
  exit 1
}
<span class="code-keyword">else</span>
{
    $LeftToGo = $AllVMs.Count
    <span class="code-keyword">if</span> ($LeftToGo -eq $<span class="code-keyword">null</span>)
    {
        $matchString = <span class="code-quote">"Only one VM matched the pattern: {0}"</span> -f $VMNameMatches
        $LeftToGo = 1
    }
    <span class="code-keyword">else</span>
    {
     $matchString = <span class="code-quote">"{0} VMs match the pattern: {1}"</span> -f $AllVMs.Count, $VMNameMatches
  }
    write-output $matchString
}

# <span class="code-object">Process</span> 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 <span class="code-keyword">if</span> yours is not in the VM directory.

    $vhdToMount = <span class="code-quote">"{0}\{1}\{1}{2}.vhd"</span> -f $VHDPath, $myVM.Name, $PostPend

    $Status = <span class="code-quote">"Processing VM:{0} VHD:{1} VMs Left:{2}"</span> -f $myVM.Name, $vhdToMount, $LeftToGo
    Write-output $Status

    # Try to attach, <span class="code-keyword">if</span> that fails... <span class="code-keyword">catch</span> error and <span class="code-keyword">continue</span>
    # This bit of code is from Taylor's blog.

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

        $DefaultDiskDrive.Parent = $VmIdeController.__Path
        $DefaultDiskDrive.Address = 0
        $NewDiskDrive = ($VMManagementService.AddVirtualSystemResources($Vm.__Path, $DefaultDiskDrive.PSBase.GetText(1)) | ProcessWMIJob $VMManagementService <span class="code-quote">"AddVirtualSystemResources"</span>).NewResources

        $DiskAllocationSetting = Get-WmiObject -computername $server -Namespace <span class="code-quote">"root\virtualization"</span> -Query <span class="code-quote">"SELECT * FROM Msvm_AllocationCapabilities WHERE ResourceSubType = 'Microsoft Virtual Hard Disk'"</span>
        $DefaultHardDisk = (Get-WmiObject -computername $server -Namespace <span class="code-quote">"root\virtualization"</span> -Query <span class="code-quote">"Associators of {$DiskAllocationSetting} Where ResultClass=Msvm_ResourceAllocationSettingData AssocClass=Msvm_SettingsDefineCapabilities"</span> | where-object {$_.InstanceID -like <span class="code-quote">"*Default"</span>})

        $DefaultHardDisk.Parent = $NewDiskDrive
        $DefaultHardDisk.Connection = $vhdToMount

        $VMManagementService.AddVirtualSystemResources($Vm.__Path, $DefaultHardDisk.PSBase.GetText(1)) | ProcessWMIJob $VMManagementService <span class="code-quote">"AddVirtualSystemResources"</span>
    }
    <span class="code-keyword">catch</span> { }

}

If your write-cache file does not reside in the virtual machine folder, you can modify the line $vhdToMount = “{0}{1}{1}{2}.vhd” -f $VHDPath, $myVM.Name, $PostPend line to have the file path or pattern for your environment.

Just to recap the process, here are the steps after creating the virtual machines:

1. Run the DumpVMs powershell script to create a text file with VMs from each host
2. Run the CopyVHD batch file with the input from DumpVMs script output
3. Run the MountVHD powershell script to attach the newly copied VHDs to the VMs

Well that is about all there is to it!

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