Private/Azure/Get-PlacementScores.ps1
|
function Get-PlacementScores { <# .SYNOPSIS Retrieves Azure VM placement likelihood scores for SKU and region combinations. .DESCRIPTION Calls Invoke-AzSpotPlacementScore (API name includes "Spot", but returned placement signal is broadly useful for VM allocation planning). Returns a hashtable keyed by "sku|region" with score metadata. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'DesiredCount', Justification = 'Used inside Invoke-WithRetry scriptblock closure')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'IncludeAvailabilityZone', Justification = 'Used inside Invoke-WithRetry scriptblock closure')] param( [Parameter(Mandatory)] [string[]]$SkuNames, [Parameter(Mandatory)] [string[]]$Regions, [ValidateRange(1, 1000)] [int]$DesiredCount = 1, [switch]$IncludeAvailabilityZone, [int]$MaxRetries = 3, [System.Collections.IDictionary]$Caches = @{} ) $scores = @{} $uniqueSkus = @($SkuNames | Where-Object { $_ } | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Select-Object -Unique) $uniqueRegions = @($Regions | Where-Object { $_ } | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ } | Select-Object -Unique) if ($uniqueSkus.Count -gt 5) { Write-Verbose "Placement score: truncating from $($uniqueSkus.Count) to 5 SKUs (API limit)." } if ($uniqueRegions.Count -gt 8) { Write-Verbose "Placement score: truncating from $($uniqueRegions.Count) to 8 regions (API limit)." } $normalizedSkus = @($uniqueSkus | Select-Object -First 5) $normalizedRegions = @($uniqueRegions | Select-Object -First 8) if ($normalizedSkus.Count -eq 0 -or $normalizedRegions.Count -eq 0) { return $scores } if (-not (Get-Command -Name 'Invoke-AzSpotPlacementScore' -ErrorAction SilentlyContinue)) { Write-Verbose 'Invoke-AzSpotPlacementScore is not available in the current Az.Compute module.' return $scores } try { $response = Invoke-WithRetry -MaxRetries $MaxRetries -OperationName 'Spot Placement Score API' -ScriptBlock { Invoke-AzSpotPlacementScore -Location $normalizedRegions -Sku $normalizedSkus -DesiredCount $DesiredCount -IsZonePlacement:$IncludeAvailabilityZone.IsPresent -ErrorAction Stop } } catch { $errorText = $_.Exception.Message $isForbidden = $errorText -match '403|forbidden|authorization|not authorized|insufficient privileges' if ($isForbidden) { if (-not $Caches.PlacementWarned403) { Write-Warning 'Placement score lookup skipped: missing permissions (Compute Recommendations Role).' $Caches.PlacementWarned403 = $true } return $scores } Write-Verbose "Failed to retrieve placement scores: $errorText" return $scores } $rows = @() if ($null -eq $response) { return $scores } if ($response -is [System.Collections.IEnumerable] -and $response -isnot [string]) { $rows = @($response) } else { $rows = @($response) } foreach ($row in $rows) { if ($null -eq $row) { continue } $sku = @($row.Sku, $row.SkuName, $row.VmSize, $row.ArmSkuName) | Where-Object { $_ } | Select-Object -First 1 $region = @($row.Region, $row.Location, $row.ArmRegionName) | Where-Object { $_ } | Select-Object -First 1 $score = @($row.Score, $row.PlacementScore, $row.AvailabilityScore) | Where-Object { $_ } | Select-Object -First 1 if (-not $sku -or -not $region) { continue } $key = "$sku|$($region.ToString().ToLower())" $scores[$key] = [pscustomobject]@{ Score = if ($score) { $score.ToString() } else { 'N/A' } IsAvailable = if ($null -ne $row.IsAvailable) { [bool]$row.IsAvailable } else { $null } IsRestricted = if ($null -ne $row.IsRestricted) { [bool]$row.IsRestricted } else { $null } } } return $scores } |