Private/Invoke-AzLocalRemoteSolutionImport.ps1
|
function Invoke-AzLocalRemoteSolutionImport { <# .SYNOPSIS Imports (stages + discovers) a sideloaded solution update on an Azure Local cluster node over WinRM so it becomes available to the apply pipeline. .DESCRIPTION Private helper for the v0.8.7 on-prem sideloading automation. Runs the documented offline import sequence on the target node: 1. Expand the CombinedSolutionBundle .zip (already robocopied into the import share) into the import\Solution folder. 2. Run Add-SolutionUpdate against the import folder to register it. 3. Poll Get-SolutionUpdate until the matching version is discovered, or until it reports AdditionalContentRequired (an OEM SBE package must be staged alongside it), or the discovery timeout elapses. This does NOT apply/install the update - that remains the job of the apply pipeline (Step.7) once UpdateSideloaded is flipped True. The function only confirms the media is discoverable so the gate can be opened. For SBE packages the operator-staged SBE content is already robocopied into the import share; the same Add-SolutionUpdate / Get-SolutionUpdate discovery applies. When a Solution reports AdditionalContentRequired and no SBE has been provided, the result is 'NeedsSbe' so the orchestrator can surface it. The remote work is isolated in a single Invoke-Command so it can be mocked in unit tests. .PARAMETER Session Open PSSession to the cluster node. .PARAMETER ImportRoot The import folder path AS SEEN ON THE NODE (e.g. C:\ClusterStorage\Infrastructure_1\Shares\SU1_Infrastructure_1\import). .PARAMETER MediaFileName For a Solution package: the .zip file name within ImportRoot to expand. For an SBE package: the staged SBE subfolder name within ImportRoot. .PARAMETER PackageType 'Solution' or 'SBE'. .PARAMETER Version The version expected to appear in Get-SolutionUpdate. .PARAMETER DiscoveryTimeoutMinutes Max minutes to poll for discovery. Default 30. .PARAMETER PollSeconds Seconds between discovery polls. Default 30. .OUTPUTS [PSCustomObject] with: ImportState 'Imported' | 'NeedsSbe' | 'Discovering' | 'Failed' DiscoveredName the discovered update name (when found) Version echoed version Message human-readable detail #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([PSCustomObject])] param( # Untyped (ValidateNotNull) so the function is unit-testable by passing a # session double and mocking Invoke-Command; callers pass a real PSSession. [Parameter(Mandatory = $true)] [ValidateNotNull()] $Session, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$ImportRoot, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$MediaFileName, [Parameter(Mandatory = $true)] [ValidateSet('Solution', 'SBE')] [string]$PackageType, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Version, [int]$DiscoveryTimeoutMinutes = 30, [int]$PollSeconds = 30 ) if (-not $PSCmdlet.ShouldProcess($Session.ComputerName, "Import solution update '$Version'")) { return [PSCustomObject]@{ ImportState = 'Discovering' DiscoveredName = '' Version = $Version Message = 'WhatIf - import not performed.' } } $remote = Invoke-Command -Session $Session -ScriptBlock { param($ImportRoot, $MediaFileName, $PackageType, $Version, $DiscoveryTimeoutMinutes, $PollSeconds) $result = @{ ImportState = 'Failed' DiscoveredName = '' Version = $Version Message = '' } try { if ($PackageType -eq 'Solution') { $zip = Join-Path -Path $ImportRoot -ChildPath $MediaFileName if (-not (Test-Path -LiteralPath $zip -PathType Leaf)) { $result.Message = "Solution bundle '$zip' not found on node." return $result } $solutionFolder = Join-Path -Path $ImportRoot -ChildPath 'Solution' if (-not (Test-Path -LiteralPath $solutionFolder)) { New-Item -ItemType Directory -Path $solutionFolder -Force | Out-Null } Expand-Archive -LiteralPath $zip -DestinationPath $solutionFolder -Force } else { $solutionFolder = Join-Path -Path $ImportRoot -ChildPath $MediaFileName if (-not (Test-Path -LiteralPath $solutionFolder)) { $result.Message = "SBE source folder '$solutionFolder' not found on node." return $result } } if (-not (Get-Command Add-SolutionUpdate -ErrorAction SilentlyContinue)) { $result.Message = 'Add-SolutionUpdate is not available on the node (Azure Local update cmdlets missing).' return $result } Add-SolutionUpdate -SolutionUpdateContentFolderPath $solutionFolder -ErrorAction Stop $deadline = (Get-Date).AddMinutes($DiscoveryTimeoutMinutes) do { Start-Sleep -Seconds $PollSeconds $updates = @(Get-SolutionUpdate -ErrorAction SilentlyContinue) $match = $updates | Where-Object { ($_.Version -eq $Version) -or ($_.DisplayName -like "*$Version*") -or ($_.Name -like "*$Version*") } | Select-Object -First 1 if ($null -ne $match) { $state = [string]$match.State if ($state -match 'AdditionalContentRequired') { $result.ImportState = 'NeedsSbe' $result.DiscoveredName = [string]$match.Name $result.Message = "Discovered '$($match.Name)' but it reports AdditionalContentRequired (OEM SBE needed)." return $result } $result.ImportState = 'Imported' $result.DiscoveredName = [string]$match.Name $result.Message = "Discovered '$($match.Name)' (State=$state)." return $result } } while ((Get-Date) -lt $deadline) $result.ImportState = 'Discovering' $result.Message = "Discovery still in progress after $DiscoveryTimeoutMinutes minute(s); will re-check on next run." return $result } catch { $result.ImportState = 'Failed' $result.Message = "Import failed on node: $($_.Exception.Message)" return $result } } -ArgumentList $ImportRoot, $MediaFileName, $PackageType, $Version, $DiscoveryTimeoutMinutes, $PollSeconds return [PSCustomObject]@{ ImportState = [string]$remote.ImportState DiscoveredName = [string]$remote.DiscoveredName Version = [string]$remote.Version Message = [string]$remote.Message } } |