Autonance.psm1
<#
.SYNOPSIS Autonance DSL maintenance container. .DESCRIPTION The Maintenance container is part of the Autonance domain-specific language (DSL) and is used to define a maintenance container block. The maintenance container block is always the topmost block and contains all sub containers and maintenance tasks. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function Maintenance { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] param ( # Name of the maintenance. [Parameter(Mandatory = $true, Position = 0)] [System.String] $Name, # Script block containing the maintenance tasks. [Parameter(Mandatory = $true, Position = 1)] [System.Management.Automation.ScriptBlock] $ScriptBlock, # Optionally parameters to use for all maintenance tasks. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = $null, # Hide autonance header. [Parameter(Mandatory = $false)] [switch] $NoHeader, # Hide autonance output. [Parameter(Mandatory = $false)] [switch] $NoOutput ) try { # Ensure that the Maintenance block is not nested if ($Script:AutonanceBlock) { throw 'Maintenance container is not the topmost block' } # Initialize context variables $Script:AutonanceBlock = $true $Script:AutonanceLevel = 0 $Script:AutonanceSilent = $NoOutput.IsPresent # Headline with module info if (!$NoHeader.IsPresent) { Write-Autonance -Message (Get-Module -Name 'Autonance' | ForEach-Object { "{0} Version {1}`n{2}" -f $_.Name, $_.Version, $_.Copyright }) -Type 'Info' } # Create the maintenance container object $containerSplat = @{ Type = 'Maintenance' Name = $Name Credential = $Credential ScriptBlock = $ScriptBlock } $container = New-AutonanceContainer @containerSplat # Invoke the root maintenance container Invoke-AutonanceContainer -Container $container # End blank lines Write-Autonance -Message "`n" -Type 'Info' } finally { # Reset context variable $Script:AutonanceBlock = $false $Script:AutonanceTimestamp = $null } } <# .SYNOPSIS Autonance DSL container to group maintenance tasks. .DESCRIPTION The TaskGroup container is part of the Autonance domain-specific language (DSL) and is used to group maintenance tasks. Optionally, the tasks within the group can be repeated in a loop. The loop can have multiple stop options or will run infinite as long as no exception occurs. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function TaskGroup { [CmdletBinding(DefaultParameterSetName = 'Simple')] param ( # Task group name. [Parameter(Mandatory = $true, Position = 0)] [System.String] $Name, # Script block containing the grouped tasks. [Parameter(Mandatory = $true, Position = 1)] [System.Management.Automation.ScriptBlock] $ScriptBlock, # Optionally parameters to use for all maintenance tasks. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = $null, # Option to repeat all tasks in the group. [Parameter(Mandatory = $false, ParameterSetName = 'Repeat')] [switch] $Repeat, # Number of times to repeat the group. Use 0 for an infinite loop. [Parameter(Mandatory = $false, ParameterSetName = 'Repeat')] [System.Int32] $RepeatCount = 0, # Script block to control the repeat loop. Return $true to continue with # the loop. Return $false to stop the loop and continue with the next # maintenance task. [Parameter(Mandatory = $false, ParameterSetName = 'Repeat')] [System.Management.Automation.ScriptBlock] $RepeatCondition = $null, # Option to show a user prompt after each loop, to inquire, if the loop # should continue or stop. [Parameter(Mandatory = $false, ParameterSetName = 'Repeat')] [switch] $RepeatInquire ) # Create and return the task group container object $containerSplat = @{ Type = 'TaskGroup' Name = $Name Credential = $Credential ScriptBlock = $ScriptBlock Repeat = $Repeat.IsPresent RepeatCount = $RepeatCount RepeatCondition = $RepeatCondition RepeatInquire = $RepeatInquire.IsPresent } New-AutonanceContainer @containerSplat } <# .SYNOPSIS Get the registered Autonance extensions. .DESCRIPTION This command wil return all registered autonance extensions in the current PowerShell session. .EXAMPLE PS C:\> Get-AutonanceExtension Returns all registered autonance extensions. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function Get-AutonanceExtension { [CmdletBinding()] param ( # Name of the extension to return [Parameter(Mandatory = $false)] [System.String] $Name ) foreach ($key in $Script:AutonanceExtension.Keys) { if ($null -eq $Name -or $key -like $Name) { $Script:AutonanceExtension[$key] } } } <# .SYNOPSIS Register an Autonance extension. .DESCRIPTION This function will register a new Autonance extension, by creating a global function with the specified extension name. The function can be called like all other DSL tasks in the Maintenance block. The script block can contain a parameter block, to specify the parameters provided to the cmdlet. If a parameter $Credential is used, the credentials will automatically be passed to the sub task, if specified. The function Write-AutonanceMessage can be used to return status messages. The Autonance module will take care of the formatting. .EXAMPLE PS C:\> Register-AutonanceExtension -Name 'WsusReport' -ScriptBlock { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, ) if ($null -eq $Credential) { Invoke-Command -ComputerName $ComputerName -ScriptBlock { wuauclt.exe /ReportNow } } else { Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock { wuauclt.exe /ReportNow } } } Register an Autonance extension to invoke the report now command for WSUS. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function Register-AutonanceExtension { [CmdletBinding()] param ( # Extension function name. [Parameter(Mandatory = $true)] [System.String] $Name, # Script block to execute for the extension. [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ScriptBlock ) # Register the Autonance extension in the current scope $Script:AutonanceExtension[$Name] = [PSCustomObject] [Ordered] @{ PSTypeName = 'Autonance.Extension' Name = $Name ScriptBlock = $ScriptBlock } # Create the helper script block which will be invoked for the extension. It # return the extension as a Autonance task item. $extensionScriptBlock = { $name = (Get-PSCallStack)[0].InvocationInfo.MyCommand.Name $extension = Get-AutonanceExtension -Name $name # Create and return the task object without using New-AutonanceTask # function, because this will be executed outside of the module scope # and there is the helper function New-AutonanceTask not available. [PSCustomObject] [Ordered] @{ PSTypeName = 'Autonance.Task' Type = $extension.Name Name = '' Credential = $Credential ScriptBlock = $extension.ScriptBlock Arguments = $PSBoundParameters } } # Concatenate the original parameters and the helper script block $extensionParameter = [String] $ScriptBlock.Ast.ParamBlock $extensionBody = [String] $extensionScriptBlock $extensionScriptBlock = [ScriptBlock]::Create($extensionParameter + $extensionBody) # Register the global function Set-Item -Path "Function:\Global:$Name" -Value $extensionScriptBlock -Force | Out-Null } <# .SYNOPSIS Unregister an Autonance extension. .DESCRIPTION This function removes a registered Autonance extension from the current session. .EXAMPLE PS C:\> Unregister-AutonanceExtension -Name 'WsusReport' Unregister the Autonance extension calles WsusReport. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function Unregister-AutonanceExtension { [CmdletBinding()] param ( # Extension function name. [Parameter(Mandatory = $true)] [System.String] $Name ) if ($Script:AutonanceExtension.ContainsKey($Name)) { # Remove the module extension $Script:AutonanceExtension.Remove($Name) # Remove the global function Remove-Item -Path "Function:\$Name" -Force } } <# .SYNOPSIS Write an Autonance task message. .DESCRIPTION This function must be used in a Autonance extension task to show the current status messages in a nice formatted output. The Autonance module will take care about the indent and message color. .EXAMPLE PS C:\> Register-AutonanceExtension -Name 'ShowMessage' -ScriptBlock { Write-AutonanceMessage -Message 'Hello, World!' } Uses the Write-AutonanceMessage function to show a nice formatted output message within a custom Autonance task. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function Write-AutonanceMessage { [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [System.String] $Message ) if (!$Script:AutonanceBlock) { throw 'Write-AutonanceMessage function not encapsulated in a Autonance task' } Write-Autonance -Message $Message } <# .SYNOPSIS Autonance DSL task to get a user confirmation. .DESCRIPTION The ConfirmTask task is part of the Autonance domain-specific language (DSL). The task uses the $Host.UI.PromptForChoice() built-in method to display a host specific confirm prompt. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function ConfirmTask { [CmdletBinding()] param ( # Message title for the confirm box. [Parameter(Mandatory = $true, Position = 0)] [System.String] $Caption, # Message body for the confirm box. [Parameter(Mandatory = $true, Position = 1)] [System.String] $Query ) if (!$Script:AutonanceBlock) { throw 'ConfirmTask task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'ConfirmTask' -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # Message title for the confirm box. [Parameter(Mandatory = $true, Position = 0)] [System.String] $Caption, # Message body for the confirm box. [Parameter(Mandatory = $true, Position = 1)] [System.String] $Query ) # Prepare the choices $choices = New-Object -TypeName 'Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]' $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes', 'Continue the maintenance')) $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No', 'Stop the maintenance')) # Query the desired choice from the user do { $result = $Host.UI.PromptForChoice($Caption, $Query, $choices, -1) } while ($result -eq -1) # Check the result and quit the execution, if necessary if ($result -ne 0) { throw "User has canceled the maintenance!" } } } <# .SYNOPSIS Autonance DSL task to invoke a local script block. .DESCRIPTION The LocalScript task is part of the Autonance domain-specific language (DSL). The task will invoke the script block on the local computer. The script can use some of the built-in PowerShell functions to return objects or control the maintenance: - Throw an terminating error to stop the whole maintenance script - Show status information with Write-Autonance .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function LocalScript { [CmdletBinding()] param ( # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # The script block to invoke. [Parameter(Mandatory = $true, Position = 0)] [System.Management.Automation.ScriptBlock] $ScriptBlock ) if (!$Script:AutonanceBlock) { throw 'LocalScript task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'LocalScript' -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # The script block to invoke. [Parameter(Mandatory = $true, Position = 0)] [System.Management.Automation.ScriptBlock] $ScriptBlock ) $ErrorActionPreference = 'Stop' if ($null -eq $Credential) { Write-Autonance -Message 'Invoke the local script block now...' & $ScriptBlock } else { try { Write-Autonance -Message "Push the impersonation context as $($Credential.UserName)" Push-ImpersonationContext -Credential $Credential -ErrorAction Stop Write-Autonance -Message 'Invoke the local script block now...' & $ScriptBlock } finally { Write-Autonance -Message 'Pop the impersonation context' Pop-ImpersonationContext -ErrorAction SilentlyContinue } } } } <# .SYNOPSIS Autonance DSL task to invoke a remote script block. .DESCRIPTION The RemoteScript task is part of the Autonance domain-specific language (DSL). The task will invoke the script block on the specified Windows computer using WinRM. A user account can be specified with the Credential parameter. The script can use some of the built-in PowerShell functions to return objects or control the maintenance: - Throw an terminating error to stop the whole maintenance script - Show status information with Write-Autonance .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function RemoteScript { [CmdletBinding()] param ( # This task restarts the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # The script block to invoke. [Parameter(Mandatory = $true, Position = 1)] [System.Management.Automation.ScriptBlock] $ScriptBlock ) if (!$Script:AutonanceBlock) { throw 'RemoteScript task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'RemoteScript' -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This task restarts the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # The script block to invoke. [Parameter(Mandatory = $true, Position = 1)] [System.Management.Automation.ScriptBlock] $ScriptBlock ) try { $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType WinRM -ErrorAction Stop Write-Autonance -Message "Invoke the remote script block now..." Invoke-Command -Session $session -ScriptBlock $ScriptBlock -ErrorAction Stop } catch { throw $_ } finally { Remove-AutonanceSession -Session $session } } } <# .SYNOPSIS Autonance DSL task to wait for the specified amount of time. .DESCRIPTION The SleepTask task is part of the Autonance domain-specific language (DSL). The task uses the Start-Sleep built-in command to wait for the specified amount of time. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function SleepTask { [CmdletBinding()] param ( # Duration in seconds to wait. [Parameter(Mandatory = $true, Position = 0)] [System.Int32] $Second ) if (!$Script:AutonanceBlock) { throw 'SleepTask task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'SleepTask' -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # Duration in seconds to wait. [Parameter(Mandatory = $true, Position = 0)] [System.Int32] $Second ) Write-Autonance -Message "Wait for $Second second(s)" # Now wait Start-Sleep -Seconds $Second } } <# .SYNOPSIS Autonance DSL task to failover an SQL Server Availability Group. .DESCRIPTION Use this task to failover the specified SQL Server Availability Group to the specified computer and instance. It will use the SQLPS module on the remote system. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function SqlServerAvailabilityGroupFailover { [CmdletBinding()] param ( # This is the target Windows computer for the planned failover. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Target SQL instance for the planned planned. [Parameter(Mandatory = $true, Position = 1)] [System.String] $SqlInstance, # The availability group name to perform a planned manual failover. [Parameter(Mandatory = $true, Position = 2)] [System.String] $AvailabilityGroup, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between availability group state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 60, # Specifies the interval between availability group state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 2 ) if (!$Script:AutonanceBlock) { throw 'SqlServerAvailabilityGroupFailover task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'SqlServerAvailabilityGroupFailover' -Name "$ComputerName $SqlInstance $AvailabilityGroup" -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This is the target Windows computer for the planned failover. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Target SQL instance for the planned planned. [Parameter(Mandatory = $true, Position = 1)] [System.String] $SqlInstance, # The availability group name to perform a planned manual failover. [Parameter(Mandatory = $true, Position = 2)] [System.String] $AvailabilityGroup, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between availability group state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 60, # Specifies the interval between availability group state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 2 ) $SqlInstancePath = $SqlInstance # Default MSSQLSERVER instance, only specified as server name if (-not $SqlInstance.Contains('\')) { $SqlInstancePath = "$SqlInstance\DEFAULT" } try { ## Part 1 - Connect to the SQL Server and load the SQLPS module $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType WinRM -ErrorAction Stop # Load the SQL PowerShell module but suppress warnings because of # uncommon cmdlet verbs. Invoke-Command -Session $session -ScriptBlock { Import-Module -Name 'SQLPS' -WarningAction 'SilentlyContinue' } -ErrorAction Stop ## Part 2 - Check the current role and state $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" | Select-Object * } -ErrorAction Stop $replica = $replicas.Where({$_.Name -eq $SqlInstance})[0] ## Part 3 - Planned manual failover if ($replica.Role -ne 'Primary') { ## Part 3a - Check replicate state Write-Autonance -Message "Replica role is $($replica.Role.ToString().ToLower())" Write-Autonance -Message "Replica state is $($replica.RollupRecoveryState.ToString().ToLower()) and $($replica.RollupSynchronizationState.ToString().ToLower())" if ($replica.RollupRecoveryState.ToString() -ne 'Online' -or $replica.RollupSynchronizationState.ToString() -ne 'Synchronized') { throw 'Replicate is not ready for planned manual failover!' } ## Part 3b - Invoke failover Write-Autonance -Message "Failover $AvailabilityGroup to $SqlInstance ..." Invoke-Command -Session $session -ScriptBlock { Switch-SqlAvailabilityGroup -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup" } -ErrorAction Stop Wait-AutonanceTask -Activity "$SqlInstance replication is restoring ..." -Count $Count -Delay $Delay -Condition { $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" | ForEach-Object { $_.Refresh(); $_ } } -ErrorAction Stop $condition = $true # Check all replica states foreach ($replica in $replicas) { # Test for primary replica if ($replica.Name -eq $SqlInstance) { $condition = $condition -and $replica.Role -eq 'Primary' $condition = $condition -and $replica.RollupRecoveryState -eq 'Online' } # Test for any replica $condition = $condition -and $replica.RollupSynchronizationState -eq 'Synchronized' } $condition } } ## Part 4 - Verify $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" } -ErrorAction Stop $replica = $replicas.Where({$_.Name -eq $SqlInstance})[0] Write-Autonance -Message "Replica role is $($replica.Role.ToString().ToLower())" Write-Autonance -Message "Replica state is $($replica.RollupRecoveryState.ToString().ToLower()) and $($replica.RollupSynchronizationState.ToString().ToLower())" if ($replica.Role -ne 'Primary') { throw 'Replica role is not primary' } if ($replica.RollupRecoveryState -ne 'Online') { throw 'Replica recovery state is not online' } if ($replica.RollupSynchronizationState -ne 'Synchronized') { throw 'Replica synchronization state is not synchronized' } } catch { throw $_ } finally { Remove-AutonanceSession -Session $session # Ensure, that the next task has a short delay Start-Sleep -Seconds 3 } } } <# .SYNOPSIS Autonance DSL task to restart a Windows computer. .DESCRIPTION The WindowsComputerRestart task is part of the Autonance domain-specific language (DSL). The task will restart the specified Windows computer using WinRM. A user account can be specified with the Credential parameter. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function WindowsComputerRestart { [CmdletBinding()] param ( # This task restarts the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 120, # Specifies the interval between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 5 ) if (!$Script:AutonanceBlock) { throw 'WindowsComputerRestart task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'WindowsComputerRestart' -Name $ComputerName -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This task restarts the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 120, # Specifies the interval between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 5 ) try { ## Part 1 - Before Reboot $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop # Get the operating system object $operatingSystem = Get-CimInstance -CimSession $session -ClassName 'Win32_OperatingSystem' -ErrorAction Stop # To verify the reboot, store the last boot up time $oldBootUpTime = $operatingSystem.LastBootUpTime Write-Autonance -Message "Last boot up time is $oldBootUpTime" ## Part 2 - Execute Reboot Write-Autonance -Message "Restart computer $ComputerName now ..." # Now, reboot the system $result = $operatingSystem | Invoke-CimMethod -Name 'Reboot' -ErrorAction Stop # Check method error code if ($result.ReturnValue -ne 0) { $errorMessage = Get-AutonanceErrorMessage -Component 'Win32OperatingSystem_Wbem' -ErrorId $result.ReturnValue throw "Failed to restart $ComputerName with error code $($result.ReturnValue): $errorMessage" } # Close old session (or try it...) Remove-AutonanceSession -Session $session -Silent # Reset variables $session = $null $operatingSystem = $null # Wait until the computer has restarted is running Wait-AutonanceTask -Activity "$ComputerName is restarting..." -Count $Count -Delay $Delay -Condition { # Prepare credentials for remote connection $credentialSplat = @{} if ($null -ne $Credential) { $credentialSplat['Credential'] = $Credential } # Test the connection and try to get the operating system object $innerOperatingSystem = Invoke-Command -ComputerName $ComputerName @credentialSplat -ScriptBlock { Get-CimInstance -ClassName 'Win32_OperatingSystem' } -WarningAction SilentlyContinue -ErrorAction SilentlyContinue # Return boolean value if the condition has passed $null -ne $innerOperatingSystem -and $oldBootUpTime -lt $innerOperatingSystem.LastBootUpTime } ## Part 3 - Verify Reboot $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop # Get the operating system object $operatingSystem = Get-CimInstance -CimSession $session -ClassName 'Win32_OperatingSystem' -ErrorAction Stop # To verify the reboot, store the new boot up time $newBootUpTime = $operatingSystem.LastBootUpTime Write-Autonance -Message "New boot up time is $newBootUpTime" # Verify if the reboot was successful if ($oldBootUpTime -eq $newBootUpTime) { throw "Failed to restart computer $ComputerName!" } } catch { throw $_ } finally { Remove-AutonanceSession -Session $session # Ensure, that the next task has a short delay Start-Sleep -Seconds 5 } } } <# .SYNOPSIS Autonance DSL task to shutdown a Windows computer. .DESCRIPTION The WindowsComputerShutdown task is part of the Autonance domain-specific language (DSL). The task will shutdown the specified Windows computer. A user account can be specified with the Credential parameter. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function WindowsComputerShutdown { [CmdletBinding()] param ( # This task stops the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 720, # Specifies the interval between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 5 ) if (!$Script:AutonanceBlock) { throw 'WindowsComputerShutdown task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'WindowsComputerShutdown' -Name $ComputerName -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This task stops the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 720, # Specifies the interval between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 5 ) ## Part 1 - Before Shutdown $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop # Get the operating system object $operatingSystem = Get-CimInstance -CimSession $session -ClassName 'Win32_OperatingSystem' -ErrorAction Stop Write-Autonance -Message "Last boot up time is $($operatingSystem.LastBootUpTime)" ## Part 2 - Execute Shutdown Write-Autonance -Message "Shutdown computer $ComputerName now ..." # Now, reboot the system $result = $operatingSystem | Invoke-CimMethod -Name 'Shutdown' -ErrorAction Stop # Check method error code if ($result.ReturnValue -ne 0) { $errorMessage = Get-AutonanceErrorMessage -Component 'Win32OperatingSystem_Wbem' -ErrorId $result.ReturnValue throw "Failed to shutdown $ComputerName with error code $($result.ReturnValue): $errorMessage" } # Close old session (or try it...) Remove-AutonanceSession -Session $session -Silent ## Part 3 - Wait for Shutdown # Wait until the computer has is disconnected Wait-AutonanceTask -Activity "Wait for computer $ComputerName disconnect..." -Count $Count -Delay $Delay -Condition { try { # Prepare credentials for remote connection $credentialSplat = @{} if ($null -ne $Credential) { $credentialSplat['Credential'] = $Credential } # Test the connection and try to get the computer name Invoke-Command -ComputerName $ComputerName @credentialSplat -ScriptBlock { $Env:ComputerName } -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null return $false } catch { return $true } } } } <# .SYNOPSIS Autonance DSL task to wait for a Windows computer. .DESCRIPTION The WindowsComputerWait task is part of the Autonance domain-specific language (DSL). The task will wait until the specified Windows computer is reachable using WinRM. A user account can be specified with the Credential parameter. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function WindowsComputerWait { [CmdletBinding()] param ( # This task waits for the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 720, # Specifies the interval between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 5 ) if (!$Script:AutonanceBlock) { throw 'WindowsComputerWait task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'WindowsComputerWait' -Name $ComputerName -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This task waits for the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 720, # Specifies the interval between computer state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 5 ) Write-Autonance -Message "Wait for computer $ComputerName..." # Wait until the computer is reachable Wait-AutonanceTask -Activity "Wait for computer $ComputerName..." -Count $Count -Delay $Delay -Condition { try { # Prepare credentials for remote connection $credentialSplat = @{} if ($null -ne $Credential) { $credentialSplat['Credential'] = $Credential } # Test the connection and try to get the computer name Invoke-Command -ComputerName $ComputerName @credentialSplat -ScriptBlock { $Env:ComputerName } -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null return $true } catch { return $false } } } } <# .SYNOPSIS Autonance DSL task to configure a Windows service. .DESCRIPTION The WindowsServiceConfig task is part of the Autonance domain-specific language (DSL). The task will configure the specified Windows service on a remote computer by using CIM to connect to the remote computer. A user account can be specified with the Credential parameter. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function WindowsServiceConfig { [CmdletBinding()] param ( # This task configures a Windows service on the specified computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies the service name for the service to be configured. [Parameter(Mandatory = $true, Position = 1)] [System.String] $ServiceName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # If specified, the target startup type will be set. [Parameter(Mandatory = $false)] [ValidateSet('Automatic', 'Manual', 'Disabled')] [System.String] $StartupType ) if (!$Script:AutonanceBlock) { throw 'WindowsServiceConfig task not encapsulated in a Maintenance container' } # Define a nice task name $name = "$ComputerName\$ServiceName" if ($PSBoundParameters.ContainsKey('StartupType')) { $name += ", StartupType=$StartupType" } New-AutonanceTask -Type 'WindowsServiceConfig' -Name $name -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This task configures a Windows service on the specified computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies the service name for the service to be configured. [Parameter(Mandatory = $true, Position = 1)] [System.String] $ServiceName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # If specified, the target startup type will be set. [Parameter(Mandatory = $false)] [ValidateSet('Automatic', 'Manual', 'Disabled')] [System.String] $StartupType ) try { $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop # Get service and throw an exception, if the service does not exist $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop if ($null -eq $service) { throw "$ServiceName service does not exist!" } if ($PSBoundParameters.ContainsKey('StartupType')) { # Do nothing, if the services startup type is correct if ($service.StartMode.Replace('Auto', 'Automatic') -eq $StartupType) { Write-Autonance -Message "$ServiceName service startup type is already set to $StartupType" } else { Write-Autonance -Message "$ServiceName service startup type is $($service.StartMode.Replace('Auto', 'Automatic'))" Write-Autonance -Message "Set $ServiceName service startup type to $StartupType" # Reconfigure service $result = $service | Invoke-CimMethod -Name 'ChangeStartMode' -Arguments @{ StartMode = $StartupType } -ErrorAction Stop # Check method error code if ($result.ReturnValue -ne 0) { throw "Failed to set $ServiceName service startup type with error code $($result.ReturnValue)!" } # Check if the service startup type is correct $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop if ($service.StartMode.Replace('Auto', 'Automatic') -ne $StartupType) { throw "Failed to set $ServiceName service startup type, current is $($service.StartMode.Replace('Auto', 'Automatic'))!" } Write-Autonance -Message "$ServiceName service startup type changed successfully" } } } catch { throw $_ } finally { Remove-AutonanceSession -Session $session } } } <# .SYNOPSIS Autonance DSL task to start a Windows service. .DESCRIPTION The WindowsServiceStart task is part of the Autonance domain-specific language (DSL). The task will start the specified Windows service on a remote computer by using CIM to connect to the remote computer. A user account can be specified with the Credential parameter. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function WindowsServiceStart { [CmdletBinding()] param ( # This task starts a Windows service on the specified computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies the service name for the service to be started. [Parameter(Mandatory = $true, Position = 1)] [System.String] $ServiceName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between service state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 30, # Specifies the interval between service state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 2 ) if (!$Script:AutonanceBlock) { throw 'WindowsServiceStart task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'WindowsServiceStart' -Name "$ComputerName\$ServiceName" -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This task starts a Windows service on the specified computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies the service name for the service to be started. [Parameter(Mandatory = $true, Position = 1)] [System.String] $ServiceName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between service state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 30, # Specifies the interval between service state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 2 ) try { $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop # Get service and throw an exception, if the service does not exist $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop if ($null -eq $service) { throw "$ServiceName service does not exist!" } # Do nothing, if the services is already running if ($service.State -eq 'Running') { Write-Autonance -Message "$ServiceName service is already running" } else { Write-Autonance -Message "$ServiceName service is not running" Write-Autonance -Message "Start $ServiceName service now" # Start service $result = $service | Invoke-CimMethod -Name 'StartService' -ErrorAction Stop # Check method error code if ($result.ReturnValue -ne 0) { $errorMessage = Get-AutonanceErrorMessage -Component 'Win32Service_StartService' -ErrorId $result.ReturnValue throw "Failed to start $ServiceName service with error code $($result.ReturnValue): $errorMessage!" } # Wait until the services is running Wait-AutonanceTask -Activity "$ServiceName service is starting..." -Count $Count -Delay $Delay -Condition { $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop $service.State -ne 'Start Pending' } # Check if the service is now running $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop if ($service.State -ne 'Running') { throw "Failed to start $ServiceName service, current state is $($service.State)!" } Write-Autonance -Message "$ServiceName service started successfully" } } catch { throw $_ } finally { Remove-AutonanceSession -Session $session } } } <# .SYNOPSIS Autonance DSL task to stop a Windows service. .DESCRIPTION The WindowsServiceStop task is part of the Autonance domain-specific language (DSL). The task will stop the specified Windows service on a remote computer by using CIM to connect to the remote computer. A user account can be specified with the Credential parameter. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function WindowsServiceStop { [CmdletBinding()] param ( # This task stops a Windows service on the specified computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies the service name for the service to be stopped. [Parameter(Mandatory = $true, Position = 1)] [System.String] $ServiceName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between service state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 30, # Specifies the interval between service state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 2 ) if (!$Script:AutonanceBlock) { throw 'WindowsServiceStop task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'WindowsServiceStop' -Name "$ComputerName\$ServiceName" -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This task stops a Windows service on the specified computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies the service name for the service to be stopped. [Parameter(Mandatory = $true, Position = 1)] [System.String] $ServiceName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # Specifies the number of retries between service state tests. [Parameter(Mandatory = $false)] [System.Int32] $Count = 30, # Specifies the interval between service state tests. [Parameter(Mandatory = $false)] [System.Int32] $Delay = 2 ) try { $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop # Get service and throw an exception, if the service does not exist $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop if ($null -eq $service) { throw "$ServiceName service does not exist!" } # Do nothing, if the services is already running if ($service.State -eq 'Stopped') { Write-Autonance -Message "$ServiceName service is already stopped" } else { Write-Autonance -Message "$ServiceName service is not stopped" Write-Autonance -Message "Stop $ServiceName service now" # Stop service $result = $service | Invoke-CimMethod -Name 'StopService' -ErrorAction Stop # Check method error code if ($result.ReturnValue -ne 0) { $errorMessage = Get-AutonanceErrorMessage -Component 'Win32Service_StopService' -ErrorId $result.ReturnValue throw "Failed to stop $ServiceName service with error code $($result.ReturnValue): $errorMessage!" } # Wait until the services is stopped Wait-AutonanceTask -Activity "$ServiceName service is stopping..." -Count $Count -Delay $Delay -Condition { $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop $service.State -ne 'Stop Pending' } # Check if the service is now stopped $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop if ($service.State -ne 'Stopped') { throw "Failed to stop $ServiceName service, current state is $($service.State)!" } Write-Autonance -Message "$ServiceName service stopped successfully" } } catch { throw $_ } finally { Remove-AutonanceSession -Session $session } } } <# .SYNOPSIS Autonance DSL task to install Windows updates. .DESCRIPTION The WindowsUpdateInstall task is part of the Autonance domain-specific language (DSL). The task will install all pending Windows updates on the target Windows computer by using WinRM and the 'Microsoft.Update.Session' COM object. A user account can be specified with the Credential parameter. .NOTES Author : Claudio Spizzi License : MIT License .LINK https://github.com/claudiospizzi/Autonance #> function WindowsUpdateInstall { [CmdletBinding()] param ( # This task restarts the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # If specified, all available updates will be installed without a query. [Parameter(Mandatory = $false)] [switch] $All ) if (!$Script:AutonanceBlock) { throw 'WindowsUpdateInstall task not encapsulated in a Maintenance container' } New-AutonanceTask -Type 'WindowsUpdateInstall' -Name $ComputerName -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock { [CmdletBinding()] param ( # This task restarts the specified Windows computer. [Parameter(Mandatory = $true, Position = 0)] [System.String] $ComputerName, # Specifies a user account that has permission to perform the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # If specified, all available updates will be installed without a query. [Parameter(Mandatory = $false)] [switch] $All ) try { $guid = New-Guid | Select-Object -ExpandProperty 'Guid' $script = Get-Content -Path "$Script:ModulePath\Scripts\WindowsUpdate.ps1" $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType WinRM -ErrorAction Stop ## Part 1: Search for pending updates Write-Autonance -Message 'Search for pending updates ...' $pendingUpdates = Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock { $updateSession = New-Object -ComObject 'Microsoft.Update.Session' $updateSearcher = $updateSession.CreateUpdateSearcher() $searchResult = $updateSearcher.Search("IsInstalled=0 and Type='Software'") foreach ($update in $searchResult.Updates) { [PSCustomObject] @{ KBArticle = 'KB' + $update.KBArticleIDs[0] Identity = $update.Identity.UpdateID Title = $update.Title } } } if ($pendingUpdates.Count -eq 0) { Write-Autonance -Message 'No pending updates found' return } Write-Autonance -Message ("{0} pending update(s) found" -f $pendingUpdates.Count) ## Part 2: Select updates if ($All.IsPresent) { Write-Autonance -Message 'All pending update(s) were preselected to install' $selectedUpdates = $pendingUpdates } else { Write-Autonance -Message 'Query the user for update(s) to install' $readHostMultipleChoiceSelection = @{ Caption = 'Choose Updates' Message = 'Please select the updates to install from the following list.' ChoiceObject = $pendingUpdates ChoiceLabel = $pendingUpdates.Title } $selectedUpdates = @(Read-HostMultipleChoiceSelection @readHostMultipleChoiceSelection) if ($selectedUpdates.Count -eq 0) { Write-Autonance -Message 'No updates selected by the user' return } Write-Autonance -Message ("{0} pending update(s) were selected to install" -f $selectedUpdates.Count) } ## Part 3: Install the updates with a one-time scheduled task Write-Autonance -Message 'Invoke a remote scheduled task to install the update(s)' # Create and start the scheduled task Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock { $using:script | Set-Content -Path "C:\Windows\Temp\WindowsUpdate-$using:guid.ps1" -Encoding UTF8 $updateList = $using:selectedUpdates.Identity -join ',' try { # Use the new scheduled tasks cmdlets $newScheduledTask = @{ Action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -File `"C:\Windows\Temp\WindowsUpdate-$using:guid.ps1`" -Id `"$using:guid`" -Update `"$updateList`"" -ErrorAction Stop Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(-1) -ErrorAction Stop Principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest -ErrorAction Stop Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -ErrorAction Stop } New-ScheduledTask @newScheduledTask -ErrorAction Stop | Register-ScheduledTask -TaskName "WindowsUpdate-$using:guid" -ErrorAction Stop | Start-ScheduledTask -ErrorAction Stop } catch { # Craete a temporary batch file "powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"C:\Windows\Temp\WindowsUpdate-$using:guid.ps1`" -Id `"$using:guid`" -Update `"$updateList`"" | Out-File "C:\Windows\Temp\WindowsUpdate-$using:guid.cmd" -Encoding Ascii # The scheduled tasks cmdlets are missing, use schtasks.exe (SCHTASKS.EXE /CREATE /RU "NT Authority\System" /SC ONCE /ST 23:59 /TN "WindowsUpdate-$using:guid" /TR "`"C:\Windows\Temp\WindowsUpdate-$using:guid.cmd`"" /RL HIGHEST /F) | Out-Null (SCHTASKS.EXE /RUN /TN "WindowsUpdate-$using:guid") | Out-Null } } # Wait for every step until it is completed foreach ($step in @('Search', 'Download', 'Install')) { $status = Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock { $step = $using:step $path = "C:\Windows\Temp\WindowsUpdate-$using:guid.xml" do { Start-Sleep -Seconds 1 if (Test-Path -Path $path) { $status = Import-Clixml -Path $path } } while ($null -eq $status -or $status.$step.Status -eq $false) Write-Output $status.$step } Write-Autonance -Message $status.Message if (-not $status.Result) { throw $status.Message } } } catch { throw $_ } finally { # Try to cleanup the scheduled task and the script file if ($null -ne $session) { Invoke-Command -Session $session -ErrorAction SilentlyContinue -ScriptBlock { Unregister-ScheduledTask -TaskName "WindowsUpdate-$using:guid" -Confirm:$false -ErrorAction SilentlyContinue Remove-Item -Path "C:\Windows\Temp\WindowsUpdate-$using:guid.cmd" -Force -ErrorAction SilentlyContinue Remove-Item -Path "C:\Windows\Temp\WindowsUpdate-$using:guid.ps1" -Force -ErrorAction SilentlyContinue Remove-Item -Path "C:\Windows\Temp\WindowsUpdate-$using:guid.xml" -Force -ErrorAction SilentlyContinue } } Remove-AutonanceSession -Session $session # Ensure, that the next task has a short delay Start-Sleep -Seconds 3 } } } function Get-AutonanceErrorMessage { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Component, [Parameter(Mandatory = $false)] [System.String] $ErrorId ) $values = Import-PowerShellDataFile -Path "$Script:ModulePath\Strings\$Component.psd1" $errorMessage = $values[$result.ReturnValue] if ([String]::IsNullOrEmpty($errorMessage)) { $errorMessage = 'Unknown' } Write-Output $errorMessage } function Initialize-ImpersonationContext { [CmdletBinding()] param () # Add Win32 native API methods to call to LogonUser() if (-not ([System.Management.Automation.PSTypeName]'Win32.AdvApi32').Type) { Add-Type -Namespace 'Win32' -Name 'AdvApi32' -MemberDefinition ' [DllImport("advapi32.dll", SetLastError = true)] public static extern bool LogonUser(string lpszUserName, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); ' } # Add Win32 native API methods to call to CloseHandle() if (-not ([System.Management.Automation.PSTypeName]'Win32.Kernel32').Type) { Add-Type -Namespace 'Win32' -Name 'Kernel32' -MemberDefinition ' [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr handle); ' } # Define enumeration for the logon type if (-not ([System.Management.Automation.PSTypeName]'Win33.Logon32Type').Type) { Add-Type -TypeDefinition ' namespace Win32 { public enum Logon32Type { Interactive = 2, Network = 3, Batch = 4, Service = 5, Unlock = 7, NetworkClearText = 8, NewCredentials = 9 } } ' } # Define enumeration for the logon provider if (-not ([System.Management.Automation.PSTypeName]'Win33.Logon32Type').Type) { Add-Type -TypeDefinition ' namespace Win32 { public enum Logon32Provider { Default = 0, WinNT40 = 2, WinNT50 = 3 } } ' } # Global variable to hold the impersonation context if ($null -eq $Script:ImpersonationContext) { $Script:ImpersonationContext = New-Object -TypeName 'System.Collections.Generic.Stack[System.Security.Principal.WindowsImpersonationContext]' } } function Invoke-AutonanceContainer { [CmdletBinding()] param ( # Autonance container to execute. [Parameter(Mandatory = $true)] [PSTypeName('Autonance.Container')] $Container ) $repeat = $Container.Repeat $repeatCount = 1 do { # Block info if ($repeat) { Write-Autonance '' -Type Info Write-Autonance "$($Container.Type) $($Container.Name) (Repeat: $repeatCount)" -Type Container } else { Write-Autonance '' -Type Info Write-Autonance "$($Container.Type) $($Container.Name)" -Type Container } # It's a container, so increment the level $Script:AutonanceLevel++ # Get all items to execute $items = & $Container.ScriptBlock # Invoke all foreach ($item in $items) { # Inherit the credentials to all sub items if ($null -eq $item.Credential -and $null -ne $Container.Credential) { $item.Credential = $Container.Credential } if ($item.PSTypeNames -contains 'Autonance.Task') { Invoke-AutonanceTask -Task $item } elseif ($item.PSTypeNames -contains 'Autonance.Container') { Invoke-AutonanceContainer -Container $item } else { Write-Warning "Unexpected Autonance task or container object: [$($item.GetType().FullName)] $item" } } # Check repeat if ($repeat) { if ($Container.RepeatCount -ne 0) { if ($repeatCount -ge $Container.RepeatCount) { $repeat = $false } } if ($null -ne $Container.RepeatCondition) { $repeatCondition = & $Container.RepeatCondition if (!$repeatCondition) { $repeat = $false } } if ($Container.RepeatInquire) { # Prepare the choices $repeatInquireChoices = New-Object -TypeName 'Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]' $repeatInquireChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Repeat', 'Repeat all child tasks')) $repeatInquireChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Continue', 'Continue with next task')) # Query the desired choice from the user do { $repeatInquireResult = $Host.UI.PromptForChoice('Repeat', "Do you want to repeat $($Container.Type) $($Container.Name)?", $repeatInquireChoices, -1) } while ($repeatInquireResult -eq -1) # Check the result and quit the execution, if necessary if ($repeatInquireResult -eq 1) { $repeat = $false } } } # Increment repeat count $repeatCount++ # Container has finished, decrement the level $Script:AutonanceLevel-- } while ($repeat) } function Invoke-AutonanceTask { [CmdletBinding()] param ( # Autonance task to execute. [Parameter(Mandatory = $true)] [PSTypeName('Autonance.Task')] $Task ) $retry = $false $retryCount = 0 do { # Block info if ($retryCount -eq 0) { Write-Autonance '' -Type Info Write-Autonance "$($Task.Type) $($Task.Name)" -Type Task } else { Write-Autonance '' -Type Info Write-Autonance "$($Task.Type) $($Task.Name) (Retry: $retryCount)" -Type Task } try { $taskArguments = $task.Arguments $taskScriptBlock = $task.ScriptBlock # If the task supports custom credentials and the credentials were # not explicit specified, set them with the parent task credentials. if ($taskScriptBlock.Ast.ParamBlock.Parameters.Name.VariablePath.UserPath -contains 'Credential') { if ($null -eq $taskArguments.Credential -and $null -ne $Task.Credential) { $taskArguments.Credential = $Task.Credential } } & $taskScriptBlock @taskArguments -ErrorAction 'Stop' $retry = $false } catch { Write-Error $_ # Prepare the retry choices in case of the exception $retryChoices = New-Object -TypeName 'Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]' $retryChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Retry', 'Retry this task')) $retryChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Continue', 'Continue with next task')) $retryChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Stop', 'Stop the maintenance')) # Query the desired choice from the user do { $retryResult = $Host.UI.PromptForChoice('Repeat', "Do you want to retry $($Task.Type) $($Task.Name)?", $retryChoices, -1) } while ($retryResult -eq -1) switch ($retryResult) { 0 { $retry = $true } 1 { $retry = $false } 2 { throw 'Maintenance stopped by user!' } } } $retryCount++ } while ($retry) } function New-AutonanceContainer { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( # The container type, e.g. Maintenance, TaskGroup, etc. [Parameter(Mandatory = $true)] [System.String] $Type, # The container name, which will be shown after the container type. [Parameter(Mandatory = $false)] [System.String] $Name = '', # The credentials, which will be used for all container tasks. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] $Credential = $null, # The script block, which contains the container tasks definitions. [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ScriptBlock, # The processing mode of the container. Currently only sequential mode # is supported. [Parameter(Mandatory = $false)] [ValidateSet('Sequential')] [System.String] $Mode = 'Sequential', # Specifies, if the container tasks should be repeated. [Parameter(Mandatory = $false)] [System.Boolean] $Repeat = $false, # If the container tasks will be repeated, define the number of repeat # loops. Use 0 for an infinite loop. [Parameter(Mandatory = $false)] [System.Int32] $RepeatCount = 0, # If the container tasks will be repeated, define the repeat condition. [Parameter(Mandatory = $false)] [System.Management.Automation.ScriptBlock] $RepeatCondition = $null, # If the container tasks will be repeated, define if the user gets an # inquire for every loop. [Parameter(Mandatory = $false)] [System.Boolean] $RepeatInquire = $false ) # Create and return the container object [PSCustomObject] [Ordered] @{ PSTypeName = 'Autonance.Container' Type = $Type Name = $Name Credential = $Credential ScriptBlock = $ScriptBlock Mode = $Mode Repeat = $Repeat RepeatCount = $RepeatCount RepeatCondition = $RepeatCondition RepeatInquire = $RepeatInquire } } function New-AutonanceSession { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [System.String] $ComputerName, [Parameter(Mandatory = $false)] [AllowNull()] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $true)] [ValidateSet('WinRM', 'CIM')] [System.String] $SessionType, [Parameter(Mandatory = $false)] [switch] $Silent ) # Session splat $sessionSplat = @{} if ($null -ne $Credential) { $sessionSplat['Credential'] = $Credential $messageSuffix = " as $($Credential.UserName)" } if (-not $Silent.IsPresent) { Write-Autonance -Message "Open $SessionType connection to $ComputerName$messageSuffix" } # Create a new session switch ($SessionType) { 'WinRM' { if ($PSCmdlet.ShouldProcess($ComputerName, 'Open WinRM session')) { New-PSSession -ComputerName $ComputerName @sessionSplat -ErrorAction Stop } } 'CIM' { if ($PSCmdlet.ShouldProcess($ComputerName, 'Open CIM session')) { New-CimSession -ComputerName $ComputerName @sessionSplat -ErrorAction Stop } } } } function New-AutonanceTask { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( # The task type, e.g. LocalScript, WindowsComputerReboot, etc. [Parameter(Mandatory = $true)] [System.String] $Type, # The task name, which will be shown after the task type. [Parameter(Mandatory = $false)] [System.String] $Name, # The credentials, which will be used for the task. [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] $Credential, # The script block, which contains the task definition. [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $ScriptBlock, # The task arguments to pass for the task execution. [Parameter(Mandatory = $false)] [System.Collections.Hashtable] $Arguments ) # Create and return the task object [PSCustomObject] [Ordered] @{ PSTypeName = 'Autonance.Task' Type = $Type Name = $Name Credential = $Credential ScriptBlock = $ScriptBlock Arguments = $Arguments } } function Pop-ImpersonationContext { [CmdletBinding()] param () Initialize-ImpersonationContext if ($Script:ImpersonationContext.Count -gt 0) { # Get the latest impersonation context $popImpersonationContext = $Script:ImpersonationContext.Pop() # Undo the impersonation $popImpersonationContext.Undo() } } function Push-ImpersonationContext { [CmdletBinding()] param ( # Specifies a user account to impersonate. [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential, # The logon type. [Parameter(Mandatory = $false)] [ValidateSet('Interactive', 'Network', 'Batch', 'Service', 'Unlock', 'NetworkClearText', 'NewCredentials')] $LogonType = 'Interactive', # The logon provider. [Parameter(Mandatory = $false)] [ValidateSet('Default', 'WinNT40', 'WinNT50')] $LogonProvider = 'Default' ) Initialize-ImpersonationContext # Handle for the logon token $tokenHandle = [IntPtr]::Zero # Now logon the user account on the local system $logonResult = [Win32.AdvApi32]::LogonUser($Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Domain, $Credential.GetNetworkCredential().Password, ([Win32.Logon32Type] $LogonType), ([Win32.Logon32Provider] $LogonProvider), [ref] $tokenHandle) # Error handling, if the logon fails if (-not $logonResult) { $errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() throw "Failed to call LogonUser() throwing Win32 exception with error code: $errorCode" } # Now, impersonate the new user account $newImpersonationContext = [System.Security.Principal.WindowsIdentity]::Impersonate($tokenHandle) $Script:ImpersonationContext.Push($newImpersonationContext) # Finally, close the handle to the token [Win32.Kernel32]::CloseHandle($tokenHandle) | Out-Null } function Read-HostMultipleChoiceSelection { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param ( [Parameter(Mandatory = $true)] [System.String] $Caption, [Parameter(Mandatory = $true)] [System.String] $Message, [Parameter(Mandatory = $true)] [System.Object[]] $ChoiceObject, [Parameter(Mandatory = $true)] [System.String[]] $ChoiceLabel ) if ($ChoiceObject.Count -ne $ChoiceLabel.Count) { throw 'ChoiceObject and ChoiceLabel item count do not match.' } Write-Host '' Write-Host $Caption Write-Host $Message for ($i = 0; $i -lt $ChoiceLabel.Count; $i++) { Write-Host ('[{0:00}] {1}' -f ($i + 1), $ChoiceLabel[$i]) } Write-Host '(input comma-separated choices or * for all)' do { $rawInputs = Read-Host -Prompt 'Choice' } while ([String]::IsNullOrWhiteSpace($rawInputs)) if ($rawInputs -eq '*') { Write-Output $ChoiceObject } else { foreach ($rawInput in $rawInputs.Split(',')) { try { $rawNumber = [Int32]::Parse($rawInput) $rawNumber-- if ($rawNumber -ge 0 -and $rawNumber -lt $ChoiceLabel.Count) { Write-Output $ChoiceObject[$rawNumber] } else { throw } } catch { Write-Warning "Unable to parse input '$rawInput'" } } } } function Remove-AutonanceSession { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $false)] [AllowNull()] [System.Object] $Session, [Parameter(Mandatory = $false)] [switch] $Silent ) # Remove existing session if ($null -ne $Session) { if ($Session -is [System.Management.Automation.Runspaces.PSSession]) { if (-not $Silent.IsPresent) { Write-Autonance -Message "Close WinRM connection to $ComputerName" } if ($PSCmdlet.ShouldProcess($Session, 'Close WinRM session')) { $Session | Remove-PSSession -ErrorAction SilentlyContinue } } if ($session -is [Microsoft.Management.Infrastructure.CimSession]) { if (-not $Silent.IsPresent) { Write-Autonance -Message "Close CIM connection to $ComputerName" } if ($PSCmdlet.ShouldProcess($Session, 'Close COM session')) { $Session | Remove-CimSession -ErrorAction SilentlyContinue } } } } function Wait-AutonanceTask { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $Activity, [Parameter(Mandatory = $true)] [System.Management.Automation.ScriptBlock] $Condition, [Parameter(Mandatory = $true)] [System.Int32] $Count, [Parameter(Mandatory = $true)] [System.Int32] $Delay ) for ($i = 1; $i -le $Count; $i++) { Write-Progress -Activity $Activity -Status "$i / $Count" -PercentComplete ($i / $Count * 100) -Verbose # Record the timestamp before the condition query $timestamp = Get-Date # Evaluate the condition and exit the loop, if the result is true $result = & $Condition if ($result) { Write-Progress -Activity $Activity -Completed return } # Calculate the remaining sleep duration $duration = (Get-Date) - $timestamp $leftover = $Delay - $duration.TotalSeconds # Sleep if required if ($leftover -gt 0) { Start-Sleep -Seconds $leftover } } Write-Progress -Activity $Activity -Completed throw "Timeout: $Activity" } function Write-Autonance { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] param ( [Parameter(Mandatory = $true, Position = 0)] [AllowEmptyString()] [System.String] $Message, [Parameter(Mandatory = $false)] [ValidateSet('Container', 'Task', 'Action', 'Info')] [System.String] $Type = 'Action' ) if (!$Script:AutonanceSilent) { $messageLines = $Message.Split("`n") if ($Script:AutonanceBlock) { $colorSplat = @{} switch ($Type) { 'Container' { $prefixFirst = ' ' * $Script:AutonanceLevel $prefixOther = ' ' * $Script:AutonanceLevel $colorSplat['ForegroundColor'] = 'Magenta' } 'Task' { $prefixFirst = ' ' * $Script:AutonanceLevel $prefixOther = ' ' * $Script:AutonanceLevel $colorSplat['ForegroundColor'] = 'Magenta' } 'Action' { $prefixFirst = ' ' * $Script:AutonanceLevel + ' - ' $prefixOther = ' ' * $Script:AutonanceLevel + ' ' $colorSplat['ForegroundColor'] = 'Cyan' } 'Info' { $prefixFirst = ' ' * $Script:AutonanceLevel $prefixOther = ' ' * $Script:AutonanceLevel } } $messageLines = "$prefixFirst$($messageLines -join "`n$prefixOther")".Split("`n") if ($Type -eq 'Info') { $messageLines | Write-Host } else { for ($i = 0; $i -lt $messageLines.Count; $i++) { if ($null -eq $Script:AutonanceTimestamp) { $Script:AutonanceTimestamp = Get-Date $timestamp = '{0:dd.MM.yyyy HH:mm:ss} 00:00:00' -f $Script:AutonanceTimestamp } else { $timestamp = ((Get-Date) - $Script:AutonanceTimestamp).ToString('hh\:mm\:ss') } $timestampWidth = $Host.UI.RawUI.WindowSize.Width - $messageLines[$i].Length - 4 if ($i -eq 0 -and $timestampWidth -ge $timestamp.Length) { Write-Host -Object $messageLines[$i] @colorSplat -NoNewline Write-Host -Object (" {0,$timestampWidth}" -f $timestamp) -ForegroundColor 'DarkGray' } else { Write-Host -Object $messageLines[$i] @colorSplat } } } } else { foreach ($messageLine in $messageLines) { Write-Verbose $messageLine } } } } # Initialize context variables $Script:AutonanceBlock = $false $Script:AutonanceLevel = 0 $Script:AutonanceSilent = $false $Script:AutonanceExtension = @{} $Script:AutonanceTimestamp = $null $Script:ModulePath = $PSScriptRoot |