Using an XML file to store and retrieve settings

If you are unfamiliar with XML then I’d suggest a quick visit to this URL should cover everything you need to know before reading the example below.

https://www.w3schools.com/xml/xml_dtd_intro.asp

You can use an XML file to store and retrieve information and as long as you know the structure you can easily locate and access the information like this: <xmlDocument>.<element>.<element>.setting – a code example should show you how to do this. You can even use where clauses etc. to extract multiple xml elements although I didn’t include any of these in my example- it’s pretty cool and I’ll try and write a few articles on using XML in your PowerShell scripts including storing password securely and creating them from within a script.

The example below has an element with an attribute and this is accessible too using the same methodology but I’d try not to use attributes in an XML doc if I were you – see the recommendation in this link.

https://www.w3schools.com/xml/xml_dtd_el_vs_attr.asp

<?xml version="1.0"?> 
 <Settings> 
  <EmailSettings available="TRUE"> 
   <SMTPServer>smtp.clan8.com</SMTPServer> 
   <SMTPPort>25</SMTPPort> 
   <MailFrom>svc-messagingtasks@man.com</MailFrom> 
   <MailTo>Jason.Bourne@clan8.com</MailTo> 
   <MailTo>James.Bond@clan8.com</MailTo> 
   <MailTo>Joe.Bloggs@clan8.com</MailTo> 
   <MailTo>Jane.Doe@clan8.com</MailTo> 
  </EmailSettings> 
 <Exclusions> 
  <DBName></DBName> 
  <DBName></DBName> 
 </Exclusions> 
 <OtherSettings> 
  <ThresholdMonday>48</ThresholdMonday> 
  <ThresholdOther>24</ThresholdOther> 
 </OtherSettings> 
</Settings>
# Given an example XML file shown above you can reference things 
# in the XML like this: 
[xml]$xmlFile = Get-Content c:\Temp\settings.txt

$smtpServer = $xmlFile.settings.emailsettings.smtpserver
$smtpPort = $xmlFile.settings.emailsettings.SMTPPort
$DistributionList = $xmlFile.settings.emailsettings.MailTo
$attributeValue = $xmlFile.settings.emailsettings.available 
ForEach ( $member in $smtpServer ) {
 write-host "SMTP Server - $member "
}

if ( $smtpPort -is [system.array] ) {
 ForEach ( $member in $smtpPort ) {
  write-host "SMTP Port - $member "
 }
}
else {
 write-host "SMTP Port - $smtpPort"
}

$smtpServer.Gettype()
$DistributionList.GetType()

ForEach ( $member in $DistributionList ) {
 write-host " - $member "
}
Advertisements

Converting a cURL into PowerShell Invoke-RESTMethod

A couple of projects recently involved using REST APIs to manage storage and auditing solutions. The problem is both the companies product support pages did not list any examples using PowerShell so here are some notes on how to convert the examples they provide into PowerShell using

Invoke-RestMethod

During my discovery someone recommended Postman as a good tool to test REST APIs – it’s a nice simple interface and it will help you make sense of the URL you need to create in order to use with the -URL switch of Invoke-RESTMetthod and speed up testing so I’;d definitely recommend this to someone trying to figure out a REST API.

The product we were testing has an interactive API page which is actually pretty cool as you can plug in values and it will actually show you on screen in the web page all the returned values or an error code if it didn’t work.

For this example I’m going to convert this cURL to Powershell
https://developer.tenable.com/reference#assets-list-assets

curl –request GET \
–url https://cloud.tenable.com/assets \
–header ‘accept: application/json’
–header ‘x-apikeys: accessKey={ACCESS_KEY};secretKey={SECRET_KEY}’

we can see some obvious values to extract here and I created variables to hold the them using even more obvious names ūüôā

$URL = ‘ https://cloud.tenable.com/assets&#8217;

and a header but if you look at the command help you can see that the header needs to be a hashtable


