Private/Azure/Get-AdvisorRetirementData.ps1
|
function Get-AdvisorRetirementData { <# .SYNOPSIS Queries Azure Advisor for VM SKU retirement recommendations. .DESCRIPTION Uses Azure Resource Graph (advisorresources table) for a single tenant-wide query when Az.ResourceGraph is available. Falls back to the per-subscription Advisor REST API. Results are cached in $script:RunContext.Caches.AdvisorRetirement for the session. #> param( [string[]]$SubscriptionId, [string[]]$ManagementGroup, [string]$ArmUrl = 'https://management.azure.com', [string]$BearerToken, [int]$MaxRetries = 3 ) # Return cached data if available (single tenant-wide cache) if ($script:RunContext -and $script:RunContext.Caches.AdvisorRetirement) { return $script:RunContext.Caches.AdvisorRetirement } $result = @{} # Strategy 1: Azure Resource Graph — single query across all subscriptions $useARG = $false try { if (Get-Command -Name 'Search-AzGraph' -ErrorAction SilentlyContinue) { $useARG = $true } } catch { Write-Verbose "Search-AzGraph availability check failed: $($_.Exception.Message)" } if ($useARG) { try { $argQuery = @" advisorresources | where type =~ 'microsoft.advisor/recommendations' | where properties.category =~ 'HighAvailability' | where properties.extendedProperties.recommendationSubCategory =~ 'ServiceUpgradeAndRetirement' | where properties.impactedField has 'VIRTUALMACHINES' | project seriesName = tostring(properties.extendedProperties.retirementFeatureName), retireDate = tostring(properties.extendedProperties.retirementDate), impact = tostring(properties.impact), vmName = tostring(properties.impactedValue), subscriptionId "@ $argParams = @{ Query = $argQuery; First = 1000 } if ($ManagementGroup) { $argParams['ManagementGroup'] = $ManagementGroup } elseif ($SubscriptionId) { $argParams['Subscription'] = $SubscriptionId } $allRecs = [System.Collections.Generic.List[PSCustomObject]]::new() do { $page = Search-AzGraph @argParams if ($page) { foreach ($r in $page) { $allRecs.Add($r) } if ($page.SkipToken) { $argParams['SkipToken'] = $page.SkipToken } else { break } } else { break } } while ($true) foreach ($rec in $allRecs) { $seriesName = $rec.seriesName $retireDate = $rec.retireDate $vmName = $rec.vmName if ($seriesName -and $retireDate) { if (-not $result[$seriesName]) { $result[$seriesName] = @{ RetireDate = $retireDate Series = $seriesName Impact = $rec.impact Status = if ([datetime]$retireDate -lt [datetime]::UtcNow) { 'Retired' } else { 'Retiring' } VMs = [System.Collections.Generic.List[string]]::new() } } if ($vmName) { $result[$seriesName].VMs.Add($vmName) } } } $totalVMs = @($result.Values | ForEach-Object { $_.VMs.Count } | Measure-Object -Sum).Sum Write-Verbose "Advisor (ARG): found $($result.Count) retirement group(s) covering $totalVMs VM(s) across tenant" } catch { Write-Verbose "ARG advisor query failed, falling back to REST API: $_" $useARG = $false $result = @{} } } # Strategy 2: Fallback — per-subscription REST API (single subscription only) if (-not $useARG) { $fallbackSubId = if ($SubscriptionId) { $SubscriptionId[0] } else { $null } if ($fallbackSubId -and $BearerToken) { try { $uri = "$($ArmUrl.TrimEnd('/'))/subscriptions/$fallbackSubId/providers/Microsoft.Advisor/recommendations?api-version=2023-01-01&`$filter=Category eq 'HighAvailability'" $headers = @{ Authorization = "Bearer $BearerToken" } $advisorResp = Invoke-WithRetry -ScriptBlock { Invoke-RestMethod -Uri $uri -Headers $headers -Method Get -TimeoutSec 30 -ErrorAction Stop } -MaxRetries $MaxRetries if ($advisorResp.value) { foreach ($rec in $advisorResp.value) { $props = $rec.properties if ($props.extendedProperties.recommendationSubCategory -ne 'ServiceUpgradeAndRetirement') { continue } if ($props.impactedField -notmatch 'VIRTUALMACHINES') { continue } $retireDate = $props.extendedProperties.retirementDate $seriesName = $props.extendedProperties.retirementFeatureName $vmName = $props.impactedValue if ($seriesName -and $retireDate) { if (-not $result[$seriesName]) { $result[$seriesName] = @{ RetireDate = $retireDate Series = $seriesName Impact = $props.impact Status = if ([datetime]$retireDate -lt [datetime]::UtcNow) { 'Retired' } else { 'Retiring' } VMs = [System.Collections.Generic.List[string]]::new() } } $result[$seriesName].VMs.Add($vmName) } } } $totalVMs = @($result.Values | ForEach-Object { $_.VMs.Count } | Measure-Object -Sum).Sum Write-Verbose "Advisor (REST): found $($result.Count) retirement group(s) covering $totalVMs VM(s) in subscription $fallbackSubId" } catch { Write-Verbose "Advisor retirement query failed (non-fatal, falling back to pattern table): $_" } } } # Cache the result (single tenant-wide hashtable) if ($script:RunContext -and $script:RunContext.Caches) { $script:RunContext.Caches.AdvisorRetirement = $result } return $result } |