From d686599cf09979d60607ef6b66787b07f13e36bc Mon Sep 17 00:00:00 2001 From: David Schroeder Date: Sat, 15 Nov 2025 23:50:24 -0600 Subject: [PATCH] Update vCenter-SSL.ps1 --- inc/vCenter-SSL.ps1 | 602 +++++++++++++++++++------------------------- 1 file changed, 252 insertions(+), 350 deletions(-) diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index 4e8d301a..31ac960d 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -1,418 +1,320 @@ #!/usr/bin/env pwsh # ----------------------------------------------------------------------------------- -# vCenter SSL Automation (Posh-ACME + PowerDNS + vCenter REST CertMgmt) -# - Uses ~/.config/Posh-ACME/LE_PROD/.../vcenter.scity.us -# - Files: cert.cer / cert.key / fullchain.cer -# - vCenter Cert API: /rest/vcenter/certificate-management/vcenter/tls -# - Auth: CIS REST session token (vmware-api-session-id) +# vCenter + Posh-ACME + PowerCLI Machine SSL automation +# - Uses Let's Encrypt via Posh-ACME +# - Does NOT re-issue if existing cert has >30 days left +# - Still pushes cert to vCenter if thumbprint differs +# - Uses Set-VIMachineCertificate (no direct REST /rest/... calls) # ----------------------------------------------------------------------------------- . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 -# ---------------------------- -# Logging -# ---------------------------- -$LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log" -$logDir = Split-Path -Path $LogFile -Parent -if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } +$ErrorActionPreference = 'Stop' +# ---------------------------- +# Config / constants +# ---------------------------- +$LogFile = "/opt/idssys/nodemgmt/logs/vc-ssl.log" +$RenewalWindow = 30 # days: don't re-issue if cert still valid longer than this +$DnsSleep = 15 # seconds for Posh-ACME DNS propagation + +# ---------------------------- +# Logging helper +# ---------------------------- function Write-Log { - param([string]$Level,[string]$Message,[string]$Color="White") - $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $line = "[{0}] {1}: {2}" -f $ts,$Level.ToUpper(),$Message - Write-Host $line -ForegroundColor $Color - Add-Content -Path $LogFile -Value $line -} + param( + [Parameter(Mandatory)][ValidateSet('INFO','WARN','ERROR')] + [string]$Level, + [Parameter(Mandatory)] + [string]$Message + ) -function Show-Banner { - param([string]$Text,[string]$Type="INFO") - switch ($Type.ToUpper()) { - "SUCCESS" { $C="Green"; $L="SUCCESS" } - "ERROR" { $C="Red"; $L="ERROR" } - "WARN" { $C="Yellow"; $L="WARN" } - default { $C="Cyan"; $L="INFO" } + $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + $line = "[{0}] {1}: {2}" -f $ts, $Level, $Message + Write-Host $line + try { + $dir = Split-Path $LogFile -Parent + if ($dir -and -not (Test-Path $dir)) { + New-Item -ItemType Directory -Path $dir -Force | Out-Null + } + Add-Content -Path $LogFile -Value $line + } catch { + Write-Host "[WARN] Failed to write to log file $LogFile : $($_.Exception.Message)" -ForegroundColor Yellow } - $line = "=" * [math]::Max($Text.Length,40) - Write-Host $line -ForegroundColor $C - Write-Host "${L}: $Text" -ForegroundColor $C - Write-Host $line -ForegroundColor $C - Add-Content -Path $LogFile -Value ("[{0}] {1}: {2}" -f (Get-Date),$L,$Text) } # ---------------------------- -# Error handling +# Error handler # ---------------------------- -$global:helpme = $null function Show-Failure { param([System.Management.Automation.ErrorRecord]$ErrorRecord) - $global:helpme = $ErrorRecord.Exception.Message - Show-Banner -Text $ErrorRecord.Exception.Message -Type "ERROR" + + $msg = $ErrorRecord.Exception.Message + Write-Log -Level 'ERROR' -Message $msg + + Write-Host "================================================================" -ForegroundColor Red + Write-Host "ERROR: A system exception was caught." -ForegroundColor Red + Write-Host $msg -ForegroundColor Red + Write-Host "================================================================" -ForegroundColor Red + exit 1 } # ---------------------------- -# Cert fingerprint helper -# ---------------------------- -function Get-CertFingerprintFromPem { - param([string]$PemString) - try { - $pemBody = $PemString -replace "`r","" -split "`n" | - Where-Object {$_ -notmatch "^-----" -and $_ -ne ""} - $bytes = [Convert]::FromBase64String(($pemBody -join "")) - $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes) - $sha256 = [System.Security.Cryptography.SHA256]::Create() - $hash = $sha256.ComputeHash($cert.RawData) - return ($hash | ForEach-Object { $_.ToString("X2") }) -join "" - } - catch { return $null } -} - -# ---------------------------- -# Discover Posh-ACME cert folder (Linux/XDG) -# ---------------------------- -function Find-LocalPACertFolder { - param([string]$HostName) - - $paRoot = Join-Path $HOME ".config/Posh-ACME" - - if (-not (Test-Path $paRoot)) { - Write-Log WARN "Posh-ACME config folder not found: $paRoot" Yellow - return $null - } - - # Look for folder named exactly like the hostname (e.g. vcenter.scity.us) - $folders = Get-ChildItem -Path $paRoot -Recurse -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -eq $HostName } - - if (-not $folders) { - Write-Log WARN "No matching cert folders found under $paRoot for $HostName" Yellow - return $null - } - - $folder = $folders | Select-Object -First 1 - Write-Log INFO "Using Posh-ACME cert folder: $($folder.FullName)" Gray - return $folder.FullName -} - -# ---------------------------- -# Load VMware PowerCLI -# ---------------------------- -if (-not (Get-Module -ListAvailable VMware.PowerCLI)) { - Install-Module VMware.PowerCLI -Force -Scope AllUsers -} -Import-Module VMware.PowerCLI -ErrorAction Stop -Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null - -# ---------------------------- -# Connect to vCenter (PowerCLI) +# Ensure modules # ---------------------------- try { - Write-Log INFO "Connecting to vCenter $VCENTERHOST..." Cyan - $vCenterConn = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force - Show-Banner "Connected to vCenter." SUCCESS -} catch { Show-Failure $_ } + if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) { + Write-Log INFO "VMware.PowerCLI not found. Installing..." + Install-Module -Name VMware.PowerCLI -Force -Scope AllUsers -AllowClobber + } + Import-Module VMware.PowerCLI -ErrorAction Stop + Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null + Write-Log INFO "VMware.PowerCLI loaded." +} catch { + Show-Failure $_ +} -# Optional: show VMs just to confirm connectivity +try { + if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { + Write-Log INFO "Posh-ACME not found. Installing..." + Install-Module -Name Posh-ACME -Force -Scope AllUsers + } + Import-Module Posh-ACME -ErrorAction Stop + Write-Log INFO "Posh-ACME loaded." +} catch { + Show-Failure $_ +} + +# ---------------------------- +# Connect to vCenter +# ---------------------------- +try { + Write-Log INFO "Connecting to vCenter $VCENTERHOST..." + $null = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force + Write-Host "========================================" + Write-Host "SUCCESS: Connected to vCenter." + Write-Host "========================================" +} catch { + Show-Failure $_ +} + +# ---------------------------- +# Optional: list VMs (sanity check) +# ---------------------------- try { $vms = Get-VM - Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter." Cyan + Write-Log INFO "Retrieved $($vms.Count) VMs from vCenter." } catch { - Write-Log WARN "Unable to retrieve VM list, continuing..." Yellow + Write-Log WARN "Unable to retrieve VM list. Continuing anyway. Error: $($_.Exception.Message)" } # ---------------------------- -# Load Posh-ACME +# Posh-ACME: find existing cert for VCENTERHOST # ---------------------------- -if (-not (Get-Module -ListAvailable Posh-ACME)) { - Install-Module Posh-ACME -Force -Scope AllUsers +$paCert = $null +try { + $allPaCerts = Get-PACertificate -List + if ($allPaCerts) { + Write-Log INFO "Found $($allPaCerts.Count) Posh-ACME cert(s)." + $allPaCerts | + ForEach-Object { + Write-Log INFO (" MainDomain={0} SANs={1} Exp={2}" -f $_.MainDomain, ($_.AllSANs -join ','), $_.NotAfter) + } + + $paCert = $allPaCerts | + Where-Object { + $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) + } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + } +} catch { + Write-Log WARN "Get-PACertificate failed: $($_.Exception.Message)" +} + +$needNewCert = $false + +if ($paCert) { + $daysLeft = ($paCert.NotAfter - (Get-Date)).TotalDays + Write-Log INFO ("Existing cert expires {0} (~{1:N0} days)" -f $paCert.NotAfter, $daysLeft) + + if ($daysLeft -le $RenewalWindow) { + Write-Log INFO "Existing cert within $RenewalWindow days of expiry. Will request new ACME certificate." + $needNewCert = $true + } else { + Write-Log INFO "Skipping ACME issuance: Existing certificate valid >$RenewalWindow days." + } +} else { + Write-Log WARN "No existing Posh-ACME certificate found for $VCENTERHOST. Will request new certificate." + $needNewCert = $true } -Import-Module Posh-ACME -ErrorAction Stop # ---------------------------- -# ACME / certificate selection logic +# ACME / Posh-ACME certificate issuance (if needed) # ---------------------------- -$certSuccess = $false -$certPath = $keyPath = $chainPath = $null - -$allPACerts = Get-PACertificate -ErrorAction SilentlyContinue -if ($allPACerts) { - Write-Log INFO "Found $($allPACerts.Count) Posh-ACME cert(s)." Gray - foreach ($c in $allPACerts) { - Write-Log INFO " MainDomain=$($c.MainDomain) SANs=$($c.AllSANs -join ',') Exp=$($c.NotAfter)" Gray - } -} - -# Select cert object for VCENTERHOST -$existingPACert = $allPACerts | - Where-Object { $_.MainDomain -eq $VCENTERHOST -or ($_.AllSANs -contains $VCENTERHOST) } | - Sort-Object NotAfter -Descending | - Select-Object -First 1 - -$renewCert = $true -$skipReason = "" - -# Rule: If existing cert valid >30 days, skip ACME -if ($existingPACert) { - $daysLeft = ($existingPACert.NotAfter - (Get-Date)).TotalDays - Write-Log INFO "Existing cert expires $($existingPACert.NotAfter) (~$([math]::Round($daysLeft)) days)" Gray - if ($daysLeft -gt 30) { - $renewCert = $false - $skipReason = "Existing certificate valid >30 days." - } -} - -# Rate-limit safety guard based on Created time -if ($renewCert -and $existingPACert) { - $hoursSince = ((Get-Date) - $existingPACert.Created).TotalHours - if ($hoursSince -lt 168) { - $renewCert = $false - $skipReason = "Rate-limit safety: cert issued $([math]::Round($hoursSince)) hours ago (<168h)." - } -} - -# ------------------------------------------------------- -# Use existing cert files (no ACME issuance) -# ------------------------------------------------------- -if (-not $renewCert -and $existingPACert) { - Write-Log INFO "Skipping ACME issuance: $skipReason" Yellow - - $certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST - if (-not $certFolder) { - Show-Banner "Could not locate certificate files on disk. Aborting." ERROR - exit 1 - } - - # Posh-ACME Linux file layout: - # cert.cer - leaf cert - # cert.key - private key - # fullchain.cer - cert + chain (we'll use this as chain) - $certPath = Join-Path $certFolder "cert.cer" - $keyPath = Join-Path $certFolder "cert.key" - $chainPath = Join-Path $certFolder "fullchain.cer" - - foreach ($f in @($certPath,$keyPath,$chainPath)) { - if (-not (Test-Path $f)) { - Write-Log ERROR "Missing certificate file: $f" Red - exit 1 - } - } - - $certSuccess = $true -} - -# ------------------------------------------------------- -# Request new ACME certificate if needed -# ------------------------------------------------------- -if ($renewCert) { - Write-Log INFO "Requesting new ACME certificate via Posh-ACME..." Cyan - - $securePDNSAPI = if ($PDNSAPI -is [string]) { - ConvertTo-SecureString $PDNSAPI -AsPlainText -Force - } else { $PDNSAPI } - - $pArgs = @{ - PowerDNSApiHost = $WDNSHOST - PowerDNSApiKey = $securePDNSAPI - PowerDNSUseTLS = $true - PowerDNSPort = 443 - PowerDNSServerName = "localhost" - } - +if ($needNewCert) { try { + # Build PowerDNS plugin args + $pArgs = @{ + PowerDNSApiHost = $WDNSHOST + PowerDNSApiKey = (if ($PDNSAPI -is [string]) { ConvertTo-SecureString $PDNSAPI -AsPlainText -Force } else { $PDNSAPI }) + PowerDNSUseTLS = $true + PowerDNSPort = 443 + PowerDNSServerName = 'localhost' + } + + Write-Log INFO "Requesting new ACME certificate via Posh-ACME..." New-PACertificate ` - -Domain $VCENTERHOST ` - -DnsPlugin PowerDNS ` + -MainDomain $VCENTERHOST ` + -Plugin PowerDNS ` -PluginArgs $pArgs ` -Contact $ACMEEMAIL ` -AcceptTOS ` - -DnsSleep 15 ` + -DnsSleep $DnsSleep ` -Force ` -Verbose - $certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST - if (-not $certFolder) { - Show-Banner "ACME succeeded but no cert folder found!" ERROR - exit 1 - } + # Refresh cert info + $paCert = Get-PACertificate + Write-Log INFO ("New cert issued. NotAfter={0}" -f $paCert.NotAfter) + } catch { + Write-Log ERROR ("ACME request failed: {0}" -f $_.Exception.Message) + Write-Host "================================================================" -ForegroundColor Yellow + Write-Host "WARN: Let’s Encrypt may have rate-limited or rejected the request." -ForegroundColor Yellow + Write-Host "================================================================" -ForegroundColor Yellow - $certPath = Join-Path $certFolder "cert.cer" - $keyPath = Join-Path $certFolder "cert.key" - $chainPath = Join-Path $certFolder "fullchain.cer" - $certSuccess = $true - Show-Banner "New ACME certificate created." SUCCESS - } - catch { - $msg = $_.Exception.Message - Write-Log ERROR "ACME failed: $msg" Red - $global:helpme = $msg - - if ($msg -like "*too many certificates*") { - Show-Banner "Let’s Encrypt rate limit hit. Using existing cert if possible." WARN - if (-not $existingPACert) { - Show-Banner "No fallback cert exists. Aborting." ERROR - exit 1 - } - $certFolder = Find-LocalPACertFolder -HostName $VCENTERHOST - if (-not $certFolder) { - Show-Banner "Fallback cert folder missing. Aborting." ERROR - exit 1 - } - $certPath = Join-Path $certFolder "cert.cer" - $keyPath = Join-Path $certFolder "cert.key" - $chainPath = Join-Path $certFolder "fullchain.cer" - $certSuccess = $true - } - else { + if (-not $paCert) { + Write-Host "==================================================" -ForegroundColor Red + Write-Host "ERROR: No usable certificate available. Aborting." -ForegroundColor Red + Write-Host "==================================================" -ForegroundColor Red exit 1 + } else { + Write-Log WARN "Falling back to existing Posh-ACME certificate despite issuance failure." } } } -# ------------------------------------------------------- -# Cert sanity check -# ------------------------------------------------------- -foreach ($f in @($certPath,$keyPath,$chainPath)) { +# Ensure we have a cert object now +if (-not $paCert) { + Write-Log ERROR "No Posh-ACME certificate object available. Aborting." + exit 1 +} + +# ---------------------------- +# Resolve cert file paths from Posh-ACME +# ---------------------------- +$certFolder = Split-Path $paCert.CertFile -Parent +if (-not $certFolder) { + Write-Log ERROR "Unable to determine certificate folder from Posh-ACME. Aborting." + exit 1 +} + +$certPath = $paCert.FullChainFile # cert + chain (ideal for browsers) +$keyPath = $paCert.KeyFile # private key +$chainPath = $paCert.ChainFile # issuing CA chain only (for Add-VITrustedCertificate) + +Write-Log INFO "Using Posh-ACME cert folder: $certFolder" +Write-Log INFO "Using certificate files:" +Write-Log INFO " CERT : $certPath" +Write-Log INFO " KEY : $keyPath" +Write-Log INFO " CHAIN: $chainPath" + +foreach ($f in @($certPath, $keyPath, $chainPath)) { if (-not (Test-Path $f)) { - Write-Log ERROR "Missing certificate file: $f" Red + Write-Log ERROR "Missing certificate file: $f" + Write-Host "" + Write-Host "================================================================" -ForegroundColor Red + Write-Host "ERROR: Missing certificate file: $f" -ForegroundColor Red + Write-Host "================================================================" -ForegroundColor Red exit 1 } } -Write-Log INFO "Using certificate files:" Green -Write-Log INFO " CERT : $certPath" Green -Write-Log INFO " KEY : $keyPath" Green -Write-Log INFO " CHAIN: $chainPath" Green - -# ------------------------------------------------------- -# Create CIS REST session (vmware-api-session-id) -# ------------------------------------------------------- +# ---------------------------- +# Ensure CA chain is trusted in vCenter +# ---------------------------- try { - $loginUri = "https://$VCENTERHOST/rest/com/vmware/cis/session" - $creds = "$VCENTERUSER`:$VCENTERPASS" - $authHeader = "Basic " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($creds)) - - $sessionResponse = Invoke-RestMethod -Uri $loginUri ` - -Method Post ` - -Headers @{ Authorization = $authHeader } ` - -SkipCertificateCheck ` - -ErrorAction Stop - - $sessionToken = $sessionResponse.value - $restHeaders = @{ "vmware-api-session-id" = $sessionToken } - - Write-Log INFO "CIS REST session established." Green -} -catch { - Show-Banner "Failed to create CIS REST session: $($_.Exception.Message)" ERROR - exit 1 -} - -# ------------------------------------------------------- -# OPTIONAL: compare fingerprints using REST cert GET -# ------------------------------------------------------- -$updateNeeded = $true -try { - $getUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls" - $resp = Invoke-RestMethod -Uri $getUri -Method Get -Headers $restHeaders -SkipCertificateCheck -ErrorAction Stop - - $currentPem = $resp.value.cert - if ($currentPem) { - $currentFp = Get-CertFingerprintFromPem $currentPem - $newFp = Get-CertFingerprintFromPem (Get-Content -Raw $certPath) - - if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) { - Show-Banner "vCenter certificate already matches Posh-ACME certificate. No update needed." SUCCESS - $updateNeeded = $false - } else { - Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required." Yellow - } + $trustedChainPem = Get-Content $chainPath -Raw + if ($trustedChainPem -and $trustedChainPem.Trim().Length -gt 0) { + Write-Log INFO "Adding/updating CA chain in vCenter trusted store (if not already present)..." + Add-VITrustedCertificate -PemCertificateOrChain $trustedChainPem -VCenterOnly -ErrorAction SilentlyContinue | Out-Null } else { - Write-Log WARN "vCenter cert GET did not return 'cert'. Assuming update required." Yellow + Write-Log WARN "CHAIN file appears empty; skipping trusted root update." } -} -catch { - Write-Log WARN "Could not read vCenter certificate via REST; assuming update required. $($_.Exception.Message)" Yellow +} catch { + Write-Log WARN "Add-VITrustedCertificate failed (non-fatal): $($_.Exception.Message)" } -# ------------------------------------------------------- -# Upload + apply certificate via /rest/vcenter/certificate-management/vcenter/tls -# ------------------------------------------------------- -$restartNeeded = $false +# ---------------------------- +# Compare current vCenter machine cert vs Posh-ACME cert +# ---------------------------- +$currentVcCert = $null +try { + $currentVcCert = Get-VIMachineCertificate -VCenterOnly +} catch { + Write-Log WARN "Get-VIMachineCertificate failed: $($_.Exception.Message). Will assume update is required." +} -if ($updateNeeded) { +$needPushToVc = $true + +if ($currentVcCert) { + Write-Log INFO ("Current vCenter cert: Subject='{0}' NotAfter={1} Thumbprint={2}" -f ` + $currentVcCert.Subject, $currentVcCert.NotValidAfter, $currentVcCert.Thumbprint) + + if ($currentVcCert.Thumbprint -eq $paCert.Thumbprint) { + Write-Log INFO "vCenter is already using the same certificate as Posh-ACME. No update needed." + $needPushToVc = $false + } else { + Write-Log INFO "vCenter certificate differs from Posh-ACME cert. Update required." + } +} else { + Write-Log WARN "Could not read current vCenter machine cert. Assuming update is required." +} + +# ---------------------------- +# Push certificate into vCenter via Set-VIMachineCertificate +# ---------------------------- +if ($needPushToVc) { try { - $uploadUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls" - $applyUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls?action=apply" + Write-Log INFO "Uploading and applying certificate via Set-VIMachineCertificate..." - $body = @{ - cert = Get-Content -Raw $certPath - key = Get-Content -Raw $keyPath - chain = Get-Content -Raw $chainPath + $pemCert = Get-Content $certPath -Raw # fullchain.cer (leaf + chain) + $pemKey = Get-Content $keyPath -Raw # cert.key (private key) + + if (-not $pemCert.Trim()) { + throw "PEM certificate content appears empty." + } + if (-not $pemKey.Trim()) { + throw "PEM key content appears empty." } - Write-Log INFO "Uploading certificate to vCenter via /rest/vcenter/certificate-management..." Cyan - Invoke-RestMethod -Uri $uploadUri ` - -Method Post ` - -Headers $restHeaders ` - -ContentType "application/json" ` - -Body ($body | ConvertTo-Json -Compress) ` - -SkipCertificateCheck ` - -ErrorAction Stop + # IMPORTANT: + # Set-VIMachineCertificate understands a PEM cert + separate PEM private key. + # It wraps the vSphere /api/vcenter/certificate-management/vcenter/tls API for us. + Set-VIMachineCertificate -PemCertificate $pemCert -PemKey $pemKey | Out-Null - Write-Log INFO "Applying uploaded certificate..." Cyan - Invoke-RestMethod -Uri $applyUri ` - -Method Post ` - -Headers $restHeaders ` - -SkipCertificateCheck ` - -ErrorAction Stop + Write-Host "====================================================================" + Write-Host "SUCCESS: vCenter Machine SSL certificate has been updated." -ForegroundColor Green + Write-Host "====================================================================" - Show-Banner "Certificate successfully uploaded and applied to vCenter." SUCCESS - $restartNeeded = $true + Write-Log INFO "Set-VIMachineCertificate completed successfully." + Write-Log INFO "Note: On vSphere 8.0 U2+ the swap is seamless; older versions may restart vCenter." + } catch { + Write-Log ERROR ("Failed to apply certificate via Set-VIMachineCertificate: {0}" -f $_.Exception.Message) + Write-Host "=============================================================================================" -ForegroundColor Red + Write-Host "ERROR: Failed to apply certificate via Set-VIMachineCertificate." -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + Write-Host "=============================================================================================" -ForegroundColor Red + exit 1 } - catch { - Show-Banner "Failed to upload/apply certificate: $($_.Exception.Message)" ERROR - $restartNeeded = $false - } -} -else { - Write-Log INFO "Skipping certificate upload/apply (already up to date)." Green +} else { + Write-Log INFO "Skipping vCenter certificate update because thumbprints already match." } -# ------------------------------------------------------- -# Restart vpxd (try REST, then show SSH fallback) -# ------------------------------------------------------- -if ($restartNeeded) { - $maxRetries = 20 - $healthUri = "https://$VCENTERHOST/rest/appliance/health/system" - $restartUri = "https://$VCENTERHOST/rest/appliance/system/services/vpxd?action=restart" - $restRestartSucceeded = $false - - for ($i=1; $i -le $maxRetries -and -not $restRestartSucceeded; $i++) { - try { - Invoke-RestMethod -Uri $healthUri -Method Get -Headers $restHeaders -SkipCertificateCheck -ErrorAction Stop - - Write-Log INFO "Requesting vpxd restart via REST..." Cyan - Invoke-RestMethod -Uri $restartUri -Method Post -Headers $restHeaders -SkipCertificateCheck -ErrorAction Stop - Show-Banner "vpxd restart requested successfully via REST." SUCCESS - $restRestartSucceeded = $true - } - catch { - $msg = $_.Exception.Message - Write-Log WARN "vpxd REST restart attempt $i/$maxRetries failed: $msg" Yellow - - if ($msg -like "*404*" -or $msg -like "*Not Found*") { - Write-Log WARN "vpxd REST endpoint not available (404). Falling back to SSH instructions." Yellow - break - } - - Start-Sleep -Seconds 15 - } - } - - if (-not $restRestartSucceeded) { - Show-Banner "Automatic vpxd restart not confirmed via REST. Please restart manually via SSH:" WARN - Write-Host " ssh root@$VCENTERHOST 'service-control --stop vpxd; service-control --start vpxd'" -ForegroundColor Yellow - } -} - -Show-Banner "Script complete. Log: $LogFile" INFO +# ---------------------------- +# Done +# ---------------------------- +Write-Host "==========================================================" -ForegroundColor Green +Write-Host "INFO: Script complete. Log: $LogFile" -ForegroundColor Green +Write-Host "==========================================================" -ForegroundColor Green