diff --git a/inc/vCenter-SSL.ps1 b/inc/vCenter-SSL.ps1 index a42ceac7..b17c89b7 100644 --- a/inc/vCenter-SSL.ps1 +++ b/inc/vCenter-SSL.ps1 @@ -1,134 +1,152 @@ #!/usr/bin/env pwsh +# ----------------------------------------------------------------------------------- +# Linux-safe vCenter + Posh-ACME Script +# - Uses HttpClient with AutomaticDecompression disabled (Fix1) +# - Handles Posh-ACME + PowerDNS plugin +# - Uploads & applies certificates to vCenter +# ----------------------------------------------------------------------------------- + . /opt/idssys/nodemgmt/conf/powerwall/settings.ps1 # ---------------------------- -# Safe Failure Handler (cross-platform) +# Global variables for troubleshooting +# ---------------------------- +$global:helpme = $null +$global:responseBody = $null + +# ---------------------------- +# Error handler (robust across platforms) # ---------------------------- function Show-Failure { param([System.Management.Automation.ErrorRecord]$ErrorRecord) - $global:helpme = "" + $global:responseBody = "" + try { $ex = $ErrorRecord.Exception - # HttpResponseMessage path (PowerShell Core) if ($ex.Response -is [System.Net.Http.HttpResponseMessage]) { $resp = $ex.Response - - if ($resp -and $resp.Content) { - try { - # Defensive read; won't deadlock and will catch disposed stream - $global:helpme = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult() - if ([string]::IsNullOrWhiteSpace($global:helpme)) { - $global:helpme = "" - } - } - catch { - $global:helpme = "" - } + try { + $global:responseBody = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult() } - else { - $global:helpme = "" + catch { + $global:responseBody = "" } } - - # Legacy WebResponse path elseif ($ex.Response -is [System.Net.WebResponse]) { try { $stream = $ex.Response.GetResponseStream() if ($stream) { $reader = [System.IO.StreamReader]::new($stream) - $global:helpme = $reader.ReadToEnd() - if ([string]::IsNullOrWhiteSpace($global:helpme)) { - $global:helpme = "" - } + $global:responseBody = $reader.ReadToEnd() } else { - $global:helpme = "" + $global:responseBody = "" } } catch { - $global:helpme = "" + $global:responseBody = "" } } - - # Fallback to exception message else { - $global:helpme = $ex.Message + $global:responseBody = $ex.Message } } catch { - # In case the extraction itself fails - $global:helpme = "" + $global:responseBody = "" } - Write-Host -ForegroundColor Red "Status: A system exception was caught." - Write-Host -ForegroundColor Red $global:helpme - Write-Host -ForegroundColor Red "The request body has been saved to `$global:helpme" + $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 + exit 1 } # ---------------------------- -# Unified REST wrapper (TLS 1.2 + disable gzip + skip cert check) +# HttpClient wrapper (TLS1.2, skip cert check, no decompression) # ---------------------------- function Invoke-SafeRestMethod { param( [Parameter(Mandatory=$true)][string]$Uri, - [string]$Method = 'Get', - [hashtable]$Headers, - $Body, + [string]$Method = 'GET', + [hashtable]$Headers = @{}, + $Body = $null, [switch]$AsJson, - [int]$TimeoutSeconds = 60 + [int]$TimeoutSec = 60 ) try { - # Ensure headers object - if (-not $Headers) { $Headers = @{} } + # Handler: disable automatic decompression + $handler = [System.Net.Http.HttpClientHandler]::new() + $handler.AutomaticDecompression = [System.Net.DecompressionMethods]::None + $handler.ServerCertificateCustomValidationCallback = { $true } # Skip cert check - # Avoid gzip → prevents GZipDecompressedContent disposal bugs - if (-not $Headers.ContainsKey("Accept-Encoding")) { - $Headers["Accept-Encoding"] = "identity" + $client = [System.Net.Http.HttpClient]::new($handler) + $client.Timeout = [System.TimeSpan]::FromSeconds($TimeoutSec) + + # Add headers + foreach ($k in $Headers.Keys) { + $client.DefaultRequestHeaders.Remove($k) | Out-Null + $client.DefaultRequestHeaders.Add($k, $Headers[$k]) } - # Ensure TLS1.2 (covers Windows & Pwsh where ServicePointManager is relevant) - try { - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - } - catch { - # If not applicable on this platform, ignore - } - - $params = @{ - Uri = $Uri - Method = $Method - Headers = $Headers - SkipCertificateCheck = $true - ErrorAction = 'Stop' - TimeoutSec = $TimeoutSeconds - } - - if ($PSBoundParameters.ContainsKey('Body') -and $Body) { + # Prepare content + if ($Body -ne $null) { if ($AsJson) { - # ConvertTo-Json can produce arrays/objects -- use moderate depth - $params.Body = $Body | ConvertTo-Json -Depth 12 -Compress - $params.ContentType = 'application/json' + $jsonBody = $Body | ConvertTo-Json -Depth 12 -Compress + $content = [System.Net.Http.StringContent]::new($jsonBody, [System.Text.Encoding]::UTF8, 'application/json') } else { - $params.Body = $Body + $content = [System.Net.Http.StringContent]::new($Body) } } + else { + $content = $null + } - return Invoke-RestMethod @params + # Send request + $method = [System.Net.Http.HttpMethod]::$Method + $request = [System.Net.Http.HttpRequestMessage]::new($method, $Uri) + if ($content) { $request.Content = $content } + + $response = $client.SendAsync($request).GetAwaiter().GetResult() + + $respBody = $null + if ($response.Content -ne $null) { + $respBody = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() + } + + if ($response.IsSuccessStatusCode) { + # Try to convert JSON if response body exists + if ($respBody -and $respBody.Trim().Length -gt 0) { + try { return $respBody | ConvertFrom-Json } + catch { return $respBody } + } + else { + return $respBody + } + } + else { + throw [System.Net.Http.HttpRequestException]::new("HTTP $($response.StatusCode): $($response.ReasonPhrase)", $null, $response) + } } catch { Show-Failure -ErrorRecord $_ - # Show-Failure calls exit 1, so code here shouldn't run, but keep for safety - throw + } + finally { + $client.Dispose() + $handler.Dispose() } } # ---------------------------- -# Variables (from settings.ps1) +# Variables # ---------------------------- $vCenterURL = $VCENTERHOST $CommonName = $VCENTERHOST @@ -136,7 +154,6 @@ $EmailContact = $ACMEEMAIL [PSCredential]$Credential = New-Object System.Management.Automation.PSCredential -ArgumentList $VCENTERUSER, (ConvertTo-SecureString $VCENTERPASS -AsPlainText -Force) -# Posh-ACME / PowerDNS plugin args (PowerDNSApiKey as plain string) $pArgs = @{ PowerDNSApiHost = $WDNSHOST PowerDNSApiKey = $PDNSAPI @@ -149,167 +166,104 @@ $pArgs = @{ # Ensure Posh-ACME Module # ---------------------------- 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 { +if (-not (Get-Module -ListAvailable -Name Posh-ACME)) { 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 - exit 0 -} - -# Try to import the module (force) and wait until available -Import-Module -Name Posh-ACME -Force -ErrorAction Stop - -$maxWaitSec = 30 -$waited = 0 -Do { - $PoshACME = Get-Module -Name Posh-ACME -ListAvailable - if (-not $PoshACME) { - Write-Host "Waiting for Posh-ACME Module to load..." -ForegroundColor Cyan - Start-Sleep -Seconds 2 - $waited += 2 - } else { - break - } -} -While ($waited -lt $maxWaitSec) - -if (-not $PoshACME) { - Write-Host "Posh-ACME failed to load within $maxWaitSec seconds." -ForegroundColor Red - exit 1 + Install-Module -Name Posh-ACME -Force -Confirm:$false -Scope AllUsers } +Import-Module Posh-ACME -ErrorAction Stop +Write-Host "Posh-ACME module loaded." -ForegroundColor Green # ---------------------------- -# vCenter API Session (Option A: POST with NO body) +# vCenter API Session (Option A) # ---------------------------- $loginUri = "https://$vCenterURL/rest/com/vmware/cis/session" +Write-Host "Connecting to vCenter at $vCenterURL ..." -ForegroundColor Cyan -try { - $session = Invoke-SafeRestMethod -Uri $loginUri -Method Post -Headers @{ } # <-- no body (Option A) +$sessionResponse = Invoke-SafeRestMethod -Uri $loginUri -Method Post -Headers @{} +if ($sessionResponse -is [string] -or -not $sessionResponse) { + # Session might be in header + $sessionToken = $null + try { + $httpclient = [System.Net.Http.HttpClient]::new() + $req = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Post, $loginUri) + $resp = $httpclient.SendAsync($req).GetAwaiter().GetResult() + if ($resp.Headers.Contains("vmware-api-session-id")) { + $sessionToken = $resp.Headers.GetValues("vmware-api-session-id") | Select-Object -First 1 + } + $httpclient.Dispose() + } + catch { + Show-Failure -ErrorRecord $_ + } } -catch { - # Invoke-SafeRestMethod will call Show-Failure and exit on error - exit 1 -} - -# Extract session token -$sessionToken = $null -if ($session -and $session.value) { - $sessionToken = $session.value +else { + $sessionToken = $sessionResponse.value } if (-not $sessionToken) { - Write-Error "Unable to get Session Token, Terminating Script" - exit 1 + Show-Failure -ErrorRecord ([pscustomobject]@{ Exception = [System.Exception] "Unable to get vCenter session token" }) } + Write-Host "Connected to vCenter API. Session established." -ForegroundColor Green # ---------------------------- -# Example: Retrieve VMs +# Retrieve VM list # ---------------------------- $headers = @{ 'vmware-api-session-id' = $sessionToken } -$vmList = Invoke-SafeRestMethod -Uri "https://$vCenterURL/rest/vcenter/vm" -Method Get -Headers $headers -Write-Host "Retrieved VM list from vCenter:" -ForegroundColor Cyan -if ($vmList -and $vmList.value) { +$vmList = Invoke-SafeRestMethod -Uri "https://$vCenterURL/rest/vcenter/vm" -Headers $headers +if ($vmList.value) { + Write-Host "Retrieved VM list from vCenter:" -ForegroundColor Cyan $vmList.value | ForEach-Object { Write-Host " - $($_.name)" } } -else { - Write-Host "No VMs returned (empty result)" -ForegroundColor Yellow -} # ---------------------------- -# PowerDNS Integration (via Posh-ACME plugin args) -# ---------------------------- -Write-Host "Configuring PowerDNS with Posh-ACME" -ForegroundColor Green -# Ensure plugin args are plain strings where required -$pArgs = @{ - PowerDNSApiHost = $WDNSHOST - PowerDNSApiKey = $PDNSAPI - PowerDNSUseTLS = $true - PowerDNSPort = 443 - PowerDNSServerName = 'localhost' -} - -# ---------------------------- -# Example ACME order with DNS plugin +# PowerDNS / Posh-ACME certificate # ---------------------------- $certName = "vcenter-cert" - try { - Write-Host "Requesting certificate for $CommonName via Posh-ACME (PowerDNS plugin)" -ForegroundColor Cyan - # This will create the order, perform DNS challenge via plugin args, and fetch certs - New-PACertificate -Domain $CommonName -DnsPlugin PowerDNS -PluginArgs $pArgs -Contact $EmailContact -AcceptTOS -Verbose + New-PACertificate -Domain $CommonName -DnsPlugin PowerDNS -PluginArgs $pArgs -Contact $EmailContact -AcceptTOS -Verbose -Force + Write-Host "ACME certificate request completed." -ForegroundColor Green } catch { Show-Failure -ErrorRecord $_ - exit 1 } # ---------------------------- -# Push certificate back to vCenter +# Collect certificate paths # ---------------------------- -try { - $paAccount = Get-PAAccount -ErrorAction Stop - $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 ($p in @($certPath, $keyPath, $chainPath)) { - if (-not (Test-Path -Path $p)) { - Write-Host "Expected certificate file not found: $p" -ForegroundColor Red - throw "Certificate file missing: $p" - } - } - - $body = @{ - cert = (Get-Content -Path $certPath -Raw) - key = (Get-Content -Path $keyPath -Raw) - chain = (Get-Content -Path $chainPath -Raw) - } - - $uploadUri = "https://$vCenterURL/rest/vcenter/certificate-management/vcenter/tls" - $headers = @{ 'vmware-api-session-id' = $sessionToken } - - 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 $_ - exit 1 +$paAccount = Get-PAAccount +$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)) { Show-Failure -ErrorRecord ([pscustomobject]@{ Exception = [System.Exception] "Certificate file missing: $f" }) } } # ---------------------------- -# Apply certificate to vCenter +# Upload certificate to vCenter # ---------------------------- -try { - $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 -} -catch { - Show-Failure -ErrorRecord $_ - exit 1 +$uploadUri = "https://$vCenterURL/rest/vcenter/certificate-management/vcenter/tls" +$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 "Certificate uploaded to vCenter." -ForegroundColor Green # ---------------------------- -# Restart vCenter Services (optional) +# Apply certificate # ---------------------------- -try { - $restartUri = "https://$vCenterURL/rest/appliance/system/services/vpxd?action=restart" - $headers = @{ 'vmware-api-session-id' = $sessionToken } +$applyUri = "https://$vCenterURL/rest/vcenter/certificate-management/vcenter/tls?action=apply" +Invoke-SafeRestMethod -Uri $applyUri -Method Post -Headers $headers +Write-Host "TLS certificate applied." -ForegroundColor Green - Invoke-SafeRestMethod -Uri $restartUri -Method Post -Headers $headers - 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 -} +# ---------------------------- +# Restart vCenter vpxd service +# ---------------------------- +$restartUri = "https://$vCenterURL/rest/appliance/system/services/vpxd?action=restart" +Invoke-SafeRestMethod -Uri $restartUri -Method Post -Headers $headers +Write-Host "vCenter vpxd service restart requested." -ForegroundColor Yellow -Write-Host "Script completed." -ForegroundColor Green +Write-Host "Script completed successfully." -ForegroundColor Green