• New Script: Get Static Desktop User

    March 11, 2019

    PowerShell, XenDesktop

    In March 2019, I was honored to present at the first CUGC Texas XL event held at the beautiful University of Texas Club. My presentation was on PowerShell Scripting with Webster.  This article shows what I talked about in the second half of my presentation.

    The week before the CUGC event, I did a Citrix App Layering project for a Choice Solutions customer in Dallas. While waiting for the first published image to build, the customer asked if I could help them with a PowerShell script to get the users for a static desktop delivery group.

    The customer’s requirements were:

    • Use a specific Delivery Group
    • Use only Assigned desktops
    • Use last connected user’s name
    • Output to text file

    To accomplish this, we used the following process:

    • Get-BrokerMachine -DesktopGroupName
    • -Filter {IsAssigned -eq $True}
    • Select LastConnectionUser
    • Out-File StaticUsers.txt

    To determine the properties needed, we ran:

    Get-BrokerMachine -DesktopGroupName "The desktop group name"

    as shown in Figure 1.

    Figure 1
    Figure 1

    That showed us the values for IsAssigned and LastConnectionUser for an assigned desktop. The data for an unassigned desktop is shown in Figure 2.

    Figure 2
    Figure 2

    In Webster’s Lab (since I can’t show you live customer data), I built three machine catalogs and delivery groups. One for random desktops, one for static applications, and one for static desktops. For the static desktops, I logged into three of the five desktops, as shown in Figure 3.

    Figure 3
    Figure 3

    Next we created a one-liner PowerShell “script”.

    Note: The one-liner is shown on separate lines so the PowerShell code could be read on the PowerPoint slide.

    Note 2: Use Get-BrokerMachine instead of Get-BrokerDesktop as Citrix deprecated Get-BrokerDesktop.

     
    Get-BrokerMachine -DesktopGroupName "Texas CUGC XL"`
    -Filter {IsAssigned -eq $True} | `
    Select LastConnectedUser | `
    Out-File c:\webster\StaticUsers.txt
    

    The output text file and data from Studio are shown in Figure 4.

    Figure 4
    Figure 4

    This proves the one-liner produces accurate data.

    How can we turn this one-liner with hard-coded values and assumptions into a generic script that is usable by anyone?

    What is wrong with out one-liner? After all, it works and produces accurate data.

    The main fault is it violates a major rule of scripting and programming: Make no assumptions.

    • What happens if I don’t run on a delivery controller?
    • What happens if the Delivery Group name is fat fingered?
    • What if I want to process all static desktop Delivery Groups?
    • What happens if the output folder doesn’t exist?
    • What if I want to specify a specific output folder?
    • What happens if the Citrix PSSnapins are not loaded?
    • What if the desktops are not static assigned?
    • What happens if there is no data?

    Let’s turn this one-liner into a generic script.

    We can fix three of the assumptions by using script parameters.

    • Not running on delivery controller: -AdminAddress
    • Specify a specific delivery group or all delivery groups: -DeliveryGroupName
    • Specify an output folder: -Folder

    Since I have many scripts available at https://CarlWebster.com/Downloads , I have a lot of reusable code at my disposal.

    The three parameters in the help text are shown below.

    .PARAMETER AdminAddress
    Specifies the address of a XenDesktop controller where the PowerShell snapins will
    connect.
    
    This can be provided as a hostname or an IP address.
    This parameter defaults to Localhost.
    This parameter has an alias of AA.
    .PARAMETER DeliveryGroupName
    By default, the script processes all Delivery Groups that deliver Static desktops.
    This is where the properties DeliveryType equals DesktopsOnly and DesktopKind
    equals Private.
    
    Use this parameter to specify a single Delivery Group.
    
    This parameter defaults to all Static Desktop Delivery Groups.
    This parameter has an alias of DGN.
    .PARAMETER Folder
    Specifies the optional output folder to save the output report.
    

    You may notice the line for the parameter DeliveryGroupName: “This is where the properties DeliveryType equals DesktopsOnly and DesktopKind
    equals Private.”

    How did I know that? I cheated. I looked at all the properties for the three delivery groups created and found what I needed as shown in Figure 5.

    Figure 5
    Figure 5

    Here is the parameter block.

    [CmdletBinding(SupportsShouldProcess = $False, 
                   ConfirmImpact = "None", 
                   DefaultParameterSetName = "") ]
    
    Param(
         [parameter(Mandatory=$False)]
         [ValidateNotNullOrEmpty()]
         [Alias("AA")]
         [string]$AdminAddress="Localhost",
    
         [parameter(Mandatory=$False)]
         [Alias("DGN")]
         [string]$DeliveryGroupName="",
    
         [parameter(Mandatory=$False)]
         [string]$Folder=""
    
         )
    

    Copying from the other Citrix related scripts, to check for the required Citrix PowerShell snapins.

    Here is the function.

    Function Check-NeededPSSnapins
    {
    	Param([parameter(Mandatory = $True)][alias("Snapin")][string[]]$Snapins)
    
    	#Function specifics
    	$MissingSnapins = @()
    	[bool]$FoundMissingSnapin = $False
    	$LoadedSnapins = @()
    	$RegisteredSnapins = @()
    
    	#Creates arrays of strings, rather than objects, we're passing strings so this will be more robust.
    	$loadedSnapins += Get-PSSnapin | ForEach-Object {$_.name}
    	$registeredSnapins += Get-PSSnapin -Registered | ForEach-Object {$_.name}
    
    	ForEach($Snapin in $Snapins)
    	{
    		#check if the snapin is loaded
    		If(!($LoadedSnapins -like $snapin))
    		{
    			#Check if the snapin is missing
    			If(!($RegisteredSnapins -like $Snapin))
    			{
    				#set the flag if it's not already
    				If(!($FoundMissingSnapin))
    				{
    					$FoundMissingSnapin = $True
    				}
    				#add the entry to the list
    				$MissingSnapins += $Snapin
    			}
    			Else
    			{
    				#Snapin is registered, but not loaded, loading it now:
    				Add-PSSnapin -Name $snapin -EA 0 *>$Null
    			}
    		}
    	}
    
    	If($FoundMissingSnapin)
    	{
    		Write-Warning "Missing Windows PowerShell snap-ins Detected:"
    		$missingSnapins | ForEach-Object {Write-Warning "($_)"}
    		Return $False
    	}
    	Else
    	{
    		Return $True
    	}
    }
    

    And the call to the function.

    #check for required Citrix snapin
    If(!(Check-NeededPSSnapins "Citrix.Broker.Admin.V2"))
    {
    	#We're missing Citrix Snapins that we need
    	$ErrorActionPreference = $SaveEAPreference
    	Write-Error "`nMissing Citrix PowerShell Snap-ins Detected, check the console above for more information. 
    	`nAre you sure you are running this script against a XenDesktop 7.0 or later Delivery Controller? 
    	`n`nIf you are running the script remotely, did you install Studio or the PowerShell snapins on $($env:computername)?
    	`n`nScript will now close."
    	Exit
    }
    

    Fortunately, one one snapin is required for this script.

    This would be much easier if Citrix would stop using the deprecated snapins and change their PowerShell to use modules. When will this happen? The Cleveland Browns, followed by the Detroit Lions, followed by the Houston Texans, and finally followed by the Jacksonville Jaguars will each have a perfect NFL season and win the Super Bowl before Citrix ever fixes their PowerShell.

    The -AdminAddress parameter is used for all the Citrix Broker related PowerShell cmdlets. You will see this in the following Figures.

    The -Folder parameter is used in a majority of my scripts, so this is again a copy and paste.

    If($Folder -ne "")
    {
    	Write-Verbose "$(Get-Date): Testing folder path"
    	#does it exist
    	If(Test-Path $Folder -EA 0)
    	{
    		#it exists, now check to see if it is a folder and not a file
    		If(Test-Path $Folder -pathType Container -EA 0)
    		{
    			#it exists and it is a folder
    			Write-Verbose "$(Get-Date): Folder path $Folder exists and is a folder"
    		}
    		Else
    		{
    			#it exists but it is a file not a folder
    			Write-Error "Folder $Folder is a file, not a folder.  Script cannot continue"
    			Exit
    		}
    	}
    	Else
    	{
    		#does not exist
    		Write-Error "Folder $Folder does not exist.  Script cannot continue"
    		Exit
    	}
    }
    ElseIf($Folder -eq "")
    {
    	$pwdpath = $pwd.Path
    }
    Else
    {
    	$pwdpath = $Folder
    }
    
    If($pwdpath.EndsWith("\"))
    {
    	#remove the trailing \
    	$pwdpath = $pwdpath.SubString(0, ($pwdpath.Length - 1))
    }
    

    Did the user enter a Folder?

    Test to see if the folder exists.

    If it exists, is it a folder and not a file?

    If a folder, the script can continue.

    If a file, the script exits.

    If it does not exist, the script exits.

    If the user did not enter a Folder, set the variable $pwdpath to the value contained in $pwd.path.

    Note: $pwd is an alias for Get-Location as shown in Figure 6.

    Figure 6
    Figure 6

    If the user did enter a Folder, set the variable $pwdpath to the value of Folder.

    If $pwdpath ends with a trailing backslash, “\”, remove it.

    For -DeliveryGroupName, check if a value was entered. If a value was not entered, process all Delivery Groups where the properties DeliveryType and DesktopKind meet the criteria for Static Desktops.

    If a value was entered, check if the specified Delivery Group exists and also contains Static Desktops. Do not assume the Delivery Group name entered exists or contains Static Desktops.

    If($DeliveryGroupName -eq "")
    {
    	$DeliveryGroups = Get-BrokerDesktopGroup -AdminAddress $AdminAddress `
    	-filter {DeliveryType -eq "DesktopsOnly" -and DesktopKind -eq "Private"} -EA 0
    	
    	If(!$?)
    	{
    		Write-Error "Unable to retrieve Delivery Groups that deliver Static Desktops.
    		`n`nScript will now close."
    		Exit
    	}
    	ElseIf($? -and $Null -eq $DeliveryGroups)
    	{
    		Write-Error "No Delivery Groups were found that deliver Static Desktops.
    		`n`nScript will now close."
    		Exit
    	}
    }
    Else
    {
    	$DeliveryGroups = Get-BrokerDesktopGroup -AdminAddress $AdminAddress `
    	-filter {DeliveryType -eq "DesktopsOnly" -and DesktopKind -eq "Private"} `
    	-Name $DeliveryGroupName -EA 0
    
    	If(!$?)
    	{
    		Write-Error "Unable to retrieve Delivery Group $DeliveryGroupName.
    		`n`nScript will now close."
    		Exit
    	}
    	ElseIf($? -and $Null -eq $DeliveryGroups)
    	{
    		Write-Error "Delivery Group $DeliveryGroupName has no Static Desktops.
    		`n`nScript will now close."
    		Exit
    	}
    }
    

    Once the Delivery Group data is retrieved, process each Delivery Group searching for assigned desktops. Do not use Get-BrokerDesktop, as that cmdlet is deprecated. Citrix recommends using Get-BrokerMachine.

    Once all assigned desktops are retrieved for a Delivery Group, check if the LastConnectionUser property is not Null.

    I tried adding that test to the -Filter condition for Get-BrokerMachine, but that always resulted in no desktops returned.

    Once all desktops, for the Delivery Group, are processed, sort the $UserNames array, remove all duplicates and output the array data to a text file.

    Using -Encoding ascii removes the property name and the row of dashes (seen in Figure 4) from the output file as shown in Figure 8.

    ForEach($DG in $DeliveryGroups)
    {
    	Write-Verbose "$(Get-Date): `tRetrieving static desktops for Delivery Group $($DG.name)"
    	$Machines = Get-BrokerMachine -Filter {IsAssigned -eq $True} -DesktopGroupName $DG.Name `
    	-EA 0 | Sort-Object DNSName
    	
    	If(!$?)
    	{
    		Write-Warning "`t`tNo desktops were found for Delivery Group $($DG.Name)"
    	}
    	Else
    	{
    		$UserNames = New-Object System.Collections.ArrayList
    		
    		ForEach($Machine in $Machines)
    		{
    			Write-Verbose "$(Get-Date): `t`tProcesing desktop $($Machine.DNSName)"
    			If($null -ne $Machine.LastConnectionUser)
    			{
    				$UserNames.Add($Machine.LastConnectionUser) > $Null
    			}
    		}
    		
    		#sort users and remove duplicates
    		$UserNames = $UserNames | Sort-Object -Unique
    		$OutputFile = "$($pwdpath)\$($DG.Name) Static Users.txt"
    		Write-Verbose "$(Get-Date): Output text file $OutputFile"
    		Out-File -FilePath $OutputFile -Encoding ascii -InputObject $UserNames 4>$Null
    	}
    }
    

    An example of the script running is shown in Figure 7.

    Figure 7
    Figure 7

    An example of the output file is shown in Figure 8.

    Figure 8
    Figure 8

    I want to thank Matt Lovett for the request for a script that I was able to turn into a generic script. Matt’s request is a good example of taking a one-liner for a specific request and then turn that one-liner into a full generic script for the community to use.

    I wrote this script for the CUGC Texas XL event and the attendees allowed me to take a panoramic picture, Figure 9, to use for this article.

    Figure 9
    Figure 9

    Thanks to the Austin, Dallas, and Houston CUGC leaders for inviting me to speak. Thanks to the attendees for for being there and listening to my presentation and for allowing me to take a wide group photo.

    The script download went live the morning of the event on March 5th.

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

    If you would like me to present at your CUGC or CUGC XL event, contact the CUGC team and maybe I will create a script for your event and get rid of some of the backlog I have for script ideas.

    Thanks

    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