AzLocal.UpdateManagement.psm1
|
#Requires -Version 5.1 <# .SYNOPSIS AzLocal.UpdateManagement module for automating updates on Azure Local (formerly Azure Stack HCI) clusters. .DESCRIPTION This module queries Azure Local clusters by name or resource ID, checks their update status, and starts specified updates on clusters that are in "Ready" state with "UpdatesAvailable". It uses the Azure REST API directly via az rest to call the Update Manager API. Includes comprehensive logging capabilities with timestamped log files, transcript support, and result export to JSON/CSV. Supports Service Principal authentication for CI/CD automation scenarios (GitHub Actions, Azure DevOps Pipelines). NOTE: Renamed from AzStackHci.ManageUpdates in v0.7.3. Module GUID is preserved across the rename. See CHANGELOG.md and the v0.7.3 release notes in the manifest for migration guidance. .PARAMETER ClusterNames An array of Azure Local cluster names to update. Use this OR ClusterResourceIds. .PARAMETER ClusterResourceIds An array of full Azure Resource IDs for the clusters to update. Use this when clusters are in different resource groups or subscriptions. Use this OR ClusterNames. The Resource IDs are validated before processing to ensure: - The format is correct (must match Azure Stack HCI cluster resource pattern) - The resource exists in Azure - You have the required permissions to access the resource Example: "/subscriptions/xxx/resourceGroups/RG1/providers/Microsoft.AzureStackHCI/clusters/Cluster01" .PARAMETER ScopeByUpdateRingTag Uses the "UpdateRing" tag to find clusters via Azure Resource Graph. Must be used together with -UpdateRingValue. This enables updating clusters at scale using the UpdateRing tagging strategy. Works across multiple subscriptions. Requires the Azure CLI 'resource-graph' extension (automatically installed if missing). .PARAMETER UpdateRingValue The value of the UpdateRing tag to match when using -ScopeByUpdateRingTag. Only clusters where the UpdateRing tag equals this value will be selected for updates. Common values: "Ring1", "Ring2", "Ring3", "Production" .PARAMETER ResourceGroupName The resource group containing the clusters. If not specified, the function will search for the cluster across all resource groups in the subscription. Only used with -ClusterNames parameter. .PARAMETER SubscriptionId The Azure subscription ID. If not specified, uses the current az CLI subscription. Only used with -ClusterNames parameter. .PARAMETER UpdateName The specific update name to apply. If not specified, the function will list available updates and prompt for selection or apply the latest available update. .PARAMETER ApiVersion The API version to use. Defaults to "2025-10-01". .PARAMETER LogFolderPath Path to the folder where log files will be created. If not specified, defaults to: C:\ProgramData\AzLocal.UpdateManagement\ This default location is accessible across different user profiles. The folder is automatically created if it doesn't exist. .PARAMETER EnableTranscript Enables PowerShell transcript recording to capture all console output. .PARAMETER ExportResultsPath Path to export results. Supports multiple formats based on file extension: - .json = JSON format with summary statistics - .csv = Standard CSV format - .xml = JUnit XML format for CI/CD pipeline integration JUnit XML is compatible with: - Azure DevOps (Publish Test Results task) - GitHub Actions (dorny/test-reporter or similar) - Jenkins (JUnit plugin) - GitLab CI (native support) - TeamCity (built-in) If no extension is provided, defaults to JSON. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .EXAMPLE # Start update on a single cluster with logging Start-AzureLocalClusterUpdate -ClusterNames "MyCluster01" -ResourceGroupName "MyRG" -LogPath "C:\Logs\update.log" .EXAMPLE # Start updates on multiple clusters with transcript and JSON export Start-AzureLocalClusterUpdate -ClusterNames @("Cluster01", "Cluster02") -EnableTranscript -ExportResultsPath "C:\Logs\results.json" .EXAMPLE # Start a specific update with full logging Start-AzureLocalClusterUpdate -ClusterNames "MyCluster01" -UpdateName "Solution12.2601.1002.38" -LogPath "C:\Logs\update.log" -EnableTranscript .EXAMPLE # Start updates on clusters in different resource groups using Resource IDs $resourceIds = @( "/subscriptions/xxx/resourceGroups/RG1/providers/Microsoft.AzureStackHCI/clusters/Cluster01", "/subscriptions/xxx/resourceGroups/RG2/providers/Microsoft.AzureStackHCI/clusters/Cluster02" ) Start-AzureLocalClusterUpdate -ClusterResourceIds $resourceIds -Force .EXAMPLE # Start updates on all clusters tagged with "UpdateRing" = "Ring1" (across all subscriptions) Start-AzureLocalClusterUpdate -ScopeByUpdateRingTag -UpdateRingValue "Ring1" -Force .EXAMPLE # Start updates on production ring clusters Start-AzureLocalClusterUpdate -ScopeByUpdateRingTag -UpdateRingValue "Production" -UpdateName "Solution12.2601.1002.38" -Force .EXAMPLE # Export results to JUnit XML for CI/CD pipeline integration (Azure DevOps, GitHub Actions, Jenkins) Start-AzureLocalClusterUpdate -ScopeByUpdateRingTag -UpdateRingValue "Ring1" -Force -ExportResultsPath "C:\Logs\update-results.xml" .NOTES Author: Neil Bird, Microsoft. Requires: Azure CLI (az) installed and authenticated API Reference: https://github.com/Azure/azure-rest-api-specs/blob/main/specification/azurestackhci/resource-manager/Microsoft.AzureStackHCI/StackHCI/stable/2026-02-01/hci.json #> # Enforce defensive coding at module scope. # Version 1.0 catches references to uninitialized variables (e.g. $cluster vs $clusterEntry typos). # Deliberately NOT -Version Latest: Azure ARM REST responses legitimately omit optional # properties (e.g. additionalProperties.SBEPublisher, tags.UpdateRing), and Latest would # throw on every such dot-notation access. Hardening those sites is tracked separately. Set-StrictMode -Version 1.0 # Module constants $script:ModuleVersion = '0.7.3' $script:DefaultApiVersion = '2025-10-01' $script:DefaultLogFolder = Join-Path -Path $env:ProgramData -ChildPath 'AzLocal.UpdateManagement' # Best-effort default for PYTHONIOENCODING. az.cmd launches python with the -I # (isolated) flag which implies -E and so causes python to IGNORE all PYTHON* # environment variables - meaning this assignment alone is NOT sufficient to # stop the cp1252 encode warning. The actual fix is the --only-show-errors # flag passed to every az invocation in Invoke-AzRestJson. This module-load # assignment is retained as harmless defence-in-depth for the case where the # user has manually patched az.cmd to remove -I, or is running in an # environment that respects the env var. See: # https://github.com/Azure/azure-cli/issues/14426 (recommended workaround), # https://github.com/Azure/azure-cli/issues/28497 (-I behaviour confirmation). if (-not $env:PYTHONIOENCODING) { $env:PYTHONIOENCODING = 'utf-8' } # Update state constants aligned with queries in Azure Local LENS workbook # States that indicate an update is installable (ready to apply) $script:ReadyStates = @('Ready', 'ReadyToInstall') # States that indicate an update is blocked by a prerequisite $script:PrereqStates = @('HasPrerequisite', 'AdditionalContentRequired') # States that indicate an update failed health validation $script:HealthCheckFailedStates = @('HealthCheckFailed') # States that indicate an update is in a transitional phase $script:TransitionalStates = @('Downloading', 'Preparing', 'HealthChecking') # Script-level variables for logging $script:LogFilePath = $null $script:ErrorLogPath = $null $script:UpdateSkippedLogPath = $null $script:UpdateStartedLogPath = $null # Service Principal authentication state $script:ServicePrincipalAuthenticated = $false # --------------------------------------------------------------------------- # Module-scope state hoisted from between function definitions during refactor. # These declarations must run BEFORE any function body that references them. # --------------------------------------------------------------------------- $script:UpdateWindowTagName = 'UpdateWindow' $script:UpdateExclusionsTagName = 'UpdateExclusions' $script:UpdateSideloadedTagName = 'UpdateSideloaded' $script:UpdateVersionInProgressTagName = 'UpdateVersionInProgress' $script:DayMap = [ordered]@{ 'Mon' = [DayOfWeek]::Monday 'Tue' = [DayOfWeek]::Tuesday 'Wed' = [DayOfWeek]::Wednesday 'Thu' = [DayOfWeek]::Thursday 'Fri' = [DayOfWeek]::Friday 'Sat' = [DayOfWeek]::Saturday 'Sun' = [DayOfWeek]::Sunday } $script:DayAbbreviations = @('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') $script:FleetOperationState = $null # --------------------------------------------------------------------------- # Dot-source all function files listed in the manifest's NestedModules. # When loaded via the .psd1, these are already imported by NestedModules; this # loop is a harmless no-op in that case (PowerShell tolerates redefinition). # When loaded via the .psm1 directly (e.g. some Pester scenarios), this # guarantees all functions are present in the module scope. # --------------------------------------------------------------------------- $manifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'AzLocal.UpdateManagement.psd1' if (Test-Path -LiteralPath $manifestPath) { $manifestData = Import-PowerShellDataFile -LiteralPath $manifestPath -ErrorAction SilentlyContinue if ($manifestData -and $manifestData.NestedModules) { foreach ($nestedModule in $manifestData.NestedModules) { $nestedPath = Join-Path -Path $PSScriptRoot -ChildPath $nestedModule if (Test-Path -LiteralPath $nestedPath) { . $nestedPath } } } } Export-ModuleMember -Function @( 'Connect-AzureLocalServicePrincipal', 'Start-AzureLocalClusterUpdate', 'Get-AzureLocalClusterUpdateReadiness', 'Get-AzureLocalClusterInventory', 'Get-AzureLocalClusterInfo', 'Get-AzureLocalUpdateSummary', 'Get-AzureLocalAvailableUpdates', 'Get-AzureLocalUpdateRuns', 'Set-AzureLocalClusterUpdateRingTag', # Fleet-Scale Operations (v0.5.6) 'Invoke-AzureLocalFleetOperation', 'Get-AzureLocalFleetProgress', 'Test-AzureLocalFleetHealthGate', 'Export-AzureLocalFleetState', 'Resume-AzureLocalFleetUpdate', 'Stop-AzureLocalFleetUpdate', # Pre-Update Health Validation (v0.6.1) 'Test-AzureLocalClusterHealth', # Fleet Status Data Collection & Reporting (v0.6.4) 'Get-AzureLocalFleetStatusData', 'New-AzureLocalFleetStatusHtmlReport', # Update Schedule Tag Helpers (v0.6.5) 'Test-AzureLocalUpdateScheduleAllowed', # Sideloaded Payload Workflow (v0.7.1) 'Reset-AzureLocalSideloadedTag' ) |