Public/Get-AACSkuAvailability.ps1
|
function Get-AACSkuAvailability { <# .EXTERNALHELP Azure.Admin.Console-help.xml .SYNOPSIS Which VM sizes you can use - for virtual machines or AKS node pools - in a region and its availability zones, with your subscription's restrictions and vCPU quota, and why a size can't be used. .DESCRIPTION Read-only, over REST (Reader is enough, no Az modules or Azure CLI, nothing created). For each subscription and region, read at once: Microsoft.Compute/skus the sizes offered, their zones, the subscription's restrictions (region or zone) and capabilities Microsoft.Compute/usages each family's and the region's vCPU quota subscription locations which physical zone each logical zone is (they differ between subscriptions) and, with -ClusterName, the AKS cluster and its node pools (Azure Resource Graph). Each size's status, the first that applies: Restricted not offered to the subscription in the region NotSupported -Service Aks: fewer than 2 vCPUs ZoneUnavailable -Zone: none of those zones can host it Partial -Zone: some of them can't NoQuota fewer free vCPUs (family or region) than vCPUs x -NodeCount Available usable with the reason, and the size's vCPUs, memory, zones, architecture, ephemeral OS disk, accelerated networking, premium storage, Spot, GPUs and data disks. -Service Aks adds AKS's rules: at least 2 vCPUs; 4 GB for system node pools, and burstable (B) sizes noted as unfit for them. What only a real deployment proves is that capacity is there at that moment; nothing here creates anything to find out. What you get depends on where the command runs: at the prompt tiles, the zones, the quota, the cluster's node pools (-ClusterName) and each size with its status and reason - a page at a time piped onward the sizes (AAC.SkuAvailability), with no view -PassThru the view and the sizes -NoDisplay the sizes only -CsvPath writes the sizes. -HtmlPath writes an interactive report (sizes, quota, zones, node pools; charts that filter them). -PdfPath writes a PDF. .PARAMETER Location The regions to check (for example uksouth, westeurope). With -ClusterName, the cluster's region by default. .PARAMETER Service VirtualMachine (the default) or Aks: AKS's node pool rules as well. .PARAMETER Zone The availability zones the size must be in (for example 1, 2, 3). Without it, zones are shown but not required. .PARAMETER SubscriptionId The subscriptions to check - restrictions and quota are per subscription. The cluster's with -ClusterName; the only one you can see otherwise. .PARAMETER ClusterName An AKS cluster: checks its region, marks the sizes its node pools use and shows each pool's size status and free quota. Implies -Service Aks. .PARAMETER ResourceGroupName The cluster's resource group, when the name alone isn't unique. .PARAMETER Series Only these VM series - the letters of the size name, for example D, E, F, NC, DC (Standard_D4s_v5 is D); wildcards allowed. .PARAMETER Sku Only these sizes, for example Standard_D4s_v5 or D4s_v5; wildcards allowed (*s_v5). .PARAMETER Architecture Only x64 or Arm64 sizes. .PARAMETER NodeCount The number of VMs or nodes the quota must fit (vCPUs x NodeCount). 1 by default. .PARAMETER CsvPath Write every size to this CSV file. .PARAMETER PdfPath Write a PDF report to this file. .PARAMETER HtmlPath Write an interactive HTML report to this file. .PARAMETER Title The PDF and HTML reports' title. .PARAMETER PassThru Show the view and also return the sizes. .PARAMETER NoDisplay Return the sizes without showing the view. .PARAMETER NoPaging Show the whole view at once instead of a page at a time. .EXAMPLE Connect-AAC Get-AACSkuAvailability -Location uksouth -Zone 1, 2, 3 -Series D, E The D and E series sizes that can run in all three zones of UK South. .EXAMPLE Get-AACSkuAvailability -ClusterName 'aks-contoso' -Zone 1, 2, 3 -Sku '*s_v5' -NodeCount 3 Sizes for a new three-node, three-zone pool on a cluster, in its region, with its current pools. .EXAMPLE Get-AACSkuAvailability -Service Aks -Location uksouth, ukwest -SubscriptionId '00000000-0000-0000-0000-000000000000' -Architecture Arm64 -HtmlPath .\out\Skus.html Arm64 sizes for AKS in two regions, as an HTML report. .EXAMPLE Get-AACSkuAvailability -Location westeurope -Zone 1, 2, 3 -NoDisplay | Where-Object Status -EQ 'Available' | Where-Object { $_.vCPUs -eq 4 -and $_.MemoryGB -ge 16 } Every 4 vCPU, 16 GB+ size usable in all three zones. .OUTPUTS AAC.SkuAvailability (piped onward, or with -PassThru or -NoDisplay) #> [CmdletBinding()] [OutputType('AAC.SkuAvailability')] param( [ValidateNotNullOrEmpty()] [string[]] $Location, [ValidateSet('VirtualMachine', 'Aks')] [string] $Service = 'VirtualMachine', [ValidatePattern('^\d$')] [string[]] $Zone, [ValidatePattern('^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$')] [string[]] $SubscriptionId, [ValidateNotNullOrEmpty()] [string] $ClusterName, [ValidateNotNullOrEmpty()] [string] $ResourceGroupName, [ValidateNotNullOrEmpty()] [string[]] $Series, [ValidateNotNullOrEmpty()] [string[]] $Sku, [ValidateSet('x64', 'Arm64')] [string] $Architecture, [ValidateRange(1, 5000)] [int] $NodeCount = 1, [string] $CsvPath, [string] $PdfPath, [string] $HtmlPath, [string] $Title = 'VM size availability', [switch] $PassThru, [switch] $NoDisplay, [switch] $NoPaging ) # A failure anywhere below ends as a Spectre.Console error panel and this # command's own terminating error, not a line inside the module. A stopped # pipeline (Select-Object -First, Ctrl+C) is no failure: just return - a # rethrow would stop the caller's whole script, not only this command. trap { if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { return }; $PSCmdlet.ThrowTerminatingError((Show-AACError -ErrorRecord $_ -Cmdlet $PSCmdlet)) } if (-not $Location -and -not $ClusterName) { throw 'Say where to look: -Location (for example uksouth), or -ClusterName for an AKS cluster''s region.' } if ($ResourceGroupName -and -not $ClusterName) { throw '-ResourceGroupName is the AKS cluster''s resource group: use it with -ClusterName.' } if ($ClusterName) { $Service = 'Aks' } $pipedOnward = $MyInvocation.PipelinePosition -lt $MyInvocation.PipelineLength $interactive = -not $NoDisplay -and -not $pipedOnward $showView = $interactive -and -not ($CsvPath -or $PdfPath -or $HtmlPath) $returnObjects = $PassThru -or $NoDisplay -or $pipedOnward $resolve = { param([string] $Path) if ($Path) { $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) } } $csvFullPath = & $resolve $CsvPath $pdfFullPath = & $resolve $PdfPath $htmlFullPath = & $resolve $HtmlPath $label = if ($Service -eq 'Aks') { 'AKS node pool' } else { 'VM' } if ($interactive) { Write-AACRule -Title "Azure Admin Console :: $label size availability" -Color 'deepskyblue3_1' } $state = Invoke-AACProgress -ScriptBlock { $null = Get-AACAccessToken $escape = { param([string] $Text) $Text -replace "'", "\'" } # --- The AKS cluster, when asked: its region, subscription and node pools ------------------------------ $cluster = $null $pools = @() if ($ClusterName) { Update-AACProgress -Id 'cluster' -Total 1 -Description "Finding the AKS cluster '$ClusterName'" $query = "resources | where type =~ 'microsoft.containerservice/managedclusters' and name =~ '$(& $escape $ClusterName)'$(if ($ResourceGroupName) { " and resourceGroup =~ '$(& $escape $ResourceGroupName)'" }) | project id, name, resourceGroup, subscriptionId, location, pools = properties.agentPoolProfiles" $found = @((Invoke-AACGraphBatch -Query @{ cluster = $query } -SubscriptionId $SubscriptionId).Rows['cluster']) if (-not $found.Count) { throw "No AKS cluster named '$ClusterName'$(if ($ResourceGroupName) { " in resource group '$ResourceGroupName'" }) was found in the subscriptions the account can see." } if ($found.Count -gt 1) { throw "$($found.Count) AKS clusters are named '$ClusterName' (in $(@($found | ForEach-Object { "$($_['resourceGroup'])" }) -join ', ')): add -ResourceGroupName." } $cluster = $found[0] $pools = @(foreach ($pool in @($cluster['pools'])) { if ($null -eq $pool) { continue } @{ Name = [string]$pool['name']; Mode = [string]$pool['mode']; VmSize = [string]$pool['vmSize']; Count = $pool['count']; Zones = @($pool['availabilityZones']); OsType = [string]$pool['osType'] } }) if (-not $Location) { $Location = @([string]$cluster['location']) } if (-not $SubscriptionId) { $SubscriptionId = @([string]$cluster['subscriptionId']) } Update-AACProgress -Id 'cluster' -Complete -Description "Found $ClusterName in $($cluster['location']): $($pools.Count) node pool(s)" } $locations = @($Location | ForEach-Object { ($_ -replace '\s', '').ToLowerInvariant() } | Select-Object -Unique) # --- Subscriptions: their names, and which to check ------------------------------------------------------ Update-AACProgress -Id 'subs' -Total 1 -Description 'Reading the subscriptions' $all = @((Invoke-AACArmParallel -Uri @('/subscriptions?api-version=2022-12-01'))['/subscriptions?api-version=2022-12-01'].Items) $names = @{} foreach ($item in $all) { $names[([string]$item['subscriptionId']).ToLowerInvariant()] = [string]$item['displayName'] } if (-not $SubscriptionId) { $enabled = @($all | Where-Object { $_['state'] -eq 'Enabled' }) if ($enabled.Count -ne 1) { throw "Restrictions and quota are per subscription: pass -SubscriptionId ($(if ($enabled.Count) { "the account can see $($enabled.Count)" } else { 'the account can see none' }))." } $SubscriptionId = @([string]$enabled[0]['subscriptionId']) } $subscriptions = @($SubscriptionId | ForEach-Object { $_.ToLowerInvariant() } | Select-Object -Unique) Update-AACProgress -Id 'subs' -Complete -Description "Checking $($subscriptions.Count) subscription(s) in $($locations -join ', ')" # --- Sizes, quota and zones: every subscription and region at once --------------------------------------- $uris = [ordered]@{} foreach ($subscription in $subscriptions) { $uris["$subscription|zones"] = "/subscriptions/$subscription/locations?api-version=2022-12-01" foreach ($region in $locations) { $uris["$subscription|$region|skus"] = "/subscriptions/$subscription/providers/Microsoft.Compute/skus?api-version=2021-07-01&`$filter=$([uri]::EscapeDataString("location eq '$region'"))" $uris["$subscription|$region|usages"] = "/subscriptions/$subscription/providers/Microsoft.Compute/locations/$region/usages?api-version=2023-09-01" } } Update-AACProgress -Id 'read' -Total $uris.Count -Description 'Reading VM sizes, quota and zones' $read = Invoke-AACArmParallel -Uri @($uris.Values) -OnProgress { param($Done, $Total) Update-AACProgress -Id 'read' -Increment 1 -Description "Read $Done of $Total (sizes, quota, zones)" } $notices = [System.Collections.Generic.List[string]]::new() $entries = @(foreach ($subscription in $subscriptions) { $zoneResult = $read[$uris["$subscription|zones"]] if ($zoneResult.Error) { $notices.Add("The zone mapping of $(if ($names.Contains($subscription)) { $names[$subscription] } else { $subscription }) couldn't be read: $($zoneResult.Error)") } foreach ($region in $locations) { $skuResult = $read[$uris["$subscription|$region|skus"]] if ($skuResult.Error) { throw "The VM sizes in $region couldn't be read for subscription $subscription`: $($skuResult.Error)" } $usageResult = $read[$uris["$subscription|$region|usages"]] $mapping = @(foreach ($place in @($zoneResult.Items)) { if ($place -and [string]$place['name'] -eq $region) { @($place['availabilityZoneMappings']) } }) @{ SubscriptionId = $subscription SubscriptionName = $(if ($names.Contains($subscription)) { $names[$subscription] } else { $subscription }) Location = $region Skus = @($skuResult.Items) Usages = $(if ($usageResult.Error) { @() } else { @($usageResult.Items) }) ZoneMappings = @($mapping | Where-Object { $_ }) } } }) $availability = ConvertTo-AACSkuAvailability -Read $entries -Service $Service -Zone @($Zone | Where-Object { $_ }) -Series @($Series | Where-Object { $_ }) -Sku @($Sku | Where-Object { $_ }) -Architecture $Architecture -NodeCount $NodeCount -Pool $pools -PoolLocation $(if ($cluster) { [string]$cluster['location'] } else { '' }) $stats = $availability.Stats Update-AACProgress -Id 'read' -Complete -Description ('{0:N0} size(s): {1:N0} available, {2:N0} in some zones, {3:N0} short of quota, {4:N0} restricted' -f $stats.Sizes, $stats.Available, $stats.Partial, $stats.NoQuota, $stats.Restricted) foreach ($notice in $availability.Notice) { $notices.Add($notice) } if ($Zone -and -not @($availability.Zones).Count) { $notices.Add('No zone mapping was returned: the region may have no availability zones.') } if (($Series -or $Sku -or $Architecture) -and -not $stats.Sizes) { $notices.Add('No size matches -Series, -Sku or -Architecture in this region.') } $availability.Notice = $notices.ToArray() $scope = [ordered]@{ Service = $(if ($Service -eq 'Aks') { 'AKS node pools' } else { 'Virtual machines' }) Regions = $locations -join ', ' Subscriptions = (@($subscriptions | ForEach-Object { if ($names.Contains($_)) { $names[$_] } else { $_ } }) -join ', ') } if ($cluster) { $scope['Cluster'] = "$($cluster['name']) ($($cluster['resourceGroup']))" } if ($Zone) { $scope['Zones'] = ($Zone | Sort-Object -Unique) -join ', ' } if ($Series) { $scope['Series'] = $Series -join ', ' } if ($Sku) { $scope['Sizes'] = $Sku -join ', ' } if ($Architecture) { $scope['Architecture'] = $Architecture } $scope['Quota for'] = "$NodeCount $(if ($Service -eq 'Aks') { 'node(s)' } else { 'VM(s)' })" $null = Invoke-AACExport -CsvPath $csvFullPath -CsvObject @($availability.Skus) -Noun 'VM size' -PdfPath $pdfFullPath -WritePdf { Write-AACSkuAvailabilityPdf -Availability $availability -Path $pdfFullPath -Title $Title -Detail $scope } -HtmlPath $htmlFullPath -WriteHtml { Write-AACSkuAvailabilityHtml -Availability $availability -Path $htmlFullPath -Title $Title -Detail $scope } @{ Availability = $availability; Scope = $scope } } if ($showView) { Invoke-AACPagedOutput -NoPaging:$NoPaging -ScriptBlock { Show-AACSkuAvailabilityView -Availability $state.Availability -Scope $state.Scope } } elseif ($interactive) { foreach ($notice in @($state.Availability.Notice)) { Write-AACMarkup "[grey58]$([Spectre.Console.Markup]::Escape($notice))[/]" } } if ($returnObjects) { $state.Availability.Skus } } |