Public/Get-SCOMAgentWorkflows.ps1

Function Get-SCOMAgentWorkflows {
  <#
      .NOTES
      Name: Get-SCOMAgentWorkflows
      Author: Tyson Paul
      Blog: https://monitoringguys.com/
      Version History:
      2022.06.09.1749 - v1
      Previously Get-SCOMRunningWorkflows was a standalone function for "running" workflows only.
 
      .DESCRIPTION
      Will output an array of objects containing helpful information on instances and the workflows running/failed for those instances.
 
      .EXAMPLE
      #
 
      #Example 1
      #All workflows (Running and Failed) are retrieved for the agent name provided. The results are saved in a variable, $r, which is piped to GridView.
      The function call is dot-sourced so that any subsequent calls to this function will execute faster.
 
      PS C:\> $r = . Get-SCOMAgentWorkflows -Status All -AgentName 'db01.contoso.com' -Verbose
      PS C:\> $r | Out-GridView
 
 
      #Example 2
      # Agent names matching the mask provided are piped to the function.
 
      PS C:\> (Get-SCOMAgent -DNSHostName *db*).DisplayName | Get-SCOMAgentWorkflows
 
 
      #Example 3
      # Results are sumarized by agents with failed workflows
      PS C:\> $result = Get-SCOMAgentWorkflows -AgentName 'ms02.contoso.com','db01.contoso.com','devdb01.contoso.com'
      PS C:\> $result | Where-Object Status -eq 'Failed' | Group-Object Agent | Select-Object @{Name="Failed";Expression={$_.Count}},Name
 
      Failed Name
      ------ ----
      14 devdb01.CONTOSO.COM
      9 DB01.CONTOSO.COM
 
 
 
      #Advanced examples for analyzing the workflow data
 
      # Storing results to XML, then consuming results from XML.
      PS C:\> Get-SCOMAgentWorkflows -AgentName 'ad01.contoso.com' | Export-Clixml -Path 'C:\Test\ad01.contoso.com.XML'
      $WFs = Import-Clixml -Path 'C:\Test\ad01.contoso.com.XML'
 
      # Total Instances
      $TOI = $WFs.instance.count
      Write-Host "Total object instances: $TOI"
 
      # Worflows types:
      $WFAT = $WFs.workflows.workflow | Group-Object Name | Measure-Object | Select-Object count -ExpandProperty count
      Write-Host "Worflows types: $WFAT"
 
      # Total WF instances running, all types:
      $TWFAT = $WFs.wfcount | Measure-Object -Sum | Select-Object sum -ExpandProperty sum
      Write-Host "Total WF instances running, all types: $TWFAT"
 
      # Workflows of category type
      $Category = 'EventCollection'
      $WFoCT = $WFs.workflows.workflow | ? {($_.Category -match $Category) -AND ($_.WriteActionCollection -notmatch 'Alert')}
      $WFoCT_sum = $WFoCT | Group-Object Name | Measure-Object | Select-Object count -ExpandProperty Count
      Write-Host "Workflows of category type [$($Category), Not Alert]: $WFoCT_sum"
 
      # Total WF instances of category type
      $TWFIoCT = $WFoCT | Group-Object Name | Measure-Object -sum count | Select-Object sum -ExpandProperty sum
      Write-Host "Total WF instances of category type [$($Category)]: $TWFIoCT"
 
      #Lists
      ###############################################
 
      # Classes
      $classes = Get-SCOMClass -ID (($wfs.InstanceName).LeastDerivedNonAbstractManagementPackClassId | Group-Object).Name
      Write-Host "$($Classes.count) class target types"
 
      # Class Names
      $Classes = ($wfs.InstanceName).LeastDerivedNonAbstractManagementPackClassId`
      | Group-Object `
      | Select-Object count `
      , @{Name="Class"; Expression = {(Get-SCOMClass -id $_.Name).Name}} `
      , @{Name="Id";Expression = {$_.Name}}, @{Name="Identifier"; Expression = {(Get-SCOMClass -id $_.Name).Identifier}} `
      | Sort-Object count `
      | Format-Table
 
      $Classes
      $allWorkflows = @{}
      # List of workflows, instance count, category
      $WFs.workflows.workflow | % {$allWorkflows[($_.Name)] = $_}
      $WFs.workflows.workflow | ? {($_.Category -match $Category) -AND ($_.WriteActionCollection -notmatch 'Alert')} | Group-Object Name | Select-Object count,name,@{Name='DisplayName';Expression = {$allWorkflows[($_.Name)].DisplayName}} ,@{Name='Category';Expression = {$Category}} | Sort-Object count | Format-Table
 
      # All workflow types with counts
      $WFs.workflows.workflow | Group-Object Name | Select-Object count,name,@{Name='DisplayName';Expression = {$allWorkflows[($_.Name)].DisplayName}} ,@{Name='Category';Expression = {$allWorkflows[($_.Name)].Category}} | Sort-Object count | Format-Table
 
  #>


  [CmdletBinding(DefaultParameterSetName='Parameter Set 1',
      SupportsShouldProcess=$false,
      PositionalBinding=$false,
      HelpUri = 'https://monitoringguys.com/',
  ConfirmImpact='Medium')]
  Param(

    # FQDN of one or more agents
    [Parameter(Mandatory=$true,
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$false,
        ValueFromRemainingArguments=$false,
        Position=0,
    ParameterSetName='AgentName')]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string[]]$AgentName,


    # Param1 help description
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
    ValueFromRemainingArguments=$false)]
    [ValidateSet('TasktResult', 'Detailed')]
    $OutputType = 'Detailed',


    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
    ValueFromRemainingArguments=$false)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [ValidateSet('Failed','Running','All')]
    $Status = 'All',


    # Seconds before function will abandon waiting for task to complete
    [Parameter(Mandatory=$false,
        ValueFromPipeline=$false,
        ValueFromPipelineByPropertyName=$false,
    ValueFromRemainingArguments=$false)]
    [int]$TimeoutSeconds=600
  )

  BEGIN {

    $HSClass = Get-SCOMClass -Name "Microsoft.SystemCenter.HealthService"
    $AllHS = (Get-SCOMClassInstance -Class $HSClass)
    [System.Collections.ArrayList]$AllResults = @()
    [System.Collections.ArrayList]$TaskNames = @()
    [System.Collections.ArrayList]$Task = @()
    $thisStatus = @{
      "Microsoft.SystemCenter.GetAllRunningWorkflows" = "Running"
      "Microsoft.SystemCenter.GetAllFailedWorkflows" = "Failed"
    }

    $hashTasks = @{
      "Microsoft.SystemCenter.GetAllRunningWorkflows" = [System.Collections.ArrayList]@()
      "Microsoft.SystemCenter.GetAllFailedWorkflows" = [System.Collections.ArrayList]@()
    }

    Switch ($Status) {
      'All' {
        $NULL = $TaskNames.Add('Microsoft.SystemCenter.GetAllRunningWorkflows')
        $NULL = $TaskNames.Add('Microsoft.SystemCenter.GetAllFailedWorkflows')
      }
      'Failed' {
        $NULL = $TaskNames.Add('Microsoft.SystemCenter.GetAllFailedWorkflows')

      }
      'Running' {
        $NULL = $TaskNames.Add('Microsoft.SystemCenter.GetAllRunningWorkflows')
      }
    }

    $MinWait = 2
    $WaitSeconds = [math]::Min($MinWait,$TimeoutSeconds)


    ############################ FUNCTIONS ################################

    <#
        .NOTES
        Name: Get-SCOMAgentWorkflowsReport
        Author: Tyson Paul
        Blog: https://monitoringguys.com/
        Version History:
        2022.10.24.0937 - Added -TaskCredentials $NULL for SCOM2022+ compatibility
        2020.09.22.1433 - v1
 
        .DESCRIPTION
        Will output an array of objects containing helpful information on instances and the workflows running for those instances.
 
        .EXAMPLE
        #
        #Example
        PS C:\> $TaskResult = Get-SCOMAgentWorkflows -AgentName 'ad01.contoso.com','db01.contoso.com' -OutputType 'TasktResult'
        PS C:\> $TaskResult | Get-SCOMAgentWorkflowsReport
 
        .EXAMPLE
        #
        #Example
        PS C:\> Get-SCOMAgentWorkflowsReport -TaskResult (Get-SCOMAgentWorkflows -AgentName 'ad01.contoso.com' -OutputType TasktResult)
 
        .EXAMPLE
        #
        #Example
        PS C:\> Get-SCOMAgentWorkflowsReport -TaskResult $TaskResult
 
        .INPUTS
        Task result from 'Microsoft.SystemCenter.GetAllRunningWorkflows' agent task.
 
        .OUTPUTS
        [System.Object[]]
    #>

    Function Get-SCOMAgentWorkflowsReport {
      Param (
        [Parameter(Mandatory=$true,
            ValueFromPipeline=$true,
            ValueFromPipelineByPropertyName=$false,
            ValueFromRemainingArguments=$false,
        ParameterSetName='Parameter Set 1')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [System.Object[]]$TaskResult,

        [Parameter(Mandatory=$true,
            ValueFromPipeline=$false,
            ValueFromPipelineByPropertyName=$false,
            ValueFromRemainingArguments=$false,
        ParameterSetName='Parameter Set 1')]
        [ValidateNotNull()]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Failed','Running')]
        $Status
      )

      BEGIN {

        ############################ FUNCTIONS ################################
        Function Build-Obj {
          Param (
            $WF,
            [string]$Type ='NotDefined'
          )
          $tmp = New-Object PSCustomObject
          $tmp | Add-Member -MemberType NoteProperty -Name 'Type' -Value $type
          $tmp | Add-Member -MemberType NoteProperty -Name 'Workflow' -Value $wf
          Return $tmp
        }
        ######################## END FUNCTIONS ################################

        Remove-Variable -Name 'Instances','rpt1' -ErrorAction Ignore
        [System.Collections.ArrayList]$arrReports = @()

        # Load all workflow types into one hash. dot-source this function to speed up multiple runs.
        If (-NOT $allWFTypes) {
          $allWFTypes=@{}
          $allClasses=@{}
          Write-Warning "Loading all workflows into cache object. This may require a few minutes. dot-source this function next time to speed up consecutive uses."
          ForEach ($item in @(Get-SCOMMonitor)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Monitor) }
          ForEach ($item in @(Get-SCOMRule)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Rule) }
          ForEach ($item in @(Get-SCOMDiscovery)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Discovery) }

          # Build hash of all class types
          ForEach ($Class in (Get-SCOMClass) ){ $allClasses[$Class.Id.Guid] = $Class}
        }
        Else {
          Write-Verbose "'allWFTypes' object [$($allWFTypes.Count)] already cached. 'allClasses' object already cached [$($allClasses.Count)]. Will not rebuild; use existing. This will save time."
        }
      }#end BEGIN

      #----------------------------------------------------------------

      PROCESS {

        ForEach ($Result in $TaskResult) {
          $ThisAgent = (Get-SCOMClassInstance -Id $Result.TargetObjectId)
          # Get the instance objects involved
          $Instances = (($Result.Output | ForEach-Object {[xml]$_}).DataItem)
          $hashInstances = @{}
          $Instances.Details.Instance | ForEach-Object {$hashInstances[($_.ID -Replace '{|}' ,'' )] = $_ }

          # Get basic workflow data: Instance ID (of target object), Workflow count, Workflow list/array
          $InstanceDetails = ([xml]($Result.Output)).DataItem.Details.Instance | Select-Object @{Name='Id';Expression={(($_.ID -Replace '{|}','').ToString())}}, @{Name='WFCount';Expression={$_.Workflow.Count} }, @{Name='Workflow';Expression={$_.Workflow | Sort-Object} } | Sort-Object WFCount,Workflow -Descending

          $InstanceInfo = [ordered]@{}
          # Build an object with lots of data about each Instance, and the workflows running on its behalf
          ForEach ($Obj in $InstanceDetails) {
            $thisInstance = (Get-SCOMClassInstance -Id $Obj.ID )
            $objHash = [Ordered]@{
              WFCount = $obj.WFCount
              Status = $Status
              Agent = $ThisAgent
              InstanceID = $Obj.ID
              InstanceFullName = ($thisInstance.FullName)
              InstanceName = $thisInstance
              Class = $allClasses[($ThisInstance.LeastDerivedNonAbstractManagementPackClassId.Guid)]
              Workflows = [array]($obj.Workflow | ForEach-Object {$allWFTypes[$_] })
            }
            Try {
              $InstanceInfo[$objHash.InstanceFullName] = $objHash
            } Catch {
              Write-Warning "$_"
            }
          }

          # Report #1
          $rpt1 = ForEach ($key in @($InstanceInfo.Keys)) {
            $Key | Select-Object `
            -Property @{Name='WFCount';Expression={$InstanceInfo[$key].WFCount}} `
            ,@{Name='Status';Expression={$InstanceInfo[$key].Status}} `
            ,@{Name='Agent';Expression={$InstanceInfo[$key].Agent}} `
            ,@{Name='InstanceFullName';Expression={$InstanceInfo[$key].InstanceFullName}} `
            ,@{Name='InstanceName';Expression={$InstanceInfo[$key].InstanceName}} `
            ,@{Name='InstanceID';Expression={$InstanceInfo[$key].InstanceID}} `
            ,@{Name='Class';Expression={$InstanceInfo[$key].Class}} `
            ,@{Name='Workflows';Expression={$InstanceInfo[$key].Workflows}} `
          }
          $Null = $arrReports.Add($rpt1)
        }#end FOREACH
      }#end PROCESS

      #----------------------------------------------------------------

      END {
        Return $arrReports
      }#end END
    } #End Function Get-SCOMAgentWorkflowsReport
    ######################## END FUNCTIONS ################################

  } #End BEGIN



  PROCESS {
    $Instance = $NULL

    Try {
      Write-Verbose "Get instances of Health Service class with name(s): $AgentName"

      $NotAvailable = $AllHS | Where-Object {($_.Path -in $AgentName) -AND ($_.IsAvailable -eq $false )}
      ForEach ($UnavailableAgent in $NotAvailable) {
        Write-Warning "UNAVAILABLE: $($UnavailableAgent.DisplayName)"
      }
      $Instance = $AllHS | Where-Object {($_.Path -in $AgentName ) -AND ($_.IsAvailable -eq $true )}
      If (-NOT $Instance) {
        Throw "Agents not found or unavailable:`n$($AgentName)`n"
      }
    } Catch {
      Write-Warning $_
      Continue;
    }

    ForEach ($TaskName in $TaskNames) {
      $TaskWF = $NULL
      $TaskResult = $NULL

      Write-Verbose "Get SCOM task: $TaskName"
      $TaskWF = Get-SCOMTask -Name $TaskName

      Try {
        Write-Verbose "Starting the task now $(Get-Date) for [$($Instance.Count)] agents:"
        #$Instance.DisplayName | ForEach-Object {Write-Verbose $_}
        $Error.Clear()
        $thisTask = Start-SCOMTask -Instance $Instance -Task $TaskWF -TaskCredentials $NULL -Verbose:$([bool]($VerbosePreference -eq 'Continue')) -ErrorAction Stop
        $ThisTask | Where-Object {$_} | ForEach-Object {  $NULL = $hashTasks[$TaskName].Add($_) }
      } Catch {
        Write-Error "Failed to start task: $TaskName. $_`nExiting. "
        Continue;
      }
    } #End ForEach TaskName

  } #end PROCESS

  END {
    $ScriptTimer = [System.Diagnostics.Stopwatch]::StartNew()

    ForEach ($TaskName in @($hashTasks.Keys)) {
      # Keep looping while tasks are still running (Scheduled, Queued, etc. (other than Failed/Succeeded))
      Do {
        $secondsRemain = "{0:N0}" -F $($TimeoutSeconds - $ScriptTimer.Elapsed.TotalSeconds)
        Write-Verbose "`n$(Get-Date): Waiting for task(s) to complete. [$($secondsRemain)] seconds remain until timeout. Sleeping for $($WaitSeconds) seconds."
        Start-Sleep -Seconds $WaitSeconds
        If (-NOT $hashTasks[$TaskName].Id.Count) {
          Write-Warning "No tasks exist for workflow: $($TaskName)"
          Continue;
        }
        $Results = (Get-SCOMTaskResult -Id $hashTasks[$TaskName].Id)
      } While ((([regex]::matches(($Results.Status),'Succeeded').Count + [regex]::matches(($Results.Status),'Failed').Count) -LT ($Instance.Count)) -AND ($ScriptTimer.Elapsed.TotalSeconds -le $TimeoutSeconds) )
      $ScriptTimer.Stop()

      # If some task is not Status=Succeeded, show report
      ForEach ($TaskResult in ($Results | Where-Object {$_.Status -notmatch "Succeeded" }) ) {
        $ThisInstance = $Instance | Where-object {$_.Id.Guid -match $TaskResult.TargetObjectId.Guid}
        Write-Warning "$($TaskName):$($ThisInstance.Path):$($TaskResult.Status)"
      } #end ForEach TaskResult

      If ($ScriptTimer.Elapsed.TotalSeconds -gt $TimeoutSeconds){
        Write-Warning "Timer expired. Check task status manually. TaskID: [$($Task.Id.Guid)]. Exiting"
      }
      Else {
        Switch ($OutputType) {
          'TasktResult' {
            $Results | Where-Object {$_} | ForEach-Object {$NULL = $AllResults.Add($_)}
          }
          'Detailed' {
            # dot-sourced to potentially reduce multiple loading of the workflow cache in this function
            (. Get-SCOMAgentWorkflowsReport -Status $thisStatus[$TaskName] -TaskResult $Results -Verbose:$VerbosePreference ) | Where-Object {$_} | ForEach-Object {$_ | ForEach-Object {$NULL = $AllResults.Add($_)} }
          }
        }
      }

    }
    Return $AllResults
  }

}