Public/Get-AzLocalFleetProgress.ps1

function Get-AzLocalFleetProgress {
    <#
    .SYNOPSIS
        Gets real-time progress of a fleet-wide update operation.
     
    .DESCRIPTION
        Queries the current status of all clusters in a fleet operation and returns
        aggregated progress information including:
        - Total, completed, in-progress, failed, pending counts
        - Estimated time remaining (based on average completion time)
        - Per-cluster status details
         
        Can be used with a state object from Invoke-AzLocalFleetOperation or
        by querying clusters directly by tag.
     
    .PARAMETER State
        A fleet operation state object. If provided, only checks clusters in this state.
     
    .PARAMETER ScopeByUpdateRingTag
        Query progress for clusters with a specific UpdateRing tag.
     
    .PARAMETER UpdateRingValue
        The UpdateRing tag value to filter by.
     
    .PARAMETER Detailed
        Include detailed per-cluster status in output.
 
    .EXAMPLE
        Get-AzLocalFleetProgress -State $fleetState
        Gets progress for clusters in the specified fleet operation.
 
    .EXAMPLE
        Get-AzLocalFleetProgress -ScopeByUpdateRingTag -UpdateRingValue "Production"
        Gets progress for all Production ring clusters.
 
    .EXAMPLE
        Get-AzLocalFleetProgress -ScopeByUpdateRingTag -UpdateRingValue "Wave1" -Detailed
        Gets detailed progress (returns per-cluster status rows on the output object).
    #>

    [CmdletBinding(DefaultParameterSetName = 'ByState')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $false, ParameterSetName = 'ByState')]
        [PSCustomObject]$State,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByTag')]
        [switch]$ScopeByUpdateRingTag,

        [ValidatePattern('^(\*\*\*|[A-Za-z0-9_-]{1,64}(;[A-Za-z0-9_-]{1,64})*)$')]
        [Parameter(Mandatory = $true, ParameterSetName = 'ByTag')]
        [string]$UpdateRingValue,

        [Parameter(Mandatory = $false)]
        [switch]$Detailed
    )
    
    Write-Log -Message "========================================" -Level Header
    Write-Log -Message "Fleet Update Progress Check" -Level Header
    Write-Log -Message "========================================" -Level Header
    
    # Get list of clusters to check
    $clustersToCheck = @()
    
    if ($PSCmdlet.ParameterSetName -eq 'ByState') {
        $stateToUse = if ($State) { $State } else { $script:FleetOperationState }
        if (-not $stateToUse) {
            Write-Warning "No fleet state available. Use -ScopeByUpdateRingTag or provide a state object."
            return $null
        }
        $clustersToCheck = $stateToUse.Clusters
        Write-Log -Message "Checking progress for Run ID: $($stateToUse.RunId)" -Level Info
    }
    else {
        # Query by tag
        Write-Log -Message "Querying clusters with UpdateRing = '$UpdateRingValue'..." -Level Info
        $inventory = Get-AzLocalClusterInventory -PassThru | Where-Object { $_.UpdateRing -eq $UpdateRingValue }
        if (-not $inventory) {
            Write-Warning "No clusters found with UpdateRing tag = '$UpdateRingValue'"
            return $null
        }
        foreach ($cluster in $inventory) {
            $clustersToCheck += [PSCustomObject]@{
                ClusterName = $cluster.ClusterName
                ResourceId = $cluster.ResourceId
                ResourceGroup = $cluster.ResourceGroup
                SubscriptionId = $cluster.SubscriptionId
            }
        }
    }
    
    Write-Log -Message "Checking status of $($clustersToCheck.Count) cluster(s)..." -Level Info

    # Get current status for each cluster via a single Azure Resource Graph
    # query against extensibilityresources/microsoft.azurestackhci/clusters/updatesummaries.
    # This replaces the previous per-cluster Get-AzLocalUpdateSummary fan-out
    # and removes the ThrottleLimit parameter.
    $clusterStatuses = @()
    $succeeded = 0
    $inProgress = 0
    $failed = 0
    $notStarted = 0
    $upToDate = 0

    $summaryByCluster = @{}
    if ($clustersToCheck.Count -gt 0) {
        Install-AzGraphExtension | Out-Null
        $idListKql = ($clustersToCheck | ForEach-Object { "'$($_.ResourceId.ToLower())'" }) -join ','
        $summariesKql = "extensibilityresources | where type =~ 'microsoft.azurestackhci/clusters/updatesummaries' | extend ids = split(id, '/') | extend ClusterResourceId_ = tolower(strcat('/subscriptions/', tostring(ids[2]), '/resourceGroups/', tostring(ids[4]), '/providers/Microsoft.AzureStackHCI/clusters/', tostring(ids[8]))) | where ClusterResourceId_ in~ ($idListKql) | project properties, ClusterResourceId_"
        $argParams = @{ Query = $summariesKql }
        # Honour the subscription on each cluster if present (mixed-sub fleets).
        $subs = @($clustersToCheck | Where-Object { $_.SubscriptionId } | Select-Object -ExpandProperty SubscriptionId -Unique)
        if ($subs.Count -eq 1) { $argParams['SubscriptionId'] = $subs[0] }
        try {
            $summaryRows = Invoke-AzResourceGraphQuery @argParams
        }
        catch {
            Write-Warning "ARG query for updatesummaries failed: $($_.Exception.Message)"
            $summaryRows = @()
        }
        foreach ($row in @($summaryRows)) {
            $summaryByCluster[[string]$row.ClusterResourceId_] = $row
        }
    }

    foreach ($cluster in $clustersToCheck) {
        $key = $cluster.ResourceId.ToLower()
        $row = $summaryByCluster[$key]
        if ($row) {
            $props = $row.properties
            $status = [PSCustomObject]@{
                ClusterName   = $cluster.ClusterName
                ResourceGroup = $cluster.ResourceGroup
                UpdateState   = $props.state
                HealthState   = $props.healthState
                LastUpdated   = $props.lastUpdated
            }
        }
        else {
            $status = [PSCustomObject]@{
                ClusterName   = $cluster.ClusterName
                ResourceGroup = $cluster.ResourceGroup
                UpdateState   = 'Unknown'
                HealthState   = 'Unknown'
                LastUpdated   = $null
            }
        }
        $clusterStatuses += $status
        switch ($status.UpdateState) {
            # Real ARM/ARG updateSummaries state values.
            'AppliedSuccessfully'   { $succeeded++;  break }
            'UpdateAvailable'       { $notStarted++; break }
            'UpdateInProgress'      { $inProgress++; break }
            'PreparationInProgress' { $inProgress++; break }
            'UpdateFailed'          { $failed++;     break }
            'PreparationFailed'     { $failed++;     break }
            'NeedsAttention'        { $failed++;     break }
            # Legacy / synonym values kept for downstream compatibility.
            'Succeeded'             { $succeeded++;  break }
            'Failed'                { $failed++;     break }
            'UpToDate'              { $upToDate++;   break }
            default                 { $notStarted++ }
        }
    }
    
    # Calculate progress
    $total = $clustersToCheck.Count
    $completed = $succeeded + $upToDate
    $progressPercent = if ($total -gt 0) { [math]::Round(($completed / $total) * 100, 1) } else { 0 }
    
    # Build progress report
    $progress = [PSCustomObject]@{
        Timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ"
        TotalClusters = $total
        Completed = $completed
        ProgressPercent = $progressPercent
        Succeeded = $succeeded
        UpToDate = $upToDate
        InProgress = $inProgress
        Failed = $failed
        NotStarted = $notStarted
        ClusterStatuses = if ($Detailed) { $clusterStatuses } else { $null }
    }
    
    # Display summary
    Write-Log -Message "" -Level Info
    Write-Log -Message "Progress Summary:" -Level Header
    Write-Log -Message " Total Clusters: $total" -Level Info
    Write-Log -Message " Completed: $completed ($progressPercent%)" -Level $(if ($completed -eq $total) { "Success" } else { "Info" })
    Write-Log -Message " - Succeeded: $succeeded" -Level $(if ($succeeded -gt 0) { "Success" } else { "Info" })
    Write-Log -Message " - Up to Date: $upToDate" -Level $(if ($upToDate -gt 0) { "Success" } else { "Info" })
    Write-Log -Message " In Progress: $inProgress" -Level $(if ($inProgress -gt 0) { "Warning" } else { "Info" })
    Write-Log -Message " Failed: $failed" -Level $(if ($failed -gt 0) { "Error" } else { "Info" })
    Write-Log -Message " Not Started: $notStarted" -Level Info
    
    if ($Detailed -and $clusterStatuses.Count -gt 0) {
        Write-Log -Message "" -Level Info
        Write-Log -Message "Per-Cluster Status:" -Level Header
        $clusterStatuses | Format-Table ClusterName, UpdateState, HealthState -AutoSize | Out-String | ForEach-Object { Write-Host $_ }
    }
    
    return $progress
}