Invoke-PingLog.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# Invoke-Pinglog v1.0 (c) Chris Redit

<#PSScriptInfo
.VERSION
    1.0
.GUID
    9c508ea0-00d2-4ad6-8d3e-e8d60a3de7f3
.AUTHOR
    Chris Redit
.COPYRIGHT
    (c) 2017 Chris Redit. All rights reserved.
.TAGS
    ping monitoring
.PROJECTURI
    https://github.com/chrisred/posh-scripts
#>


<#
.SYNOPSIS
    Test and monitor network connectivity using ICMP echo packets.
.DESCRIPTION
    The Invoke-Pinglog cmdlet tests and monitors network connectivity using ICMP echo packets. Behavior is similar to the Windows ping command and essentially wraps the Win32_PingStatus WMI class.
 
    Failure and success responses can be filtered by a chosen threshold to aid monitoring of connections, and logging to a CSV format file is possible. Logs are not effected by filtering.
 
    If a domain name resolves to both an IPv4 and an IPv6 address then the IPv6 address will be preferred, this is a behaviour of the Win32_PingStatus class with no option to force either protocol.
.PARAMETER Address
    Specifies the host to send ICMP echo packets to.
.PARAMETER Count
    Specifies the number of ICMP echo packets to send, default is 0 (unlimited).
.PARAMETER Timeout
    Specifies the time to wait in seconds before no response is assumed, default is 4.
.PARAMETER BufferSize
    Specifies the number of data bytes to be sent in the ICMP echo packet, this does not include the 8 byte ICMP header or the IP header (typically 20 bytes), default is 32.
.PARAMETER TimeToLive
    Specifies the TTL (time to live) value for the IP packet which is effectively a maximum hop count, the TTL field will be decremented by each router the packet passes through, default is 128.
.PARAMETER WriteThreashold
    Specifies the number of "success" or "failure" responses to output before silencing them base on the -WriteType parameter, default is 4.
.PARAMETER WriteType
    Specifies which responses are output, "Both" with output all responses, "Failed" will output only failed responses, "Success" only successful responses, default is "Both".
.PARAMETER ResolveAddress
    Include this parameter to enable IP address to domain name resolution attempts for the address specified.
.PARAMETER NoFragmentation
    Include this parameter to set the "Do not fragment" bit in the IP header, this will trigger "Packet too big" ICMP responses if the -BufferSize parameter causes the total packet size to exceed the MTU of the first hop.
.PARAMETER DisableFormat
    Include this parameter to remove the custom formatting for the "Win32_Pingstatus" object, this will also stop formatting data for all the .NET types being reloaded when the script exits.
.PARAMETER EnableLog
    Include this parameter to enable CSV logging to a file, default is the console working directory.
.PARAMETER LogPath
    Specifies the location to save the CSV log file to, default is the console working directory.
.PARAMETER ComputerName
    Runs the cmdlet on the specified computer using remote WMI (DCOM), default is the local computer.
.OUTPUTS
    System.Management.ManagementObject#Win32_PingStatus on success.
    A non-terminating error when a W32_PingStatus object can not be created.
    A terminating error when the log file can not be written to.
.EXAMPLE
    .\Invoke-Pinglog.ps1 8.8.8.8 -ResolveAddress -Count 4
.EXAMPLE
    .\Invoke-Pinglog.ps1 google.com -WriteType Failed -EnableLog -LogPath "C:\Users\Administrator\Documents"
#>

Param(
    [Parameter(Position=0,Mandatory=$true)]
    [String]$Address,
    [Int]$Count = 0,
    [Int]$Timeout = 4,
    [Int]$BufferSize = 32,
    [Int]$TimeToLive = 128,
    [Int]$WriteThreashold = 4,
    [ValidateSet('Both','Failed','Success')]
    [String]$WriteType = 'Both',
    [Switch]$ResolveAddress,
    [Switch]$NoFragmentation,
    [Switch]$DisableFormat,
    [Switch]$EnableLog,
    [String]$LogPath = '.',
    [String]$ComputerName = $env:COMPUTERNAME
)

