• Documenting Citrix XenDesktop 5.x with Microsoft PowerShell

    March 2, 2015

    PowerShell, XenDesktop 5.x

    Ever since I released the first documentation scripts in October 2011, people have been harassing, I mean asking, me when I will release a XenDesktop script. After 23 months of tedious effort and with the help of almost 80 testers, we are pleased to present you with Version 1.0 of the XenDesktop 5.x documentation script.

    I would also like to thank the engineers at Citrix who helped me figure out how to retrieve and gather some data I just couldn’t find and also verify the way I gathered some data was indeed correct.

    The script has been tested with all versions of XenDesktop 5:

    • 5.0
    • 5.0 SP1
    • 5.5
    • 5.6
    • 5.6 FP1

    This is the first publicly released script that generates output in four formats:

    1. Microsoft Word 2010 and 2013
    2. PDF
    3. Formatted Text
    4. HTML

    As far as I know, this script documents every nook and cranny in Citrix Studio that I can find. The following is documented:

    • Machines (aka Machine Catalogs)
      • Optionally, details on every machine in every catalog
    • Assignments (aka Desktop Groups or Delivery Groups)
      • Optionally, details on every machine (desktop) in every group
    • Applications
      • Optionally, details for every application
    • HDX Policies
      • Optionally, details for every XenDesktop 5.x Site and Active Directory (AD) based Citrix policy
    • Site Configuration
    • Administrators
    • Controllers
    • Hosting
      • Optionally, basic details for every machine using a hosting connection
    • Licensing

    Machine Catalogs

    In order to make sure I gathered all the accurate information I could, I created one of every possible type of machine catalog for both MCS (Machine Creation Services) and PVS (Provisioning Services). I also did everything I could to make sure the wording in the output matched the wording used in Studio.

    Citrix did not document how to determine a catalog type or a machine type. As I created each catalog, I kept track of what changed for two catalog properties: AllocationType and CatalogKind. This is what I came up with and Citrix verified as correct:

    If($Catalog.AllocationType -eq "Permanent" -and $Catalog.CatalogKind -eq "ThinCloned")
    {
    	$xCatalogType = "Dedicated"
    	$xMachineType = "Dedicated"
    }
    ElseIf($Catalog.AllocationType -eq "Permanent" -and $Catalog.CatalogKind -eq "PowerManaged")
    {
    	$xCatalogType = "Existing"
    	$xMachineType = "Existing"
    }
    ElseIf($Catalog.AllocationType -eq "Permanent" -and $Catalog.CatalogKind -eq "Unmanaged")
    {
    	$xCatalogType = "Physical"
    	$xMachineType = "Physical"
    }
    ElseIf($Catalog.AllocationType -eq "Random" -and $Catalog.CatalogKind -eq "SingleImage")
    {
    	$xCatalogType = "Pooled-Random"
    	$xMachineType = "Pooled"
    }
    ElseIf($Catalog.AllocationType -eq "Permanent" -and $Catalog.CatalogKind -eq "SingleImage")
    {
    	$xCatalogType = "Pooled-Static"
    	$xMachineType = "Pooled"
    }
    ElseIf($CanUsePvD -and ($Catalog.AllocationType -eq "Permanent" -and $Catalog.CatalogKind -eq "Pvd"))
    {
    	$xCatalogType = "Pooled with personal vDisk"
    	$xMachineType = "Pooled with personal vDisk"
    }
    ElseIf($Catalog.AllocationType -eq "Random" -and $Catalog.CatalogKind -eq "Pvs")
    {
    	$xCatalogType = "Streamed"
    	$xMachineType = "Streamed"
    }
    ElseIf($CanUsePvD -and ($Catalog.AllocationType -eq "Random" -and $Catalog.CatalogKind -eq "PvsPvd"))
    {
    	$xCatalogType = "Streamed with personal vDisk"
    	$xMachineType = "Streamed with personal vDisk"
    }
    Else
    {
    	$xCatalogType = "Unable to determine Catalog type. AllocationType: $($Catalog.AllocationType) CatalogKind: $($Catalog.CatalogKind)"
    	$xMachineType = "Unable to determine Machine type. AllocationType: $($Catalog.AllocationType) CatalogKind: $($Catalog.CatalogKind)"
    }
    

    The next items I had to figure out (since the numbers were not documented) for catalogs were the columns for With user, Without user, Assigned, and Free. It made no sense to me but the matching catalog properties were:

    • With user = AssignedCount
    • Without user = UnassignedCount
    • Assigned = UsedCount
    • Free = AvailableCount

    The last item to deal with was the detail available for a machine. When a catalog is double-clicked in Studio a list of machines in that catalog are shown. If you right-click on the column header row and click Select Columns, there are a lot of columns to chose from as shown in Figure 1.

    Figure 1
    Figure 1

    I decided to include every column in the optional machine details. The same details are also used for assignments/desktop groups/delivery groups. If you decide to use both machine catalog details and delivery group details, your report will be extremely long and take an exceptionally long time to run. I recommend you chose one or neither.

    Delivery Groups

    For delivery groups, Citrix used easy to understand property names to match column names for the Total desktops, Available, In use, Disconnected, and Unregistered columns. Those properties are:

    • Total desktops = TotalDesktops
    • Available = DesktopsAvailable
    • In use = DesktopsInUse
    • Disconnected = DesktopsDisconnected
    • Unregistered = DesktopsUnregistered

    Now why couldn’t Citrix have followed the same logical naming scheme for catalogs?

    Applications

    Basic application information was easy. Detailed information, not so much.

    There are five tabs that are used to display application details: Details, Desktop Groups, Machines, Sessions and Users.

    The details information was fairly easy to determine.

    To get the desktop groups, you have to get the Application’s Uid property and that pass that as a parameter to Get-BrokerDesktopGroup,

    For machines, it is a little more tricky. First get the desktop groups again, then for each desktop group get the desktop group’s Uid and determine if the group is a SharedApp or PrivateApp. Then use that to determine whether to use Get-BrokerSharedAppDesktop or Get-BrokerPrivateAppDesktop to get the basic machine information.

    For sessions, pass the application’s Uid to Get-BrokerSession and then for each session, pass the session’s Uid to Get-BrokerDesktop to get basic information on the desktop.

    For users, pass the application’s Uid to Get-BrokerAccessPolicyRule with the parameter IncludedApplicationFilterEnabled. Then build an array of users from the object returned and process each user in the array.

    HDX Policies

    This was the hardest section to do. I worked with a couple of engineers from Citrix and never could get the policy module, functions or its PowerShell drives to work.

    The policies are controlled by two things. The citrix.common.grouppolicy snapin and the citrix.grouppolicy.commands module. The snapin is supposed to automatically create a LocalFarmGPO PSDrive but it doesn’t. The functions inside the group policy module look for the LocalFarmGPO drive by default unless another PSDrive is specified.

    One day I was searching around and found an article on the changes to the XenDesktop 7 Computer and User policies.  There was one line in the article that showed creating the PSDrive with a PSDrive name of Site and using the -Controller parameter. I took that line, changed the drive name to LocalFarmGPO, added -Controller and also -Scope Global and lo and behold, all the policy stuff just worked. The -Controller parameter is also documented in eDocs in the XenDesktop 7.6 About the XenApp and XenDesktop SDK section.

    I was able to copy and paste all my policy code from the XenApp 6.5 script, make a couple of changes for the non-existent LocalFarmGPO drive and the code just worked. I had to remove all the XenApp 6.5 policy settings that don’t exist in XenDesktop 5.x and add in the XenDesktop 5.x policy settings. Even the code to process AD based policies worked.

    A note on AD based Citrix policies. All the code does is look in the SYSVOL share for all files named policies.gpf and returns an array of all the AD policy names. That array is then processed through the policy functions. The policies.gpf is used by XenApp 6, XenApp 6.5, XenDesktop 5.x and XenDesktop 7.x. There is absolutely nothing in the policies.gpf file that tells me if the file is from XenApp 6, XenApp 6.5, XenDesktop 5.x or XenDesktop 7.x. If you just happen to have all four versions of XenApp and XenDesktop in use and also have Citrix AD based policies created for each of those products, this script will process and report on every one of those policies. There is nothing that I am aware of I can do to filter out every policy except XenDesktop 5.x. Sorry. Don’t blame me.

    By the way, when Citrix added in support for the -Controller parameter for the PSDrive, that means you can now run the script remotely AND get policies included in your output.  Before you XenApp 6.x users get your hopes up, we already tested it and the updated group policy module does not work on XenApp 6.x with the -Controller parameter.

    By the way #2, if you download and install Citrix Scout, you will find the updated Citrix.GoupPolicy.Commands.psm1 module. I also have a link to the file in the ReadMe for this script. You can also use the updated module file with XenApp 6 and XenApp 6.5 you just can’t use the -Controller parameter.

    Site Configuration

    Nothing special to do to retrieve data in this section.

    Administrators

    Citrix did not document how to determine the administrator type. Here is what I came up with and Citrix verified as correct.

    If( $Admin.BrokerAdmin -eq $False -and $Admin.FullAdmin -eq $True -and $Admin.ProvisioningAdmin -eq $False -and $Admin.ReadOnly -eq $False )
    {
    	$tmpRole = "Full"
    	$AllCatalogs = "All Catalogs"
    	$AllGroups   = "All Desktop Groups"
    }
    ElseIf( $Admin.BrokerAdmin -eq $False -and $Admin.FullAdmin -eq $False -and $Admin.ProvisioningAdmin -eq $True -and $Admin.ReadOnly -eq $False )
    {
    	$tmpRole = "Machine"
    	$AllCatalogs = "All Catalogs"
    	$AllGroups   = "-"
    }
    ElseIf( $Admin.BrokerAdmin -eq $True -and $Admin.FullAdmin -eq $False -and $Admin.ProvisioningAdmin -eq $False -and $Admin.ReadOnly -eq $False )
    {
    	$tmpRole = "Assignment"
    	$AllCatalogs = " "
    	$AllGroups   = "All Desktop Groups"
    }
    ElseIf( $Admin.BrokerAdmin -eq $False -and $Admin.FullAdmin -eq $False -and $Admin.ProvisioningAdmin -eq $False -and $Admin.ReadOnly -eq $True )
    {
    	$tmpRole = "Read only"
    	$AllCatalogs = "-"
    	$AllGroups   = "-"
    }
    ElseIf( $Admin.BrokerAdmin -eq $False -and $Admin.FullAdmin -eq $False -and $Admin.ProvisioningAdmin -eq $False -and $Admin.ReadOnly -eq $False )
    {
    	$tmpRole = "Help desk"
    	$AllCatalogs = "-"
    	$AllGroups   = " "
    }
    ElseIf( $Admin.BrokerAdmin -eq $True -and $Admin.FullAdmin -eq $False -and $Admin.ProvisioningAdmin -eq $True -and $Admin.ReadOnly -eq $False )
    {
    	$tmpRole = "Machine; Assignment"
    	$AllCatalogs = "All Catalogs"
    	$AllGroups   = "All Desktop Groups"
    }
    

    Controllers

    Nothing special to do to retrieve data in this section.

    Hosting

    I initially could not figure out how to retrieve all the hosting information but a former Citrite worked on it, figured it out, sent me the code and then quit Citrix! LOL Hope it wasn’t my fault.

    All the hosting data is contained in PSDrives.

    $vmstorage = @()
    $pvdstorage = @()
    $vmnetwork = @()
    
    $HostingUnits = get-childitem @XDParams1 -path 'xdhyp:\hostingunits'
    If($? -and $HostingUnits -ne $Null)
    {
    	ForEach ($item in $HostingUnits)
    	{
    		ForEach ($storage in $item.Storage)
    		{
    			$vmstorage += $storage.StoragePath
    		}
    		ForEach ($storage in $item.PersonalvDiskStorage)
    		{
    			$pvdstorage += $storage.StoragePath
    		}
    		ForEach ($network in $item.NetworkPath)
    		{
    			$vmnetwork += $network
    		}
    	}
    }
    
    $Hypervisors = Get-BrokerHypervisorConnection @XDParams1
    If($? -and $Hypervisors -ne $Null)
    {
    	ForEach ($Hypervisor in $Hypervisors)
    	{
    		$hypvmstorage = @()
    		$hyppvdstorage = @()
    		$hypnetwork = @()
    		$capabilities = $Hypervisor.Capabilities -join ', '
    		ForEach ($storage in $vmstorage)
    		{
    			If($storage.Contains($Hypervisor.Name))
    			{
    				$hypvmstorage += $storage
    			}
    		}
    		ForEach ($storage in $pvdstorage)
    		{
    			If($storage.Contains($Hypervisor.Name))
    			{
    				$hyppvdstorage += $storage
    			}
    		}
    		ForEach ($network in $vmnetwork)
    		{
    			If($network.Contains($Hypervisor.Name))
    			{
    				$hypnetwork += $network
    			}
    		}
    		$xStorageName = ""
    		ForEach($Unit in $HostingUnits)
    		{
    			If($Unit.HypervisorConnection.HypervisorConnectionName -eq $Hypervisor.Name)
    			{
    				$xStorageName = $Unit.HostingUnitName
    			}
    		}
    		$xAddress = ""
    		$xHAAddress = @()
    		$xUserName = ""
    		$xMaintMode = $False
    		$xConnectionType = ""
    		$xState = ""
    		$xPowerActions = @()
    		$Connections = get-childitem @XDParams1 -path 'xdhyp:\connections'
    
    		If($? -and $Connections -ne $Null)
    		{
    			ForEach($Connection in $Connections)
    			{
    				If($Connection.HypervisorConnectionName -eq $Hypervisor.Name)
    				{
    					$xAddress = $Connection.HypervisorAddress[0]
    					ForEach($tmpaddress in $Connection.HypervisorAddress)
    					{
    						$xHAAddress += $tmpaddress
    					}
    					$xUserName = $Connection.UserName
    					$xMaintMode = $Connection.MaintenanceMode
    					$xConnectionType = $Connection.ConnectionType
    					$xState = $Hypervisor.State
    					$xPowerActions = $Connection.metadata
    				}
    			}
    		}
    		OutputHosting $Hypervisor $xConnectionType $xAddress $xState $xUserName $xMaintMode $xStorageName $xHAAddress $xPowerActions
    	}
    }
    

    Licensing

    The only tricky thing in licensing is getting the correct version number in order to get the correct SA date. When using Get-BrokerController, the version data is the same for XenDesktop 5.0 SP1 and 5.5.  Both display version 5.1. Citrix showed me the preferred method is to retrieve the version number from the registry of the controller at HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Citrix Desktop Delivery Controller\DisplayVersion.

    Changing the Way I Write Scripts

    All the previous scripts I have written, I just started at the top and wrote code to create the necessary data for the report.  Which means the code is hard to maintain, hard to change and very difficult to add outputting to text and HTML. Starting with the XenDesktop 5.x and 7.x scripts, I start with a general outline of what I want the script to do and move outwards from that. Instead of a huge blob of code, the core of the script is now:

    #region script core
    #Script begins
    ProcessScriptSetup
    
    SetFileName1andFileName2 "$($XDSiteName)"
    
    #START BUILDING XENDESKTOP REPORT
    
    ProcessMachines
    
    ProcessAssignments
    
    ProcessApplications
    
    #make sure Citrix.GroupPolicy.Commands module is loaded
    If(!(Check-LoadedModule "Citrix.GroupPolicy.Commands"))
    {
    	Write-Warning "The Citrix Group Policy module Citrix.GroupPolicy.Commands.psm1 could not be loaded `nPlease see the Prerequisites section in the ReadMe file (<a href="https://carlwebster.com/d-sdc2aff73d424737a">https://carlwebster.com/d-sdc2aff73d424737a</a>). `nCitrix Policy documentation will not take place"
    	Write-Verbose "$(Get-Date): "
    }
    Else
    {
    	ProcessPolicies
    }
    
    ProcessConfiguration
    
    ProcessAdministrators
    
    ProcessControllers
    
    ProcessHosting
    
    ProcessLicensing
    #endregion
    

    This is by far the largest script I have written at just under 11,000 lines! I hope you enjoy what I and the almost 80 testers have created for you.

    The script has extremely detailed help text to explain all the parameters.

    .PARAMETER CompanyName
    	Company Name to use for the Cover Page.  
    	Default value is contained in HKCU:\Software\Microsoft\Office\Common\UserInfo\CompanyName or
    	HKCU:\Software\Microsoft\Office\Common\UserInfo\Company, whichever is populated on the 
    	computer running the script.
    	This parameter has an alias of CN.
    .PARAMETER CoverPage
    	What Microsoft Word Cover Page to use.
    	Only Word 2010 and 2013 are supported.
    	(default cover pages in Word en-US)
    	Valid input is:
    		Alphabet (Word 2010. Works)
    		Annual (Word 2010. Doesn't work well for this report)
    		Austere (Word 2010. Works)
    		Austin (Word 2010/2013. Doesn't work in 2013, mostly works in 2010 but Subtitle/Subject &amp; Author fields need to me moved after title box is moved up)
    		Banded (Word 2013. Works)
    		Conservative (Word 2010. Works)
    		Contrast (Word 2010. Works)
    		Cubicles (Word 2010. Works)
    		Exposure (Word 2010. Works if you like looking sideways)
    		Facet (Word 2013. Works)
    		Filigree (Word 2013. Works)
    		Grid (Word 2010/2013.Works in 2010)
    		Integral (Word 2013. Works)
    		Ion (Dark) (Word 2013. Top date doesn't fit, box needs to be manually resized or font changed to 8 point)
    		Ion (Light) (Word 2013. Top date doesn't fit, box needs to be manually resized or font changed to 8 point)
    		Mod (Word 2010. Works)
    		Motion (Word 2010/2013. Works if top date is manually changed to 36 point)
    		Newsprint (Word 2010. Works but date is not populated)
    		Perspective (Word 2010. Works)
    		Pinstripes (Word 2010. Works)
    		Puzzle (Word 2010. Top date doesn't fit, box needs to be manually resized or font changed to 14 point)
    		Retrospect (Word 2013. Works)
    		Semaphore (Word 2013. Works)
    		Sideline (Word 2010/2013. Doesn't work in 2013, works in 2010)
    		Slice (Dark) (Word 2013. Doesn't work)
    		Slice (Light) (Word 2013. Doesn't work)
    		Stacks (Word 2010. Works)
    		Tiles (Word 2010. Date doesn't fit unless changed to 26 point)
    		Transcend (Word 2010. Works)
    		ViewMaster (Word 2013. Works)
    		Whisp (Word 2013. Works)
    	Default value is Sideline.
    	This parameter has an alias of CP.
    .PARAMETER UserName
    	User name to use for the Cover Page and Footer.
    	Default value is contained in $env:username
    	This parameter has an alias of UN.
    .PARAMETER AdminAddress
    	Specifies the address of a XenDesktop controller the PowerShell snapins will connect to. 
    	This can be provided as a host name or an IP address. 
    	This parameter defaults to LocalHost.
    	This parameter has an alias of AA.
    .PARAMETER PDF
    	SaveAs PDF file instead of DOCX file.
    	This parameter is disabled by default.
    	The PDF file is roughly 5X to 10X larger than the DOCX file.
    .PARAMETER Text
    	Creates a formatted text file with a .txt extension.
    	This parameter is disabled by default.
    .PARAMETER MSWord
    	SaveAs DOCX file
    	This parameter is set True if no other output format is selected.
    .PARAMETER HTML
    	Creates an HTML file with an .html extension.
    	This parameter is disabled by default.
    .PARAMETER MachineCatalogs
    	Gives detailed information for all machines in all Machine Catalogs.
    	Using the MachineCatalogs parameter can cause the report to take a very long time to complete and
    	can generate an extremely long report.
    	Using both the MachineCatalogs and DeliveryGroups parameters can cause the report to take an
    	extremely long time to complete and generate an exceptionally long report.
    	This parameter is disabled by default.
    	This parameter has an alias of MC.
    .PARAMETER DeliveryGroups
    	Gives detailed information for all desktops in all Desktop (Delivery) Groups.
    	Using the DeliveryGroups parameter can cause the report to take a very long time to complete and
    	can generate an extremely long report.
    	Using both the MachineCatalogs and DeliveryGroups parameters can cause the report to take an
    	extremely long time to complete and generate an exceptionally long report.
    	This parameter is disabled by default.
    	This parameter has an alias of DG.
    .PARAMETER Applications
    	Gives detailed information for all applications.
    	This parameter is disabled by default.
    	This parameter has an alias of Apps.
    .PARAMETER Policies
    	Give detailed information for HDX Policies.
    	Using the Policies parameter can cause the report to take a very long time to complete and
    	can generate an extremely long report.
    	This parameter is disabled by default.
    .PARAMETER Hosting
    	Give detailed information for Hosts.
    	This parameter is disabled by default.
    .PARAMETER AddDateTime
    	Adds a date time stamp to the end of the file name.
    	Time stamp is in the format of yyyy-MM-dd_HHmm.
    	June 1, 2015 at 6PM is 2015-06-01_1800.
    	Output filename will be ReportName_2015-06-01_1800.docx (or .pdf).
    	This parameter is disabled by default.
    
    

    I know that no matter how many testers I have testing the script, it takes real world use to find all the bugs and little things that were overlooked.  If you find a bug, an issue, a typo, some data I missed in Studio or have an enhancement request, send me an email to webster@carlwebster.com.

    NOTE: This script is continually updated. You can always find the most current version by going to https://carlwebster.com/where-to-get-copies-of-the-documentation-scripts/

    Thanks and enjoy

    Webster







    About Carl Webster

    Webster is a Sr. Solutions Architect for Choice Solutions, LLC and specializes in Citrix, Active Directory and Technical Documentation. Webster has been working with Citrix products for many years starting with Multi-User OS/2 in 1990.

    View all posts by Carl Webster

    No comments yet.

    Leave a Reply