Public/Get-AzLocalDeploymentStatus.ps1
|
Function Get-AzLocalDeploymentStatus { <# .SYNOPSIS Checks deployment status for all clusters defined in a CSV file. .DESCRIPTION Reads a cluster deployment CSV file and checks the current ARM deployment status for each cluster with ReadyToDeploy = TRUE. Reports the status of each cluster and generates JUnit XML output for CI/CD pipeline visibility. Designed to be called by a monitoring pipeline on a schedule (e.g., every 15 minutes) to track the progress of long-running Azure Local deployments. Status categories: - NotStarted: No deployment found for this cluster - ValidateInProgress: Validation deployment is running - ValidateSucceeded: Validation completed successfully (ready for Deploy) - ValidateFailed: Validation failed - DeployInProgress: Deploy deployment is running - DeploySucceeded: Deployment completed successfully - DeployFailed: Deployment failed - ClusterExists: Cluster resource already exists .PARAMETER CsvFilePath Path to the cluster deployments CSV file. .PARAMETER JUnitOutputPath Optional. Path to write JUnit XML test results. .PARAMETER LogFilePath Optional. Path to a log file for diagnostic output. .PARAMETER HtmlOutputPath Optional. Path to write an HTML deployment status report. .PARAMETER MarkdownOutputPath Optional. Path to write a Markdown deployment status report (for GitHub Step Summary or Azure DevOps). .PARAMETER ReportTitle Optional. Title displayed in the HTML/Markdown report header. Default: 'Azure Local Deployment Status Report'. .EXAMPLE Get-AzLocalDeploymentStatus -CsvFilePath './automation-pipelines/cluster-deployments.csv' .EXAMPLE Get-AzLocalDeploymentStatus -CsvFilePath './automation-pipelines/cluster-deployments.csv' -JUnitOutputPath './reports/status.xml' .EXAMPLE Get-AzLocalDeploymentStatus -CsvFilePath './automation-pipelines/cluster-deployments.csv' -HtmlOutputPath './reports/status.html' -MarkdownOutputPath './reports/status.md' #> [OutputType([PSCustomObject[]])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string]$CsvFilePath, [Parameter(Mandatory = $false)] [string]$JUnitOutputPath = "", [Parameter(Mandatory = $false)] [string]$LogFilePath = "", [Parameter(Mandatory = $false)] [string]$HtmlOutputPath = "", [Parameter(Mandatory = $false)] [string]$MarkdownOutputPath = "", [Parameter(Mandatory = $false)] [string]$ReportTitle = "Azure Local Deployment Status Report" ) # Reset module-scoped log path (prevents bleed-over from previous function calls) $script:AzLocalLogFilePath = $null # Initialise log file if specified if (-not [string]::IsNullOrWhiteSpace($LogFilePath)) { Initialize-AzLocalLogFile -LogFilePath $LogFilePath } Write-AzLocalLog "========================================================" -Level Info -NoTimestamp Write-AzLocalLog " Deployment Status Monitor" -Level Info -NoTimestamp Write-AzLocalLog " CSV File: $CsvFilePath" -Level Info -NoTimestamp Write-AzLocalLog "========================================================" -Level Info -NoTimestamp # Load naming configuration $NamingConfig = Get-AzLocalNamingConfig # Import CSV (ReadyToDeploy = TRUE only) # Wrap in @() to ensure array even for single-row CSV (PS 5.1 + StrictMode compatibility) $clusters = @(Import-AzLocalDeploymentCsv -CsvFilePath $CsvFilePath -ReadyOnly) if ($clusters.Count -eq 0) { Write-AzLocalLog "No clusters with ReadyToDeploy = TRUE found in CSV." -Level Warning return @() } Write-AzLocalLog "Checking status for $($clusters.Count) cluster(s)." -Level Info $allResults = @() foreach ($cluster in $clusters) { $uniqueID = $cluster.UniqueID $startTime = Get-Date # Resolve resource names $resourceGroupName = Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.resourceGroupName -UniqueID $uniqueID $clusterName = Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.clusterName -UniqueID $uniqueID $deploymentName = Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.deploymentName -UniqueID $uniqueID -TypeOfDeployment $cluster.TypeOfDeployment Write-AzLocalLog "Checking: $uniqueID (RG: $resourceGroupName, Deployment: $deploymentName)" -Level Info # Set subscription context try { Set-AzContext -SubscriptionId $cluster.SubscriptionId -TenantId $cluster.TenantId -ErrorAction Stop | Out-Null } catch { $allResults += [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName DeploymentStatus = 'ContextError' ProvisioningState = 'N/A' Message = "Failed to set Azure context: $($_.Exception.Message)" Duration = 0 } continue } # Check if cluster already exists $clusterResourceId = "/subscriptions/$($cluster.SubscriptionId)/resourceGroups/$resourceGroupName/providers/Microsoft.AzureStackHCI/clusters/$clusterName" $existingCluster = Get-AzResource -ResourceId $clusterResourceId -ErrorAction SilentlyContinue if ($existingCluster) { $duration = ((Get-Date) - $startTime).TotalSeconds Write-AzLocalLog " ${uniqueID}: Cluster already exists." -Level Success $allResults += [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName DeploymentStatus = 'ClusterExists' ProvisioningState = 'Succeeded' Message = "Cluster '$clusterName' exists in resource group '$resourceGroupName'." Duration = [math]::Round($duration, 2) } continue } # Check resource group exists $rg = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue if (-not $rg) { $duration = ((Get-Date) - $startTime).TotalSeconds Write-AzLocalLog " ${uniqueID}: Resource group not found." -Level Warning $allResults += [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName DeploymentStatus = 'NotStarted' ProvisioningState = 'N/A' Message = "Resource group '$resourceGroupName' does not exist." Duration = [math]::Round($duration, 2) } continue } # Check deployment status $deployment = Get-AzResourceGroupDeployment -ResourceGroupName $resourceGroupName -Name $deploymentName -ErrorAction SilentlyContinue $duration = ((Get-Date) - $startTime).TotalSeconds if (-not $deployment) { Write-AzLocalLog " ${uniqueID}: No deployment found." -Level Info $allResults += [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName DeploymentStatus = 'NotStarted' ProvisioningState = 'N/A' Message = "No deployment '$deploymentName' found." Duration = [math]::Round($duration, 2) } continue } $provState = $deployment.ProvisioningState $deploymentDuration = if ($deployment.Duration) { $deployment.Duration.ToString() } else { "N/A" } # Determine deployment status category # Check the deploymentMode parameter in the deployment to know if it was a Validate or Deploy $deployedMode = 'Unknown' try { $modeParam = $deployment.Parameters['deploymentMode'] if ($modeParam) { $deployedMode = $modeParam.Value } } catch { } $statusCategory = switch ($provState) { 'Running' { if ($deployedMode -eq 'Deploy') { 'DeployInProgress' } else { 'ValidateInProgress' } } 'Accepted' { if ($deployedMode -eq 'Deploy') { 'DeployInProgress' } else { 'ValidateInProgress' } } 'Succeeded' { if ($deployedMode -eq 'Deploy') { 'DeploySucceeded' } else { 'ValidateSucceeded' } } 'Failed' { if ($deployedMode -eq 'Deploy') { 'DeployFailed' } else { 'ValidateFailed' } } 'Canceled' { if ($deployedMode -eq 'Deploy') { 'DeployCanceled' } else { 'ValidateCanceled' } } default { $provState } } $levelColour = switch ($statusCategory) { 'ValidateSucceeded' { 'Success' } 'DeploySucceeded' { 'Success' } 'ValidateInProgress' { 'Warning' } 'DeployInProgress' { 'Warning' } default { 'Error' } } Write-AzLocalLog " ${uniqueID}: $statusCategory (ARM Duration: $deploymentDuration)" -Level $levelColour $allResults += [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName DeploymentStatus = $statusCategory ProvisioningState = $provState Message = "Deployment '$deploymentName' state: $provState (Mode: $deployedMode, Duration: $deploymentDuration)" Duration = [math]::Round($duration, 2) } } # Generate JUnit XML report if (-not [string]::IsNullOrWhiteSpace($JUnitOutputPath)) { $junitResults = foreach ($r in $allResults) { $testStatus = switch ($r.DeploymentStatus) { 'DeploySucceeded' { 'Passed' } 'ValidateSucceeded' { 'Passed' } 'ClusterExists' { 'Passed' } 'NotStarted' { 'Skipped' } 'ValidateInProgress' { 'Skipped' } 'DeployInProgress' { 'Skipped' } default { 'Failed' } } [PSCustomObject]@{ TestName = "Status-$($r.UniqueID)" ClassName = "AzLocalDeploymentAutomation.Monitor" Status = $testStatus Message = $r.Message Duration = $r.Duration } } New-AzLocalJUnitXml -TestResults $junitResults -SuiteName 'AzLocalDeployment-Monitor' -OutputPath $JUnitOutputPath } # Generate HTML / Markdown reports if (-not [string]::IsNullOrWhiteSpace($HtmlOutputPath) -or -not [string]::IsNullOrWhiteSpace($MarkdownOutputPath)) { New-AzLocalDeploymentReport -StatusResults $allResults ` -HtmlOutputPath $HtmlOutputPath ` -MarkdownOutputPath $MarkdownOutputPath ` -ReportTitle $ReportTitle | Out-Null } # Summary $statusGroups = $allResults | Group-Object -Property DeploymentStatus Write-AzLocalLog "========================================================" -Level Info -NoTimestamp Write-AzLocalLog " Deployment Status Summary" -Level Info -NoTimestamp foreach ($group in $statusGroups) { Write-AzLocalLog " $($group.Name): $($group.Count)" -Level Info -NoTimestamp } Write-AzLocalLog "========================================================" -Level Info -NoTimestamp return $allResults } |