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”.

<#
.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.