Using Citrix WMI (Part 2 – Defining the function)

Posted: June 11, 2014 in Citrix, General
Tags: , , ,

At this point, we’ve created the parameter block for the get-zdc function.  It’s set up to accept pipeline data, and it is a mandatory parameter.

The next step is to initialize a few variables that we will be using.  Most powershell variables don’t require initialization, but for my own purposes, I frequently find it easier to have it initialized first.

	$Namespace = "root\Citrix"
	$FarmCanBeReached = $false # This is used for Ping & WMI tests
	$FarmIsAvailable = $false

As an option, all powershell functions have 3 major blocks that are processed

	BEGIN { #code }
	PROCESS { #code }
	END { #code }

The Begin block is processed once, the Process block is repeated with a loop, and the End block is processed once. These 3 lines could be placed inside the BEGIN block if desired. It’s not required per-se, for long scripts it is an option for organization.

The first line shows the namespace for WMI we’ll be using. Virtually all the scripts I’ve seen for ‘training’ purposes, or ‘how to’ scripts always use Root\CIMV2. But, as you can see, Citrix has it’s own provider. This is one of the reasons that WMI Explorer is so useful (see the first post). The 2nd and 3rd lines ore just establishing that we have not validated that we can reach a farm or not.

Next, we start a loop to process all of the objects being passed into the function. (This entire section of the loop could be embedded in the PROCESS section as mentioned above).

	foreach ($Computer in $ComputerName) {
	If ($FarmCanBeReached) {
		Continue
	} else {

The lines are pretty straight forward – We’re processing each individual computer from the ComputerName string that was passed to us, either by the pipeline, or by specifying the data on the command line. This could also be done using a ForEach-Object { loop. Using the foreach is actually slightly faster than ForEach-Object. The ForEach-Object loop uses the pipeline for each of the objects, and that pipelining adds some overhead. However, without a large number of objects (tens of thousands and above), you are likely to not notice a difference between the two, unless you are using measure-object.

So, for each of the objects in the computername list, we check to see if the $FarmCanBeReached variable is true. Since it is a boolean variable, and it starts out initialized as $false. If it is $true, then we use the Continue command. This command allows you to skip the remaining code in the loop, and move on to the next object. This is a Powershell V3 command. It can be done with V2, with a slightly different structure. (To use the alternate construction, you label your loop, and use a break statement. Your foreach loop becomes :LabelName foreach ($Computer in $ComputerName){ and our Continue becomes break LabelName. And obviously, our reason for this is if it is $true, we’ve established contact with the farm, and we don’t need to continue to try.

Next we do a ping check to validate that the computer is online. Obviously, you must either have the Windows firewall disabled, or allow ping access.

	$PingResult = Test-Connection -ComputerName $Computer -Count 1 -TimeToLive 5 -Quiet
	If ($PingResult) {

The Test-Connection command does a WMI ping. The interesting part is the -Quiet switch. This switch does not appear in the help for Test-Connection. The switch converts the output of the command to a boolean $true or $false depending on what happens. I’m storing it in a variable to provide an easier method of checking the data again later in the script.

We then start the If check to find out if the server can be pinged. If the ping succeeds, we start testing WMI. If pings are not allowed, the If ($PingResult) test could be removed. This next segment of code was designed to verify that WMI connectivity can be established. My coworker wrote the segment, and I don’t fully understand how it works, but it does. The gist of it is that it first attempts to bind to the specified WMI namespace (Recall that we set this to Root\Citrix) and tries to access the __Provider class, which is a required default for any WMI provider. It uses this to check for an Access Denied condition, and if that fails, it changes to Packet Privacy for the WMI calls, instead of plain text.

	try { Get-WmiObject -ComputerName $Computer -Namespace $Namespace -Class "__Provider" -ErrorAction Stop }
	catch [System.Management.ManagementException] { $Error[0].Exception.ErrorCode }
	catch { $Error[0] }
	)
	$Authentication = $(
		if ($AccessTestErrorCode -eq [System.Management.ManagementStatus]::AccessDenied) {
			[System.Management.AuthenticationLevel]::PacketPrivacy
		} else {
			[System.Management.AuthenticationLevel]::Default
		}
		)

Next we attempt to pull the Citrix_Product class from the provider. If it fails, it will not return anything, and if it does, we don’t care what it is, just that it exists.

$ProductWmi = Get-WmiObject -ComputerName $Computer -Authentication $Authentication -Namespace $Namespace -Class Citrix_Product -ErrorAction SilentlyContinue
	if ($ProductWMI -eq $null) {
		#WMI test failed
		$WMIResult = $false
	} else {
		$WMIResult = $true
	} #End of If ProductWMI is null test

You’ll notice, I tend to tag the end of my blocks of code by adding a comment (i.e. #End of If ProductWMI…. It makes it much easier to keep track of keeping all the blocks properly closed. (It is very difficult to track down a code block that is not properly closed (as in does not have the closing symbol such as a ) or }.

One possible optimization I have not tried to make yet with this script function, would be to embed the WMI code inside of the pingresult block. If you can’t ping the server, there is a good chance that you can’t connect to it by WMI and the WMI timeout is pretty long. Currently, that could be an issue where a machine could be pinged by not connected to by WMI.

But, we verify the combination of tests, and if they work, then we set the $FarmCanBeReached to $true. This lets the main loop skip over the other machines once we have succesfully connected.

If ($PingResult -and -$WMIResult) {
	$FarmCanBeReached = $true
	Continue
} #End of Ping & WMI result tests

In the next block, once we have established the fact that we have a machine in the farm that we can connect to, we start enumerating the Zones through WMI. If you notice the Select-Object -first 1 line, we’re selecting the first zone that populates. Since we only want a ZDC for the farm, we really don’t care which zone appears first. We do this first because the Zone Data Collector is a property of a Zone. I put a check in there in case the WMI call to get the zones were to fail, but in theory, it should not happen, since Citrix zones *always* have a ZDC, even if there is only one server, and/or all of them are set to Not Preferred, etc.

if ($FarmCanBeReached) {
	$ZoneWmi = Get-WmiObject -ComputerName $ComputerName -Authentication $Authentication -Namespace $Namespace -Class Citrix_Zone -ErrorAction SilentlyContinue | Select-Object -First 1
	#Test the WMI
if ($ZoneWmi -eq $null) {
	Continue
}

This is one of the most important lines of code here. It actually gets the name of the Zone Data Collector from the Zone itself.

	$MyZDC = ([System.Management.ManagementObject]$ZoneWMI.DataCollector).ServerName
} else {
	$MyZDC = $null
} #End of If FarmIsAvailable block

The construction of the line is important. Using the normal order of operations, the items inside of the parenthesis are processed first. So, we declare the type [System.Management.ManagementObject]. Just like most things in Powershell, using the square brackets indicates that the item is a .Net item (property, method, etc.). Declaring this is not strictly necessary, but it definitely helps decipher what is supposed to be happening in the code. And we are pulling the property with the ZDC ($ZoneWMI.DataCollector). However, if you look at it in WMI Explorer, this is a full WMI Path that is returned by default. This can be parsed directly, but if the path ever changes, the script will break, and since it is all object based, we just pull it directly. We do that by adding the property .ServerName at the end of ($ZoneWMI.DataCollector). And we deliberately set $MyZDC to null if we don’t find anything (for legibility/comprehension), even though it should never be possible. (It is actually possible, if you don’t provide *any* servers that are in the farm, are not contactable by ping and WMI, or possibly even a corrupt WMI stack on all the provided machines.)

And finally, we just wrap up the function and return the results.

 return $MyZDC 

I’ve attached the full script here. The full script has comment based help, and also has a lot of write-verbose statements to show what it is doing as it processes. The script also includes a function I had found online to make it easier to identify what line a script is at during execution, especially when combined with the write-verbose. The short version of this is that the by default, the MyInvocation is going to pull information about the execution of the script itself. By burying it in a function, it pulls the information about the execution of the function.

# ==============================================================================================
# 
# Microsoft PowerShell Source File -- Created with SAPIEN Technologies PrimalScript 2012
# 
# NAME: Get-ZDC.ps1 
# 
# AUTHOR: David Figueroa
# DATE  : 6/11/2014
# 
# COMMENT: This script is used to get the Zone Data Collector when given a Citrix server name
# 			or multiple Citrix server names
# 
# ==============================================================================================

function Get-CurrentLineNumber
{
	$MyInvocation.ScriptLineNumber
}
New-Alias -Name __LINE__ -Value Get-CurrentLineNumber –Description 'Returns the current line number in a PowerShell script file.'

Function Get-ZDC {

<#
	.SYNOPSIS
	Takes a computer name and get the Citrix Zone data collector through WMI calls
	
	.DESCRIPTION
	The function accepts a series of servernames within a single farm.  (If the servers
	span multiple farms, then any farms after the first successful one are ignored).
	The script does a ping test against each of them, and a WMI test for Citrix.  
	If both tests succeed, then the function queries the zone and gets the ZDC as of that moment.
		
	.PARAMETER ComputerName
	This is the name of the computer(s) for the initial query.   Multiple computer names can be sent
	in order to try and guarantee that the ZDC is found. 
	
	.EXAMPLE
	PS C:\> Get-ZDC -ComputerName server01
	serverzdc01
	PS C:\>
	
	.EXAMPLE
	PS C:\>Get-ZDC -ComputerName server01,server02
	serverzdc01
	PS C:\>
#>

	
	[CmdletBinding()]
	
	Param(
		[Parameter(mandatory=$true, valuefrompipeline=$true)]
		[Alias("CN")]
		[string[]]$ComputerName
		)
		
		write-verbose "$(__LINE__)`tEntering PROCESS block of Get-ZDC"
		#Get the list of servers from the ComputeName list and
		#parse them looking for ones that can be pinged & reached with WMI
	
		$Namespace = "Root\Citrix"
		write-verbose "$(__LINE__)`t`$NameSpace is $NameSpace"
		
		$FarmCanBeReached = $false # This is used for Ping & WMI tests
		write-verbose "$(__LINE__)`t`$FarmCanBeReached is $FarmCanBeReached"
			
		write-verbose "$(__LINE__)`tEntering ForEach Computer block"
		foreach ($Computer in $ComputerName)
		{
		write-verbose "$(__LINE__)`t`t`$Computer is $computer"
			If ($FarmCanBeReached) {
			write-verbose "$(__LINE__)`t`t`$FarmCanBeReached is True, Skip to the next server"
			Continue
			}
			else
			{
			write-verbose "$(__LINE__)`t`t`$FarmCanBeReached is False"
			write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
			write-verbose "$(__LINE__)`t`t# Connectivity tests, ping & WMI "
			write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
			write-verbose "$(__LINE__)`t`tPinging $Computer"
			$PingResult = Test-Connection -ComputerName $Computer -Count 1 -TimeToLive 5 -Quiet
				write-verbose "$(__LINE__)`t`tPing result is $PingResult"
			If ($PingResult)
			{
				write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
				write-verbose "$(__LINE__)`t`t# WMI"
				write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"
				write-verbose "$(__LINE__)`t`tBeginning WMI test"
				write-verbose "$(__LINE__)`t`tGet WMI Citrix_Product class from '$Namespace' namespace..."
				$AccessTestErrorCode = $(
			try { Get-WmiObject -ComputerName $Computer -Namespace $Namespace -Class "__Provider" -ErrorAction Stop }
				catch [System.Management.ManagementException] { $Error[0].Exception.ErrorCode }
				catch { $Error[0] }
			)
				write-verbose "$(__LINE__)`t`t`$AccessTestErrorCode = $AccessTestErrorCode"
				$Authentication = $(
				if ($AccessTestErrorCode -eq [System.Management.ManagementStatus]::AccessDenied)
				{
					[System.Management.AuthenticationLevel]::PacketPrivacy
				}
				else
				{
					[System.Management.AuthenticationLevel]::Default
				}
				)
			$ProductWmi = Get-WmiObject -ComputerName $Computer -Authentication $Authentication -Namespace $Namespace -Class Citrix_Product -ErrorAction SilentlyContinue
			if ($ProductWMI -eq $null)
				{
					#WMI test failed
					$WMIResult = $false
					write-verbose "$(__LINE__)`t`t`$WMIResult is failed, `$ProductWMI is null"
				}
				else
				{
					$WMIResult = $true
					write-verbose "$(__LINE__)`t`t`WMIResult is good.  `$Product is not null"
				} #End of If ProductWMI is null test
				
				write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++"
				write-verbose "$(__LINE__)`t`t# End of WMI test"
				write-verbose "$(__LINE__)`t`t#++++++++++++++++++++++++++++++++++++++++++++++++++"
			} #end of $PingResult block
			
			#Now that connectivity tests are done..
			#Validate that they are both good to receive the name of the ZDC
			If ($PingResult -and -$WMIResult) {
					write-verbose "$(__LINE__)`tPing & WMI results are good, FarmCanBeReached set to true"
				$FarmCanBeReached = $true 
					write-verbose "$(__LINE__)`tFound a good machine, using Continue to skip to the next computer"
				Continue
			} #End of Ping & WMI result tests
				write-verbose "$(__LINE__)`tEnd of if FarmCanBeReached block"
		} #End of If FarmCanBeReached test

		write-verbose "$(__LINE__) End of ForEach computer block"
		} #End of ForEach $computer block

		#Test if the farm is available and proceed from there.
		if ($FarmCanBeReached) {
			write-verbose "$(__LINE__)`tGetting WMI for Zones collection"
				$ZoneWmi = Get-WmiObject -ComputerName $ComputerName -Authentication $Authentication -Namespace $Namespace -Class Citrix_Zone -ErrorAction SilentlyContinue | Select-Object -First 1
			
			#Test the WMI
			if ($ZoneWmi -eq $null) {
				Continue
			}
				$MyZDC = ([System.Management.ManagementObject]$ZoneWMI.DataCollector).ServerName
				write-verbose "$(__LINE__)`t`$MyZDC is $MyZDC"
		} else {
		
				#If we get this far, no successful ZDC was found, return $null
				write-verbose "$(__LINE__)`tCould not locate ZDC.  Please investigate and rerun.  A likely cause is no valid servers are in the config file"
				$MyZDC = $null 
		
		} #End of If FarmIsAvailable block
	
		Write-Verbose -Message "Returning `$MyZDC ($MyZDC)"
		return $MyZDC 

} #End of Get-ZDC function

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s