Search code examples
powershelltimedomaincontroller

Compare time offset of all domain-controllers in PowerShell


I would like to compare all domain-controllers' time difference via PowerShell.

I found something like this:

$output1 = & w32tm /monitor /domain:mydomain.local /threads:5
    $stdOutStart = 8
    $output = $output1[$stdOutStart..$output1.Length]

I want to extract the data of $output and compare only the ntp offset time, and if it is more than one second it should send a mail alert.

the $output is not an object, its just text, how can I extract the needed fields?

$output looks like
server2.mydomain.local[192.168.22.22:123]:
    ICMP: 1ms delay
    **NTP: +0.0017247s offset** from server1.mydomain.local
        RefID: server1.mydomain.local [192.168.22.122.]
        Stratum: 3

Solution

  • I don't mean to keep this going but a few point's that I can't make in a comment.

    w32tm.exe can indeed return a negative number for example -1.001755 . That's not accommodated in the latest answer. Also, I'd use -ge & -le instead of -gt & -lt, as there's an edge case of the difference being +- exactly 1 second. I know that's probably trivial in a script like this, but it also doesn't cost anything to be thorough.

    I'd establish the $mailParams hash before the For loop and only reset the value of body in the if block.

    I'm not exactly sure why we're using the [Double]::Parse() method. I work in the US where there's less need for globalization features. However, in quick testing I don't see any difference in the outcome. Happy to have that explained further.

    The key to this was the ability to look backward through the array relative to the NTP line. How we're matching / identifying the line and splitting thereafter is a matter of preference I suppose. However, I did find a flaw in my approach, because "NTP: error" might get echoed in some cases. That's simple enough to fix without the more elaborate RegEx. I added the error lines to the sample output just to cover it in further examples.

    I think you like the more RegEx intense approach and of course using it helps us learn. As such I'm going to give both adjusted examples below below.

    You should also decide if you want to receive multiple email alerts or just 1 email with all the alert data per/run. Since we all suffer from some degree of email overload I'd imagine it's the latter, but I put a Boolean variable in there so you can decide for yourself.

    I made a comment in my original answer to polish up the $CurrentServer. @Theo did that with .TrimEnd(':'), however I think we can take that further. At first I thought this is a port number I can do something like -replace ":\d{1,5}:" that's going to chop the trailing colon 1-5 digits colon. It worked but doesn't feel right. The possibility of NTP straying from well known port 123 is extremely remote. So, instead let's chop it using a literal match like -replace ":123]:","]" Note: .Replace(":123]:", "]") would probably work just as well.

    Example Output from w32tm:

    # Sample Output, in practice set this to the output of w32tm.exe
    $W32t_Output =
    @(
    'server2.mydomain.local[192.168.22.22:123]:'
    '    ICMP: 1ms delay'
    '    NTP: +0.0017247s offset from server1.mydomain.local'
    '        RefID: server1.mydomain.local [192.168.22.122.]'
    '        Stratum: 3'
    'server2.mydomain.local[192.168.22.22:123]:'
    '    ICMP: 1ms delay'
    '    NTP: -1.0017247s offset from server1.mydomain.local'
    '        RefID: server1.mydomain.local [192.168.22.122.]'
    '        Stratum: 3'
    'server2.mydomain.local[192.168.22.22:123]:'
    '    ICMP: 1ms delay'
    '    NTP: +2.0017247s offset from server1.mydomain.local'
    '        RefID: server1.mydomain.local [192.168.22.122.]'
    '        Stratum: 3'
    'server2.mydomain.local[192.168.200.200:123]:'
    '    ICMP: 0ms delay'
    '    NTP: error ERROR_TIMEOUT - no response from server in 1000ms'
    'DownServer.mydomain.local [error WSAHOST_NOT_FOUND]'
    )
    

    Example 1 using my original approach:

    # > Set $OneAlert to true if you want 1 alert for all the interesting time variances per run.
    # > Set to false if you want a single alert for each interesting variance encountered per run.
    $OneAlert       = $true
    $Body           = [Collections.ArrayList]@()
    $AlertThreshold = 1 # Number of seconds before an alert...
    
    $mailParams = 
    @{
        To         = '[email protected]'
        From       = '[email protected]'
        SmtpServer = 'mailserver.yourdomain.com'
        Subject    = 'Alert Server Time Difference'
        Body       = ''
        Priority   = 'High'
    }
    
    For( $i = 0; $i -lt $W32t_Output.Length; ++$i )
    {
        $CurrentLine = $W32t_Output[$i].Trim()
    
        If( $CurrentLine -match '^NTP: ' -and $CurrentLine -notmatch 'error' )
        {
            $CurrentLineFields = $CurrentLine.Split(' ')                    # Break up the NTP line, as long as the output is predictable this should work.
            [Double]$OffSet    = $CurrentLineFields[1].Replace('s','')      # The second index should be the offset, clean it up to be a number...
            $DiffServer        =  $CurrentLineFields[-1]                    # Last thing on the current line should be the server is offset from.
            $CurrentServer     = $W32t_Output[$i-2] -replace ":123]:","]"   # Look back through the output array to get the current server. 
    
            # Logic for echoing and/or email alerting:
            If( $OffSet -ge $AlertThreshold -or $OffSet -le -$AlertThreshold )
            {
                If($OneAlert)
                {
                    [Void]$Body.Add( "Alert : $CurrentServer time offset $Offset seconds from $DiffServer !" )
                }
                Else
                {
                    $mailParams['Body'] = "Alert : $CurrentServer time offset $Offset seconds from $DiffServer !"
                    Send-MailMessage @mailParams
                }
            }
        }
    }
    
    # $Body will only be populated if $OneAlert is true
    If( $Body )
    {
        $mailParams['body'] = $Body -join "`r`n"
        Send-MailMessage @mailParams   
    }
    

    Example 2 further combining @Theo 's approach:

    # > Set $OneAlert to true if you want 1 alert for all the interesting time variances per run.
    # > Set to false if you want a single alert for each interesting variance encountered per run.
    $OneAlert       = $false
    $Body           = [Collections.ArrayList]@()
    $AlertThreshold = 1 # Number of seconds before an alert...
    
    $mailParams = 
    @{
        To         = '[email protected]'
        From       = '[email protected]'
        SmtpServer = 'mailserver.yourdomain.com'
        Subject    = 'Alert Server Time Difference'
        Body       = ''
        Priority   = 'High'
    }
    
    for( $i = 0; $i -lt $output.Count; $i++ ) {
        if ($output[$i] -match 'NTP:\s+([+-]?\d+(?:\.\d+)?)s') {
            $seconds = [double]::Parse($matches[1], [cultureinfo]::InvariantCulture)
    
            # Adjusted the if condition a little:
            if ( $seconds -ge $AlertThreshold -or $seconds -le -$AlertThreshold ) {
                $currentServer = $output[$i - 2] -replace ":123]:","]" # changed a little
                $refServer = ($output[$i] -split ' ')[-1]
    
                If($OneAlert) # prepare to send an email alert
                {
                    [Void]$Body.Add( "Alert: $currentServer time offset $seconds seconds from $refServer !" )
                }
                Else
                {
                    $mailParams['body'] = "Alert: $currentServer time offset $seconds seconds from $refServer !"
                    Send-MailMessage @mailParams
                }
            }
        }
    }
    
    # $Body will only be populated if $OneAlert is true
    If( $Body )
    {
        $mailParams['body'] = $Body -join "`r`n"
        Send-MailMessage @mailParams   
    }
    

    Obviously this is a little blue sky, and I probably got carried away. At any rate let me/us know what you think.