Public/Invoke-SDPRestCall.ps1
|
<# .SYNOPSIS Custom rest call for Kaminario K2 platform .EXAMPLE (after logging into a K2) Invoke-SDPRestCall -endpoint volume_groups -method GET This will return the .hits return for the https://{k2Server}/api/v2/volume_groups API endpoint using the method GET. .EXAMPLE Invoke-SDPRestCall -endpoint volume -method PATCH -body $body -context TestDev This will render the .hits return for the https://{k2Server}/api/v2/volumes API endpoint. .NOTES Authored by J.R. Phillips (GitHub: JayAreP) .LINK https://github.com/silk-us/silk-sdp-powershell-sdk #> function Invoke-SDPRestCall { param( [parameter(Mandatory)] [string] $endpoint, [parameter(Mandatory)] [ValidateSet('GET','POST','PATCH','DELETE')] [string] $method, [parameter()] [array] $body, [parameter()] [hashtable] $parameterList, [parameter()] [string] $context = 'sdpconnection', [parameter()] [int] $limit = 9999, [parameter()] [switch] $strictURI, [parameter()] [switch] $strictString, [parameter()] [array] $strictURIgte, [parameter()] [array] $strictURIlte, [parameter()] [switch] $noLimit, [parameter()] [switch] $fullResponse, [parameter()] [int] $timeOut = 15 ) # Construct the base URI. New-SDPURI returns a value ending in '?'; # strip it so we have a clean endpoint and can decide later whether # to append a query string (legacy path) or pass query params via # -Body to Invoke-RestMethod (strictURI path). $endpointURI = (New-SDPURI -endpoint $endpoint -context $context).TrimEnd('?') # Strip CommonParameters and context from the parameter list so we # only walk user-supplied filters. if ($parameterList) { foreach ($p in [System.Management.Automation.PSCmdlet]::CommonParameters) { $parameterList.Remove($p) | Out-Null } $parameterList.Remove('context') | Out-Null } # Decide how to deliver query parameters. # # strictURI + GET: build a hashtable of operator-suffixed keys # (name__in, id__gt, etc.) and pass it via -Body to Invoke-RestMethod. # On a GET, Invoke-RestMethod auto-serializes a hashtable body to a # URL-encoded query string - cleaner than the manual concatenation # we used to do, and properly URL-encoded for free. # # Everything else (legacy GET, POST/PATCH/DELETE): keep the URL plain # and append __limit the old way for compatibility with cmdlets that # haven't been migrated to strictURI yet. They'll still post-fetch # filter client-side below. $queryParams = $null if ($strictURI -and $method -eq 'GET') { $queryParams = @{} if (-not $noLimit) { $queryParams.Add('__limit', $limit) } if ($parameterList -and $parameterList.Count -gt 0) { Write-Verbose "-- REST (strictURI) using parameters --" $parameterList | ConvertTo-Json -Depth 10 | Write-Verbose foreach ($p in $parameterList.Keys) { $value = $parameterList[$p] if ($value.ref) { Write-Verbose "$p declared as REF; skipping URI" continue } if ($value -is [int]) { if ($strictURIgte -contains $p) { $queryParams.Add("${p}__gt", $value) } elseif ($strictURIlte -contains $p) { $queryParams.Add("${p}__lt", $value) } else { $queryParams.Add("${p}__in", $value) } } elseif ($value -is [bool]) { $queryParams.Add($p, $value) } else { # Strings: __in is the new default. -strictString is retained # for back-compat but is now a no-op since __in == bare equality # for a single scalar value. $queryParams.Add("${p}__in", $value) } } Write-Verbose "-- REST (strictURI) using keylist --" $queryParams | ConvertTo-Json -Depth 10 | Write-Verbose } } else { # Legacy path: append __limit to URL for non-migrated cmdlets. if ($method -eq 'GET' -and -not $noLimit) { $endpointURI = $endpointURI + '?__limit=' + $limit } $endpointURI = New-URLEncode -URL $endpointURI -context $context if ($parameterList -and $parameterList.Count -gt 0) { Write-Verbose "-- REST using parameters (post-fetch filter) --" $parameterList | ConvertTo-Json -Depth 10 | Write-Verbose } } # JSON body for POST/PATCH. if ($body) { $bodyjson = $body | ConvertTo-Json -Depth 10 Write-Verbose "-- REST Using following JSON body --" Write-Verbose $bodyjson } Write-Verbose "Invoke-SDPRestCall --> Requesting $method from $endpointURI <--- Final URI" # declare the requested context's credential information $restContext = Get-Variable -Scope Global -Name $context -ValueOnly -ErrorAction SilentlyContinue if (-not $restContext) { Write-Error "No SDP session found for context '$context'. Run 'Connect-SDP' (or pass -context <name> if you connected with a custom context name)." -Category AuthenticationError return } # Make the call. # # Two failure modes need disambiguation in the catch: # 1. HTTP 2xx with empty body and Content-Type: application/json. Invoke- # RestMethod's deserializer throws on empty-body JSON. The operation # actually succeeded; we should swallow the throw and return $null. # 2. HTTP 4xx/5xx, with or without a body. The operation actually failed # and the caller needs to know - even when the server didn't bother # to include a JSON error_msg. # The resolver inspects $_.Exception.Response.StatusCode to tell them apart. $resolveRestException = { param($errorRecord) $statusCode = $null if ($errorRecord.Exception.Response) { try { $statusCode = [int]$errorRecord.Exception.Response.StatusCode } catch { } } # Case 1: HTTP success, deserializer choked on empty body. if ($statusCode -and $statusCode -ge 200 -and $statusCode -lt 300) { Write-Verbose "Invoke-RestMethod threw on empty-body HTTP $statusCode; treating as success." return $true } # Case 2: real failure. Build the most informative message we can. $detailMsg = $errorRecord.ErrorDetails.Message $msg = $null if (-not [string]::IsNullOrWhiteSpace($detailMsg)) { try { $parsed = $detailMsg | ConvertFrom-Json -ErrorAction Stop if ($parsed.error_msg) { $msg = $parsed.error_msg } else { $msg = $detailMsg } } catch { $msg = $detailMsg } } if (-not $msg) { if ($statusCode) { $msg = "API request failed with HTTP $statusCode and no response body." } else { $msg = $errorRecord.Exception.Message } } Write-Error $msg return $false } if ($PSVersionTable.PSEdition -eq 'Core') { if ($body) { try { $results = Invoke-RestMethod -Method $method -Uri $endpointURI -body $bodyjson -ContentType 'application/json' -Credential $restContext.credentials -SkipCertificateCheck -TimeoutSec $timeOut } catch { if (& $resolveRestException $_) { $results = $null } else { return } } } elseif ($queryParams) { try { $results = Invoke-RestMethod -Method $method -Uri $endpointURI -body $queryParams -Credential $restContext.credentials -SkipCertificateCheck -TimeoutSec $timeOut } catch { if (& $resolveRestException $_) { $results = $null } else { return } } } else { try { $results = Invoke-RestMethod -Method $method -Uri $endpointURI -Credential $restContext.credentials -SkipCertificateCheck -TimeoutSec $timeOut } catch { if (& $resolveRestException $_) { $results = $null } else { return } } } } elseif ($PSVersionTable.PSEdition -eq 'Desktop') { if ([System.Net.ServicePointManager]::CertificatePolicy -notlike 'TrustAllCertsPolicy') { Write-Verbose "Correcting certificate policy" Unblock-CertificatePolicy } if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol + 'Tls12' } if ($body) { try { $results = Invoke-RestMethod -Method $method -Uri $endpointURI -body $bodyjson -ContentType 'application/json' -Credential $restContext.credentials -TimeoutSec $timeOut } catch { if (& $resolveRestException $_) { $results = $null } else { return } } } elseif ($queryParams) { try { $results = Invoke-RestMethod -Method $method -Uri $endpointURI -body $queryParams -Credential $restContext.credentials -TimeoutSec $timeOut } catch { if (& $resolveRestException $_) { $results = $null } else { return } } } else { try { $results = Invoke-RestMethod -Method $method -Uri $endpointURI -Credential $restContext.credentials -TimeoutSec $timeOut } catch { if (& $resolveRestException $_) { $results = $null } else { return } } } } <# Due to how the API accepts arguments, I often need to capture all results and filter for the desired objects after-the-fact. If this looks inefficient, it's because it is. Thankfully there's not a lot of metadata presented through these queries, so the operational impact is minimal. #> if ($fullResponse) { return $results } else { $results = $results.hits } if ($parameterList.Count -gt 0 -and $strictURI -eq $false) { $rcount = $results.Count Write-Verbose "Found $rcount results" if ($parameterList.keys) { $searchkeys = $parameterList.keys.split() } foreach ($i in $searchkeys) { Write-Verbose "Working with key: $i" $parseTarget = $parameterList[$i] # return $parseTarget if ($parseTarget.ref) { $results = $results | where-object {$_.$i.ref -eq $parseTarget.ref} $rcount = $results.Count Write-Verbose "Searching for key $parseTarget as REF" Write-Verbose "Found $rcount results for key $i" } else { $results = $results | where-object {$_.$i -eq $parseTarget} $rcount = $results.Count Write-Verbose "Searching for key $parseTarget" Write-Verbose "Found $rcount results for key $i" } } } # Return the results of the call back to the cmdlet. foreach ($o in $results) { if ($o.id) { $o | Add-Member -MemberType NoteProperty -Name 'pipeId' -Value $o.id } if ($o.name) { $o | Add-Member -MemberType NoteProperty -Name 'pipeName' -Value $o.name } } if ($restContext.throttleCorrection.IsPresent) { Start-Sleep -Seconds 1 } return $results } |