Private/NutanixApi.ps1
|
<# Low-level HTTP/auth/pagination/rate-limiting for Prism Central v4 REST. Unlike vCenter (PowerCLI/SOAP session), Prism Central v4 is stateless plain REST with per-request HTTP Basic auth, so there is no Connect/Disconnect session step here - Get-NutanixApiContext just resolves credentials and builds the base URI + auth header. Reference: https://developers.nutanix.com/api-reference-v4/ , https://www.nutanix.dev/nutanix-api-user-guide/ (pagination, rate limits) #> function Get-NutanixApiContext { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [NutanixEnvironmentConfiguration]$EnvironmentConfig ) Write-CustomLog -Message "Resolving credentials for Nutanix Prism Central: $($EnvironmentConfig.PrismCentralFQDN)" -Severity 'DEBUG' $credential = Get-SecureCredential -Target $EnvironmentConfig.WindowsCredentialEntry $username = $credential.UserName $password = $credential.GetNetworkCredential().Password $basicHeader = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("${username}:${password}")) return [pscustomobject]@{ BaseUri = "https://$($EnvironmentConfig.PrismCentralFQDN):$($EnvironmentConfig.PrismCentralPort)/api" Headers = @{ 'Authorization' = "Basic $basicHeader" 'Accept' = 'application/json' } SkipCertificateCheck = $EnvironmentConfig.SkipCertificateCheck RateGate = [System.Threading.SemaphoreSlim]::new(1, 1) RequestStarts = [System.Collections.Concurrent.ConcurrentQueue[long]]::new() } } function Build-NutanixQueryString { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [hashtable]$Query = @{} ) if ($null -eq $Query -or $Query.Count -eq 0) { return '' } $pairs = foreach ($key in $Query.Keys) { $value = $Query[$key] if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string]$value)) { continue } "{0}={1}" -f [Uri]::EscapeDataString($key), [Uri]::EscapeDataString([string]$value) } if ($pairs.Count -eq 0) { return '' } return '?' + ($pairs -join '&') } function Wait-NutanixApiRequestSlot { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$Context ) while ($true) { [void]$Context.RateGate.Wait() $slotAcquired = $false $waitMilliseconds = 1 try { $nowMilliseconds = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() [long]$oldestStart = 0 while ($Context.RequestStarts.TryPeek([ref]$oldestStart) -and ($nowMilliseconds - $oldestStart) -ge 1000) { [long]$discardedStart = 0 $null = $Context.RequestStarts.TryDequeue([ref]$discardedStart) } if ($Context.RequestStarts.Count -lt $script:NUTANIX_API_MAX_REQUESTS_PER_SECOND) { $Context.RequestStarts.Enqueue($nowMilliseconds) $slotAcquired = $true } elseif ($Context.RequestStarts.TryPeek([ref]$oldestStart)) { $waitMilliseconds = [Math]::Max(1, 1000 - ($nowMilliseconds - $oldestStart)) } } finally { $null = $Context.RateGate.Release() } if ($slotAcquired) { return } Start-Sleep -Milliseconds $waitMilliseconds } } function Get-NutanixApiFailureStatusCode { param( [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) if ($null -eq $ErrorRecord.Exception.Response) { return $null } return [int]$ErrorRecord.Exception.Response.StatusCode.value__ } function Get-NutanixApiRetryDelay { param( [Parameter(Mandatory = $true)] [int]$Attempt, [Parameter(Mandatory = $false)] [AllowNull()] [Nullable[int]]$StatusCode, [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) $backoffSeconds = [Math]::Min(30, [Math]::Pow(2, $Attempt)) if ($StatusCode -ne 429 -or $null -eq $ErrorRecord.Exception.Response.Headers) { return $backoffSeconds } $retryAfter = [string]$ErrorRecord.Exception.Response.Headers['Retry-After'] $retryAfterSeconds = 0 if ([int]::TryParse($retryAfter, [ref]$retryAfterSeconds) -and $retryAfterSeconds -gt 0) { return [Math]::Min(60, $retryAfterSeconds) } return $backoffSeconds } function Invoke-NutanixApiRequest { <# .SYNOPSIS GETs a single page from a Prism Central v4 endpoint, with 429 backoff and request pacing. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$Context, [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $false)] [hashtable]$Query = @{} ) $uri = "$($Context.BaseUri)/$Path" + (Build-NutanixQueryString -Query $Query) $attempt = 0 while ($true) { $attempt++ Wait-NutanixApiRequestSlot -Context $Context try { $response = Invoke-WebRequestWithLogging -Uri $uri -Method 'GET' -Headers $Context.Headers -UseBasicParsing -SkipCertificateCheck:$Context.SkipCertificateCheck return ($response.Content | ConvertFrom-Json) } catch { $statusCode = Get-NutanixApiFailureStatusCode -ErrorRecord $_ $isTransientFailure = ($null -eq $statusCode -or $statusCode -in @(408, 429, 502, 503, 504)) if (-not $isTransientFailure -or $attempt -gt $script:NUTANIX_API_MAX_RETRIES) { throw "Nutanix API request failed. Uri=$uri Attempt=$attempt Error=$($_.Exception.Message)" } $backoffSeconds = Get-NutanixApiRetryDelay -Attempt $attempt -StatusCode $statusCode -ErrorRecord $_ Write-CustomLog -Message "Transient Nutanix API failure (HTTP $statusCode). Backing off ${backoffSeconds}s before retry $attempt/$($script:NUTANIX_API_MAX_RETRIES). Uri=$uri" -Severity 'WARNING' Start-Sleep -Seconds $backoffSeconds } } } function Get-NutanixPaginatedResults { <# .SYNOPSIS Pages through a Prism Central v4 list endpoint, concatenating '.data' across pages. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [pscustomobject]$Context, [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $false)] [hashtable]$ExtraQuery = @{} ) $results = [System.Collections.Generic.List[object]]::new() $page = 0 $limit = $script:NUTANIX_STATS_PAGE_LIMIT while ($true) { $query = @{} + $ExtraQuery $query['$limit'] = $limit $query['$page'] = $page $response = Invoke-NutanixApiRequest -Context $Context -Path $Path -Query $query $pageData = @($response.data) foreach ($item in $pageData) { $results.Add($item) } if ($pageData.Count -lt $limit) { break } $page++ } return $results } |