Private/Format/Invoke-RecommendMode.ps1
|
function Invoke-RecommendMode { param( [Parameter(Mandatory)] [string]$TargetSkuName, [Parameter(Mandatory)] [array]$SubscriptionData, [hashtable]$FamilyInfo = @{}, [hashtable]$Icons = @{}, [bool]$FetchPricing = $false, [bool]$ShowSpot = $false, [bool]$ShowPlacement = $false, [bool]$AllowMixedArch = $false, [int]$MinvCPU = 0, [int]$MinMemoryGB = 0, [Nullable[int]]$MinScore, [int]$TopN = 5, [int]$DesiredCount = 1, [bool]$JsonOutput = $false, [int]$MaxRetries = 3, [Parameter(Mandatory)] [pscustomobject]$RunContext, [int]$OutputWidth = 122, [hashtable]$SkuProfileCache = $null ) $targetSku = $null $targetRegionStatus = @() foreach ($subData in $SubscriptionData) { foreach ($data in $subData.RegionData) { $region = Get-SafeString $data.Region if ($data.Error) { continue } foreach ($sku in $data.Skus) { if ($sku.Name -eq $TargetSkuName) { $restrictions = Get-RestrictionDetails $sku $targetRegionStatus += [pscustomobject]@{ Region = [string]$region Status = $restrictions.Status ZonesOK = $restrictions.ZonesOK.Count } if (-not $targetSku) { $targetSku = $sku } } } } } if (-not $targetSku) { Write-Host "`nSKU '$TargetSkuName' was not found in any scanned region." -ForegroundColor Red Write-Host "Check the SKU name and ensure the scanned regions support this SKU family." -ForegroundColor Yellow return } $targetCaps = Get-SkuCapabilities -Sku $targetSku $targetProcessor = Get-ProcessorVendor -SkuName $targetSku.Name $targetHasNvme = $targetCaps.NvmeSupport $targetDiskCode = Get-DiskCode -HasTempDisk ($targetCaps.TempDiskGB -gt 0) -HasNvme $targetHasNvme $targetProfile = @{ Name = $targetSku.Name vCPU = [int](Get-CapValue $targetSku 'vCPUs') MemoryGB = [int](Get-CapValue $targetSku 'MemoryGB') Family = Get-SkuFamily $targetSku.Name FamilyVersion = Get-SkuFamilyVersion $targetSku.Name Generation = $targetCaps.HyperVGenerations Architecture = $targetCaps.CpuArchitecture PremiumIO = (Get-CapValue $targetSku 'PremiumIO') -eq 'True' Processor = $targetProcessor TempDiskGB = $targetCaps.TempDiskGB DiskCode = $targetDiskCode AccelNet = $targetCaps.AcceleratedNetworkingEnabled MaxDataDiskCount = $targetCaps.MaxDataDiskCount MaxNetworkInterfaces = $targetCaps.MaxNetworkInterfaces EphemeralOSDiskSupported = $targetCaps.EphemeralOSDiskSupported UltraSSDAvailable = $targetCaps.UltraSSDAvailable UncachedDiskIOPS = $targetCaps.UncachedDiskIOPS UncachedDiskBytesPerSecond = $targetCaps.UncachedDiskBytesPerSecond EncryptionAtHostSupported = $targetCaps.EncryptionAtHostSupported } # Score all candidate SKUs across all regions $candidates = [System.Collections.Generic.List[object]]::new() foreach ($subData in $SubscriptionData) { foreach ($data in $subData.RegionData) { $region = Get-SafeString $data.Region if ($data.Error) { continue } foreach ($sku in $data.Skus) { if ($sku.Name -eq $TargetSkuName) { continue } $restrictions = Get-RestrictionDetails $sku if ($restrictions.Status -eq 'RESTRICTED') { continue } # Use cached profile if available; otherwise build and cache it $candidateProfile = $null $caps = $null $candidateProcessor = $null $candidateDiskCode = $null if ($SkuProfileCache -and $SkuProfileCache.ContainsKey($sku.Name)) { $cached = $SkuProfileCache[$sku.Name] $candidateProfile = $cached.Profile $caps = $cached.Caps $candidateProcessor = $cached.Processor $candidateDiskCode = $cached.DiskCode } else { $caps = Get-SkuCapabilities -Sku $sku $candidateProcessor = Get-ProcessorVendor -SkuName $sku.Name $candidateHasNvme = $caps.NvmeSupport $candidateDiskCode = Get-DiskCode -HasTempDisk ($caps.TempDiskGB -gt 0) -HasNvme $candidateHasNvme $candidateProfile = @{ Name = $sku.Name vCPU = [int](Get-CapValue $sku 'vCPUs') MemoryGB = [int](Get-CapValue $sku 'MemoryGB') Family = Get-SkuFamily $sku.Name FamilyVersion = Get-SkuFamilyVersion $sku.Name Generation = $caps.HyperVGenerations Architecture = $caps.CpuArchitecture PremiumIO = (Get-CapValue $sku 'PremiumIO') -eq 'True' DiskCode = $candidateDiskCode AccelNet = $caps.AcceleratedNetworkingEnabled MaxDataDiskCount = $caps.MaxDataDiskCount MaxNetworkInterfaces = $caps.MaxNetworkInterfaces EphemeralOSDiskSupported = $caps.EphemeralOSDiskSupported UltraSSDAvailable = $caps.UltraSSDAvailable UncachedDiskIOPS = $caps.UncachedDiskIOPS UncachedDiskBytesPerSecond = $caps.UncachedDiskBytesPerSecond EncryptionAtHostSupported = $caps.EncryptionAtHostSupported } if ($SkuProfileCache) { $SkuProfileCache[$sku.Name] = @{ Profile = $candidateProfile; Caps = $caps; Processor = $candidateProcessor; DiskCode = $candidateDiskCode } } } # Architecture filtering — skip candidates that don't match target arch unless opted out if (-not $AllowMixedArch -and $candidateProfile.Architecture -ne $targetProfile.Architecture) { continue } # Hard compatibility gate — candidate must meet or exceed target on critical dimensions $compat = Test-SkuCompatibility -Target $targetProfile -Candidate $candidateProfile if (-not $compat.Compatible) { continue } $simScore = Get-SkuSimilarityScore -Target $targetProfile -Candidate $candidateProfile -FamilyInfo $FamilyInfo $priceHr = $null $priceMo = $null $spotPriceHr = $null $spotPriceMo = $null if ($FetchPricing -and $RunContext.RegionPricing[[string]$region]) { $regionPriceData = $RunContext.RegionPricing[[string]$region] $regularPriceMap = Get-RegularPricingMap -PricingContainer $regionPriceData $spotPriceMap = Get-SpotPricingMap -PricingContainer $regionPriceData $skuPricing = $regularPriceMap[$sku.Name] if ($skuPricing) { $priceHr = $skuPricing.Hourly $priceMo = $skuPricing.Monthly } if ($ShowSpot) { $spotPricing = $spotPriceMap[$sku.Name] if ($spotPricing) { $spotPriceHr = $spotPricing.Hourly $spotPriceMo = $spotPricing.Monthly } } } $candidates.Add([pscustomobject]@{ SKU = $sku.Name Region = [string]$region vCPU = $candidateProfile.vCPU MemGiB = $candidateProfile.MemoryGB Family = $candidateProfile.Family Purpose = if ($FamilyInfo[$candidateProfile.Family]) { $FamilyInfo[$candidateProfile.Family].Purpose } else { '' } Gen = (($caps.HyperVGenerations -replace 'V', '') -replace ',', ',') Arch = $candidateProfile.Architecture CPU = $candidateProcessor Disk = $candidateDiskCode TempGB = $caps.TempDiskGB AccelNet = $caps.AcceleratedNetworkingEnabled MaxDisks = $caps.MaxDataDiskCount MaxNICs = $caps.MaxNetworkInterfaces IOPS = $caps.UncachedDiskIOPS Score = $simScore Capacity = $restrictions.Status ZonesOK = $restrictions.ZonesOK.Count PriceHr = $priceHr PriceMo = $priceMo SpotPriceHr = $spotPriceHr SpotPriceMo = $spotPriceMo }) | Out-Null } } } # Apply minimum spec filters and separate smaller options for callout $belowMinSpecDict = @{} $filtered = @($candidates) if ($MinvCPU) { $filtered | Where-Object { $_.vCPU -lt $MinvCPU -and $_.Capacity -eq 'OK' } | ForEach-Object { if (-not $belowMinSpecDict.ContainsKey($_.SKU)) { $belowMinSpecDict[$_.SKU] = $_ } } $filtered = @($filtered | Where-Object { $_.vCPU -ge $MinvCPU }) } if ($MinMemoryGB) { $filtered | Where-Object { $_.MemGiB -lt $MinMemoryGB -and $_.Capacity -eq 'OK' } | ForEach-Object { if (-not $belowMinSpecDict.ContainsKey($_.SKU)) { $belowMinSpecDict[$_.SKU] = $_ } } $filtered = @($filtered | Where-Object { $_.MemGiB -ge $MinMemoryGB }) } $belowMinSpec = @($belowMinSpecDict.Values) if ($null -ne $MinScore) { $filtered = @($filtered | Where-Object { $_.Score -ge $MinScore }) } if (-not $filtered -or $filtered.Count -eq 0) { $RunContext.RecommendOutput = New-RecommendOutputContract -TargetProfile $targetProfile -TargetAvailability @($targetRegionStatus) -RankedRecommendations @() -Warnings @() -BelowMinSpec @($belowMinSpec) -MinScore $MinScore -TopN $TopN -FetchPricing ([bool]$FetchPricing) -ShowPlacement ([bool]$ShowPlacement) -ShowSpot ([bool]$ShowSpot ) if ($JsonOutput) { $RunContext.RecommendOutput | ConvertTo-Json -Depth 6 return } Write-RecommendOutputContract -Contract $RunContext.RecommendOutput -Icons $Icons -FetchPricing ([bool]$FetchPricing) -FamilyInfo $FamilyInfo -OutputWidth $OutputWidth return } $ranked = $filtered | Sort-Object @{Expression = 'Score'; Descending = $true }, @{Expression = { if ($_.Capacity -eq 'OK') { 0 } elseif ($_.Capacity -eq 'LIMITED') { 1 } else { 2 } } }, @{Expression = 'ZonesOK'; Descending = $true } | Group-Object SKU | ForEach-Object { $_.Group | Select-Object -First 1 } | Select-Object -First $TopN if ($ShowPlacement) { $placementScores = Get-PlacementScores -SkuNames @($ranked | Select-Object -ExpandProperty SKU) -Regions @($ranked | Select-Object -ExpandProperty Region) -DesiredCount $DesiredCount -MaxRetries $MaxRetries -Caches $RunContext.Caches $ranked = @($ranked | ForEach-Object { $item = $_ $key = "{0}|{1}" -f $item.SKU, $item.Region.ToLower() $allocScore = if ($placementScores.ContainsKey($key)) { $placementScores[$key].Score } else { 'N/A' } [pscustomobject]@{ SKU = $item.SKU Region = $item.Region vCPU = $item.vCPU MemGiB = $item.MemGiB Family = $item.Family Purpose = $item.Purpose Gen = $item.Gen Arch = $item.Arch CPU = $item.CPU Disk = $item.Disk TempGB = $item.TempGB AccelNet = $item.AccelNet MaxDisks = $item.MaxDisks MaxNICs = $item.MaxNICs IOPS = $item.IOPS Score = $item.Score Capacity = $item.Capacity AllocScore = $allocScore ZonesOK = $item.ZonesOK PriceHr = $item.PriceHr PriceMo = $item.PriceMo SpotPriceHr = $item.SpotPriceHr SpotPriceMo = $item.SpotPriceMo } }) } else { $ranked = @($ranked | ForEach-Object { $item = $_ [pscustomobject]@{ SKU = $item.SKU Region = $item.Region vCPU = $item.vCPU MemGiB = $item.MemGiB Family = $item.Family Purpose = $item.Purpose Gen = $item.Gen Arch = $item.Arch CPU = $item.CPU Disk = $item.Disk TempGB = $item.TempGB AccelNet = $item.AccelNet MaxDisks = $item.MaxDisks MaxNICs = $item.MaxNICs IOPS = $item.IOPS Score = $item.Score Capacity = $item.Capacity AllocScore = $null ZonesOK = $item.ZonesOK PriceHr = $item.PriceHr PriceMo = $item.PriceMo SpotPriceHr = $item.SpotPriceHr SpotPriceMo = $item.SpotPriceMo } }) } # Compatibility warning detection (shared by JSON and console output) $compatWarnings = @() $uniqueCPUs = @($ranked | Select-Object -ExpandProperty CPU -Unique) $uniqueAccelNet = @($ranked | Select-Object -ExpandProperty AccelNet -Unique) if ($AllowMixedArch) { $uniqueArchs = @($ranked | Select-Object -ExpandProperty Arch -Unique) if ($uniqueArchs.Count -gt 1) { $compatWarnings += "Mixed architectures (x64 + ARM64) — each requires a separate OS image." } } if ($uniqueCPUs.Count -gt 1) { $compatWarnings += "Mixed CPU vendors ($($uniqueCPUs -join ', ')) — performance characteristics vary." } $hasTempDisk = @($ranked | Where-Object { $_.Disk -match 'T' }) $noTempDisk = @($ranked | Where-Object { $_.Disk -notmatch 'T' }) if ($hasTempDisk.Count -gt 0 -and $noTempDisk.Count -gt 0) { $compatWarnings += "Mixed temp disk configs — some SKUs have local temp disk, others don't. Drive paths differ." } $hasNvme = @($ranked | Where-Object { $_.Disk -match 'NV' }) $hasScsi = @($ranked | Where-Object { $_.Disk -match 'SC' }) if ($hasNvme.Count -gt 0 -and $hasScsi.Count -gt 0) { $compatWarnings += "Mixed storage interfaces (NVMe vs SCSI) — disk driver and device path differences." } if ($uniqueAccelNet.Count -gt 1) { $compatWarnings += "Mixed accelerated networking support — network performance will vary across the inventory." } $RunContext.RecommendOutput = New-RecommendOutputContract -TargetProfile $targetProfile -TargetAvailability @($targetRegionStatus) -RankedRecommendations @($ranked) -Warnings @($compatWarnings) -BelowMinSpec @($belowMinSpec) -MinScore $MinScore -TopN $TopN -FetchPricing ([bool]$FetchPricing) -ShowPlacement ([bool]$ShowPlacement) -ShowSpot ([bool]$ShowSpot ) if ($JsonOutput) { $RunContext.RecommendOutput | ConvertTo-Json -Depth 6 return } Write-RecommendOutputContract -Contract $RunContext.RecommendOutput -Icons $Icons -FetchPricing ([bool]$FetchPricing) -FamilyInfo $FamilyInfo -OutputWidth $OutputWidth } |