Citrix XenDesktop 7.x VDI deployment with PowerShell using vSphere, local storage and PVS

At a customer a while back we had an Citrix XenDesktop 7.9 environment. VMware vSphere 6.0 was used as hypervisor, Citrix Provisioning Services 7.9 for deployment and Microsoft Windows 10 as OS for the virtual desktops. Since these desktops were non-persistent we were able to use the local solid state storage in the hypervisors. This did however present some challenges when it came to the deployment of the virtual desktops. We (my colleague Leon van Efferen and myself) ended up with a PowerShell script that did just about everything.

In this post I will explain what the script does and what you need to do to use it in your own XenDesktop/PVS/vSphere environment. You can find the actual script at the end of the post.
Keep in mind that the customer used VMware vSphere (XenServer or Hyper-V were a big no-no for unknown reasons) and therefore the script only contains the commands for vSphere (PowerCLI 6.x). If you would like to use it for other hypervisors, feel free to alter it in any way and present it as your own on your own blogsite.

How and why:
When deploying virtual desktops with Citrix Provisioning Services you first need to create the virtual machines on your hypervisor. This includes deploying them from a template, create devices (based on the MAC address) in Provisioning Service, add the correct vDisk to the devices, create computer accounts in Active Directory and add them to a XenDesktop Machine Catalog.
PVS presents you with two wizards to help you with these tasks. The Streamed VM Wizard and the XenDesktop Setup Wizard. The difference between the two is that de latter one will also add the machine to a Machine Catalog in your XenDesktop site.
While these wizards are very powerful and should be your first choice for deploying your virtual desktops, they unfortunately did not fit our needs. I will try to explain why.
The mentioned wizards require a template from which to create the virtual machines. Since we are using PVS this is just an empty template with the correct virtual hardware configuration, and (if you use Cache in memory with overflow to disk) a local (formatted) drive. This template should be available for all hypervisor hosts and therefore should be on shared storage. The wizards however deploy the resulting virtual machine on the same storage as the template. This would mean that all our virtual machines end up in shared storage which is something we didn’t want or need.

So our only options were to do everything manually or automate it with PowerShell. For obvious reasons we chose the latter and this was actually a fun thing to do. As I have said before in other posts: I am in no way a PowerShell expert and usually go by the rule ‘If it looks stupid but it works, it ain’t stupid’, so keep that in mind.

Because the wizards also added the Boot Device Manager partition to the VM’s we needed to create a template that already includes the BDM partition.
(Check this excellent blog by George Spiers for more information about the various boot methods of PVS.)

Basically the goals were as follows:
– Deploy the virtual desktops on local storage from a template on shared storage
– Add the virtual desktops to PVS with the correct vDisk
– Add the virtual desktops to a XenDesktop Machine Catalog
– Add computer accounts to Active Directory for the virtual desktops
– Choose the least used hypervisor and the least used network

Unfortunately we had multiple networks configured in vSphere with one being smaller than the rest (/23 subnet instead of /22). This meant that getting the least used network based on the amount of machines in each network wouldn’t work so we used percentages along with the maximum network size instead.

Script information:
Like I have said before, this script is not a one-size-fits-all (or one-script-automates-all) solution, so you would need to alter it to accommodate your environment. There are a lot of variables that need to be changed. I have commented these with REPLACE THIS! and REPLACE THIS IF NEEDED! notices. I have tried to make all the other comments and variables as easy to understand as possible so a experienced Citrix and vSphere administrator shouldn’t have any problem filling this out.

So save the script as ps1 file. Change all the variables and run it. It will ask you what kind of virtual machine you want to create (Acceptance or Production, if you have that kind of setup) and how many virtual machines you want. It will provide an overview of what it will do for you and, after confirmation, will start deploying. During deployment it will skip virtual machine names that already exist. So if there are some missing in your overall numbering, it will fill in the missing ones.
Keep in mind that it will use the local storage of your hypervisor hosts. If there are multiple local datastores, it will use the one with the most space available.

