Track IP History and Alert on Change

I wanted to know how often dynamic IP addresses change, and be alerted when they do. In fact, some colleagues have reported that even “static” IPs are sometimes changed without warning, so this script would address that as well.

The script saves two files, an .xml file with the latest public IP address, and a .log file listing the history of IP addresses. Specify as the first parameter where you want those files stored. If you don’t specify a log file path, the files will be stored in the same folder where the script is located. (Note the use of the %IT_Scripts% environment variable in the screen shot below—see this post for details.)

By default, the script uses whatismyip.akamai.com to retrieve the machine’s public IP address. You can change this with the second and third parameters. See the parameter notes in the script.

The script returns 0 if the IP address has not changed and 1001 if it has. Deploy this as a 24×7 Check with with Max Remote Management (MaxRM). Depending on your settings, that will run the script every 5-60 minutes. Each execution takes less than a second. If you want to receive an email when the IP changes, set the Alert Settings to email you on an “Outage”.  Note that the first time the script is run, it will report an IP change because it doesn’t have any history to compare to.

Track IP History 1

Script output is available in the MaxRM dashboard:

Track IP History 2

Cut and paste the script below and save it e.g. as “MCB.TrackPublicIPHistory.ps1”.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
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
<#
.Synopsis
  Keep a history of the computer's public IP in a text file.
  Return "error" code 1001 when the IP changes so we can optionally
  set up monitoring software to send an email notification.
 
  Copyright (c) 2016 by MCB Systems. All rights reserved.
  Free for personal or commercial use.  May not be sold.
  No warranties.  Use at your own risk.
.Notes
  Name:       MCB.TrackPublicIPHistory.ps1
  Author:     Mark Berry, MCB Systems
  Created:    03/31/2016
  Last Edit:  03/31/2016
.Changes
  03/31/2016  Initial version.
 
.Description
  Retrieve the public IP address.  If it has changed, return 1001 and record the
  change to a text file.
.Parameter ScriptLogFilesPath
  Where to store the status and log files for this script.
  If not specified, files go in the same directory as the script.
  Default:  "".
.Parameter PublicIPSource
  The HTTP address from which to retrieve an external (WAN) IP address.
  Default:  http://whatismyip.akamai.com/
.Parameter PublicIPXMLXPath
  The optional XPATH to use for extracting the text block containing the
  IP address from the text returned by PublicIPSource.  For example, if the
  IP is in the Body of an HTML page, use "/html/body".  http://whatismyip.akamai.com/
  does not return any HTML with the IP, so the default is empty.
  Default: ""
  Note:  AFTER extracting this XML element, a regular expression will extract just the
         IP address, so it's okay if there is some text before and/or after the IP.
.Parameter LogFile
  Path to a log file. Required by MaxRM script player.  Not used here.
  Default:  "".
.Example
  MCB.TrackPublicIPHistory `
    -ScriptLogFilesPath ($Env:IT_Scripts + 'LogFiles\') `
    -PublicIPSource "http://whatismyip.akamai.com/" `
    -PublicIPXMLXPath "" `
#>
 
