Public/Import-StmClusteredScheduledTask.ps1
|
function Import-StmClusteredScheduledTask { <# .SYNOPSIS Imports clustered scheduled tasks from XML to a Windows failover cluster. .DESCRIPTION The Import-StmClusteredScheduledTask function imports clustered scheduled tasks from XML definitions to a Windows failover cluster. This function supports three modes of operation: - Single file import: Import a task from a single XML file using the -Path parameter - XML string import: Import a task from an XML string using the -Xml parameter - Bulk directory import: Import all XML files from a directory using the -DirectoryPath parameter The function extracts the task name from the XML's RegistrationInfo/URI element by default, but allows overriding the task name for single imports using the -TaskName parameter. When a task with the same name already exists, the function will error unless the -Force parameter is specified, which causes the existing task to be unregistered before importing the new one. For bulk directory imports, the function processes all .xml files in the specified directory, reports progress, and continues processing even if individual tasks fail to import. A summary object is returned with details about successful and failed imports. .PARAMETER Path Specifies the path to a single XML file containing the scheduled task definition. The file must exist and contain valid Task Scheduler XML format. This parameter is mandatory when using the XmlFile parameter set. .PARAMETER Xml Specifies the XML content defining the scheduled task configuration. The XML should follow the Task Scheduler schema format. This parameter is mandatory when using the XmlString parameter set. .PARAMETER DirectoryPath Specifies the path to a directory containing XML files to import. All files with the .xml extension in the directory will be processed. This parameter is mandatory when using the Directory parameter set. The -TaskName parameter cannot be used with this parameter. .PARAMETER Cluster Specifies the name or FQDN of the cluster where the tasks will be registered. This parameter is mandatory for all parameter sets. .PARAMETER TaskType Specifies the type of clustered scheduled task to register. Valid values are: - ResourceSpecific: Task runs on a specific cluster resource - AnyNode: Task can run on any node in the cluster - ClusterWide: Task runs across the entire cluster This parameter is mandatory for all parameter sets. .PARAMETER TaskName Optionally overrides the task name extracted from the XML's RegistrationInfo/URI element. This parameter is only applicable to single file or XML string imports and cannot be used with the -DirectoryPath parameter. .PARAMETER Credential Specifies credentials to use when connecting to the cluster. If not provided, the current user's credentials will be used for the connection. .PARAMETER Force Overwrites existing tasks with the same name. Without this parameter, an error occurs if a task with the same name already exists on the cluster. When specified, the existing task is unregistered before importing the new task definition. .EXAMPLE Import-StmClusteredScheduledTask -Path 'C:\Tasks\BackupTask.xml' -Cluster 'MyCluster' -TaskType 'AnyNode' Imports a single clustered scheduled task from an XML file. The task name is extracted from the XML's URI element. .EXAMPLE $params = @{ Path = 'C:\Tasks\Task.xml' Cluster = 'MyCluster' TaskType = 'AnyNode' TaskName = 'CustomName' } Import-StmClusteredScheduledTask @params Imports a clustered scheduled task from an XML file, overriding the task name with 'CustomName'. .EXAMPLE Import-StmClusteredScheduledTask -DirectoryPath 'C:\Tasks\' -Cluster 'MyCluster' -TaskType 'ClusterWide' -Force Imports all XML files from the specified directory as clustered scheduled tasks. The -Force parameter ensures any existing tasks with the same names are replaced. .EXAMPLE $xml = Get-Content -Path 'C:\Tasks\Task.xml' -Raw Import-StmClusteredScheduledTask -Xml $xml -Cluster 'MyCluster' -TaskType 'AnyNode' Imports a clustered scheduled task from an XML string variable. .EXAMPLE $credential = Get-Credential $params = @{ Path = 'C:\Tasks\Task.xml' Cluster = 'MyCluster.contoso.com' TaskType = 'ResourceSpecific' Credential = $credential Force = $true } Import-StmClusteredScheduledTask @params Imports a clustered scheduled task using specified credentials, replacing any existing task with the same name. .INPUTS None. You cannot pipe objects to Import-StmClusteredScheduledTask. .OUTPUTS Microsoft.Management.Infrastructure.CimInstance#MSFT_ClusteredScheduledTask For single file or XML string imports, returns the registered clustered scheduled task object. PSCustomObject For directory imports, returns a summary object with the following properties: - TotalFiles: The total number of XML files found - SuccessCount: The number of successfully imported tasks - FailureCount: The number of tasks that failed to import - ImportedTasks: Array of successfully imported task names - FailedTasks: Array of objects describing failed imports (FileName, TaskName, Error) .NOTES This function requires: - The FailoverClusters PowerShell module to be installed on the target cluster - Appropriate permissions to register clustered scheduled tasks - Network connectivity to the cluster - Valid Task Scheduler XML format for the task definitions The XML definitions must follow the Task Scheduler schema and should contain a RegistrationInfo/URI element for automatic task name extraction. If the URI element is missing and -TaskName is not specified, the import will fail. When importing from a directory, the function uses non-terminating errors for individual task failures, allowing the import to continue with remaining files. Check the returned summary object for details about any failures. .LINK Export-StmClusteredScheduledTask .LINK Register-StmClusteredScheduledTask .LINK Unregister-StmClusteredScheduledTask .LINK Get-StmClusteredScheduledTask #> [CmdletBinding(DefaultParameterSetName = 'XmlFile', SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, ParameterSetName = 'XmlFile')] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter(Mandatory = $true, ParameterSetName = 'XmlString')] [ValidateNotNullOrEmpty()] [string] $Xml, [Parameter(Mandatory = $true, ParameterSetName = 'Directory')] [ValidateNotNullOrEmpty()] [string] $DirectoryPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Cluster, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.ClusterTaskTypeEnum] $TaskType, [Parameter(Mandatory = $false, ParameterSetName = 'XmlFile')] [Parameter(Mandatory = $false, ParameterSetName = 'XmlString')] [ValidateNotNullOrEmpty()] [string] $TaskName, [Parameter(Mandatory = $false)] [ValidateNotNull()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [switch] $Force ) begin { Write-Verbose "Starting Import-StmClusteredScheduledTask on cluster '$Cluster'" # Validate that TaskName is not used with Directory parameter set if ($PSCmdlet.ParameterSetName -eq 'Directory' -and $PSBoundParameters.ContainsKey('TaskName')) { $errorMsg = ( 'The -TaskName parameter cannot be used with -DirectoryPath. ' + 'Task names are extracted from each XML file.' ) $errorRecordParameters = @{ Exception = [System.ArgumentException]::new($errorMsg) ErrorId = 'InvalidParameterCombination' ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument TargetObject = $DirectoryPath Message = $errorMsg RecommendedAction = ( 'Remove the -TaskName parameter when using -DirectoryPath, ' + 'or use -Path for single file import with a custom task name.' ) } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } # Validate file/directory exists based on parameter set if ($PSCmdlet.ParameterSetName -eq 'XmlFile') { if (-not (Test-Path -Path $Path -PathType Leaf)) { $exceptionMessage = "The file '$Path' was not found." $errorRecordParameters = @{ Exception = [System.IO.FileNotFoundException]::new($exceptionMessage) ErrorId = 'XmlFileNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $Path Message = "The XML file '$Path' does not exist or is not accessible." RecommendedAction = 'Verify the file path is correct and the file exists.' } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } } elseif ($PSCmdlet.ParameterSetName -eq 'Directory') { if (-not (Test-Path -Path $DirectoryPath -PathType Container)) { $exceptionMessage = "The directory '$DirectoryPath' was not found." $errorRecordParameters = @{ Exception = [System.IO.DirectoryNotFoundException]::new($exceptionMessage) ErrorId = 'DirectoryNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::ObjectNotFound TargetObject = $DirectoryPath Message = ( "The directory '$DirectoryPath' does not exist or is not accessible." ) RecommendedAction = ( 'Verify the directory path is correct and the directory exists.' ) } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } } } process { switch ($PSCmdlet.ParameterSetName) { 'XmlFile' { Write-Verbose "Importing task from file: $Path" try { $xmlContent = Get-Content -Path $Path -Raw -ErrorAction 'Stop' } catch { $errorRecordParameters = @{ Exception = $_.Exception ErrorId = 'XmlFileReadFailed' ErrorCategory = [System.Management.Automation.ErrorCategory]::ReadError TargetObject = $Path Message = "Failed to read XML file '$Path'. $($_.Exception.Message)" RecommendedAction = 'Verify you have read permissions for the file and the file is not locked.' } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } Import-SingleTask -XmlContent $xmlContent -Cluster $Cluster -TaskType $TaskType ` -TaskNameOverride $TaskName -Credential $Credential -Force:$Force } 'XmlString' { Write-Verbose 'Importing task from XML string' Import-SingleTask -XmlContent $Xml -Cluster $Cluster -TaskType $TaskType ` -TaskNameOverride $TaskName -Credential $Credential -Force:$Force } 'Directory' { Write-Verbose "Importing tasks from directory: $DirectoryPath" $xmlFiles = Get-ChildItem -Path $DirectoryPath -Filter '*.xml' -File $totalFiles = $xmlFiles.Count if ($totalFiles -eq 0) { Write-Warning "No XML files found in directory '$DirectoryPath'" return [PSCustomObject]@{ TotalFiles = 0 SuccessCount = 0 FailureCount = 0 ImportedTasks = @() FailedTasks = @() } } Write-Verbose "Found $totalFiles XML file(s) to import" $activity = "Importing clustered scheduled tasks from '$DirectoryPath'" $successCount = 0 $importedTasks = [System.Collections.Generic.List[string]]::new() $failedTasks = [System.Collections.Generic.List[PSCustomObject]]::new() for ($i = 0; $i -lt $totalFiles; $i++) { $xmlFile = $xmlFiles[$i] $percentComplete = [math]::Round((($i + 1) / $totalFiles) * 100) $status = "Processing file $($i + 1) of $totalFiles`: $($xmlFile.Name)" Write-Progress -Activity $activity -Status $status -PercentComplete $percentComplete $extractedName = $null try { $xmlContent = Get-Content -Path $xmlFile.FullName -Raw -ErrorAction 'Stop' $extractedName = Get-TaskNameFromXml -XmlContent $xmlContent if ([string]::IsNullOrWhiteSpace($extractedName)) { throw ( 'Could not extract task name from XML. ' + 'The RegistrationInfo/URI element is missing or empty.' ) } $importParams = @{ XmlContent = $xmlContent Cluster = $Cluster TaskType = $TaskType Credential = $Credential Force = $Force ErrorAction = 'Stop' } $null = Import-SingleTask @importParams $successCount++ $importedTasks.Add($extractedName) Write-Verbose "Successfully imported task '$extractedName' from '$($xmlFile.Name)'" } catch { $failedTask = [PSCustomObject]@{ FileName = $xmlFile.Name TaskName = $extractedName Error = $_.Exception.Message } $failedTasks.Add($failedTask) Write-Warning "Failed to import task from '$($xmlFile.Name)': $($_.Exception.Message)" } } Write-Progress -Activity $activity -Completed Write-Verbose "Import completed: $successCount of $totalFiles task(s) imported successfully" if ($failedTasks.Count -gt 0) { $warningMessage = ( "$($failedTasks.Count) task(s) failed to import. " + 'See FailedTasks property for details.' ) Write-Warning $warningMessage } [PSCustomObject]@{ TotalFiles = $totalFiles SuccessCount = $successCount FailureCount = $failedTasks.Count ImportedTasks = $importedTasks.ToArray() FailedTasks = $failedTasks.ToArray() } } } } end { Write-Verbose 'Completed Import-StmClusteredScheduledTask' } } function Import-SingleTask { <# .SYNOPSIS Internal helper function to import a single clustered scheduled task. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [string] $XmlContent, [Parameter(Mandatory = $true)] [string] $Cluster, [Parameter(Mandatory = $true)] [Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.ClusterTaskTypeEnum] $TaskType, [Parameter(Mandatory = $false)] [string] $TaskNameOverride, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] [switch] $Force ) # Determine effective task name if (-not [string]::IsNullOrWhiteSpace($TaskNameOverride)) { $effectiveTaskName = $TaskNameOverride Write-Verbose "Using provided task name override: '$effectiveTaskName'" } else { $effectiveTaskName = Get-TaskNameFromXml -XmlContent $XmlContent if ([string]::IsNullOrWhiteSpace($effectiveTaskName)) { $errorRecordParameters = @{ Exception = [System.InvalidOperationException]::new( 'Could not determine task name from XML.' ) ErrorId = 'TaskNameNotFound' ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidData TargetObject = $XmlContent Message = ( 'Could not extract task name from XML. ' + 'The RegistrationInfo/URI element is missing or empty.' ) RecommendedAction = ( 'Ensure the XML contains a valid RegistrationInfo/URI element, ' + 'or use the -TaskName parameter to specify the task name.' ) } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } Write-Verbose "Extracted task name from XML: '$effectiveTaskName'" } # Check if task already exists $existingTask = $null try { $getTaskParams = @{ TaskName = $effectiveTaskName Cluster = $Cluster ErrorAction = 'Stop' } if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { $getTaskParams['Credential'] = $Credential } $existingTask = Get-StmClusteredScheduledTask @getTaskParams } catch { # Task doesn't exist, which is fine Write-Verbose "Task '$effectiveTaskName' does not exist on cluster '$Cluster'" $existingTask = $null } if ($null -ne $existingTask) { if ($Force) { Write-Verbose "Task '$effectiveTaskName' exists. -Force specified, unregistering existing task..." $target = "Task '$effectiveTaskName' on cluster '$Cluster'" $operation = 'Unregister existing clustered scheduled task' if ($PSCmdlet.ShouldProcess($target, $operation)) { $unregisterParams = @{ TaskName = $effectiveTaskName Cluster = $Cluster ErrorAction = 'Stop' } if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { $unregisterParams['Credential'] = $Credential } Unregister-StmClusteredScheduledTask @unregisterParams Write-Verbose "Existing task '$effectiveTaskName' unregistered" } } else { $exceptionMessage = ( "A clustered scheduled task named '$effectiveTaskName' " + "already exists on cluster '$Cluster'." ) $errorRecordParameters = @{ Exception = [System.InvalidOperationException]::new($exceptionMessage) ErrorId = 'TaskAlreadyExists' ErrorCategory = [System.Management.Automation.ErrorCategory]::ResourceExists TargetObject = $effectiveTaskName Message = "$exceptionMessage Use -Force to overwrite." RecommendedAction = ( 'Use the -Force parameter to overwrite the existing task, ' + 'or choose a different task name with -TaskName.' ) } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } } # Register the task $target = "cluster '$Cluster'" $operation = "Import clustered scheduled task '$effectiveTaskName'" if ($PSCmdlet.ShouldProcess($target, $operation)) { try { $cimSession = New-StmCimSession -ComputerName $Cluster -Credential $Credential -ErrorAction 'Stop' Write-Verbose "CIM session established to cluster '$Cluster'" Write-Verbose "Registering clustered scheduled task '$effectiveTaskName'..." $registerParams = @{ TaskName = $effectiveTaskName Xml = $XmlContent CimSession = $cimSession TaskType = $TaskType ErrorAction = 'Stop' } $result = Register-ClusteredScheduledTask @registerParams Write-Verbose "Successfully imported task '$effectiveTaskName'" return $result } catch { $errorRecordParameters = @{ Exception = $_.Exception ErrorId = 'TaskRegistrationFailed' ErrorCategory = [System.Management.Automation.ErrorCategory]::OperationStopped TargetObject = $effectiveTaskName Message = ( "Failed to register clustered scheduled task '$effectiveTaskName' " + "on cluster '$Cluster'. $($_.Exception.Message)" ) RecommendedAction = ( 'Verify the XML is valid Task Scheduler format, ' + 'the cluster is accessible, and you have appropriate permissions.' ) } $errorRecord = New-StmError @errorRecordParameters $PSCmdlet.ThrowTerminatingError($errorRecord) } } } |