Known issues so far:
There was one known issue when using this script. When creating the virtual machine on vSphere, it will actually do this against the hypervisor host itself. It will take a while (20 to 60 seconds) for vCenter to notice there is a new VM on the host. Only after that can you add it to a XenDesktop Machine Catalog. To work around this it will try adding it three times with 30 seconds interval (continuing if it is successful of course).
The script also contains a password it plain text. I know this isn’t the smartest thing to do, but hey, I was just lazy. Feel free to add a credential prompt.

Wrapping things up:
That’s it again for now. Feel free to use this script, alter it, publish it. If you have any tips or improvements, I would be happy to hear them.
Next up is another script that I created for another customer to update virtual desktops through Machine Creation Services (without doing it from Citrix Studio).

The actual script:

# SCRIPT INFO -------------------
# --- VDI non-persistent deployment script ---
# By Leon van Efferen & Chris Jeucken
# v0.99
# Various script sections borrowed from Thomas Fuhrmann
# -------------------------------------------------------
# Create target devices divided over two VMware vSphere clusters with local storage and multiple VLANs
# Run on machine with:
#   - Citrix Studio
#   - Citrix Provisioning Services Console
#   - VMware vSphere PowerCLI 6.x
# -------------------------------

# PREREQUISITES -----------------
    Write-Host "1. Import Modules and Snapins" -ForegroundColor Green
    $ErrorActionPreference = 'Stop'
# Import Active Directory PowerShell module ("Active Directory Module for Windows Powershell"-feature has to be installed on the machine where the script is executed)
    Import-Module ActiveDirectory
# Add vSphere PowerShell Snapin
    Import-Module -Name "VMware.VimAutomation.Core"  
# Install and add Citrix PowerShell Snapins
    $InstallCTXUtil = $env:systemroot + '\Microsoft.NET\Framework64\v4.0.30319\installutil.exe'
    & $InstallCTXUtil $env:programfiles + '\Citrix\Provisioning Services Console\Citrix.PVS.SnapIn.dll' | Out-Null
    Add-PSSnapin Citrix* | Out-Null
# Set decimal culture
    $DecimalCulture = New-Object System.Globalization.CultureInfo -ArgumentList "en-us",$false
    $DecimalCulture.NumberFormat.PercentDecimalDigits = 2
    Write-Host "`n"
# -------------------------------

    Write-Host "2. Set transcending variables" -ForegroundColor Green
# Define vCenter server hostname and credentials
	$VCHost = "vCenter01.local.lan" # <<< REPLACE THIS!
	$VCUser = "LOCAL\SVC-vCenter" # <<< REPLACE THIS!
	$VCPassword = "Password01" # <<< REPLACE THIS!
# Define Provisioning Services server hostname
	$PVSHost = "PVS1.local.lan" # <<< REPLACE THIS!
# Define XenDesktop Delivery Controller hostname
    $XDDCHost = "XDDC1.local.lan" # <<< REPLACE THIS!
# Define maximum amount of VM's per host
    $MaxVMHost = 65  # <<< REPLACE THIS!
# Define Active Directory domain for deployment
    $DomainFQDN = "LOCAL.LAN"  # <<< REPLACE THIS!
    $DomainNetBIOS = "LOCAL"  # <<< REPLACE THIS!
# Define Provisioning Services device description
	$PVSDescription = "Deployed with VDI deploy script -" # <<< REPLACE THIS!
# Define Provisioning Services site for deployment
	$PVSSite = "PVS-Site" # <<< REPLACE THIS!
# Define Provisioning Services store for deployment
    $PVSStore = "PVS-Store" # <<< REPLACE THIS!
# Define XenDesktop Hypervisor Connection name
    $XDHypervisorName = "vSphere-Hypervisor" # <<< REPLACE THIS!
# Setup current time variable
    $CurrentTime = (Get-Date).ToString('dd/MM/yyyy HH:mm:ss')   
    Set-PSBreakpoint -variable currenttime -mode Read -Action { $global:CurrentTime = (Get-Date).ToString('dd/MM/yyyy HH:mm:ss') } | Out-Null
