Broadcom.Community.VDefendSSP.psm1
|
# --------------------------------------------------------------------------- # Shared internal helpers # --------------------------------------------------------------------------- Function Assert-SspiConnection { param($Connection) if (-not $Connection) { throw "No SSPI connection available. Run Connect-SspInstaller first (it sets the default connection automatically), or pass -Connection explicitly." } if ($Connection.PSTypeNames -notcontains 'Sspi.Connection') { throw "Expected a connection object from Connect-SspInstaller." } } Function Get-SspiAuthHeader { param([Parameter(Mandatory)][PSCredential]$Credential) $pair = "{0}:{1}" -f $Credential.UserName, $Credential.GetNetworkCredential().Password $bytes = [System.Text.Encoding]::UTF8.GetBytes($pair) return @{ Authorization = "Basic " + [Convert]::ToBase64String($bytes) } } Function Enable-SspiInsecureTls { # Windows PowerShell 5.1 has no per-call -SkipCertificateCheck; this is the # process-wide fallback (matches curl -k's scope: this session only). if (-not ("SspiTrustAllCertsPolicy" -as [type])) { Add-Type @" using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; public class SspiTrustAllCertsPolicy : ICertificatePolicy { public bool CheckValidationResult(ServicePoint sp, X509Certificate cert, WebRequest req, int problem) { return true; } } "@ } [System.Net.ServicePointManager]::CertificatePolicy = New-Object SspiTrustAllCertsPolicy } Function Add-SspiProgressStreamType { # A Stream wrapper that counts bytes as they're read, so upload progress # can be reported. Deliberately does NOT call back into PowerShell from # here — Read/ReadAsync run on whatever thread HttpClient's internals # choose, same runspace trap as the cert-callback bug above. Instead it # just increments a thread-safe counter; the caller polls TotalBytesRead # from the main PowerShell thread and drives Write-Progress itself. if (-not ("SspiProgressStream" -as [type])) { Add-Type @" using System; using System.IO; using System.Threading; using System.Threading.Tasks; public class SspiProgressStream : Stream { private readonly Stream _inner; private long _totalRead; public SspiProgressStream(Stream inner) { _inner = inner; } public long TotalBytesRead { get { return Interlocked.Read(ref _totalRead); } } public override bool CanRead { get { return true; } } public override bool CanSeek { get { return false; } } public override bool CanWrite { get { return false; } } public override long Length { get { return _inner.Length; } } public override long Position { get { return _inner.Position; } set { throw new NotSupportedException(); } } public override void Flush() { _inner.Flush(); } public override int Read(byte[] buffer, int offset, int count) { int n = _inner.Read(buffer, offset, count); if (n > 0) { Interlocked.Add(ref _totalRead, n); } return n; } public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { int n = await _inner.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); if (n > 0) { Interlocked.Add(ref _totalRead, n); } return n; } public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } protected override void Dispose(bool disposing) { if (disposing) { _inner.Dispose(); } base.Dispose(disposing); } } "@ } } Function Invoke-SspiApi { <# Single choke point for every non-upload SSPI call. Handles the -Troubleshoot printing so individual Get/New/Remove functions don't have to duplicate it. #> param( [Parameter(Mandatory)]$Connection, [Parameter(Mandatory)][string]$Method, [Parameter(Mandatory)][string]$Path, [string]$BodyJson, [string]$RedactedBodyJson, [switch]$Troubleshoot ) Assert-SspiConnection $Connection $uri = "$($Connection.BaseUrl)$Path" if ($Troubleshoot) { Write-Host "[TROUBLESHOOT] $Method $uri" $display = if ($RedactedBodyJson) { $RedactedBodyJson } else { $BodyJson } if ($display) { Write-Host $display } } $headers = Get-SspiAuthHeader -Credential $Connection.Credential # Tolerate an older/stale connection object that predates -TimeoutSec (property # would be $null, which Invoke-RestMethod rejects outright) by falling back to 15s. $timeoutSec = if ($Connection.TimeoutSec) { $Connection.TimeoutSec } else { 15 } $params = @{ Method = $Method; Uri = $uri; Headers = $headers; TimeoutSec = $timeoutSec } if ($BodyJson) { $params.Body = $BodyJson; $params.ContentType = 'application/json' } if ($Connection.Insecure) { if ($PSVersionTable.PSVersion.Major -ge 6) { $params.SkipCertificateCheck = $true } else { Enable-SspiInsecureTls } } try { Invoke-RestMethod @params } catch { $statusCode = $null if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } if ($statusCode -eq 401 -or $statusCode -eq 403) { throw [System.Exception]::new( "Authentication failed (HTTP $statusCode) calling $Method $uri. Please re-authenticate using Connect-SspInstaller — your session/credentials are no longer valid.", $_.Exception) } $message = $_.ErrorDetails.Message if (-not $message -and $_.Exception.Response) { try { $reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) $message = $reader.ReadToEnd() } catch { } } if (-not $message) { $message = $_.Exception.Message } # Single clean terminating error instead of Write-Error + rethrow (which # duplicated the message on screen). Original exception kept as InnerException. throw [System.Exception]::new("SSPI API error ($Method $uri): $message", $_.Exception) } } Function Out-SspiResult { # Returns a clean, real PSCustomObject (or array of them) — nothing # printed here. List endpoints (GetAllX) wrap their payload in a # ListResult envelope ({offset, number_of_results, total_result_count, # total_pages, sort_ascending, sort_by, <payload array>}) — unwrap that # so callers just get the items, not the pagination shell. The payload # array's property name is NOT consistent across the API: bundles and # platforms call it "results", providers calls it "configs". So detect # the envelope by its pagination fields, then take whichever OTHER # top-level property holds the array, rather than hardcoding a name. # # Deliberately does NOT pretty-print via Write-Host: PowerShell's console # auto-formats whatever a function returns when it isn't captured into a # variable, so printing here as well just produces the same data twice — # once readable, once as the ugly default @{...} dump. Pipe the result # through `ConvertTo-Json -Depth 10` yourself when you want that view; # the object itself is untouched either way. param($Result) if ($null -eq $Result) { return $Result } $toReturn = $Result $listMetadataKeys = @('offset', 'number_of_results', 'total_result_count', 'total_pages', 'sort_ascending', 'sort_by') $propNames = @($Result.PSObject.Properties.Name) $looksLikeListResult = @($propNames | Where-Object { $listMetadataKeys -contains $_ }).Count -gt 0 if ($looksLikeListResult) { $payloadProp = $propNames | Where-Object { $listMetadataKeys -notcontains $_ } | Select-Object -First 1 if ($payloadProp) { $toReturn = $Result.$payloadProp } } return $toReturn } Function ConvertTo-PoolObject { # "start-end[:name]" -> ordered hashtable {start, end, [name]} param([Parameter(Mandatory)][string]$Value, [string]$FlagName = 'pool') if ($Value -notmatch '-') { throw "'$Value' for $FlagName is missing the '-' separator between start and end (use START-END or START-END:NAME)" } $rangeParts = $Value -split '-', 2 $rest = $rangeParts[1] -split ':', 2 if ($rest.Count -eq 2) { return [ordered]@{ start = $rangeParts[0]; end = $rest[0]; name = $rest[1] } } else { return [ordered]@{ start = $rangeParts[0]; end = $rest[0] } } } Function Copy-RedactedHashtable { # Manual copy + redact — [ordered]@{}.Clone() is unreliable across PS versions. param([Parameter(Mandatory)]$Source, [string[]]$RedactKeys) $copy = [ordered]@{} foreach ($key in $Source.Keys) { $copy[$key] = $Source[$key] } foreach ($key in $RedactKeys) { if ($copy.Contains($key)) { $copy[$key] = '********' } } return $copy } # --------------------------------------------------------------------------- # Connect-SspInstaller # --------------------------------------------------------------------------- Function Connect-SspInstaller { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Connect to the SSP Installer (SSPI) API .DESCRIPTION Creates a connection object (host + credential) and sets it as the default for every other SspInstaller.ps1 function in this session. There is no server-side session here — this just packages what every other call needs and stashes it so you don't have to pass -Connection every time. .PARAMETER SspiHost The hostname/FQDN or IP address of the SSP Installer appliance .PARAMETER Credential Credential for the SSPI API (Basic auth). Prompted for if omitted. .PARAMETER Insecure Skip TLS certificate validation. Defaults to $true, matching a lab environment with self-signed certificates. .PARAMETER TimeoutSec Request timeout in seconds. Defaults to 15; raise it if you're on a slow link or hitting genuinely slow endpoints. .EXAMPLE Connect-SspInstaller -SspiHost ssp-inst01.vcf.lab #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$SspiHost, [PSCredential]$Credential, [bool]$Insecure = $true, # Invoke-RestMethod's own default (~100s) makes real connectivity # problems look like a hang. 15s surfaces them fast; raise it if # you're on a slow link or hitting genuinely slow endpoints. [int]$TimeoutSec = 15 ) if (-not $Credential) { $Credential = Get-Credential -Message "SSPI credentials for https://$SspiHost/sspi" -UserName 'admin' } $conn = [PSCustomObject]@{ PSTypeName = 'Sspi.Connection' SspiHost = $SspiHost BaseUrl = "https://$SspiHost/sspi" Credential = $Credential Insecure = $Insecure TimeoutSec = $TimeoutSec } $script:SspiConnection = $conn Write-Host "Default SSPI connection set: $($conn.BaseUrl) (user: $($Credential.UserName)). Pass -Connection to override for a specific call." return $conn } # --------------------------------------------------------------------------- # End User License Agreement: GET /sspi/eula/content, GET/POST /sspi/eula/acceptance # --------------------------------------------------------------------------- Function Get-SspInstallerEula { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Returns the SSP End User License Agreement text or its acceptance status .DESCRIPTION By default, this cmdlet returns whether the EULA has been accepted on this SSPI instance. Pass -Content to instead return the full EULA text. Use Approve-SspInstallerEula to accept it. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER Content Return the full EULA text instead of the acceptance status .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Get-SspInstallerEula .EXAMPLE Get-SspInstallerEula -Content #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [switch]$Content, [switch]$Troubleshoot ) $path = if ($Content) { '/eula/content' } else { '/eula/acceptance' } Out-SspiResult (Invoke-SspiApi -Connection $Connection -Method GET -Path $path -Troubleshoot:$Troubleshoot) } Function Approve-SspInstallerEula { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Accepts the SSP End User License Agreement .DESCRIPTION This cmdlet POSTs acceptance of the SSP End User License Agreement to SSPI. This must be accepted before a platform can be deployed (see New-SspInstallerDeployment). Use Get-SspInstallerEula to review the EULA text or check its current acceptance status first. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Approve-SspInstallerEula #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [switch]$Troubleshoot ) Invoke-SspiApi -Connection $Connection -Method POST -Path '/eula/acceptance' -Troubleshoot:$Troubleshoot Write-Host "EULA successfully accepted." } # --------------------------------------------------------------------------- # vCenter provider registration: POST/GET/DELETE /sspi/providers # --------------------------------------------------------------------------- Function New-SspInstallerVCenterServer { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Register a vCenter Server with the SSP Installer .DESCRIPTION This cmdlet registers (POSTs) a vCenter Server as a provider in SSPI so it can later be used as the deployment target for New-SspInstallerDeployment. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER VCenterServer The hostname/FQDN or IP address of the vCenter Server to register .PARAMETER VCenterCredential Credential for the vCenter Server. Prompted for if omitted. .PARAMETER CertificateFile Path to a PEM-encoded certificate file to trust for this vCenter Server. Optional. .PARAMETER Troubleshoot Print the HTTP method, URI, and request body (with secrets redacted) sent to the API, without affecting the actual request. .EXAMPLE New-SspInstallerVCenterServer -VCenterServer vc.vcf.lab .EXAMPLE New-SspInstallerVCenterServer -VCenterServer vc.vcf.lab -CertificateFile ~/Desktop/certs/vc.crt #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [Parameter(Mandatory)][string]$VCenterServer, [PSCredential]$VCenterCredential, [string]$CertificateFile, [switch]$Troubleshoot ) Assert-SspiConnection $Connection if (-not $VCenterCredential) { $VCenterCredential = Get-Credential -Message "vCenter credentials for $VCenterServer" } $cert = $null if ($CertificateFile) { if (-not (Test-Path $CertificateFile)) { throw "Certificate file not found: $CertificateFile" } $cert = Get-Content -Raw -Path $CertificateFile } $body = [ordered]@{ server = $VCenterServer user = $VCenterCredential.UserName password = $VCenterCredential.GetNetworkCredential().Password } if ($cert) { $body.certificate = $cert } $json = $body | ConvertTo-Json -Depth 5 $redactedJson = (Copy-RedactedHashtable -Source $body -RedactKeys 'password') | ConvertTo-Json -Depth 5 Invoke-SspiApi -Connection $Connection -Method POST -Path '/providers' ` -BodyJson $json -RedactedBodyJson $redactedJson -Troubleshoot:$Troubleshoot } Function Get-SspInstallerVCenterServer { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Returns vCenter Server(s) registered with the SSP Installer .DESCRIPTION This cmdlet returns either all vCenter Server providers registered with SSPI, or a single one when -ProviderId is specified. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER ProviderId The ID of a specific vCenter Server provider to return. Returns all providers if omitted. .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Get-SspInstallerVCenterServer .EXAMPLE Get-SspInstallerVCenterServer -ProviderId dcd85e06-49f1-42d9-8241-ec754439c9be #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [string]$ProviderId, [switch]$Troubleshoot ) $path = if ($ProviderId) { "/providers/$ProviderId" } else { '/providers' } Out-SspiResult (Invoke-SspiApi -Connection $Connection -Method GET -Path $path -Troubleshoot:$Troubleshoot) } Function Remove-SspInstallerVCenterServer { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Removes a vCenter Server registered with the SSP Installer .DESCRIPTION This cmdlet deletes (DELETEs) a vCenter Server provider from SSPI. This is a destructive operation and executes immediately. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER VCenterId The ID of the vCenter Server provider to remove (see Get-SspInstallerVCenterServer) .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Remove-SspInstallerVCenterServer -VCenterId dcd85e06-49f1-42d9-8241-ec754439c9be #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [Parameter(Mandatory)][string]$VCenterId, [switch]$Troubleshoot ) Invoke-SspiApi -Connection $Connection -Method DELETE -Path "/providers/$VCenterId" -Troubleshoot:$Troubleshoot Write-Host "vCenter Server '$VCenterId' successfully removed." } # --------------------------------------------------------------------------- # SSP bundle upload: POST /sspi/bundles/local, GET/DELETE /sspi/bundles(/{id}) # x-large-file-upload: true in the spec -> stream from disk, don't buffer. # Uses HttpClient directly (not Invoke-SspiApi) so it has its own # Troubleshoot handling. # --------------------------------------------------------------------------- Function New-SspInstallerPackage { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Uploads an SSP bundle (.tar) to the SSP Installer's package depot .DESCRIPTION This cmdlet streams a bundle file from disk (rather than buffering it in memory, since bundles can be multi-GB) to SSPI's package depot, showing a progress bar as it uploads. Upload is asynchronous on the server side — pass -PollStatus to wait and watch it move out of IN_PROGRESS. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER FilePath Path to the .tar bundle file to upload .PARAMETER BundleType PLATFORM (default) | INSTALLER | PLATFORM_PATCH | AVI_OPERATIONS | AVI_OPERATIONS_PATCH | BMS | SVM .PARAMETER PollStatus After a successful upload, poll the bundle's status every 5 seconds until it leaves IN_PROGRESS (READY / FAILED / ERROR) .PARAMETER Troubleshoot Print the HTTP method, URI, and upload details, without affecting the actual request. .EXAMPLE New-SspInstallerPackage -FilePath ~/Desktop/ssp-platform-5.2.0.tar -PollStatus #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [Parameter(Mandatory)][string]$FilePath, [ValidateSet('PLATFORM', 'INSTALLER', 'PLATFORM_PATCH', 'AVI_OPERATIONS', 'AVI_OPERATIONS_PATCH', 'BMS', 'SVM')] [string]$BundleType = 'PLATFORM', [switch]$PollStatus, [switch]$Troubleshoot ) Assert-SspiConnection $Connection $resolved = Resolve-Path -Path $FilePath -ErrorAction Stop $fileInfo = Get-Item -Path $resolved $uri = "$($Connection.BaseUrl)/bundles/local?type=$BundleType" if ($Troubleshoot) { Write-Host "[TROUBLESHOOT] POST $uri" Write-Host ("Body: multipart/form-data; file={0} ({1:N2} MB)" -f $fileInfo.Name, ($fileInfo.Length / 1MB)) } $handler = [System.Net.Http.HttpClientHandler]::new() if ($Connection.Insecure) { # NOT a PowerShell scriptblock: { $true } gets invoked by .NET on the TLS # negotiation thread, which has no PowerShell runspace attached and fails # with "There is no Runspace available to run scripts in this thread." # This is .NET's own built-in bypass delegate — a real compiled delegate, # so it has no runspace dependency. $handler.ServerCertificateCustomValidationCallback = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator } # Explicitly enable Tls12/Tls13 rather than relying on the OS/.NET default — # a common cause of "SSL connection could not be established" against # self-signed lab appliances on a custom HttpClientHandler (this exact # symptom, on this exact code path — Invoke-RestMethod elsewhere in this # script uses a different internal handler and isn't affected). try { $handler.SslProtocols = [System.Security.Authentication.SslProtocols]::Tls12 -bor [System.Security.Authentication.SslProtocols]::Tls13 } catch { $handler.SslProtocols = [System.Security.Authentication.SslProtocols]::Tls12 } $client = [System.Net.Http.HttpClient]::new($handler) $client.Timeout = [System.TimeSpan]::FromHours(4) $pair = "{0}:{1}" -f $Connection.Credential.UserName, $Connection.Credential.GetNetworkCredential().Password $bytes = [System.Text.Encoding]::UTF8.GetBytes($pair) $client.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new( 'Basic', [Convert]::ToBase64String($bytes)) Add-SspiProgressStreamType $stream = $null try { $stream = [System.IO.File]::OpenRead($fileInfo.FullName) $progressStream = [SspiProgressStream]::new($stream) $streamContent = [System.Net.Http.StreamContent]::new($progressStream) $streamContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream') $multipart = [System.Net.Http.MultipartFormDataContent]::new() $multipart.Add($streamContent, 'file', $fileInfo.Name) $activity = "Uploading $($fileInfo.Name)" $totalBytes = $fileInfo.Length try { $postTask = $client.PostAsync($uri, $multipart) $barWidth = 30 while (-not $postTask.IsCompleted) { Start-Sleep -Milliseconds 500 $sent = $progressStream.TotalBytesRead $pct = if ($totalBytes -gt 0) { [Math]::Min(100, [Math]::Round(($sent / $totalBytes) * 100, 1)) } else { 0 } $filled = [Math]::Floor(($pct / 100) * $barWidth) $bar = ('#' * $filled).PadRight($barWidth, '-') $statusText = "[{0}] {1}% ({2:N1} MB / {3:N1} MB)" -f $bar, $pct, ($sent / 1MB), ($totalBytes / 1MB) # A single in-place widget (Write-Progress), not scrolling text — this # host doesn't render its own fill bar for the status text, so we build # one manually here. Write-Progress -Activity $activity -Status $statusText -PercentComplete $pct } Write-Progress -Activity $activity -Completed $response = $postTask.GetAwaiter().GetResult() } catch { Write-Progress -Activity $activity -Completed # Unwrap the full InnerException chain — .NET's outer message for any TLS # failure is the same generic "SSL connection could not be established", # the actual reason (expired cert, name mismatch, protocol mismatch, etc.) # is buried in .InnerException and gets silently dropped otherwise. $chain = @() $ex = $_.Exception while ($ex) { $chain += $ex.Message; $ex = $ex.InnerException } throw [System.Exception]::new("Upload connection to $uri failed: " + ($chain -join ' -> '), $_.Exception) } $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() if (-not $response.IsSuccessStatusCode) { throw "Upload failed: HTTP $([int]$response.StatusCode) $($response.ReasonPhrase) - $body" } $result = $body | ConvertFrom-Json if ($PollStatus -and $result.id) { do { Start-Sleep -Seconds 5 $bundle = Invoke-SspiApi -Connection $Connection -Method GET -Path "/bundles/$($result.id)" -Troubleshoot:$Troubleshoot Write-Host ("Status: {0} Progress: {1}% {2}" -f $bundle.status, $bundle.progress, $bundle.message) } while ($bundle.status -eq 'IN_PROGRESS') return $bundle } return $result } finally { if ($stream) { $stream.Dispose() } $client.Dispose() } } Function Get-SspInstallerPackage { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Returns SSP bundle(s) uploaded to the SSP Installer's package depot .DESCRIPTION This cmdlet returns either all bundles in SSPI's package depot, or a single one when -BundleId is specified. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER BundleId The ID of a specific bundle to return. Returns all bundles if omitted. .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Get-SspInstallerPackage .EXAMPLE Get-SspInstallerPackage -BundleId dcd85e06-49f1-42d9-8241-ec754439c9be #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [string]$BundleId, [switch]$Troubleshoot ) $path = if ($BundleId) { "/bundles/$BundleId" } else { '/bundles' } Out-SspiResult (Invoke-SspiApi -Connection $Connection -Method GET -Path $path -Troubleshoot:$Troubleshoot) } Function Remove-SspInstallerPackage { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Removes an SSP bundle from the SSP Installer's package depot .DESCRIPTION This cmdlet deletes (DELETEs) a bundle from SSPI's package depot. This is a destructive operation and executes immediately. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER BundleId The ID of the bundle to remove (see Get-SspInstallerPackage) .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Remove-SspInstallerPackage -BundleId dcd85e06-49f1-42d9-8241-ec754439c9be #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [Parameter(Mandatory)][string]$BundleId, [switch]$Troubleshoot ) Invoke-SspiApi -Connection $Connection -Method DELETE -Path "/bundles/$BundleId" -Troubleshoot:$Troubleshoot Write-Host "Bundle '$BundleId' successfully removed." } # --------------------------------------------------------------------------- # vCenter friendly-name -> MoRef resolution (requires VMware.PowerCLI) # Only invoked if a *Name parameter is supplied without its *Id counterpart. # --------------------------------------------------------------------------- Function Resolve-SspVCenterMoRefs { param( [string]$VCenterServer, [PSCredential]$VCenterCredential, [string]$DatacenterName, [string]$ClusterName, [string]$DatastoreName, [string]$StoragePolicyName, [string]$ResourcePoolName, [string]$NetworkName, [string]$PortgroupName ) if (-not (Get-Module -Name VMware.VimAutomation.Core -ListAvailable)) { throw "Resolving vSphere names to MoRef IDs requires the VMware.PowerCLI module (VMware.VimAutomation.Core). " + "Install it (Install-Module VMware.PowerCLI -Scope CurrentUser), or pass the *Id parameters directly instead of *Name." } Import-Module VMware.VimAutomation.Core -ErrorAction Stop | Out-Null $connectedHere = $false if (-not $global:DefaultVIServers -or $global:DefaultVIServers.Count -eq 0) { if (-not $VCenterServer) { throw "-VCenterServer is required to resolve names to MoRef IDs (no existing PowerCLI connection found)." } if (-not $VCenterCredential) { $VCenterCredential = Get-Credential -Message "vCenter credentials for $VCenterServer" } Connect-VIServer -Server $VCenterServer -Credential $VCenterCredential -Force -ErrorAction Stop | Out-Null $connectedHere = $true } try { $result = [ordered]@{} $dcObj = $null $clusterObj = $null if ($DatacenterName) { $dcObj = Get-Datacenter -Name $DatacenterName -ErrorAction Stop $result.DatacenterId = $dcObj.ExtensionData.MoRef.Value } if ($ClusterName) { $clusterParams = @{ Name = $ClusterName } if ($dcObj) { $clusterParams.Location = $dcObj } $clusterObj = Get-Cluster @clusterParams -ErrorAction Stop $result.ClusterId = $clusterObj.ExtensionData.MoRef.Value } if ($DatastoreName) { # Get-Datastore -Location only accepts Datacenter, Folder, or DatastoreCluster # objects — NOT Cluster, unlike Get-Cluster/Get-ResourcePool/Get-Datastore's # own siblings. Scope to the datacenter only. $dsParams = @{ Name = $DatastoreName } if ($dcObj) { $dsParams.Location = $dcObj } $dsObj = Get-Datastore @dsParams -ErrorAction Stop $result.DatastoreId = $dsObj.ExtensionData.MoRef.Value } if ($ResourcePoolName) { $rpParams = @{ Name = $ResourcePoolName } if ($clusterObj) { $rpParams.Location = $clusterObj } $rpObj = Get-ResourcePool @rpParams -ErrorAction Stop $result.ResourcePoolId = $rpObj.ExtensionData.MoRef.Value } if ($StoragePolicyName) { if (-not (Get-Module -Name VMware.VimAutomation.Storage -ListAvailable)) { throw "Resolving -StoragePolicyName requires the VMware.VimAutomation.Storage module (part of VMware.PowerCLI)." } Import-Module VMware.VimAutomation.Storage -ErrorAction Stop | Out-Null $policyObj = Get-SpbmStoragePolicy -Name $StoragePolicyName -ErrorAction Stop $result.StoragePolicyId = $policyObj.Id } if ($PortgroupName) { $pgObj = Get-VDPortgroup -Name $PortgroupName -ErrorAction Stop $result.PortgroupId = $pgObj.ExtensionData.MoRef.Value # network_id (the parent DVS) is a separate required field from # portgroup_id in the API, but a portgroup name already implies # which switch it lives on — derive it here so callers don't have # to separately pass -NetworkName just to name the obvious parent. # An explicit -NetworkName below still overrides this if given. if ($pgObj.VDSwitch) { $result.NetworkId = $pgObj.VDSwitch.ExtensionData.MoRef.Value } } if ($NetworkName) { $vdsObj = Get-VDSwitch -Name $NetworkName -ErrorAction Stop $result.NetworkId = $vdsObj.ExtensionData.MoRef.Value } return [PSCustomObject]$result } finally { if ($connectedHere) { Disconnect-VIServer -Server $VCenterServer -Confirm:$false -ErrorAction SilentlyContinue } } } # --------------------------------------------------------------------------- # Deployment: POST/GET/DELETE /sspi/platforms (CreatePlatform / PlatformFullConfig) # --------------------------------------------------------------------------- Function New-SspInstallerDeployment { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Deploys (or drives the lifecycle of) an SSP platform via the SSP Installer .DESCRIPTION This cmdlet POSTs a CreatePlatform request to SSPI, describing the compute, network, and service configuration for an SSP platform deployment against a registered vCenter Server. Compute/network objects can be identified either by their vSphere MoRef/UUID (*Id params) or by friendly name (*Name params, which requires VMware.PowerCLI and -VCenterServer/-VCenterCredential to resolve). -Operation controls where the deployment lifecycle goes next (precheck, start, continue, retry, etc.). .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER VCenterId The ID of the vCenter Server provider to deploy to (see Get-SspInstallerVCenterServer) .PARAMETER Operation The desired lifecycle state to move the deployment to: PRECHECK_ONLY (default) | RESET_PRECHECK | CONTINUE | START | STOP | RETRY | CLEAN .PARAMETER DatacenterName Friendly name of the vSphere datacenter. Resolved to -DatacenterId via PowerCLI; use -DatacenterId directly to skip resolution. .PARAMETER DatacenterId MoRef ID of the vSphere datacenter .PARAMETER ClusterName Friendly name of the vSphere cluster. Resolved to -ClusterId via PowerCLI; use -ClusterId directly to skip resolution. .PARAMETER ClusterId MoRef ID of the vSphere cluster .PARAMETER DatastoreName Friendly name of the content datastore. Resolved to -DatastoreId via PowerCLI; use -DatastoreId directly to skip resolution. .PARAMETER DatastoreId MoRef ID of the content datastore .PARAMETER StoragePolicyName Friendly name of the storage policy. Resolved to -StoragePolicyId via PowerCLI (requires VMware.VimAutomation.Storage); use -StoragePolicyId directly to skip resolution. .PARAMETER StoragePolicyId ID of the storage policy .PARAMETER ResourcePoolName Friendly name of the resource pool. Resolved to -ResourcePoolId via PowerCLI; use -ResourcePoolId directly to skip resolution. .PARAMETER ResourcePoolId MoRef ID of the resource pool .PARAMETER EnableResourceReservation Whether to reserve compute resources for the deployment. Defaults to $true. .PARAMETER VCenterServer Hostname/FQDN of the vCenter Server to connect to via PowerCLI, only used to resolve any *Name parameters above. Not needed if you're already connected via PowerCLI, or if you only pass *Id parameters. .PARAMETER VCenterCredential Credential for -VCenterServer. Prompted for if omitted and needed. .PARAMETER Dns One or more DNS server IP addresses .PARAMETER Ntp NTP server hostname/IP .PARAMETER SearchDomain DNS search domain .PARAMETER NetworkName Friendly name of the distributed virtual switch. Resolved to -NetworkId via PowerCLI; use -NetworkId directly to skip resolution. .PARAMETER NetworkId MoRef ID of the distributed virtual switch .PARAMETER PortgroupName Friendly name of the portgroup. Resolved to -PortgroupId (and, if -NetworkId/-NetworkName isn't given, its parent switch) via PowerCLI; use -PortgroupId directly to skip resolution. .PARAMETER PortgroupId MoRef ID of the portgroup .PARAMETER PlatformSubnet Subnet (CIDR) for the platform network .PARAMETER PlatformDefaultGateway Default gateway IP for the platform network .PARAMETER NodePool One or more node IP pools, each formatted as "START-END" or "START-END:NAME" .PARAMETER ServicePool One or more service IP pools, each formatted as "START-END" or "START-END:NAME" .PARAMETER InstanceFqdn Ingress FQDN for the SSP platform instance .PARAMETER MessagingFqdn Kafka/messaging FQDN for the SSP platform instance .PARAMETER SspBundleId The ID of the uploaded SSP bundle to deploy (see Get-SspInstallerPackage). Its add-ons are used automatically unless -AddOnIds is passed explicitly. .PARAMETER InstanceName Friendly name for the SSP platform instance .PARAMETER AdminPassword Admin password for the deployed platform .PARAMETER AuditPassword Audit user password for the deployed platform .PARAMETER AddOnIds One or more add-on IDs to include. Defaults to every add-on carried by -SspBundleId; pass this to override that. .PARAMETER PreserveAddons Whether to preserve existing add-ons across an update .PARAMETER SspType ATP (default) | AVI_OPERATIONS .PARAMETER FormFactor SMALL | MEDIUM | LARGE | EXTRA_LARGE .PARAMETER ControllerCount Number of controller nodes. Defaults to 3 — LARGE/MEDIUM/EXTRA_LARGE form factors require exactly 3; AVI_OPERATIONS uses 1 instead. .PARAMETER WorkerCount Number of worker nodes .PARAMETER Troubleshoot Print the HTTP method, URI, and request body (with secrets redacted) sent to the API, without affecting the actual request. .EXAMPLE New-SspInstallerDeployment -VCenterId dcd85e06-49f1-42d9-8241-ec754439c9be ` -SspBundleId a1b2c3d4-... -InstanceName ssp01 ` -DatacenterName DC01 -ClusterName Cluster01 -DatastoreName vsanDatastore ` -Dns 10.0.0.10 -Ntp ntp.vcf.lab -SearchDomain vcf.lab ` -NetworkName VDS01 -PortgroupName SSP-Mgmt ` -PlatformSubnet 10.0.1.0/24 -PlatformDefaultGateway 10.0.1.1 ` -NodePool 10.0.1.10-10.0.1.20 -ServicePool 10.0.1.21-10.0.1.30 ` -InstanceFqdn ssp01.vcf.lab -MessagingFqdn ssp01-msg.vcf.lab #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [Parameter(Mandatory)][string]$VCenterId, [ValidateSet('PRECHECK_ONLY', 'RESET_PRECHECK', 'CONTINUE', 'START', 'STOP', 'RETRY', 'CLEAN')] [string]$Operation = 'PRECHECK_ONLY', # --- compute: friendly name OR direct MoRef/UUID id --- [string]$DatacenterName, [string]$DatacenterId, [string]$ClusterName, [string]$ClusterId, [string]$DatastoreName, [string]$DatastoreId, [string]$StoragePolicyName, [string]$StoragePolicyId, [string]$ResourcePoolName, [string]$ResourcePoolId, [bool]$EnableResourceReservation = $true, # vCenter connection used only for *Name resolution above [string]$VCenterServer, [PSCredential]$VCenterCredential, # --- network --- [Parameter(Mandatory)][string[]]$Dns, [Parameter(Mandatory)][string]$Ntp, [Parameter(Mandatory)][string]$SearchDomain, [string]$NetworkName, [string]$NetworkId, [string]$PortgroupName, [string]$PortgroupId, [Parameter(Mandatory)][string]$PlatformSubnet, [Parameter(Mandatory)][string]$PlatformDefaultGateway, [Parameter(Mandatory)][string[]]$NodePool, [Parameter(Mandatory)][string[]]$ServicePool, # --- service --- [Parameter(Mandatory)][string]$InstanceFqdn, [Parameter(Mandatory)][string]$MessagingFqdn, [Parameter(Mandatory)][string]$SspBundleId, [Parameter(Mandatory)][string]$InstanceName, [string]$AdminPassword, [string]$AuditPassword, [string[]]$AddOnIds, [bool]$PreserveAddons, # --- system --- [ValidateSet('ATP', 'AVI_OPERATIONS')][string]$SspType = 'ATP', [ValidateSet('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE')][string]$FormFactor, # LARGE/MEDIUM/EXTRA_LARGE form factors require exactly 3 (min=max=3 per the API). # Only AVI_OPERATIONS uses 1 instead — override explicitly if you're on that SspType. [int]$ControllerCount = 3, [int]$WorkerCount, [switch]$Troubleshoot ) Assert-SspiConnection $Connection # A bundle carries its own add-ons (Bundle.addons[] — see GET /bundles/{id}); unless the # caller explicitly overrides with -AddOnIds, pull all of them automatically so that's one # less thing to look up by hand. if (-not $PSBoundParameters.ContainsKey('AddOnIds')) { $bundle = Invoke-SspiApi -Connection $Connection -Method GET -Path "/bundles/$SspBundleId" -Troubleshoot:$Troubleshoot if ($bundle -and $bundle.addons) { $AddOnIds = @($bundle.addons | ForEach-Object { $_.id }) } } # Resolve any friendly names to MoRef/UUID IDs (skips entirely if only *Id params were given). $needsResolution = $DatacenterName -or $ClusterName -or $DatastoreName -or $StoragePolicyName -or $ResourcePoolName -or $NetworkName -or $PortgroupName if ($needsResolution) { $resolved = Resolve-SspVCenterMoRefs -VCenterServer $VCenterServer -VCenterCredential $VCenterCredential ` -DatacenterName $DatacenterName -ClusterName $ClusterName -DatastoreName $DatastoreName ` -StoragePolicyName $StoragePolicyName -ResourcePoolName $ResourcePoolName ` -NetworkName $NetworkName -PortgroupName $PortgroupName if ($resolved.DatacenterId) { $DatacenterId = $resolved.DatacenterId } if ($resolved.ClusterId) { $ClusterId = $resolved.ClusterId } if ($resolved.DatastoreId) { $DatastoreId = $resolved.DatastoreId } if ($resolved.StoragePolicyId) { $StoragePolicyId = $resolved.StoragePolicyId } if ($resolved.ResourcePoolId) { $ResourcePoolId = $resolved.ResourcePoolId } if ($resolved.NetworkId) { $NetworkId = $resolved.NetworkId } if ($resolved.PortgroupId) { $PortgroupId = $resolved.PortgroupId } } foreach ($pair in @( @{ Name = 'DatacenterId'; Value = $DatacenterId } @{ Name = 'ClusterId'; Value = $ClusterId } @{ Name = 'DatastoreId'; Value = $DatastoreId } @{ Name = 'StoragePolicyId'; Value = $StoragePolicyId } @{ Name = 'NetworkId'; Value = $NetworkId } @{ Name = 'PortgroupId'; Value = $PortgroupId } )) { if (-not $pair.Value) { throw "$($pair.Name) could not be determined — pass it directly, or pass the matching *Name plus -VCenterServer/-VCenterCredential to resolve it." } } $compute = [ordered]@{ datacenter_id = $DatacenterId cluster_id = $ClusterId storage_policy_id = $StoragePolicyId content_datastore_id = $DatastoreId enable_resource_reservation = $EnableResourceReservation } if ($ResourcePoolId) { $compute.resource_pool_id = $ResourcePoolId } $networkConfig = [ordered]@{ network_id = $NetworkId portgroup_id = $PortgroupId platform_subnet = $PlatformSubnet platform_default_gateway = $PlatformDefaultGateway node_pools = @($NodePool | ForEach-Object { ConvertTo-PoolObject -Value $_ -FlagName '-NodePool' }) service_pools = @($ServicePool | ForEach-Object { ConvertTo-PoolObject -Value $_ -FlagName '-ServicePool' }) } $network = [ordered]@{ dns = @($Dns) ntp = $Ntp search_domain = $SearchDomain network_configs = @($networkConfig) } $service = [ordered]@{ ingress_fqdn = $InstanceFqdn kafka_fqdn = $MessagingFqdn ssp_bundle_id = $SspBundleId instance_name = $InstanceName } if ($AdminPassword -or $AuditPassword) { $service.password_configuration = [ordered]@{ admin_password = $AdminPassword; audit_password = $AuditPassword } } if ($AddOnIds) { $service.addon_ids = @($AddOnIds) } if ($PSBoundParameters.ContainsKey('PreserveAddons')) { $service.preserve_addons = $PreserveAddons } $system = [ordered]@{ ssp_type = $SspType } if ($FormFactor) { $system.form_factor = $FormFactor } $system.controller_count = $ControllerCount if ($PSBoundParameters.ContainsKey('WorkerCount')) { $system.worker_count = $WorkerCount } $payload = [ordered]@{ desired_state = $Operation provider_id = $VCenterId compute = $compute network = $network service = $service system = $system } $json = $payload | ConvertTo-Json -Depth 12 $redactedService = Copy-RedactedHashtable -Source $service -RedactKeys @() if ($redactedService.Contains('password_configuration')) { $redactedService.password_configuration = Copy-RedactedHashtable -Source $service.password_configuration -RedactKeys @('admin_password', 'audit_password') } $redactedPayload = [ordered]@{ desired_state = $Operation; provider_id = $VCenterId compute = $compute; network = $network; service = $redactedService; system = $system } $redactedJson = $redactedPayload | ConvertTo-Json -Depth 12 Invoke-SspiApi -Connection $Connection -Method POST -Path '/platforms' ` -BodyJson $json -RedactedBodyJson $redactedJson -Troubleshoot:$Troubleshoot } Function Get-SspInstallerDeployment { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Returns SSP platform deployment(s) from the SSP Installer .DESCRIPTION This cmdlet returns either all platform deployments known to SSPI, or a single one when -PlatformId is specified. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER PlatformId The ID of a specific platform deployment to return. Returns all deployments if omitted. .PARAMETER Status Return only the deployment's status (requires -PlatformId) .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Get-SspInstallerDeployment .EXAMPLE Get-SspInstallerDeployment -PlatformId dcd85e06-49f1-42d9-8241-ec754439c9be -Status #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [string]$PlatformId, [switch]$Status, [switch]$Troubleshoot ) $path = if ($PlatformId -and $Status) { "/platforms/$PlatformId/status" } elseif ($PlatformId) { "/platforms/$PlatformId" } else { '/platforms' } Out-SspiResult (Invoke-SspiApi -Connection $Connection -Method GET -Path $path -Troubleshoot:$Troubleshoot) } Function Remove-SspInstallerDeployment { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Removes an SSP platform deployment from the SSP Installer .DESCRIPTION This cmdlet deletes (DELETEs) a platform deployment from SSPI. This is a destructive operation and executes immediately. .PARAMETER Connection The SSPI connection object returned by Connect-SspInstaller. Defaults to the session's connection, so you don't need to pass this. .PARAMETER PlatformId The ID of the platform deployment to remove (see Get-SspInstallerDeployment) .PARAMETER Force Force the deletion even if the platform is not in a normally deletable state .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Remove-SspInstallerDeployment -PlatformId dcd85e06-49f1-42d9-8241-ec754439c9be .EXAMPLE Remove-SspInstallerDeployment -PlatformId dcd85e06-49f1-42d9-8241-ec754439c9be -Force #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspiConnection, [Parameter(Mandatory)][string]$PlatformId, [switch]$Force, [switch]$Troubleshoot ) $path = "/platforms/$PlatformId" if ($Force) { $path += "?action=force_delete" } Invoke-SspiApi -Connection $Connection -Method DELETE -Path $path -Troubleshoot:$Troubleshoot Write-Host "Deployment '$PlatformId' successfully removed." } # --------------------------------------------------------------------------- # SSP Instance (the deployed platform itself, NOT the SSP Installer): NSX # Manager onboarding via GET/POST/DELETE /ssp/sites # # This is a second, separate connection type from Connect-SspInstaller above — # SSPI deploys the platform, this talks to the platform's own site-service # once it's up. Source of truth: common/api/site-service/public_specs/sites.yaml # and site_schemas.yaml (NsxManagerSite / SiteConnectionInfo). # Base URL: https://<host>/ssp # --------------------------------------------------------------------------- Function Assert-SspInstanceConnection { param($Connection) if (-not $Connection) { throw "No SSP instance connection available. Run Connect-SspInstance first (it sets the default connection automatically), or pass -Connection explicitly." } if ($Connection.PSTypeNames -notcontains 'Ssp.InstanceConnection') { throw "Expected a connection object from Connect-SspInstance." } } Function Invoke-SspInstanceApi { <# Single choke point for every SSP instance (site-service) call. Mirrors Invoke-SspiApi's -Troubleshoot printing so individual Get/New/Remove functions don't have to duplicate it. #> param( [Parameter(Mandatory)]$Connection, [Parameter(Mandatory)][string]$Method, [Parameter(Mandatory)][string]$Path, [string]$BodyJson, [string]$RedactedBodyJson, [switch]$Troubleshoot ) Assert-SspInstanceConnection $Connection $uri = "$($Connection.BaseUrl)$Path" if ($Troubleshoot) { Write-Host "[TROUBLESHOOT] $Method $uri" $display = if ($RedactedBodyJson) { $RedactedBodyJson } else { $BodyJson } if ($display) { Write-Host $display } } $headers = Get-SspiAuthHeader -Credential $Connection.Credential $timeoutSec = if ($Connection.TimeoutSec) { $Connection.TimeoutSec } else { 15 } $params = @{ Method = $Method; Uri = $uri; Headers = $headers; TimeoutSec = $timeoutSec } if ($BodyJson) { $params.Body = $BodyJson; $params.ContentType = 'application/json' } if ($Connection.Insecure) { if ($PSVersionTable.PSVersion.Major -ge 6) { $params.SkipCertificateCheck = $true } else { Enable-SspiInsecureTls } } try { Invoke-RestMethod @params } catch { $statusCode = $null if ($_.Exception.Response) { $statusCode = [int]$_.Exception.Response.StatusCode } if ($statusCode -eq 401 -or $statusCode -eq 403) { throw [System.Exception]::new( "Authentication failed (HTTP $statusCode) calling $Method $uri. Please re-authenticate using Connect-SspInstance — your session/credentials are no longer valid.", $_.Exception) } $message = $_.ErrorDetails.Message if (-not $message -and $_.Exception.Response) { try { $reader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream()) $message = $reader.ReadToEnd() } catch { } } if (-not $message) { $message = $_.Exception.Message } throw [System.Exception]::new("SSP instance API error ($Method $uri): $message", $_.Exception) } } Function Connect-SspInstance { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Connect to a deployed SSP Instance (the platform itself, not the SSP Installer) .DESCRIPTION Creates a connection object (host + credential) and sets it as the default for every other SSP instance function in this session (Get-/New-/Remove-SspInstanceNsx). There is no server-side session here — this just packages what every other call needs and stashes it so you don't have to pass -Connection every time. This is a separate connection from Connect-SspInstaller: that one talks to the installer (SSPI); this one talks to the deployed platform's own API once it's up and running. .PARAMETER SspInstanceHost The hostname/FQDN or IP address of the deployed SSP Instance .PARAMETER Credential Credential for the SSP Instance API (Basic auth). Prompted for if omitted. Defaults to username 'admin'. .PARAMETER Insecure Skip TLS certificate validation. Defaults to $true, matching a lab environment with self-signed certificates. .PARAMETER TimeoutSec Request timeout in seconds. Defaults to 15. .EXAMPLE Connect-SspInstance -SspInstanceHost ssp-inst01.vcf.lab #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$SspInstanceHost, [PSCredential]$Credential, [bool]$Insecure = $true, [int]$TimeoutSec = 15 ) if (-not $Credential) { $Credential = Get-Credential -Message "SSP instance credentials for https://$SspInstanceHost/ssp" -UserName 'admin' } $conn = [PSCustomObject]@{ PSTypeName = 'Ssp.InstanceConnection' SspInstanceHost = $SspInstanceHost BaseUrl = "https://$SspInstanceHost/ssp" Credential = $Credential Insecure = $Insecure TimeoutSec = $TimeoutSec } $script:SspInstanceConnection = $conn Write-Host "Default SSP instance connection set: $($conn.BaseUrl) (user: $($Credential.UserName)). Pass -Connection to override for a specific call." return $conn } Function Get-SspInstanceNsx { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Returns NSX Manager(s) onboarded to a deployed SSP Instance .DESCRIPTION This cmdlet returns either all NSX Manager sites onboarded to the SSP Instance (GET /ssp/sites?site_type=NSX_MANAGER), or a single one when -NsxManagerId is specified. Note the platform never stores the username/password used to onboard a site, so those fields always come back empty. .PARAMETER Connection The SSP instance connection object returned by Connect-SspInstance. Defaults to the session's connection, so you don't need to pass this. .PARAMETER NsxManagerId The site ID of a specific NSX Manager to return. Returns all onboarded NSX Managers if omitted. .PARAMETER Troubleshoot Print the HTTP method and URI sent to the API, without affecting the actual request. .EXAMPLE Get-SspInstanceNsx .EXAMPLE Get-SspInstanceNsx -NsxManagerId 2dc09816-34c0-43fb-99a6-b19b249a43da #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspInstanceConnection, [string]$NsxManagerId, [switch]$Troubleshoot ) $path = if ($NsxManagerId) { "/sites/$NsxManagerId" } else { '/sites?site_type=NSX_MANAGER' } Out-SspiResult (Invoke-SspInstanceApi -Connection $Connection -Method GET -Path $path -Troubleshoot:$Troubleshoot) } Function New-SspInstanceNsx { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Onboards a new NSX Manager to a deployed SSP Instance .DESCRIPTION This cmdlet POSTs a new NSX_MANAGER site to the SSP Instance's site-service (POST /ssp/sites) with a DYNAMIC connection (SSP discovers all manager IPs in the cluster from the hostname given). Onboarding runs asynchronously on the platform side: prechecks first, then the actual onboarding. Pass -PollStatus to wait and watch it move out of ONBOARD_IN_PROGRESS. The username/password given here are one-time use to establish the connection — SSP does not store or re-use them. .PARAMETER Connection The SSP instance connection object returned by Connect-SspInstance. Defaults to the session's connection, so you don't need to pass this. .PARAMETER NsxManager The hostname/FQDN of the NSX Manager to onboard. SSP does not support connecting via IP for this call. .PARAMETER NsxCredential Credential for the NSX Manager (one-time use to establish the connection). Prompted for if omitted. .PARAMETER CertificateFile Path to a PEM-encoded certificate file used to authenticate with the NSX Manager. .PARAMETER SiteName Friendly name for the site. Not editable after onboarding. Defaults to -NsxManager if omitted. .PARAMETER Force Use if the NSX Manager was previously onboarded to a different SSP instance and stale references remain on it (SiteRegistrationPrecheck failing). Deletes those stale artifacts and onboards to this instance instead. This does NOT replace properly offboarding the manager from its old SSP instance first — use with care. .PARAMETER PollStatus After a successful onboard request, poll the site's status every 5 seconds until it leaves ONBOARD_IN_PROGRESS. .PARAMETER Troubleshoot Print the HTTP method, URI, and request body (with secrets redacted) sent to the API, without affecting the actual request. .EXAMPLE New-SspInstanceNsx -NsxManager nsx01.vcf.lab -NsxCredential $nsxCred -CertificateFile ~/Desktop/nsx01.vcf.lab.pem -PollStatus #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspInstanceConnection, [Parameter(Mandatory)][string]$NsxManager, [PSCredential]$NsxCredential, [Parameter(Mandatory)][string]$CertificateFile, [string]$SiteName, [switch]$Force, [switch]$PollStatus, [switch]$Troubleshoot ) Assert-SspInstanceConnection $Connection if (-not $NsxCredential) { $NsxCredential = Get-Credential -Message "NSX Manager credentials for $NsxManager" } if (-not (Test-Path $CertificateFile)) { throw "Certificate file not found: $CertificateFile" } $cert = Get-Content -Raw -Path $CertificateFile if (-not $SiteName) { $SiteName = $NsxManager } $body = [ordered]@{ site_type = 'NSX_MANAGER' site_name = $SiteName desired_state = 'ONBOARD' site_connection_info = [ordered]@{ connection_type = 'DYNAMIC' hostname = $NsxManager username = $NsxCredential.UserName password = $NsxCredential.GetNetworkCredential().Password certificate = $cert } } $json = $body | ConvertTo-Json -Depth 6 $redactedConnInfo = Copy-RedactedHashtable -Source $body.site_connection_info -RedactKeys @('password', 'certificate') $redactedBody = [ordered]@{ site_type = $body.site_type site_name = $body.site_name desired_state = $body.desired_state site_connection_info = $redactedConnInfo } $redactedJson = $redactedBody | ConvertTo-Json -Depth 6 $path = '/sites' if ($Force) { $path += '?force=true' } $result = Invoke-SspInstanceApi -Connection $Connection -Method POST -Path $path ` -BodyJson $json -RedactedBodyJson $redactedJson -Troubleshoot:$Troubleshoot if ($PollStatus -and $result.id) { do { Start-Sleep -Seconds 5 $site = Invoke-SspInstanceApi -Connection $Connection -Method GET -Path "/sites/$($result.id)" -Troubleshoot:$Troubleshoot $status = $site.status.configuration_status.current_status Write-Host ("Status: {0} {1}" -f $status, $site.status.configuration_status.configuration_message) } while ($status -eq 'ONBOARD_IN_PROGRESS') return $site } return $result } Function Remove-SspInstanceNsx { <# .NOTES =========================================================================== Created by: William Lam Organization: Broadcom Blog: http://www.williamlam.com Twitter: @lamw =========================================================================== .SYNOPSIS Offboards and removes an NSX Manager from a deployed SSP Instance .DESCRIPTION This cmdlet DELETEs an NSX_MANAGER site from the SSP Instance (DELETE /ssp/sites/{site-id}). This is a destructive operation and executes immediately (offboarding itself runs asynchronously on the platform side — poll Get-SspInstanceNsx -NsxManagerId until it 404s to confirm completion). Unless -Force is passed, NSX Manager admin credentials are required so SSP can clean up platform-specific artifacts on the manager itself. .PARAMETER Connection The SSP instance connection object returned by Connect-SspInstance. Defaults to the session's connection, so you don't need to pass this. .PARAMETER NsxManagerId The site ID of the NSX Manager to remove (see Get-SspInstanceNsx) .PARAMETER NsxCredential NSX Manager admin credential, used once to clean up platform-specific artifacts on the manager during offboarding. Required unless -Force is passed. Prompted for if omitted and required. .PARAMETER Force Offboard on a best-effort basis, ignoring errors communicating with the NSX Manager (e.g. if it's unreachable). The site is still removed from SSP, but references to SSP may remain on the manager — re-onboarding it elsewhere may then require -Force there too. .PARAMETER Troubleshoot Print the HTTP method, URI, and request body (with secrets redacted) sent to the API, without affecting the actual request. .EXAMPLE Remove-SspInstanceNsx -NsxManagerId 2dc09816-34c0-43fb-99a6-b19b249a43da .EXAMPLE Remove-SspInstanceNsx -NsxManagerId 2dc09816-34c0-43fb-99a6-b19b249a43da -Force #> [CmdletBinding()] param( [Parameter(ValueFromPipeline)]$Connection = $script:SspInstanceConnection, [Parameter(Mandatory)][string]$NsxManagerId, [PSCredential]$NsxCredential, [switch]$Force, [switch]$Troubleshoot ) Assert-SspInstanceConnection $Connection if (-not $Force -and -not $NsxCredential) { $NsxCredential = Get-Credential -Message "NSX Manager admin credentials to offboard NSX Manager '$NsxManagerId' (required unless -Force)" } $json = $null $redactedJson = $null if ($NsxCredential) { $credBody = [ordered]@{ username = $NsxCredential.UserName; password = $NsxCredential.GetNetworkCredential().Password } $json = $credBody | ConvertTo-Json $redactedJson = (Copy-RedactedHashtable -Source $credBody -RedactKeys 'password') | ConvertTo-Json } $path = "/sites/$NsxManagerId" if ($Force) { $path += '?force=true' } Invoke-SspInstanceApi -Connection $Connection -Method DELETE -Path $path ` -BodyJson $json -RedactedBodyJson $redactedJson -Troubleshoot:$Troubleshoot Write-Host "NSX Manager site '$NsxManagerId' offboard request accepted." } |