################################################################################
# STEP 1:  Get command line arguments.
################################################################################
# Must be the first statement in the script
param(
    [Parameter(Mandatory = $false,
                    Position = 0,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$ScriptLogFilesPath="",
 
    [Parameter(Mandatory = $false,
                    Position = 1,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$PublicIPSource="http://whatismyip.akamai.com/",
 
    [Parameter(Mandatory = $false,
                    Position = 2,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$PublicIPXMLXPath="",
 
    [Parameter(Mandatory = $false,
                    Position = 3,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$LogFile=""
)
 
################################################################################
# STEP 2:  Script setup
################################################################################
 
# Set up and start stopwatch so we can print out how long it takes to run script
# http://stackoverflow.com/questions/3513650/timing-a-commands-execution-in-powershell
$StopWatch = [Diagnostics.Stopwatch]::StartNew()
 
# In case this script was retrieved from Path, get its directory
$executingScriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
# Also get its name, without extension--used to name the .xml and .log files
$executingScriptBaseName = (Get-Item $MyInvocation.MyCommand.Name).BaseName
# Override $executingScriptBaseName with a fixed name--comment out to use script's name
$executingScriptBaseName = "MCB.TrackPublicIPHistory"
 
if ($ScriptLogFilesPath -eq "") {
  # if $ScriptLogFilesPath parameter not specified
   $ScriptLogFilesPath = $executingScriptDirectory
}
elseif ( !(Test-Path $ScriptLogFilesPath) ) {
  # $ScriptLogFilesPath parameter was specified but folder doesn't exist
   $ScriptLogFilesPath = $executingScriptDirectory
}
# Use Join-Path to get full paths--adds "\" if necessary
$StatusFileFullPath = Join-Path -Path $ScriptLogFilesPath -ChildPath "$executingScriptBaseName.xml"
$LogFileFullPath = Join-Path -Path $ScriptLogFilesPath -ChildPath "$executingScriptBaseName.log"
 
# Look for $StatusFileFullPath file as determined above.
# If the file is found, load the values into a hash.  If not found, initialize an empty hash.
if (Test-Path ($StatusFileFullPath)) {
  # Retrieve the hash table containing the status values
  $IPChangeStatusHash = Import-Clixml ($StatusFileFullPath)
}
else {
  # Status file doesn't exist yet. Create an empty hash.  Assume IP not registered.
  $IPChangeStatusHash = @{"CurrentIPAddress"="";
                          "CurrentIPAddressChangedAt"=""}
}
 
$MainErrorFound = $false
 
# Concatenate output to $Output so we can prepend a one-line $Status
$Output = ""
 
################################################################################
# STEP 3:  Get public IP
################################################################################
 
$Output += "`n======================================================"
$Output += "`nRetrieve public IP from a ""What is my IP?"" service"
$Output += "`n======================================================"
 
function GetAndDisplayIP([String]$PublicIPSource="", [String]$PublicIPXMLXPath="") {
  # initialize the three objects we will return
  $FuncErrorFound = $false
  $PublicIPContent = ""
  $PublicIP = ""
 
  try {
    $PublicIPContent = (New-Object System.Net.WebClient).DownloadString($PublicIPSource)
  }
  catch {
    $Output += "`nFailed to download content from """ + $PublicIPSource + """. System message:"
    $Output += "`n   " + $_.Exception.Message.ToString()
    $FuncErrorFound = $true
  }
   
  # Extract string using XPath:  see Karl Prosser's 11/16/2007 post here:
  # http://www.techtalkz.com/microsoft-windows-powershell/172318-xmldocument-selectsinglenode-doesnt-work.html
  if ($PublicIPXMLXPath -ne "") {
    try {
      # Treat $PublicIPContent as XML - use XPath to extract string
      [xml]$PublicIPXML = $PublicIPContent
      $PublicIPContent = $PublicIPXML.SelectSingleNode($PublicIPXMLXPath).InnerText
    }
    catch {
      $Output += "`nFailed to extract data from """ + $PublicIPSource + `
        """ using XPath """ + $PublicIPXMLXPath + """. System message:"
      $Output += "`n   " + $_.Exception.Message.ToString()
      $FuncErrorFound = $true
    }
  }
   
  # Extract the IP only using a regular expression from this post:
  # http://stackoverflow.com/questions/2890896/extract-ip-address-from-an-html-string-python
  $RegEx = '[0-9]+(?:\.[0-9]+){3}'
  if ($PublicIPContent -match $RegEx) {
    $PublicIP = $matches[0] # $matches array automatically populated by -match
  }
  else {
    $PublicIP = ""
    $PublicIPContent = 'No IP address found in content "' + $PublicIPContent + '".'
  }
 
  # Always return three objects
  $FuncErrorFound
  $PublicIPContent
  $PublicIP
} # end of GetAndDisplayIP function
 
if ($PublicIPSource -ne "") {
  $ErrorFound,$PublicIPContent,$PublicIP  = GetAndDisplayIP $PublicIPSource $PublicIPXMLXPath
  $Output += "`n" + $PublicIPSource + ':  ' + $PublicIPContent # output results
  if ($ErrorFound) {
    $MainErrorFound = $true
  }
}
 
################################################################################
# STEP 4:  Check for and record IP change
################################################################################
 
$OldCurrentIPAddress = $IPChangeStatusHash.get_Item("CurrentIPAddress") # the _old_ CurrentIPAddress
 
if ($PublicIP -eq "") {
 
  $IPComparisonResult = "No public IP found, so IP change not checked"
  $Output += "`n" + $IPComparisonResult
}
 
elseif ($PublicIP -eq $OldCurrentIPAddress) { # compare to the _old_ CurrentIPAddress
 
  $IPComparisonResult = "Current public IP $PublicIP matches previous IP, so no change recorded"
  $Output += "`n" + $IPComparisonResult
 
} else {
 
  # new public IP found
  $IPComparisonResult = "Current IP $PublicIP does not match previous IP $OldCurrentIPAddress"
  $Output += "`n" + $IPComparisonResult
   
  # Call this an "error" to return 1001 so monitoring software can send email alert on IP change
  $MainErrorFound = $true
   
  $Output += "`n"
  $Output += "`n======================================================"
  $Output += "`nRecord the IP address change"
  $Output += "`n======================================================"
  $LineToAppend = (get-date -format "yyyy/MM/dd HH:mm:ss") + " - " + $PublicIP
   
  # Append one line to text file
  Add-Content -Path $LogFileFullPath -Value ($LineToAppend)
     
  $Output += "`nAdded this line to $LogFileFullPath :"
  $Output += "`n" + $LineToAppend
  
  # Update the Current IP and time to the status hash (to be written out at end of script)
  $IPChangeStatusHash.set_Item("CurrentIPAddress", $PublicIP)
  $IPChangeStatusHash.set_Item("CurrentIPAddressChangedAt", (Get-Date -Format "G")) # short date + long time with AM/PM
    
} # new public IP found
 
################################################################################
# STEP 5:  Output the IP history from the .log file
################################################################################
 
$Output += "`n"
$Output += "`n======================================================"
$Output += "`nIP history from $LogFileFullPath"
$Output += "`n======================================================"
$Output += "`n"
# Get-Content returns a string array.  Pipe to Out-String to convert to string and preserve line breaks.
$Output += (Get-Content -Path $LogFileFullPath | Out-String)
 
################################################################################
# STEP 6:  Wrap up
################################################################################
 
# Save the hash table to a file in $ScriptLogFilesPath as determined above
$IPChangeStatusHash | Export-Clixml ($StatusFileFullPath)
 
$Status = $IPComparisonResult
 
#Prepare SummaryLine for display in monitoring system. Abbreviate date/time.
$SummaryLine = $Status + " [" + (Get-Date -Format "MM/dd HH:mm") + "]"
 
$SummaryLine
$Output
 
"======================================================"
"Local Machine Time:  " + (Get-Date -Format G)
 
# Stop the stopwatch and calculate the elapsed time in seconds, rounded to 0.1 seconds
$StopWatch.Stop()
$ElapsedSecondsRounded = [Math]::Round($StopWatch.Elapsed.TotalSeconds, 1)
$ElapsedString = [string]::Format("{0:0.0} second(s)", $ElapsedSecondsRounded)
'Script execution took ' + $ElapsedString + '.'
 
if ($MainErrorFound) {
  $ExitCode = 1001
}
else {
  $ExitCode = 0
}
'Exit Code: ' + $ExitCode
 
Exit $ExitCode

Leave a Reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail. You can also subscribe without commenting.