Public/Set-StmClusteredScheduledTask.ps1
|
function Set-StmClusteredScheduledTask { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '', Justification = 'Mirrors native scheduled task cmdlet interface which uses User/Password strings')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'Mirrors native scheduled task cmdlet interface which uses plain text password')] <# .SYNOPSIS Modifies a clustered scheduled task in a Windows failover cluster. .DESCRIPTION The Set-StmClusteredScheduledTask function modifies the properties of a clustered scheduled task in a Windows failover cluster. Since there is no native Set-ClusteredScheduledTask cmdlet, this function exports the current task configuration, modifies it, and re-registers the task. The function can modify the following task properties: - Actions: The commands or programs the task executes - Triggers: The schedules that determine when the task runs - Settings: Task configuration options like run behavior and power management - Principal: The security context under which the task runs - TaskType: The cluster task type (ResourceSpecific, AnyNode, ClusterWide) The function performs the following operations: 1. Exports the current task configuration using Export-StmClusteredScheduledTask 2. Modifies the XML configuration based on provided parameters 3. Retrieves the original task type if not specified 4. Unregisters the current task 5. Re-registers the task with the modified configuration At least one modification parameter (Action, Trigger, Settings, Principal, User, Password, or TaskType) must be specified. This function requires appropriate permissions to manage clustered scheduled tasks. .PARAMETER TaskName Specifies the name of the clustered scheduled task to modify. This parameter is mandatory and must match the exact name of the task as it appears in the cluster. .PARAMETER InputObject Specifies a clustered scheduled task object to modify. This parameter accepts pipeline input from Get-StmClusteredScheduledTask. When using this parameter, the TaskName is extracted from the object. .PARAMETER Cluster Specifies the name or FQDN of the cluster where the scheduled task is located. This parameter is mandatory and must be a valid Windows failover cluster. .PARAMETER Action Specifies an array of action objects that define what the task executes. Use New-ScheduledTaskAction to create action objects. When specified, this replaces all existing actions on the task. .PARAMETER Trigger Specifies an array of trigger objects that define when the task runs. Use New-ScheduledTaskTrigger to create trigger objects. When specified, this replaces all existing triggers on the task. .PARAMETER Settings Specifies a settings object that defines task behavior. Use New-ScheduledTaskSettingsSet to create a settings object. When specified, this merges with existing task settings. .PARAMETER Principal Specifies a principal object that defines the security context for the task. Use New-ScheduledTaskPrincipal to create a principal object. This parameter cannot be used together with User or Password parameters. .PARAMETER User Specifies the user account under which the task runs. This is an alternative to using the Principal parameter. Cannot be used together with the Principal parameter. .PARAMETER Password Specifies the password for the user account specified by the User parameter. This is an alternative to using the Principal parameter. Cannot be used together with the Principal parameter. .PARAMETER TaskType Specifies the cluster task type. Valid values are: - ResourceSpecific: Task runs on a specific cluster resource - AnyNode: Task can run on any cluster node - ClusterWide: Task runs on all cluster nodes .PARAMETER Credential Specifies credentials to use when connecting to the cluster. If not specified, the current user's credentials are used for the connection. .PARAMETER PassThru Returns an object representing the modified clustered scheduled task. By default, this cmdlet does not generate any output. .EXAMPLE $action = New-ScheduledTaskAction -Execute 'PowerShell.exe' -Argument '-File C:\Scripts\Backup.ps1' Set-StmClusteredScheduledTask -TaskName 'ClusterBackup' -Cluster 'MyCluster' -Action $action Modifies the action of the clustered scheduled task named "ClusterBackup" to run a different PowerShell script. .EXAMPLE $trigger = New-ScheduledTaskTrigger -Daily -At '3:00 AM' Set-StmClusteredScheduledTask -TaskName 'MaintenanceTask' -Cluster 'ProdCluster' -Trigger $trigger Modifies the trigger of the clustered scheduled task to run daily at 3:00 AM. .EXAMPLE $settings = New-ScheduledTaskSettingsSet -RunOnlyIfNetworkAvailable -WakeToRun $credential = Get-Credential $params = @{ TaskName = 'SyncTask' Cluster = 'MyCluster' Settings = $settings Credential = $credential } Set-StmClusteredScheduledTask @params Modifies the settings of the clustered scheduled task using specified credentials for the cluster connection. .EXAMPLE Get-StmClusteredScheduledTask -TaskName 'ReportTask' -Cluster 'MyCluster' | Set-StmClusteredScheduledTask -Cluster 'MyCluster' -User 'DOMAIN\ServiceAccount' -Password 'P@ssw0rd' Uses pipeline input to modify the user account under which the clustered task runs. .EXAMPLE Set-StmClusteredScheduledTask -TaskName 'FlexibleTask' -Cluster 'MyCluster' -TaskType 'AnyNode' Changes the task type of a clustered scheduled task to run on any available node. .INPUTS Microsoft.Management.Infrastructure.CimInstance You can pipe a clustered scheduled task object from Get-StmClusteredScheduledTask to this cmdlet. .OUTPUTS None or System.Object When you use the PassThru parameter, this cmdlet returns the modified task object. Otherwise, this cmdlet does not generate any output. .NOTES This function requires: - PowerShell remoting to be enabled on the target cluster - The FailoverClusters PowerShell module to be installed on the target cluster - Appropriate permissions to manage clustered scheduled tasks - Network connectivity to the cluster on the WinRM ports (default 5985/5986) The function performs a complete re-registration of the task, which involves: - Exporting the current task configuration - Modifying the configuration based on parameters - Unregistering the current task - Re-registering the task with the new configuration This operation temporarily removes the task from the cluster during the re-registration process. The task will be unavailable for execution during this brief period. At least one modification parameter (Action, Trigger, Settings, Principal, User, Password, or TaskType) must be specified. The Principal parameter cannot be combined with User or Password. The function supports the -WhatIf and -Confirm parameters for safe operation in automated environments. .LINK Get-StmClusteredScheduledTask .LINK Export-StmClusteredScheduledTask .LINK Register-StmClusteredScheduledTask .LINK Unregister-StmClusteredScheduledTask #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = 'ByName')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'ByName', ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [string] $TaskName, [Parameter(Mandatory = $true, ParameterSetName = 'ByInputObject', ValueFromPipeline = $true)] [ValidateNotNull()] [Microsoft.Management.Infrastructure.CimInstance] $InputObject, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Cluster, [Parameter(Mandatory = $false)] [Microsoft.Management.Infrastructure.CimInstance[]] $Action, [Parameter(Mandatory = $false)] [Microsoft.Management.Infrastructure.CimInstance[]] $Trigger, [Parameter(Mandatory = $false)] [Microsoft.Management.Infrastructure.CimInstance] $Settings, [Parameter(Mandatory = $false)] [Microsoft.Management.Infrastructure.CimInstance] $Principal, [Parameter(Mandatory = $false)] [string] $User, [Parameter(Mandatory = $false)] [string] $Password, [Parameter(Mandatory = $false)] [Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.ClusterTaskTypeEnum] $TaskType, [Parameter(Mandatory = $false)] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [switch] $PassThru ) begin { Write-Verbose 'Starting Set-StmClusteredScheduledTask' # Validate mutually exclusive parameters (Principal vs User/Password) if ($PSBoundParameters.ContainsKey('Principal') -and ($PSBoundParameters.ContainsKey('User') -or $PSBoundParameters.ContainsKey('Password'))) { $errorMsg = 'The Principal parameter cannot be used with User or Password parameters.' $errorRecordParameters = @{ Exception = [System.ArgumentException]::new($errorMsg) ErrorId = 'InvalidParameterCombination' ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument TargetObject = $null Message = $errorMsg RecommendedAction = 'Use either Principal or User/Password, not both.' } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } # Validate that at least one modification parameter is specified $modificationParams = @('Action', 'Trigger', 'Settings', 'Principal', 'User', 'Password', 'TaskType') $hasModification = $modificationParams | Where-Object { $PSBoundParameters.ContainsKey($_) } if (-not $hasModification) { $errorMsg = @( 'At least one task property (Action, Trigger, Settings, Principal,' 'User, Password, or TaskType) must be specified.' ) -join ' ' $errorRecordParameters = @{ Exception = [System.ArgumentException]::new($errorMsg) ErrorId = 'NoModificationSpecified' ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument TargetObject = $null Message = $errorMsg RecommendedAction = 'Specify at least one property to modify.' } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } } process { try { # Determine task name based on parameter set if ($PSCmdlet.ParameterSetName -eq 'ByInputObject') { $effectiveTaskName = $InputObject.TaskName Write-Verbose "Processing task from pipeline: '$effectiveTaskName'" } else { $effectiveTaskName = $TaskName } $target = "clustered task '$effectiveTaskName' on cluster '$Cluster'" $operation = 'Set clustered scheduled task properties' Write-Verbose "Exporting clustered scheduled task '$effectiveTaskName'..." $exportParams = @{ TaskName = $effectiveTaskName Cluster = $Cluster Credential = $Credential } $taskXml = Export-StmClusteredScheduledTask @exportParams if (-not $taskXml) { $errorMsg = "Failed to export current configuration for task '$effectiveTaskName'." $errorRecordParameters = @{ Exception = [System.InvalidOperationException]::new($errorMsg) ErrorId = 'ClusteredTaskExportFailed' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $effectiveTaskName Message = $errorMsg RecommendedAction = 'Verify the task name is correct and that the task exists on the cluster.' } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } # Get current task to retrieve TaskType if not specified Write-Verbose 'Retrieving current task information...' $getTaskParams = @{ TaskName = $effectiveTaskName Cluster = $Cluster Credential = $Credential } $currentTask = Get-StmClusteredScheduledTask @getTaskParams $effectiveTaskType = if ($PSBoundParameters.ContainsKey('TaskType')) { $TaskType } else { $currentTask.ClusteredScheduledTaskObject.TaskType } # Load XML for modification [xml]$taskXmlDocument = $taskXml # Modify Actions if specified if ($PSBoundParameters.ContainsKey('Action')) { Write-Verbose 'Modifying Actions in task XML...' $actionsNode = $taskXmlDocument.Task.Actions # Clear existing Exec actions using simple child node enumeration $execActions = @($actionsNode.ChildNodes | Where-Object { $_.LocalName -eq 'Exec' }) foreach ($exec in $execActions) { $actionsNode.RemoveChild($exec) | Out-Null } # Add new actions foreach ($act in $Action) { $ns = $taskXmlDocument.DocumentElement.NamespaceURI $execElement = $taskXmlDocument.CreateElement('Exec', $ns) $cmdElement = $taskXmlDocument.CreateElement('Command', $ns) $cmdElement.InnerText = $act.Execute $execElement.AppendChild($cmdElement) | Out-Null if ($act.Arguments) { $argsElement = $taskXmlDocument.CreateElement('Arguments', $ns) $argsElement.InnerText = $act.Arguments $execElement.AppendChild($argsElement) | Out-Null } if ($act.WorkingDirectory) { $wdElement = $taskXmlDocument.CreateElement('WorkingDirectory', $ns) $wdElement.InnerText = $act.WorkingDirectory $execElement.AppendChild($wdElement) | Out-Null } $actionsNode.AppendChild($execElement) | Out-Null } } # Modify Triggers if specified if ($PSBoundParameters.ContainsKey('Trigger')) { Write-Verbose 'Modifying Triggers in task XML...' $triggersNode = $taskXmlDocument.Task.Triggers # Clear existing triggers $triggersNode.RemoveAll() # Add new triggers based on type foreach ($trig in $Trigger) { $triggerElement = $null $triggerType = $trig.CimClass.CimClassName $ns = $taskXmlDocument.DocumentElement.NamespaceURI switch -Wildcard ($triggerType) { '*Daily*' { $triggerElement = $taskXmlDocument.CreateElement('CalendarTrigger', $ns) $schedByDay = $taskXmlDocument.CreateElement('ScheduleByDay', $ns) $daysInterval = $taskXmlDocument.CreateElement('DaysInterval', $ns) $daysInterval.InnerText = if ($trig.DaysInterval) { $trig.DaysInterval } else { '1' } $schedByDay.AppendChild($daysInterval) | Out-Null $triggerElement.AppendChild($schedByDay) | Out-Null } '*Weekly*' { $triggerElement = $taskXmlDocument.CreateElement('CalendarTrigger', $ns) $schedByWeek = $taskXmlDocument.CreateElement('ScheduleByWeek', $ns) $triggerElement.AppendChild($schedByWeek) | Out-Null } '*Once*' { $triggerElement = $taskXmlDocument.CreateElement('TimeTrigger', $ns) } '*Logon*' { $triggerElement = $taskXmlDocument.CreateElement('LogonTrigger', $ns) } '*Boot*' { $triggerElement = $taskXmlDocument.CreateElement('BootTrigger', $ns) } default { $triggerElement = $taskXmlDocument.CreateElement('TimeTrigger', $ns) } } if ($triggerElement) { # Add start boundary if available if ($trig.StartBoundary) { $startBoundary = $taskXmlDocument.CreateElement('StartBoundary', $ns) $startBoundary.InnerText = $trig.StartBoundary $triggerElement.PrependChild($startBoundary) | Out-Null } # Add enabled status $enabled = $taskXmlDocument.CreateElement('Enabled', $ns) $enabled.InnerText = if ($trig.Enabled -eq $false) { 'false' } else { 'true' } $triggerElement.AppendChild($enabled) | Out-Null $triggersNode.AppendChild($triggerElement) | Out-Null } } } # Modify Settings if specified if ($PSBoundParameters.ContainsKey('Settings')) { Write-Verbose 'Modifying Settings in task XML...' $settingsNode = $taskXmlDocument.Task.Settings $ns = $taskXmlDocument.DocumentElement.NamespaceURI # Map common settings properties to XML elements $settingsMap = @{ 'AllowDemandStart' = 'AllowStartOnDemand' 'AllowHardTerminate' = 'AllowHardTerminate' 'DisallowStartIfOnBatteries' = 'DisallowStartIfOnBatteries' 'StopIfGoingOnBatteries' = 'StopIfGoingOnBatteries' 'Hidden' = 'Hidden' 'RunOnlyIfNetworkAvailable' = 'RunOnlyIfNetworkAvailable' 'Enabled' = 'Enabled' 'WakeToRun' = 'WakeToRun' 'RunOnlyIfIdle' = 'RunOnlyIfIdle' 'StartWhenAvailable' = 'StartWhenAvailable' 'DisallowStartOnRemoteAppSession' = 'DisallowStartOnRemoteAppSession' 'UseUnifiedSchedulingEngine' = 'UseUnifiedSchedulingEngine' } foreach ($prop in $settingsMap.Keys) { $value = $Settings.$prop if ($null -ne $value) { $xmlProp = $settingsMap[$prop] $existingNode = $settingsNode.SelectSingleNode($xmlProp) if ($existingNode) { $existingNode.InnerText = $value.ToString().ToLower() } else { $newNode = $taskXmlDocument.CreateElement($xmlProp, $ns) $newNode.InnerText = $value.ToString().ToLower() $settingsNode.AppendChild($newNode) | Out-Null } } } # Handle Priority if ($null -ne $Settings.Priority) { $priorityNode = $settingsNode.SelectSingleNode('Priority') if ($priorityNode) { $priorityNode.InnerText = $Settings.Priority.ToString() } else { $newNode = $taskXmlDocument.CreateElement('Priority', $ns) $newNode.InnerText = $Settings.Priority.ToString() $settingsNode.AppendChild($newNode) | Out-Null } } # Handle ExecutionTimeLimit if ($Settings.ExecutionTimeLimit) { $limitNode = $settingsNode.SelectSingleNode('ExecutionTimeLimit') if ($limitNode) { $limitNode.InnerText = $Settings.ExecutionTimeLimit.ToString() } else { $newNode = $taskXmlDocument.CreateElement('ExecutionTimeLimit', $ns) $newNode.InnerText = $Settings.ExecutionTimeLimit.ToString() $settingsNode.AppendChild($newNode) | Out-Null } } } # Modify Principal if specified if ($PSBoundParameters.ContainsKey('Principal')) { Write-Verbose 'Modifying Principal in task XML...' $principalsNode = $taskXmlDocument.Task.Principals $principalNode = $principalsNode.Principal $ns = $taskXmlDocument.DocumentElement.NamespaceURI if ($Principal.UserId) { $userIdNode = $principalNode.SelectSingleNode('UserId') if ($userIdNode) { $userIdNode.InnerText = $Principal.UserId } else { $newNode = $taskXmlDocument.CreateElement('UserId', $ns) $newNode.InnerText = $Principal.UserId $principalNode.AppendChild($newNode) | Out-Null } } if ($Principal.LogonType) { $logonTypeNode = $principalNode.SelectSingleNode('LogonType') $logonTypeValue = switch ($Principal.LogonType) { 'Password' { 'Password' } 'S4U' { 'S4U' } 'Interactive' { 'InteractiveToken' } 'InteractiveOrPassword' { 'InteractiveTokenOrPassword' } 'ServiceAccount' { 'ServiceAccount' } default { $Principal.LogonType.ToString() } } if ($logonTypeNode) { $logonTypeNode.InnerText = $logonTypeValue } else { $newNode = $taskXmlDocument.CreateElement('LogonType', $ns) $newNode.InnerText = $logonTypeValue $principalNode.AppendChild($newNode) | Out-Null } } if ($Principal.RunLevel) { $runLevelNode = $principalNode.SelectSingleNode('RunLevel') $runLevelValue = switch ($Principal.RunLevel) { 'Highest' { 'HighestAvailable' } 'Limited' { 'LeastPrivilege' } default { $Principal.RunLevel.ToString() } } if ($runLevelNode) { $runLevelNode.InnerText = $runLevelValue } else { $newNode = $taskXmlDocument.CreateElement('RunLevel', $ns) $newNode.InnerText = $runLevelValue $principalNode.AppendChild($newNode) | Out-Null } } } # Modify User/Password if specified if ($PSBoundParameters.ContainsKey('User')) { Write-Verbose 'Modifying User in task XML...' $principalsNode = $taskXmlDocument.Task.Principals $principalNode = $principalsNode.Principal $ns = $taskXmlDocument.DocumentElement.NamespaceURI $userIdNode = $principalNode.SelectSingleNode('UserId') if ($userIdNode) { $userIdNode.InnerText = $User } else { $newNode = $taskXmlDocument.CreateElement('UserId', $ns) $newNode.InnerText = $User $principalNode.AppendChild($newNode) | Out-Null } # If password is provided, set LogonType to Password if ($PSBoundParameters.ContainsKey('Password')) { $logonTypeNode = $principalNode.SelectSingleNode('LogonType') if ($logonTypeNode) { $logonTypeNode.InnerText = 'Password' } else { $newNode = $taskXmlDocument.CreateElement('LogonType', $ns) $newNode.InnerText = 'Password' $principalNode.AppendChild($newNode) | Out-Null } } } $modifiedXml = $taskXmlDocument.OuterXml if ($PSCmdlet.ShouldProcess($target, $operation)) { Write-Verbose "Unregistering clustered scheduled task '$effectiveTaskName'..." $cimSessionParams = @{ ComputerName = $Cluster Credential = $Credential } $cimSession = New-StmCimSession @cimSessionParams $unregisterParams = @{ TaskName = $effectiveTaskName CimSession = $cimSession ErrorAction = 'Stop' } Unregister-ClusteredScheduledTask @unregisterParams $msg = "Re-registering clustered task '$effectiveTaskName'..." Write-Verbose $msg $registerParams = @{ TaskName = $effectiveTaskName Cluster = $Cluster Xml = $modifiedXml TaskType = $effectiveTaskType Credential = $Credential } $null = Register-StmClusteredScheduledTask @registerParams $successMsg = "Clustered scheduled task '$effectiveTaskName' has been successfully modified." Write-Verbose $successMsg if ($PassThru) { # Return the updated task $getTaskParams = @{ TaskName = $effectiveTaskName Cluster = $Cluster Credential = $Credential } Write-Output (Get-StmClusteredScheduledTask @getTaskParams) } } else { Write-Verbose 'Operation cancelled by user.' } } catch { $errorRecordParameters = @{ Exception = $_.Exception ErrorId = 'ClusteredScheduledTaskSetFailed' ErrorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified TargetObject = $effectiveTaskName Message = ( "Failed to modify clustered scheduled task '$effectiveTaskName' on cluster '$Cluster'. {$_}" ) RecommendedAction = ( 'Verify the task name is correct, that the task exists, that the provided ' + 'Action/Trigger/Settings/Principal objects are valid, and that you have permission to ' + 'manage clustered scheduled tasks.' ) } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } } end { Write-Verbose "Completed Set-StmClusteredScheduledTask" } } |