Files
NodeMgmt/inc/vCenter-SSL.ps1

370 lines
14 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env pwsh
# -----------------------------------------------------------------------------------
# vCenter + Posh-ACME Script
# Fully automated, idempotent, with logging and fingerprint comparison
# -----------------------------------------------------------------------------------
. /opt/idssys/nodemgmt/conf/powerwall/settings.ps1
# ----------------------------
# Logging setup
# ----------------------------
$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
}
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
}
function Show-Banner {
param(
[string]$Text,
[string]$Type = "INFO"
)
switch ($Type.ToUpper()) {
"SUCCESS" { $color = "Green"; $level = "SUCCESS" }
"ERROR" { $color = "Red"; $level = "ERROR" }
"WARN" { $color = "Yellow"; $level = "WARN" }
default { $color = "Cyan"; $level = "INFO" }
}
$line = ("=" * [Math]::Max($Text.Length, 40))
Write-Host $line -ForegroundColor $color
Write-Host "${level}: $Text" -ForegroundColor $color
Write-Host $line -ForegroundColor $color
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$log = "[{0}] {1}: {2}" -f $ts, $level, $Text
Add-Content -Path $LogFile -Value $log
}
# ----------------------------
# Global variables for troubleshooting
# ----------------------------
$global:helpme = $null
$global:responseBody = $null
# ----------------------------
# Error handler
# ----------------------------
function Show-Failure {
param([System.Management.Automation.ErrorRecord]$ErrorRecord)
$global:responseBody = $ErrorRecord.Exception.Message
$global:helpme = $global:responseBody
Show-Banner -Text $ErrorRecord.Exception.Message -Type "ERROR"
Write-Log -Level "ERROR" -Message "Exception: $($ErrorRecord | Out-String)" -Color "Red"
exit 1
}
# ----------------------------
# Certificate fingerprint helper (SHA-256)
# ----------------------------
function Get-CertFingerprintFromPem {
param(
[Parameter(Mandatory = $true)]
[string]$PemString
)
try {
$pem = $PemString -replace "\r","" -split "`n" | Where-Object {
($_ -notlike "-----BEGIN*") -and ($_ -notlike "-----END*") -and ($_ -ne "")
} | Out-String
$pem = $pem.Trim()
$bytes = [Convert]::FromBase64String($pem)
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes)
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$hash = $sha256.ComputeHash($cert.RawData)
($hash | ForEach-Object { $_.ToString("X2") }) -join ""
}
catch {
Write-Log -Level "WARN" -Message "Failed to compute fingerprint: $($_.Exception.Message)" -Color "Yellow"
return $null
}
}
# ----------------------------
# Ensure PowerCLI module
# ----------------------------
if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
Write-Log -Level "INFO" -Message "VMware.PowerCLI not found. Installing..." -Color "Yellow"
Install-Module -Name VMware.PowerCLI -Force -Scope AllUsers
}
Import-Module VMware.PowerCLI -ErrorAction Stop
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false | Out-Null
# ----------------------------
# Connect to vCenter
# ----------------------------
try {
Write-Log -Level "INFO" -Message "Connecting to vCenter at $VCENTERHOST ..." -Color "Cyan"
$vCenterConn = Connect-VIServer -Server $VCENTERHOST -User $VCENTERUSER -Password $VCENTERPASS -Force
Show-Banner -Text "Connected to vCenter API. Session established." -Type "SUCCESS"
} catch {
Show-Failure -ErrorRecord $_
}
# ----------------------------
# Retrieve VM list (optional)
# ----------------------------
try {
$vms = Get-VM
Write-Log -Level "INFO" -Message "Retrieved VM list ($($vms.Count) VMs)." -Color "Cyan"
$vms | ForEach-Object { Write-Log -Level "INFO" -Message "VM: $($_.Name)" -Color "Gray" }
} catch {
Write-Log -Level "WARN" -Message "Unable to retrieve VM list, continuing..." -Color "Yellow"
}
# ----------------------------
# Ensure Posh-ACME module
# ----------------------------
if (-not (Get-Module -ListAvailable -Name Posh-ACME)) {
Write-Log -Level "INFO" -Message "Posh-ACME not found. Installing..." -Color "Yellow"
Install-Module -Name Posh-ACME -Force -Scope AllUsers
}
Import-Module Posh-ACME -ErrorAction Stop
# ----------------------------
# ACME / PowerDNS certificate request with 30-day + rate-limit handling
# ----------------------------
$certSuccess = $false
$certPath = $null
$keyPath = $null
$chainPath = $null
# Get latest existing cert (Posh-ACME)
$existingPACert = Get-PACertificate -Domain $VCENTERHOST -ErrorAction SilentlyContinue |
Sort-Object NotAfter -Descending |
Select-Object -First 1
$renewCert = $true
$skipReason = ""
if ($existingPACert) {
$daysLeft = ($existingPACert.NotAfter - (Get-Date)).TotalDays
Write-Log -Level "INFO" -Message "Existing Posh-ACME cert expires $($existingPACert.NotAfter) (~$([math]::Round($daysLeft)) days left)." -Color "Gray"
if ($daysLeft -gt 30) {
$renewCert = $false
$skipReason = "Existing certificate is valid for more than 30 days."
}
}
if (-not $renewCert -and $existingPACert) {
Write-Log -Level "INFO" -Message "Skipping ACME request: $skipReason" -Color "Green"
$certPath = $existingPACert.CertificatePath
$keyPath = $existingPACert.PrivateKeyPath
$chainPath = $existingPACert.ChainPath
$certSuccess = $true
}
else {
# Need or want to renew
if ($PDNSAPI -is [string]) {
$securePDNSAPI = ConvertTo-SecureString $PDNSAPI -AsPlainText -Force
} else {
$securePDNSAPI = $PDNSAPI
}
$pArgs = @{
PowerDNSApiHost = $WDNSHOST
PowerDNSApiKey = $securePDNSAPI
PowerDNSUseTLS = $true
PowerDNSPort = 443
PowerDNSServerName = 'localhost'
}
try {
Write-Log -Level "INFO" -Message "Requesting new certificate via Posh-ACME..." -Color "Cyan"
New-PACertificate -Domain $VCENTERHOST -DnsPlugin PowerDNS -PluginArgs $pArgs `
-Contact $ACMEEMAIL -AcceptTOS -Verbose -Force -DnsSleep 15
$newCert = Get-PACertificate -Domain $VCENTERHOST |
Sort-Object NotAfter -Descending |
Select-Object -First 1
if ($newCert) {
$certPath = $newCert.CertificatePath
$keyPath = $newCert.PrivateKeyPath
$chainPath = $newCert.ChainPath
$certSuccess = $true
Show-Banner -Text "New ACME certificate successfully created." -Type "SUCCESS"
} else {
Write-Log -Level "ERROR" -Message "New-PACertificate succeeded but Get-PACertificate returned nothing." -Color "Red"
}
}
catch {
$msg = $_.Exception.Message
Write-Log -Level "ERROR" -Message "ACME certificate request failed: $msg" -Color "Red"
$global:helpme = $msg
# If we hit LE rate limit, fall back to existing cert (if any)
if ($msg -like "*too many certificates*") {
Show-Banner -Text "Lets Encrypt rate-limit reached. Using existing certificate if available." -Type "WARN"
if ($existingPACert) {
$certPath = $existingPACert.CertificatePath
$keyPath = $existingPACert.PrivateKeyPath
$chainPath = $existingPACert.ChainPath
$certSuccess = $true
} else {
Write-Log -Level "ERROR" -Message "No existing certificate available to fall back to." -Color "Red"
$certSuccess = $false
}
} else {
$certSuccess = $false
}
}
}
# ----------------------------
# Verify certificate files exist
# ----------------------------
if ($certSuccess) {
foreach ($f in @($certPath, $keyPath, $chainPath)) {
if (-not (Test-Path $f)) {
Write-Log -Level "ERROR" -Message "Certificate file missing: $f" -Color "Red"
$certSuccess = $false
}
}
}
if (-not $certSuccess) {
Show-Banner -Text "No usable certificate available. Aborting before vCenter update." -Type "ERROR"
exit 1
}
# ----------------------------
# Compare fingerprints with current vCenter cert
# ----------------------------
$updateNeeded = $true
$sessionHeaders = @{
'vmware-api-session-id' = $vCenterConn.ExtensionData.Content.SessionManager.SessionId
}
$vcenterCertUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls"
try {
Write-Log -Level "INFO" -Message "Querying current vCenter TLS certificate..." -Color "Cyan"
$vcResp = Invoke-RestMethod -Uri $vcenterCertUri -Method Get -Headers $sessionHeaders -SkipCertificateCheck -ErrorAction Stop
# Try to find PEM in common shapes
$currentPem = $null
if ($vcResp.value -and $vcResp.value.cert) {
$currentPem = $vcResp.value.cert
} elseif ($vcResp.certificate) {
$currentPem = $vcResp.certificate
}
if ($currentPem) {
$currentFp = Get-CertFingerprintFromPem -PemString $currentPem
$newPem = Get-Content -Path $certPath -Raw
$newFp = Get-CertFingerprintFromPem -PemString $newPem
if ($currentFp -and $newFp -and ($currentFp -eq $newFp)) {
Write-Log -Level "INFO" -Message "vCenter already has the same certificate (SHA-256 fingerprint match)." -Color "Green"
$updateNeeded = $false
} else {
Write-Log -Level "INFO" -Message "vCenter certificate fingerprint differs. Update is required." -Color "Yellow"
$updateNeeded = $true
}
} else {
Write-Log -Level "WARN" -Message "Could not find existing vCenter certificate in REST response. Assuming update is required." -Color "Yellow"
$updateNeeded = $true
}
}
catch {
Write-Log -Level "WARN" -Message "Failed to query current vCenter certificate: $($_.Exception.Message). Proceeding with update." -Color "Yellow"
$updateNeeded = $true
}
# ----------------------------
# Upload and apply certificate via REST (only if needed)
# ----------------------------
$restartNeeded = $false
if ($updateNeeded) {
try {
$body = @{
cert = Get-Content -Path $certPath -Raw
key = Get-Content -Path $keyPath -Raw
chain = Get-Content -Path $chainPath -Raw
}
Write-Log -Level "INFO" -Message "Uploading TLS certificate to vCenter..." -Color "Cyan"
Invoke-RestMethod -Uri $vcenterCertUri -Method Post -Body ($body | ConvertTo-Json -Compress) `
-ContentType 'application/json' -Headers $sessionHeaders -SkipCertificateCheck
Show-Banner -Text "Certificate uploaded to vCenter." -Type "SUCCESS"
$uriApply = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls?action=apply"
Write-Log -Level "INFO" -Message "Applying TLS certificate to vCenter..." -Color "Cyan"
Invoke-RestMethod -Uri $uriApply -Method Post -Headers $sessionHeaders -SkipCertificateCheck
Show-Banner -Text "Certificate applied to vCenter." -Type "SUCCESS"
$restartNeeded = $true
}
catch {
Write-Log -Level "ERROR" -Message "Certificate upload/apply failed: $($_.Exception.Message)" -Color "Red"
$global:helpme = $_.Exception.Message
Show-Banner -Text "Failed to upload/apply certificate to vCenter." -Type "ERROR"
$restartNeeded = $false
}
} else {
Show-Banner -Text "vCenter certificate is already up to date. No upload/apply needed." -Type "SUCCESS"
$restartNeeded = $false
}
# ----------------------------
# Automatic vpxd restart via REST (only if we changed the cert)
# ----------------------------
if ($restartNeeded) {
$maxRetries = 20
$retryCount = 0
$restartSucceeded = $false
while ($retryCount -lt $maxRetries -and -not $restartSucceeded) {
try {
$healthUri = "https://$VCENTERHOST/rest/appliance/health/system"
Write-Log -Level "INFO" -Message "Checking vCenter REST health endpoint..." -Color "Cyan"
Invoke-RestMethod -Uri $healthUri -Method Get -SkipCertificateCheck -ErrorAction Stop
$restartUri = "https://$VCENTERHOST/rest/appliance/system/services/vpxd?action=restart"
Write-Log -Level "INFO" -Message "Requesting vpxd service restart via REST..." -Color "Cyan"
Invoke-RestMethod -Uri $restartUri -Method Post -SkipCertificateCheck -ErrorAction Stop
Show-Banner -Text "vpxd service restart requested successfully." -Type "SUCCESS"
$restartSucceeded = $true
}
catch {
Write-Log -Level "WARN" -Message "vpxd REST endpoint not ready, retrying in 15 seconds... (Attempt $($retryCount+1)/$maxRetries)" -Color "Yellow"
Start-Sleep -Seconds 15
$retryCount++
}
}
if (-not $restartSucceeded) {
Show-Banner -Text "Automatic vpxd restart failed after $maxRetries attempts. Please restart manually via SSH." -Type "ERROR"
Write-Log -Level "ERROR" -Message "Manual restart command: ssh root@$VCENTERHOST 'service-control --stop vpxd; service-control --start vpxd'" -Color "Red"
}
} else {
Write-Log -Level "INFO" -Message "Skipping vpxd restart because no certificate changes were applied." -Color "Green"
}
# ----------------------------
# Completion message
# ----------------------------
Show-Banner -Text "Script completed. Check $LogFile and `$global:helpme for details if needed." -Type "INFO"