Tuesday, February 7, 2017

From Test-Connection to a one line long Filter to ping via PowerShell with a timeout

Having had to deal with mass activation of WinRM in large environments, I have been asked to provide a report of the servers actually answering to remote requests. Sure enough I wrote a PowerShell function for this task, which I am going to present in a future post. Today I just want to take the time to do a bit of a back-to-the-basics post to explain the first part of the function that does a pretty common job: pinging servers to see if they are alive.

PITFALLS OF TEST-CONNECTION

You must be thinking that this is easily accomplished with Test-Connection, but the problem of this cmdlet is that it can be super slow in large environments, where you probably have servers that happen to be offline, servers that don't exist anymore but are still referenced in the Active Directory or in the DNS, servers that block ICMP and servers which are just temporarily unresponsive.

The reason for not to use Test-Connection is that, believe it or not, Test-Connection does not support a -TimeOut parameter so each ping by default respects the timeout of 1000ms that was designed with networks of 20-30 years ago in mind. So for each offline computer you have by default to wait for four times 1000ms and pinging many servers can take a long time.

A little side note here: do not confuse the -TimeToLive or TTL parameter that comes with Test-Connection with the Timeout parameter we hope to have: a TTL is the maximum number of hops it can take to get from one host to another, not the time to wait for a reply. It is used to make certain a packet doesn't live forever on your network when it's lost.  The PowerShell help and a lot of other resources out there are confusing those and that's why in IPv6 TimeToLive has been renamed to Hop Limit:


So, even if Test-Connection does not have a -TimeOut parameter, there are a couple of ways to make it run a bit faster. Let's have a lot at those ways of pinging and see how and why they make the classical error handling a bit complex.

The first option to speed it up is to use it with the -Quiet switch set to True and the -Count parameter set to 1, like in the following example:

Test-Connection $Computer -Quiet -Count 1

Now, for the error handling part, you could think that including that command in a Try{}Catch{} block would work. You're wrong: using the -Quiet parameter forces Test-Connection to suppress the errors and just return a Boolean value, which is $True if any of the four pings succeeds, otherwise it is $False.

So you have to enclose the command in a conditional statement to return its outcome:

if(Test-Connection $Computer -Quiet -Count 1) { 'OK' } else { 'NOK' }

Now if you run Test-Connection without -Quiet, the cmdlet returns a Win32_PingStatus object when the query is succesful:

Test-Connection $Computer -Count 1 | Get-Member

   TypeName: System.Management.ManagementObject#root\cimv2\Win32_PingStatus

Name                           MemberType     Definition
----                           ----------     ----------
PSComputerName                 AliasProperty  PSComputerName = __SERVER
Address                        Property       string Address {get;set;}
BufferSize                     Property       uint32 BufferSize {get;set;}
NoFragmentation                Property       bool NoFragmentation {get;set;}
PrimaryAddressResolutionStatus Property       uint32 PrimaryAddressResolutionStatus
ProtocolAddress                Property       string ProtocolAddress {get;set;}

On the contrary, if the query is unsuccessful, it doesn't return anything at all and you have to use a standard Try{}Catch{} block in order to retrieve the result:

try { Test-Connection $Computer -Count 1 -ErrorAction Stop; 'OK' } catch { 'NOK' }

To sum up, sending just one echo request reduces the execution time but, still, if I can't shorten the timeout, each query will last way too long for the inventory of many servers.

Let's now have a look then at something that manages very low timeouts (down to 5 milliseconds or less).

THE WIN32_PINGSTATUS CLASS

The first possibility is through Get-WmiObject Win32_PingStatus, which supports a timeout: as you can read in the documentation:


Get-WmiObject Win32_PingStatus -filter "address='$computer' and timeout=5"

The issue with this class is that Win32_PingStatus -f "Address='Serverdoesnotexist'" doesn't lead to an error, so you have to rely on the StatusCode property to see if the ping succeeded or not:

if ((Get-WmiObject Win32_PingStatus -filter "address='$computer' and timeout=5").StatusCode -eq 0) {
    'OK'
}
else {
    'NOK'
}

