Public/ntp/Get-NTPConfiguration.ps1
|
#Requires -Version 5.1 function Get-NTPConfiguration { <# .SYNOPSIS Retrieves the current Windows Time Service (W32Time) NTP configuration and status .DESCRIPTION This function queries the Windows Time Service using w32tm commands to retrieve the complete NTP configuration, including configured servers, poll intervals, synchronization status, peer details, and last successful sync time. Supports both local and remote computers. Remote queries use WinRM (Invoke-Command). For bulk queries, errors per computer are non-terminating to allow the pipeline to continue processing remaining computers. Returns a structured PSCustomObject with all relevant NTP configuration data for easy consumption by other scripts or for display purposes. .PARAMETER ComputerName One or more computer names to query. Accepts pipeline input. Defaults to the local computer ($env:COMPUTERNAME). .PARAMETER IncludePeerDetails When specified, includes detailed peer information in the output object. This adds verbose information about each configured NTP peer. .EXAMPLE Get-NTPConfiguration Retrieves the current NTP configuration for the local computer. .EXAMPLE Get-NTPConfiguration -ComputerName 'SRV01', 'SRV02' Retrieves NTP configuration for two remote servers. .EXAMPLE 'SRV01', 'SRV02' | Get-NTPConfiguration Pipeline usage: queries NTP configuration on both servers. .EXAMPLE Get-NTPConfiguration -Verbose | Format-List Retrieves NTP configuration with verbose logging and displays all properties as a list. .EXAMPLE $ntpConfig = Get-NTPConfiguration -IncludePeerDetails $ntpConfig.ConfiguredServers $ntpConfig.PeerDetails Retrieves configuration with peer details and accesses specific properties. .OUTPUTS PSWinOps.NtpConfiguration NTP client configuration including source, type, and poll intervals. .NOTES Author: Franck SALLET Version: 2.0.0 Last Modified: 2026-03-19 Requires: PowerShell 5.1+, Windows Time Service (w32time) Permissions: Standard user for local queries; WinRM + admin rights for remote .LINK https://docs.microsoft.com/en-us/windows-server/networking/windows-time-service/windows-time-service-tools-and-settings #> [CmdletBinding()] [OutputType('PSWinOps.NtpConfiguration')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [switch]$IncludePeerDetails ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting - PowerShell $($PSVersionTable.PSVersion)" # Script block used for REMOTE execution only (Invoke-Command). # Uses full path to w32tm.exe because remote sessions don't inherit local mock context. $w32tmRemoteScriptBlock = { $w32tmPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\w32tm.exe' if (-not (Test-Path -Path $w32tmPath)) { throw "w32tm.exe not found at '$w32tmPath'" } @{ ServiceStatus = (Get-Service -Name 'w32time' -ErrorAction Stop).Status Config = & $w32tmPath /query /configuration 2>&1 Status = & $w32tmPath /query /status /verbose 2>&1 Peers = & $w32tmPath /query /peers 2>&1 } } } process { foreach ($computer in $ComputerName) { Write-Verbose "[$($MyInvocation.MyCommand)] Querying '$computer'" try { $isLocal = ($computer -eq $env:COMPUTERNAME) -or ($computer -eq 'localhost') -or ($computer -eq '.') if ($isLocal) { # Local execution: call commands by name so they are mockable in Pester $rawData = @{ ServiceStatus = (Get-Service -Name 'w32time' -ErrorAction Stop).Status Config = w32tm /query /configuration 2>&1 Status = w32tm /query /status /verbose 2>&1 Peers = w32tm /query /peers 2>&1 } } else { $rawData = Invoke-Command -ComputerName $computer ` -ScriptBlock $w32tmRemoteScriptBlock -ErrorAction Stop } $configOutput = $rawData.Config $statusOutput = $rawData.Status $peersOutput = $rawData.Peers # Parse configuration $ntpServerLine = $configOutput | Select-String -Pattern 'NtpServer:\s*(.+)\s*\(.*\)' | Select-Object -First 1 $configuredServers = if ($ntpServerLine) { ($ntpServerLine.Matches.Groups[1].Value -split '\s+') | Where-Object { $_ -ne '' } } else { @() } $typeMatch = $configOutput | Select-String -Pattern 'Type:\s*(.+)' | Select-Object -First 1 $syncType = if ($typeMatch) { $typeMatch.Matches.Groups[1].Value.Trim() } else { 'Unknown' } $specialPollMatch = $configOutput | Select-String -Pattern 'SpecialPollInterval:\s*(\d+)' | Select-Object -First 1 $specialPollInterval = if ($specialPollMatch) { [int]$specialPollMatch.Matches.Groups[1].Value } else { $null } $minPollMatch = $configOutput | Select-String -Pattern 'MinPollInterval:\s*(\d+)' | Select-Object -First 1 $minPollInterval = if ($minPollMatch) { [int]$minPollMatch.Matches.Groups[1].Value } else { $null } $maxPollMatch = $configOutput | Select-String -Pattern 'MaxPollInterval:\s*(\d+)' | Select-Object -First 1 $maxPollInterval = if ($maxPollMatch) { [int]$maxPollMatch.Matches.Groups[1].Value } else { $null } # Parse status $sourceMatch = $statusOutput | Select-String -Pattern 'Source:\s*(.+)' | Select-Object -First 1 $currentSource = if ($sourceMatch) { $sourceMatch.Matches.Groups[1].Value.Trim() } else { 'Unknown' } $lastSyncMatch = $statusOutput | Select-String -Pattern 'Last Successful Sync Time:\s*(.+)' | Select-Object -First 1 $lastSyncTime = if ($lastSyncMatch) { $lastSyncMatch.Matches.Groups[1].Value.Trim() } else { 'Never' } $stratumMatch = $statusOutput | Select-String -Pattern 'Stratum:\s*(\d+)' | Select-Object -First 1 $stratum = if ($stratumMatch) { [int]$stratumMatch.Matches.Groups[1].Value } else { $null } $leapMatch = $statusOutput | Select-String -Pattern 'Leap Indicator:\s*(.+)' | Select-Object -First 1 $leapIndicator = if ($leapMatch) { $leapMatch.Matches.Groups[1].Value.Trim() } else { 'Unknown' } # Build result object $result = [PSCustomObject]@{ PSTypeName = 'PSWinOps.NtpConfiguration' ComputerName = $computer ServiceName = 'w32time' ServiceStatus = $rawData.ServiceStatus SyncType = $syncType ConfiguredServers = $configuredServers CurrentSource = $currentSource LastSuccessfulSync = $lastSyncTime Stratum = $stratum LeapIndicator = $leapIndicator SpecialPollInterval = $specialPollInterval MinPollInterval = $minPollInterval MaxPollInterval = $maxPollInterval MinPollIntervalSec = if ($minPollInterval) { [math]::Pow(2, $minPollInterval) } else { $null } MaxPollIntervalSec = if ($maxPollInterval) { [math]::Pow(2, $maxPollInterval) } else { $null } Timestamp = Get-Date -Format 'o' } # Add peer details if requested if ($IncludePeerDetails) { Write-Verbose "[$($MyInvocation.MyCommand)] Including peer details for '$computer'" $result | Add-Member -MemberType NoteProperty -Name 'PeerDetails' -Value ($peersOutput -join "`n") } Write-Verbose "[$($MyInvocation.MyCommand)] Configuration retrieved successfully for '$computer'" $result } catch [Microsoft.PowerShell.Commands.ServiceCommandException] { Write-Error "[$($MyInvocation.MyCommand)] Windows Time Service not found on '$computer': $_" continue } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed to retrieve NTP configuration from '$computer': $_" continue } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed" } } |