Private/connect-functions.ps1
|
# Functions to connect and manage connections function Get-CSConnection { <# .SYNOPSIS Returns the current CloudStack connection information. .DESCRIPTION Retrieves the stored connection parameters. Used internally by other functions. #> if ($null -eq $script:CSConnection) { throw "Not connected to CloudStack server. Please run Connect-CSServer first." } return $script:CSConnection } function New-CSSignature { param( [string]$QueryString, [string]$SecretKey ) $hmacsha1 = New-Object System.Security.Cryptography.HMACSHA1 $hmacsha1.Key = [Text.Encoding]::UTF8.GetBytes($SecretKey) $signatureBytes = $hmacsha1.ComputeHash([Text.Encoding]::UTF8.GetBytes($QueryString.ToLower())) $signature = [Convert]::ToBase64String($signatureBytes) return $signature } function Invoke-CSApiRequest { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$Command, [Parameter(Mandatory=$false)] [hashtable]$Parameters = @{} ) $connection = Get-CSConnection # Start with base parameters $apiParams = @{ command = $Command apiKey = $connection.ApiKey response = 'json' } # Copy the caller's parameters, skipping only $null. An explicit empty string # is intentional and must be sent (e.g. Set-CSConfiguration -Value '' to clear # a setting), so it is not dropped here. foreach ($key in $Parameters.Keys) { if ($null -ne $Parameters[$key]) { $apiParams[$key] = $Parameters[$key] } } # Sort parameters alphabetically (case-insensitive) and build query string for signing $sortedParams = $apiParams.GetEnumerator() | Sort-Object {$_.Name.ToLower()} $canonicalQueryStringParts = @() $urlQueryStringParts = @() foreach ($param in $sortedParams) { $key = $param.Name.ToLower() $rawValue = $param.Value if ($rawValue -is [System.IFormattable]) { # Format numbers with the invariant culture so a decimal such as 2.5 # is never sent (or signed) as '2,5' on comma-decimal locales. $value = $rawValue.ToString($null, [System.Globalization.CultureInfo]::InvariantCulture) } else { $value = $rawValue.ToString() } # CloudStack canonical signing encodes values, not field names. This # matters for indexed names such as tags[0].key. Encode both pieces # only when constructing the transmitted URL. $encodedValue = [System.Uri]::EscapeDataString($value) $canonicalQueryStringParts += "$key=$encodedValue" $encodedKey = [System.Uri]::EscapeDataString($key) $urlQueryStringParts += "$encodedKey=$encodedValue" } $canonicalQueryString = $canonicalQueryStringParts -join '&' $urlQueryString = $urlQueryStringParts -join '&' Write-Verbose "Query string for signing: $canonicalQueryString" # Generate signature $signature = New-CSSignature -QueryString $canonicalQueryString -SecretKey $connection.SecretKey $encodedSignature = [System.Uri]::EscapeDataString($signature) # Build final URL $url = "$($connection.BaseUrl)?$urlQueryString&signature=$encodedSignature" Write-Verbose "API Request: $Command" Write-Verbose "Full URL: $url" try { $response = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 30 -ErrorAction Stop Write-Verbose "API Response received successfully" if ($response.PSObject.Properties.Name -match 'errorresponse') { $errorMsg = "CloudStack API Error: $($response.errorresponse.errortext)" Write-Error $errorMsg throw $errorMsg } return $response } catch [System.Net.WebException] { $statusCode = [int]$_.Exception.Response.StatusCode $statusDescription = $_.Exception.Response.StatusDescription Write-Error "HTTP $statusCode - $statusDescription" Write-Error "URL: $url" if ($_.Exception.Response) { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $responseBody = $reader.ReadToEnd() Write-Error "Response: $responseBody" } throw } catch { Write-Error "CloudStack API request failed: $_" Write-Error "Command: $Command" Write-Error "URL: $url" throw } } |