Private/api-helpers.ps1
|
# Shared response/async-job helpers used by the public network functions. function ConvertFrom-CSResponse { <# .SYNOPSIS Unwraps a CloudStack API response down to the useful payload. .DESCRIPTION CloudStack wraps every response as <command>response. List responses hold a 'count' plus one array property (e.g. physicalnetwork); create/update responses hold one object property (e.g. network). When the body holds exactly one object/array property (ignoring 'count') that value is returned; otherwise the body itself is returned (e.g. { success = true } or an async { jobid, id } pair). An empty list returns nothing. #> param( [Parameter(Mandatory = $true)] [AllowNull()] [object]$Response, [Parameter(Mandatory = $true)] [string]$Command ) if ($null -eq $Response) { return } $body = $Response.("$($Command.ToLowerInvariant())response") if ($null -eq $body) { return } $props = @($body.PSObject.Properties | Where-Object { $_.Name -ne 'count' }) if ($props.Count -eq 0) { return } if ($props.Count -eq 1 -and $null -ne $props[0].Value -and -not ($props[0].Value -is [string] -or $props[0].Value -is [ValueType])) { return $props[0].Value } return $body } function Wait-CSAsyncJob { <# .SYNOPSIS Polls queryAsyncJobResult until a CloudStack async job finishes. .DESCRIPTION Returns the unwrapped job result on success (e.g. the created physical network) and throws with the CloudStack error text on failure. Writes a warning and returns nothing if the job is still pending at the timeout. #> param( [Parameter(Mandatory = $true)] [string]$JobId, [int]$TimeoutSeconds = 300, [int]$PollSeconds = 3 ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ($true) { $status = ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command 'queryAsyncJobResult' -Parameters @{ jobid = $JobId }) -Command 'queryAsyncJobResult' switch ([int]$status.jobstatus) { 1 { $result = $status.jobresult if ($null -eq $result) { return } $props = @($result.PSObject.Properties) if ($props.Count -eq 1 -and $null -ne $props[0].Value -and -not ($props[0].Value -is [string] -or $props[0].Value -is [ValueType])) { return $props[0].Value } return $result } 2 { throw "CloudStack async job $JobId failed: $($status.jobresult.errortext)" } } if ((Get-Date) -ge $deadline) { Write-Warning "CloudStack async job $JobId did not finish within $TimeoutSeconds seconds." return } Write-Verbose "Async job $JobId still pending; checking again in $PollSeconds seconds." Start-Sleep -Seconds $PollSeconds } } function Invoke-CSAsyncApiRequest { <# .SYNOPSIS Sends an asynchronous CloudStack command, optionally waiting for the result. .DESCRIPTION Without -Wait, returns the { jobid, id } job handle immediately (the same behaviour as the module's other async wrappers). With -Wait, polls the job and returns the finished result instead. #> param( [Parameter(Mandatory = $true)] [string]$Command, [hashtable]$Parameters = @{}, [switch]$Wait ) $body = ConvertFrom-CSResponse -Response (Invoke-CSApiRequest -Command $Command -Parameters $Parameters) -Command $Command if ($null -eq $body -or -not $body.jobid) { return $body } Write-Verbose "$Command started. Job ID: $($body.jobid)" if ($Wait) { return Wait-CSAsyncJob -JobId $body.jobid } return $body } function Add-CSOptionalParameter { <# .SYNOPSIS Copies bound optional parameters into a CloudStack API parameter table. .DESCRIPTION Map is PowerShellParameterName -> apiparametername. Only parameters the caller actually supplied are copied. Arrays are comma-joined, booleans and switches are sent as 'true'/'false'. #> param( [Parameter(Mandatory = $true)] [hashtable]$ApiParameters, [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$BoundParameters, [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Map ) foreach ($name in $Map.Keys) { if (-not $BoundParameters.ContainsKey($name)) { continue } $value = $BoundParameters[$name] if ($value -is [System.Management.Automation.SwitchParameter]) { $value = $value.IsPresent } if ($value -is [bool]) { $value = $value.ToString().ToLowerInvariant() } elseif ($value -is [array]) { $value = ($value -join ',') } $ApiParameters[$Map[$name]] = $value } } function ConvertTo-CSDateParameter { <# .SYNOPSIS Formats a date for a CloudStack API date parameter. .DESCRIPTION Strings are passed through unchanged, so CloudStack's own rules apply (listUsageRecords, for example, reads a bare '2026-09-30' end date as the end of that day). DateTime values are formatted with -Format; the default includes the UTC offset (e.g. 2026-09-30T18:00:00-0400) so the server does not reinterpret the time in its own time zone. #> param( [Parameter(Mandatory = $true)] [object]$Value, [string]$Format = 'Offset' ) if ($Value -is [string]) { return $Value } if ($Value -isnot [datetime]) { throw "Expected a date string or DateTime, got a $($Value.GetType().Name)." } if ($Format -ne 'Offset') { return $Value.ToString($Format, [System.Globalization.CultureInfo]::InvariantCulture) } $offset = if ($Value.Kind -eq [System.DateTimeKind]::Utc) { [TimeSpan]::Zero } else { [System.TimeZoneInfo]::Local.GetUtcOffset($Value) } $sign = if ($offset -lt [TimeSpan]::Zero) { '-' } else { '+' } $abs = $offset.Duration() return $Value.ToString("yyyy-MM-dd'T'HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture) + ('{0}{1:00}{2:00}' -f $sign, $abs.Hours, $abs.Minutes) } $script:CSResourceTypeIds = [ordered]@{ VirtualMachine = 0 PublicIp = 1 Volume = 2 Snapshot = 3 Template = 4 Project = 5 Network = 6 Vpc = 7 Cpu = 8 Memory = 9 PrimaryStorage = 10 SecondaryStorage = 11 } function ConvertTo-CSResourceTypeId { <# .SYNOPSIS Maps a friendly resource-type name to its CloudStack resourcetype number. .DESCRIPTION CloudStack's limit APIs key resources by an integer resourcetype (0 = VirtualMachine, 1 = PublicIp, 2 = Volume, 3 = Snapshot, 4 = Template, 5 = Project, 6 = Network, 7 = Vpc, 8 = Cpu, 9 = Memory, 10 = PrimaryStorage, 11 = SecondaryStorage). A name from that list is translated; a value that is already the number is returned as-is. #> param( [Parameter(Mandatory = $true)] [object]$ResourceType ) if ($script:CSResourceTypeIds.Contains([string]$ResourceType)) { return $script:CSResourceTypeIds[[string]$ResourceType] } return $ResourceType } function Add-CSMapParameter { <# .SYNOPSIS Expands a hashtable into CloudStack's indexed map parameter syntax. .DESCRIPTION CloudStack takes map parameters in two shapes: - keyed: details[0].cpuNumber=2&details[0].memory=4096 (used when -KeyField/-ValueField are omitted) - paired: nicnetworklist[0].nic=NIC1&nicnetworklist[0].network=net-uuid (one index per entry, with the hashtable key and value stored under -KeyField and -ValueField) Does nothing when -Map is null or empty. #> param( [Parameter(Mandatory = $true)] [hashtable]$ApiParameters, [Parameter(Mandatory = $true)] [string]$Name, [System.Collections.IDictionary]$Map, [string]$KeyField, [string]$ValueField ) if ($null -eq $Map -or $Map.Count -eq 0) { return } $index = 0 foreach ($key in $Map.Keys) { if ($KeyField) { $ApiParameters["$Name[$index].$KeyField"] = [string]$key $ApiParameters["$Name[$index].$ValueField"] = [string]$Map[$key] $index++ } else { $ApiParameters["$Name[0].$key"] = [string]$Map[$key] } } } |