# Define global deployment result variable
    [System.Collections.ArrayList]$GlobalDeployResults = @()
    Write-Host "`n"
# -------------------------------

	Write-Host "3. Set environment specific variables" -ForegroundColor Green
# Define template to be used for deployment
	$VMTemplateName = "VM-Template" # <<< REPLACE THIS!
# Define vSphere datacenter and clusters for deployment
    $VMDatacenterName = "Datacenter" # <<< REPLACE THIS!
	$VMClusterName1 = "Session-1" # <<< REPLACE THIS!
	$VMClusterName2 = "Session-2" # <<< REPLACE THIS!
# Define networkprefix of VDI networks
	$NetworkA = "VDI-A-" # <<< REPLACE THIS!
	$NetworkP = "VDI-P-" # <<< REPLACE THIS!
    $NetworkSize = 1000 # <<< REPLACE THIS!
# Define smaller networks (if any)
    $SmallNetwork1 = "VDI-P-01" # <<< REPLACE THIS IF NEEDED!
    $SmallNetwork1Size = 500 # <<< REPLACE THIS IF NEEDED!
# Define blocked network for deployment (if any)
    $BlockedNetwork1 = "VDI-P-02" # <<< REPLACE THIS IF NEEDED!
    $BlockedNetwork2 = "" # <<< REPLACE THIS IF NEEDED!
# Define VM name prefix and first number
	$VMPrefix = "VDI-W10-" # <<< REPLACE THIS!
    $VMNumber = 1
# Define Active Directory organizational unit for deployment
	$OUA = "Resources/VDI-W10/A" # <<< REPLACE THIS!
	$OUP = "Resources/VDI-W10/P" # <<< REPLACE THIS!
# Define Provisioning Services device collection for deployment
	$PVSCollectionA = "Acceptance" # <<< REPLACE THIS!
	$PVSCollectionP = "Production" # <<< REPLACE THIS!
# Define Provisioning Services vDisk for deployment
    $PVSvDiskA = "vDisk-Win10-v1" # <<< REPLACE THIS!
    $PVSvDiskP = "vDisk-Win10-v2" # <<< REPLACE THIS!
# Define XenDesktop Machine Catalog for deployment
    $XDCatalogNameA = "Catalog-Acceptance" # <<< REPLACE THIS!
    $XDCatalogNameP = "Catalog-Production" # <<< REPLACE THIS!
    Write-Host "`n"
# -------------------------------

    Write-Host "4. Ask for user-defined parameters" -ForegroundColor Green
    Write-Host "`n"

