Public/ntp/Sync-NTPTime.ps1
|
#Requires -Version 5.1 function Sync-NTPTime { <# .SYNOPSIS Forces NTP time resynchronization on Windows machines .DESCRIPTION Forces a resynchronization of the Windows Time Service (w32tm) on one or more local or remote machines. Optionally restarts the Windows Time service before resyncing to ensure a clean state. Uses the w32tm exit code (locale-agnostic) to determine success or failure per machine. Uses direct w32tm calls for local execution (mockable in Pester) and Invoke-Command for remote targets, matching the pattern used by all NTP functions. Each machine is processed independently with per-machine error isolation -- a failure on one target does not prevent processing of subsequent targets. Supports -WhatIf and -Confirm via SupportsShouldProcess. Since ConfirmImpact is Medium and the default ConfirmPreference is High, the function does not prompt by default. Pass -Confirm to force a prompt, or -Confirm:$false to suppress it explicitly in automation contexts. .PARAMETER ComputerName One or more computer names to resynchronize. Accepts pipeline input. Defaults to the local machine ($env:COMPUTERNAME). Each value must be a non-empty string. .PARAMETER RestartService When specified, restarts the Windows Time service (w32time) on each target machine before running w32tm /resync. This can help recover from a stale service state. The restart action also goes through ShouldProcess confirmation. .EXAMPLE Sync-NTPTime Forces an NTP resync on the local machine using default parameters. .EXAMPLE Sync-NTPTime -ComputerName 'SRV-DC01' Forces an NTP resync on a single remote machine. .EXAMPLE Sync-NTPTime -ComputerName 'SRV-DC01', 'SRV-DC02' -RestartService -Verbose Restarts the w32time service then forces an NTP resync on two remote machines, with verbose logging enabled. .EXAMPLE Get-Content -Path 'C:\Admin\servers.txt' | Sync-NTPTime -RestartService Reads a list of server names from a file and pipelines them into Sync-NTPTime, restarting the w32time service on each before resyncing. .OUTPUTS PSWinOps.NtpResyncResult Resynchronization result with status and any error details. .NOTES Author: Franck SALLET Version: 1.1.0 Last Modified: 2026-03-20 Requires: PowerShell 5.1+ / Windows only Permissions: Requires admin rights (local and remote) to restart services and run w32tm /resync. Remote targets require PSRemoting enabled. .LINK https://docs.microsoft.com/en-us/windows-server/networking/windows-time-service/windows-time-service-tools-and-settings #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [OutputType('PSWinOps.NtpResyncResult')] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [switch]$RestartService ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting - PowerShell $($PSVersionTable.PSVersion)" if (-not (Test-IsAdministrator)) { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.UnauthorizedAccessException]::new('This operation requires Administrator privileges.'), 'ElevationRequired', [System.Management.Automation.ErrorCategory]::PermissionDenied, $null ) ) } # Script block used for REMOTE execution only (Invoke-Command). # Uses full path because remote sessions don't inherit local mock context. $resyncScriptBlock = { $w32tmPath = Join-Path -Path $env:SystemRoot -ChildPath 'System32\w32tm.exe' if (-not (Test-Path -Path $w32tmPath)) { throw "[ERROR] w32tm.exe not found at '$w32tmPath'" } $rawOutput = & w32tm.exe /resync 2>&1 [PSCustomObject]@{ Output = ($rawOutput | Out-String).Trim() ExitCode = $LASTEXITCODE } } # Script block used for REMOTE execution only (Invoke-Command). $restartScriptBlock = { Restart-Service -Name 'w32time' -Force -ErrorAction Stop Start-Sleep -Seconds 2 } } process { foreach ($targetComputer in $ComputerName) { try { Write-Verbose "[$($MyInvocation.MyCommand)] Processing: $targetComputer" $isLocal = ($targetComputer -eq $env:COMPUTERNAME) $serviceRestarted = $false # Optionally restart w32time service before resync if ($RestartService) { if ($PSCmdlet.ShouldProcess($targetComputer, 'Restart Windows Time service (w32time)')) { Write-Verbose "[$($MyInvocation.MyCommand)] Restarting w32time on '$targetComputer'..." if ($isLocal) { Restart-Service -Name 'w32time' -Force -ErrorAction Stop Start-Sleep -Seconds 2 } else { $null = Invoke-Command -ComputerName $targetComputer -ScriptBlock $restartScriptBlock -ErrorAction Stop } $serviceRestarted = $true Write-Verbose "[$($MyInvocation.MyCommand)] w32time restarted on '$targetComputer'" } } # Force NTP resynchronization if ($PSCmdlet.ShouldProcess($targetComputer, 'Force NTP resynchronization')) { Write-Verbose "[$($MyInvocation.MyCommand)] Running w32tm /resync on '$targetComputer'..." if ($isLocal) { # Local execution: call by bare name so Pester can mock it $rawOutput = w32tm /resync 2>&1 $resyncResult = [PSCustomObject]@{ Output = ($rawOutput | Out-String).Trim() ExitCode = $LASTEXITCODE } } else { $resyncResult = Invoke-Command -ComputerName $targetComputer -ScriptBlock $resyncScriptBlock -ErrorAction Stop } $outputText = [string]$resyncResult.Output $exitCode = [int]$resyncResult.ExitCode $isSuccess = ($exitCode -eq 0) if ($isSuccess) { Write-Verbose "[$($MyInvocation.MyCommand)] [OK] Resync succeeded on '$targetComputer'" Write-Verbose "[$($MyInvocation.MyCommand)] w32tm output: $outputText" } else { Write-Warning "[$($MyInvocation.MyCommand)] Resync failed on '$targetComputer' (exit code $exitCode): $outputText" } [PSCustomObject]@{ PSTypeName = 'PSWinOps.NtpResyncResult' ComputerName = $targetComputer Success = $isSuccess ServiceRestarted = $serviceRestarted Message = $outputText Timestamp = Get-Date -Format 'o' } } } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed to sync NTP on '$targetComputer': $_" } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed" } } |