Private/Test-AzLocalClusterPreFlight.ps1
|
Function Test-AzLocalClusterPreFlight { <# .SYNOPSIS Runs pre-flight checks for a single cluster deployment. .DESCRIPTION Validates that a cluster is ready for deployment by checking: 1. Resource names pass Azure naming validation (Test-AzLocalResourceNames) 2. Resource group exists in the target subscription 3. Azure prerequisites: required resource providers are registered (auto-registers missing ones) and RBAC role assignments are present (advisory) 4. All expected Arc node resources (Microsoft.HybridCompute/machines) are registered 5. No ARM deployment is currently in-progress for this cluster 6. The cluster resource does not already exist (already deployed) Returns a result object with Status (Passed/Failed/Skipped) and diagnostic Messages. .PARAMETER ClusterRow A PSCustomObject from the CSV representing one cluster deployment. .PARAMETER NamingConfig The naming configuration object from Get-AzLocalNamingConfig. .PARAMETER DeploymentMode The deployment mode (Validate or Deploy) to check for in-progress deployments. #> [OutputType([PSCustomObject])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [PSCustomObject]$ClusterRow, [Parameter(Mandatory = $true, Position = 1)] [PSCustomObject]$NamingConfig, [Parameter(Mandatory = $true, Position = 2)] [ValidateSet("Validate", "Deploy")] [string]$DeploymentMode ) $uniqueID = $ClusterRow.UniqueID $messages = @() $status = 'Passed' $startTime = Get-Date Write-AzLocalLog "Pre-flight check: $uniqueID" -Level Info # 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 $ClusterRow.TypeOfDeployment $keyVaultName = Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.keyVaultName -UniqueID $uniqueID $customLocation = Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.customLocation -UniqueID $uniqueID $resourceBridgeName = Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.resourceBridgeName -UniqueID $uniqueID $diagnosticStorageAccountName = Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.diagnosticStorageAccountName -UniqueID $uniqueID $clusterWitnessStorageAccountName = Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.clusterWitnessStorageAccountName -UniqueID $uniqueID # Determine effective node count $nodeCount = [int]$ClusterRow.NodeCount if ($ClusterRow.TypeOfDeployment -eq 'SingleNode') { $effectiveNodeCount = 1 } else { $effectiveNodeCount = $nodeCount } # Build node names $nodeNames = @() for ($i = 1; $i -le $effectiveNodeCount; $i++) { $nodeNames += Resolve-AzLocalResourceName -Pattern $NamingConfig.namingStandards.nodeNamePattern -UniqueID $uniqueID -NodeNumber $i } # 1. Validate resource names try { $namesToValidate = @{ 'ClusterName' = $clusterName 'ResourceGroupName' = $resourceGroupName 'KeyVaultName' = $keyVaultName 'CustomLocation' = $customLocation 'ResourceBridgeName' = $resourceBridgeName 'DiagnosticStorageAccountName' = $diagnosticStorageAccountName 'ClusterWitnessStorageAccountName' = $clusterWitnessStorageAccountName 'DeploymentName' = $deploymentName } # Add node names for ($i = 0; $i -lt $nodeNames.Count; $i++) { $namesToValidate["NodeName$($i + 1)"] = $nodeNames[$i] } Test-AzLocalResourceNames -Names $namesToValidate $messages += "Resource name validation: PASSED" } catch { $status = 'Failed' $messages += "Resource name validation: FAILED - $($_.Exception.Message)" # Return early - no point checking Azure if names are invalid $duration = ((Get-Date) - $startTime).TotalSeconds return [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName Status = $status Messages = $messages Duration = [math]::Round($duration, 2) } } # 2. Check resource group exists $rg = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue if (-not $rg) { $status = 'Failed' $messages += "Resource group '$resourceGroupName': NOT FOUND" $duration = ((Get-Date) - $startTime).TotalSeconds return [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName Status = $status Messages = $messages Duration = [math]::Round($duration, 2) } } $messages += "Resource group '$resourceGroupName': EXISTS" # 3. Check Azure prerequisites (resource providers and RBAC) $prereqResult = Test-AzLocalAzurePrerequisites -SubscriptionId $ClusterRow.SubscriptionId -ResourceGroupName $resourceGroupName $messages += $prereqResult.Messages if ($prereqResult.Status -eq 'Failed') { $status = 'Failed' $messages += "Azure prerequisite checks: FAILED" $duration = ((Get-Date) - $startTime).TotalSeconds return [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName Status = $status Messages = $messages Duration = [math]::Round($duration, 2) } } $messages += "Azure prerequisite checks: PASSED" # 4. Check all Arc nodes are registered $allNodesPresent = $true foreach ($nodeName in $nodeNames) { $arcResourceId = "/subscriptions/$($ClusterRow.SubscriptionId)/resourceGroups/$resourceGroupName/providers/Microsoft.HybridCompute/machines/$nodeName" $arcNode = Get-AzResource -ResourceId $arcResourceId -ErrorAction SilentlyContinue if ($arcNode) { $messages += "Arc node '$nodeName': REGISTERED" } else { $allNodesPresent = $false $messages += "Arc node '$nodeName': NOT FOUND" } } if (-not $allNodesPresent) { $status = 'Failed' $messages += "Pre-flight FAILED: Not all Arc nodes are registered." $duration = ((Get-Date) - $startTime).TotalSeconds return [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName Status = $status Messages = $messages Duration = [math]::Round($duration, 2) } } # 5. Check for existing cluster resource (already deployed) $clusterResourceId = "/subscriptions/$($ClusterRow.SubscriptionId)/resourceGroups/$resourceGroupName/providers/Microsoft.AzureStackHCI/clusters/$clusterName" $existingCluster = Get-AzResource -ResourceId $clusterResourceId -ErrorAction SilentlyContinue if ($existingCluster) { $status = 'Skipped' $messages += "Cluster '$clusterName' already exists in resource group. Skipping." $duration = ((Get-Date) - $startTime).TotalSeconds return [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName Status = $status Messages = $messages Duration = [math]::Round($duration, 2) } } $messages += "Cluster '$clusterName': Not yet deployed (eligible)" # 6. Check for in-progress deployment $existingDeployment = Get-AzResourceGroupDeployment -ResourceGroupName $resourceGroupName -Name $deploymentName -ErrorAction SilentlyContinue if ($existingDeployment) { $provState = $existingDeployment.ProvisioningState if ($provState -eq 'Running' -or $provState -eq 'Accepted') { $status = 'Skipped' $messages += "Deployment '$deploymentName' is already in-progress (State: $provState). Skipping." } elseif ($provState -eq 'Succeeded' -and $DeploymentMode -eq 'Validate') { $status = 'Skipped' $messages += "Validation deployment '$deploymentName' already succeeded. Skipping." } elseif ($provState -eq 'Failed') { $messages += "Previous deployment '$deploymentName' failed (can be retried)." } else { $messages += "Existing deployment '$deploymentName' state: $provState" } } else { $messages += "No existing deployment '$deploymentName' found (new deployment)." } $duration = ((Get-Date) - $startTime).TotalSeconds return [PSCustomObject]@{ UniqueID = $uniqueID ClusterName = $clusterName ResourceGroupName = $resourceGroupName DeploymentName = $deploymentName Status = $status Messages = $messages Duration = [math]::Round($duration, 2) } } |