VcfEdgeAtScale.psm1
|
# Copyright (c) 2026 Broadcom. All Rights Reserved. # Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc. # and/or its subsidiaries. # # ============================================================================= # # SOFTWARE LICENSE AGREEMENT # # Copyright (c) CA, Inc. All rights reserved. # # You are hereby granted a non-exclusive, worldwide, royalty-free license # under CA, Inc.'s copyrights to use, copy, modify, and distribute this # software in source code or binary form for use in connection with CA, Inc. # products. # # This copyright notice shall be included in all copies or substantial # portions of the software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. # # ============================================================================= # # PowerShell Module: VcfEdgeAtScale # Module Version: see VcfEdgeAtScale.psd1 # Last modified: 2026-05-14 # # Private implementation files (dot-sourced below): # Private/Logging.ps1 — logging, vCenter connectivity, content library, witness prep # Private/Cluster.ps1 — cluster, datastore, vSAN, VMFS, disk operations # Private/Networking.ps1 — VDS, VMkernel cleanup, management restore # Private/Supervisor.ps1 — supervisor, Harbor, Argo CD, workload networking # Private/Validation.ps1 — cleanup, deployment bootstrap, validation, vLCM helpers # Private/EntryPoints.ps1 — Start-VcfEdgeAtScale, configuration help (exported) # # Module-scope initialization helpers. These are private functions defined before the script-scope # variable block so they can be called during initialization and remain available to dot-sourced # private files. try/catch at raw module scope triggers PSScriptAnalyzer LSP false positives in # some editors; extracting into named functions avoids those false positives while retaining proper # error handling and user-visible warnings. Read-VcfEdgeAtScaleManifestVersion is removed from the # Function: drive after use (one-shot helper). Get-VcfEdgeAtScaleVcfCmd is kept as a private # lazy-resolver callable by the dot-sourced private files. function Read-VcfEdgeAtScaleManifestVersion { Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$ManifestPath ) try { return (Import-PowerShellDataFile -Path $ManifestPath).ModuleVersion } catch { Write-Warning "VcfEdgeAtScale: Could not read module version from '$ManifestPath' — $($_.Exception.Message). Version will be reported as 'unknown'." return "unknown" } } function Get-VcfEdgeAtScaleVcfCmd { <# .SYNOPSIS Returns the VCF CLI executable name, resolving and caching it on first call. .DESCRIPTION Performs the VCF CLI PATH scan (Get-Command -CommandType Application) lazily — only on first invocation — and caches the result in $Script:VcfCmd. Subsequent calls return the cached value immediately with no I/O. Deferring the scan from module-load time to first use avoids ~1-2 seconds of PATH scan latency on every new shell startup (macOS). .OUTPUTS String. The resolved VCF CLI command name (e.g. "vcf" or "vcf.exe"). .NOTES Sets $Script:VcfCmd as a side-effect. Many call sites in Private/Supervisor.ps1 reference $Script:VcfCmd directly rather than calling this function. They are safe because Get-EnvironmentSetup (called at the start of every Start-VcfEdgeAtScale run via New-LogFile) invokes this function first. Any code path that bypasses Get-EnvironmentSetup must call this function explicitly before using $Script:VcfCmd. #> if ($null -ne $Script:VcfCmd) { return $Script:VcfCmd } try { $candidates = if ($IsWindows) { @("vcf.exe", "vcf") } else { @("vcf") } foreach ($name in $candidates) { if (Get-Command -Name $name -CommandType Application -ErrorAction SilentlyContinue) { $Script:VcfCmd = $name return $name } } } catch { $defaultName = if ($IsWindows) { "vcf.exe" } else { "vcf" } Write-Warning "VcfEdgeAtScale: VCF CLI auto-detection failed — $($_.Exception.Message). Defaulting to '$defaultName'." } # No candidate found on PATH; fall back to the platform default name. $Script:VcfCmd = if ($IsWindows) { "vcf.exe" } else { "vcf" } return $Script:VcfCmd } #region Script scope variables $local:manifestPath = Join-Path -Path $PSScriptRoot -ChildPath "VcfEdgeAtScale.psd1" $Script:ModuleVersion = if (Test-Path -LiteralPath $local:manifestPath) { Read-VcfEdgeAtScaleManifestVersion -ManifestPath $local:manifestPath } else { Write-Warning "VcfEdgeAtScale: Module manifest not found at '$local:manifestPath'. Version will be reported as 'unknown'." "unknown" } # Root directory of this module; used by private files that need to locate the manifest at runtime # (e.g. Invoke-VcfEdgeAtScaleModuleVersionStalenessCheck). $Script:ModuleRoot = $PSScriptRoot # Set platform-specific command names for cross-platform compatibility. $Script:ArgocdCmd = if ($IsWindows) { "argocd.exe" } else { "argocd" } $Script:KubectlCmd = if ($IsWindows) { "kubectl.exe" } else { "kubectl" } # VCF CLI command name: resolved lazily on first use via Get-VcfEdgeAtScaleVcfCmd to avoid a # PATH scan (Get-Command -CommandType Application) at module-load time. Deferred initialization # keeps profile load time fast; the scan runs once when the user first invokes a VCF CLI operation. $Script:VcfCmd = $null # Remove the one-shot manifest-version helper from the Function: drive; it must not appear in Get-Command -Module VcfEdgeAtScale. # Get-VcfEdgeAtScaleVcfCmd is intentionally kept — it is a private lazy-resolver used by the dot-sourced private files. Remove-Item -Path Function:\Read-VcfEdgeAtScaleManifestVersion -ErrorAction SilentlyContinue # Define log level hierarchy (lower number = lower priority, higher number = higher priority) $Script:LogLevelHierarchy = @{ "DEBUG" = 0 "INFO" = 1 "ADVISORY" = 2 "WARNING" = 3 "EXCEPTION" = 4 "ERROR" = 5 } # Initialize log level (will be set by Start-VcfEdgeAtScale) $Script:ConfiguredLogLevel = "INFO" # When $true, Invoke-PauseBeforeRollbackIfRequested will skip its prompt (cleanup confirmation is sufficient). Set during -CleanUp cleanup. $Script:CleanUpOnly = $false # Rollback on failure: $null = prompt (Y/N/Always), $true = always rollback (unattended), $false = never rollback (leave site broken, continue to next). Set by Start-VcfEdgeAtScale -RollbackOnFailure. $Script:RollbackOnFailurePreference = $null # When user chooses "Always" at the prompt, no further prompts for remaining sites; always rollback. Reset at start of each run. $Script:RollbackAlwaysFromPrompt = $false # Typed exception thrown when user chooses No (or preference is never); caught by main loop to continue to next site. # Using a class instead of a string sentinel makes the control flow refactor-safe and type-checkable. class RollbackSkippedException : System.Exception { RollbackSkippedException() : base("Rollback skipped by user; continue to next site.") {} } # Typed exception for known deployment failures that have already been logged via Write-LogMessage -Type ERROR. # Caught by the top-level catch in Start-VcfEdgeAtScale, which shows the friendly footer without rethrowing the # raw exception — suppressing the scary "Exception: file:line" block that appears after user-readable [ERROR] output. class VcfDeploymentException : System.Exception { VcfDeploymentException() : base("Deployment failed.") {} VcfDeploymentException([string]$message) : base($message) {} VcfDeploymentException([string]$message, [System.Exception]$inner) : base($message, $inner) {} } # Set when Invoke-VsanDeploymentRollback (or other rollback) is entered so the main catch does not prompt/run rollback again. $Script:RollbackAttempted = $false # Set when rollback fails (e.g. Remove-Cluster failed); main catch rethrows immediately so the script fails exit. $Script:RollbackFailed = $false # Set when we enter the ArgoCD deployment try block (Set-ArgoCDService, Add-ArgoCDNamespace, etc.); cleared at start of each cluster. Used to choose ArgoCD-only rollback (remove namespace, keep supervisor) vs supervisor-only rollback when deployment fails after supervisor is enabled. $Script:ArgoCDPhaseStarted = $false # Set when we enter the Harbor deployment block (Set-HarborService, Install-HarborSupervisorService, etc.); cleared at start of each cluster. Used to choose Harbor-only rollback vs ArgoCD or supervisor rollback when Harbor deployment fails. $Script:HarborPhaseStarted = $false # Highest installed VCF.PowerCLI version after Initialize-ScriptVcfPowerCliModuleVersion (used for 9.0 vs 9.1 compatibility gates). $Script:VcfPowerCliModuleVersion = $null # Parent directory of the infrastructure JSON file; set by Update-InfrastructureJsonReferencedFilePaths for Resolve-InfrastructureReferencedFilePath when combining supervisorServices parentDirectory with file names. $Script:InfrastructureJsonParentForPathResolution = $null # Set to $true by New-LogFile when a new daily log file is created; used by Start-VcfEdgeAtScale to trigger the once-per-day PSGallery update check. $Script:NewLogFileCreatedThisSession = $false # ============================================================================= # SUPERVISOR SERVICE REGISTRY # Central definition of all supervisor services. When adding a new service (e.g. "Velero"), # add an entry here and update the functions listed in each property's inline comment. # This replaces the previous 8-step manual checklist. # # Properties per service: # DisableFlag — string used in infrastructure JSON and Get-EffectiveSupervisorServiceFlag ValidateSet # YamlPathProperty — logical YAML path property name for Get-EffectiveSupervisorServicesYamlPath ValidateSet # CleanupScope — scope name used in Invoke-VcfEdgeAtScaleCleanup and Start-VcfEdgeAtScale ValidateSet # PhaseStarted — script-scope boolean flag name (see companion $Script: declarations above) # # Wiring status: DisableFlag, YamlPathProperty, CleanupScope, PhaseStarted are currently read by # per-service code paths. Tracked as code review item C3 — future refactor will drive all # dispatch loops from this table to fully eliminate the per-service boilerplate. # ============================================================================= $Script:SupervisorServiceRegistry = [ordered]@{ ArgoCD = @{ DisableFlag = "disableArgoCD" YamlPathProperty = "argoCDDeploymentYamlPath" CleanupScope = "ArgoCD" PhaseStarted = "ArgoCDPhaseStarted" } Harbor = @{ DisableFlag = "disableHarbor" YamlPathProperty = "harborServiceYamlPath" CleanupScope = "Harbor" PhaseStarted = "HarborPhaseStarted" } } # ArgoCD deployment timeout defaults (used by Add-ArgoCDInstance when no TimeoutConfig key is supplied). $Script:ArgoCDAuthTimeoutSeconds = 60 $Script:ArgoCDPodReadyTimeoutSeconds = 600 $Script:ArgoCDPodReadyCheckIntervalSeconds = 5 $Script:ArgoCDWebhookReadyTimeoutSeconds = 1200 $Script:ArgoCDWebhookReadyCheckIntervalSeconds = 5 $Script:ArgoCDWebhookRetryTimeoutSeconds = 60 # Service YAML basenames shipped under Templates; update this list when template versions change (used by Start-VcfEdgeAtScale -Initialize). $Script:VcfEdgeAtScaleServiceYamlTemplateFileNames = @( "1.1.0-25100889.yml", "argocd-deployment.yml", "harbor-data-values-v2.14.2.yml", "legacy-harbor-svs-v2.14.2+vmware.2-vks.1-25220498.yml" ) # Enforce minimum engine version (must match PowerShellVersion in VcfEdgeAtScale.psd1). if ($PSVersionTable.PSVersion -lt [Version]"7.4") { throw "VcfEdgeAtScale requires PowerShell 7.4 or later. Current version is $($PSVersionTable.PSVersion). Install a newer pwsh and retry." } # Backward compatibility: the env var was misspelled "VcfEdgeatScaleRootDirectory" (lowercase 't') # before 1.0.3.1007. On Windows, env vars are case-insensitive so both names resolve identically. # On macOS/Linux (case-sensitive), migrate the old value to the corrected name automatically # so existing $PROFILE lines from earlier -Initialize runs continue to work without manual editing. if (-not $IsWindows -and -not [String]::IsNullOrEmpty($env:VcfEdgeatScaleRootDirectory) -and [String]::IsNullOrEmpty($env:VcfEdgeAtScaleRootDirectory)) { $env:VcfEdgeAtScaleRootDirectory = $env:VcfEdgeatScaleRootDirectory Remove-Item Env:\VcfEdgeatScaleRootDirectory -ErrorAction SilentlyContinue } #endregion # Dot-source private implementation files in dependency order. # Logging.ps1 is first because every other file calls Write-LogMessage. . (Join-Path -Path $PSScriptRoot -ChildPath "Private/Logging.ps1") . (Join-Path -Path $PSScriptRoot -ChildPath "Private/Cluster.ps1") . (Join-Path -Path $PSScriptRoot -ChildPath "Private/Networking.ps1") . (Join-Path -Path $PSScriptRoot -ChildPath "Private/Supervisor.ps1") . (Join-Path -Path $PSScriptRoot -ChildPath "Private/Validation.ps1") . (Join-Path -Path $PSScriptRoot -ChildPath "Private/EntryPoints.ps1") |