Functions/Public/IntegrationsOther.ps1
|
# WebRTC, Genesys, Cisco, and Cloud Agent functions Function Get-NectarCiscoCluster { <# .SYNOPSIS Return base information about all Cisco clusters .DESCRIPTION Return base information about all Cisco clusters .PARAMETER ClusterName Return the cluster matching the given name .PARAMETER TimePeriod The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK'. .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .EXAMPLE Get-NectarCiscoCluster .NOTES Version 1.0 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$ClusterName, [Parameter(Mandatory=$False)] [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK', IgnoreCase=$True)] [string]$TimePeriod = 'LAST_DAY', [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName, [Parameter(Mandatory=$False)] [ValidateRange(1,100000)] [int]$PageSize = 1000 ) Begin { Connect-NectarCloud } Process { Try { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } $FilterSession = Set-NectarFilterParams -Scope DEFAULT -Platform CISCO -TenantName $TenantName -TimePeriod $TimePeriod $URI = "https://$Global:NectarCloud/dapi/platform/clusters?pageSize=$PageSize&platform=CISCO&tenant=$TenantName" Write-Verbose $URI $Results = (Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -WebSession $FilterSession).elements If ($TenantName) {$Results | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} If ($ClusterName) { $Results = $Results | Where-Object {$_.name -like "*$ClusterName*"} } Return $Results } Catch { Write-Error "Could not get platform items. $($_.Exception.Message)" If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ } } } } Function Get-NectarCiscoClusterInventory { <# .SYNOPSIS Return inventory information for a given Cisco cluster .DESCRIPTION Return inventory information for a given Cisco cluster .PARAMETER ClusterID The Nectar-defined ClusterID for the given cluster. Accepts pipelined results .PARAMETER InventorySet What specified inventory set to pull information about .PARAMETER SearchQuery A string to search for. Will search for match against all fields .PARAMETER OrderByField Sort the output by the selected field .PARAMETER OrderDirection Sort ordered output in ascending or descending order .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .PARAMETER PageSize The size of the page used to return data. Defaults to 10000 .PARAMETER ResultSize The total number of results to return. Maximum result size is 9,999,999 results .EXAMPLE Get-NectarCiscoClusterInventory -ClusterID 39_1 -InventorySet CallManagers Returns information about the call managers for the specified clusterID .EXAMPLE Get-NectarCiscoCluster | Get-NectarCiscoClusterInventory -InventorySet Gateways Returns information about gateways from all Cisco clusters .NOTES Version 1.0 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$ClusterID, [Parameter(Mandatory=$True)] [ValidateSet('Annunciators','ApplicationServers','AssignedPresenceServers','CallManagers','ConferenceBridges','DevicePools','Gatekeepers','Gateways','GatewayEndpoints','H323Gateways','HuntLists','HuntPilots','InteractiveVoiceResponses','LicenseUsers','Locations','Mtp','MusicOnholdServers','Phones','RecordingProfiles','Services','SipTrunk','Transcoders','Users','VoiceMailPilots','VoiceMailPorts', IgnoreCase=$True)] [string]$InventorySet, [Parameter(Mandatory=$False)] [string]$SearchQuery, [Parameter(Mandatory=$False)] [string]$OrderByField = 'idx', [Parameter(Mandatory=$False)] [ValidateSet('asc', 'desc', IgnoreCase=$True)] [string]$OrderDirection = 'asc', [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName, [Parameter(Mandatory=$False)] [ValidateRange(1,999999)] [int]$PageSize = 10000, [Parameter(Mandatory=$False)] [ValidateRange(1,999999)] [int]$ResultSize, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$Name ) Begin { Connect-NectarCloud } Process { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } # The URLs for each inventory set is usually split on capital letters # This little function takes care of that # EG: MySampleTests turns into /my/sample/tests $InventoryURL = ($InventorySet -CReplace '([A-Z])', '/$1').ToLower() # Take care of any cases that don't match the above rule Switch ($InventorySet) { 'HuntLists' { $InventoryURL = '/hunts'; Break} } # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items # Otherwise, set page size (defaults to 1000) If ($ResultSize) { $PageSize = $ResultSize } $Params = @{ 'pageNumber' = 1 'pageSize' = $PageSize 'orderByField' = $OrderByField 'orderDirection' = $OrderDirection 'platform' = 'CISCO' 'tenant' = $TenantName } If ($SearchQuery) { $Params.Add('q', $SearchQuery) } $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/inventory$InventoryURL" Write-Verbose $URI $Results = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params $TotalPages = $Results.totalPages If ($TenantName) { $Results.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } If ($Name) { $Results.elements | Add-Member -Name 'ClusterName' -Value $Name -MemberType NoteProperty } $Results.elements If ($TotalPages -gt 1 -and !($ResultSize)) { $PageNum = 2 Write-Verbose "Page size: $PageSize" While ($PageNum -le $TotalPages) { Write-Verbose "Working on page $PageNum of $TotalPages" $Params.PageNumber = $PageNum $Results = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params If ($TenantName) {$Results.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} If ($Name) { $Results.elements | Add-Member -Name 'ClusterName' -Value $Name -MemberType NoteProperty } $Results.elements $PageNum++ } } } } ################################################################################################################################################# # # # Platform Functions # # # Function New-NectarGenesysConfig { <# .SYNOPSIS Enables Genesys Cloud integration with Nectar DXP .DESCRIPTION Enables Genesys Cloud integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins. .PARAMETER DisplayName The display name for the configuration. Defaults to 'Genesys Cloud tenantname'. Can only contain letters, numbers and spaces. .PARAMETER AWSEventBus The AWS Event bus URL for the application. .PARAMETER AWSRegion The AWS cloud region where the event bus resides .PARAMETER GenesysOAuthClientID The OAuth Client ID of the Genesys Cloud application. .PARAMETER GenesysOAuthClientSecret The OAuth Client secret of the Genesys Cloud application. .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .NOTES Version 1.0 #> Param ( [ValidatePattern("^[a-zA-Z0-9 ]+$")] [Parameter(ValueFromPipelineByPropertyName, HelpMessage = "Display name can only contain letters, numbers and spaces", Mandatory=$False)] [string]$DisplayName, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$AWSEventBus, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)][ValidateSet( "us-east-1", "us-east-2", "us-west-1", "us-west-2", "af-south-1", "ap-east-1", "ap-south-1", "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ca-central-1", "ca-west-1", "eu-central-1", "eu-central-2", "eu-west-1", "eu-west-2", "eu-west-3", "eu-north-1", "eu-south-1", "eu-south-2", "il-central-1", "me-central-1", "me-south-1", "sa-east-1", "us-gov-east-1", "us-gov-west-1", "cn-north-1", "cn-northwest-1" )] [string]$AWSRegion, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$GenesysOAuthClientID, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [string]$GenesysOAuthClientSecret, [Parameter(Mandatory=$False)] [string]$TenantName ) Begin { Connect-NectarCloud } Process { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } $CloudAgentList = Get-NectarCloudAgent If ($CloudAgentList.Count -gt 1) { $CloudAgentName = Select-FromList -Prompt 'Multiple cloud agents found. Pick the desired cloud agent from the below list:' -Items $CloudAgentList.cloudAgentName } Else { $CloudAgentName = $CloudAgentList[0].cloudAgentName } If (!$PSBoundParameters.ContainsKey('DisplayName')) { $DisplayName = "Genesys Cloud $TenantName" } # Build the JSON body for creating the config $Body = @{ awsApiKey = '' awsEventBus = $AWSEventBus cloudAgentName = $CloudAgentName cloudRegion = $AWSRegion.Replace('-','_') displayName = $DisplayName oauthClientId = $GenesysOAuthClientID oauthClientSecret = $GenesysOAuthClientSecret tenant = $TenantName } $URI = "https://$($Global:NectarCloud)/aapi/clouddatasources/configuration/genesys?tenant=$TenantName" Write-Verbose $URI $JSONBody = $Body | ConvertTo-Json Write-Verbose $JSONBody $JSON = Invoke-RestMethod -Method POST -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json' Return $JSON.data } } Function Test-NectarGenesysConnectivity { <# .SYNOPSIS Tests whether a tenant's Genesys Cloud OAuth client can authenticate and access the APIs Nectar DXP relies on. .DESCRIPTION Retrieves a tenant's Genesys Cloud OAuth Client ID and Secret via Get-NectarTenantDatasourceDetails, then obtains an OAuth client-credentials access token from Genesys Cloud and exercises a set of key Platform API endpoints (organization, users, groups, stations, telephony edges, telephony trunks, conversation analytics) to confirm the client has the access Nectar DXP needs to monitor the tenant's Genesys Cloud environment. .PARAMETER ID The Nectar DXP datasource ID for the tenant's Genesys Cloud integration. Accepts pipelined results from Get-NectarTenantDatasource. .PARAMETER Region The Genesys Cloud domain to authenticate against, eg. mypurecloud.com, usw2.pure.cloud, mypurecloud.ie, mypurecloud.de, mypurecloud.jp, mypurecloud.com.au. Genesys Cloud OAuth clients are region-specific. If not specified, this is derived from the datasource's cloudRegion field (returned by Get-NectarTenantDatasourceDetails). If cloudRegion can't be mapped to a known Genesys Cloud domain, this falls back to mypurecloud.com. Override explicitly if that's incorrect - the domain can be found in the browser address bar when logged into the org's Genesys Cloud (eg. apps.mypurecloud.ie). .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .PARAMETER API Instead of running the full PASS/FAIL test suite, query a single API and return up to the first 10 items it returns, so the data itself can be eyeballed. Select from Organization, Users, Groups, Stations, TelephonyEdges, TelephonyTrunks, ConversationsAnalytics. The OAuth token is still obtained first, since the API call depends on it. If omitted, the full PASS/FAIL test suite is run instead. .PARAMETER HideOutput Suppresses the PASS/FAIL console output. The results are still returned as an object. Has no effect when -API is used. .EXAMPLE Get-NectarTenantDatasource -TenantName contoso | Where-Object {$_.dataSource -eq 'genesys'} | Test-NectarGenesysConnectivity -Region usw2.pure.cloud Tests Genesys Cloud API access for the contoso tenant's configured Genesys Cloud integration. .EXAMPLE Test-NectarGenesysConnectivity -ID 42 -TenantName contoso .EXAMPLE Test-NectarGenesysConnectivity -ID 42 -TenantName contoso -API Stations Returns up to the first 10 stations Genesys Cloud reports, to verify the data is actually there. .NOTES Version 1.0 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [Alias('DatasourceID')] [int]$ID, [Parameter(Mandatory=$False)] [string]$Region = 'mypurecloud.com', [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName, [Parameter(Mandatory=$False)] [ValidateSet('Organization','Users','Groups','Stations','TelephonyEdges','TelephonyTrunks','ConversationsAnalytics')] [string]$API, [Parameter(Mandatory=$False)] [switch]$HideOutput ) Begin { Connect-NectarCloud } Process { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } $GenesysConfig = Get-NectarTenantDatasourceDetails -ID $ID -DatasourceType genesys -TenantName $TenantName Write-Verbose "Retrieved Genesys Cloud datasource details for ID $ID on tenant $($TenantName): $($GenesysConfig | ConvertTo-Json -Depth 5)" If (!$GenesysConfig.oauthClientId -Or !$GenesysConfig.oauthClientSecret) { Throw "Could not retrieve a Genesys Cloud OAuth Client ID/Secret for datasource ID $ID on tenant $TenantName." } # Maps the AWS region a Genesys Cloud org is deployed in (returned as cloudRegion, eg. 'us_east_1') to that org's login/API domain # Source: https://help.genesys.cloud/articles/aws-regions-for-genesys-cloud-deployment/ $GenesysRegionMap = @{ 'us-east-1' = 'mypurecloud.com' 'us-east-2' = 'use2.us-gov-pure.cloud' 'us-west-2' = 'usw2.pure.cloud' 'ca-central-1' = 'cac1.pure.cloud' 'sa-east-1' = 'sae1.pure.cloud' 'mx-central-1' = 'mxc1.pure.cloud' 'eu-central-1' = 'mypurecloud.de' 'eu-west-1' = 'mypurecloud.ie' 'eu-west-2' = 'euw2.pure.cloud' 'eu-central-2' = 'euc2.pure.cloud' 'eusc-de-east-1' = 'edee1.eusc-pure.cloud' 'me-central-1' = 'mec1.pure.cloud' 'ap-south-1' = 'aps1.pure.cloud' 'ap-northeast-1' = 'mypurecloud.jp' 'ap-northeast-2' = 'apne2.pure.cloud' 'ap-northeast-3' = 'apne3.pure.cloud' 'ap-southeast-1' = 'apse1.pure.cloud' 'ap-southeast-2' = 'mypurecloud.com.au' } If (!$PSBoundParameters.ContainsKey('Region')) { $CloudRegionKey = $GenesysConfig.cloudRegion -Replace '_', '-' If ($GenesysRegionMap.ContainsKey($CloudRegionKey)) { $Region = $GenesysRegionMap[$CloudRegionKey] } ElseIf ($GenesysConfig.cloudRegion) { Write-Warning "Could not map datasource cloudRegion '$($GenesysConfig.cloudRegion)' to a known Genesys Cloud domain. Falling back to $Region - pass -Region explicitly if this is incorrect." } } $APIRoot = "https://api.$Region/api/v2" # When returning actual data for a single API, pull back up to 10 items instead of just enough to prove connectivity $ItemLimit = If ($API) { 10 } Else { 1 } # Key APIs used by Nectar DXP for Genesys Cloud monitoring # ResultProperty is the property holding the list of items in that API's response, used when returning data for a single -API $Endpoints = [ordered]@{ Organization = @{ Method = 'GET'; URI = "$APIRoot/organizations/me"; ResultProperty = $Null } Users = @{ Method = 'GET'; URI = "$APIRoot/users?pageSize=$ItemLimit"; ResultProperty = 'entities' } Groups = @{ Method = 'GET'; URI = "$APIRoot/groups?pageSize=$ItemLimit"; ResultProperty = 'entities' } Stations = @{ Method = 'GET'; URI = "$APIRoot/stations?pageSize=$ItemLimit"; ResultProperty = 'entities' } TelephonyEdges = @{ Method = 'GET'; URI = "$APIRoot/telephony/providers/edges?pageSize=$ItemLimit"; ResultProperty = 'entities' } TelephonyTrunks = @{ Method = 'GET'; URI = "$APIRoot/telephony/providers/edges/trunks?pageSize=$ItemLimit"; ResultProperty = 'entities' } ConversationsAnalytics = @{ Method = 'POST' URI = "$APIRoot/analytics/conversations/details/query" ResultProperty = 'conversations' Body = @{ interval = "$((Get-Date).AddDays(-1).ToString('yyyy-MM-ddT00:00:00.000Z'))/$((Get-Date).ToString('yyyy-MM-ddT00:00:00.000Z'))" paging = @{ pageSize = $ItemLimit; pageNumber = 1 } } | ConvertTo-Json } } # Test only the requested API, if one was specified - otherwise run the full suite If ($API) { $Endpoints = [ordered]@{ $API = $Endpoints[$API] } } $AccessResults = [pscustomobject][ordered]@{ [string]'OAuthToken' = 'FAIL' } ForEach ($Key in $Endpoints.Keys) { $AccessResults | Add-Member -MemberType NoteProperty -Name $Key -Value 'FAIL' } If ($TenantName -And !$HideOutput) { Write-Host "TenantName: $TenantName - Genesys Region: $Region" } # Get an OAuth access token via the client credentials grant Try { $ClientDetailsText = "$($GenesysConfig.oauthClientId):$($GenesysConfig.oauthClientSecret)" $ClientDetails64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($ClientDetailsText)) $AuthHeaders = @{ Authorization = "Basic $ClientDetails64" 'Content-Type' = 'application/x-www-form-urlencoded' } $AuthBody = @{ grant_type = 'client_credentials' } $AuthURI = "https://login.$Region/oauth/token" Write-Verbose $AuthURI If (!$HideOutput) { Write-Host 'OAuth Token Status: ' -NoNewLine } $AuthResponse = Invoke-RestMethod -Method POST -URI $AuthURI -Headers $AuthHeaders -Body $AuthBody If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green } $AccessResults.OAuthToken = 'PASS' } Catch { If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red Get-JSONErrorStream -JSONResponse $_ } } If (!$AuthResponse.access_token) { Write-Error "Could not obtain a Genesys Cloud access token for datasource ID $ID on tenant $TenantName. Verify the OAuth client credentials are correct and that -Region ($Region) matches the org's Genesys Cloud domain." Return $AccessResults } $Headers = @{ Authorization = "Bearer $($AuthResponse.access_token)" } # -API was specified - query just that one API and return the actual data instead of a PASS/FAIL summary If ($API) { $Endpoint = $Endpoints[$API] Try { Write-Verbose $Endpoint.URI If ($Endpoint.Method -eq 'POST') { $Response = Invoke-RestMethod -Method POST -URI $Endpoint.URI -Headers $Headers -Body $Endpoint.Body -ContentType 'application/json' } Else { $Response = Invoke-RestMethod -Method GET -URI $Endpoint.URI -Headers $Headers } If ($Endpoint.ResultProperty) { Return $Response.($Endpoint.ResultProperty) | Select-Object -First 10 } Else { Return $Response } } Catch { Write-Error "Failed to query the $API API. $($_.Exception.Message)" Get-JSONErrorStream -JSONResponse $_ Return } } ForEach ($Endpoint in $Endpoints.GetEnumerator()) { Try { If (!$HideOutput) { Write-Host "$($Endpoint.Key) Status:$(' ' * [Math]::Max(1, 24 - $Endpoint.Key.Length))" -NoNewLine } Write-Verbose $Endpoint.Value.URI If ($Endpoint.Value.Method -eq 'POST') { $NULL = Invoke-RestMethod -Method POST -URI $Endpoint.Value.URI -Headers $Headers -Body $Endpoint.Value.Body -ContentType 'application/json' } Else { $NULL = Invoke-RestMethod -Method GET -URI $Endpoint.Value.URI -Headers $Headers } If (!$HideOutput) { Write-Host 'PASS' -ForegroundColor Green } $AccessResults.($Endpoint.Key) = 'PASS' } Catch { If (!$HideOutput) { Write-Host 'FAIL' -ForegroundColor Red } } } Return $AccessResults } } ################################################################################################################################################# # # # Zoom Functions # # # Function Get-NectarWebRTCConfig { <# .SYNOPSIS Return information about an existing WebRTC call data integration with Nectar DXP .DESCRIPTION Return information about an existing WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins. .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .EXAMPLE Get-NectarWebRTCConfig .NOTES Version 1.0 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName ) Begin { Connect-NectarCloud } Process { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } # Get the existing WebRTC configuration $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc?tenant=$TenantName" Write-Verbose $URI $Body = (Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader).data Return $Body } } Function Set-NectarWebRTCConfig { <# .SYNOPSIS Modify an existing WebRTC call data integration with Nectar DXP .DESCRIPTION Modify an existing WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins. .PARAMETER Platforms One or more platforms to pull WebRTC call data from .PARAMETER DisplayName The internal display name of the WebRTC configuration .PARAMETER RTCCollectionIntervalSecs How often (in seconds) to sample call quality data. Defaults to 60 .PARAMETER LoginRefreshIntervalSecs How often (in seconds) to refresh login credentials for client WebRTC connections. Defaults to 86400 (1 day). .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .EXAMPLE Set-NectarWebRTCConfig -CertID .NOTES Version 1.0 #> [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')] Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [ValidateSet('Teams','Zoom','AirBnB','CiscoWebEx', IgnoreCase=$False)] [string[]]$Platforms, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$DisplayName, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [int]$RTCCollectionIntervalSecs, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [int]$LoginRefreshIntervalSecs, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName ) Begin { Connect-NectarCloud } Process { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } # Get the existing WebRTC config details $Body = (Get-NectarWebRTCConfig)[0] $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc/$($Body.id)" Write-Verbose $URI # Remove extraneous details $Body.psobject.Properties.Remove('Id') $Body.psobject.Properties.Remove('stateId') $Body.psobject.Properties.Remove('lastUpdatedTime') $Body.psobject.Properties.Remove('clientId') $Body.psobject.Properties.Remove('kafkaTopic') $Body.psobject.Properties.Remove('sourceId') $Body | Add-Member -MemberType NoteProperty -Name 'tenant' -Value $TenantName # Update with entered parameters ForEach ($Param in $PSBoundParameters.GetEnumerator()) { $Body.($Param.key) = $Param.value } $JSONBody = $Body | ConvertTo-Json Write-Verbose $JSONBody If ($PSCmdlet.ShouldProcess(("Updating Nectar DXP WebRTC config on tenant {0}" -f $TenantName), ("Update Nectar DXP WebRTC config on tenant {0}?" -f $TenantName), 'Nectar DXP WebRTC Config Update')) { Try { $NULL = Invoke-RestMethod -Method PUT -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8' } Catch { If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ } } } } } Function New-NectarWebRTCConfig { <# .SYNOPSIS Enables WebRTC call data integration with Nectar DXP .DESCRIPTION Enables WebRTC call data integration with Nectar DXP. Requires a global admin account. Not available to tenant-level admins. .PARAMETER Platforms One or more platforms to pull WebRTC call data from .PARAMETER DisplayName The internal display name of the WebRTC configuration .PARAMETER RTCCollectionIntervalSecs How often (in seconds) to sample call quality data. Defaults to 60 .PARAMETER LoginRefreshIntervalSecs How often (in seconds) to refresh login credentials for client WebRTC connections. Defaults to 86400 (1 day). .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .EXAMPLE New-NectarWebRTCConfig .NOTES Version 1.0 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)] [ValidateSet('Teams','Zoom','AirBnB','CiscoWebEx', IgnoreCase=$False)] [string[]]$Platforms, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$DisplayName = 'WebRTC', [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [int]$RTCCollectionIntervalSecs = 60, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [int]$LoginRefreshIntervalSecs = 86400, [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName ) Begin { Connect-NectarCloud } Process { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc" Write-Verbose $URI # Check for existing Teams/Azure config # Will increment the sourceID if one already exists [int]$WebRTCSourceID = ((Get-NectarWebRTCConfig -ErrorAction:SilentlyContinue).sourceID | Measure-Object -Maximum).Maximum + 1 # Build the JSON body for creating the config $Body = @{ tenant = $TenantName platforms = $Platforms displayName = $DisplayName rtcCollectionIntervalSecs = $RTCCollectionIntervalSecs loginRefreshIntervalSecs = $LoginRefreshIntervalSecs sourceId = $WebRTCSourceID } $JSONBody = $Body | ConvertTo-Json Write-Verbose $JSONBody Try { $NULL = Invoke-RestMethod -Method POST -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8' } Catch { If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ } } } } Function Remove-NectarWebRTCConfig { <# .SYNOPSIS Removes an existing WebRTC call data integration from Nectar DXP .DESCRIPTION Removes an existing WebRTC call data integration from Nectar DXP. Requires a global admin account. Not available to tenant-level admins. .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .EXAMPLE Remove-NectarWebRTCConfig -TenantName contoso .NOTES Version 1.0 #> [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')] Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName ) Begin { Connect-NectarCloud } Process { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } # Get the existing WebRTC config details $ExistingConfig = (Get-NectarWebRTCConfig)[0] $URI = "https://$Global:NectarCloud/aapi/configuration/webrtc/$($ExistingConfig.id)" Write-Verbose $URI $Body = @{ tenant = $TenantName } $JSONBody = $Body | ConvertTo-JSON Write-Verbose $JSONBody If ($PSCmdlet.ShouldProcess(("Deleting Nectar DXP WebRTC config on tenant {0}" -f $TenantName), ("Delete Nectar DXP WebRTC config on tenant {0}?" -f $TenantName), 'Nectar DXP WebRTC Config Deletion')) { Try { $NULL = Invoke-RestMethod -Method DELETE -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONBody -ContentType 'application/json; charset=utf-8' } Catch { If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ } } } } } ################################################################################################################################################# # # # Supporting Admin Functions # # # Function Get-NectarCloudAgent { <# .SYNOPSIS Returns a list of cloud agents for a given tenant from Nectar DXP .DESCRIPTION Returns a list of cloud agents for a given tenant from Nectar DXP. Requires a global admin account. Not available to tenant-level admins. .PARAMETER TenantName The name of the Nectar DXP tenant. Used in multi-tenant configurations. .EXAMPLE Get-NectarCloudAgent -TenantName nectarcorp .NOTES Version 1.0 #> Param ( [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)] [string]$TenantName ) Begin { Connect-NectarCloud } Process { # Use globally set tenant name, if one was set and not explicitly included in the command If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { $TenantName = $Global:NectarTenantName } ElseIf ($TenantName) { If ($TenantName -NotIn $Global:NectarTenantList) { $TList = $Global:NectarTenantList -join ', ' Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)" } } # Get the cloud agents for the specified tenant $URI = "https://$($Global:NectarCloud)/aapi/cloudagents?tenant=$TenantName" Write-Verbose $URI Try { $Response = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader Return $Response } Catch { Write-Error "Failed to retrieve cloud agents. $($_.Exception.Message)" If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ } } } } ################################################################################################################################################# # # # Supporting Functions # # # # SIG # Begin signature block # MIIlcwYJKoZIhvcNAQcCoIIlZDCCJWACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA4PXEZ0HmRqoru # VpYkI5Kr+ezbGoxOAY+0QYyAbgC3gKCCEvMwggXdMIIDxaADAgECAgh7LJvTFoAy # mTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx # EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8G # A1UEAwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAe # Fw0xNjAyMTIxNzM5MzlaFw00MTAyMTIxNzM5MzlaMHwxCzAJBgNVBAYTAlVTMQ4w # DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENv # cnBvcmF0aW9uMTEwLwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlvbiBB # dXRob3JpdHkgUlNBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+Q/d # oyt9y9Aq/uxnhabnLhu6d+Hj9a+k7PpKXZHEV0drGHdrdvL9k+Q9D8IWngtmw1aU # nheDhc5W7/IW/QBi9SIJVOhlF05BueBPRpeqG8i4bmJeabFf2yoCfvxsyvNB2O3Q # 6Pw/YUjtsAMUHRAOSxngu07shmX/NvNeZwILnYZVYf16OO3+4hkAt2+hUGJ1dDyg # +sglkrRueiLH+B6h47LdkTGrKx0E/6VKBDfphaQzK/3i1lU0fBmkSmjHsqjTt8qh # k4jrwZe8jPkd2SKEJHTHBD1qqSmTzOu4W+H+XyWqNFjIwSNUnRuYEcM4nH49hmyl # D0CGfAL0XAJPKMuucZ8POsgz/hElNer8usVgPdl8GNWyqdN1eANyIso6wx/vLOUu # qfqeLLZRRv2vA9bqYGjqhRY2a4XpHsCz3cQk3IAqgUFtlD7I4MmBQQCeXr9/xQiY # ohgsQkCz+W84J0tOgPQ9gUfgiHzqHM61dVxRLhwrfxpyKOcAtdF0xtfkn60Hk7ZT # NTX8N+TD9l0WviFz3pIK+KBjaryWkmo++LxlVZve9Q2JJgT8JRqmJWnLwm3KfOJZ # X5es6+8uyLzXG1k8K8zyGciTaydjGc/86Sb4ynGbf5P+NGeETpnr/LN4CTNwumam # du0bc+sapQ3EIhMglFYKTixsTrH9z5wJuqIz7YcCAwEAAaNjMGEwHQYDVR0OBBYE # FN0ECQei9Xp9UlMSkpXuOIAlDaZZMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgw # FoAU3QQJB6L1en1SUxKSle44gCUNplkwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 # DQEBCwUAA4ICAQAgGBGUKfsmnRweHnBh8ZVyk3EkrWiTWI4yrxuzcAP8JSt0hZA9 # eGr0uYullzu1GJG7Hqf5QFuR+VWZrx4R0Fwdp2bjsZQHDDI5puobsHnYHZxwROOK # 3cT5lR+KOEM/AYWlR6c9RrK85SJo93uc2Cw+CiHILTOsv8WBmTF0wXVxxb6x8CNF # 9J1r/BljnaO8BMYYCyW7U4kPs4BQ3kXuRH+rlHhkmNP2KN2H2HBldPsOuRPrpw9h # qTKWzN677WNMGLupQPegVG4giHF1GOp6tDRy4CMnd1y2kOqGJUCr7zMPy5+CvqIg # +/a1LRrmwoWxdA/7yGUCpFIBR91JIsG/2OtrrH7e7GMzFbcjCI/GD41BWt2OxbmP # 5UU/eNu60htAsf5xTT/ggaK6XrTsFeCT3QgffuFVmQsh3pOeCvvmo0m9NjD+53ey # oHWXtS2BiBdlIPfakACfyVLMMso1fPU9D9gr1/UmbMkGNJYW6nBZGjJ5eQu2iH8P # Ukg9v2zYokQu0U63cljTiROV/kSr+NeLG26cvCygW9VqAK9fN+HV+hALmJyG5yaP # zvDsbopXC4DjTrLAoGNhkLpVaDd0araS25+hhiK2ZScO7LafQmDkZ8K12kELxNOL # YRu8+h+RK9dEB166KazZxenvU0ha64DxKFghzbAGVfsnP1OQcKkEHlcnuTCCBnIw # ggRaoAMCAQICCGQzUdPHOJ8IMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVT # MQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NM # IENvcnBvcmF0aW9uMTEwLwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlv # biBBdXRob3JpdHkgUlNBMB4XDTE2MDYyNDIwNDQzMFoXDTMxMDYyNDIwNDQzMFow # eDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9u # MREwDwYDVQQKDAhTU0wgQ29ycDE0MDIGA1UEAwwrU1NMLmNvbSBDb2RlIFNpZ25p # bmcgSW50ZXJtZWRpYXRlIENBIFJTQSBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP # ADCCAgoCggIBAJ+DE3OqsMZtIcvbi3qHdNBx3I6Xcprku4g0tN2AA8YvRaR0mr8e # D1Dqnm1485/6USapPZ3RspRXPvs5iRuRK1bvZ8vmC+MOOYzGNfSMPd0l6QGsF0J9 # WBZA3PnVKEQdlWQwYTpk8pfXc0x9eyMCbfN161U9b6otxK++dKxd/mq2/OpceekP # Q5y1UgUP7z6xsY/QSa2m40IZVD/zLw6hy3z+E/kjOdolHLg+AEo6bzIwN2Qex651 # B9hV0hjJDoq8o1zwfAqnhYHCDq+PmVzTYCW8g1ppHCUTzXL165yAm9wsZ8TdyQmY # 1XPrxCGj5TKOPi9SmMZgN2SMsm9KVHIYzCeH+s11omMhTLU9ZP0rpptVryZMYLS5 # XP6rQ72t0BNmUB8L0omm/9eABvHDEQIzM2EX91Yfji87aOcV8XdWSimeA9rCKyZh # MlugVuVJKY02p/XHUqJWAyAvOHiAvfYGrkE0y5RFvZvHiRgfC7r/qa5qQJkT3e9Q # 3wG68gTW0DHfNDheV1vIOB5W1KxIpu3/+bjBO+3CJL5EYKd3zdU9mFm0Q+qqYH3N # wuUv8ev11CDVlzRuXQRrBRHS05KMCSdE7U81MUZ+dBkFYuyJ4+ojcJjk0S/UihMY # RpNl5Vhz00w9J3oiP8P4o1W3+eaHguxFHsVuOnyxTrmraPebY9WRQbypAgMBAAGj # gfswgfgwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTdBAkHovV6fVJTEpKV # 7jiAJQ2mWTAwBggrBgEFBQcBAQQkMCIwIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3Nw # cy5zc2wuY29tMBEGA1UdIAQKMAgwBgYEVR0gADATBgNVHSUEDDAKBggrBgEFBQcD # AzA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8vY3Jscy5zc2wuY29tL3NzbC5jb20t # cnNhLVJvb3RDQS5jcmwwHQYDVR0OBBYEFFTC/hCVAJPNavXnwNfZsku4jwzjMA4G # A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA9Q8mh3CvmaLK9dbJ8I1m # PTmC04gj2IK/j1SEJ7bTgwfXnieJTYSOVNEg7mBD21dCPMewlfa+zOqjPY5PBsYr # WYZ/63MbyuVAJuA9b8z2vXHGzX0OIEA51gXSr5QIv3/CUbcrtXuDIfBj2uWc4Wku # dR1Oy2Ee9aUz3wKdFdntaZNXukZFLoC8Zb7nEj7eR/+QnBCt9laypNT61vwuvJch # s3aD0pH6BlDRsYAogP7brQ9n7fh93NlwW3q6aLWzSmYXj+fw51fdaf68XuHVjJ8T # u5WaFft5K4XVbT5nR24bB1z7VEUPFhEuEcOwvLVuHDNXlB7+QjRGjjFQTtszV5X6 # OOTmEturWC5Ft9kiyvRaR0ksKOhPjEI8ZGjp5kOsGZGpxxOCX/xxCje3nVB7PF33 # olKCNeS159MKb2v+jfmk19UdS+d9Ygj42desmUnbtYRBFC72LmCXU0ua/vGIenS6 # nnXp4NqnycwsO3tMCnjPlPc2YLaDPIpUy04NaCqUEXUmFOogN8zreRd2VXhxbeJJ # ODM32+RsWccjYua8zi5US/1eAyrI3R5LcUTQdT4xYmWLKabtJOF6HYQ0f6QXfLSs # fT81WMvDvxrdn1RWbUXlU/OIiisxo8o+UNEANOwnCMNnxlzoaL/PLhZluDxm/zuy # lauajZ3MlPDteFB/7GRHo50wggaYMIIEgKADAgECAhBHaHS/7B9x7N3XWwMIISW7 # MA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQ # MA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NT # TC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjEwHhcNMjYw # NzA5MTQzMTE4WhcNMjcxMDEwMTQzMTE4WjB/MQswCQYDVQQGEwJVUzERMA8GA1UE # CAwITmV3IFlvcmsxEDAOBgNVBAcMB0plcmljaG8xHjAcBgNVBAoMFU5lY3RhciBT # ZXJ2aWNlcyBDb3JwLjELMAkGA1UECwwCSVQxHjAcBgNVBAMMFU5lY3RhciBTZXJ2 # aWNlcyBDb3JwLjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJmOMPRo # al1bEwe/HPHB533mkxLFf2LGg8R3yZQfDMkVWS0ME52aHJT33z61eMLqFamuvqY7 # qeb6jcOjqenYUhNmFcnl1Oq2hFWWE7McdgYAc0boWHemqxHWrUXN53sCp1DFVTh3 # NFwTo2PaY/KLdIjsXXvmA9qQBytzxEmeaF5HyCmmTlQ/N4eDeqpMOAbQn1n7lLi7 # xiHq1UAll3uvB+y7Vq/3V/dCO33/aiQif1t6h2R+N2JhoW0vtvd+yMyY58klGfN6 # O9WHto+MAiGoyP8rs9XWAzhIRZ2JjwFjfTqQOxVv0lUH+ZolhJ9uQHvybL3wHwLN # Qu8szoII3tqksiosdPc4Xk1nSyzrBfETafVVpey19HD6RhSIHrnPPHwWOzrdsT9j # O9szScE3i+cfJcKiL98lJkhFq/ZGOIwAPgxPvZZQZqbzEcsR0iddA+KNLxKqzkv9 # 1GtETHdFoU07xY7+2NsY1xhs9jFFNhoeCVh+dk3lIBvpWkuMwM8mZKziUQIDAQAB # o4IBlTCCAZEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRUwv4QlQCTzWr158DX # 2bJLuI8M4zB6BggrBgEFBQcBAQRuMGwwSAYIKwYBBQUHMAKGPGh0dHA6Ly9jZXJ0 # LnNzbC5jb20vU1NMY29tLVN1YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNl # cjAgBggrBgEFBQcwAYYUaHR0cDovL29jc3BzLnNzbC5jb20wUQYDVR0gBEowSDAI # BgZngQwBBAEwPAYMKwYBBAGCqTABAwMBMCwwKgYIKwYBBQUHAgEWHmh0dHBzOi8v # d3d3LnNzbC5jb20vcmVwb3NpdG9yeTATBgNVHSUEDDAKBggrBgEFBQcDAzBNBgNV # HR8ERjBEMEKgQKA+hjxodHRwOi8vY3Jscy5zc2wuY29tL1NTTGNvbS1TdWJDQS1D # b2RlU2lnbmluZy1SU0EtNDA5Ni1SMS5jcmwwHQYDVR0OBBYEFB7iZgW8t2SQ3I8n # dKd+fgqBvDYxMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAhCFC # ydfNbwEfqJhrp612HM4Qh7JUphMm5ZLNomSEsonhjz1ltmMpB/0nGVH9Ond23hac # 12+xzfnDAB2HsNhA9vVWYbgbxcH1duQbdRatjrtWKd1+YOfsvAS+O652yRQf26OL # +2efdAnzaqsTYFoEJwFcgl1M5Uo+uSht961Jm7McHouRm/zYOnC+NjvGOgIL40KN # P1pLQTxK5zc54+yFQANjiCPNI07AnkYCSgHDJCoKZbJTcUk26kQNqNE9AvjsIlpY # uY8sTXHSIkN3WNuSPYr66GHaYj9m0AGUKGt/ZZePKmOsLu+nON5hwqXx+QX75yc2 # AIlKw/ItfZvBLeRuejoKnbOh6QiyDxn0lfAH8f/upDPCUfvBVADF56itElnCo+fC # 36rzFnfBaVPbLVzqlAGSs7e6Yn669snsTHkKQm1fALEiZyt3dqOvppr8AO1f+oCc # r/0jVezz0VyNUemChoAMBosREctn/fOYfL5NClq/mnI5m+tAHOajVk8LI6h7mlNv # tIFKwfHCIH2EJM4ze+ZrSXWt8bfY+6pQwd4xCB9UmCw2DBBMQ11rvu1rLD+u3kIL # NBAXHQGEnCoP+sY6h17wc/2Jb8BPg+yAvPXevqXmAdAKJrhvMmgsbttRxOQuVjrq # 42h1me0i3Jy0obhODmpXsrVPm02ZBIYgiRcK/q8xghHWMIIR0gIBATCBjDB4MQsw # CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAP # BgNVBAoMCFNTTCBDb3JwMTQwMgYDVQQDDCtTU0wuY29tIENvZGUgU2lnbmluZyBJ # bnRlcm1lZGlhdGUgQ0EgUlNBIFIxAhBHaHS/7B9x7N3XWwMIISW7MA0GCWCGSAFl # AwQCAQUAoHwwEAYKKwYBBAGCNwIBDDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQB # gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE # MSIEINpSlvHo9RP2ddPbsQ15LikFSTvjSNyXfB3EGbLNXXt5MA0GCSqGSIb3DQEB # CwUABIIBgCxUqdgXaTfO9t/lOJXCtKHRbea9LdU2OxmmuZM2DjZNtFYdUWu7QwT7 # XqC4d3IA3nzDh5vezLKQYtFnOAFhT+1pjtMCsFCONSTixovo0e0j+/QSR/D9b00o # FfQaYqMFGq08SWmzhVaKMKfVzgVBSA2ctVODR47OA8pJUeiWgQI/HbzllH1dVLLV # kvMFdGb4jRzoH7oMTQbgkwaN5IvghJS6lKGE1Kzsfjv6yAa30NYCRFYXJkw1Tm4j # /xV39RBVMMtPfGjegxu4Bw0vYLfvR2Fog7oTUa6JSwtkdhubsfSBmEMnoIrqGYwU # bIQxMDCOJ94KmO09Mn4/L6Sxe9Hos0FWpn4XMpgZJmQ5JwR01w1cuoKHD809QtXr # Rthqu9dG7zWzc8Vrn7zXi2JfRPCp+eV/9B3xPIMFwlltaBRZJREWzIf+iLJTNgDA # +0l8PGz9aMOsAg2pddxzKBghFM5C3DtHih0zCPkXTRfVWPEFDV6mdAKM0yHfS3Vp # HihmPsmRO6GCDxwwgg8YBgorBgEEAYI3AwMBMYIPCDCCDwQGCSqGSIb3DQEHAqCC # DvUwgg7xAgEDMQ0wCwYJYIZIAWUDBAIBMH0GCyqGSIb3DQEJEAEEoG4EbDBqAgEB # BgwrBgEEAYKpMAEDBgEwLzALBglghkgBZQMEAgEEIB8MGdTWRCExscXPTtCZkwtU # qwaH8cF+jrQB6HfUucqIAggR5stEQp6YDxgPMjAyNjA5MTUxNzI5MjZaMAMCAQEC # BgGgph3Mq6CCDAAwggT8MIIC5KADAgECAhAfaxZi0i4bbF3xwMGgYA44MA0GCSqG # SIb3DQEBCwUAMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UE # BwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20g # VGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxMB4XDTI1MDIxODE2MzIwMloX # DTM0MTExMjE4NTAwNVowbjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAw # DgYDVQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEqMCgGA1UEAwwhU1NM # LmNvbSBUaW1lc3RhbXBpbmcgVW5pdCAyMDI1IEUxMFkwEwYHKoZIzj0CAQYIKoZI # zj0DAQcDQgAEG/tRUcdv5lWW7E9eV8Tczq2DReerx2Jz47e884JGlqVQzW870D4Z # HNJVWPLKAeFisHDrZcsWHWS/t77JF39pNqOCAVowggFWMB8GA1UdIwQYMBaAFAyd # ECWOmqcbmYdDzwh+4b2BkPTPMFEGCCsGAQUFBwEBBEUwQzBBBggrBgEFBQcwAoY1 # aHR0cDovL2NlcnQuc3NsLmNvbS9TU0wuY29tLXRpbWVTdGFtcGluZy1JLVJTQS1S # MS5jZXIwUQYDVR0gBEowSDA8BgwrBgEEAYKpMAEDBgEwLDAqBggrBgEFBQcCARYe # aHR0cHM6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MAgGBmeBDAEEAjAWBgNVHSUB # Af8EDDAKBggrBgEFBQcDCDBGBgNVHR8EPzA9MDugOaA3hjVodHRwOi8vY3Jscy5z # c2wuY29tL1NTTC5jb20tdGltZVN0YW1waW5nLUktUlNBLVIxLmNybDAdBgNVHQ4E # FgQUznzZwASAxSQQagnqHKslPRH9qNIwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3 # DQEBCwUAA4ICAQCAc3Ukhb1mU2KnTsV9j2vUsnAspOXWH/L2vUGMOAcwTPtTsnuY # DLfYnEDUovKMIImo2S5F+EMcYUR9m2NM6u7sBAwNIOJQO8IJzeNrPmnL2Ma/Ah7m # emQttepeED5KoLMbvX1RKKDCEeRivu/w2JehpjRe7TenQGJlmt5mWmeCYYH37zo3 # 3gWogXHYjlnmK67t3iPtoA5kE3F9T2MUMggYO1Z9Z4KkXRDyssT/cMcOXMkqzkiX # eL9Wg6XutNT3fyhKvEzDDDoYMGUpfysYfG+SOAhv0xeRWCUlIMew0BkN4JL+KdrE # ocD4KG4Hwrg7EjFrqTV754cHKlqQBjfC43vDs+U+aE3qTkh2pmfcdkezZWOhHzjV # n3CZU8V0YN2QFntc6Zvk5lRoq5+y+0RHRVtjYOTNqoBoi23WRz4j4VTPs+JXPY9T # Ol6CR+1FHG+s/IgvTxuUlOdsxDReuoM3SsR+5Mu/heGGcrIlpeHEJR2M79xG6YzN # nflBNQwi0FbLXEanSLKgVWcDJrak+xUy4Aj6zLXPGU5L2XmJLG8onyCmek6COphN # ru7V7Jmj7gmVwaiKJHXsu2ExOsXWrra07nE6kjy3FRnqC0oa2QlXrB2P69ktzApn # Yz3capWk6jpQGUaPHWwqxVnsAhTMlmWLg0nQzYphyt82eV5uRgqkOdKpbDCCBvww # ggTkoAMCAQICEG1SGHCH6CNNhWAA0ICPk1YwDQYJKoZIhvcNAQELBQAwfDELMAkG # A1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYD # VQQKDA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0 # aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTkxMTEzMTg1MDA1WhcNMzQxMTEy # MTg1MDA1WjBzMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcM # B0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8wLQYDVQQDDCZTU0wuY29tIFRp # bWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMTCCAiIwDQYJKoZIhvcNAQEBBQAD # ggIPADCCAgoCggIBAK5REBPS+TwgoCCF3slQHGTJ4f3F6TT/Cn8xSOhyWsVeqGH9 # 8Yf3UVz7t+bQwcITsD7CY6KoGP04OskBgareubfeMKcdKwIE1YBBjKhq4urwiOqx # LUmVcvb2oM0wx3BnxQ3NBLu9ZkwMnjQlIY2mEwZMgDaqfZuiEa2BFzinXf3kRLKl # Q5oa8ne3QU0vcG4qZvphy0xxBQXayqigzN3z2HQTq6N28EOjpnA2dajGPtiZ9aNJ # eDfcDka5j3KbhBkzk4RWCjx5vP8H6DKHIIs02GHgxv/jG8JMIxWY1isG+IaB09li # vKbxlvzhNAKZK5fQmUstrpYrVo7qqXAhJtv1tUaHzrp6QpuUL9dE/bSAC7UKO9xh # yJSA1OsYWDx/wAmBA84JzX8IJ1olJjCEmlJ2F4o6dCARKA2Zhk+EU4LogpowBReT # lTW2NNwUKAW+8Cte0rhrMBZQ47Vjd92V0gEvouOTMtQJgk2QVeqGwFVw8y4HSdQN # a8sl8+Kay2MnyUXhLoQLFaeVaLs4SVXBOe3Ua1Gp5j3J2+8Yue1T4V5wrsNuocNR # 3frpSt4yRIG3N68Bz1qqhk+eNUyO8WpXWlg6POZOJUdm0BzzRsB8V7kst8nM8joO # e03KqhunBN69Ckeo8M32qo07zeveRrDwD2P4dmJLDYBflwZ1A/SQbS+HN+AHAgMB # AAGjggGBMIIBfTASBgNVHRMBAf8ECDAGAQH/AgEAMB8GA1UdIwQYMBaAFN0ECQei # 9Xp9UlMSkpXuOIAlDaZZMIGDBggrBgEFBQcBAQR3MHUwUQYIKwYBBQUHMAKGRWh0 # dHA6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5L1NTTGNvbVJvb3RDZXJ0aWZpY2F0 # aW9uQXV0aG9yaXR5UlNBLmNydDAgBggrBgEFBQcwAYYUaHR0cDovL29jc3BzLnNz # bC5jb20wPwYDVR0gBDgwNjA0BgRVHSAAMCwwKgYIKwYBBQUHAgEWHmh0dHBzOi8v # d3d3LnNzbC5jb20vcmVwb3NpdG9yeTATBgNVHSUEDDAKBggrBgEFBQcDCDA7BgNV # HR8ENDAyMDCgLqAshipodHRwOi8vY3Jscy5zc2wuY29tL3NzbC5jb20tcnNhLVJv # b3RDQS5jcmwwHQYDVR0OBBYEFAydECWOmqcbmYdDzwh+4b2BkPTPMA4GA1UdDwEB # /wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAkhl1DaZaQs8ZB9ny/JT6wJvwFelE # llovcTPdUOUTe5mTdw/E+3JtV8u6ppyLRbpIHbYlMy20KJAychU6xdaci4BsP9oV # NxSRMsEjfHKz7ARqPNdpclhYAINLjsFGMO1iUNbXiAsnF/xboNCgfeMcMYbLyQYk # U6UMobv9isrtQZ8e0EAQNV7qXJn4W0KyuTt0P8iIv/5DdDpIUBIktDZcjz2KEW6B # 1gvvsKIM1esjYwWylAazBcQAake5pANMdSn8t1HdPKsiwuWfOguyRQazAX8oXz6S # lZSIok0Lis9a02vGVtdhEaB0R3HxIyNRMMKWV1yuSeUXFuoexWav3GRPZC0WYb50 # SrW/l+wgrS8doetaMwyZon2L7ioYlIPSy1h9Dq/Q911PsSkbEZ3zrsB1roVnIfBu # 5BJp0xvQrQ/Q4LavuvCoFR7QFoypNrotbNYi2AGMZw5td4zGZtCqUTPZi0BwSuRm # +HRYAEMMThTwbJX/fYV1oC8mBN970yIvadIGKhh7+DmYdRJYBrL8inVFCZAK+YX2 # w1+qWEnCSPL/VTWJtSRMhQFfceDKbJC+pBNksvKzqkva0J1ZyMj1i4vDfSuBmbz4 # rfzsvvJxS+quZDdkmW6MeXevWGBXvqzdbAw+AqTVsAQUyP6tFeKZIL4S/fSFdl2r # Ix2X+KXkqx3S+EYxggJYMIICVAIBATCBhzBzMQswCQYDVQQGEwJVUzEOMAwGA1UE # CAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8w # LQYDVQQDDCZTU0wuY29tIFRpbWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMQIQ # H2sWYtIuG2xd8cDBoGAOODALBglghkgBZQMEAgGgggFhMBoGCSqGSIb3DQEJAzEN # BgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwOTE1MTcyOTI2WjAoBgkq # hkiG9w0BCTQxGzAZMAsGCWCGSAFlAwQCAaEKBggqhkjOPQQDAjAvBgkqhkiG9w0B # CQQxIgQgMPoGsqpzP7OlWG4tdwyJbpEcXxVST6Irwjz/2zmJsAQwgckGCyqGSIb3 # DQEJEAIvMYG5MIG2MIGzMIGwBCBUKvmhao1yLmYRSXiK6ZTBipqu5aZcs0SiVJr5 # bHnHizCBizB3pHUwczELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYD # VQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEvMC0GA1UEAwwmU1NMLmNv # bSBUaW1lc3RhbXBpbmcgSXNzdWluZyBSU0EgQ0EgUjECEB9rFmLSLhtsXfHAwaBg # DjgwCgYIKoZIzj0EAwIERzBFAiB+tcenNabMnYVnXT3q3ISgQ11JQd44n79hVCid # VpjSHAIhAOwXLgU6y6K900qUkCsfbU9W11fFhrR4x2KmUvJ+SgB4 # SIG # End signature block |