Public/Resolve-AzLocalSideloadPlan.ps1
|
function Resolve-AzLocalSideloadPlan { <# .SYNOPSIS Builds the per-cluster sideload plan: which solution-update version each UpdateAuthAccountId-tagged Azure Local cluster should have sideloaded, and whether it is due within the configured lead window. .DESCRIPTION Public planner for the v0.8.7 on-prem sideloading automation. It does NOT change any state - it only reads the fleet (Azure Resource Graph), the apply-updates schedule, the auth map and the catalog, then emits one plan object per cluster (plus error rows for misconfigurations such as an unknown auth account id, a version not in the allow-list, or a missing catalog entry). The plan objects feed Invoke-AzLocalSideloadUpdate (the re-entrant Step.6 state machine). Selection reuses Select-AzLocalNextUpdateForCluster so the sideload path and the apply path agree on which update is "next". .PARAMETER SchedulePath Path to the apply-updates schedule YAML (Get-AzLocalApplyUpdatesScheduleConfig). .PARAMETER AuthMapPath Path to the sideload auth-map CSV (Get-AzLocalSideloadAuthMap). .PARAMETER CatalogPath Path to the sideload catalog YAML (Get-AzLocalSideloadCatalog). .PARAMETER LeadDays How many days before a cluster's next apply window the media should be sideloaded. A cluster is "due" when (nextWindow - LeadDays) <= Now. .PARAMETER UpdateRingValue Optional UpdateRing tag filter (single, list, or '***' wildcard for all rings). When omitted, all clusters carrying an UpdateAuthAccountId tag are considered. .PARAMETER FqdnSuffix Global default FQDN suffix appended to a cluster name to form the remoting host when the auth-map row does not override it (SIDELOAD_REMOTING_FQDN_SUFFIX). .PARAMETER Now The reference time (UTC). Defaults to now. .PARAMETER SubscriptionId Optional subscription scope for the Resource Graph query. .OUTPUTS [PSCustomObject[]] plan + error rows. #> [CmdletBinding()] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$SchedulePath, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$AuthMapPath, [Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][string]$CatalogPath, [Parameter(Mandatory = $false)][ValidateRange(0, 365)][int]$LeadDays = 7, [Parameter(Mandatory = $false)][string]$UpdateRingValue = '***', [Parameter(Mandatory = $false)][string]$FqdnSuffix = '', [Parameter(Mandatory = $false)][datetime]$Now = [datetime]::UtcNow, [Parameter(Mandatory = $false)][string]$SubscriptionId ) $config = Get-AzLocalApplyUpdatesScheduleConfig -Path $SchedulePath $authMap = Get-AzLocalSideloadAuthMap -Path $AuthMapPath $catalog = @(Get-AzLocalSideloadCatalog -Path $CatalogPath) $ringFilter = ConvertTo-AzLocalUpdateRingKqlFilter -UpdateRingValue $UpdateRingValue $argQuery = "resources | where type =~ 'microsoft.azurestackhci/clusters' | where isnotempty(tags['UpdateAuthAccountId']) $ringFilter | project id, name, resourceGroup, subscriptionId, tags" $clusterRows = if ($PSBoundParameters.ContainsKey('SubscriptionId') -and $SubscriptionId) { Invoke-AzResourceGraphQuery -Query $argQuery -SubscriptionId $SubscriptionId } else { Invoke-AzResourceGraphQuery -Query $argQuery } # Pre-compute the next 366 days of firings so we can find each cluster's next window. $firings = @(Get-AzLocalApplyUpdatesScheduleNextFirings -Schedule $config -StartDate $Now.Date -Days 366) $results = New-Object System.Collections.Generic.List[object] foreach ($cluster in $clusterRows) { $clusterName = [string]$cluster.name $accountId = [string]$cluster.tags.UpdateAuthAccountId $ringTag = [string]$cluster.tags.UpdateRing $row = [ordered]@{ ClusterName = $clusterName ClusterResourceId = [string]$cluster.id ResourceGroup = [string]$cluster.resourceGroup SubscriptionId = [string]$cluster.subscriptionId UpdateAuthAccountId = $accountId Ring = $ringTag NextWindowUtc = $null LeadDays = $LeadDays DueNow = $false SelectedVersion = '' SelectedUpdateName = '' PackageType = '' CatalogEntry = $null RemotingHost = '' TargetPath = '' AuthRow = $null Status = 'Planned' Message = '' } # Auth-map row. if (-not $authMap.ContainsKey($accountId)) { $row.Status = 'UnknownAuthAccountId' $row.Message = "Cluster '$clusterName' has UpdateAuthAccountId '$accountId' which is not present in the auth map." $results.Add([PSCustomObject]$row); continue } $authRow = $authMap[$accountId] $row.AuthRow = $authRow # Remoting host + target path. $remotingHost = if (-not [string]::IsNullOrWhiteSpace([string]$authRow.RemotingTargetFqdn)) { [string]$authRow.RemotingTargetFqdn } else { $suffix = if (-not [string]::IsNullOrWhiteSpace([string]$authRow.FqdnSuffix)) { [string]$authRow.FqdnSuffix } else { $FqdnSuffix } if (-not [string]::IsNullOrWhiteSpace($suffix)) { ('{0}{1}' -f $clusterName, $suffix) } else { $clusterName } } $row.RemotingHost = $remotingHost $row.TargetPath = Resolve-AzLocalSideloadTargetPath -RemotingHost $remotingHost -ImportSharePath ([string]$authRow.ImportSharePath) # Next apply window for this cluster's ring. $nextFiring = $firings | Where-Object { $_.Rings -and ($_.Rings -contains $ringTag) -and ($_.DateUtc -ge $Now.Date) } | Sort-Object DateUtc | Select-Object -First 1 if ($nextFiring) { $row.NextWindowUtc = $nextFiring.DateUtc $dueDate = ([datetime]$nextFiring.DateUtc).AddDays(-$LeadDays) $row.DueNow = ($Now -ge $dueDate) } # Resolve allow-list for the cluster (today's resolution gives the list). $ringInfo = Resolve-AzLocalCurrentUpdateRing -Schedule $config -Now $Now $allowed = @($ringInfo.AllowedUpdateVersions) # Available Ready updates -> adapter shape for Select-AzLocalNextUpdateForCluster. $available = @(Get-AzLocalAvailableUpdates -ClusterResourceId ([string]$cluster.id) -ErrorAction SilentlyContinue) $readyAdapters = @( $available | Where-Object { [string]$_.UpdateState -eq 'Ready' } | ForEach-Object { [PSCustomObject]@{ name = [string]$_.UpdateName PackageType = [string]$_.PackageType properties = [PSCustomObject]@{ version = [string]$_.Version; state = [string]$_.UpdateState } } } ) $selection = Select-AzLocalNextUpdateForCluster -ReadyUpdates $readyAdapters -AllowedUpdateVersions $allowed switch ($selection.Reason) { 'Selected' { $sel = $selection.SelectedUpdate $version = [string]$sel.properties.version if ([string]::IsNullOrWhiteSpace($version)) { $version = [string]$sel.name } $row.SelectedVersion = $version $row.SelectedUpdateName = [string]$sel.name $row.PackageType = [string]$sel.PackageType $entry = $catalog | Where-Object { [string]$_.Version -eq $version } | Select-Object -First 1 if ($null -eq $entry) { $row.Status = 'NoCatalogEntry' $row.Message = "Selected version '$version' for cluster '$clusterName' has no entry in the sideload catalog." } else { $row.CatalogEntry = $entry if (-not $row.PackageType) { $row.PackageType = [string]$entry.PackageType } $row.Status = if ($row.DueNow) { 'Planned' } else { 'NotDue' } $row.Message = if ($row.DueNow) { "Cluster '$clusterName' due for sideload of '$version' (window $($row.NextWindowUtc))." } else { "Cluster '$clusterName' not yet within the $LeadDays-day lead window for '$version'." } } } 'NotInAllowList' { $row.Status = 'NotInAllowList' $row.Message = "No Ready update for cluster '$clusterName' matches the allow-list [$($selection.AllowDisplay)]. Ready: [$($selection.ReadyDisplay)]." } 'NoneReady' { $row.Status = 'NoneReady' $row.Message = "Cluster '$clusterName' has no Ready updates available." } default { $row.Status = [string]$selection.Reason $row.Message = "Cluster '$clusterName' selection reason: $($selection.Reason)." } } $results.Add([PSCustomObject]$row) } return $results.ToArray() } |