THE SYSTEM.NET.NETWORKINFORMATION.PING CLASS

The other possibility is to rely on the System.Net.NetworkInformation.Ping class.

$ping = new-object System.Net.NetworkInformation.Ping 
$ping.Send($computer)

In this case, while you have to mess with the PingOptions class to add such things as a TTL or to set a DontFragment flag, you can directly use the (IPAddress^, Int32) overload to define the maximum number of milliseconds to wait for the ping to succeeded:

$ping.Send($computer,5)

As you can see I have set the TimeOut in seconds to a very low value if compared to the standard 1000ms of the classical ping command: setting the value so low will make your ping sweep go much faster while keeping a very good precision since, on modern network, servers that are alive will send an echo reply at the speed of light, even if there are a bunch of routers in between.

The difficulty with using this class is that there are three possible cases that require different error handling methods:

Case n. 1: you try to ping a server that is temporarily offline

$ping.send($offline).status

This won't return an error but will return a PingReply object:


This object will have the status set to TimedOut, so to catch this you have to use an If statement:

if($ping.send($offline).Status -eq 'Success'){'OK'}else{'NOK'}

Case n. 2: you try to ping a server that doesn't exist anymore (meaning no DNS record)

$ping.send($not_existing)

This will return an exception:

Exception calling "Send" with "1" argument(s): "An exception occurred during a Ping request."

that you can grab with Try{}Catch{}:

try {$ping.send($not_existing)}catch{'NOK'}

Case n. 3: you try to ping a server that is up and running

$ping.send($ok).Status

Since this case will return Success, using an If statement is the only option:

if($ping.send($ok).Status -eq 'Success'){'OK'}else{'NOK'}

At the end of the day, it's clear that in order to get all the possible errors, you have to enclose that command in both a Try{}Catch{} block and in a If{}() block:

try {
    $ping = new-object System.Net.NetworkInformation.Ping
    if ($ping.send($computer,5)) {
      'NOK'
    }
    else {
      'OK'
    }
  } catch {
    'NOK'
  }

SPEED COMPARISON

Now let's do a few measures to compare all the methods presented above and see what is actually the fastest way to ping in PowerShell when you try to reach a server that is temporarily offline:

$sb = { Test-Connection $offline -Quiet -Count 1 }
(Measure-Command $sb).TotalSeconds

$sb = { Get-WmiObject Win32_PingStatus -filter "address='$offline' and timeout=5" }
(Measure-Command $sb).TotalSeconds

$sb = { $ping = new-object System.Net.NetworkInformation.Ping,$ping.send($offline,5) }
(Measure-Command $sb).TotalSeconds

The first Test-Connection will last four seconds, while Win32_PingStatus will last 0,3 seconds and System.Net.NetworkInformation.Ping will last 0,1 seconds.

Once we know that System.Net.NetworkInformation.Ping is the faster method, let's build something that allows us to easy reuse it.

BUILDING A FILTER

My idea is just to use something old but still useful in some cases: a PowerShell Filter:

filter Invoke-FastPing {(New-Object System.Net.NetworkInformation.Ping).send($_,5)}

Here's three examples of how you can quickly take advantage of this Filter:

Example 1: ping two or more hosts and suppresses errors for unresponsive servers:
'srv1','srv2' | Invoke-FastPing 2>0

Example 2: ping sweep a whole subnet:
1..254| foreach { "192.168.1.$_"| invoke-fastping } | Where-Object status -eq 'success' | Format-Table * -AutoSize

Example3: ping al the servers in your Active Directory and present the results in a dynamic table:
(Get-ADComputer -filter {OperatingSystem -Like "*Server*"} -searchbase "OU=Servers,DC=MyCompany,DC=Com").dnshostname | Invoke-FastPing | Out-GridView

As you can see, just one line of code and you have a small tool to ping remote hosts with a small timeout. Great.

FUTURE IMPROVEMENTS

To end this post, let me tell you that there is an open issue (by J. Snover himself) on the necessity to add a timeout to Test-Connection:

https://github.com/PowerShell/PowerShell/issues/2478

Hope this is implemented soon.

Stay tuned for more PowerShell.

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...