Recently I was asked how to use PowerShell to get a list of offline Citrix XenApp servers. Being new to PowerShell, this gave me an opportunity to use some of my new knowledge. At the time this article is being written, I have been working with PowerShell for less than one month. Feel free to offer suggestions for how to improve the script in this four part article series. To use PowerShell with XenApp 6, you will need the XenApp 6 PowerShell SDK (Software Development Kit) available from http://tinyurl.com/xa6pssdk (Figure 1). Figure 1 To use PowerShell with XenApp 5, you will need the XenApp Commands Technology Preview (v3) available from http://tinyurl.com/xa5ctpv3 (MyCitrix login required) (Figure 2).
Figure 2 Note: The scripts in this article should work for XenApp 6 and XenApp 5 for Windows Server 2008 and XenApp 5 for Windows Server 2003. To develop and test this script, my lab setup includes three servers running Citrix XenApp 6 for Microsoft Windows Server 2008 R2 SP1. The XenApp server names are XenApp1, XenApp2 and XenApp3. Only XenApp1 is powered on. This gives two offline servers to test the script against. In their PowerShell cmdlets (pronounced CommandLets), Citrix does not have a specific cmdlet that returns a list of offline servers. There are only two Citrix cmdlets that are needed in order to generate this list. The first cmdlet is Get-XAServer which returns a list of all XenApp servers (Online and Offline) in a XenApp farm (Figure 3). Figure 3 Note: Get-XAServer returns 21 properties, of which only one is used by this script: ServerName. The PowerShell command line in Figure 3 takes the results of Get-XAServer, selects just the ServerName
property, sorts the results in alphabetical order by ServerName and then displays the results in a formatted table displaying only the ServerName property. The second cmdlet needed is Get-XAZone. This cmdlet returns a list of all the Zones in a XenApp farm. The reason this cmdlet is needed is that the Get-XAServer cmdlet has a switch OnlineOnly that, believe it or not, returns a list of XenApp servers that are online. But, that switch requires the ZoneName parameter. Combining Get-XAZone piped to Get-XAServer with the OnlineOnly switch will return a list of online servers for every Zone in a XenApp farm (Figure 4). Figure 4 Didn t I just say the Get-XAServer cmdlet with the OnlineOnly switch required the ZoneName parameter? Yes I did. One of the many smart things that PowerShell handles under the covers is seeing that both Get-XAZone and Get-XAServer have the ZoneName parameter in common. How does Get- XAServer automatically handle this? To find out, I typed Get-Help Get-XAServer full and scrolled down to ZoneName (Figure 5). I saw that ZoneName accepts pipeline input by property name. Figure 5 Note: I learned this from Don Jones, PowerShell MVP at The Experts Conference 2011 in his preconference PowerShell Deep-Dive Workshop. This is part of the magic of PowerShell, handling stuff under the covers to make working in PowerShell less complicated. Since Citrix does not provide, at this time, an OfflineOnly switch, I have what is needed to build a list of offline servers. I have a list of all servers in the farm and a list of servers that are online. Any server that is in the all servers list but not in the online servers list is an offline server. This is fairly easy to do in PowerShell. First, get the list of all servers in the farm sorted by ServerName. $AllXAServers = Get-XAServer Sort-Object ServerName
$XAServers = @() ForEach( $XAServer in $AllXAServers ) $XAServers += $XAServer.ServerName Next, get all the online servers, also sorted by ServerName. $OnlineXAServers = Get-XAZone Get-XAServer OnlineOnly Sort-Object ServerName $OnlineServers = @() ForEach( $OnlineServer in $OnlineXAServers ) $OnlineServers += $OnlineServer.ServerName Create an array to hold the offline servers. $OfflineServers = @() Compare the array of online servers to the array of all servers. If a server is not in the online array, then it is offline. ForEach( $Server in $XAServers ) If( $OnLineServers -notcontains $Server ) $OfflineServers += $Server Display the list of offline servers. Write-Output $OfflineServers The entire script being run in my lab: $AllXAServers = Get-XAServer Sort-Object ServerName $XAServers = @() ForEach( $XAServer in $AllXAServers ) $XAServers += $XAServer.ServerName $OnlineXAServers = Get-XAzone Get-XAServer -OnlineOnly Sort-Object ServerName $OnlineServers = @() ForEach( $OnlineServer in $OnlineXAServers ) $OnlineServers += $OnlineServer.ServerName $OfflineServers = @() ForEach( $Server in $XAServers ) If( $OnLineServers -notcontains $Server ) $OfflineServers += $Server Write-Output $OfflineServers Which produces the following output (Figure 6):
Figure 6 As it stands, the script is fully functional, works and produces the desired output a list of offline servers in the XenApp farm. But, the script is not really as good as it could be and could create a Resume Generating Event if run on a large XenApp farm with zones spanning countries or continents. The script can be a lot better. Creating a simple function in PowerShell is easy. Function Name #PowerShell statements go here The first thing needed is a name for the function. The purpose of the function is to Get XenApp Offline Servers. Following the naming convention used by Citrix, Get-XANoun, the name could be Get- XAOfflineServer. Why XAOfflineServer and not XAOfflineServers? PowerShell convention is to use singular and not plural. Function Get-XAOfflineServer #PowerShell statements Adding in the original script to the new function. Function Get-XAOfflineServer $AllXAServers = Get-XAServer Sort-Object ServerName $XAServers = @() ForEach( $XAServer in $AllXAServers ) $XAServers += $XAServer.ServerName $OnlineXAServers = Get-XAZone Get-XAServer -OnlineOnly Sort-Object ServerName $OnlineServers = @() ForEach( $OnlineServer in $OnlineXAServers ) $OnlineServers += $OnlineServer.ServerName $OfflineServers = @() ForEach( $Server in $XAServers ) If( $OnLineServers -notcontains $Server ) $OfflineServers += $Server
Write-Output $OfflineServers Running this produces the original output. PS Z:\> Get-XAOfflineserver XENAPP2 XENAPP3 PS Z:\> Now that the function has been verified to work properly, how to allow the output to be restricted to one Zone? Adding a parameter to allow a Zone name to be passed is the answer. Get-XAServer uses ZoneName as a parameter. To be consistent, that is the same parameter this function will use. Function Get-XAOfflineServer Param( [string]$zonename = '' ) <snip> This allows the function to be called with a specific Zone name: Get-XAOfflineServer ZoneName NAZone The function needs to be changed to work when a Zone name is passed. First, the function needs to work if no Zone name is given, just as the original script. If( $ZoneName -eq '' ) $XAServers = @() ForEach( $XAServer in $AllXAServers ) $XAServers += $XAServer.ServerName $OnlineXAServers = Get-XAZone Get-XAServer -OnlineOnly Sort-Object ServerName $OnlineServers = @() ForEach( $OnlineServer in $OnlineXAServers ) $OnlineServers += $OnlineServer.ServerName Else $OfflineServers = @() ForEach( $Server in $XAServers ) If( $OnLineServers -notcontains $Server ) $OfflineServers += $Server If a Zone name is passed, two lines need to be changed.
All servers need to be retrieved only for the Zone specified. $AllXAServers = Get-XAServer -ZoneName $ZoneName Sort-Object ServerName Get-XAZone is no longer needed as online servers can be retrieved for the Zone specified. $OnlineXAServers = Get-XAServer -ZoneName $ZoneName -OnlineOnly Sort-Object ServerName The updated function with the new statements. Function Get-XAOfflineServer Param( [string]$zonename = '' ) If( $ZoneName -eq '' ) <snip> Else $AllXAServers = Get-XAServer -ZoneName $ZoneName Sort-Object ServerName $XAServers = @() ForEach( $XAServer in $AllXAServers ) $XAServers += $XAServer.ServerName $OnlineXAServers = Get-XAServer -ZoneName $ZoneName -OnlineOnly ` Sort-Object ServerName $OnlineServers = @() ForEach( $OnlineServer in $OnlineXAServers ) $OnlineServers += $OnlineServer.ServerName $OfflineServers = @() ForEach( $Server in $XAServers ) If( $OnLineServers -notcontains $Server ) $OfflineServers += $Server Write-Output $OfflineServers Running the function with no Zone specified produces the following (Figure 1). Figure 1
Running the function with a Zone specified produces the following (Figure 2). Figure 2 Running the function using the ZoneName parameter produces the following (Figure 3). Figure 3 Great! The functions works with no Zone specified and Zone name specified with and without using ZoneName. Or does it? What happens if an invalid Zone name is used (Figure 4)? Figure 4 OOPS! An IMAException error was returned because the Zone GoTitans doesn t exist in the Farm. How should that be handled? The function needs to have error handling, and more, added. The current script (PowerShell uses the ` character for line continuation): Function Get-XAOfflineServer Param( [string]$zonename = '' ) If( $ZoneName -eq '' ) $AllXAServers = Get-XAServer Sort-Object ServerName $OnlineXAServers = Get-XAZone Get-XAServer -OnlineOnly ` Sort-Object ServerName Else $AllXAServers = Get-XAServer -ZoneName $ZoneName Sort-Object ServerName $OnlineXAServers = Get-XAServer -ZoneName $ZoneName -OnlineOnly ` Sort-Object ServerName $XAServers = @() ForEach( $XAServer in $AllXAServers ) $XAServers += $XAServer.ServerName
$OnlineServers = @() ForEach( $OnlineServer in $OnlineXAServers ) $OnlineServers += $OnlineServer.ServerName $OfflineServers = @() ForEach( $Server in $XAServers ) If( $OnLineServers -notcontains $Server ) $OfflineServers += $Server Write-Output $OfflineServers A function in PowerShell can consist of up to three sections. Function Name Begin Process End The Begin section gets processed once at the beginning of the function. The Process section gets processed for each object in the pipeline. For this function, that is once. The End section gets processed once at the end of the function. We will use the Begin section to validate the Zone and the End section to handle output and cleanup. The Process section will contain the bulk of the original function. Using these three sections, the new function will look like this: Function Get-XAOfflineServer Param( [string]$zonename = '' ) Begin #validate zone if one entered Process If( $ZoneName -eq '' ) #snip Else #snip End
Write-Output $OfflineServers $AllXAServers = $null $OnlineXAServers = $null $XAServers = $null $OnlineServers = $null $OfflineServers = $null To validate the Zone name entered, the following code is added to the Begin section: Begin If($ZoneName -ne '') $ValidZone = IsValidZoneName $ZoneName If(-not $ValidZone) Write-Error "Invalid zone name $ZoneName entered" Break IsValidZoneName is a helper function that needs to be added to the script before the Get- XAOfflineServer function. The IsValidZoneName function needs to be declared before it can be used. That means it has to appear in the script before it can be used. We only need to test for a valid Zone name if a Zone name was passed to the Get-XAOfflineServer function. The IsValidZoneName function: Function IsValidZoneName Param( [string]$zonename ) $ValidZone = $false $Zones = Get-XAZone -ErrorAction SilentlyContinue If( -not $? ) Write-Error "Zone information could not be retrieved" Return $ValidZone ForEach($Zone in $Zones) If($Zone.ZoneName -eq $ZoneName) $ValidZone = $true $Zones = $null Return $ValidZone This helper function retrieves a list of Zones in the XenApp farm if possible. If the Zones cannot be retrieved, an error message is displayed and the function returns a False value. If the Zones are retrieved, the Zone names are compared to the Zone name passed to the helper function. If a match is
found, the function returns True, else it returns False. Since an object named $Zones was created, we clean up after ourselves and set the $Zones object to $null. Returning to the example from the end of Part 2 that caused the script to abruptly end, the script now gives a descriptive error (shown in bold) and ends. PS Z:\> Get-XAOfflineserver GoTitans Get-XAOfflineServer : Invalid zone name GoTitans entered At line:1 char:20 + Get-XAOfflineserver <<<< GoTitans + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get- XAOfflineServer PS Z:\> Running the script with no Zone name entered gives the original results. PS Z:\> Get-XAOfflineserver XENAPP2 XENAPP3 PS Z:\> Running the script with a valid zone name also gives the original results. PS Z:\> Get-XAOfflineserver GoVols XENAPP2 XENAPP3 PS Z:\> Just to make sure the script works, I powered on XenApp 2. PS Z:\> Get-XAOfflineServer XENAPP3 PS Z:\> I then shut down XenApp2 and powered on XenApp3. PS Z:\> Get-XAOfflineServer XENAPP2 PS Z:\> The finished script: Function IsValidZoneName Param( [string]$zonename ) $ValidZone = $false $Zones = Get-XAZone -ErrorAction SilentlyContinue If( -not $? ) Write-Error "Zone information could not be retrieved" Return $ValidZone ForEach($Zone in $Zones)
If($Zone.ZoneName -eq $ZoneName) $ValidZone = $true $Zones = $null Return $ValidZone Function Get-XAOfflineServer Param( [string]$zonename = '' ) Begin If($ZoneName -ne '') $ValidZone = IsValidZoneName $ZoneName If(-not $ValidZone) Write-Error "Invalid zone name $ZoneName entered" Break Process If( $ZoneName -eq '' ) $AllXAServers = Get-XAServer Sort-Object ServerName $OnlineXAServers = Get-XAZone Get-XAServer -OnlineOnly ` Sort-Object ServerName Else $AllXAServers = Get-XAServer -ZoneName $ZoneName Sort-Object ServerName $OnlineXAServers = Get-XAServer -ZoneName $ZoneName -OnlineOnly ` Sort-Object ServerName $XAServers = @() ForEach( $XAServer in $AllXAServers ) $XAServers += $XAServer.ServerName $OnlineServers = @() ForEach( $OnlineServer in $OnlineXAServers ) $OnlineServers += $OnlineServer.ServerName End $OfflineServers = @() ForEach( $Server in $XAServers ) If( $OnLineServers -notcontains $Server ) $OfflineServers += $Server Write-Output $OfflineServers $AllXAServers = $null $OnlineXAServers = $null $XAServers = $null $OnlineServers = $null
$OfflineServers = $null This script has been tested with XenApp 5 for Server 2003 both 32-bit and 64-bit, XenApp 5 for Server 2008 32-bit and XenApp 6. There is still one more way to get a list of offline servers. The original question was could PowerShell be used to get a list of offline servers. The answer is yes. But, there is another way. At The Experts Conference in June 2011, I took a PowerShell class given by Don Jones. One of the questions he asked was how would you map a drive letter using PowerShell? The answer? Net use d: \\Server\Share How do you find offline XenApp servers using PowerShell? Qfarm /offline (Figure 1) Figure 7 Being fairly new to PowerShell, I thought I could just run qfarm /offline convertto-html and my job would be done. But it was not that easy. Fortunately, my friend and Exchange MVP Michael B. Smith is also a PowerShell guru and helped me walk through the learning process. What follows is how Michael helped me generate HTML output. What did I get from running the commands? qfarm /offline convertto-html Garbage! (Figure 2)
Figure 8 The first thing Michael had me do was determine the type of output being generated by running: $obj = qfarm /offline $obj.gettype() This assigns the results of qfarm /offline to a variable $obj. Then the type of the variable is retrieved (Figure 3). Figure 9 Next, find the type of the members of $obj (Figure 4) by using $obj get-member
Figure 10 Since $obj is an array, Michael also wanted the members of the first element of the $obj array (Figure 5). $obj[0] get-member Note: PowerShell arrays start at 0. Figure 11
Michael then had me run the following code (results shown in Figure 6): $obj.count for( $i=0; $i -lt $obj.count; $i++ ) $obj[$i].gettype().fullname; $obj[$i].length Figure 12 Does the output look familiar? Compare Figure 6 to Figure 2. Michael then showed me how to get formatted HTML output similar to the qfarm /offline output (Figure 7). $str = $obj = qfarm /offline for( $i=0 ; $i lt $obj.count ; $i++ ) $str += $obj[ $i ] + <br/> ; ConvertTo-HTML body $str > C:\test.html Invoke-Item c:\test.html $str = $null $obj = $null Figure 13 If you know how to create and use a CSS file, the ConvertTo-HTML cmdlet allows one to be used to format the output more precisely. With PowerShell, there are usually several ways to accomplish a given task. If there is a command line utility available, use it. There is no need to reinvent the wheel.