OnPremisesDataGatewayMgmt.psm1
$script:nullString = (New-Guid).Guid [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 <# .SYNOPSIS Get default environment. .DESCRIPTION This function get environment from on-premises data gateway configuration. #> Function Get-DefaultEnvironment { $script:adClientId = ($settings | ? { $_.name -eq "AzureADClientID" }).value if ($script:adClientId -eq $null) { $script:adClientId = "ea0616ba-638b-4df5-95b9-636659ae5121" } $script:adAuthority = ($settings | ? { $_.name -eq "AzureADAuthorityAddress" }).value $script:adRedirect = ($settings | ? { $_.name -eq "AzureADRedirectAddress" }).value if ($script:adRedirect -eq $null) { $script:adRedirect = "urn:ietf:wg:oauth:2.0:oob" } $script:adResource = ($settings | ? { $_.name -eq "AzureADResource" }).value $script:gsEndpoint = ($settings | ? { $_.name -eq "GlobalServiceEndpoint" }).value $script:gsBackendUriOverride = ($settings | ? { $_.name -eq "BackendUriOverride" }).value } <# .SYNOPSIS Get email properties. .DESCRIPTION This function setup environment by getting email properties from global service. .PARAMETER EmailAddress The email address. #> Function Get-EmailProperties( [Parameter(Mandatory=$true)] [string]$EmailAddress) { try { $uri = $script:gsEndpoint + "/powerbi/globalservice/v201606/environments/discover?user=" + $EmailAddress $epResponse = Invoke-WebRequest -Uri $uri -Method Post $cloudEnv = $epResponse.Content | ConvertFrom-Json $gateway = $cloudEnv.clients | ? { $_.name -eq "powerbi-gateway" } $pbiBackend = $cloudEnv.services | ? { $_.name -eq "powerbi-backend" } $aadBackend = $cloudEnv.services | ? { $_.name -eq "aad" } $script:adClientId = $gateway.appId $script:adAuthority = $aadBackend.endpoint $script:adRedirect = $gateway.redirectUri $script:adResource = $pbiBackend.resourceId $script:gsEndpoint = $pbiBackend.endpoint return $true } catch { Write-WebRequestFailure return $false } } <# .SYNOPSIS Get response from web exception. .DESCRIPTION This function return response from web exception. .PARAMETER Exception The web exception. #> Function Get-ResponseFromWebException() { If ($_.Exception.Response) { $result = $_.Exception.Response.GetResponseStream() $reader = New-Object System.IO.StreamReader($result) return $reader.ReadToEnd() } } <# .SYNOPSIS Write web request failure. .DESCRIPTION This function write web request failure to host. #> Function Write-WebRequestFailure() { If ($_.ErrorDetails.Message) { try { If (($_.ErrorDetails.Message | ConvertFrom-Json).error.code -eq "TokenExpired") { Write-Warning "Token expired, please login again." return } } catch { # Ignore json convert exception } } Write-Warning $_.Exception.Message $responseBody = Get-ResponseFromWebException If ($responseBody) { Write-Warning $responseBody } } <# .SYNOPSIS Clean up user account. .DESCRIPTION This function clean up token, backend uri and gateway regions. #> Function Remove-OnPremisesDataGatewayUserAccount() { $script:token = $null $script:backendUri = $null $script:selectedBackend = $null $script:allRegions = $null } <# .SYNOPSIS Set up user account. .DESCRIPTION This function read setting from on-premises data gateway configuration, get token from active directory and get backend uri from global service. .PARAMETER GatewayInstallDirectory The gateway install directory, default install directory will be used if this value is $null. .PARAMETER EmailAddress The email address. #> Function Set-OnPremisesDataGatewayUserAccount( [Parameter(Mandatory=$true)] [string]$EmailAddress) { $GWDir = $PSScriptRoot $configPath = $GWDir + "\Microsoft.PowerBI.DataMovement.GatewayCommon.dll.config" $config = [xml](gc $configPath) $settings = $config.configuration.applicationSettings.'Microsoft.PowerBI.DataMovement.GatewayCommon.Properties.GatewayCommonSettings'.setting $EmailDiscoveryDisable = ($settings | ? { $_.name -eq "EmailDiscoveryDisable" }).value $script:RegionSupport = ($settings | ? { $_.name -eq "RegionSupport" }).value $script:gsEndpoint = ($settings | ? { $_.name -eq "GlobalServiceEndpoint" }).value # Get environment If ($EmailDiscoveryDisable) { Get-DefaultEnvironment } else { $script:gsBackendUriOverride = $null $r = Get-EmailProperties $EmailAddress if (!$r) { Write-Warning "Get email properties from global service failed, will use default environment." Get-DefaultEnvironment } } if ($script:adResource -eq $null -or $script:gsEndpoint -eq $null) { Write-Error "Read AD resource and global service endpoint failed, make sure install gateway first or provide correct install directory!" return } # Get token Add-Type -Path $( join-path $GWDir "Microsoft.IdentityModel.Clients.ActiveDirectory.dll") Add-Type -Path $( join-path $GWDir "Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll") Add-Type -Path $( join-path $GWDir "Microsoft.PowerBI.DataMovement.GatewayRegions.dll") Add-Type -Path $( join-path $GWDir "Newtonsoft.Json.dll") $script:token = Get-Token If (!$script:token) { Write-Warning "Acquire token failed." return } # Get backend uri If ($script:gsBackendUriOverride -ne $null) { $script:backendUri = $script:gsBackendUriOverride } else { $clusterUri = $script:gsEndpoint + "/spglobalservice/GetOrInsertClusterUrisByTenantLocation" $gsResponse = Invoke-Webrequest -Uri $clusterUri -Headers @{Authorization = "Bearer " + $token.AccessToken} -Method Put $gsResponse = $gsResponse.Content | ConvertFrom-Json $script:backendUri = $gsResponse.FixedClusterUri } $script:selectedBackend = $script:backendUri Write-Host -ForegroundColor Green "Current backend is: " $script:selectedBackend } <# .SYNOPSIS Get token from AD. .DESCRIPTION Get token from AD. #> Function Get-Token(){ $context=[Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]::new($script:adAuthority); $queryParams = "prompt=select_account&msafed=0&login_hint=" + $script:emailAddress $userId = [Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier]::AnyUser return $context.AcquireToken($script:adResource, $script:adClientId, $script:adRedirect, 0, $userId, $queryParams) } <# .SYNOPSIS Get gateway regions from CDN. .DESCRIPTION Get gateway regions from CDN. #> Function Get-Regions() { $script:allRegions = [Microsoft.PowerBI.DataMovement.GatewayRegions.GatewayRegionConfiguration]::GetRegionsAllowedForTenant($script:backendUri) } <# .SYNOPSIS Get gateway regions. .DESCRIPTION Get gateway regions allowed for tenant. #> Function Get-OnPremisesDataGatewayRegions() { If (!$script:selectedBackend) { Write-Warning "Please log in first." return } If ($script:RegionSupport -ne $true) { Write-Warning "Region isn't supported." return } Get-Regions If ($script:allRegions.Count) { Write-Host -ForegroundColor Green "Available regions:" ForEach ($region in $script:allRegions) { Write-Host $region.Region } } Else { Write-Host -ForegroundColor Green "No region available." } } <# .SYNOPSIS Set gateway region. .DESCRIPTION Set gateway region, will set to default region if this value is $null. .PARAMETER Region The gateway region. #> Function Set-OnPremisesDataGatewayRegion([string]$Region) { If (!$script:selectedBackend) { Write-Warning "Please log in first." return } If ($Region) { $selectedRegion = $script:allRegions | ? { $_.Region -eq $Region } If (!$selectedRegion) { Write-Warning "Invalid region." return } $script:selectedBackend = $selectedRegion.BackendUri.ToString() } Else { $script:selectedBackend = $script:backendUri } Write-Host -ForegroundColor Green "Current backend is: " $script:selectedBackend } <# .SYNOPSIS Get gateway clusters. .DESCRIPTION This function read all gateway clusters owned by the user. #> Function Get-OnPremisesDataGatewayClusters(){ If (!$script:selectedBackend) { Write-Warning "Please log in first." return } try { $gatewayclusters = Invoke-Webrequest -Uri ($script:selectedBackend + "/unifiedgateway/gatewayclusters") -Headers @{Authorization = "Bearer " + $script:token.AccessToken} $gatewayclusters = $gatewayclusters.Content | ConvertFrom-Json ForEach ($cluster in $gatewayclusters) { $cluster.PSObject.Properties.Remove("annotation") $cluster.PSObject.Properties.Remove("publickey") $cluster.PSObject.Properties.Remove("keyword") $cluster.PSObject.Properties.Remove("metadata") $cluster.PSObject.Properties.Remove("gatewayId") $cluster.PSObject.Properties.Remove("loadBalancingSettings") ForEach ($gw in $cluster.gateways) { $gwAnnotation = $gw.gatewayAnnotation | ConvertFrom-Json $gw | Add-Member -MemberType NoteProperty -Name gatewayContactInformation -Value ([System.String]::Join(" ", $gwAnnotation.gatewayContactInformation)) $gw | Add-Member -MemberType NoteProperty -Name gatewayMachine -Value $gwAnnotation.gatewayMachine $gw.PSObject.Properties.Remove("gatewayAnnotation") $gw.PSObject.Properties.Remove("gatewayStaticCapabilities") $gw.PSObject.Properties.Remove("gatewayLoadBalancingSettings") } $cluster.gateways = $cluster.gateways | ConvertTo-Json $cluster.permission = $cluster.permission | ConvertTo-Json } return $gatewayclusters } catch { Write-WebRequestFailure } } <# .SYNOPSIS Get gateway clusters without skipping annotation and static capabilities. .DESCRIPTION This function read all gateway clusters owned by the user. #> Function Get-OnPremisesDataGatewayClustersInternal(){ If (!$script:selectedBackend) { Write-Warning "Please log in first." return } try { $gatewayclusters = Invoke-Webrequest -Uri ($script:selectedBackend + "/unifiedgateway/gatewayclusters") -Headers @{Authorization = "Bearer " + $script:token.AccessToken} $gatewayclusters = $gatewayclusters.Content | ConvertFrom-Json return $gatewayclusters } catch { Write-WebRequestFailure } } <# .SYNOPSIS Get gateway status. .DESCRIPTION This function get gateway status. .PARAMETER ClusterObjectId The cluster object Id. .PARAMETER GatewayObjectId The gateway objectId. #> Function Get-OnPremisesDataGatewayStatus( [Parameter(Mandatory=$true)] [Guid] $ClusterObjectId, [Parameter(Mandatory=$true)] [Guid] $GatewayObjectId){ If (!$script:selectedBackend) { Write-Warning "Please log in first." return } try { $gatewayStatus = Invoke-Webrequest -Uri ($script:selectedBackend + "/unifiedgateway/gatewayclusters/" + $ClusterObjectId + "/gateways/" + $GatewayObjectId + "/status") -Headers @{Authorization = "Bearer " + $script:token.AccessToken} $gatewayStatus = $gatewayStatus.Content | ConvertFrom-Json return $gatewayStatus } catch { $response = Get-ResponseFromWebException $_.Exception | ConvertFrom-Json If ($response.error.code -eq "DM_GWPipeline_Client_GatewayUnreachable") { $gwObj = New-Object -TypeName PSObject -Property ([ordered]@{gatewayStatus = "Unreachable"; gatewayVersion = "Unknown"; gatewayUpgradeState = "Unknown"}) return $gwObj } Else { Write-Warning $_.Exception.Message If ($responseBody) { Write-Warning $responseBody } } } } <# .SYNOPSIS Set gateway info. .DESCRIPTION This function set gateway info and return the updated info, note it won't update the field if it's value is null. .PARAMETER ClusterObjectId The cluster object Id. .PARAMETER GatewayObjectId The gateway objectId. .PARAMETER MemberStatus The cluster member status. .PARAMETER GatewayContactInformation The gateway contact information. .PARAMETER Name The gateway name. #> Function Set-OnPremisesDataGateway( [Parameter(Mandatory=$true)] [Guid] $ClusterObjectId, [Parameter(Mandatory=$true)] [Guid] $GatewayObjectId, [ValidateSet("None","Enabled")] [string] $MemberStatus = $script:nullString, [string] $GatewayContactInformation = $script:nullString, [string] $Name = $script:nullString) { If (!$script:selectedBackend) { Write-Warning "Please log in first." return } $request = @{} If ($MemberStatus -ne $script:nullString) { $request.Add("clusterMemberStatus", $MemberStatus)} If ($Name -ne $script:nullString) { If ($Name.Trim() -eq '') { Write-Warning 'Gateway name cannot be empty.' return } $request.Add("gatewayName",$Name) } If ($GatewayContactInformation -ne $script:nullString) { $annotation = Create-GatewayAnnotation $ClusterObjectId $GatewayObjectId $GatewayContactInformation If ($annotation) { $request.Add("gatewayAnnotation",$annotation) } Else { return } } $json = $request | ConvertTo-Json $uri = $script:selectedBackend + "/unifiedgateway/gatewayclusters/" + $ClusterObjectId + "/gateways/" + $GatewayObjectId try { $response = Invoke-Webrequest -Uri $uri -Headers @{Authorization = "Bearer " + $script:token.AccessToken} -Method Patch -Body $json -ContentType 'application/json' $response = $response.Content | ConvertFrom-Json $response.PSObject.Properties.Remove("gatewayStaticCapabilities") $response.PSObject.Properties.Remove("gatewayStatus") #Extract contact info and machine from annotation $gwAnnotation = $response.gatewayAnnotation | ConvertFrom-Json $response | Add-Member -MemberType NoteProperty -Name gatewayContactInformation -Value ([System.String]::Join(" ", $gwAnnotation.gatewayContactInformation)) $response | Add-Member -MemberType NoteProperty -Name gatewayMachine -Value $gwAnnotation.gatewayMachine $response.PSObject.Properties.Remove("gatewayAnnotation") $response.PSObject.Properties.Remove("gatewayLoadBalancingSettings") return $response } catch { Write-WebRequestFailure } } <# .SYNOPSIS Get gateway. .DESCRIPTION This function get gateway by searching cluster and gateway in all clusters. .PARAMETER ClusterObjectId The cluster object Id. .PARAMETER GatewayObjectId The gateway objectId. #> Function Get-OnPremisesDataGateway( [Parameter(Mandatory=$true)] [Guid] $ClusterObjectId, [Parameter(Mandatory=$true)] [Guid] $GatewayObjectId) { If (!$script:selectedBackend) { Write-Warning "Please log in first." return } $clusters = Get-OnPremisesDataGatewayClustersInternal $cluster = $clusters | Where-Object { $_.objectId -eq $ClusterObjectId } If (!$cluster) { Write-Host -ForegroundColor Red "Invalid cluster object Id" return } $gateway = $cluster.gateways | Where-Object { $_.gatewayObjectId -eq $GatewayObjectId } If (!$gateway) { Write-Host -ForegroundColor Red "Invalid gateway object Id" return } return $gateway } <# .SYNOPSIS Create gateway annotation. .DESCRIPTION This function get gateway annotation then fill with given contact information and machine name. .PARAMETER ClusterObjectId The cluster object Id. .PARAMETER GatewayObjectId The gateway objectId. .PARAMETER GatewayContactInformation The gateway contact information. #> Function Create-GatewayAnnotation( [Parameter(Mandatory=$true)] [Guid] $ClusterObjectId, [Parameter(Mandatory=$true)] [Guid] $GatewayObjectId, [string] $GatewayContactInformation = $script:nullString) { $gateway = Get-OnPremisesDataGateway $ClusterObjectId $GatewayObjectId If ($gateway) { $gwAnnotation = $gateway.gatewayAnnotation | ConvertFrom-Json If ($GatewayContactInformation -ne $script:nullString) { $gwAnnotation.gatewayContactInformation = @($GatewayContactInformation) } return $gwAnnotation | ConvertTo-Json } } <# .SYNOPSIS Delete gateway. .DESCRIPTION This function delete gateway, note it will return success for nonexistent gateway. .PARAMETER ClusterObjectId The cluster object Id. .PARAMETER GatewayObjectId The gateway objectId. #> Function Remove-OnPremisesDataGateway( [Parameter(Mandatory=$true)] [Guid] $ClusterObjectId, [Parameter(Mandatory=$true)] [Guid] $GatewayObjectId) { If (!$script:selectedBackend) { Write-Warning "Please log in first." return } try { $uri = $script:selectedBackend + "/unifiedgateway/gatewayclusters/" + $ClusterObjectId + "/gateways/" + $GatewayObjectId $response = Invoke-Webrequest -Uri $uri -Headers @{Authorization = "Bearer " + $script:token.AccessToken} -Method Delete $response = $response.Content | ConvertFrom-Json return $response } catch { Write-WebRequestFailure } } <# .SYNOPSIS Set gateway cluster info. .DESCRIPTION This function set gateway cluster info and return the updated info, note it won't update the field if it's value is null. .PARAMETER ClusterObjectId The cluster object Id. .PARAMETER Name The cluster name. .PARAMETER Description The cluster description. #> Function Set-OnPremisesDataGatewayCluster( [Parameter(Mandatory=$true)] [Guid] $ClusterObjectId, [string] $Name = $script:nullString, [string] $Description = $script:nullString) { If (!$script:selectedBackend) { Write-Warning "Please log in first." return } $request = @{} If ($Name -ne $script:nullString) { If ($Name.Trim() -eq '') { Write-Warning 'Cluster name cannot be empty.' return } $request.Add("name",$Name) } If ($Description -ne $script:nullString) { $request.Add("description",$Description) } $json = $request | ConvertTo-Json $uri = $script:selectedBackend + "/unifiedgateway/gatewayclusters/" + $ClusterObjectId try { $response = Invoke-Webrequest -Uri $uri -Headers @{Authorization = "Bearer " + $script:token.AccessToken} -Method Patch -Body $json -ContentType 'application/json' $response = $response.Content | ConvertFrom-Json $response.PSObject.Properties.Remove("annotation") $response.PSObject.Properties.Remove("loadBalancingSettings") return $response } catch { Write-WebRequestFailure } } <# .SYNOPSIS Get all gateway info in cluster. .DESCRIPTION This function list all gateways in cluster. .PARAMETER ClusterObjectId The cluster object Id. #> Function Get-OnPremisesDataGatewayClusterInfo( [Parameter(Mandatory = $true)][Guid] $ClusterObjectId) { $clusters = Get-OnPremisesDataGatewayClustersInternal If ($clusters -eq $null) { return } $cluster = $clusters | Where-Object { $_.objectId -eq $ClusterObjectId } $gateways = New-Object System.Collections.ArrayList If ($cluster -eq $null) { Write-Host -ForegroundColor Red "Invalid cluster object Id" return } ForEach ($gateway in $cluster.gateways) { $gwStatus = Get-OnPremisesDataGatewayStatus $cluster.objectId $gateway.gatewayObjectId $gwAnnotation = $gateway.gatewayAnnotation | ConvertFrom-Json $gwObj = New-Object PSObject -Property ( [ordered]@{ gatewayId = $gateway.gatewayId; gatewayObjectId = $gateway.gatewayObjectId; gatewayName = $gateway.gatewayName; isAnchorGateway = $gateway.isAnchorGateway; gatewayStatus = $gwStatus.gatewayStatus; gatewayVersion = $gwStatus.gatewayVersion; gatewayUpgradeState = $gwStatus.gatewayUpgradeState; gatewayClusterStatus = $gateway.gatewayClusterStatus; gatewayMachine = $gwAnnotation.gatewayMachine; }) $gateways.Add($gwObj) > $null } return $gateways } Set-Alias Login-OnPremisesDataGateway Set-OnPremisesDataGatewayUserAccount Set-Alias Logout-OnPremisesDataGateway Remove-OnPremisesDataGatewayUserAccount Export-ModuleMember -Function Get-OnPremisesDataGatewayClusters Export-ModuleMember -Function Get-OnPremisesDataGatewayStatus Export-ModuleMember -Function Set-OnPremisesDataGateway Export-ModuleMember -Function Remove-OnPremisesDataGateway Export-ModuleMember -Function Set-OnPremisesDataGatewayCluster Export-modulemember -Function Get-OnPremisesDataGatewayRegions Export-ModuleMember -Function Set-OnPremisesDataGatewayRegion Export-ModuleMember -Function Set-OnPremisesDataGatewayUserAccount Export-ModuleMember -Function Remove-OnPremisesDataGatewayUserAccount Export-ModuleMember -Function Get-OnPremisesDataGatewayClusterInfo Export-ModuleMember -Alias Login-OnPremisesDataGateway Export-ModuleMember -Alias Logout-OnPremisesDataGateway # SIG # Begin signature block # MIIdeAYJKoZIhvcNAQcCoIIdaTCCHWUCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUDxFJN5t8jppGn8gl/meYckIu # nXOgghhSMIIEwTCCA6mgAwIBAgITMwAAANjkdflFb0j3rgAAAAAA2DANBgkqhkiG # 9w0BAQUFADB3MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEw # HwYDVQQDExhNaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EwHhcNMTcxMDAyMjI1NzU3 # WhcNMTkwMTAyMjI1NzU3WjCBsTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEMMAoGA1UECxMDQU9DMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo3 # MERELTRCNUItNDU2ODElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy # dmljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMR2sf6C9y+kIdyj # 16QouyVvlvPjzdmE1FFIAb7gUl7UCknd2nlquXeGJHQdX22zmY18QH/8EjH8voTu # 57DqKJRFkqkD+PQ87+M4j2aW27QCHiHVATEHdHelT0ANUSoxETlAe4d6gd6sL/aA # wkqFSqTncLfVeAenMJ7Te3tLmLYBk59CI/Tmf2YCsU+Z0nQ0S0AH6IHAKbDLLUPZ # 1KW4d5Mmig1YMInsaoDHJvmuXyUZ6GxluZ7GX+WxF2XoxFRuMo6OWrCER3gnx/W3 # omzHOc1/C/oBI8hELBQH8uTJYqI8iGx8yqDyYETHoZNdH0oFeIOfAvVFlUYTE3JW # pbMtF0cCAwEAAaOCAQkwggEFMB0GA1UdDgQWBBSgHm6PHyXdykNng5Up+ne9UdYe # VjAfBgNVHSMEGDAWgBQjNPjZUkZwCu1A+3b7syuwwzWzDzBUBgNVHR8ETTBLMEmg # R6BFhkNodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9N # aWNyb3NvZnRUaW1lU3RhbXBQQ0EuY3JsMFgGCCsGAQUFBwEBBEwwSjBIBggrBgEF # BQcwAoY8aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNyb3Nv # ZnRUaW1lU3RhbXBQQ0EuY3J0MBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3 # DQEBBQUAA4IBAQCFDewTPdV6/bMFMK0kqgvI8Y7t1YvHrGmvpdA/Y2zx+ERd0g9d # ENtHTlfAPC1X15YlDXZNPdo7LY6wMJco/rXcjzFZ/tIvGHcIaQE52tJKW+pmXfrv # QWW4X3pQdbPTsCwcDSGPcDImnec0dathWPicWxBg1NIeSDDsdsqpESp0kSs9g9fL # QWUi9wHlFehburgOJCWpQ1jkNspUvJ7xMmtTTEIu6WPEDGHU8LxHraClsL0/BzPN # KE85uB3+5/yOurKU/V8kH/obxzB03XxI4QpbpU1D2yasOd7JVmCGEbHBRamtHVz6 # SnVVVviJUsoGV5/jdzHuXyUIy9LKSUwuTdykMIIGADCCA+igAwIBAgITMwAAAMMO # m6fYstz3LAAAAAAAwzANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWdu # aW5nIFBDQSAyMDExMB4XDTE3MDgxMTIwMjAyNFoXDTE4MDgxMTIwMjAyNFowdDEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEeMBwGA1UEAxMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC # AQEAu1fXONGxBn9JLalts2Oferq2OiFbtJiujdSkgaDFdcUs74JAKreBU3fzYwEK # vM43hANAQ1eCS87tH7b9gG3JwpFdBcfcVlkA4QzrV9798biQJ791Svx1snJYtsVI # mzNiBdGVlKW/OSKtjRJNRmLaMhnOqiJcVkixb0XJZ3ZiXTCIoy8oxR9QKtmG2xoR # JYHC9PVnLud5HfXiHHX0TszH/Oe/C4BHKf/PzWmxDAtg62fmhBubTf1tRzrH2cFh # YfKVEqENB65jIdj0mRz/eFWB7qV56CCCXwratVMZVAFXDYeRjcJ88VSGgOFi24Jz # PiZe8EAS0jnVJgMNhYgxXwoLiwIDAQABo4IBfzCCAXswHwYDVR0lBBgwFgYKKwYB # BAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0OBBYEFKcTXR8hiVXoA+6eFzbq8lSINRmv # MFEGA1UdEQRKMEikRjBEMQwwCgYDVQQLEwNBT0MxNDAyBgNVBAUTKzIzMDAxMitj # ODA0YjVlYS00OWI0LTQyMzgtODM2Mi1kODUxZmEyMjU0ZmMwHwYDVR0jBBgwFoAU # SG5k5VAF04KqFzc3IrVtqMp1ApUwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljQ29kU2lnUENBMjAxMV8yMDEx # LTA3LTA4LmNybDBhBggrBgEFBQcBAQRVMFMwUQYIKwYBBQUHMAKGRWh0dHA6Ly93 # d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljQ29kU2lnUENBMjAxMV8y # MDExLTA3LTA4LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBN # l080fvFwk5zj1RpLnBF+aybEpST030TUJLqzagiJmZrLMedwm/8UHbAHOX/kMDsT # It4OyJVnu25++HyVpJCCN5Omg9NJAsGsrVnvkbenZgAOokwl1NznXQcCyig0ZTs5 # g62VKo7KoOgIOhz+PntASZRNjlQlCuWxxwrucTfGm1429adCRPu8h7ANwDXZJodf # /2fvKHT3ijAEEYpnzEs1YGoh58ONB4Nem6udcR8pJgkR1PWC09I2Bymu6JJtkH8A # yahb7tAEZfuhDldTzPKYifOfFZPIBsRjUmECT1dIHPX7dRLKtfn0wmlfu6GdDWmD # J+uDPh1rMcPuDvHEhEOH7jGcBgAyfLcgirkII+pWsBjUsr0V7DftZNNrFQIjxooz # hzrRm7bAllksoAFThAFf8nvBerDs1NhS9l91gURZFjgnU7tQ815x3/fXUdwx1Rpj # NSqXfp9mN1/PVTPvssq8LCOqRB7u+2dItOhCww+KUViiRgJhJloZv1yU6ahAcOdb # MEx8gNRQZ6Kl7g7rPbXx5Xke4fVYGW+7iW144iBYJf/kSLPmr/GyQAQXRlDUDGyR # FH3uyuL2Jt4bOwRnUS4PpBf3Qv8/kYkx+Ke8s+U6UtwqM39KZJFl2GURtttqt7Rs # Uvy/i3EWxCzOc5qg6V0IwUVFpSmG7AExbV50xlYxCzCCBgcwggPvoAMCAQICCmEW # aDQAAAAAABwwDQYJKoZIhvcNAQEFBQAwXzETMBEGCgmSJomT8ixkARkWA2NvbTEZ # MBcGCgmSJomT8ixkARkWCW1pY3Jvc29mdDEtMCsGA1UEAxMkTWljcm9zb2Z0IFJv # b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTA3MDQwMzEyNTMwOVoXDTIxMDQw # MzEzMDMwOVowdzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO # BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEh # MB8GA1UEAxMYTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBMIIBIjANBgkqhkiG9w0B # AQEFAAOCAQ8AMIIBCgKCAQEAn6Fssd/bSJIqfGsuGeG94uPFmVEjUK3O3RhOJA/u # 0afRTK10MCAR6wfVVJUVSZQbQpKumFwwJtoAa+h7veyJBw/3DgSY8InMH8szJIed # 8vRnHCz8e+eIHernTqOhwSNTyo36Rc8J0F6v0LBCBKL5pmyTZ9co3EZTsIbQ5ShG # Lieshk9VUgzkAyz7apCQMG6H81kwnfp+1pez6CGXfvjSE/MIt1NtUrRFkJ9IAEpH # ZhEnKWaol+TTBoFKovmEpxFHFAmCn4TtVXj+AZodUAiFABAwRu233iNGu8QtVJ+v # HnhBMXfMm987g5OhYQK1HQ2x/PebsgHOIktU//kFw8IgCwIDAQABo4IBqzCCAacw # DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUIzT42VJGcArtQPt2+7MrsMM1sw8w # CwYDVR0PBAQDAgGGMBAGCSsGAQQBgjcVAQQDAgEAMIGYBgNVHSMEgZAwgY2AFA6s # gmBAVieX5SUT/CrhClOVWeSkoWOkYTBfMRMwEQYKCZImiZPyLGQBGRYDY29tMRkw # FwYKCZImiZPyLGQBGRYJbWljcm9zb2Z0MS0wKwYDVQQDEyRNaWNyb3NvZnQgUm9v # dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHmCEHmtFqFKoKWtTHNY9AcTLmUwUAYDVR0f # BEkwRzBFoEOgQYY/aHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJv # ZHVjdHMvbWljcm9zb2Z0cm9vdGNlcnQuY3JsMFQGCCsGAQUFBwEBBEgwRjBEBggr # BgEFBQcwAoY4aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNy # b3NvZnRSb290Q2VydC5jcnQwEwYDVR0lBAwwCgYIKwYBBQUHAwgwDQYJKoZIhvcN # AQEFBQADggIBABCXisNcA0Q23em0rXfbznlRTQGxLnRxW20ME6vOvnuPuC7UEqKM # bWK4VwLLTiATUJndekDiV7uvWJoc4R0Bhqy7ePKL0Ow7Ae7ivo8KBciNSOLwUxXd # T6uS5OeNatWAweaU8gYvhQPpkSokInD79vzkeJkuDfcH4nC8GE6djmsKcpW4oTmc # Zy3FUQ7qYlw/FpiLID/iBxoy+cwxSnYxPStyC8jqcD3/hQoT38IKYY7w17gX606L # f8U1K16jv+u8fQtCe9RTciHuMMq7eGVcWwEXChQO0toUmPU8uWZYsy0v5/mFhsxR # VuidcJRsrDlM1PZ5v6oYemIp76KbKTQGdxpiyT0ebR+C8AvHLLvPQ7Pl+ex9teOk # qHQ1uE7FcSMSJnYLPFKMcVpGQxS8s7OwTWfIn0L/gHkhgJ4VMGboQhJeGsieIiHQ # Q+kr6bv0SMws1NgygEwmKkgkX1rqVu+m3pmdyjpvvYEndAYR7nYhv5uCwSdUtrFq # PYmhdmG0bqETpr+qR/ASb/2KMmyy/t9RyIwjyWa9nR2HEmQCPS2vWY+45CHltbDK # Y7R4VAXUQS5QrJSwpXirs6CWdRrZkocTdSIvMqgIbqBbjCW/oO+EyiHW6x5PyZru # SeD3AWVviQt9yGnI5m7qp5fOMSn/DsVbXNhNG6HY+i+ePy5VFmvJE6P9MIIHejCC # BWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC # VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV # BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv # b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcN # MjYwNzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIIC # IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2 # WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSH # fpRgJGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQ # z7NEt13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHml # SSnnDb6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3o # iU+EGvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6 # nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6ep # ZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf # 28AVs70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCn # q47f7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx # 7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O # 9JawvEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0G # A1UdDgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMA # dQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAW # gBRyLToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8v # Y3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQy # MDExXzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZC # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQy # MDExXzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCB # gzA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9k # b2NzL3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABf # AHAAbwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEB # CwUAA4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LR # bYP+vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r # 4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb # 7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6v # mSiXmE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/ # sfQn+N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad2 # 5UAqZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUf # FL5hYbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWx # m6U/RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMj # aHXmr/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7 # qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCBJAwggSM # AgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD # VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAm # BgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAADDDpun # 2LLc9ywAAAAAAMMwCQYFKw4DAhoFAKCBpDAZBgkqhkiG9w0BCQMxDAYKKwYBBAGC # NwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQx # FgQUusvh4nJlBV5vGvh7tPmGFhPnGTcwRAYKKwYBBAGCNwIBDDE2MDSgFIASAE0A # aQBjAHIAbwBzAG8AZgB0oRyAGmh0dHBzOi8vd3d3Lm1pY3Jvc29mdC5jb20gMA0G # CSqGSIb3DQEBAQUABIIBAEgpUT/STulsh/L0OpxKy9rihLqYgOlVvVQ5b0VrthtW # Ku1hCyJntiLJDPpv7c8bM177YqinDt9fGf+cPXjoiakQTC8DgEWUD9wpzzhQ0pTd # TjihIi39fp7gL0RFMwgH6r6Gzb6cVm8s/4BBiLIn5p1L9OMo02wq3QRoDutFKx1p # nVm3/8F4Ih9V6ZatXPnlBECdSWMNniijBXV8+vv8+DRxm1XV5oZUJ41ygtJksy7X # xpMZ0OBC3l1d3Au2wJLd1gHhkLyU+Mk4bbHtJy87l/qgq9wMjuQDxHvr2jhAfXMY # 1BSmX21QY0lx9ET6TjboHw2DPsTiZJmGhA5KaNdY55ahggIoMIICJAYJKoZIhvcN # AQkGMYICFTCCAhECAQEwgY4wdzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBAhMzAAAA # 2OR1+UVvSPeuAAAAAADYMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0BCQMxCwYJKoZI # hvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xODAxMTIxOTEwNTJaMCMGCSqGSIb3DQEJ # BDEWBBStmvmPiUVznRNZuXlfOsfGWWDZdzANBgkqhkiG9w0BAQUFAASCAQCTUYd0 # 5UsnWeIJtFVIi7iX5ybiK7XDl/MLedx8DOWGPJWpIN85nKK8LuKP+3DmtyXFpIb2 # P8vADOY6j2/N9Qis/JqNFD5PICV5QZqZS8RTRiJgBoxLNOfiHzubu6Rlva59/7Z7 # 7UCcbBGDDyfYkqTOroPp3O9DKbAc4gdWtsLEfvGf3iuJNdY2uZOMQa3Ch1vaRrPm # 7QqFOIXa73vLkLvT5mMOFjur75u6A9eX5KcTXEVpVp8JCBKHkTsiU/GlCR1v+2ve # GGEx2zlN0+Y7YFTSeqa+5+qITwVPseMLc6iU8Lkf9rEN60sOKmTSeXj4x973dBb4 # G32EVvrwwTAv63Kd # SIG # End signature block |