Public/ntp/Get-NTPPeer.ps1
|
#Requires -Version 5.1 function Get-NTPPeer { <# .SYNOPSIS Retrieves NTP peer information from the Windows Time service .DESCRIPTION Parses the output of 'w32tm /query /peers' to return structured NTP peer objects. Supports both modern and legacy w32tm output formats, including French-locale output. Uses block-based parsing: raw output is split on blank lines, the header block is skipped, and each subsequent block is parsed as one peer entry. .PARAMETER ComputerName One or more computer names to query. Defaults to the local machine. .EXAMPLE Get-NTPPeer Returns NTP peer information for the local computer. .EXAMPLE Get-NTPPeer -ComputerName 'SRV01', 'SRV02' Returns NTP peer information for two remote servers. .EXAMPLE 'SRV01', 'SRV02' | Get-NTPPeer Pipeline usage: queries NTP peers on both servers. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-03-12 Requires: PowerShell 5.1+, Windows Time service (w32time) Permissions: Local user for local queries; remote admin for Invoke-Command remoting #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string[]]$ComputerName = $env:COMPUTERNAME ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting" $w32tmScriptBlock = { $w32tmPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\w32tm.exe' if (-not (Test-Path -Path $w32tmPath)) { throw "w32tm.exe not found at '$w32tmPath'" } $peerOutput = & $w32tmPath /query /peers 2>&1 if ($LASTEXITCODE -ne 0) { throw "w32tm /query /peers failed (exit code $LASTEXITCODE): $($peerOutput -join ' ')" } $peerOutput } } process { foreach ($targetComputer in $ComputerName) { Write-Verbose "[$($MyInvocation.MyCommand)] Querying '$targetComputer'" try { $isLocal = ($targetComputer -eq $env:COMPUTERNAME) -or ($targetComputer -eq 'localhost') -or ($targetComputer -eq '.') if ($isLocal) { $rawOutput = & $w32tmScriptBlock } else { $rawOutput = Invoke-Command -ComputerName $targetComputer ` -ScriptBlock $w32tmScriptBlock -ErrorAction Stop } # Convert to string array and normalize $lines = @($rawOutput | ForEach-Object { "$_" }) # Split into blocks on blank lines $blocks = [System.Collections.Generic.List[System.Collections.Generic.List[string]]]::new() $currentBlock = [System.Collections.Generic.List[string]]::new() foreach ($line in $lines) { if ([string]::IsNullOrWhiteSpace($line)) { if ($currentBlock.Count -gt 0) { $blocks.Add($currentBlock) $currentBlock = [System.Collections.Generic.List[string]]::new() } } else { $currentBlock.Add($line.Trim()) } } if ($currentBlock.Count -gt 0) { $blocks.Add($currentBlock) } # First block is the header (#Peers: N) -- skip it if ($blocks.Count -le 1) { Write-Warning "[$($MyInvocation.MyCommand)] No NTP peers found on '$targetComputer'" continue } $peerBlocks = $blocks.GetRange(1, $blocks.Count - 1) foreach ($peerBlock in $peerBlocks) { # First line is the Peer line $peerLine = $peerBlock[0] $peerName = $null $peerFlags = $null if ($peerLine -match '^Peer:\s*(.+)$') { $peerValue = $Matches[1].Trim() if ($peerValue -match '^(.+?),\s*(.+)$') { $peerName = $Matches[1].Trim() $peerFlags = $Matches[2].Trim() } else { $peerName = $peerValue } } # Parse remaining lines as key:value $peerState = $null $timeRemaining = [double]0 $peerMode = $null $peerStratum = $null $peerPollInterval = $null $hostPollInterval = $null $lastSyncTime = $null $pollInterval = $null for ($i = 1; $i -lt $peerBlock.Count; $i++) { $kvLine = $peerBlock[$i] $label = '' $kvValue = '' if ($kvLine -match '^([^:]+):\s*(.*)$') { $label = $Matches[1].Trim() $kvValue = $Matches[2].Trim() } # State if ($label -match 'State|tat') { $peerState = $kvValue } # Time Remaining / Temps restant elseif ($label -match 'Time Remaining|restant') { if ($kvValue -match '([\d,\.]+)\s*s') { $numStr = $Matches[1] -replace ',', '.' $timeRemaining = [double]$numStr } } # Mode elseif ($label -match '^Mode') { $peerMode = $kvValue } # Stratum elseif ($label -match 'Strat') { $peerStratum = $kvValue } # PeerPoll Interval elseif ($label -match 'PeerPoll') { if ($kvValue -match '(\d+)') { $peerPollInterval = [int]$Matches[1] } } # HostPoll Interval elseif ($label -match 'HostPoll') { if ($kvValue -match '(\d+)') { $hostPollInterval = [int]$Matches[1] } } # Poll Interval (old format, but NOT PeerPoll/HostPoll) elseif (($label -match 'Poll') -and ($label -notmatch 'PeerPoll|HostPoll')) { if ($kvValue -match '(\d+)') { $pollInterval = [int]$Matches[1] } } # Date pattern for LastSyncTime (anywhere on the line) if ($kvLine -match '\d{1,2}[/.\-]\d{1,2}[/.\-]\d{2,4}') { # Extract the value portion after the colon if ($kvValue -and ($kvValue -as [datetime])) { $lastSyncTime = [datetime]$kvValue } } } [PSCustomObject]@{ PSTypeName = 'PSWinOps.NTPPeer' ComputerName = $targetComputer PeerName = $peerName PeerFlags = $peerFlags State = $peerState TimeRemaining = $timeRemaining Mode = $peerMode Stratum = $peerStratum PeerPollInterval = $peerPollInterval HostPollInterval = $hostPollInterval LastSyncTime = $lastSyncTime PollInterval = $pollInterval Timestamp = Get-Date -Format 'o' } } } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed to query '$targetComputer': $_" continue } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed" } } |