Private/Invoke-PVERequest.ps1
|
function Invoke-PVERequest { <# .SYNOPSIS Single entry point for every call to the Proxmox VE REST API. .DESCRIPTION - Uses the /api2/json formatter. - Adds authentication: an API token header, or the PVEAuthCookie ticket plus a CSRFPreventionToken header on non-GET requests. - Renews a ticket that is older than $script:PVETicketRenewAfter, and on HTTP 401 logs in again once with the stored credential and retries. - Parameters are sent as a query string for GET/DELETE and as a form-encoded body for POST/PUT. -Form sends a multipart/form-data body instead (file uploads), built by New-PVEMultipartContent in the field order given; it is built afresh for the retry after a 401, since the file stream is read once per request. - Any HTTP status >= 400 becomes a terminating error carrying PVE's own message (taken from the HTTP reason phrase, plus any per-parameter "errors" in the body). Returns the "data" member of the response. #> [CmdletBinding()] param( [Parameter(Mandatory)] [psobject]$Connection, [ValidateSet('GET', 'POST', 'PUT', 'DELETE')] [string]$Method = 'GET', [Parameter(Mandatory)] [string]$Path, [hashtable]$Parameters, # Multipart body for POST uploads (Invoke-WebRequest -Form). Use an ordered dictionary. [System.Collections.IDictionary]$Form, # Send no authentication (used for the ticket request itself). [switch]$NoAuth, # Do not attempt a re-login and retry on HTTP 401. [switch]$NoRetry ) if (-not $Path.StartsWith('/')) { $Path = "/$Path" } $state = $null if (-not $NoAuth) { $state = $script:PVEAuthState[$Connection.Id] if ($null -eq $state) { throw "The connection to $($Connection.Server) is not open. Run Connect-PVE first." } if ($state.AuthType -eq 'Ticket' -and ((Get-Date) - $state.TicketTime) -gt $script:PVETicketRenewAfter) { Write-Verbose "Ticket for $($Connection.Server) is older than $($script:PVETicketRenewAfter); renewing." Update-PVEAuthTicket -Connection $Connection $state = $script:PVEAuthState[$Connection.Id] } } $uri = '{0}/api2/json{1}' -f $Connection.BaseUri, $Path $encoded = ConvertTo-PVEFormData -Parameters $Parameters $request = @{ Method = $Method Headers = @{} SkipHttpErrorCheck = $true SkipCertificateCheck = [bool]$Connection.SkipCertificateCheck ErrorAction = 'Stop' Verbose = $false Debug = $false } $multipart = $null if ($Method -in 'GET', 'DELETE') { if ($encoded) { $uri = '{0}?{1}' -f $uri, $encoded } } elseif ($Form) { $multipart = New-PVEMultipartContent -Form $Form $request.Body = $multipart } elseif ($encoded) { $request.Body = $encoded $request.ContentType = 'application/x-www-form-urlencoded' } $request.Uri = $uri if (-not $NoAuth) { if ($state.AuthType -eq 'ApiToken') { $request.Headers['Authorization'] = $state.TokenHeader } else { $request.Headers['Cookie'] = 'PVEAuthCookie={0}' -f $state.Ticket if ($Method -ne 'GET') { $request.Headers['CSRFPreventionToken'] = $state.CsrfToken } } } # Never log the body: it carries passwords on /access/ticket. Write-Verbose ('{0} {1}{2}' -f $Method, ($Connection.BaseUri + '/api2/json' + $Path), $(if ($Method -in 'GET', 'DELETE' -and $encoded) { '?' + $encoded } else { '' })) try { $response = Invoke-WebRequest @request } catch [System.Management.Automation.PipelineStoppedException] { throw } catch { $record = [System.Management.Automation.ErrorRecord]::new( [System.Net.Http.HttpRequestException]::new("Unable to reach $($Connection.BaseUri): $($_.Exception.Message)", $_.Exception), 'PVEConnectionFailed', [System.Management.Automation.ErrorCategory]::ConnectionError, $Connection.BaseUri ) $PSCmdlet.ThrowTerminatingError($record) } finally { if ($multipart) { $multipart.Dispose() } } $status = [int]$response.StatusCode $reason = $response.BaseResponse.ReasonPhrase $content = $response.Content if ($content -is [byte[]]) { $content = [System.Text.Encoding]::UTF8.GetString($content) } $body = $null if (-not [string]::IsNullOrWhiteSpace($content)) { try { $body = $content | ConvertFrom-Json -Depth 100 } catch { $body = $null } } if ($status -eq 401 -and -not $NoAuth -and -not $NoRetry -and $state.AuthType -eq 'Ticket' -and $state.Credential) { Write-Verbose "HTTP 401 from $($Connection.Server); logging in again and retrying once." Update-PVEAuthTicket -Connection $Connection -ForceLogin $retry = @{ Connection = $Connection Method = $Method Path = $Path Parameters = $Parameters NoRetry = $true } if ($Form) { $retry.Form = $Form } return Invoke-PVERequest @retry } if ($status -ge 400) { $message = if ($reason) { $reason.Trim() } else { "HTTP $status" } $bodyMessage = ([string](Get-PVEValue $body 'message')).Trim() if ($bodyMessage -and $bodyMessage -ne $message) { $message = '{0}: {1}' -f $message, $bodyMessage } $bodyErrors = Get-PVEValue $body 'errors' if ($bodyErrors) { $details = foreach ($property in $bodyErrors.PSObject.Properties) { '{0}: {1}' -f $property.Name, ([string]$property.Value).Trim() } if ($details) { $message = '{0} ({1})' -f $message, ($details -join '; ') } } $category = switch ($status) { 400 { [System.Management.Automation.ErrorCategory]::InvalidArgument } 401 { [System.Management.Automation.ErrorCategory]::AuthenticationError } 403 { [System.Management.Automation.ErrorCategory]::PermissionDenied } 404 { [System.Management.Automation.ErrorCategory]::ObjectNotFound } default { [System.Management.Automation.ErrorCategory]::InvalidOperation } } $exception = [System.Net.Http.HttpRequestException]::new( ('{0} {1} failed with HTTP {2}: {3}' -f $Method, $Path, $status, $message), $null, [System.Net.HttpStatusCode]$status ) $record = [System.Management.Automation.ErrorRecord]::new($exception, "PVEHttp$status", $category, $Path) $PSCmdlet.ThrowTerminatingError($record) } if ($null -eq $body) { return $null } if ($body -is [psobject] -and $body.PSObject.Properties['data']) { return $body.data } $body } |