174 lines
9.5 KiB
PowerShell
174 lines
9.5 KiB
PowerShell
#region Invoke-UnifiRequest
|
|
Function Invoke-UnifiRequest
|
|
{
|
|
<#
|
|
.SYNOPSIS
|
|
Executes a single UniFi request against the network interface, degrading to the integration API when it cannot answer.
|
|
|
|
.DESCRIPTION
|
|
Degradation is per request rather than per run, for the same reason it is in the Omada equivalent. A session that expires or a
|
|
route a particular controller release does not implement should cost only the data that request carried.
|
|
|
|
The two transports disagree about both the path and the envelope. The network interface answers with a meta block carrying a
|
|
result code alongside a data array, whereas the integration API answers with a data array alongside paging counters. Both are
|
|
unwrapped to the same shape so that the caller never has to care which answered.
|
|
|
|
Supplying only one path restricts the request to that transport, which is how capabilities unique to the network interface,
|
|
such as network definitions and wireless configuration, are expressed.
|
|
|
|
.PARAMETER Context
|
|
The connection context produced by Connect-UnifiController.
|
|
|
|
.PARAMETER NetworkPath
|
|
The path relative to the network application, for example 'api/s/default/stat/device'.
|
|
|
|
.PARAMETER IntegrationPath
|
|
The path relative to the integration API, for example 'v1/sites'.
|
|
|
|
.PARAMETER QueryString
|
|
An optional query string appended to whichever path is used, supplied without a leading delimiter.
|
|
|
|
.EXAMPLE
|
|
$Result = Invoke-UnifiRequest -Context $UnifiContext -NetworkPath "api/s/$($SiteName)/stat/device"
|
|
|
|
.NOTES
|
|
Author: Grace Solutions
|
|
Version: 2026.08.02.0000
|
|
#>
|
|
|
|
[CmdletBinding()]
|
|
Param
|
|
(
|
|
[Parameter(Mandatory=$True)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[PSObject]$Context,
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowEmptyString()]
|
|
[String]$NetworkPath = '',
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowEmptyString()]
|
|
[String]$IntegrationPath = '',
|
|
|
|
[Parameter(Mandatory=$False)]
|
|
[AllowEmptyString()]
|
|
[String]$QueryString = ''
|
|
)
|
|
|
|
Try
|
|
{
|
|
#region Build the attempt order, honoring the transport that the context selected
|
|
$AttemptList = New-Object -TypeName 'System.Collections.Generic.List[PSObject]'
|
|
|
|
[ScriptBlock]$AddAttempt = {
|
|
Param
|
|
(
|
|
[String]$Transport,
|
|
[String]$Path,
|
|
[System.Collections.Specialized.OrderedDictionary]$Header,
|
|
[Boolean]$PathWasSupplied
|
|
)
|
|
|
|
Switch (($PathWasSupplied -eq $True) -and ($Null -ine $Header))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$AttemptProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$AttemptProperties.Transport = "$($Transport)"
|
|
$AttemptProperties.Path = "$($Path)"
|
|
$AttemptProperties.Header = $Header
|
|
|
|
$AttemptList.Add((New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($AttemptProperties)))
|
|
}
|
|
}
|
|
}
|
|
|
|
$NetworkURIPath = "$($Context.NetworkPrefix)/$("$($NetworkPath)".TrimStart('/'))"
|
|
$IntegrationURIPath = "$($Context.NetworkPrefix)/integration/$("$($IntegrationPath)".TrimStart('/'))"
|
|
|
|
$NetworkWasSupplied = [String]::IsNullOrWhiteSpace($NetworkPath) -eq $False
|
|
$IntegrationWasSupplied = [String]::IsNullOrWhiteSpace($IntegrationPath) -eq $False
|
|
|
|
Switch ($Context.Mode -ieq 'Controller')
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$Null = $AddAttempt.InvokeReturnAsIs('Network', $NetworkURIPath, $Context.ControllerHeader, $NetworkWasSupplied)
|
|
$Null = $AddAttempt.InvokeReturnAsIs('Integration', $IntegrationURIPath, $Context.IntegrationHeader, $IntegrationWasSupplied)
|
|
}
|
|
|
|
{($_ -eq $False)}
|
|
{
|
|
$Null = $AddAttempt.InvokeReturnAsIs('Integration', $IntegrationURIPath, $Context.IntegrationHeader, $IntegrationWasSupplied)
|
|
$Null = $AddAttempt.InvokeReturnAsIs('Network', $NetworkURIPath, $Context.ControllerHeader, $NetworkWasSupplied)
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Execute each attempt until one produces a usable payload
|
|
$ResultProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
|
|
$ResultProperties.IsSuccess = $False
|
|
$ResultProperties.Transport = 'None'
|
|
$ResultProperties.Result = $Null
|
|
$ResultProperties.ErrorMessage = 'No transport was available for this request.'
|
|
|
|
$QueryDelimiter = Switch ([String]::IsNullOrWhiteSpace($QueryString)) {{($_ -eq $False)} {'?'} Default {''}}
|
|
|
|
:AttemptLoop For ($AttemptIndex = 0; $AttemptIndex -lt $AttemptList.Count; $AttemptIndex++)
|
|
{
|
|
$Attempt = $AttemptList[$AttemptIndex]
|
|
|
|
$Response = Invoke-RestAPIRequest -URI ([System.URI]"$($Context.BaseAddress)$($Attempt.Path)$($QueryDelimiter)$($QueryString)") -Method 'GET' -Header ($Attempt.Header) -SkipCertificateCheck:($Context.SkipCertificateCheck) -AcceptableStatusCodeList @('*') -MaximumRetryCount 1
|
|
|
|
#region Both transports carry their rows within a data array, but only one reports a result code
|
|
$ResponseIsUsable = ($Response.StatusCode -ge 200) -and ($Response.StatusCode -lt 300)
|
|
|
|
Switch (($ResponseIsUsable -eq $True) -and ([String]::IsNullOrWhiteSpace("$($Response.Content.meta.rc)") -eq $False))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ResponseIsUsable = "$($Response.Content.meta.rc)" -ieq 'ok'
|
|
}
|
|
}
|
|
|
|
Switch (($ResponseIsUsable -eq $True) -and ($Null -ieq $Response.Content.data))
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ResponseIsUsable = $False
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
Switch ($ResponseIsUsable)
|
|
{
|
|
{($_ -eq $True)}
|
|
{
|
|
$ResultProperties.IsSuccess = $True
|
|
$ResultProperties.Transport = "$($Attempt.Transport)"
|
|
$ResultProperties.Result = $Response.Content.data
|
|
$ResultProperties.ErrorMessage = $Null
|
|
|
|
Break AttemptLoop
|
|
}
|
|
|
|
{($_ -eq $False)}
|
|
{
|
|
$ResultProperties.ErrorMessage = "$($Attempt.Transport) returned status code $($Response.StatusCode). $($Response.Content.meta.msg)$($Response.Content.message)".Trim()
|
|
|
|
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - $($Attempt.Transport) could not answer `"$($Attempt.Path)`". [$($ResultProperties.ErrorMessage)]"
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ResultProperties))
|
|
}
|
|
Catch
|
|
{
|
|
Throw
|
|
}
|
|
}
|
|
#endregion
|