PowerShell Time Sync: Error Handling in Parallel in Workflows

Doctor Scripto

Summary: Guest blogger, Rudolf Vesely talks about error handling in workflows and repair actions.

Microsoft Scripting Guy, Ed Wilson, is here. Today is Part 2 of a 3-part series written by guest blogger Rudolf Vesely. Read yesterday’s post to catch up and to learn more about Rudolf: PowerShell Time Sync: Get and Evaluate Synchronization State. Take it away, Rudolf…

Yesterday, I described the Time Sync module and I explained how the main inline script works. Those of you who checked my source code may realize that I used special error handling for the whole inline script (I do not mean error handling inside of the inline script).

Note  Setting the operations discussed in today’s post require elevated rights; otherwise, you will get an Access is denied error message.

Parallel operations

When you run parallel operations, for example, using foreach –parallel {}, or in my case, using {} –PSComputerName $multipleServers, it is not possible to simply enclose whole inline script in a Try/Catch block because multiple exceptions may happen at the same time. You need to catch all the exceptions in a specified variable (-PSError $myVar), and then process them.

If I run Get-VSystemTimeSynchronization against a local server or against a single remote server, I do not do the operations in parallel; and therefore, I also need to use a Try/Catch block.

Later, I want to process all ErrorRecord objects, and I do not care if the exception happened in standard or in parallel execution. This is the reason I save all errors to the same variable.

try

{

    InlineScript

    {

        # Some code

    } -PSComputerName $ComputerName `

        -PSPersist:$false `

        -PSError $inlineScriptErrorParallelItems

}

catch [System.Management.Automation.Remoting.PSRemotingTransportException]

{

    $inlineScriptErrorItems = $_

}

if ($inlineScriptErrorParallelItems) { $inlineScriptErrorItems = $inlineScriptErrorParallelItems }

Now I have all the exceptions in the same variable and I can process it. Objects from parallel execution are enclosed in another object; therefore, I need to take them out of the Exception property:

foreach ($inlineScriptErrorFullItem in $inlineScriptErrorItems)

{

    if ($inlineScriptErrorFullItem.PSObject.Properties.Name -eq 'Exception')

    {

        $inlineScriptErrorItem = $inlineScriptErrorFullItem.Exception

    }

    else

    {

        $inlineScriptErrorItem = $inlineScriptErrorFullItem

    }

Then I continue with error handling. All objects are of the same type; and therefore, they have the same properties.

Ignoring certain errors

I implemented a feature that allows you to ignore wrong computer names or computers that are not accessible. If you use it, you can, for example, specify to be notified when the time synchronization does not work (the Property status of the output object is $false), but you do not care when the computer is inaccessible (for example, because of network failure or during a service window).

    # Ignore defined errors

    if ($IgnoreError -contains 'WrongComputerName' -and

        ($inlineScriptErrorItem.ErrorRecord.CategoryInfo | Select-Object -First 1 -ExpandProperty Category) -eq 'ResourceUnavailable' -and

        $inlineScriptErrorItem.TransportMessage -like 'The network path was not found.*')

    {

        Write-Warning -Message 'Device does not exists (not in DNS).'

    }

    elseif ($IgnoreError -contains 'DeviceIsNotAccessible' -and

        ($inlineScriptErrorItem.ErrorRecord.CategoryInfo | Select-Object -First 1 -ExpandProperty Category) -eq 'ResourceUnavailable' -and

        $inlineScriptErrorItem.TransportMessage -like '*Verify that the specified computer name is valid, that the computer is accessible over the network*')

    {

        Write-Warning -Message 'Device exists (defined in DNS) but it is not reachable (not running, FW issue, etc.).'

    }

Logging errors

I wrote the main inline script so that any exceptions are captured and logged in the ErrorEvents property of the output object. But when there is a mistake, I want to terminate the script immediately (for example, when an unhandled exception occurs). I wrote the workflow with $ErrorActionPreference = ‘Stop’ (all exceptions are terminating), so a simple Write-Error is enough to terminate the script.

    else

    {

        # Terminating error

        Write-Error -Exception $inlineScriptErrorItem.ErrorRecord.Exception

    }

}

That is all for the most important workflow, Get-VSystemTimeSynchronization. Let‘s continue with Start-VSystemTimeSynchronization.

Start-VSystemTimeSynchronization is a simple workflow to invoke time synchronization. As usual, I start with ErrorActionPreference and I do not want to see the progress bars:

$ErrorActionPreference = 'Stop'

$ProgressPreference = 'SilentlyContinue' 

Start the Windows Time service

I need to start the W32Time (Windows Time) service to use the w32tm command:

if ((Get-Service -Name W32Time).Status -ne 'Running')

{

    Write-Verbose -Message '[Start] [Start service] [Verbose] Start service: W32Time (Windows Time)'

    Start-Service -Name W32Time

}

Invoke time sync

Then I can invoke time synchronization. The most common command, w32tm /resync /force, triggers immediate time synchronization, and command w32tm /resync /rediscover rediscovers sources. Source rediscovery is important if the current source does not match the defined NTP server. This can happen, for example, when a computer cannot access the NTP server, and therefore, it uses only the internal clock. In this case, the current source is the internal clock, and that is not the desired state.

if ($Rediscover)

{

    $w32tmOutput = InlineScript { & 'w32tm' '/resync', '/rediscover' }

}

elseif ($Force)

{

    $w32tmOutput = InlineScript { & 'w32tm' '/resync', '/force' }

}

else

