I was recently asked to participate in a project, in which, the customer wanted the entire virtual desktop provisioning process to be automated based on pre-populated computer objects in AD.  I’m excited to share all I’ve learned with the hopes that it helps to demonstrate the level of automation that can be achieved with Citrix products and powershell.

Business Drivers

  • Provision hundreds of virtual desktops in minutes via an automated process.
  • Provisioned virtual desktops use PVS for streamed OS delivery.
  • Active Directory computer accounts are created by a separate automated process in advance of the virtual desktop provisioning process.

Powershell Script Snap-ins

  • XenServer 6 SDK
  • XenDesktop 5 SDK
  • PVS 5 SDK
  • Active Directory Module

These 4 powershell snap-ins will allow the whole process to be automated using one script.  The collective use of these snap-ins will allow us to automate:

  • XenServer Virtual Machine builds
  • Active Directory Computer account associations
  • Adding Virtual Machines to Catalogs and Desktop groups
  • Creating Target Devices in PVS and adding them to Device Collections

 Use Cases

  1. AD Computer accounts can be created independently of the virtual desktops.
  2. Computer names do not have to follow a naming scheme (i.e. BaseName##).
  3. Existing AD Computer accounts can be linked to new virtual desktops with the same names.

 

There is an awesome blog series by Ed York which talks about writing powershell scripts leveraging the XD 5 SDK.  I highly recommend checking those out for a more well rounded perspective on what can be automated and how it’s been done by others in the field.  This blog features a script I wrote, which is geared towards a specific use case, however I believe these methods can shed light on yet another way of accomplishing a high level of automation.

Powershell Script Goals

The customer for whom this script was created wanted to automate the whole virtual desktop process.  The driving factor was, when they do provision virtual desktops, it would generally be in batches of a few hundred.  Therefore a manual provisioning process was not desired to say the least.  In addition, they wanted to have another internal group handle the Active Directory computer account creation process.  According to the customer, the AD accounts could be created well in advance of the virtual desktops themselves.  The requirement was to automate the process of creating computer accounts in a specified OU by one internal group and then have the script written by Citrix Consulting look into that same specified OU and provision virtual desktops based on the recently added computer accounts in AD.  Not only does the script do this, it also creates virtual machines on Xenserver based on the computer account names.  In addition, using the same names in XD and PVS as the script executes.

Environment

The script runs from the Desktop Delivery Controller.  By default the XD SDK is installed with XD and just needs to be registered within the current shell.  To access all the cmdlets in my script, I usually just run the following command:

Add-PSSnapin *Citrix*,*PVS*

In addition, the Xenserver, PVS, and Active directory cmdlets need to be installed on the Desktop Delivery Controller for the added functionality.

  1. Xenserver SDK can be found and downloaded from the Citrix Community SDK Central site located here.
    1. Add-PSSnapin *Xen*
  2. Active Directory Module can be installed by adding the Active Directory Domain Services role to the Desktop Delivery Controller.  Once added, run the following command to add the cmdlets:
    1. Import-Module ActiveDirectory (no space between the words).
  3. Provisioning Services cmdlets are installed via a 2 step process.
    1. First, install the provisioning services console on the Desktop Delivery Controller.  This installs all the necessary binaries.
    2. Second, run the following command to install the MCLIPSSnapin for the PVS cmdlets:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\Installutil.exe “C:\Program Files\Citrix\Provisioning Services Console\MCLiPSSnapin.dll”

***A brief note, I have successfully ran this command against different versions of the .NET framework.  So, use whichever works for your environment.  I’ve also used both 32 and 64 bit.  However, for this blog, I used the 32 bit version of Powershell and .NET Framework.

The DDC which is to be used to run the script does not have to be an active DDC for which the VDA clients need to register or have ICA connections brokered.  It can be a part of the farm for access to all the objects but not actively used.  However, as the DDC is normally not a bottleneck, the production DDC can be used to run the script with the added installed binaries without negatively affecting performance or functionality.

Permissions

The script runs from the Desktop Delivery Controller by a user who is logged on with Domain Admin credentials.  However, this is not a requirement. 

Remote Script Execution

This script does not have to be executed from the Desktop Delivery Controller.  It can be executed from any client machine within the domain.  The Active Directory module can be installed by installing Microsoft’s Remote Server Administration Tool(RSAT).  This tool can be installed on a Windows 7 client for example.  Once installed, the Active Directory powershell module needs to be enabled as a feature from within Control Panel/Programs and Features/Turn Window features on and off.

Active Directory Module
Active Directory Module

The cmdlets for XenDesktop as well as the PVS snap-ins that also come with the XD SDK can be installed from the same media used to install XenDesktop.  The XenDesktop powershell snap-ins are included in MSI files for easy installation.  They can be found on the XenDesktop media in the following location: D:\x64\Citrix Desktop Delivery Controller (or: D:\x86\Citrix Desktop Delivery Controller, for the 32 bit version).

XenDesktop Powerhsell Snapins
XenDesktop Powerhsell Snapins

PVS Snapins (***Note, these PVS snapins are different than the ones that come with installing the PVS console and running installutil.exe)

XenDesktop PVS Snapin
XenDesktop PVS Snapin

The XenServer cmdlets can be installed the same way as the Desktop Delivery Controller installation.

The MCLI-PSSnapins installed via the PVS console can be installed the same way as on the Desktop Delivery Controller.

PVS Powershell SDK vs MCLIPSSnapin SDK

Due diligence compels me to speak to the differences between these 2 snap-ins.  The PVS snap-in gets installed with XenDesktop and contains cmdlets that allows the acquisition of information from the PVS server.  However, they do not contain any snap-ins that will allow you to create objects on the PVS server or to modify those objects properties.  For example, you cannot use them to create target devices in PVS.  In order to create target devices and assign them to device collections as well as vDisks, you need to use the MCLI-PSSnapins whose binaries are installed during the PVS console installation.

 

Cmdlets used in the Powershell Script

Now let’s talk about the specific cmdlets I used to accomplish each segment. 

Active Directory:

Cmdlet Name Description
Get-ADComputer This cmdlet acquires all the computer objects in a specified OU.

 

XenServer:

Cmdlet Name Description
Connect-XenServer Establishes a connection to the XenServer host.
Get-XenServer:SR Gets the storage repository in which the new virtual machines are to be created and stored.
Get-XenServer:VM Acquires a Virtual Machine object and returns it.
Invoke-XenServer:VM.Copy Copies a virtual machine and creates a new one from the original.
Get-XenServer:VM.VIFs Acquires the MAC address associated with a virtual machine.

 

XenDesktop:

Cmdlet Name Description
Get-BrokerMachine Acquires information associated with Xendesktop virtual machines and returns as an object.
Get-BrokerDesktopGroup Acquires object information about XenDesktop Desktop Group.
Get-Catalog Acquires object information about XenDesktop Catalogs.
Get-HypVMMacAddress Acquires all virtual machine MAC addresses and their associated Virtual Machine Host IDs from a connected Host server.
New-BrokerMachine Creates new XenDesktop virtual desktops.

 

Provisioning Services:

Cmdlet Name Description
Mcli-Run setupConnection Creates a remote connection to the PVS server.
Mcli-Add Device Creates a new target device in the PVS server and adds it to a device collection.
Mcli-Run AssignDiskLocator Assigns a vDisk to the target device.

 

Global Variables

[string]$MasterVM = Read-Host ‘Enter the Master VM Name:’

[string]$Storage = Read-Host ‘Enter the Storage Repository Name:’

[string]$XenDesktopOU = Read-Host ‘Enter the OU of the Computer accounts:’

[string]$XenDesktopGroup=Read-Host ‘Enter the name of the Desktop Group:’

[string]$XenDesktopCatalog=Read-Host ‘Enter the name of the Catalog:’

[string]$PVSServer=Read-Host ‘Enter the IP or DNS Name of the PVS server:’

[string]$XenServer=Read-Host ‘Enter the IP of the XenServer host or Pool Master:’

[string]$XenServerUser=Read-Host ‘Enter the username for Xenserver connection:’

[string]$XenServerPwd=Read-Host ‘Enter the password for Xenserver connection:’

$ADC = Get-ADComputer -LDAPFilter “(name=*)” -searchbase $XenDesktopOU

$HYP=’XDHyp:\Connections\XLXS’

$NewDesktopCount = 0

$DesktopGroup = Get-BrokerDesktopGroup –Name $XenDesktopGroup

$Catalog = Get-BrokerCatalog -Name $XenDesktopCatalog

$StoreRecord=mcli-get store -f StoreID

$SiteRecord=mcli-get store -f SiteID

$StoreID = foreach ($a in $StoreRecord) { if ($a -match ‘-‘) {$a.Replace(‘storeId: ‘,”)}}

$SiteID = foreach ($b in $SiteRecord) { if ($b -match ‘-‘) {$b.Replace(‘siteId: ‘,”)}}

There are 9 variables which are listed first and denoted by ‘[string]’.  With these 9 variables, I prompt the user to enter the required information in oppose to statically coding the values.  This will allow the person running the script to choose:

  • Master VM to use as a base for the copies
  • Storage repository to store the VMs
  • OU to acquire the AD computer accounts
  • Catalog and Desktop group necessary for XenDesktop
  • IP or DNS name of the PVS server
  • IP or DNS name of the XenServer host
  • Credentials for the XenServer host connection

 

The $HYP variable contains the connection XenDesktop uses to connect to the host server containing the virtual machines.  XDHYP:\ is a Powershell drive in which you can navigate and list directories.  Once here you can navigate to ‘\Connections’.  Within connections, will be whatever you would see within the Hosts node of Desktop Studio:

XenDeskop Hypervisor Provider
XenDeskop Hypervisor Provider

In my lab, the connection to my XenServer from within Desktop Studio is called XLXS.  This is the value I ultimately, statically coded.  If multiple hosts are used within your environment, you can just use the Resource Pool master’s connection name.

Desktop Studio Host Configuration
Desktop Studio Host Configuration

Finally, in regards to the global variables, is the Store and Site IDs which are necessary for creating new target devices in PVS.  This script was designed around a single site.  In a future blog, I’ll talk about accommodating multiple sites in PVS, within the script.

Script Output

Below are images that illustrate the output of the script:

Active Directory Computer Objects
Active Directory Computer Objects
Provisioned VMs in XenServer
Provisioned VMs in XenServer
Provisioned Desktops in Desktop Studio
Provisioned Desktops in Desktop Studio
Provisioned Target Devices in PVS
Provisioned Target Devices in PVS

 

The Script

Import-Module ActiveDirectory
Add-PSSnapin *citrix*,*pvs*,*xen*,*mcli*
##*******************************************************************
##*******************************************************************
##Global Variables
[string]$MasterVM = 'nht2' ##Read-Host 'Enter the Master VM Name:'
[string]$Storage = 'local storage 2' ##Read-Host 'Enter the Storage Repository Name:'
[string]$XenDesktopOU = 'OU=Client VMs,OU=Citrix Infrastructure,DC=DomainName,DC=net' ##Read-Host 'Enter the OU of the Computer accounts:'
[string]$XenDesktopGroup= 'Windows 7 Virtual Desktop' ##Read-Host 'Enter the name of the Desktop Group:'
[string]$XenDesktopCatalog= 'Persistent Windows 7' ##Read-Host 'Enter the name of the Catalog:'
[string]$PVSServer= '192.168.1.138' ##Read-Host 'Enter the IP or DNS Name of the PVS server:'
[string]$XenServer= '192.168.1.133' ##Read-Host 'Enter the IP of the XenServer host or Pool Master:'
[string]$XenServerUser= 'root' ##Read-Host 'Enter the username for Xenserver connection:'
[string]$XenServerPwd= '123456' ##Read-Host 'Enter the password for Xenserver connection:'
##*******************************************************************
##The following line of code is the first step in creating an object for message box prompts.
$MsgBoxObj = New-Object -ComObject wscript.shell

##Verify connectivity to the PVS server and Xenserver before initiating the process.  If  there is an error, quit the script.
##*******************************************************************
##*******************************************************************
$Error.Clear()
Mcli-Run setupConnection -p server=$PVSServer
if($error)
{
$MsgBoxObj.Popup('Unsuccessful connection to the PVS Server.  Exiting Script!')
exit;

}

##*******************************************************************
##*******************************************************************
##Acquire the list of computer names from the specified OU in AD.  If there is an error, quit the script.
$Error.Clear()
$ADC = Get-ADComputer -LDAPFilter "(name=*)" -searchbase $XenDesktopOU

if ($error)
{

$MsgBoxObj.Popup("Cannot locate AD Computer objects with path specified.  Please try again.  Exiting script!")
exit;

}
##*******************************************************************
##*******************************************************************
##XenDesktop hypervisor connection, as listed within Desktop Studio.
##I also initiate a variable to 0 in order to use it to count the amount of successfully created virtual desktops.
$HYP='XDHyp:\Connections\XLXS'
$NewDesktopCount = 0

##*******************************************************************
##*******************************************************************
##Acquire the XenDesktop Group for the new desktops.
##Error catching for non-existent XenDesktop Group.
$Error.Clear()
$DesktopGroup = Get-BrokerDesktopGroup -Name $XenDesktopGroup

if($Error)
{ 
$MsgBoxObj.Popup("The Desktop Group name you entered does not exist. Please try again.  Exiting script!")
exit;      

}

##*******************************************************************
##*******************************************************************
##Acquire the Catalog for new desktops.
##Error catching for non-existent XenDesktop Catalog.
$Error.Clear()
$Catalog = Get-BrokerCatalog -Name $XenDesktopCatalog

if ($Error)
{
$MsgBoxObj.Popup("The Catalog name you entered does not exist.  Please try again.  Exiting script!")
exit;

}

##*******************************************************************
##*******************************************************************
##Acquire accounts to be provisioned from Active Directory and evaluate them one at a time.
foreach ($x in $ADC)

{
     if(Get-BrokerMachine -HostedMachineName $x.Name)

    {
       $x.Name+' :Is already a XenDesktop Virtual Desktop!'
}
##Before I inserted this elseif statement, the script was retreiving the master VM twice, which threw off the rest of the script. 

##The get-xenserverVM cmdlet was receiving 2 instaces of the Master VM object.
  elseif($x.Name -eq $MasterVM)  
    {
        $x.Name+' :Is the Master VM Image and will not be copied!'
    }
    else
    {

##*******************************************************************
##*******************************************************************
##Begin creating VMs based on AD Computer names.
##Create XenServer connection.
##Error catching for XenServer connection.
$Error.Clear()
$O = Connect-XenServer -Server $XenServer -UserName $XenServerUser -Password $XenServerPwd
if($Error)

{
$MsgBoxObj.Popup("The connection to Xenserver was not successful.  Please re-enter the required parameters.  Exiting script!")
exit;

}

##*******************************************************************
##*******************************************************************
##Acquire storage for new VMs.
##In a future blog, I'll account for dynamically managing multiple storage repositories
##Error catching for non-existent Storage or, insufficient disk space.
$Error.Clear()
$SR = Get-XenServer:SR -NameFilter $Storage

if($Error)

{
$MsgBoxObj.Popup("The local storage you entered does not exist.  Please check the name of the local storage and try again.  Exiting script!")
exit;

}

##*******************************************************************
##*******************************************************************
##Error catching for non-existent MasterVM name.
$Error.Clear()
$MVM = Get-XenServer:VM -NameFilter $MasterVM

if($Error)
{
$MsgBoxObj.Popup("The Master VM name you entered does not exist.  Please check the name and try again.  Exiting script!")
exit;

}

##*******************************************************************
##*******************************************************************
##Create VMs on Xenserver Host.
##Error catching for copying of VM.
$Error.Clear()
$O=Invoke-XenServer:VM.Copy -VM $MVM.uuid -NewName ($x.Name) -SR $SR.uuid

if($Error)

{

$MsgBoxObj.Popup("The VM copy was not successful, please check evailable disk space.  Exiting script!")
exit;

}

##*******************************************************************
##*******************************************************************
##Error catching for recently created VM on XenServer.
$Error.Clear()
$XSVM = Get-XenServer:VM -NameFilter ($x.Name)

if($Error)

{
$MsgBoxObj.Popup("Error retreiving information for the recently created VM.  Exiting script!  Here is the error:  $Error")
exit;

}

##*******************************************************************
##*******************************************************************
##Error catching for recently created VM's MAC Address.
$Error.Clear()
$VMMacObject = Get-XenServer:VM.VIFs -VM $XSVM.uuid

if($Error)

{
$MsgBoxObj.Popup("Error receiving the MAC address of the recently created VM.  Exiting script! Here is the error:  $Error")
exit;

}

##*******************************************************************
##*******************************************************************
##Error catching for HypVMMacAddress command which acquires all MAC addresses on the host server.
$Error.Clear()
$VMInfo = Get-HypVMMacAddress -LiteralPath $HYP

if($Error)

{

$MsgBoxObj.Popup("Error acquiring the list of MAC addresses of the VMs on the host server.  Exiting script!")
exit;

}

##*******************************************************************
##*******************************************************************
##Error catching for MAC address of recently created VM.
$Error.Clear()
$GetVmId = $VMInfo | Where-Object {$_.MacAddress -eq $VMMacObject.MAC}

if($Error)

{
$MsgBoxObj.Popup("Error acquiring the MAC address of the newly created VM.  Exiting script!")
exit;

}

##*******************************************************************
##*******************************************************************
##Error catching for new XenDesktop Broker Machine.
$Error.Clear()
$O=New-BrokerMachine -CatalogUid 1 -HostedMachineId $GetVmId.VMID -HypervisorConnectionUid 2 -MachineName ('DomainName\'+$x.Name+'$')
if($Error)

{
$MsgBoxObj.Popup("Error creating new XenDesktop Broker Machine.  Exiting script!  Here is the error: $Error")
exit;

}

##*******************************************************************
##*******************************************************************
## PVS connection from DDC.
Mcli-Run setupConnection -p server=$PVSServer
$StoreRecord=mcli-get store -f StoreID
$SiteRecord=mcli-get store -f SiteID
$StoreID = foreach ($a in $StoreRecord) { if ($a -match '-') {$a.Replace('storeId: ','')}}
$SiteID = foreach ($b in $SiteRecord) { if ($b -match '-') {$b.Replace('siteId: ','')}}

##*******************************************************************
##*******************************************************************

        $Device = $x.Name
        $MAC = $GetVmId.MacAddress.replace(':','-')
##*******************************************************************
##*******************************************************************
##Error catching for the adding of a new target device to a collection in PVS.
$Error.Clear()
$O=Mcli-Add Device -r DeviceName=$Device, deviceMac=$MAC, description='This is a Rocking Script!!!', collectionName='Device Collection Name', bootfrom=1, siteid=$SiteID

if($Error)

{
    $MsgBoxObj.Popup("There was an error adding the device to the collection in PVS.  Exiting Script!  Here is the error:  $Error")
    exit;

}

##*******************************************************************
##*******************************************************************
##Error catching of the vDisk for the new Target Device.
$Error.Clear()
$O=Mcli-Run AssignDiskLocator -p DeviceName=$Device,DiskLocatorName='NRLAB',siteId=$SiteID,storeId=$StoreID

if($Error)

{
    $MsgBoxObj.Popup("There was an error adding the vDisk to the Target Device in PVS.  Exiting Script!  Here is the error:  $Error")
}

##*******************************************************************
##*******************************************************************

$NewDesktopCount++
    }

}
##*******************************************************************
##*******************************************************************
##Error catching for the inability to add the machines into a Desktop group from a catalog.
$Error.Clear()
Add-BrokerMachinesToDesktopGroup -Catalog $Catalog -DesktopGroup $DesktopGroup -Count $NewDesktopCount

if($Error)
{
$MsgBoxObj.Popup("There was an error adding the XenDesktop machines from the Catalog to the XenDesktop group.  Exiting Script!  Here is the error:  $Error")
exit;

}