# Ask for variables
	Write-Host "For which environment do you want to deploy VDIs?" -ForegroundColor Yellow
	$Environment = Read-Host "Press A for ACCEPTANCE or P for PRODUCTION"

	If ($Environment -ne "A" -and $Environment -ne "P") {
		Write-Host "You have supplied an invalid answer." -ForegroundColor Red

	Write-Host "`n"

# Set variables
	if ($Environment -eq "A") {
        $PVSCollection = $PVSCollectionA
        $PVSvDisk = $PVSvDiskA  
		$OU = $OUA
		$XDCatalogName = $XDCatalogNameA
		$Network = $NetworkA

	If ($Environment -eq "P") {
        $PVSCollection = $PVSCollectionP
        $PVSvDisk = $PVSvDiskP
		$OU = $OUP
		$XDCatalogName = $XDCatalogNameP
		$Network = $NetworkP

# Ask for amount of VDIs to create
    Write-Host "How many VDIs do you want to create?" -ForegroundColor Yellow
    $Num_VMs_Total = Read-Host "Please specify the amount"
	If ($Num_VMs_Total -le 0) {
		Write-Host "Please enter number greater than 0"
		$Num_VMs_Total = 1
    Write-Host "`n"

# Provide planned deployment summary
	Write-Host "VMs will be created as follows:"
	Write-Host "Amount = " -NoNewline
	Write-Host "$Num_VMs_Total VMs" -ForegroundColor Yellow
	Write-Host "Name prefix = " -NoNewline 
	Write-Host "$VM_Prefix" -ForegroundColor Yellow 
	Write-Host "Network = " -NoNewline 
	Write-Host "$Network" -ForegroundColor Yellow
	Write-Host "Provisioning Services = " -NoNewline
	Write-Host "Collection $PVSCollection in site $PVSSite with vDisk $PVSvDisk" -ForegroundColor Yellow 
	Write-Host "Active Directory OU = " -NoNewline
	Write-Host "$OU" -ForegroundColor Yellow 
	Write-Host "Hypervisor = " -NoNewline
	Write-Host "Cluster $VMClusterName1 and $VMClusterName2 in datacenter $VMDatacenterName" -ForegroundColor Yellow
	Write-Host "XenDesktop = " -NoNewline
	Write-Host "Machine catalog $XDCatalogName" -ForegroundColor Yellow
	Write-Host "--------------------------------------------"
	Write-Host "Would you like to continue?" -ForegroundColor Yellow
	$Continue = Read-Host "Press (Y)es to continue or any other key to quit."
	If ($Continue -ne "Y") {
		Write-Host "Script cancelled by user" -ForegroundColor Red
# -------------------------------

# SCRIPT ------------------------
	Write-Host "5. Create machines" -ForegroundColor Green
	Write-Host "5.1 Connect to backend servers" -ForegroundColor Cyan
# Connect to vCenter server
	Write-Host "5.1.1 Connect to vCenter server" -ForegroundColor Magenta
	Set-PowerCLIConfiguration -InvalidCertificateAction "Ignore" -DisplayDeprecationWarnings:$false -Confirm:$false | out-Null
	Connect-VIServer -Server $VCHost -user $VCUser -Password $VCPassword -Force
# Connect to Provisioning Services server
	Write-Host "5.1.2 Connect to Provisioning Services server" -ForegroundColor Magenta
    Set-PvsConnection -server $PVSHost -port 54321
    Write-Host "`n"
# Read vSphere clusters
	Write-Host "5.2 Query vSphere clusters" -ForegroundColor Cyan
	$VMCluster1 = Get-Cluster -Name $VMClusterName1 | Sort-Object -Unique
	$VMCluster2 = Get-Cluster -Name $VMClusterName2 | Sort-Object -Unique
    Write-Host "`n"
# Read vSphere template 
    $VMTemplate = Get-Template -Name "$VMTemplateName" | Sort-Object -Unique
# Read XenDesktop Machine Catalog
    Write-Host "5.3 Query XenDesktop Machine Catalog" -ForegroundColor Cyan
    $XDCatalog = Get-BrokerCatalog -AdminAddress $XDDCHost -Name $XDCatalogName
# Read XenDesktop Hypervisor Connection
    Write-Host "5.4 Query XenDesktop Hypervisor Connection" -ForegroundColor Cyan
    $XDHypervisor = Get-BrokerHypervisorConnection -AdminAddress $XDDCHost -Name $XDHypervisorName
# Various stuff
	$TaskTab = @{} 
	$Counter = 0
# Set first VM name
    $VMNumber = "{0:00000}" -f $VMNumber
	$TargetVMName = $VMPrefix + $VMNumber
# Find least used network
	Write-Host "5.5 Gather all relevant networks and find least used network" -ForegroundColor Cyan
    $AllNetworks = Get-VM | Where-Object { ($_ | Get-NetworkAdapter | Where-Object {$_.networkname -match $Network -and $_.networkname -ne $BlockedNetwork1 -and $_.networkname -ne $BlockedNetwork2})} | Select-Object Name,@{N="PortGroups";E={Get-VirtualPortGroup -VM $_ | ForEach-Object {$_.Name}}}
    $PortGroups = $AllNetworks | ForEach-Object {$_.PortGroups} | Select-Object -Unique
    $AllNetworksCount = @{}
    $TempObject = @{}
    Foreach ($PortGroup in $PortGroups) {
        $TempObject.add("$PortGroup",($AllNetworks | Where-Object {$_.PortGroups -contains $PortGroup} | Measure-Object).Count)
    $AllNetworksCount = $TempObject
# Create VM section	
	1..$Num_VMs_Total | ForEach-Object {
		Write-Host "5.6 Create next VM" -ForegroundColor Cyan
# Find least used vSphere host
		Write-Host "5.6.1 Find host in specified vSphere clusters with least amount of VMs on it" -ForegroundColor Magenta
		$AllVMHosts = Get-Cluster | Where-Object {$_.Name -eq $VMCluster1.Name -or $_.Name -eq $VMCluster2.Name} | Sort-Object -Unique | Get-VMHost -State Connected | Select-Object Name,@{N="NumVM";E={@(($_ | Get-Vm )).Count}}
		$AllVMHostFilter = $AllVMHosts | Where-Object {$_.NumVM -lt $MaxVMHost}
		$LeastPopulatedVMHost = $AllVMHostFilter | Sort-Object NumVM | Select-Object -First 1 
		$TargetVMHost = Get-VMHost -Name $LeastPopulatedVMHost.Name | Sort-Object -Unique
# Find local datastore on vSphere host
		Write-Host "5.6.2 Define local datastore with most space available" -ForegroundColor Magenta
		$LeastUsedDatastore = Get-VMHost -Name $LeastPopulatedVMHost.Name | Get-Datastore | Where-Object {($_.ExtensionData.Summary.MultipleHostAccess -eq $false)} | Sort-Object FreeSpaceGB | Select-Object -Last 1
# Find least used network
		Write-Host "5.6.3 Find network with least amount of VMs" -ForegroundColor Magenta
        [array]$AllNetworksPercentage1 = $AllNetworksCount.GetEnumerator() | Where-Object {$_.Name -eq $SmallNetwork1} | Select-Object Name,Value,@{L="Percentage";E={($_.Value/$SmallNetwork1Size).ToString("P",$DecimalCulture)}}
        [array]$AllNetworksPercentage2 = $AllNetworksCount.GetEnumerator() | Where-Object {$_.Name -ne $SmallNetwork1} | Select-Object Name,Value,@{L="Percentage";E={($_.Value/$NetworkSize).ToString("P",$DecimalCulture)}}
        $AllNetworksPercentage = $AllNetworksPercentage1 + $AllNetworksPercentage2
        $LeastUsedNetwork = $AllNetworksPercentage | Sort-Object Percentage | Select-Object -First 1
        $TargetNetwork = $LeastUsedNetwork.Name
# Determine first free VM name
		Write-Host "5.6.4 Determine first free VM name" -ForegroundColor Magenta
		While ((Get-VM -Name $TargetVMName -ErrorAction SilentlyContinue) -ne $null) {
			$VMNumber = $Counter
			$VMNumber = "{0:00000}" -f $VMNumber
			$TargetVMName = $VMPrefix + $VMNumber
# Create VM on vSphere
		Write-Host "5.6.5 Create virtual machine $TargetVMName on $($LeastPopulatedVMHost.Name) using template $VMTemplate" -ForegroundColor Magenta
		$TaskTab[(New-VM -Name $TargetVMName -VMHost $TargetVMHost -Template $VMTemplate -Datastore $LeastUsedDatastore -Notes "$PVSDescription $CurrentTime" -Server $VCHost).ID]=$TargetVMName 
		$TargetVMMAC = Get-NetworkAdapter -VM $TargetVMName | ForEach-Object {$_.MacAddress} | ForEach-Object {$_ -replace ':',"-"}
		Get-NetworkAdapter -VM $TargetVMName | Set-NetworkAdapter -NetworkName $TargetNetwork -Confirm:$false
        Write-Host "`n"
# Import device in Provisioning Services (Create new or alter current one when it already exists)
		Write-Host "5.6.6 Import device in Provisioning Services to collection $PVSCollection on PVS site $PVSSite" -ForegroundColor Magenta
        $ErrorActionPreference = "SilentlyContinue"
        If ((Test-Path variable:global:PVSObject) -eq $true) { 
            Remove-Variable PVSObject 
        Get-PvsDevice -Name $TargetVMName | Out-Null
        If ($? -eq $false) {
            $ErrorActionPreference = "Stop"
		    New-PvsDevice -Name $TargetVMName -CollectionName $PVSCollection -SiteName $PVSSite -DeviceMac $TargetVMMAC -Description "$PVSDescription $CurrentTime" -copyTemplate -BdmBoot | Out-Null
        } Else {
            $ErrorActionPreference = "Stop"
            $PVSObject = Get-PvsDevice -Name $TargetVMName -Fields DeviceMac
            $PVSObject.DeviceMac = $TargetVMMAC
            Set-PvsDevice $PVSObject
			Set-PvsDevice -Name $TargetVMName -Description "$PVSDescription $CurrentTime"
        Write-Host "`n"
# Add machine to domain through Provisioning Services
        Write-Host "5.6.7 Add machine to domain in OU $OU" -ForegroundColor Magenta
        $ErrorActionPreference = "SilentlyContinue"
        $PVSADAccount = Get-PvsADAccount -Name $TargetVMName -Domain $DomainFQDN
        If (!$PVSADAccount) {
            $ErrorActionPreference = "Stop"
            Add-PvsDeviceToDomain -Name $TargetVMName -Domain $DomainFQDN -OrganizationUnit $OU
        } Else {
            $ErrorActionPreference = "Stop"
            Remove-PvsDeviceFromDomain -Name $TargetVMName -Domain $DomainFQDN
            Add-PvsDeviceToDomain -Name $TargetVMName -Domain $DomainFQDN -OrganizationUnit $OU
        Write-Host "`n"
# Link vDisk to Provisioning Services device
        Write-Host "5.6.8 Add vDisk $PVSvDisk to device in Provisioning Services" -ForegroundColor Magenta
		$PVSObject2 = Get-PvsDevice -Name $TargetVMName | Get-PvsDiskLocator
		If ($PVSObject2) {
			Get-PvsDevice -Name $TargetVMName | Remove-PvsDiskLocatorFromDevice -DiskLocatorId $PVSObject2.DiskLocatorId
        Add-PvsDiskLocatorToDevice -DeviceName $TargetVMName -DiskLocatorName $PVSvDisk -SiteName $PVSSite -StoreName $PVSStore
        Write-Host "`n"
# Add machine to XenDesktop Machine Catalog
        Write-Host "5.6.9 Add machine to XenDesktop Machine Catalog $XDCatalogName" -ForegroundColor Magenta
        Write-Host "Wait 15 seconds"
        $ErrorActionPreference = "Continue"
        Start-Sleep -s 15
        New-VIProperty -ObjectType VirtualMachine -Name Cluster -Value {$Args[0].VMHost.Parent} -Force | Out-Null
        $VMObject1 = Get-VM | Select-Object -Property Name,Cluster | Where-Object {($_.Name -like "$TargetVMName")}
        $VMDeployCluster = $VMObject1.Cluster.Name | Sort-Object -Unique
        $VMDeployPath = "XDHyp:\Connections\$XDHypervisorName\$VMDatacenterName\$VMDeployCluster.cluster\$TargetVMName.vm"
        $VMObject2 = Get-Item $VMDeployPath -AdminAddress $XDDCHost
        Get-Item $VMDeployPath -AdminAddress $XDDCHost | Out-Null
        New-BrokerMachine -CatalogUid $XDCatalog.Uid -HypervisorConnectionUid $XDHypervisor.Uid -HostedMachineId $VMObject2.Id -MachineName $DomainNetBIOS\$TargetVMName | Out-Null
        If ($?) { 
            Write-Host "Add to catalog SUCCESS"
        } Else {
            Write-Host "Add to catalog FAIL. Trying again in 30 seconds."
            Start-Sleep -s 30
            New-BrokerMachine -CatalogUid $XDCatalog.Uid -HypervisorConnectionUid $XDHypervisor.Uid -HostedMachineId $VMObject2.Id -MachineName $DomainNetBIOS\$TargetVMName | Out-Null
            If ($?) {
                Write-Host "Add to catalog SUCCESS"
            } Else {
                Write-Host "Add to catalog FAIL. Trying again in 30 seconds."
                Start-Sleep -s 30
                New-BrokerMachine -CatalogUid $XDCatalog.Uid -HypervisorConnectionUid $XDHypervisor.Uid -HostedMachineId $VMObject2.Id -MachineName $DomainNetBIOS\$TargetVMName |Out-Null
                If ($?) {
                    Write-Host "Add to catalog SUCCESS"
                } Else {
                    Write-Host "Add to catalog FAIL. Quitting script."
# Alter network table
        Write-Host "5.6.10 Alter current network table" -ForegroundColor Magenta
		$TempHashTable = @{}
		$AllNetworksCount.GetEnumerator() | Where-Object {$_.Name -eq $TargetNetwork} | ForEach-Object {$TempHashTable[$_.Name]=$_.value +1}
		$AllNetworksCount.Set_Item(($TempHashTable.GetEnumerator()).Name,($TempHashTable.GetEnumerator() | ForEach-Object {$_.Value}))
# Add machine to global deployment summary
        $GlobalDeployResults.add("$TargetVMName") | Out-Null
# Show deployment summary and move to next VM (if any)
        Write-Host "5.6.11 VM successfully created from $VMTemplate" -ForegroundColor Magenta
        Write-Host "Name = $TargetVMName"
        Write-Host "Network = $TargetVMMAC in $TargetNetwork"
        Write-Host "Provisioning Services = Collection $PVSCollection in site $PVSSite with vDisk $PVSvDisk"
        Write-Host "Active Directory OU = $OU"
        Write-Host "Hypervisor = Host $TargetVMHost on datastore $LeastUsedDatastore in cluster $VMDeployCluster"
        Write-Host "XenDesktop = Machine catalog $XDCatalogName"
        Write-Host "-------------------------------------------------------------------" -ForegroundColor Magenta
        Write-Host "On to next VM (if any)" -ForegroundColor Cyan
        Write-Host "`n"
# -------------------------------

# CLEAN UP ----------------------
    Write-Host "6. Script completed - Cleaning up" -ForegroundColor Green
	$TaskTab = @{} 
 	Disconnect-VIServer * -Confirm:$false
    Write-Host "--- DONE ---" -ForegroundColor Green
    Write-Host "`n"
    Write-Host "Global deployment results:" -ForegroundColor Yellow
    Read-Host "--- Press ENTER to close ---"
    # -------------------------------


12 thoughts on “Citrix XenDesktop 7.x VDI deployment with PowerShell using vSphere, local storage and PVS

  1. nava chou

    This is great.
    That’s what I understand.
    However, it seems that there are still some differences with XDSW, which is exactly what I am confused about.
    For example, to copy a virtual machine from a template, it is clear that PVS do not simply perform ‘new-vm’.
    PVS XDSW will not copy the disk whether or not the template has one, and ‘new-vm’ will.
    XDSW will copy the template first, then use the copied template as the template to create the virtual machine, and then delete the template.
    There are other differences from your script.
    Can you tell me more about what you know
    Thanks a lot

    1. Chris Jeucken Post author


      Yes, indeed there are differences. We didn’t really look into how XDSW handles the templates, but the results were that the created machines ended up in the shared storage where the template is stored. The goal was to put the machines on the local storage of the specific hypervisor host.
      We asked Citrix as well as Atlantis about how we should do this and both told us you would need to script it.
      So that is what we did.

      How did you found out that XDSW first copies the template? I didn’t notice it when playing around with this last year.


    1. chrisadm Post author

      Nee, helaas. Maar je weet dat je de PowerShell commando’s kunt inzien?
      Voeg bijv. nieuwe machines toe aan een catalog en kijk vervolgens in het bovenste menu van Citrix Studio onder het tabblad PowerShell.
      Dan zie je de commando’s die gebruikt zijn voor deze handeling en deze kun je vervolgens in je script opnemen.

      Heel concreet krijg je dan zoiets:
      $Log = Start-LogHighLevelOperation -AdminAddress xddc01.local.lan -StartTime "1/2/2019 10:13:06 AM" -Text "Adds 1 Machines to Machine Catalog `'Pool 01`'"
      $Pool = Get-AcctIdentityPool -AdminAddress xddc01.local.lan -IdentityPoolName "Pool 01" -MaxRecordCount 2147483647
      Set-AcctIdentityPool -AdminAddress xddc01.local.lan -AllowUnicode -Domain "local.lan" -IdentityPoolName "Pool 01" -LoggingId $

      Get-AcctADAccount -AdminAddress xddc01.local.lan -IdentityPoolUid $Pool.IdentityPoolUid -Lock $False -MaxRecordCount 2147483647 -State "Available"
      New-AcctADAccount -AdminAddress xddc01.local.lan -Count 1 -IdentityPoolUid $Pool.IdentityPoolUid -LoggingId $
      Get-ProvScheme -AdminAddress xddc01.local.lan -MaxRecordCount 2147483647 -ProvisioningSchemeName "Pool 01"

      $VM = New-ProvVM -ADAccountName @("LOCAL\VDI0104$") -AdminAddress xddc01.local.lan -LoggingId $ -ProvisioningSchemeName "Pool 01" -RunAsynchronously
      Lock-ProvVM -AdminAddress xddc01.local.lan -LoggingId $ -ProvisioningSchemeName "Pool 01" -Tag "Brokered" -VMID @($VM.VMId)

      New-BrokerMachine -AdminAddress xddc01.local.lan -CatalogUid 4 -MachineName "VDI0104.local.lan"

      Stop-LogHighLevelOperation -AdminAddress xddc01.local.lan -EndTime "1/2/2019 10:13:25 AM" -HighLevelOperationId $ -IsSuccessful $True

      Laat maar even weten of je daar iets aan hebt, anders kan ik wel iets meer uitgebreid tikken.

      1. seref

        Bedankt voor je uitleg, als je tijd en zin hebt iets meer info zal voor mij heel handig zijn.


        1. chrisadm Post author

          Ja, dat is goed. Al is het wel handig om te weten wat je doel precies is. Wat wil je automatiseren?
          Dit omdat het bij MCS al heel erg geautomatiseerd is.


  2. navy

    This is very useful, I have written similar script, of course, there are some flaws like said above.And I always wanted to know how to create a partition for BDM with powershell and make it work.Do you have any advice for me?

    1. chrisadm Post author


      Sorry for the late reply:
      I actually used the XenDesktop Setup Wizard in the PVS Console to create a single machine with a BDM partition and used that to create a template in vSphere.
      That template is used by my script to deploy all the machines and in that way every machine will get the BDM partition.
      So I don’t have any script for you to create the partition.


  3. Mark

    Hello Chris,
    Thank you for sharing your script for creating a MCS Catalog for PVS. I am trying to automate my Citrix Home lab to automatically create the MCS catalog for MCS but due to not being very good with Powershell I am trying to find a script and came across your post.

    I have looked at some of the examples on Citrix’s own SDK site but I don’t know how to put everything together, I even looked into using the Powershell commands within Studio but haven’t been able to put together a script that will create a new Catalog and VMs without errors.

    Would you be able to share your immense Powershell knowledge and some day when you have time please post a script for some of us novices to learn how to automate things in Citrix.


    1. Chris Jeucken Post author


      Sadly I don’t have a script lying around to create a new MCS catalog. I would have to create that so I will add that to my todo list.
      I do have one that deploys a new snapshot to all the VMs in an existing MCS catalog (basically an automated version of ‘Update machine’).


  4. Mark

    Hello Chris,
    Thank you for your reply. I managed to find a basic script which I was able to change things around by trial and error and got it working for Desktops but it is not working for Servers/Virtual Apps.

    Yes, I am in search for a good script which can take a new snapshot to an existing pool of non-persistent desktops and servers. Ideally, what I would like to test is be able to run the script on a monthly schedule.



Leave a Reply

Your email address will not be published. Required fields are marked *