{

    $w32tmOutput = InlineScript { & 'w32tm' '/resync' }

}

At the end, it is handy to check if the command ran successfully and return a Boolean value that informs about it:

if ($w32tmOutput | Select-String -Pattern 'The command completed successfully.')

{

    Write-Debug -Message ('[Start] [Synchronization] [Debug] Command completed successfully.')

    $true

}

else

{

    Write-Warning -Message ('[Start] [Synchronization] [Error] Command did not completed successfully.')

    $false

}

Wait for success

Another workflow in the module is Wait-VSystemTimeSynchronization. This workflow basically uses the Get-VSystemTimeSynchronization and Start-VSystemTimeSynchronization workflows. It starts the time sync if needed and waits for success.

It is possible to limit the number of attempts to fix the time synchronization. 0 (default) means infinite attempts,  1 means only one attempt, and so on.

$repetitionCountCurrent = 1

$status = $false

while (!$status -and ($RepetitionCount -eq 0 -or $repetitionCountCurrent -le $RepetitionCount))

{

    if ($RepetitionCount -gt 0)

    {

        Write-Verbose -Message ('[Wait] Repetition: {0} / {1}' -f

            $repetitionCountCurrent, $RepetitionCount)

        $repetitionCountCurrent++

    }

    else

    {

        Write-Verbose -Message ('[Wait] Repetition')

    }

The current state is obtained from all the servers in parallel:

$outputItems = Get-VSystemTimeSynchronization `

            -ComputerName $ComputerName `

            -RequiredSourceName $RequiredSourceName `

            -RequiredSourceTypeConfiguredInRegistry $RequiredSourceTypeConfiguredInRegistry `

            -RequiredSourceTypeNotLocal $RequiredSourceTypeNotLocal `

            -RequiredSourceTypeNotByHost $RequiredSourceTypeNotByHost `

            -LastTimeSynchronizationMaximumNumberOfSeconds $LastTimeSynchronizationMaximumNumberOfSeconds `

            -CompareWithNTPServerName $CompareWithNTPServerName `

            -CompareWithNTPServerMaximumTimeDifferenceSeconds $CompareWithNTPServerMaximumTimeDifferenceSeconds `

            -IgnoreError $IgnoreError `

            -Verbose:$false `

            -PSPersist:$false

Produce a report

Now let’s count the number of successful and unsuccessful results (custom objects from all servers). All unsuccessful results are divided into groups. One group is servers with the wrong source and another group is servers with other issues.

$outputOKItems = $outputItems |

    Where-Object -FilterScript { $_.Status -eq $true }

$outputWrongSourceItems = $outputItems |

    Where-Object -FilterScript { $_.StatusSourceName -eq $false -or $_.StatusSourceType -eq $false }

$outputOtherErrorItems = $outputItems |

    Where-Object -FilterScript { $_.Status -ne $true -and

    ($_.StatusSourceName -ne $false -or $_.StatusSourceType -ne $false) }

Now let’s do the corrective actions. If the server is in group with the wrong source, the corrective action is to rediscover the source. Other servers are only forced to run immediate synchronization.

if ($CorrectiveActions -and ($outputWrongSourceItems -or $outputOtherErrorItems))

{

    if ($outputWrongSourceItems)

    {

        Write-Verbose -Message ('[Wait] Correction action: Rediscover ({0}): {1}' -f

            @($outputWrongSourceItem).Count, ($outputWrongSourceItem.ComputerNameNetBIOS -join ', '))

        $null = Start-VSystemTimeSynchronization `

            -Rediscover:$true `

            -PSComputerName $outputWrongSourceItem.ComputerNameNetBIOS

    }

    if ($outputOtherErrorItems)

    {

        Write-Verbose -Message ('[Wait] Correction action: Immediate synchronization ({0}): {1}' -f

            @($outputOtherErrorItems).Count, ($outputOtherErrorItems.ComputerNameNetBIOS -join ', '))

        $null = Start-VSystemTimeSynchronization `

            -Force:$true `

            -PSComputerName $outputWrongSourceItem.ComputerNameNetBIOS

    }

}

else

{

    $status = $true

}

Now we wait a specified number of seconds before another attempt. If the current attempt is the last one, there is no reason to wait anymore.

if ($RepetitionDelaySeconds -gt 0 -and

    ($RepetitionCount -eq 0 -or $repetitionCountCurrent -le $RepetitionCount))

{

    Write-Debug -Message ('[Wait] [Debug] Delay: {0} seconds' -f

            $RepetitionDelaySeconds)

    Start-Sleep -Seconds $RepetitionDelaySeconds

}

At the end, all objects from Get-VSystemTimeSynchronization are returned, regardless of the corrective action results:

if ($outputItems)

{

    if (@($outputItems).Count -eq @($computerNameItems).Count)

    {

        Write-Verbose -Message ('[Wait] [Verbose] Finish: {0} devices' -f

            @($outputItems).Count)

    }

    else

    {

        Write-Warning -Message ('[Wait] [Error] Not all devices were queried: {0} / {1}' -f

                @($outputItems).Count, @($computerNameItems).Count)

    }

    $outputItems

}

else

{

    Write-Warning -Message ('[Wait] [Error] No data')

}

The output from the commands is shown in the following image:

Image of command output

That is all for today. In the next and last post, I will explain the last two workflows from the Time Sync module.

~Rudolf

Thank you, Rudolf. Please join us tomorrow for the final post in this series. 

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

0 comments

Discussion is closed.

Feedback usabilla icon