$PingStatusFormat = @"
<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
    <ViewDefinitions>
        <View>
            <Name>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</Name>
            <ViewSelectedBy>
                <TypeName>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</TypeName>
            </ViewSelectedBy>
            <TableControl>
                <TableHeaders>
                    <TableColumnHeader>
                        <Width>20</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <!--<AutoSize/>-->
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>10</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>12</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>ResponseTTL</Label>
                        <Width>12</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>30</Width>
                    </TableColumnHeader>
                </TableHeaders>
                <TableRowEntries>
                    <TableRowEntry>
                        <TableColumnItems>
                            <TableColumnItem>
                                <PropertyName>Timestamp</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>ProtocolAddress</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>ReplySize</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>ResponseTime</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>ResponseTimeToLive</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Status</PropertyName>
                            </TableColumnItem>
                        </TableColumnItems>
                    </TableRowEntry>
                </TableRowEntries>
            </TableControl>
        </View>
    </ViewDefinitions>
</Configuration>
"@


$StatusCodeText = @{
    0 = 'Success'
    11001 = 'Buffer Too Small'
    11002 = 'Destination Net Unreachable'
    11003 = 'Destination Host Unreachable'
    11004 = 'Destination Protocol Unreachable'
    11005 = 'Destination Port Unreachable'
    11006 = 'No Resources'
    11007 = 'Bad Option'
    11008 = 'Hardware Error'
    11009 = 'Packet Too Big'
    11010 = 'Request Timed Out'
    11011 = 'Bad Request'
    11012 = 'Bad Route'
    11013 = 'TimeToLive Expired Transit'
    11014 = 'TimeToLive Expired Reassembly'
    11015 = 'Parameter Problem'
    11016 = 'Source Quench'
    11017 = 'Option Too Big'
    11018 = 'Bad Destination'
    11032 = 'Negotiating IPSEC'
    11050 = 'General Failure'
}

Function Write-Log ($Value)
{
    if ($EnableLog)
    {
        Out-File -InputObject $Value -FilePath (Join-Path -Path $LogPath -ChildPath "pinglog-$($ScriptTime).csv") -Append
    }
}

$ScriptTime = Get-Date -Format yyMMddHHmm
$TimeoutMilliseconds = $Timeout * 1000
$SuccessTotal = 0
$SuccessCurrent = 0
$FailedTotal = 0
$FailedCurrent = 0
$ResponseMaximum = $null
$ResponseMinimum = $null
$ResponseSum = 0

