Files
GraceSolutions 7669cb01ab Almost complete
2026-08-02 20:57:25 -04:00

222 lines
14 KiB
PowerShell

#region Connect-OmadaController
Function Connect-OmadaController
{
<#
.SYNOPSIS
Establishes a connection context for an Omada controller, preferring the controller interface and degrading to the Open API.
.DESCRIPTION
Two transports exist and they are not equivalent.
The controller interface, reached under /{omadacId}/api/v2, is what the Omada web interface itself consumes. It exposes the
complete data set including switch uplinks and downlinks, per port LLDP neighbour detail, VLAN definitions with their gateway
subnets, and the real SSID configuration. It is authenticated with a controller login and is not a published contract, so it
can change between controller releases.
The Open API, reached under /openapi/v1, is a published and versioned contract authenticated with a client identifier and
secret. It is stable but narrower. It has no uplink information at all and does not expose SSID configuration, which forces
SSIDs to be inferred from active client sessions.
This function therefore prefers the controller interface and falls back to the Open API, so that the richer topology is used
when it is available and collection still succeeds when it is not. The returned context records which mode was selected and
carries whichever credentials were successfully established, so a caller can degrade a single request rather than the whole run.
.PARAMETER BaseURL
The base URL of the Omada controller.
.PARAMETER Credential
The controller login used for the preferred transport. When omitted, the Open API is used directly.
.PARAMETER ClientID
The Open API client identifier used for the fallback transport.
.PARAMETER ClientSecret
The Open API client secret used for the fallback transport.
.PARAMETER SkipCertificateCheck
Ignores server certificate validation errors. Usually required because controllers present a self signed certificate.
.EXAMPLE
$OmadaContext = Connect-OmadaController -BaseURL $BaseURL -Credential $Credential -ClientID $ClientID -ClientSecret $ClientSecret -SkipCertificateCheck
.NOTES
Author: Grace Solutions
Version: 2026.08.02.0000
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[System.URI]$BaseURL,
[Parameter(Mandatory=$False)]
[AllowNull()]
[System.Management.Automation.PSCredential]$Credential,
[Parameter(Mandatory=$False)]
[AllowEmptyString()]
[String]$ClientID = '',
[Parameter(Mandatory=$False)]
[AllowEmptyString()]
[String]$ClientSecret = '',
[Parameter(Mandatory=$False)]
[Switch]$SkipCertificateCheck
)
Try
{
$BaseAddress = "$("$($BaseURL.AbsoluteUri)".TrimEnd('/'))"
#region The controller identifier is unauthenticated and is required by both transports
$ControllerInformationResponse = Invoke-RestAPIRequest -URI ([System.URI]"$($BaseAddress)/api/info") -Method 'GET' -SkipCertificateCheck:($SkipCertificateCheck.IsPresent)
Switch ($ControllerInformationResponse.IsSuccess)
{
{($_ -eq $False)}
{
Throw "The Omada controller information could not be retrieved from `"$($BaseAddress)`". [Status Code: $($ControllerInformationResponse.StatusCode)] [$($ControllerInformationResponse.ErrorMessage)]"
}
}
$OmadacID = "$($ControllerInformationResponse.Content.result.omadacId)"
Switch ([String]::IsNullOrWhiteSpace($OmadacID))
{
{($_ -eq $True)}
{
Throw "The Omada controller at `"$($BaseAddress)`" did not return a controller identifier."
}
}
#endregion
$ContextProperties = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ContextProperties.BaseAddress = "$($BaseAddress)"
$ContextProperties.OmadacID = "$($OmadacID)"
$ContextProperties.ControllerVersion = "$($ControllerInformationResponse.Content.result.controllerVer)"
$ContextProperties.SkipCertificateCheck = $SkipCertificateCheck.IsPresent
$ContextProperties.Mode = 'None'
$ContextProperties.ControllerHeader = $Null
$ContextProperties.OpenAPIHeader = $Null
#region Preferred transport, which is the controller interface
Switch ($Null -ine $Credential)
{
{($_ -eq $True)}
{
$LoginBody = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$LoginBody.username = "$($Credential.UserName)"
$LoginBody.password = "$($Credential.GetNetworkCredential().Password)"
$LoginResponse = Invoke-RestAPIRequest -URI ([System.URI]"$($BaseAddress)/$($OmadacID)/api/v2/login") -Method 'POST' -Body ($LoginBody) -SkipCertificateCheck:($SkipCertificateCheck.IsPresent) -AcceptableStatusCodeList @('*')
Switch (("$($LoginResponse.Content.errorCode)" -eq '0') -and ([String]::IsNullOrWhiteSpace("$($LoginResponse.Content.result.token)") -eq $False))
{
{($_ -eq $True)}
{
#region The shared client does not maintain a cookie container, so the session cookie is replayed by hand
$SessionCookieList = New-Object -TypeName 'System.Collections.Generic.List[String]'
ForEach ($SetCookieValue In "$($LoginResponse.ResponseHeader['Set-Cookie'])".Split(';,', [System.StringSplitOptions]::RemoveEmptyEntries))
{
Switch ("$($SetCookieValue)".Trim() -imatch '^(TPOMADA_SESSIONID|TPEAP_SESSIONID|JSESSIONID)=')
{
{($_ -eq $True)}
{
$SessionCookieList.Add("$($SetCookieValue)".Trim())
}
}
}
#endregion
$ControllerHeader = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$ControllerHeader.'Csrf-Token' = "$($LoginResponse.Content.result.token)"
$ControllerHeader.Accept = 'application/json'
Switch ($SessionCookieList.Count -gt 0)
{
{($_ -eq $True)}
{
$ControllerHeader.Cookie = "$($SessionCookieList -Join '; ')"
}
}
$ContextProperties.ControllerHeader = $ControllerHeader
$ContextProperties.Mode = 'Controller'
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Signed in to the Omada controller interface as `"$($Credential.UserName)`". [Controller Version: $($ContextProperties.ControllerVersion)]"
}
{($_ -eq $False)}
{
Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - The Omada controller sign in failed, so collection will fall back to the Open API. [Error Code: $($LoginResponse.Content.errorCode)] [$($LoginResponse.Content.msg)]"
}
}
}
}
#endregion
#region Fallback transport, which is the Open API
Switch (([String]::IsNullOrWhiteSpace($ClientID) -eq $False) -and ([String]::IsNullOrWhiteSpace($ClientSecret) -eq $False))
{
{($_ -eq $True)}
{
$TokenRequestBody = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$TokenRequestBody.omadacId = "$($OmadacID)"
$TokenRequestBody.client_id = "$($ClientID)"
$TokenRequestBody.client_secret = "$($ClientSecret)"
$TokenResponse = Invoke-RestAPIRequest -URI ([System.URI]"$($BaseAddress)/openapi/authorize/token?grant_type=client_credentials") -Method 'POST' -Body ($TokenRequestBody) -SkipCertificateCheck:($SkipCertificateCheck.IsPresent) -AcceptableStatusCodeList @('*')
Switch (("$($TokenResponse.Content.errorCode)" -eq '0') -and ([String]::IsNullOrWhiteSpace("$($TokenResponse.Content.result.accessToken)") -eq $False))
{
{($_ -eq $True)}
{
$OpenAPIHeader = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary'
$OpenAPIHeader.Authorization = "AccessToken=$($TokenResponse.Content.result.accessToken)"
$OpenAPIHeader.Accept = 'application/json'
$ContextProperties.OpenAPIHeader = $OpenAPIHeader
Switch ($ContextProperties.Mode -ieq 'None')
{
{($_ -eq $True)}
{
$ContextProperties.Mode = 'OpenAPI'
}
}
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Acquired an Omada Open API access token, which remains available as a fallback."
}
{($_ -eq $False)}
{
Write-Warning -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [WARNING] - An Omada Open API access token could not be acquired. [Error Code: $($TokenResponse.Content.errorCode)] [$($TokenResponse.Content.msg)]"
}
}
}
}
#endregion
Switch ($ContextProperties.Mode -ieq 'None')
{
{($_ -eq $True)}
{
Throw "No usable Omada transport could be established against `"$($BaseAddress)`". Supply either a controller login or an Open API client identifier and secret."
}
}
Write-Verbose -Message "[$([System.DateTime]::UtcNow.ToString('yyyy/MM/dd HH:mm:ss.FFF'))] - [INFO] - Omada collection will use the $(Switch ($ContextProperties.Mode) {{($_ -ieq 'Controller')} {'controller interface, with the Open API held in reserve'} Default {'Open API'}})."
Write-Output -InputObject (New-Object -TypeName 'System.Management.Automation.PSObject' -Property ($ContextProperties))
}
Catch
{
Throw
}
}
#endregion