$headers = @{} # to create a hashtable
$headers.Add(‘accept’,’application/json’
$headers.Add(‘x-apikeys’,”accessKey=$accessKey;secretKey=$secretKey)
# these keys are generated int eh management portal

Now we are ready to call the REST-API like this

$response = Invoke-RestMethod -Uri $URL -ContentType ‘application/json’ `
-Headers $headers

You can now start to interogate the response variable to get what you need.

In this case $response.Access provided the information on each asset in the system – however there was a limit of 5000 objects returned. When this happens there is usually a way of controlling how many objects are returned and where to start, i.e.a limit and an offset. Once you find out how to do this you make multiple calls to the URL using a different offset value each time.

Nasuni does this by sending a parameter which is a string appended to the end of the URL like this:

https://$host.api/v1.1/volumes/folder-quotas/?limit=$limit&offset=$offset

we use a simple loop and increment the $offset variable each time in the loop.

For this system Teneble though this command did not have any controls to page through the data instead you need to request the system build a report and then you can download the report in chunks ( as they call them ).

This page tells you how :

https://developer.tenable.com/reference#exports

curl --request POST \   
--url https://cloud.tenable.com/assets/export \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-apikeys: accessKey={ACCESS_KEY};secretKey={SECRET_KEY}'

and this returns and export UUID : 'export_uuid'
$URL = "https://cloud.tenable.com/assets/export"
$exportUUID = Invoke-RestMethod -Uri $URL -ContentType 'application/json'
-Headers $headers

Next you monitor the progress of the export by calling this URL

curl --request GET \
--url https://cloud.tenable.com/assets/export/export_uuid/status \
--header 'accept: application/json' \
--header 'x-apikeys: accessKey={ACCESS_KEY};secretKey={SECRET_KEY}'

$URL = "https://cloud.tenable.com/assets/export/$exportUUID/status"
$Status = Invoke-RestMethod -Uri $URL -ContentType 'application/json'
-Headers $headers

This will return an object with these attributes
‘status’ and ‘chunks_available’ ‘ put the code in a while loop and wait for status to be ‘FINISHED’
While { <script block here> } ($status -ne ‘FINISHED’)

make sure you have a way of exiting this loop if it takes too long or something goes wrong

Now when the loop has completed you can iterate through the chunks using a for loop and download each chunk like this

curl --request GET \   
--url https://cloud.tenable.com/assets/export/export_uuid/chunks/chunk_id \ --header 'accept: application/json' \
--header 'x-apikeys: accessKey={ACCESS_KEY};secretKey={SECRET_KEY}'
for ($index = 1; $index -lt $($status.chunks_available.count); $index++) {
$URL = "https://cloud.tenable.com/assets/export/$exportUUID/chunks/$index"
$(Invoke-RestMethod -Uri $URL -ContentType 'application/json'
-Headers $headers) | export-csv c:\temp\myAssets.csv -append
}

Hopefully you get the idea – oh one last problem is most of the columns in this instance are arrays so these don’t export very well to a CSV file but if you google this function your problems will be solved just pipe it through this function first.

https://gallery.technet.microsoft.com/scriptcenter/Convert-OutoutForCSV-6e552fc6

I hope this helps speed up your REST API scripting …


Quick and Dirty way of creating objects in a loop so you can easily export to a report

You may, or may not be familiar with the quick and dirty way of creating a custom Powershell Object where you pipe an empty string to Select-Object and store the result in a variable like this:

$myCustomObject = “” | select ‘jpegFile’,’guid’

Then you can assign values to the attributes like this

$myCustomObject.’jpegFile’ = <FilePathtoJpeg>

$myCustomObject.’guid’ = <GUID>

I’ve often used this in a loop and create an array of objects to later export using export-csv.¬† This means inside the loop I was first creating a new object and setting the attribute values using the lines above in every iteration.¬† I have no idea how long I’ve been doing this but today working on another script I realised that I could shorten this to a single line of code inside the loop.

$photosToUploadtoAD += “” | select @{n=’jpegFile’;e={“$($SPImportPath)\$($employee.sAMAccountName.toLower()).jpg”}},@{n=’guid’;e={$employee.ObjectGUID.toString()}}

Oh so simple and I have no idea why I’d not thought of doing such an obvious thing before. ¬†I guess for complex objects the command line might get unhealthy long but then you can use the other little trick of creating a variable to hold the select-object statement, e.g.

$objectProperties = (
@{n=’jpegFile’;e={“$($SPImportPath)\$($employee.sAMAccountName.toLower()).jpg”}}
@{n=’guid’;e={$employee.ObjectGUID.toString()}}
)

$photosToUpdate +=¬†“” | select $objectProperties

Get-CSVDelimiter

This is a function I got from the internet ( http://www.powertheshell.com/autodetecting-csv-delimiter/ ) and modified a little can be used to determine the delimiter used in a CSV file.

It does this in such a simple way you’ll be wondering why you didn’t think of it yourself.¬† It parses each line and counts the obvious ‘candidates’ for a delimiter and then after each line checks if the ‘usual suspects’ ( great film by the way and¬† if you haven’t seen it yet the what have you been watching, neighbors, come on this should be on everyone’s watch list)¬† then you can use the appropriate ‚Äďdelmiter switch on the import-command.

A CSV file will use different delimiters depending on the country you live in apparently.¬† so the C in Comma Separated Values clearly does not stand for comma ūüôā

In the UK it’s not uncommon to see PIPE delimited file i.e. using | instead of , probably because when you open it with excel it doesn’t¬† mangle the ‘integer looking’ values in the row, e.g. a telephone number like this +44 20 7123 1234¬†canget converted into a numeric value including an exponential and don’t get me started on how Excel mangles date formats depending on the workstation local, but we should all blame ourselves for allowing each country to ‘choose’ a date format.¬† Why can’t we all agree one way or the other which parts of a date are which, i.e is this the 12th of the 1st and of which month, 12/1/2018 – you are either slightly early or exceptionally late to the party!

12/1/2018 – you are either slightly early or exceptionally late to the party! Neither is a good place to be.

Anyway,. let talk about CSV files.  This handy function should be able to work out what character is being used as a delimiter and then you can use this information in an import-csv file to ensure that you import the data correctly.

e.g.¬†Import-Csv ‚ÄďDelimiter ¬†(Get-CSVDelimiter c:\temp\mycsvfile.csv )

Although I’d put a little more error checking around it if you are planning on using this in your scripts.

Like all code I get from the internet I try to analyse how it does what it does and in doing this I saw some nice ideas around formatting the output.  This blog post on PSCustom objects will tell you all ( hopefully everything ) you need to know about PSCustom objects.  I particularly liked the elegant solutions on controlling the output attributes, something we take for granted when we output an objects contents to screen.

Haven’t you noticed that poweshell does not always show every attribute of an object – try using ‘| select * | fl’ some time to see the difference.¬† I wish the Quest commandlets would do this as those commandlets return a lot of attributes which is why they tend to be a lot slower than the AD commandlet equivalents.

https://kevinmarquette.github.io/2016-10-28-powershell-everything-you-wanted-to-know-about-pscustomobject/

function Get-CSVDelimiter { Version 1.00
[CmdletBinding()]
param (
  # Path name to CSV file
  # can be submitted as string or as File object
  [Parameter(ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)]
  [Alias('FullName')]
  [String]$Path
)
begin {
  # list of ascii codes that typically cannot be a delimiter: 0-9, A-Z, a-z, and space:
  $excluded = ([Int][Char]'0'..[Int][Char]'9') + ([Int][Char]'A'..[Int][Char]'Z') + ([Int][Char]'a'..[Int][Char]'z')  + 32
  
  function Get-DelimitersFromLine {
   param($TextLine)
   $quoted = $false
   $result = @{}
   # examine line character by character
   foreach($char in $line.ToCharArray()) {
    # if a double-quote is detected, toggle quoting flag - ignores everything inside a double quoted string
    if ($char -eq '"') { $quoted = -not $quoted } elseif ($quoted -eq $false) { if ($excluded -notcontains [Int]$char) {  $result.$([Int]$char) ++  } }
   } # foreach($char in $line.ToCharArray()) {
   $result
  } # Get-DelimitersFromLine
} # Begin
 process {
  # initialize variables
  $oldcandidates = $null
  # examine each line in CSV file:
  $file = [System.IO.File]::OpenText($Path)
  $lines = 0
  $foundDelimiter = $false
  While (-not $file.EndOfStream) {
   $line = $file.ReadLine()
   $lines++
   if ( $foundDelimiter ) { continue }
   # examine current line and get possible delimiters:
   $candidates = Get-DelimitersFromLine $line
   # if this is the first line, skip analysis
   if ($oldcandidates -eq $null) {
    # if first line starts with "#", ignore
    if (-not $line.StartsWith('#')) { $oldcandidates = $candidates }
   } # else, identify ascii codes that have the same frequency in both this line and the previous line, and store in hash table $new:
   else {
    $new = @{}
    $keys = $oldcandidates.Keys
    foreach($key in $keys) {
     if ($candidates.$key -eq $oldcandidates.$key) {
      $new.$key = $candidates.$key
     }
    }
    $oldcandidates = $new
    # if only 1 possible delimiter is left, we are done
    # exit loop, no necessity to examine the remaining lines:
    if ($oldcandidates.keys.count -lt 2) {
     $foundDelimiter = $true
     Continue
    }
   }
  } # While (-not $file.EndOfStream) {
  $file.Close()
  # create return object
#  $rv = New-Object PSObject | Select-Object -Property Name, Path, Delimiter, FriendlyName, ASCII, Columns, Rows, Status
#  $rv.Rows = $lines
#  $rv.Path = $Path
#  $rv.Name = Split-Path -Path $Path -Leaf
  # no keys found:
  if ($oldcandidates.keys.count -eq 0) {
   $rvStatus = 'No delimiter found'
  } # exactly one key found, good:
  elseif ($oldcandidates.keys.count -eq 1) {
   $ascii = $oldcandidates.keys | ForEach-Object { $_ }
   $rvASCII = $ascii
   # convert ascii to real character:
   $rvDelimiter = [String][Char]$ascii
   # number of Columns is frequency of delimiter plus 1:
   $rvColumns = $oldcandidates.$ascii + 1
   # add friendly names for the most common delimiter types:
   switch ($ascii) {
      9 { $rvFriendlyName = 'TAB'       }
     43 { $rvFriendlyName = "Plus"      }
     44 { $rvFriendlyName = 'Comma'     }
     58 { $rvFriendlyName = 'colon'     }
     59 { $rvFriendlyName = 'Semicolon' }
    124 { $rvFriendlyName = 'Pipe'      } 
   }
   $rvStatus = 'Found'
  } # elseif ($oldcandidates.keys.count -eq 1) {
  # ambiguous delimiters detected, list ambiguous delimiters
  else {
   # convert delimiter ascii keys in a comma-separated list of quoted characters:
   $delimiters = (($oldcandidates.keys | ForEach-Object { ('"{0}"' -f [String][Char]$_) }) -join ',')
   $rvStatus =  "Ambiguous separator keys: $delimiters"
  }
  $rv = [PSCustomObject]@{
    'PSTypeName'   = 'My.DelimiterTypeObject'
    'FileName'      = $(Split-Path -Path $Path -Leaf)
    'Path'          = $Path
    'Columns'       = $rvColumns
    'Rows'          = $lines
    'DelimiterName' = $rvFriendlyName
    'Delimiter'     =  $rvDelimiter
    'ASCII'         = $rvASCII
    'Status'        = $rvStatus
  }
  # ETS: set default visible properties on return object (applies to PS 3.0 only, no effect in PS 2.0):
  [String[]]$properties = 'FileName','DelimiterName','Columns','Rows'
  [System.Management.Automation.PSMemberInfo[]]$PSStandardMembers = New-Object System.Management.Automation.PSPropertySet DefaultDisplayPropertySet,$properties
  $rv | Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $PSStandardMembers
  # return result:
  $rv
} # process {
 
} #                     Get-CSVDelimiter Version 1.00

A simple way of adding a menu to a script

I was actually showing someone at work how easy it was to access SQL using PowerShell
and as I was walking my colleague through the code another team member scoffed at me
as I used the words “iterate through the data rows returned to print them out to screen”.

Ahh it’s hard not to eaves drop when in the office, especially if the topic is one you
are familiar with and even harder not to comment after overhearing something,
I do it all the time ūüôā

The following discussion was that there are better ways of displaying the returned SQL
table data than ‘iterating’ through the rows and using write-host to print to screen.

I thought immediately of using ‘Out-Gridview’ perhaps, so I did a quick google to see
what other people were doing and came across this interesting idea using ‘Out-Gridview’¬†
to create a simple user menu.

The blog contained this simple bit of code:

$Menu = [ordered]@{
  1 = 'Do something'
  2 = 'Do this instead'
  3 = 'Do whatever you  want'
}

$Result = $Menu | Out-GridView -PassThru  -Title 'Make a  selection'
Switch ($Result)  {
  {$Result.Name -eq 1} {'Do something'}
  {$Result.Name -eq 2} {'Do this instead'}
  {$Result.Name -eq 3} {'Do whatever you  want'}
}

This is such a clever idea, so I wanted to convert it to a function that I could use
in some future scripts and of course blog about it.

To convert a script into a function ( and this is just a quick conversion as I’ve not added any parameter help etc. ) I do the following:

I usually write the function in place just above the original code so I  can run some tests through the two sets of code side by side.  It also means I can see both the original code and my function right next to each other to confirm I translated it properly.

Lets start then by naming the function, using an approved verb.

Function Get-MenuItem {

}

Now add a parameter block but what do I need to parameterise?¬† The point of a function is to reuse code.¬† Hard coding the menu items to choose from in the function might be a little limiting but you could use¬†a default value for the parameters if you had a regular menu that you wanted to use.¬† I’ve not done that here by the way.

To add a default value just put an equals sign after the parameter name and set a value. When calling the function the default value will be used if the parameter is not provided on the calling command line.

Choosing what to convert to a parameter

Function Get-MenuItem {
 param (
  $menuItems
 )
}

If you look at the section of code you are converting and identify the things that either are already variables or are hard coded lines of code you might want to set at run time rather than have them hard coded in your script.

In this case the $menu variable holds the menu items to choose so it’s this that I’ll use as a parameter.

$menu is of type [System.Collections.Specialized.OrderedDictionary] – see the scripting guy blog for details on this new variable type found in Powershell 3.0

I’ve called my parameter $menuItems but it’s not a [System.Collections.Specialized.OrderedDictionary] it’s an array variable, we will
build the OrderedDictionary inside the function.

Another reason to use a function is to ‘hide’ the complexity of a task and simplify the main section of the script.

We can call the script using a one liner:

Get-MenuItem @('Do something','Do whatever you want','Do this instead')

I used a loop to build the OrderedDictionary using the $menuItems parameter to provide the values.¬† I could add code to sort this data before I put the array elements into the OrderedDictionary – you can see how it’s easy to get carried away and add more and more functionality.¬† Lets resist that temptation for now, we just need to get it working first and add functionality later if we have time.

 $Menu  = [ordered]@{} # intialises the OrderedDictionary
 $items = 1 # used to create a unique key 1,2,3 etc.
 ForEach ( $menuItems in $menuItems ) {
  $Menu.Add($items,$menuItems) # adds the menu item to the OrderedDictionary
  $items++
 }

At this point I could have finished the function with the remaining code from the original script

$Result = $Menu | Out-GridView -PassThru  -Title 'Make a  selection'
Switch ($Result)  {
  {$Result.Name -eq 1} {'Do something'}
  {$Result.Name -eq 2} {'Do this instead'}
  {$Result.Name -eq 3} {'Do whatever you  want'}
}

But wait how is that dynamic and set at run time?

It’s not so I need find a way of writing a generic select statement. I ran the script with a break point at the start of the switch statement¬†so I could inspect the variable and I realised that the switch statement was redundant completely.

$result was a ‘DictionaryEntry‘ e.g.

Name     Value
—-¬† ¬† ¬† ¬† ¬† ¬† —–
2               Do this instead

So rather than use a switch statement that prints the same text that held in $result.Value I can just return $result.Value – easy!

I was done at this point but before moving onto the next challenge I added some code to make the function a little more resilient to errors.

The user could select more than one item, not realise they need to click OK or perhaps they click the close button.  To handle these scenarios I added this code:

 $selectionMade = $false # used to break out of the do loop when a valid choice is made by the user
 do {
  $Result = $Menu | Out-GridView -PassThru  -Title 'Make a  selection and click OK'
  if ( ( $Result.Count -eq 1 ) -or ( $Result -eq $null ) ) { # only 1 item selected so we can break out of the loop
   $selectionMade = $true
  }
  elseif ( $Result.Count -gt 1 ) { # more than one item selected so we need to tell the user to select just one option
   [System.Windows.MessageBox]::Show('You cannot select multiple choices - please select 1 menu option') | Out-Null
  }
 } until ( $selectionMade )
 if ( $Result -eq $null ) { # this use case is when the user pressed close or cancel we'll return $null
  [System.Windows.MessageBox]::Show('No Choice was made so cannot continue - Quitting script') | Out-Null
  Return
 }

I also used this quick way of informing the user whats happening with a message box:

[System.Windows.MessageBox]::Show('No Choice was made so cannot continue')

And that’s it! ( for now anyway ) – here is the full function:

Function Get-MenuItem {
 param (
  $menuItems
 )
 $Menu = [ordered]@{}
 $items = 1
 ForEach ( $menuItems in $menuItems ) {
  $Menu.Add($items,$menuItems)
  $items++
 }
 $selectionMade = $false
 do {
  $Result = $Menu | Out-GridView -PassThru  -Title 'Make a  selection and click OK'
  if ( ( $Result.Count -eq 1 ) -or ( $Result -eq $null ) ) {
   $selectionMade = $true
  }
  elseif ( $Result.Count -gt 1 ) {
   [System.Windows.MessageBox]::Show('You cannot select multiple choices - please select 1 menu option') | Out-Null
  }
 } until ( $selectionMade )
 if ( $Result -eq $null ) {
  [System.Windows.MessageBox]::Show('No Choice was made so cannot continue - Quitting script')  | Out-Null
  Return
 }
 Return $Result.value
}

Get-MenuItem  @('Do something','Do whatever you want','Do this instead')

 

Finding your Exchange Servers

For similar reason I mentioned in this post¬†‘How to locate your ARS servers using the service connection point’ I wrote a function to find my exchange servers.

It’s never a good idea to hard code stuff into your scripts as these make your code less portable and also you are at the mercy of environment changes.¬† Your scripts will fail when the hard coded variable value no longer matches the server or object your are trying to connect to.

By default the function returns exchange servers from the local site of the machine running the script. if the $InSiteOnly parameter is specified the function only returns exchange servers in the local / specified site unless there are no servers in that site when it will return servers from all sites.

Function Get-ExchangeServers { # Version 2.00
 [cmdletbinding()]
 param (
  [parameter(Mandatory=$false,Position=1,HelpMessage='Returns the exchange server names from the specified site in preference to any other site')][string]$ADSiteName,
 [parameter(Mandatory=$false,Position=2,HelpMessage='When present will only return exchange servers from the local / specified site unless the site does not contain more than "$maxNumberOfServers" exchange servers then it will return servers from all sites in addition to the specified site')][switch]$InSiteOnly,
[parameter(Mandatory=$false,Position=0,HelpMessage='The maximum number of exchange servers to return, it will by default return local / specified site servers at the top of the list')][ValidateRange(1,[int]::MaxValue)]
[int]$maxNumberOfServers=[int]::MaxValue,
 [parameter(Mandatory=$false,Position=3,HelpMessage='Returns the specified exchange server version only from the specified site in preference to any other site')] [ValidateSet("2013","2016")][string]$Version 
 )
 if ( $Version ) {
  switch ( $Version ) {
   "2013" { $VersionString = "Version 15.0" }
   "2016" { $VersionString = "Version 15.1" }
  }
 }
 if ( $ADSiteName ) { # AD Site name specified so get the site DN  $computerSiteDN = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().Sites  | Where-Object { $_.name -eq $ADSiteName } | Select-Object @{name="DN";expression={$_.GetDirectoryEntry().distinguishedName}} | Select-Object -ExpandProperty DN }
 else { 
$ADSiteName = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite().GetDirectoryEntry().name
 }
 if ( $computerSiteDN -eq $null ) { 
# AD Site name not specified or not found so get the local machines site
  $computerSiteDN = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite().GetDirectoryEntry().distinguishedName
 }
 if ( $computerSiteDN -eq $null ) {
  Throw "FATAL ERROR: Unable to get the AD site DN"
 }
 $returnData = @() # ensures that an array of server names is always returned
 # search the site for exchange servers
 $SearchTool = <strong>New-Object</strong> DirectoryServices.DirectorySearcher([ADSI]('LDAP://' + ([ADSI]'LDAP://RootDse').configurationNamingContext))

 $SearchTool.Filter = "(objectClass=msExchExchangeServer)"
 $ExchangeServers = $SearchTool.FindAll()
 # get the exchange servers that are in the local / specified AD Site
 $exchangeServersInSite    = @($ExchangeServers | <strong>Where-Object</strong> { $_.Properties.msexchserversite -eq $computerSiteDN })
 if ( $VersionString ) {
  $exchangeServersInSite    = @($exchangeServersInSite  | Where-Object { $_.Properties.serialnumber.substring(0,12) -eq $VersionString })
 }
 $exchangeServersInSite = @($exchangeServersInSite | Select-Object</strong> @{name="name";expression={$_.properties.name}} | Select-Object  `
-ExpandProperty  name)
 if ( $exchangeServersInSite.count -le 0 ) { # no servers found in local / specified AD site so lets get all exchange servers in all sites
  $exchangeServersInSite = @($ExchangeServers | <strong>Where-Object</strong> { $_.Properties.msexchserversite -ne $computerSiteDN } )
  if ( $VersionString ) {
   $exchangeServersInSite    = @($exchangeServersInSite  | <strong>Where-Object</strong> { $_.Properties.serialnumber.substring(0,12) -eq $VersionString })}
  $exchangeServersInSite = @($exchangeServersInSite | Select-Object @{name="name";expression={$_.properties.name}} | Select-Object `
-ExpandProperty</em>  name)}
 if ( $exchangeServersInSite.count -le 0 ) {
  Throw "FATAL ERROR: Unable to find any Exchange servers"
 }
 if ( $InSiteOnly ) {
  # Return just the exchange servers we have so far unless the site specified 
  # has no servers then return servers from all sites
  $returnData += $exchangeServersInSite | Get-Random -Count $(if ($exchangeServersInSite.count -le $maxNumberOfServers ) { $exchangeServersInSite.count } else { $maxNumberOfServers })}
 else {
  if ( $maxNumberOfServers -le $exchangeServersInSite.count ) { 
# number of servers requested can be delivered from the in site server list so 
# lets return them
 $returnData += $exchangeServersInSite | Get-Random -Count $maxNumberOfServers }
  else { 
# we need more servers so lets add in additional servers from the other sites
   $exchangeServersNotInSite = @($ExchangeServers | Where-Object { $_.Properties.msexchserversite -ne $computerSiteDN } )
   if ( $VersionString ) {
    $exchangeServersNotInSite    = @($exchangeServersNotInSite  | Where-Object { $_.Properties.serialnumber.substring(0,12) -eq $VersionString })
   }
   $exchangeServersNotInSite = @($exchangeServersNotInSite | Select-Object @{name="name";expression={$_.properties.name}} | Select-Object -ExpandProperty</em>  name)

   $returnData += $($exchangeServersInSite + $( $exchangeServersNotInSite | Get-Random -Count</em> $(if ($exchangeServersNotInSite.count -le $($maxNumberOfServers - $exchangeServersInSite.count) ) { $exchangeServersNotInSite.count } else { $($maxNumberOfServers - $exchangeServersInSite.count) })))
  }
 }
 if ( $ReturnData.count -le 0 ) {
  Write-Error "ERROR: No exchange Servers Returned for site: '$ADSiteName'"
 }
 Return ,$returnData
}            # Get-ExchangeServers            Version 2.00