try
{
    if (-not $DisableFormat)
    {
        # Create a temporary format data file, and load in to the current session, this works from PSv2 onwards. Using a
        # custom object and "DefaultDisplayPropertySet" which would be an alternative does not work for PSv2 for example.
        $FormatPath = "$($env:Temp)\pinglog.format.ps1xml"
        Set-Content -Value $PingStatusFormat -Path $FormatPath -Encoding 'UTF8' -Force
        # PSv2 raises a non-terminating error about the format data file being "already present"
        Update-FormatData -PrependPath $FormatPath -EA SilentlyContinue
    }

    for ($i=1; $i -le $Count -or $Count -eq 0; $i++)
    {
        if ($ResolveAddress) {$ResolveAddressString = " AND ResolveAddressNames='TRUE'"} else {$ResolveAddressString = ''}
        if ($NoFragmentation) {$NoFragmentationString = " AND NoFragmentation='TRUE'"} else {$NoFragmentationString = ''}
        $PingStatusQuery = "SELECT * FROM Win32_PingStatus WHERE Address='{0}' AND Timeout={1} AND BufferSize={2} AND TimeToLive={3}{4}{5}"
        $PingStatus = Get-WmiObject -ComputerName $ComputerName -Query ($PingStatusQuery -f $Address, $TimeoutMilliseconds, $BufferSize, $TimeToLive, $ResolveAddressString, $NoFragmentationString) -EA Continue

        if ($PingStatus -ne $null)
        {
            if ($PingStatus.PrimaryAddressResolutionStatus -gt 0)
            {
                throw "Invalid IP address or domain name."
            }

            # add TimeStamp and Status properites to the $PingStatus object
            $StatusCode = [Int]$PingStatus.StatusCode
            Add-Member -InputObject $PingStatus -Name 'TimeStamp' -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -MemberType NoteProperty
            Add-Member -InputObject $PingStatus -Name 'Status' -Value $StatusCodeText[$StatusCode] -MemberType NoteProperty

            # calculate the success/failed min/max/avg totals for display on termination
            if ($StatusCode -lt 1)
            {
                $SuccessTotal++
                $SuccessCurrent++
                $FailedCurrent = 0
                $ResponseSum += $PingStatus.ResponseTime

                if ($i -gt 1)
                {
                    # run the calculations for all except the first iteration
                    if($PingStatus.ResponseTime -gt $ResponseMaximum -or $ResponseMaximum -eq $null) {$ResponseMaximum = $PingStatus.ResponseTime}
                    if($PingStatus.ResponseTime -lt $ResponseMinimum -or $ResponseMinimum -eq $null) {$ResponseMinimum = $PingStatus.ResponseTime}
                    $ResponseAverage = $ResponseSum / $SuccessTotal
                }
                else
                {
                    $ResponseMaximum = $PingStatus.ResponseTime
                    $ResponseMinimum = $PingStatus.ResponseTime
                    $ResponseAverage = $PingStatus.ResponseTime
                }
            }
            else
            {
                $FailedTotal++
                $FailedCurrent++
                $SuccessCurrent = 0
            }

            if ($WriteType -eq 'Both')
            {
                Write-Output $PingStatus
            }
            elseif ($WriteType -eq 'Failed' -and $WriteThreashold -ge $SuccessCurrent)
            {
                Write-Output $PingStatus
                if ($WriteThreashold -eq $SuccessCurrent)
                {
                    Write-Warning "Silencing success messages after $WriteThreashold replies."
                }
            }
            elseif ($WriteType -eq 'Success' -and $WriteThreashold -ge $FailedCurrent)
            {
                Write-Output $PingStatus
                if ($WriteThreashold -eq $FailedCurrent)
                {
                    Write-Warning "Silencing failed messages after $WriteThreashold failures."
                }
            }

            # log to a file in CSV format when logging is enabled
            if ($i -gt 1)
            {
                # skip the header for all except the first iteration
                Write-Log ($PingStatus | select TimeStamp,Address,ProtocolAddress,ReplySize,ResponseTime,ResponseTimeToLive,Status | ConvertTo-Csv -NoTypeInformation | select -Last 1)
            }
            else
            {
                Write-Log ($PingStatus | select TimeStamp,Address,ProtocolAddress,ReplySize,ResponseTime,ResponseTimeToLive,Status | ConvertTo-Csv -NoTypeInformation)
            }

            if ($StatusCode -ne 11010 -or $StatusCode -ne 11003)
            {
                # delay next iteration for 1 second unless the request timed out or host unreachable, these get the timeout applied
                Start-Sleep -Seconds 1
            }
        }
        else
        {
            # hardware/driver error or use of reserved address space if $PingStatus is $null
            Write-Log $Error[0].ToString()
            Start-Sleep -Seconds 1
        }
    }
}
catch
{
    throw
}
finally
{
    if (-not $DisableFormat)
    {
        # reload the built-in format data file to revert the temp file loaded previously
        Update-FormatData -PrependPath "$PSHOME\DotNetTypes.format.ps1xml" -EA SilentlyContinue
    }

    $PacketTotal = $SuccessTotal + $FailedTotal
    if ($PacketTotal -gt 0)
    {
        # display ping-like stats on termination, must have at least one response
        $FailedPercentage = "{0:P0}" -f ($FailedTotal / $PacketTotal)
        if ($PingStatus.ProtocolAddress -eq '') {$AddressText = $Address} else {$AddressText = $PingStatus.ProtocolAddress}
        if ($ResolveAddress) {$ResolveAddressText = " [$($PingStatus.ProtocolAddressResolved)]"} else {$ResolveAddressText = ''}
        Write-Host ""
        Write-Host "Ping statistics for $($AddressText)$($ResolveAddressText):"
        Write-Host " Packets: Sent = $PacketTotal, Received = $SuccessTotal, Lost = $FailedTotal ($FailedPercentage loss)"
        
        if ($SuccessTotal -gt 0)
        {
            $ResponseAverageFormat = "{0:D1}" -f [Int]$ResponseAverage
            Write-Host "Approximate round trip time in milli-seconds:"
            Write-Host " Minimum = $($ResponseMinimum)ms, Maximum = $($ResponseMaximum)ms, Average = $($ResponseAverageFormat)ms"
            Write-Host ""
        }
    }
}