Saas/Publish-PerTenantExtensionApps.ps1
|
<#
.Synopsis Function for publishing PTE apps to an online tenant .Description Function for publishing PTE apps to an online tenant Please consult the CI/CD Workshop document at http://aka.ms/cicdhol to learn more about this function .Parameter clientId ClientID of Azure AD App for authenticating to Business Central (SecureString or String) .Parameter clientSecret ClientSecret of Azure AD App for authenticating to Business Central (SecureString or String) .Parameter tenantId TenantId of tenant in which you want to publish the Per Tenant Extension Apps .Parameter environment Name of the environment inside the tenant in which you want to publish the Per Tenant Extension Apps .Parameter companyName Company Name in which the Azure AD App is registered .Parameter appFiles Array or comma separated string of apps or .zip files containing apps, which needs to be published The apps will be sorted by dependencies and published+installed .Parameter useNewLine Add this switch to add a newline to progress indicating periods during wait. Azure DevOps doesn't update logs until a newline is added. .Parameter hideInstalledExtensionsOutput Add this parameter to hide the output that lists installed extensions on the specified environment before and after installation of new and updated PTE extensions. .Parameter unpublishPreviousVersions Add this switch to unpublish previous versions of apps after upgrading to a new version. #> function Publish-PerTenantExtensionApps { [CmdletBinding(DefaultParameterSetName="AC")] Param( [Parameter(Mandatory=$true, ParameterSetName="CC")] $clientId, [Parameter(Mandatory=$true, ParameterSetName="CC")] $clientSecret, [Parameter(Mandatory=$true, ParameterSetName="CC")] [string] $tenantId, [Parameter(Mandatory=$true, ParameterSetName="AC")] [Hashtable] $bcAuthContext, [Parameter(Mandatory=$true)] [string] $environment, [Parameter(Mandatory=$false)] [string] $companyName, [Parameter(Mandatory=$true)] $appFiles, [ValidateSet('Add','Force')] [string] $schemaSyncMode = 'Add', [ValidateSet('','Current version','Next minor version','Next major version')] [string] $schedule = '', [switch] $useNewLine, [switch] $hideInstalledExtensionsOutput, [switch] $unpublishPreviousVersions ) $telemetryScope = InitTelemetryScope -name $MyInvocation.InvocationName -parameterValues $PSBoundParameters -includeParameters @() try { function GetAuthHeaders { $script:authContext = Renew-BcAuthContext -bcAuthContext $script:authContext return @{ "Authorization" = "Bearer $($script:authContext.AccessToken)" } } $newLine = @{} if (!$useNewLine) { $newLine = @{ "NoNewLine" = $true } } if ($PsCmdlet.ParameterSetName -eq "CC") { if ($clientId -is [SecureString]) { $clientID = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($clientID)) } if ($clientId -isnot [String]) { throw "ClientID needs to be a SecureString or a String" } if ($clientSecret -is [String]) { $clientSecret = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force } if ($clientSecret -isnot [SecureString]) { throw "ClientSecret needs to be a SecureString or a String" } $script:authContext = New-BcAuthContext ` -clientID $clientID ` -clientSecret $clientSecret ` -tenantID $tenantId ` -scopes "https://api.businesscentral.dynamics.com/.default" if (-not ($script:AuthContext)) { throw "Authentication failed" } } else { $script:authContext = Renew-BcAuthContext -bcAuthContext $bcAuthContext } $appFolder = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString()) try { $appFiles = CopyAppFilesToFolder -appFiles $appFiles -folder $appFolder $automationApiUrl = "$($bcContainerHelperConfig.apiBaseUrl.TrimEnd('/'))/v2.0/$environment/api/microsoft/automation/v2.0" Write-Host "$automationApiUrl/companies" $companies = Invoke-RestMethod -Headers (GetAuthHeaders) -Method Get -Uri "$automationApiUrl/companies" -UseBasicParsing $company = $companies.value | Where-Object { ($companyName -eq "") -or ($_.name -eq $companyName) } | Select-Object -First 1 if (!($company)) { throw "No company $companyName" } $companyId = $company.id if ($companyName -eq "") { $companyName = $company.name } Write-Host "Company '$companyName' has id $companyId" Write-Host "$automationApiUrl/companies($companyId)/extensions" $getExtensions = Invoke-WebRequest -Headers (GetAuthHeaders) -Method Get -Uri "$automationApiUrl/companies($companyId)/extensions" -UseBasicParsing $extensions = (ConvertFrom-Json $getExtensions.Content).value | Sort-Object -Property DisplayName if(!$hideInstalledExtensionsOutput) { Write-Host "Extensions before:" $extensions | ForEach-Object { Write-Host " - $($_.DisplayName), Version $($_.versionMajor).$($_.versionMinor).$($_.versionBuild).$($_.versionRevision), Installed=$($_.isInstalled)" } Write-Host } $body = @{"schedule" = "Current Version"} $appDep = $extensions | Where-Object { $_.DisplayName -eq 'Application' } $appDepVer = [System.Version]"$($appDep.versionMajor).$($appDep.versionMinor).$($appDep.versionBuild).$($appDep.versionRevision)" if ($appDepVer -ge [System.Version]"21.2.0.0") { if ($schemaSyncMode -eq 'Force') { $body."SchemaSyncMode" = "Force Sync" } else { $body."SchemaSyncMode" = "Add" } } else { if ($schemaSyncMode -eq 'Force') { throw 'SchemaSyncMode Force is not supported before version 21.2' } } if($schedule) { $body."schedule" = $schedule } $ifMatchHeader = @{ "If-Match" = '*'} $jsonHeader = @{ "Content-Type" = 'application/json'} $streamHeader = @{ "Content-Type" = 'application/octet-stream'} try { Sort-AppFilesByDependencies -appFiles $appFiles -excludeRuntimePackages | ForEach-Object { $appFile = $_ Write-Host @newline "$([System.IO.Path]::GetFileName($appFile)) - " $appJson = Get-AppJsonFromAppFile -appFile $appFile $previousApp = $null $existingApp = $extensions | Where-Object { $_.id -eq $appJson.id -and $_.isInstalled } if ($existingApp) { if ($existingApp.isInstalled) { $existingVersion = [System.Version]"$($existingApp.versionMajor).$($existingApp.versionMinor).$($existingApp.versionBuild).$($existingApp.versionRevision)" if ($existingVersion -ge $appJson.version) { Write-Host "already installed" } else { Write-Host @newLine "upgrading" $previousApp = $existingApp $existingApp = $null } } else { Write-Host @newLine "installing" $existingApp = $null } } else { Write-Host @newLine "publishing and installing" } if (!$existingApp) { $extensionUpload = (Invoke-RestMethod -Method Get -Uri "$automationApiUrl/companies($companyId)/extensionUpload" -Headers (GetAuthHeaders)).value Write-Host @newLine "." if ($extensionUpload -and $extensionUpload.systemId) { $extensionUpload = Invoke-RestMethod ` -Method Patch ` -Uri "$automationApiUrl/companies($companyId)/extensionUpload($($extensionUpload.systemId))" ` -Headers ((GetAuthHeaders) + $ifMatchHeader + $jsonHeader) ` -Body ($body | ConvertTo-Json -Compress) } else { $ExtensionUpload = Invoke-RestMethod ` -Method Post ` -Uri "$automationApiUrl/companies($companyId)/extensionUpload" ` -Headers ((GetAuthHeaders) + $jsonHeader) ` -Body ($body | ConvertTo-Json -Compress) } Write-Host @newLine "." if ($null -eq $extensionUpload.systemId) { throw "Unable to upload extension" } # Use stream instead of reading the entire file into memory $fileStream = [System.IO.File]::OpenRead($appFile) Invoke-RestMethod ` -Method Patch ` -Uri $extensionUpload.'extensionContent@odata.mediaEditLink' ` -Headers ((GetAuthHeaders) + $ifMatchHeader + $streamHeader) ` -Body $fileStream | Out-Null $fileStream.Close() Write-Host @newLine "." Invoke-RestMethod ` -Method Post ` -Uri "$automationApiUrl/companies($companyId)/extensionUpload($($extensionUpload.systemId))/Microsoft.NAV.upload" ` -Headers ((GetAuthHeaders) + $ifMatchHeader) ` -ErrorAction SilentlyContinue | Out-Null Write-Host @newLine "." $completed = $false $errCount = 0 $sleepSeconds = 30 $lastStatus = '' while (!$completed) { Start-Sleep -Seconds $sleepSeconds try { $extensionDeploymentStatusResponse = Invoke-WebRequest -Headers (GetAuthHeaders) -Method Get -Uri "$automationApiUrl/companies($companyId)/extensionDeploymentStatus" -UseBasicParsing $extensionDeploymentStatuses = (ConvertFrom-Json $extensionDeploymentStatusResponse.Content).value $thisExtension = $extensionDeploymentStatuses | Where-Object { $_.publisher -eq $appJson.publisher -and $_.name -eq $appJson.name -and $_.appVersion -eq $appJson.version } if ($null -eq $thisExtension) { throw "Unable to find extension deployment status" } $thisExtension | ForEach-Object { if ($_.status -ne $lastStatus) { if (!$useNewLine) { Write-Host } Write-Host @newLine $_.status $lastStatus = $_.status } if ($_.status -eq "InProgress") { $errCount = 0 $sleepSeconds = 5 Write-Host @newLine "." } elseif ($_.Status -eq "Unknown") { throw "Unknown Error" } elseif ($_.Status -eq "Completed") { if (!$useNewLine) { Write-Host } $completed = $true } else { $errCount = 5 throw $_.status } } } catch { if (!$useNewLine) { Write-Host } if ($errCount++ -gt 4) { Write-Host $_.Exception.Message throw "Unable to publish app. Please open the Extension Deployment Status Details page in Business Central to see the detailed error message." } $sleepSeconds += $sleepSeconds Write-Host "Error: $($_.Exception.Message). Retrying in $sleepSeconds seconds" } } if ($unpublishPreviousVersions -and $previousApp -and ($appDepVer -ge [System.Version]"25.4.0.0")) { # New unpublish API available from 25.4 Write-Host @newLine "Unpublishing previous version" Invoke-RestMethod ` -Method Post ` -Uri "$automationApiUrl/companies($companyId)/extensions($($previousApp.packageId))/Microsoft.NAV.unpublish" ` -Headers (GetAuthHeaders) | Out-Null } } } } catch [System.Net.WebException],[System.Net.Http.HttpRequestException] { if (!$useNewLine) { Write-Host } Write-Host "ERROR $($_.Exception.Message)" Write-Host $_.ScriptStackTrace throw (GetExtendedErrorMessage $_) } catch { if (!$useNewLine) { Write-Host } Write-Host "ERROR: $($_.Exception.Message) [$($_.Exception.GetType().FullName)]" throw } finally { $getExtensions = Invoke-WebRequest -Headers (GetAuthHeaders) -Method Get -Uri "$automationApiUrl/companies($companyId)/extensions" -UseBasicParsing $extensions = (ConvertFrom-Json $getExtensions.Content).value | Sort-Object -Property DisplayName if (!$hideInstalledExtensionsOutput) { Write-Host Write-Host "Extensions after:" $extensions | ForEach-Object { Write-Host " - $($_.DisplayName), Version $($_.versionMajor).$($_.versionMinor).$($_.versionBuild).$($_.versionRevision), Installed=$($_.isInstalled)" } } } } catch [System.Net.WebException],[System.Net.Http.HttpRequestException] { Write-Host "ERROR $($_.Exception.Message)" throw (GetExtendedErrorMessage $_) } finally { if (Test-Path $appFolder) { Remove-Item $appFolder -Recurse -Force -ErrorAction SilentlyContinue } } } catch { TrackException -telemetryScope $telemetryScope -errorRecord $_ throw } finally { $script:authContext = $null TrackTrace -telemetryScope $telemetryScope } } Export-ModuleMember -Function Publish-PerTenantExtensionApps # SIG # Begin signature block # MIIoLQYJKoZIhvcNAQcCoIIoHjCCKBoCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBWCWINe1jT9ccY # kzwzQ4QQ9o29ZgLDb1zQSw9D7Xym/KCCDXYwggX0MIID3KADAgECAhMzAAAEhV6Z # 7A5ZL83XAAAAAASFMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjUwNjE5MTgyMTM3WhcNMjYwNjE3MTgyMTM3WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDASkh1cpvuUqfbqxele7LCSHEamVNBfFE4uY1FkGsAdUF/vnjpE1dnAD9vMOqy # 5ZO49ILhP4jiP/P2Pn9ao+5TDtKmcQ+pZdzbG7t43yRXJC3nXvTGQroodPi9USQi # 9rI+0gwuXRKBII7L+k3kMkKLmFrsWUjzgXVCLYa6ZH7BCALAcJWZTwWPoiT4HpqQ # hJcYLB7pfetAVCeBEVZD8itKQ6QA5/LQR+9X6dlSj4Vxta4JnpxvgSrkjXCz+tlJ # 67ABZ551lw23RWU1uyfgCfEFhBfiyPR2WSjskPl9ap6qrf8fNQ1sGYun2p4JdXxe # UAKf1hVa/3TQXjvPTiRXCnJPAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUuCZyGiCuLYE0aU7j5TFqY05kko0w # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwNTM1OTAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBACjmqAp2Ci4sTHZci+qk # tEAKsFk5HNVGKyWR2rFGXsd7cggZ04H5U4SV0fAL6fOE9dLvt4I7HBHLhpGdE5Uj # Ly4NxLTG2bDAkeAVmxmd2uKWVGKym1aarDxXfv3GCN4mRX+Pn4c+py3S/6Kkt5eS # DAIIsrzKw3Kh2SW1hCwXX/k1v4b+NH1Fjl+i/xPJspXCFuZB4aC5FLT5fgbRKqns # WeAdn8DsrYQhT3QXLt6Nv3/dMzv7G/Cdpbdcoul8FYl+t3dmXM+SIClC3l2ae0wO # lNrQ42yQEycuPU5OoqLT85jsZ7+4CaScfFINlO7l7Y7r/xauqHbSPQ1r3oIC+e71 # 5s2G3ClZa3y99aYx2lnXYe1srcrIx8NAXTViiypXVn9ZGmEkfNcfDiqGQwkml5z9 # nm3pWiBZ69adaBBbAFEjyJG4y0a76bel/4sDCVvaZzLM3TFbxVO9BQrjZRtbJZbk # C3XArpLqZSfx53SuYdddxPX8pvcqFuEu8wcUeD05t9xNbJ4TtdAECJlEi0vvBxlm # M5tzFXy2qZeqPMXHSQYqPgZ9jvScZ6NwznFD0+33kbzyhOSz/WuGbAu4cHZG8gKn # lQVT4uA2Diex9DMs2WHiokNknYlLoUeWXW1QrJLpqO82TLyKTbBM/oZHAdIc0kzo # STro9b3+vjn2809D0+SOOCVZMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg # Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 # a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr # rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg # OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy # 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 # sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh # dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k # A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB # w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn # Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 # lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w # ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o # ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa # BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG # AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV # HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG # AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl # AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb # C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l # hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 # I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 # wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 # STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam # ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa # J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah # XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA # 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt # Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr # /Xmfwb1tbWrJUnMTDXpQzTGCGg0wghoJAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAASFXpnsDlkvzdcAAAAABIUwDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIKGLS97DxXlqnllIizEG8KwV # Vjw22qCB947a33fWbGngMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAdQsqNAoWvlxUvjT+AmY7vftAN7fTNUiBFrMrCMNyBILnr21MTmIpnmod # +GR1iYtK1AtlcKKsE9mHSeJAef0W9HPRVyaXJxkOIUNvvKU+MCoO4e80n4+hietq # ziC/kIAElisxfnR4zjNM8XKB/EstLWAeBrXPPON12YqaSbILCC1y5Y/pttBz7e+C # +M3leL4LJwOtHdHFJ2tHVWpuINincdY5h5Ul59kp55QxQXcoLbClmmoJNYT4MvEr # F65jSJ0hkj6GjSeS/rbDFCmI+cDy/GtiUzyHsEHnfvSVPnFAg8R2YFStQVeGQY/m # aFetBBeGQPhmUqgeYeJq9mwTC7Lx36GCF5cwgheTBgorBgEEAYI3AwMBMYIXgzCC # F38GCSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq # hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCAMy9eMk323Edx2M2dyno406RcfZ4OSNWY5BCI5DcSkRAIGaWjgagpq # GBMyMDI2MDExOTEwMTkwMi4wNTZaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l # cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0w # NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg # ghHtMIIHIDCCBQigAwIBAgITMwAAAgcsETmJzYX7xQABAAACBzANBgkqhkiG9w0B # AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yNTAxMzAxOTQy # NTJaFw0yNjA0MjIxOTQyNTJaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z # MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0wNUUwLUQ5NDcxJTAjBgNV # BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQDFP/96dPmcfgODe3/nuFveuBst/JmSxSkOn89ZFytH # Qm344iLoPqkVws+CiUejQabKf+/c7KU1nqwAmmtiPnG8zm4Sl9+RJZaQ4Dx3qtA9 # mdQdS7Chf6YUbP4Z++8laNbTQigJoXCmzlV34vmC4zpFrET4KAATjXSPK0sQuFhK # r7ltNaMFGclXSnIhcnScj9QUDVLQpAsJtsKHyHN7cN74aEXLpFGc1I+WYFRxaTgq # SPqGRfEfuQ2yGrAbWjJYOXueeTA1MVKhW8zzSEpfjKeK/t2XuKykpCUaKn5s8sqN # bI3bHt/rE/pNzwWnAKz+POBRbJxIkmL+n/EMVir5u8uyWPl1t88MK551AGVh+2H4 # ziR14YDxzyCG924gaonKjicYnWUBOtXrnPK6AS/LN6Y+8Kxh26a6vKbFbzaqWXAj # zEiQ8EY9K9pYI/KCygixjDwHfUgVSWCyT8Kw7mGByUZmRPPxXONluMe/P8CtBJMp # uh8CBWyjvFfFmOSNRK8ETkUmlTUAR1CIOaeBqLGwscShFfyvDQrbChmhXib4nRMX # 5U9Yr9d7VcYHn6eZJsgyzh5QKlIbCQC/YvhFK42ceCBDMbc+Ot5R6T/Mwce5jVyV # CmqXVxWOaQc4rA2nV7onMOZC6UvCG8LGFSZBnj1loDDLWo/I+RuRok2j/Q4zcMnw # kQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFHK1UmLCvXrQCvR98JBq18/4zo0eMB8G # A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG # Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy # MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w # XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy # dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG # A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD # AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQDju0quPbnix0slEjD7j2224pYOPGTmdDvO # 0+bNRCNkZqUv07P04nf1If3Y/iJEmUaU7w12Fm582ImpD/Kw2ClXrNKLPTBO6nfx # vOPGtalpAl4wqoGgZxvpxb2yEunG4yZQ6EQOpg1dE9uOXoze3gD4Hjtcc75kca8y # ivowEI+rhXuVUWB7vog4TGUxKdnDvpk5GSGXnOhPDhdId+g6hRyXdZiwgEa+q9M9 # Xctz4TGhDgOKFsYxFhXNJZo9KRuGq6evhtyNduYrkzjDtWS6gW8akR59UhuLGsVq # +4AgqEY8WlXjQGM2OTkyBnlQLpB8qD7x9jRpY2Cq0OWWlK0wfH/1zefrWN5+be87 # Sw2TPcIudIJn39bbDG7awKMVYDHfsPJ8ZvxgWkZuf6ZZAkph0eYGh3IV845taLkd # LOCvw49Wxqha5Dmi2Ojh8Gja5v9kyY3KTFyX3T4C2scxfgp/6xRd+DGOhNVPvVPa # /3yRUqY5s5UYpy8DnbppV7nQO2se3HvCSbrb+yPyeob1kUfMYa9fE2bEsoMbOaHR # gGji8ZPt/Jd2bPfdQoBHcUOqPwjHBUIcSc7xdJZYjRb4m81qxjma3DLjuOFljMZT # YovRiGvEML9xZj2pHRUyv+s5v7VGwcM6rjNYM4qzZQM6A2RGYJGU780GQG0QO98w # +sucuTVrfTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI # hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy # MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC # VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV # BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp # bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC # AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg # M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF # dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6 # GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp # Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu # yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E # XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0 # lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q # GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ # +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA # PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw # EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG # NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV # MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj # cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK # BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC # AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX # zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v # cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI # KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG # 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x # M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC # VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449 # xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM # nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS # PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d # Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn # GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs # QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL # jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL # 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNQ # MIICOAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp # bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw # b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn # MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjg2MDMtMDVFMC1EOTQ3MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQDT # vVU/Yj9lUSyeDCaiJ2Da5hUiS6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7Rf7aTAiGA8yMDI2MDExOTAwMzgw # MVoYDzIwMjYwMTIwMDAzODAxWjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDtF/tp # AgEAMAoCAQACAg8PAgH/MAcCAQACAhNSMAoCBQDtGUzpAgEAMDYGCisGAQQBhFkK # BAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJ # KoZIhvcNAQELBQADggEBAMpfTfdoI9DxYGqJE6aA0e29Gyo9xTkxWIA7l/6F1fZ+ # ekgcHclQsohqdEq2b+NSbphTpGZCx5QsGzwnBCSvT1cRKxGaOVPhLZpWSuza9nKI # jVEvsDF3fOt6YuKaaZzW1Sh8xAGxFLQ5Wd9KEjptpsBr3mx0r/Ptd+OtbtQ6N4hz # puuPvo6PM3kY3r57uaMlZ4Zx6HJMJeoJz7QY9jr8SVw0BB2QiM1OajrwDRZ15zMg # glrGx8Tug9zHIVh0NGGeLFq1xOWD624Rnal8M9ftoiIo+sh593oQkRSPYlhORlUb # zuNOJwASrweAY9wZWOF/VqWhSuqCSKxj0ZROqWUx1x0xggQNMIIECQIBATCBkzB8 # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N # aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAgcsETmJzYX7xQABAAAC # BzANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEE # MC8GCSqGSIb3DQEJBDEiBCCMjyrbxnj1je+g9LbNeRmJNNfdSNz0w+SXDT5dY6xj # vzCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIC/31NHQds1IZ5sPnv59p+v6 # BjBDgoDPIwiAmn0PHqezMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB # IDIwMTACEzMAAAIHLBE5ic2F+8UAAQAAAgcwIgQgJQBIGTovxDNXWrIMkpITrOHf # JWT8mX4Woe/GlTW4TOYwDQYJKoZIhvcNAQELBQAEggIAPR6M4aaoZG2t9s9jK0Pt # noNoqmBw2atUuUTMS6udrVh4y1i0SfHZQr66cmf6AVuxK/GITsKrK0PLx8L8NTc1 # fTglLEEBSONuK+ADYQyF2UVxA+/8D3BotpDO0arrwb2pRpk9avb8o3ooqNZEa2sq # gcVuWzYXIOI+VdHlDKPC7+ktZ8Hux5Ybkve0Y8juNv/pPgP8krPO+7vsmQThlls1 # 95p1Mx3K1+LXD6X08cI7qVlhjUxVw2Vsf99aaaM9K71McDNQgdR2+pmjSpVUmP6f # 1RTMNMNZbdfDpmwRCO3M7ozYH4okjX3NxrMlK01SSCWhEJe6BGBYdTUrcbgRbtr1 # plbML213ZdcVLSDDKhBGQARQ7JlKcH/vb8EumVtPMYZJS8cyMv8SWJK84XLVfjrk # sfN4zpUvFGLolPmcYOlK5WcPORlFu9zQ6wYYHLyriuSyLbiL9l8p2sHg1Zrwox/P # qUaKByZY8KxilaXN/6DlEFB266XVaAfXLCyEMBxUV8dNEZYDP/jUyeSnu0lfrFVL # 0XAxA7feeyusaz6yDJn1o6pxZ9Ke/jcb4fIAI/2T3LMHpqN8mH3Ib8d12zoRZGUp # y+duDHIxQK+GIQojA+EcaV+aRjsKgoR6MHzHoK09cjG1riJr8MWCeyccJFWqdds0 # jWeyXssTZwmL+AwT0qTCoZA= # SIG # End signature block |