<# // // Copyright (c) Microsoft Corporation. All rights reserved. // // // // Helper script cmdlets intended only for internal use by Server Manager // via Deployment Scenario workflow Add-_InternalWindowsRole // in its internal module SererManagerShell // // // // The primary implementation of these M3P workflows is in // Add-_InternalWindowsRole.xaml. These helper functions are invoked via // InvokeCommandActivity and InlineScriptActivity instances in those workflows. // #> <# WFGetCimGuid and the WFTrace primitives are called directly by InlineScriptActivity instances in the M3P workflows. Best Practices: Parameters: Scripts run via InlineScriptActivity do not have access to workflow variables, so these must be passed as explicit function parameters. Use the $using:WFVariableName syntax in the activity declaration to pass the value. Tracing: Because debugging support for M3P workflows is so limited, it is critical to include generous tracing support in M3P workflows and their support scripts. If possible, trace the entry and exit for key functions, and trace the values of key inputs, outputs, and internal variables. Errors: Workflows and their script helpers should report errors directly to the error output stream. New-WinEvent: Note that the payload must be provided with types consistent with the defined payload in the instrumentation manifest. Otherwise the event will be generated but the payload will be defined as an error. In particular, bools must be specified as ints. We use SilentlyContinue with New-WinEvent to ensure that tracing problems do not affect the workflow. #> # Workflow helper functions # Creates a dynamic instance of CimInstance#MSFT_ServerManagerRequestGuid # and writes it to the output stream. Also writes the raw GUID # so that the workflow can use it for tracing purposes. function WFGetCimGuid { $guid = [Guid]::NewGuid() $guidBytes = $guid.ToByteArray() $guidHigh = [BitConverter]::ToUInt64($guidBytes, 0) $guidLow = [BitConverter]::ToUInt64($guidBytes, 8) $cimInstance = New-CimInstance -ClassName MSFT_ServerManagerRequestGuid -Namespace root/Microsoft/Windows/ServerManager -ClientOnly -Property @{ LowHalf = [UInt64]$guidLow ; HighHalf = [UInt64]$guidHigh} $cimInstance # Also write the raw GUID $guid } function GetTargetComputer( [array][parameter(Position = 0)]$TargetComputers # can't be mandatory, could be empty ) { $targetComputer = "" if ($TargetComputers) { if ($TargetComputers.Count -ge 1) { if ($TargetComputers[0]) { $targetComputer = $TargetComputers[0] } } } $targetComputer } function SafeString( [parameter(Position = 0)]$obj ) { if (-not ($obj -is [string])) { $obj = "" } $obj } function SafeInt( [parameter(Position = 0)]$obj ) { if (-not (($obj -is [int]) -or ($obj -is [uint32]) -or ($obj -is [byte]))) { $obj = -1 } [int]$obj } function SafeBoolToInt( [parameter(Position = 0)]$obj ) { if (-not ($obj -is [bool])) { $obj = $false } [int]$obj } function WFTraceAddWorkflowEnter( [array]$TargetComputers, # can't be mandatory, could be empty $ServerComponentDescriptors, [bool]$Remove, [string]$PathToVhdFile, [bool]$PermitReboot, [string]$Source, [bool]$DeleteComponents ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers $serverComponentNames = $ServerComponentDescriptors | % {$_.CimSystemProperties.ClassName} $nameString = SafeString ($serverComponentNames -join " ") $removeParam = SafeBoolToInt $Remove $vhdpath = SafeString $PathToVhdFile $permitRebootParam = SafeBoolToInt $PermitReboot $sourcepath = SafeString $Source $deleteComponentsParam = SafeBoolToInt $DeleteComponents # Note that New-WinEvent requires boolean payload to be passed as ints New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4000 -Payload @($targetComputer,$nameString,$removeParam,$vhdpath,$permitRebootParam,$sourcepath,$deleteComponentsParam) -ErrorAction SilentlyContinue } function WFTraceAddWorkflowExit( [array]$TargetComputers, # can't be mandatory, could be empty $AlterationState ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers $requestState = -1 $restartRequired = 0 $errorMessage = "" $errorId = "" $errorCategory = 0 $warnings = "" if ($null -ne $AlterationState) { # Need to cast these so that New-WinEvent will work $requestState = SafeInt $AlterationState.RequestState $restartRequired = SafeBoolToInt $AlterationState.RestartRequired $error = $AlterationState.Error if ($null -ne $error) { $errorMessage = SafeString $error.ErrorMessage $errorId = SafeString $error.ErrorId $errorCategory = SafeInt $error.ErrorCategory } $warnings = SafeString ($AlterationState.Warnings -join "; ") } $id = 4001 if (1 -ne $requestState) # 1 means Success { $id = 4002 } # Note that New-WinEvent requires boolean payload to be passed as int New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id $id -Payload @($targetComputer,$requestState,$restartRequired,$errorMessage,$errorId,$errorCategory,$warnings) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowInstallLaunchStarted( [array]$TargetComputers, # can't be mandatory, could be empty [System.Guid]$RequestGuid ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers # Note that New-WinEvent requires boolean payload to be passed as ints New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4010 -Payload @($targetComputer,$RequestGuid) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowInstallLaunchEnded( [array]$TargetComputers, # can't be mandatory, could be empty [System.Guid]$RequestGuid, $AlterationState ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers $requestState = -1 $restartRequired = 0 $progressTicks = -1 $totalTicks = -1 $errorMessage = "" $errorId = "" $errorCategory = 0 $warnings = "" if ($null -ne $AlterationState) { # Need to cast these so that New-WinEvent will work $requestState = SafeInt $AlterationState.RequestState $restartRequired = SafeBoolToInt $AlterationState.RestartRequired $progressTicks = SafeInt $AlterationState.ProgressTicks $totalTicks = SafeInt $AlterationState.TotalTicks $error = $AlterationState.Error if ($null -ne $error) { $errorMessage = SafeString $error.ErrorMessage $errorId = SafeString $error.ErrorId $errorCategory = SafeInt $error.ErrorCategory } $warnings = SafeString ($AlterationState.Warnings -join "; ") } # Note that New-WinEvent requires boolean payload to be passed as int New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4011 -Payload @($targetComputer,$RequestGuid,$requestState,$restartRequired,$progressTicks,$totalTicks,$errorMessage,$errorId,$errorCategory,$warnings) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowPollStarted( [array]$TargetComputers, # can't be mandatory, could be empty [System.Guid]$RequestGuid ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers # Note that New-WinEvent requires boolean payload to be passed as ints New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4012 -Payload @($targetComputer,$RequestGuid) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowPollEnded( [array]$TargetComputers, # can't be mandatory, could be empty [System.Guid]$RequestGuid, $AlterationState ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers $requestState = -1 $restartRequired = 0 $progressTicks = -1 $totalTicks = -1 $errorMessage = "" $errorId = "" $errorCategory = 0 $warnings = "" if ($null -ne $AlterationState) { # Need to cast these so that New-WinEvent will work $requestState = SafeInt $AlterationState.RequestState $restartRequired = SafeBoolToInt $AlterationState.RestartRequired $progressTicks = SafeInt $AlterationState.ProgressTicks $totalTicks = SafeInt $AlterationState.TotalTicks $error = $AlterationState.Error if ($null -ne $error) { $errorMessage = SafeString $error.ErrorMessage $errorId = SafeString $error.ErrorId $errorCategory = SafeInt $error.ErrorCategory } $warnings = SafeString ($AlterationState.Warnings -join "; ") } # Note that New-WinEvent requires boolean payload to be passed as int New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4013 -Payload @($targetComputer,$RequestGuid,$requestState,$restartRequired,$progressTicks,$totalTicks,$errorMessage,$errorId,$errorCategory,$warnings) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowRestartCheckStarted( [array]$TargetComputers, # can't be mandatory, could be empty [DateTime]$InitialLastBootTime ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers # Note that New-WinEvent requires boolean payload to be passed as ints New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4020 -Payload @($targetComputer,$InitialLastBootTime.ToFileTime()) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowRestartCheckEnded( [array]$TargetComputers, # can't be mandatory, could be empty [DateTime]$InitialLastBootTime, [DateTime]$CurrentLastBootTime, [bool]$AlreadyRebooted ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers # Note that New-WinEvent requires boolean payload to be passed as ints New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4021 -Payload @($targetComputer,$InitialLastBootTime.ToFileTime(),$CurrentLastBootTime.ToFileTime(),[int]$AlreadyRebooted) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowRestartStarted( [array]$TargetComputers # can't be mandatory, could be empty ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4022 -Payload @($targetComputer) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowRestartEnded( [array]$TargetComputers # can't be mandatory, could be empty ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4023 -Payload @($targetComputer) -ErrorAction SilentlyContinue } function WFTraceAddWindowsRoleWorkflowRestartTimeout( [array]$TargetComputers # can't be mandatory, could be empty ) { $targetComputer = GetTargetComputer -TargetComputers $TargetComputers New-WinEvent -Provider Microsoft-Windows-ServerManager-MultiMachine -Id 4024 -Payload @($targetComputer) -ErrorAction SilentlyContinue } # Returns 0 if the target computer is unreachable # Returns 1 if LastBootUpTime has not changed # Returns 2 if LastBootUpTime has changed # If target machine is unreachable, this is $false # Used internally by WFRestartComputer, not called directly from workflow function Test-LastBootUpTimeChanged( [array]$TargetComputers, [DateTime]$InitialLastBootTime, [PSCredential]$Credential ) { trap { continue; } # Get-CimInstance can fail when target machine is rebooting WFTraceAddWindowsRoleWorkflowRestartCheckStarted -TargetComputers $TargetComputers -InitialLastBootTime $InitialLastBootTime if ($TargetComputers.Count -gt 0) { # It is expected that the computer might still be rebooting if (ActiveCredential $Credential) { # Need to create an explicit session in order to pass a credential $session = New-CimSession -ComputerName $TargetComputers[0] -Credential $Credential -ErrorAction SilentlyContinue if ($session) { try { $instance = Get-CimInstance -ClassName Win32_OperatingSystem -CimSession $session -ErrorAction SilentlyContinue } finally { Remove-CimSession $session } } } else { $instance = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $TargetComputers[0] -ErrorAction SilentlyContinue } } else { $instance = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue } $alreadyRebooted = 0 if ($null -ne $instance) { $current = $instance.LastBootUpTime if ($null -ne $current) { if ($InitialLastBootTime -eq $current) { $alreadyRebooted = 1 } else { $alreadyRebooted = 2 } } WFTraceAddWindowsRoleWorkflowRestartCheckEnded -TargetComputers $TargetComputers -InitialLastBootTime $InitialLastBootTime -CurrentLastBootTime $current -AlreadyRebooted ($alreadyRebooted -eq 2) } $alreadyRebooted } # Restart the target computer unless it can be shown to have already restarted. # We need this to run as a single InlineScriptActivity so that M3P will not # persist between retrieving the current LastBootUpTime and restarting; # allowing this could potentially cause an infinite loop. function WFRestartComputer( [array]$TargetComputers, # can't be mandatory, could be empty [DateTime][Parameter(Mandatory=$true)]$StartTime, [DateTime][Parameter(Mandatory=$true)]$InitialLastBootTime, [PSCredential]$Credential ) { # We need to double-check that LastBootUpTime has not changed, # since this could be a workflow resume after local computer reboot. # Restart-Computer -Wait against the local computer does not have # the full LastBootUpTime logic, so we still need this in # the workflow. $rebooted = $false Write-Progress -Activity "Reboot target computer" -PercentComplete 80 -SourceId 1 while ($true) { $alreadyRebooted = Test-LastBootUpTimeChanged -TargetComputers $TargetComputers -InitialLastBootTime $InitialLastBootTime -Credential $Credential switch ($alreadyRebooted) { 0 { # Target not reachable break } 1 { # Target not yet rebooted break } 2 { # reboot was successful return } default # Should not happen { return } } # Terminate the workflow if reboot is still not complete # and the reboot sequence has been running for over an hour if (-not (CheckStartTimeWithinOneHour -StartTime $StartTime)) { WFTraceAddWindowsRoleWorkflowRestartTimeout -TargetComputers $TargetComputers return } if ((1 -eq $alreadyRebooted) -and (-not $rebooted)) { $rebooted = $true WFTraceAddWindowsRoleWorkflowRestartStarted $TargetComputers if ($TargetComputers.Count -gt 0) { if (ActiveCredential $Credential) { Restart-Computer -Force -Protocol WSMan -ComputerName $TargetComputers[0] -Credential $Credential } else { Restart-Computer -Force -Protocol WSMan -ComputerName $TargetComputers[0] } } else { # Local reboot is over DCOM in case WSMAN is down/disabled Restart-Computer -Force } WFTraceAddWindowsRoleWorkflowRestartEnded $TargetComputers } Start-Sleep -Seconds 15 } } <# This method is used by the "Final GetAlterationRequestState" loop at the end of the ServerManagerShell\Add-WindowsRole.xaml workflow. It returns true if timeout occurs, false otherwise. #> function WFCheckTimeout( [array]$TargetComputers, # can't be mandatory, could be empty [Parameter(Mandatory=$true)]$StartTime ) { if (CheckStartTimeWithinOneHour -StartTime $StartTime) { return $false } WFTraceAddWindowsRoleWorkflowRestartTimeout -TargetComputers $TargetComputers return $true } function ActiveCredential( [PSCredential]$Credential ) { if ($Credential) { if (-not [System.String]::IsNullOrEmpty($Credential.UserName)) { return $true } } return $false } function CheckStartTimeWithinOneHour( [Parameter(Mandatory=$true)]$StartTime ) { # //!! FUTURE We should use (Get-Date).ToUniversalTime() to avoid rare issues associated with Daylight Savings Time. # This would have to be applied here and also when StartTime is read. # //!! FUTURE Should probably present an error or warning message in this scenario $currentTime = Get-Date $elapsed = $currentTime - $StartTime[0] $withinOneHour = ($elapsed.TotalSeconds -lt (60*60)) $withinOneHour }