Public/Get-CloudPCReport.ps1
|
function Get-CloudPCReport { <# .SYNOPSIS Retrieves Windows 365 Cloud PC report rows from Microsoft Graph beta stream reports. .DESCRIPTION Calls the Microsoft Graph beta Cloud PC report stream actions under /deviceManagement/virtualEndpoint/reports and parses the downloaded report file into objects. Graph returns these reports as files with a Schema array and a Values array. The cmdlet reads each Schema column, converts DateTime and common numeric types, and emits one WindowsCloudPC.ReportRow object per row. The ReportName parameter accepts only cloudPcReportName enum members that were verified to return report streams in a live tenant. Deprecated reports, tenant-state-dependent reports that returned Graph 400s, and enum values without a callable Graph action are intentionally excluded. realTimeRemoteConnectionStatus is also supported because it uses the same Graph report stream payload shape, but is exposed as a GET function scoped to a Cloud PC ID instead of a POST report action. .PARAMETER ReportName The Microsoft Graph beta cloudPcReportName enum member to retrieve. .PARAMETER CloudPcId Optional Cloud PC ID used to build the required CloudPcId filter for reports that are scoped to one Cloud PC, such as remoteConnectionHistoricalReports. If Filter is also provided, the CloudPcId clause is combined with it. For realTimeRemoteConnectionStatus, this calls the report for a single Cloud PC. When omitted, the cmdlet retrieves all Cloud PCs and calls the report once for each Cloud PC. .PARAMETER ActivityId Optional remote connection activity ID used to build the required ActivityId filter for rawRemoteConnectionReports. If Filter is also provided, the ActivityId clause is combined with it. .PARAMETER Select Optional report columns to request. Graph returns only selected columns when the target report action supports select. For rawRemoteConnectionReports, the Graph stream uses Timestamp and AvailableBandwidthInMBps column names. Common aliases SignInDateTime and AvailableBandwidthInMbps are normalized automatically. .PARAMETER Filter Optional OData filter expression for the report action. .PARAMETER Search Optional search string for the report action. .PARAMETER GroupBy Optional report columns to group by. For Graph report actions that support groupBy, this usually must match Select. .PARAMETER OrderBy Optional report columns or expressions to sort by. .PARAMETER Skip Optional number of rows to skip. .PARAMETER Top Optional number of rows to return. .PARAMETER OutputFilePath Optional path where the raw Graph report file should be saved. When not provided, a temporary file is used and removed after parsing. .PARAMETER MaxRetryCount Maximum number of retries for Graph 429, 503, and 504 responses. The retry delay honors Graph Retry-After when present and otherwise uses exponential backoff. .PARAMETER InitialRetryDelaySeconds First retry delay used when Graph does not return a Retry-After header. .PARAMETER MaxRetryDelaySeconds Maximum retry delay used when Graph does not return a Retry-After header. .PARAMETER RequestDelayMilliseconds Optional delay between per-Cloud-PC calls for realTimeRemoteConnectionStatus. Use this to proactively pace large tenants instead of relying only on Graph throttling responses. .PARAMETER Raw Return a WindowsCloudPC.ReportPayload object containing the parsed file, schema, values, action name, and output file path instead of row objects. .EXAMPLE $pc = Get-CloudPC | Select-Object -First 1 Get-CloudPCReport -ReportName remoteConnectionHistoricalReports -CloudPcId $pc.Id -Top 50 | Format-Table ManagedDeviceName,SignInDateTime,SignOutDateTime,UsageInHour .EXAMPLE $activity = Get-CloudPCReport -ReportName remoteConnectionHistoricalReports -CloudPcId '<cloud-pc-id>' -Top 1 Get-CloudPCReport -ReportName rawRemoteConnectionReports -ActivityId $activity.ActivityId -Select SignInDateTime,RoundTripTimeInMs,AvailableBandwidthInMbps | Sort-Object Timestamp -Descending .EXAMPLE Get-CloudPCReport -ReportName realTimeRemoteConnectionStatus | Format-Table ManagedDeviceName,SignInStatus,DaysSinceLastSignIn,LastActiveTime .EXAMPLE Get-CloudPCReport -ReportName frontlineLicenseUsageReport -Top 100 | Format-Table Timestamp,DisplayName,LicenseCount,ClaimedLicenseCount .EXAMPLE Get-CloudPCReport -ReportName regionalConnectionQualityTrendReport -Top 50 | Format-Table GatewayRegionName,WeeklyAvgRoundTripTimeInMs,WeeklyAvgAvailableBandwidthInMbps #> [CmdletBinding()] [OutputType('WindowsCloudPC.ReportRow', 'WindowsCloudPC.ReportPayload')] param( [Parameter(Mandatory, Position = 0)] [ValidateSet( 'remoteConnectionHistoricalReports', 'dailyAggregatedRemoteConnectionReports', 'totalAggregatedRemoteConnectionReports', 'noLicenseAvailableConnectivityFailureReport', 'frontlineLicenseUsageReport', 'frontlineLicenseUsageRealTimeReport', 'frontlineLicenseHourlyUsageReport', 'frontlineRealtimeUserConnectionsReport', 'inaccessibleCloudPcReports', 'actionStatusReport', 'rawRemoteConnectionReports', 'performanceTrendReport', 'inaccessibleCloudPcTrendReport', 'regionalConnectionQualityTrendReport', 'regionalConnectionQualityInsightsReport', 'bulkActionStatusReport', 'cloudPcInsightReport', 'regionalInaccessibleCloudPcTrendReport', 'cloudPcUsageCategoryReport', 'realTimeRemoteConnectionStatus' )] [string]$ReportName, [string]$CloudPcId, [string]$ActivityId, [Alias('Property')] [string[]]$Select, [string]$Filter, [string]$Search, [string[]]$GroupBy, [string[]]$OrderBy, [ValidateRange(0, 2147483647)] [int]$Skip, [ValidateRange(1, 2147483647)] [int]$Top, [string]$OutputFilePath, [ValidateRange(0, 10)] [int]$MaxRetryCount = 6, [ValidateRange(1, 3600)] [int]$InitialRetryDelaySeconds = 3, [ValidateRange(1, 3600)] [int]$MaxRetryDelaySeconds = 120, [ValidateRange(0, 60000)] [int]$RequestDelayMilliseconds = 0, [switch]$Raw ) begin { Connect-CloudPC | Out-Null } process { $definition = Resolve-CloudPCReportDefinition -ReportName $ReportName if ($definition.UnsupportedReason) { throw $definition.UnsupportedReason } if ($definition.Action -eq 'getRealTimeRemoteConnectionStatus') { $unsupportedParameters = @( 'ActivityId', 'Select', 'Filter', 'Search', 'GroupBy', 'OrderBy', 'Skip', 'Top' ) | Where-Object { $PSBoundParameters.ContainsKey($_) } if ($unsupportedParameters) { throw "realTimeRemoteConnectionStatus does not support the following parameters: $($unsupportedParameters -join ', '). Use -CloudPcId to scope to one Cloud PC, or omit it to query all Cloud PCs." } if ($OutputFilePath -and -not $CloudPcId) { throw "realTimeRemoteConnectionStatus supports -OutputFilePath only when -CloudPcId is specified. Omit -OutputFilePath when querying all Cloud PCs." } $targets = if ($CloudPcId) { @([pscustomobject]@{ Id = $CloudPcId; Name = $null }) } else { @(Get-CloudPC | ForEach-Object { [pscustomobject]@{ Id = $_.Id; Name = $_.Name } }) } foreach ($target in $targets) { if ([string]::IsNullOrWhiteSpace($target.Id)) { continue } $escapedCloudPcId = [uri]::EscapeDataString($target.Id) $uri = "https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/reports/getRealTimeRemoteConnectionStatus(cloudPcId='$escapedCloudPcId')" $removeOutputFile = -not $OutputFilePath if ($OutputFilePath) { $reportPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputFilePath) } else { $reportPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "windowscloudpc-report-$ReportName-$($target.Id)-$([guid]::NewGuid().ToString('N')).json") } try { $parentPath = Split-Path -Path $reportPath -Parent if ($parentPath -and -not (Test-Path -LiteralPath $parentPath)) { New-Item -ItemType Directory -Path $parentPath -Force | Out-Null } Invoke-CloudPCGraphRequestWithRetry ` -Method GET ` -Uri $uri ` -Headers @{ Prefer = 'include-unknown-enum-members' } ` -OutputFilePath $reportPath ` -MaxRetryCount $MaxRetryCount ` -InitialRetryDelaySeconds $InitialRetryDelaySeconds ` -MaxRetryDelaySeconds $MaxRetryDelaySeconds ` -ErrorAction Stop | Out-Null if (-not (Test-Path -LiteralPath $reportPath)) { throw "Graph did not write the expected report file: $reportPath" } $content = Get-Content -LiteralPath $reportPath -Raw -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($content)) { throw "Graph returned an empty report file: $reportPath" } $payload = $content | ConvertFrom-Json -AsHashtable -ErrorAction Stop if (-not $payload.Schema) { $payload['Schema'] = @( @{ Column = 'ManagedDeviceName'; PropertyType = 'String' } @{ Column = 'CloudPcId'; PropertyType = 'String' } @{ Column = 'DaysSinceLastSignIn'; PropertyType = 'Int64' } @{ Column = 'SignInStatus'; PropertyType = 'String' } @{ Column = 'LastActiveTime'; PropertyType = 'DateTime' } ) } if (-not $payload.Values -or @($payload.Values).Count -eq 0) { $payload['TotalRowCount'] = 1 $payload['Values'] = @(, @($target.Name, $target.Id, $null, 'NotSignedIn', $null)) } if ($Raw) { [pscustomobject]@{ PSTypeName = 'WindowsCloudPC.ReportPayload' ReportName = $ReportName Action = $definition.Action TotalRowCount = $payload.TotalRowCount Schema = $payload.Schema Values = $payload.Values OutputFilePath = $reportPath Raw = $payload } } else { $payload | ConvertFrom-CloudPCReportPayload -ReportName $ReportName -Action $definition.Action -OutputFilePath $reportPath } } finally { if ($removeOutputFile -and (Test-Path -LiteralPath $reportPath)) { Remove-Item -LiteralPath $reportPath -Force -ErrorAction SilentlyContinue } } if ($RequestDelayMilliseconds -gt 0) { Start-Sleep -Milliseconds $RequestDelayMilliseconds } } return } $body = [ordered]@{} if ($definition.IncludeReportName) { $body['reportName'] = $definition.GraphReportName } $effectiveFilter = $Filter $includeFilter = -not [string]::IsNullOrWhiteSpace($Filter) if (-not $includeFilter -and $definition.PSObject.Properties.Name -contains 'DefaultFilter') { $effectiveFilter = $definition.DefaultFilter $includeFilter = $true } if ($CloudPcId) { $cloudPcFilter = "CloudPcId eq '$CloudPcId'" $effectiveFilter = if ($effectiveFilter) { "$cloudPcFilter and ($effectiveFilter)" } else { $cloudPcFilter } $includeFilter = $true } if ($ActivityId) { $activityFilter = "ActivityId eq '$ActivityId'" $effectiveFilter = if ($effectiveFilter) { "$activityFilter and ($effectiveFilter)" } else { $activityFilter } $includeFilter = $true } if (-not $includeFilter -and $definition.DefaultFilterDays) { $start = (Get-Date).ToUniversalTime().AddDays(-[int]$definition.DefaultFilterDays).ToString('yyyy-MM-ddTHH:mm:ss.fffZ') $effectiveFilter = "EventDateTime gt datetime'$start'" $includeFilter = $true } if ($ReportName -eq 'remoteConnectionHistoricalReports' -and -not $includeFilter) { throw "remoteConnectionHistoricalReports requires -CloudPcId or -Filter with a CloudPcId clause, for example: Get-CloudPCReport -ReportName remoteConnectionHistoricalReports -CloudPcId '<cloud-pc-id>' -Top 10" } if ($ReportName -eq 'rawRemoteConnectionReports' -and -not $includeFilter) { throw "rawRemoteConnectionReports requires -ActivityId or -Filter with an ActivityId clause. Get an ActivityId from remoteConnectionHistoricalReports first, then run: Get-CloudPCReport -ReportName rawRemoteConnectionReports -ActivityId '<activity-id>' -Top 10" } if ($includeFilter) { $body['filter'] = $effectiveFilter } $effectiveSelect = if ($Select) { @($Select) } elseif ($definition.DefaultSelect) { @($definition.DefaultSelect) } else { $null } if ($ReportName -eq 'rawRemoteConnectionReports' -and $effectiveSelect) { $effectiveSelect = @( foreach ($column in $effectiveSelect) { switch ($column) { 'SignInDateTime' { 'Timestamp' } 'AvailableBandwidthInMbps' { 'AvailableBandwidthInMBps' } default { $column } } } ) } if ($effectiveSelect) { $body['select'] = $effectiveSelect } if ($PSBoundParameters.ContainsKey('Search')) { $body['search'] = $Search } elseif ($definition.PSObject.Properties.Name -contains 'DefaultSearch') { $body['search'] = $definition.DefaultSearch } if ($GroupBy) { $body['groupBy'] = @($GroupBy) } if ($OrderBy) { $body['orderBy'] = @($OrderBy) } elseif ($definition.PSObject.Properties.Name -contains 'DefaultOrderBy') { $body['orderBy'] = @($definition.DefaultOrderBy) } if ($PSBoundParameters.ContainsKey('Skip')) { $body['skip'] = $Skip } elseif ($definition.PSObject.Properties.Name -contains 'DefaultSkip') { $body['skip'] = [int]$definition.DefaultSkip } if ($PSBoundParameters.ContainsKey('Top')) { $body['top'] = $Top } $uri = "https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/reports/$($definition.Action)" $removeOutputFile = -not $OutputFilePath if ($OutputFilePath) { $reportPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputFilePath) } else { $reportPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "windowscloudpc-report-$ReportName-$([guid]::NewGuid().ToString('N')).json") } try { $parentPath = Split-Path -Path $reportPath -Parent if ($parentPath -and -not (Test-Path -LiteralPath $parentPath)) { New-Item -ItemType Directory -Path $parentPath -Force | Out-Null } $jsonBody = $body | ConvertTo-Json -Depth 8 Invoke-CloudPCGraphRequestWithRetry ` -Method POST ` -Uri $uri ` -Body $jsonBody ` -ContentType 'application/json' ` -Headers @{ Prefer = 'include-unknown-enum-members' } ` -OutputFilePath $reportPath ` -MaxRetryCount $MaxRetryCount ` -InitialRetryDelaySeconds $InitialRetryDelaySeconds ` -MaxRetryDelaySeconds $MaxRetryDelaySeconds ` -ErrorAction Stop | Out-Null if (-not (Test-Path -LiteralPath $reportPath)) { throw "Graph did not write the expected report file: $reportPath" } $content = Get-Content -LiteralPath $reportPath -Raw -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($content)) { throw "Graph returned an empty report file: $reportPath" } $payload = $content | ConvertFrom-Json -AsHashtable -ErrorAction Stop if ($Raw) { [pscustomobject]@{ PSTypeName = 'WindowsCloudPC.ReportPayload' ReportName = $ReportName Action = $definition.Action TotalRowCount = $payload.TotalRowCount Schema = $payload.Schema Values = $payload.Values OutputFilePath = $reportPath Raw = $payload } } else { $payload | ConvertFrom-CloudPCReportPayload -ReportName $ReportName -Action $definition.Action -OutputFilePath $reportPath } } finally { if ($removeOutputFile -and (Test-Path -LiteralPath $reportPath)) { Remove-Item -LiteralPath $reportPath -Force -ErrorAction SilentlyContinue } } } end { } } |