Public/Get-StmClusteredScheduledTask.ps1

function Get-StmClusteredScheduledTask {
    <#
    .SYNOPSIS
        Retrieves clustered scheduled tasks from a Windows failover cluster.

    .DESCRIPTION
        The Get-StmClusteredScheduledTask function retrieves clustered scheduled tasks from a Windows failover cluster.
        This function connects to the specified cluster and retrieves information about clustered scheduled tasks,
        including their current state, ownership, and configuration. You can filter tasks by name, state, or type.
        The function returns detailed information about each task including its scheduled task object, current owner,
        and cluster-specific properties.

    .PARAMETER TaskName
        Specifies the name of a specific clustered scheduled task to retrieve. If not specified, all clustered
        scheduled tasks on the cluster will be returned. This parameter is optional.

    .PARAMETER Cluster
        Specifies the name or FQDN of the cluster to query for clustered scheduled tasks. This parameter is mandatory.

    .PARAMETER TaskState
        Specifies the state of the tasks to filter by. Valid values are: Unknown, Disabled, Queued, Ready, Running.
        If not specified, tasks in all states will be returned. This parameter is optional.

    .PARAMETER TaskType
        Specifies the type of clustered tasks to filter by. Valid values are: ResourceSpecific, AnyNode, ClusterWide.
        If not specified, tasks of all types will be returned. This parameter is optional.

    .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 CimSession
        Specifies an existing CIM session to use for the connection to the cluster. If not provided, a new CIM session
        will be created using the Cluster and Credential parameters.

    .EXAMPLE
        Get-StmClusteredScheduledTask -Cluster "MyCluster"

        Retrieves all clustered scheduled tasks from cluster "MyCluster" using the current user's credentials.

    .EXAMPLE
        Get-StmClusteredScheduledTask -TaskName "BackupTask" -Cluster "MyCluster.contoso.com"

        Retrieves the specific clustered scheduled task named "BackupTask" from cluster "MyCluster.contoso.com".

    .EXAMPLE
        Get-StmClusteredScheduledTask -Cluster "MyCluster" -TaskState "Ready" -TaskType "ClusterWide"

        Retrieves all clustered scheduled tasks that are in "Ready" state and are "ClusterWide" type from cluster
        "MyCluster".

    .EXAMPLE
        $credentials = Get-Credential
        Get-StmClusteredScheduledTask -Cluster "MyCluster" -Credential $credentials |
            Where-Object { $_.CurrentOwner -eq "Node01" }

        Retrieves all clustered scheduled tasks from cluster "MyCluster" using specified credentials and filters to show
        only tasks owned by "Node01".

    .EXAMPLE
        $session = New-CimSession -ComputerName "MyCluster"
        Get-StmClusteredScheduledTask -Cluster "MyCluster" -CimSession $session

        Retrieves all clustered scheduled tasks using an existing CIM session.

    .INPUTS
        None. You cannot pipe objects to Get-StmClusteredScheduledTask.

    .OUTPUTS
        PSCustomObject
        Returns custom objects containing:
        - ScheduledTaskObject: The underlying ScheduledTask object
        - CurrentOwner: The current owner node of the task
        - TaskName: The name of the clustered scheduled task
        - TaskState: The current state of the task
        - TaskType: The type of clustered task
        - Cluster: The cluster name where the task is located

    .NOTES
        This function requires:
        - The FailoverClusters PowerShell module to be installed on the target cluster
        - Appropriate permissions to access clustered scheduled tasks
        - Network connectivity to the cluster
        - The cluster must be properly configured with clustered scheduled tasks

        The function uses Get-ClusteredScheduledTask internally to retrieve the task information from the cluster.
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]
        $TaskName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $Cluster,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Microsoft.PowerShell.Cmdletization.GeneratedTypes.ScheduledTask.StateEnum]
        $TaskState,

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [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)]
        [ValidateNotNullOrEmpty()]
        [Microsoft.Management.Infrastructure.CimSession]
        $CimSession
    )

    begin {
        # Track CIM sessions for cleanup
        $cimSessionsToCleanup = [System.Collections.Generic.List[Microsoft.Management.Infrastructure.CimSession]]::new()
        $clusteredScheduledTasksParameters = @{
            Cluster = $Cluster
        }

        $taskNameProvided = $PSBoundParameters.ContainsKey('TaskName')
        if ($taskNameProvided) {
            Write-Verbose "Starting Get-StmClusteredScheduledTask for task '$TaskName' on cluster '$Cluster'"
            $clusteredScheduledTasksParameters['TaskName'] = $TaskName
        }
        else {
            Write-Verbose "Starting Get-StmClusteredScheduledTask for all tasks on cluster '$Cluster'"
        }

        if ($PSBoundParameters.ContainsKey('TaskType')) {
            Write-Verbose "Filtering tasks by type: '$TaskType'"
            $clusteredScheduledTasksParameters['TaskType'] = $TaskType
        }

        if ($PSBoundParameters.ContainsKey('CimSession')) {
            Write-Verbose "Using provided CIM session for cluster '$Cluster'"
            $clusteredScheduledTasksParameters['CimSession'] = $CimSession
        }
        else {
            Write-Verbose "Creating new CIM session for cluster '$Cluster'"
            $cimSessionParameters = @{
                ComputerName = $Cluster
                Credential   = $Credential
                ErrorAction  = 'Stop'
            }
            $clusterCimSession = New-StmCimSession @cimSessionParameters
            $clusteredScheduledTasksParameters['CimSession'] = $clusterCimSession
            $cimSessionsToCleanup.Add($clusterCimSession)
        }
    }

    process {
        Write-Verbose "Retrieving clustered scheduled tasks from cluster '$Cluster'"
        $clusteredScheduledTasks = Get-ClusteredScheduledTask @clusteredScheduledTasksParameters
        if ($clusteredScheduledTasks.Count -eq 0) {
            if ($taskNameProvided) {
                $notFoundException = [System.InvalidOperationException]::new(
                    "Clustered scheduled task '$TaskName' not found on cluster '$Cluster'.")
                $errorRecordParameters = @{
                    Exception         = $notFoundException
                    ErrorId           = 'ClusteredScheduledTaskNotFound'
                    ErrorCategory     = [System.Management.Automation.ErrorCategory]::ObjectNotFound
                    TargetObject      = $TaskName
                    Message           = (
                        "Clustered scheduled task '$TaskName' was not found on cluster '$Cluster'."
                    )
                    RecommendedAction = (
                        'Verify the task name (including case) and that it is registered as a clustered task.'
                    )
                }
                $errorRecord = New-StmError @errorRecordParameters
                $PSCmdlet.WriteError($errorRecord)
            }
            else {
                Write-Warning (
                    "No clustered scheduled tasks found on cluster '$Cluster'. " +
                    'Ensure the cluster is properly configured.'
                )
            }
            return
        }
        Write-Verbose "Found $($clusteredScheduledTasks.Count) clustered scheduled task(s) on cluster '$Cluster'"

        $uniqueTaskOwners = @(
            $clusteredScheduledTasks |
                Select-Object -ExpandProperty 'CurrentOwner' |
                Where-Object { -not [string]::IsNullOrEmpty($_) } |
                Select-Object -Unique
        )
        if ($uniqueTaskOwners.Count -eq 0) {
            Write-Error (
                "No current owners found for tasks in cluster '$Cluster'. " +
                'Ensure the cluster is properly configured.'
            )
            return
        }
        else {
            Write-Verbose "Found $($uniqueTaskOwners.Count) unique task owner(s): $($uniqueTaskOwners -join ', ')"
        }
        foreach ($taskOwner in $uniqueTaskOwners) {
            if ([string]::IsNullOrEmpty($taskOwner)) {
                Write-Verbose 'Skipping task owner with null or empty name'
                continue
            }

            $clusteredScheduledTasksOwnedByCurrentOwner = $clusteredScheduledTasks | Where-Object {
                $_.CurrentOwner -eq $taskOwner
            }
            $taskNames = $clusteredScheduledTasksOwnedByCurrentOwner.TaskName

            # Create hashtable for O(1) lookup by TaskName (avoids repeated Where-Object)
            $clusteredTaskLookup = @{}
            foreach ($cst in $clusteredScheduledTasksOwnedByCurrentOwner) {
                $clusteredTaskLookup[$cst.TaskName] = $cst
            }

            try {
                # Note: Task owner CIM sessions are NOT cleaned up because the returned
                # ScheduledTaskObject contains CIM instance references that depend on them
                $taskOwnerCimSession = New-StmCimSession -ComputerName $taskOwner -Credential $Credential
                Write-Verbose "Retrieving scheduled tasks from owner '$taskOwner' using CIM session"
                # Suppress per-name cmdletization errors; we re-emit them as structured errors below
                # so the user sees one diagnostic per missing task instead of a raw red leak.
                $getScheduledTaskParameters = @{
                    TaskName    = $taskNames
                    CimSession  = $taskOwnerCimSession
                    ErrorAction = 'SilentlyContinue'
                }
                $scheduledTasksFromOwner = Get-ScheduledTask @getScheduledTaskParameters

                # Diff requested vs returned: cluster registry references the task but the owner
                # node didn't return it (task may have just failed over, or local copy is missing).
                $foundTaskNames = @($scheduledTasksFromOwner | Select-Object -ExpandProperty 'TaskName')
                foreach ($expectedTaskName in $taskNames) {
                    if ($foundTaskNames -notcontains $expectedTaskName) {
                        $ownerLookupException = [System.InvalidOperationException]::new(
                            "Owner '$taskOwner' did not return clustered task '$expectedTaskName'.")
                        $ownerLookupErrorParameters = @{
                            Exception         = $ownerLookupException
                            ErrorId           = 'ClusteredScheduledTaskOwnerLookupFailed'
                            ErrorCategory     = [System.Management.Automation.ErrorCategory]::ObjectNotFound
                            TargetObject      = $expectedTaskName
                            Message           = (
                                "Clustered scheduled task '$expectedTaskName' is registered on cluster " +
                                "'$Cluster' but owner node '$taskOwner' did not return it. The task may " +
                                'have just failed over, or its local copy on the owner may be missing.'
                            )
                            RecommendedAction = (
                                "Verify the task exists on '$taskOwner' and that cluster ownership is consistent."
                            )
                        }
                        $ownerLookupErrorRecord = New-StmError @ownerLookupErrorParameters
                        $PSCmdlet.WriteError($ownerLookupErrorRecord)
                    }
                }

                if ($PSBoundParameters.ContainsKey('TaskState')) {
                    Write-Verbose "Filtering scheduled tasks by state '$TaskState' on owner '$taskOwner'"
                    $scheduledTasksFromOwner = $scheduledTasksFromOwner | Where-Object { $_.State -eq $TaskState }
                }

                foreach ($scheduledTaskFromOwner in $scheduledTasksFromOwner) {
                    $procMsg = (
                        "Processing scheduled task '" + $scheduledTaskFromOwner.TaskName +
                        "' from owner '" + $taskOwner + "'"
                    )
                    Write-Verbose $procMsg
                    $findMsg = (
                        "Finding matching clustered scheduled task for '" +
                        $scheduledTaskFromOwner.TaskName + "'"
                    )
                    Write-Verbose $findMsg
                    # Use hashtable lookup for O(1) performance
                    $clusteredScheduledTask = $clusteredTaskLookup[$scheduledTaskFromOwner.TaskName]

                    if ($null -eq $clusteredScheduledTask) {
                        $noMatchMsg = (
                            "No matching clustered task found for '" +
                            $scheduledTaskFromOwner.TaskName + "'"
                        )
                        Write-Warning $noMatchMsg
                        continue
                    }

                    try {
                        Write-Verbose 'Merging properties from clustered scheduled task and task info'
                        $mergeParameters = @{
                            FirstObject      = $clusteredScheduledTask
                            FirstObjectName  = 'ClusteredScheduledTaskObject'
                            SecondObject     = $scheduledTaskFromOwner
                            SecondObjectName = 'ScheduledTaskObject'
                            AsHashtable      = $true
                            ErrorAction      = 'Stop'
                        }
                        $mergedHashtable = Merge-Object @mergeParameters
                        [PSCustomObject]$mergedHashtable
                    }
                    catch {
                        $mergeFailMsg = (
                            "Failed to merge objects for task '" +
                            $scheduledTaskFromOwner.TaskName + "': " + $_.Exception.Message
                        )
                        Write-Warning $mergeFailMsg
                    }
                }
            }
            catch {
                # Clean up the session on error since no valid objects will be returned
                if ($taskOwnerCimSession) {
                    Remove-CimSession -CimSession $taskOwnerCimSession -ErrorAction SilentlyContinue -WhatIf:$false
                }
                $ownerErrMsg = (
                    "Failed to retrieve tasks from owner '" + $taskOwner +
                    "': " + $_.Exception.Message
                )
                Write-Error $ownerErrMsg
            }
        }
    }

    end {
        foreach ($session in $cimSessionsToCleanup) {
            if ($session) {
                Remove-CimSession -CimSession $session -ErrorAction SilentlyContinue -WhatIf:$false
            }
        }
        Write-Verbose "Finished Get-StmClusteredScheduledTask for cluster '$Cluster'"
    }
}