diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index 2f025c04..46268133 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -1,49 +1,106 @@ #!/usr/bin/env pwsh +# Integrated, Linux-safe vCenter + Posh-ACME script +# - Uses Accept-Encoding: identity to avoid GZipDecompressedContent disposal bugs +# - Safe Show-Failure that won't try to read disposed streams +# - Option A: vCenter session POST with NO body +# - Defensive file/content reads and clearer logging + . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 # ---------------------------- -# Error handler +# Global variables for troubleshooting +# ---------------------------- +$global:helpme = $null +$global:responseBody = $null + +# ---------------------------- +# Error handler (robust across platforms) # ---------------------------- function Show-Failure { - param($ErrorRecord) + param([System.Management.Automation.ErrorRecord]$ErrorRecord) + + $global:responseBody = "" try { - $response = $ErrorRecord.Exception.Response - if ($response -is [System.Net.Http.HttpResponseMessage]) { - $global:responseBody = $response.Content.ReadAsStringAsync().Result + $ex = $ErrorRecord.Exception + + # If the exception contains an HttpResponseMessage (PowerShell Core) + if ($ex.Response -is [System.Net.Http.HttpResponseMessage]) { + $resp = $ex.Response + + if ($resp -and $resp.Content) { + try { + # Safe synchronous read of async task + $global:responseBody = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult() + } + catch { + # If content was disposed or unreadable, capture the message + $global:responseBody = "" + } + } + else { + $global:responseBody = "" + } } - elseif ($response -is [System.Net.WebResponse]) { - $stream = $response.GetResponseStream() - $reader = New-Object System.IO.StreamReader($stream) - $global:responseBody = $reader.ReadToEnd() + # Legacy WebResponse (rare with pwsh, but handle anyway) + elseif ($ex.Response -is [System.Net.WebResponse]) { + try { + $stream = $ex.Response.GetResponseStream() + if ($stream) { + $reader = [System.IO.StreamReader]::new($stream) + $global:responseBody = $reader.ReadToEnd() + } + else { + $global:responseBody = "" + } + } + catch { + $global:responseBody = "" + } } else { - $global:responseBody = $ErrorRecord.Exception.Message + # default to exception message if no response object present + $global:responseBody = $ex.Message } } catch { - $global:responseBody = $_.Exception.Message + $global:responseBody = "" } - Write-Host -BackgroundColor Black -ForegroundColor Red "Status: A system exception was caught." - Write-Host -BackgroundColor Black -ForegroundColor Red $global:responseBody - Write-Host -BackgroundColor Black -ForegroundColor Red "The request body has been saved to `$global:helpme" - break + # Save for later inspection + $global:helpme = $global:responseBody + + Write-Host "----------------------------------------" -ForegroundColor Red + Write-Host "Status: A system exception was caught." -ForegroundColor Red + Write-Host $global:responseBody -ForegroundColor Red + Write-Host "The request/response body has been saved to `$global:helpme" -ForegroundColor Red + Write-Host "----------------------------------------" -ForegroundColor Red + + # Terminate script (consistent and explicit) + exit 1 } # ---------------------------- -# Unified REST wrapper (TLS 1.2 + skip cert check) +# Unified REST wrapper (TLS 1.2 + skip cert check + gzip-safe) # ---------------------------- function Invoke-SafeRestMethod { param( - [string]$Uri, + [Parameter(Mandatory=$true)][string]$Uri, [string]$Method = 'Get', [hashtable]$Headers, $Body, - [switch]$AsJson + [switch]$AsJson, + [int]$TimeoutSec = 60 ) try { + if (-not $Headers) { $Headers = @{} } + + # Force identity encoding to avoid GZip decompression disposal bugs on pwsh + if (-not $Headers.ContainsKey('Accept-Encoding')) { + $Headers['Accept-Encoding'] = 'identity' + } + $params = @{ Uri = $Uri Method = $Method @@ -51,13 +108,16 @@ function Invoke-SafeRestMethod { SslProtocol = 'Tls12' SkipCertificateCheck = $true ErrorAction = 'Stop' + TimeoutSec = $TimeoutSec } - if ($Body) { + if ($PSBoundParameters.ContainsKey('Body') -and $Body -ne $null) { if ($AsJson) { - $params.Body = ($Body | ConvertTo-Json -Depth 10 -Compress) + # Convert to JSON with reasonable depth for certificates etc. + $params.Body = $Body | ConvertTo-Json -Depth 12 -Compress $params.ContentType = 'application/json' - } else { + } + else { $params.Body = $Body } } @@ -65,13 +125,13 @@ function Invoke-SafeRestMethod { return Invoke-RestMethod @params } catch { + # Provide the full ErrorRecord to Show-Failure Show-Failure -ErrorRecord $_ - exit 1 } } # ---------------------------- -# Variables +# Variables (pulled from settings.ps1) # ---------------------------- $vCenterURL = $VCENTERHOST $CommonName = $VCENTERHOST @@ -81,45 +141,61 @@ $EmailContact = $ACMEEMAIL $pArgs = @{ PowerDNSApiHost = $WDNSHOST - PowerDNSApiKey = $PDNSAPI | ConvertTo-SecureString -AsPlainText -Force + PowerDNSApiKey = $PDNSAPI # keep as plain string for plugin PowerDNSUseTLS = $true PowerDNSPort = 443 PowerDNSServerName = 'localhost' } # ---------------------------- -# Ensure Posh-ACME Module +# Ensure Posh-ACME Module (install if missing, then import) # ---------------------------- Write-Host "Checking for Required Module Posh-ACME" -ForegroundColor Green if (Get-Module -ListAvailable -Name Posh-ACME) { Write-Host "Posh-ACME Module Already Installed" -ForegroundColor Green -} -else { +} else { Write-Host "Posh-ACME Module Not Found, Installing..." -ForegroundColor Yellow - Install-Module -Name Posh-ACME -Force -Confirm:$false - Write-Host "Please restart this script after module install." -ForegroundColor Yellow - return + Install-Module -Name Posh-ACME -Force -Confirm:$false -Scope AllUsers + Write-Host "Posh-ACME installed. Continuing..." -ForegroundColor Green } -Do { - Write-Host "Waiting for Posh-ACME Module to load..." -ForegroundColor Cyan - $PoshACME = Get-Module -ListAvailable -Name Posh-ACME - Start-Sleep -Seconds 5 -} -While ($PoshACME -eq $null) +# Try to import module (loop until available) +$importAttempts = 0 +do { + try { + Import-Module Posh-ACME -ErrorAction Stop + } + catch { + $importAttempts++ + if ($importAttempts -ge 6) { + Write-Host "Unable to import Posh-ACME after multiple attempts." -ForegroundColor Red + Show-Failure -ErrorRecord $_ + } + Write-Host "Waiting for Posh-ACME module to become available..." -ForegroundColor Cyan + Start-Sleep -Seconds 5 + } +} while (-not (Get-Module -Name Posh-ACME)) + +Write-Host "Posh-ACME module loaded." -ForegroundColor Green # ---------------------------- -# vCenter API Session +# vCenter API Session (Option A: POST with NO body) # ---------------------------- -$loginUri = "https://$vCenterURL/rest/com/vmware/cis/session" -$session = Invoke-SafeRestMethod -Uri $loginUri -Method Post -Headers @{ } -Body @{ } -AsJson -$sessionToken = $session.value +$loginUri = "https://$vCenterURL/rest/com/vmware/cis/session" -if (-not $sessionToken) { +Write-Host "Connecting to vCenter at $vCenterURL ..." -ForegroundColor Cyan +$sessionResponse = Invoke-SafeRestMethod -Uri $loginUri -Method Post -Headers @{} +if (-not $sessionResponse) { Write-Error "Unable to get Session Token, Terminating Script" exit 1 } + +$sessionToken = $sessionResponse.value +if (-not $sessionToken) { + Write-Error "Unable to get Session Token value, Terminating Script" + exit 1 +} Write-Host "Connected to vCenter API. Session established." -ForegroundColor Green # ---------------------------- @@ -127,13 +203,17 @@ Write-Host "Connected to vCenter API. Session established." -ForegroundColor Gre # ---------------------------- $headers = @{ 'vmware-api-session-id' = $sessionToken } $vmList = Invoke-SafeRestMethod -Uri "https://$vCenterURL/rest/vcenter/vm" -Headers $headers -Write-Host "Retrieved VM list from vCenter:" -ForegroundColor Cyan -$vmList.value | ForEach-Object { Write-Host " - $($_.name)" } +if ($vmList -and $vmList.value) { + Write-Host "Retrieved VM list from vCenter:" -ForegroundColor Cyan + $vmList.value | ForEach-Object { Write-Host " - $($_.name)" } +} else { + Write-Host "No VMs returned or unable to retrieve VM list." -ForegroundColor Yellow +} # ---------------------------- # PowerDNS Integration (via Posh-ACME plugin args) # ---------------------------- -Write-Host "Configuring PowerDNS with Posh-ACME" -ForegroundColor Green +Write-Host "Configuring PowerDNS plugin args for Posh-ACME" -ForegroundColor Green $pArgs = @{ PowerDNSApiHost = $WDNSHOST PowerDNSApiKey = $PDNSAPI @@ -142,58 +222,95 @@ $pArgs = @{ PowerDNSServerName = 'localhost' } +# ---------------------------- # Example ACME order with DNS plugin +# ---------------------------- $certName = "vcenter-cert" -New-PACertificate $CommonName -DnsPlugin PowerDNS -PluginArgs $pArgs -Contact $EmailContact -AcceptTOS -Verbose +Write-Host "Requesting certificate for $CommonName using PowerDNS plugin..." -ForegroundColor Cyan -# ---------------------------- -# Push certificate back to vCenter (example) -# ---------------------------- -$certPath = (Join-Path -Path (Get-PAAccount).CertFolder -ChildPath "$certName\cert.pem") -$keyPath = (Join-Path -Path (Get-PAAccount).CertFolder -ChildPath "$certName\privkey.pem") -$chainPath = (Join-Path -Path (Get-PAAccount).CertFolder -ChildPath "$certName\chain.pem") - -# Upload cert (example REST call to vCenter CertMgmt API) -$uploadUri = "https://$vCenterURL/rest/vcenter/certificate-management/vcenter/tls" -$headers = @{ 'vmware-api-session-id' = $sessionToken } - -$body = @{ - cert = Get-Content -Path $certPath -Raw - key = Get-Content -Path $keyPath -Raw - chain = Get-Content -Path $chainPath -Raw +# Wrap in try/catch to present friendly errors +try { + New-PACertificate -Domain $CommonName -DnsPlugin PowerDNS -PluginArgs $pArgs -Contact $EmailContact -AcceptTOS -Verbose -Force + Write-Host "ACME certificate request completed (or is present)." -ForegroundColor Green +} +catch { + Show-Failure -ErrorRecord $_ } -Invoke-SafeRestMethod -Uri $uploadUri -Method Post -Headers $headers -Body $body -AsJson -Write-Host "New TLS certificate uploaded to vCenter" -ForegroundColor Green +# ---------------------------- +# Collect certificate paths from current Posh-ACME account +# ---------------------------- +try { + $paAccount = Get-PAAccount + if (-not $paAccount) { + throw "Get-PAAccount returned no account information." + } + + $certFolder = $paAccount.CertFolder + $certPath = Join-Path -Path $certFolder -ChildPath "$certName\cert.pem" + $keyPath = Join-Path -Path $certFolder -ChildPath "$certName\privkey.pem" + $chainPath = Join-Path -Path $certFolder -ChildPath "$certName\chain.pem" + + foreach ($f in @($certPath, $keyPath, $chainPath)) { + if (-not (Test-Path $f)) { + throw "Required certificate file not found: $f" + } + } +} +catch { + Show-Failure -ErrorRecord $_ +} + +# ---------------------------- +# Push certificate back to vCenter (REST API expects JSON) +# ---------------------------- +Write-Host "Uploading certificate to vCenter..." -ForegroundColor Cyan +try { + $uploadUri = "https://$vCenterURL/rest/vcenter/certificate-management/vcenter/tls" + $headers = @{ 'vmware-api-session-id' = $sessionToken } + + $body = @{ + cert = (Get-Content -Path $certPath -Raw) + key = (Get-Content -Path $keyPath -Raw) + chain = (Get-Content -Path $chainPath -Raw) + } + + Invoke-SafeRestMethod -Uri $uploadUri -Method Post -Headers $headers -Body $body -AsJson + Write-Host "New TLS certificate uploaded to vCenter" -ForegroundColor Green +} +catch { + Show-Failure -ErrorRecord $_ +} # ---------------------------- # Apply certificate to vCenter # ---------------------------- try { + Write-Host "Applying new TLS certificate to vCenter..." -ForegroundColor Cyan $applyUri = "https://$vCenterURL/rest/vcenter/certificate-management/vcenter/tls?action=apply" $headers = @{ 'vmware-api-session-id' = $sessionToken } Invoke-SafeRestMethod -Uri $applyUri -Method Post -Headers $headers - Write-Host "New TLS certificate applied to vCenter" -ForegroundColor Green + Write-Host "New TLS certificate applied to vCenter (action requested)." -ForegroundColor Green } catch { Show-Failure -ErrorRecord $_ - exit 1 } # ---------------------------- # Restart vCenter Services (optional) # ---------------------------- try { + Write-Host "Restarting vCenter service (vpxd)..." -ForegroundColor Yellow $restartUri = "https://$vCenterURL/rest/appliance/system/services/vpxd?action=restart" $headers = @{ 'vmware-api-session-id' = $sessionToken } Invoke-SafeRestMethod -Uri $restartUri -Method Post -Headers $headers - Write-Host "vCenter service restart initiated (vpxd)" -ForegroundColor Yellow + Write-Host "vCenter service restart initiated (vpxd)." -ForegroundColor Yellow Write-Host "Note: UI/API may briefly be unavailable while services restart." -ForegroundColor Yellow } catch { Show-Failure -ErrorRecord $_ - exit 1 } +Write-Host "Script completed." -ForegroundColor Green