Files
NodeMgmt/inc/vCenter-SSL.ps1

393 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 {
# Strip the BEGIN/END lines and blank lines
$pemBody = $PemString -replace "`r","" -split "`n" | Where-Object {
($_ -notlike "-----BEGIN*") -and
($_ -notlike "-----END*") -and
($_ -ne "")
}
$pemBody = ($pemBody -join "") # single base64 string
$bytes = [Convert]::FromBase64String($pemBody)
$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 / Posh-ACME certificate logic (v4.30-compatible)
# ----------------------------
$certSuccess = $false
$certPath = $null
$keyPath = $null
$chainPath = $null
# Get most recent Posh-ACME certificate for this domain
$existingPACert = Get-PACertificate |
Where-Object { $_.MainDomain -eq $VCENTERHOST } |
Sort-Object NotAfter -Descending |
Select-Object -First 1
$renewCert = $true
$skipReason = ""
# Rule 1: Skip if cert valid >30 days
if ($existingPACert) {
$daysLeft = ($existingPACert.NotAfter - (Get-Date)).TotalDays
Write-Log -Level "INFO" -Message "Existing 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."
}
}
# Rule 2: LE rate-limit safety (don't request if last issuance < 168h)
if ($renewCert -and $existingPACert) {
$hoursSinceIssued = ((Get-Date) - $existingPACert.Created).TotalHours
if ($hoursSinceIssued -lt 168) {
$renewCert = $false
$skipReason = "LE rate-limit safety: last cert issued $([math]::Round($hoursSinceIssued)) hours ago (must wait 168h)."
}
}
if (-not $renewCert -and $existingPACert) {
Write-Log -Level "INFO" -Message "Skipping ACME issuance: $skipReason" -Color "Yellow"
$certPath = $existingPACert.CertificatePath
$keyPath = $existingPACert.PrivateKeyPath
$chainPath = $existingPACert.ChainPath
$certSuccess = $true
}
else {
# We either have no existing cert, it's near expiry, or outside LE safety window
Write-Log -Level "INFO" -Message "Requesting new ACME certificate via Posh-ACME..." -Color "Cyan"
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 {
New-PACertificate -Domain $VCENTERHOST `
-DnsPlugin PowerDNS `
-PluginArgs $pArgs `
-Contact $ACMEEMAIL `
-AcceptTOS `
-DnsSleep 15 `
-Force `
-Verbose
$newCert = Get-PACertificate |
Where-Object { $_.MainDomain -eq $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 "ACME issuance succeeded but no certificate object found from Get-PACertificate." -Color "Red"
$certSuccess = $false
}
}
catch {
$errorMessage = $_.Exception.Message
Write-Log -Level "ERROR" -Message "ACME request failed: $errorMessage" -Color "Red"
$global:helpme = $errorMessage
# If LE rate-limit hit, try to fall back to existing cert
if ($errorMessage -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 {
Show-Banner -Text "No existing certificate available to fall back to!" -Type "ERROR"
$certSuccess = $false
}
} else {
$certSuccess = $false
}
}
}
# Verify cert 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
}
# ----------------------------
# vCenter REST: Compare fingerprints and update if needed
# ----------------------------
$sessionHeaders = @{
'vmware-api-session-id' = $vCenterConn.ExtensionData.Content.SessionManager.SessionId
}
$vcenterCertUri = "https://$VCENTERHOST/rest/vcenter/certificate-management/vcenter/tls"
$updateNeeded = $true
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
$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 (fingerprint match)." -Color "Green"
$updateNeeded = $false
} else {
Write-Log -Level "INFO" -Message "vCenter certificate differs from Posh-ACME cert. Update is required." -Color "Yellow"
$updateNeeded = $true
}
} else {
Write-Log -Level "WARN" -Message "Could not parse existing vCenter certificate from REST response. Assuming update 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/apply cert 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
}
# ----------------------------
# vpxd restart via REST (only if cert changed)
# ----------------------------
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: 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"