Private/Show-AACSkuAvailabilityView.ps1
|
function Show-AACSkuAvailabilityView { <# .SYNOPSIS Renders Get-AACSkuAvailability's result as a Spectre.Console view. .DESCRIPTION The scope, tiles (sizes checked, available, in some zones, short of quota, zone unavailable, not supported, restricted), then: Zones logical to physical, per subscription and region Quota the region's vCPUs and each family the sizes use, most used first Node pools -ClusterName: each pool's size, its status now, and the family's free vCPUs Sizes each size with its status, vCPUs, memory, zones, features and reason - usable first, up to -MaxSize Wrap the call in Invoke-AACPagedOutput to page it. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable] $Availability, [System.Collections.IDictionary] $Scope, [int] $MaxSize = 250 ) $escape = { param($Text) [Spectre.Console.Markup]::Escape([string]$Text) } $glyph = Get-AACGlyph $stats = $Availability.Stats $statusColor = @{ Available = 'green3'; Partial = 'orange1'; NoQuota = 'orange1'; ZoneUnavailable = 'red1'; NotSupported = 'red1'; Restricted = 'red1'; NotChecked = 'grey50' } $statusText = @{ Available = 'Available'; Partial = 'Some zones'; NoQuota = 'No quota'; ZoneUnavailable = 'Not in zones'; NotSupported = 'Not for AKS'; Restricted = 'Restricted'; NotChecked = 'Not checked' } $status = { param([string] $Name) "[bold $($statusColor[$Name])]$($statusText[$Name])[/]" } $number = { param($Value) if ($null -eq $Value) { '[grey50]-[/]' } else { '{0:N0}' -f $Value } } $newTable = { param([string] $Title, [string] $Color, [object[]] $Columns) $table = [Spectre.Console.Table]::new() $table.Border = [Spectre.Console.TableBorder]::Rounded $table.BorderStyle = [Spectre.Console.Style]::Parse($Color) $table.Expand = $true $table.Title = [Spectre.Console.TableTitle]::new($Title) foreach ($column in $Columns) { $spectre = [Spectre.Console.TableColumn]::new("[grey62]$($column[0])[/]") if ($column[1]) { $spectre.Alignment = [Spectre.Console.Justify]::Right; $spectre.NoWrap = $true } if ($column.Count -gt 2 -and $column[2]) { $spectre.NoWrap = $true } $table.AddColumn($spectre) | Out-Null } $table } $addRow = { param($Table, [string[]] $Cells) [Spectre.Console.TableExtensions]::AddRow($Table, [Spectre.Console.Rendering.IRenderable[]]@($Cells | ForEach-Object { [Spectre.Console.Markup]::new($_) })) | Out-Null } $multiple = $stats.Locations -gt 1 -or $stats.Subscriptions -gt 1 # The features column only where there's room for it; the reports always have it. $wide = [Spectre.Console.AnsiConsole]::Profile.Width -ge 140 $facts = [System.Collections.Generic.List[string]]::new() if ($script:AACSession) { $facts.Add("[white]$(& $escape $script:AACSession.Account)[/]") } if ($Scope) { foreach ($key in $Scope.Keys) { $facts.Add("$(& $escape $key): $(& $escape $Scope[$key])") } } $facts.Add((Get-Date).ToString('d MMM yyyy HH:mm')) Write-AACMarkup "[grey58]$($facts -join " $($glyph.Dot) ")[/]" [Spectre.Console.AnsiConsole]::WriteLine() Show-AACTileRow -Tile @( @{ Value = '{0:N0}' -f $stats.Sizes; Caption = 'sizes checked'; Color = 'deepskyblue1' } @{ Value = '{0:N0}' -f $stats.Available; Caption = 'available'; Color = $(if ($stats.Available) { 'green3' } else { 'grey50' }) } @{ Value = '{0:N0}' -f $stats.Partial; Caption = 'in some zones only'; Color = $(if ($stats.Partial) { 'orange1' } else { 'grey50' }) } @{ Value = '{0:N0}' -f $stats.NoQuota; Caption = 'short of quota'; Color = $(if ($stats.NoQuota) { 'orange1' } else { 'grey50' }) } @{ Value = '{0:N0}' -f ($stats.ZoneUnavailable + $stats.NotSupported); Caption = 'not in the zones / not for AKS'; Color = $(if ($stats.ZoneUnavailable + $stats.NotSupported) { 'red1' } else { 'grey50' }) } @{ Value = '{0:N0}' -f $stats.Restricted; Caption = 'restricted for the subscription'; Color = $(if ($stats.Restricted) { 'red1' } else { 'grey50' }) } ) [Spectre.Console.AnsiConsole]::WriteLine() foreach ($notice in @($Availability.Notice | Where-Object { $_ })) { Write-AACMarkup "[deepskyblue1]i[/] [grey70]$(& $escape $notice)[/]" } if (@($Availability.Notice).Count) { [Spectre.Console.AnsiConsole]::WriteLine() } # --- Zones: logical to physical ------------------------------------------------------------------------- if (@($Availability.Zones).Count) { $table = & $newTable "[bold]$($glyph.Bullet) Availability zones[/] [grey58]$($glyph.Dot) logical zone to physical zone; another subscription's zone 1 may be a different datacentre[/]" 'grey42' @(@('Subscription', $false), @('Region', $false), @('Zones', $false)) foreach ($group in @($Availability.Zones | Group-Object -Property SubscriptionName, Location)) { $first = $group.Group[0] & $addRow $table @("[bold]$(& $escape $first.SubscriptionName)[/]", (& $escape $first.Location), ((@($group.Group | Sort-Object LogicalZone | ForEach-Object { "[white]$($_.LogicalZone)[/] [grey50]->[/] $(& $escape $_.PhysicalZone)" })) -join ' ')) } [Spectre.Console.AnsiConsole]::Write($table) [Spectre.Console.AnsiConsole]::WriteLine() } # --- Quota ----------------------------------------------------------------------------------------------- if (@($Availability.Quotas).Count) { $columns = [System.Collections.Generic.List[object]]::new() if ($multiple) { $columns.Add(@('Subscription', $false)); $columns.Add(@('Region', $false)) } foreach ($column in @(@('vCPU quota', $false), @('Used', $true), @('Limit', $true), @('Free', $true), @('Used %', $true))) { $columns.Add($column) } $table = & $newTable "[bold mediumpurple2]$($glyph.Bullet) vCPU quota[/] [grey58]$($glyph.Dot) the region, then each family of the sizes checked, most used first[/]" 'mediumpurple2' $columns.ToArray() $shown = @($Availability.Quotas | Where-Object { -not $_.Family -or $_.Used -gt 0 -or $_.Limit -eq 0 } | Select-Object -First 40) foreach ($item in $shown) { $color = if ($null -eq $item.UsedPercent) { 'grey50' } elseif ($item.UsedPercent -ge 90) { 'red1' } elseif ($item.UsedPercent -ge 70) { 'orange1' } else { 'green3' } & $addRow $table @( if ($multiple) { (& $escape $item.SubscriptionName); (& $escape $item.Location) } $(if ($item.Family) { & $escape $item.Quota } else { "[bold]$(& $escape $item.Quota)[/]" }) (& $number $item.Used); (& $number $item.Limit) $(if ($item.Free -le 0) { "[bold red1]$(& $number $item.Free)[/]" } else { & $number $item.Free }) $(if ($null -eq $item.UsedPercent) { '[grey50]-[/]' } else { "[$color]$($item.UsedPercent)%[/]" }) ) } [Spectre.Console.AnsiConsole]::Write($table) $unused = @($Availability.Quotas).Count - $shown.Count if ($unused -gt 0) { Write-AACMarkup "[grey50] $unused more famil$(if ($unused -eq 1) { 'y' } else { 'ies' }) with nothing used; -HtmlPath or -PdfPath lists them.[/]" } [Spectre.Console.AnsiConsole]::WriteLine() } # --- AKS node pools -------------------------------------------------------------------------------------- if (@($Availability.Pools).Count) { $table = & $newTable "[bold deepskyblue1]$($glyph.Bullet) The cluster's node pools[/] [grey58]$($glyph.Dot) their size here now, and the family's free vCPUs[/]" 'deepskyblue3' @(@('Pool', $false), @('Size', $false, $true), @('Nodes', $true), @('Zones', $false), @('Size status', $false), @('Family free', $true), @('Why', $false)) foreach ($item in $Availability.Pools) { & $addRow $table @( "[bold]$(& $escape $item.Pool)[/]$(if ($item.Mode) { "`n[grey50]$(& $escape $item.Mode)$(if ($item.OsType) { ", $(& $escape $item.OsType)" })[/]" })" (& $escape $item.VmSize); (& $number $item.Nodes) $(if ($item.Zones) { & $escape $item.Zones } else { '[grey50]none[/]' }) (& $status $item.SizeStatus); (& $number $item.FamilyVCpuFree) "[grey70]$(& $escape $item.Reason)[/]" ) } [Spectre.Console.AnsiConsole]::Write($table) [Spectre.Console.AnsiConsole]::WriteLine() } # --- Sizes ----------------------------------------------------------------------------------------------- $skus = @($Availability.Skus) if ($skus.Count) { $columns = [System.Collections.Generic.List[object]]::new() foreach ($column in @(@('Size', $false, $true), @('Status', $false))) { $columns.Add($column) } if ($multiple) { $columns.Add(@('Where', $false)) } foreach ($column in @(@('vCPUs', $true), @('GB', $true), @('Zones', $false))) { $columns.Add($column) } if ($wide) { $columns.Add(@('Family free', $true)); $columns.Add(@('Features', $false)) } $columns.Add(@('Why', $false)) $table = & $newTable "[bold green3]$($glyph.Bullet) VM sizes[/] [grey58]$($glyph.Dot) usable first$(if ($skus.Count -gt $MaxSize) { ", the first $MaxSize of $($skus.Count)" })[/]" 'green4' $columns.ToArray() foreach ($item in @($skus | Select-Object -First $MaxSize)) { $features = @( if ($item.Architecture -eq 'Arm64') { 'Arm64' } if ($item.GPUs) { "$($item.GPUs) GPU" } if ($item.EphemeralOSDisk) { 'ephemeral OS' } if ($item.AcceleratedNetworking) { 'accel. net' } if ($item.PremiumStorage) { 'premium' } ) $zones = if ($item.Zones) { & $escape $item.Zones } else { '[grey50]none[/]' } if ($item.ZonesMissing) { $zones += " [red1]-$(& $escape $item.ZonesMissing)[/]" } $why = @($item.Reason, $item.Note, $(if ($item.InUse) { "in use: $($item.InUse)" })) | Where-Object { $_ } & $addRow $table @( "[bold]$(& $escape $item.Sku)[/]" (& $status $item.Status) if ($multiple) { "[grey70]$(& $escape $item.Location)$(if ($stats.Subscriptions -gt 1) { "`n$(& $escape $item.SubscriptionName)" })[/]" } (& $number $item.vCPUs); ('{0:0.##}' -f $item.MemoryGB); $zones if ($wide) { (& $number $item.FamilyVCpuFree); "[grey70]$(& $escape ($features -join ', '))[/]" } "[grey70]$(& $escape ($why -join '; '))[/]" ) } [Spectre.Console.AnsiConsole]::Write($table) if ($skus.Count -gt $MaxSize) { Write-AACMarkup "[grey50] ... and $($skus.Count - $MaxSize) more: narrow with -Series, -Sku or -Architecture, or use -PassThru, -CsvPath or -HtmlPath for them all.[/]" } [Spectre.Console.AnsiConsole]::WriteLine() } Write-AACMarkup '[grey42]Read-only: Microsoft.Compute SKUs (offered zones and the subscription''s restrictions), vCPU usage and the zone mapping. Capacity at deployment time can''t be checked without deploying. Add -PassThru (or pipe the command) for the sizes as objects; -CsvPath, -PdfPath or -HtmlPath for a report.[/]' } |