Obs/scripts/ExtensionHelper.psm1
##------------------------------------------------------------------ ## <copyright file="ExtensionHelper.psm1" company="Microsoft"> ## Copyright (C) Microsoft. All rights reserved. ## </copyright> ##------------------------------------------------------------------ #region Imports Import-Module PackageManagement -Global -DisableNameChecking -Verbose:$false #endregion Imports #region Constants $global:extensionRootLocation = Split-Path -Parent $PSScriptRoot $global:packageBinPath = Join-Path -Path $global:extensionRootLocation -ChildPath "bin" ## Value will be updated during Set-HandlerLogFile function execution. $global:LogFile = $null ## Value will be updated by Set-ObsNugetStorePath function execution. $global:ObsNugetStorePath = $null $WrapperConstants = @{ Exception = @{ ## We throw the error using Name property of the error constant and than catch it in the caller function, where using the name property (same as Key) we retrieve the error message and error code. UnhandledException = @{ Code = 1 Name = "UnhandledException" Message = "An unhandled exception occurred." } HandlerEnvJsonDoesNotExist = @{ Code = 2 Name = "HandlerEnvJsonDoesNotExist" Message = "HandlerEnvironment.json file doesn't exist, cannot proceed." } LogFolderDoesNotExist = @{ Code = 3 Name = "LogFolderDoesNotExist" Message = "Log folder doesn't exist, cannot proceed." } StatusFolderDoesNotExist = @{ Code = 4 Name = "StatusFolderDoesNotExist" Message = "Status folder doesn't exist, cannot proceed." } ConfigFolderDoesNotExist = @{ Code = 5 Name = "ConfigFolderDoesNotExists" Message = "Config folder doesn't exist, cannot proceed." } PackageNotInstalled = @{ Code = 6 Name = "PackageNotInstalled" Message = "Package ({0}) is not installed at {1}." } GetPackageCommandNotFound = @{ Code = 7 Name = "GetPackageCommandNotFound" Message = "Get-Package command not found, cannot proceed." } HeartBeatFileValueDoesNotExist = @{ Code = 8 Name = "HeartBeatFileValueDoesNotExist" Message = "Heartbeat file value does not exist in the HandlerEnvironment.json file, cannot proceed." } NupkgVersionNotFound = @{ Code = 9 Name = "NupkgVersionNotFound" Message = "Unable to find package version for nupkg ({0}) in path ({1})." } NupkgFileNotFound = @{ Code = 10 Name = "NupkgFileNotFound" Message = "Nupkg file not found for {0} in path: {1}" } NugetStorePathDirectoryNotFound = @{ Code = 11 Name = "NugetStorePathDirectoryNotFound" Message = "NugetStorePath directory ({0}) was supposed to exist, cannot proceed." } SetupScriptPathNotFound = @{ Code = 12 Name = "SetupScriptPathNotFound" Message = "Setup script path ({0}) not found, cannot proceed." } } RequiredObsPackageNames = @{ ObsExtSetupScripts = "Microsoft.AzureStack.Observability.ObsExtSetupScripts" GMA = "Microsoft.AzureStack.Observability.GenevaMonitoringAgent" TestObservability = "Microsoft.AzureStack.Observability.TestObservability" ObsDeployment = "Microsoft.AzureStack.Observability.ObservabilityDeployment" FDA = "Microsoft.AzureStack.Observability.FDA.FleetDiagnosticsAgent" MAWatchDog = "Microsoft.AzureStack.Solution.Diagnostics.HCIWatchdog" SBCClient = "Microsoft.AzureStack.Services.SupportBridgeController.Client" ObsAgent = "Microsoft.AzureStack.SupportBridge.LogCollector.WinService" UtcExporter = "Microsoft.Windows.Utc.Exporters.GenevaExporter" NetObs = "Microsoft.AS.Network.Observability.Extension" } NugetDetails = @{ ProviderName = "Nuget" } ## One liner constants HandlerLogFileName = "ObservabilityExtension.log" HandlerEnvFileName = "HandlerEnvironment.json" } #endregion Constants #region Functions #region Handler Functions function Get-ConfigSequenceNumber { [CmdletBinding()] Param() if ($null -eq $env:ConfigSequenceNumber) { 0 } else { $env:ConfigSequenceNumber } } function Get-HandlerEnvInfo { [CmdletBinding()] Param ( [Parameter(Mandatory=$False)] [System.String] $LogFile ) <# Sample HandlerEnvironment.json content: [ { "handlerEnvironment": { "configFolder": "C:\\Packages\\Plugins\\Microsoft.AzureStack.Observability.Observability\\0.0.0.4\\RuntimeSettings", "deploymentid": "", "heartbeatFile": "C:\\Packages\\Plugins\\Microsoft.AzureStack.Observability.Observability\\0.0.0.4\\status\\HeartBeat.Json", "hostResolverAddress": "", "instance": "", "logFolder": "C:\\ProgramData\\GuestConfig\\extension_logs\\Microsoft.AzureStack.Observability.Observability", "rolename": "", "statusFolder": "C:\\Packages\\Plugins\\Microsoft.AzureStack.Observability.Observability\\0.0.0.4\\status" }, "name": "Microsoft.RecoveryServices.Test.AzureSiteRecovery", "version": "1" } ] #> $envFile = Join-path -Path $global:extensionRootLocation -ChildPath $WrapperConstants.HandlerEnvFileName if (-not (Test-Path $envFile -PathType Leaf)) { throw $WrapperConstants.Exception.HandlerEnvJsonDoesNotExist.Name } ## Read handler config $envJson = Get-Content -Path $envFile -Raw | ConvertFrom-Json if ($envJson -is [System.Array]) { $envJson = $envJson[0] } return $envJson.handlerEnvironment } function Get-HandlerHeartBeatFile { [CmdletBinding()] Param ( [Parameter(Mandatory=$False)] [System.String] $LogFile ) $handlerEnvInfo = Get-HandlerEnvInfo if ($null -eq $handlerEnvInfo.heartbeatFile) { throw $WrapperConstants.Exception.HeartBeatFileValueDoesNotExist.Name } return $handlerEnvInfo.heartbeatFile } function Get-HandlerConfigSettings { [CmdletBinding()] Param ( [Parameter(Mandatory=$False)] [System.String] $LogFile ) <# Sample config json file: ------------------------------------------------------------ { "runtimeSettings": [ { "handlerSettings": { "publicSettings": { "region": "eastus", "cloudName": "AzureCanary" } } } ] } ----------------------------------------------------------- Or If you don't want to pass any values, it can be empty as follows: { "runtimeSettings": [ { "handlerSettings":{ "publicSettings":{} } } ] } #> $functionName = $MyInvocation.MyCommand.Name $handlerEnvInfo = Get-HandlerEnvInfo if (-not (Test-Path $handlerEnvInfo.configFolder -PathType Container)) { Write-Log "[$functionName] $($WrapperConstants.Exception.ConfigFolderDoesNotExist.Message)" ` -Level "ERROR" throw $WrapperConstants.Exception.ConfigFolderDoesNotExist.Name } $configFile = Get-ChildItem -Path $handlerEnvInfo.configFolder | Sort-Object CreationTime -Descending | Select-Object -First 1 ## Parse config file to read parameters $configJson = Get-Content -Path $configFile.FullName -Raw | ConvertFrom-Json return $configJson.runtimeSettings[0].handlerSettings.publicSettings } function Get-LogFolderPath { [CmdletBinding()] Param ( [Parameter(Mandatory=$False)] [System.String] $LogFile ) $handlerEnvInfo = Get-HandlerEnvInfo if (-not (Test-Path $handlerEnvInfo.logFolder -PathType Container)) { throw $WrapperConstants.Exception.LogFolderDoesNotExist.Name } return $handlerEnvInfo.logFolder } function Get-StatusFolderPath { [CmdletBinding()] Param ( [Parameter(Mandatory=$False)] [System.String] $LogFile ) $handlerEnvInfo = Get-HandlerEnvInfo if (-not (Test-Path $handlerEnvInfo.statusFolder -PathType Container)) { throw $WrapperConstants.Exception.StatusFolderDoesNotExist.Name } return $handlerEnvInfo.statusFolder } function Get-StatusFilePath { [CmdletBinding()] Param() $configSeqNum = Get-ConfigSequenceNumber $statusFolder = Get-StatusFolderPath return "$statusFolder\$configSeqNum.status" } function Set-HandlerLogFile { [CmdletBinding()] Param() $functionName = $MyInvocation.MyCommand.Name if ($null -eq $global:LogFile) { $global:LogFile = Join-Path $(Get-LogFolderPath) -ChildPath $WrapperConstants.HandlerLogFileName Write-Log "[$functionName] Setting the global:LogFile with the log file path of $($global:LogFile)." } } function Get-HandlerLogFile { [CmdletBinding()] Param() Set-HandlerLogFile return $global:LogFile } function Set-Status { [CmdletBinding()] Param ( [Parameter(Mandatory=$True)] [System.String] $Name, [Parameter(Mandatory=$True)] [System.String] $Operation, [Parameter(Mandatory=$True)] [System.String] $Message, [Parameter(Mandatory=$True)] [System.String] $Status, [Parameter(Mandatory=$True)] [System.Int16] $Code ) & "$PSScriptRoot\ReportStatus.ps1" ` -Name $Name ` -Operation $Operation ` -Message $Message ` -Status $Status ` -Code $Code } #endregion Handler Functions #region Observability Functions function Set-NugetPackageProvider { [CmdletBinding()] Param() $functionName = $MyInvocation.MyCommand.Name $providerName = $WrapperConstants.NugetDetails.ProviderName $nugetProvider = Get-PackageProvider | Where-Object { $_.Name -eq $providerName } if ($null -eq $nugetProvider) { Write-Log "[$functionName] Attempting to install $providerName package provider." Install-PackageProvider $providerName -Force -ForceBootstrap } else { Write-Log "[$functionName] Package provider $providerName already installed." } } function Set-AclsForGivenPath { [CmdletBinding()] Param( [Parameter(Mandatory=$True)] [System.String] $PathToSetAcls ) $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)" $requiredACLs = @{ "BUILTIN\Administrators" = "FullControl" "Everyone" = "ReadAndExecute" # "NT AUTHORITY\SYSTEM" = "FullControl" } ## Query existing Acls $aclObj = Get-Acl $PathToSetAcls # Check if required ACLs are missing $missingACLs = $requiredACLs.Keys | Where-Object { $_ -notin $aclObj.Access.IdentityReference } if ($missingACLs.Count -gt 0) { Write-Log "[$functionName] Re-configuring ACLs as some are missing. Missing ACLs = $($missingACLs -join ',')" ## Disable inheritance for PathToSetAcls and remove inherited acls ## https://learn.microsoft.com/en-us/dotnet/api/system.security.accesscontrol.objectsecurity.setaccessruleprotection?view=net-8.0 $aclObj.SetAccessRuleProtection($True, $False) ## Remove all existing access rules $aclObj.Access | ForEach-Object { $aclObj.RemoveAccessRule($_) } | Out-Null ## Re-configure acls foreach ($acl in $requiredACLs.GetEnumerator()) { $account = $acl.Name $access = $acl.Value Write-Log "[$functionName] Give '$access' access to '$account'." # 3,0 allows child items of Path to inhertit the acls $newAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($account, $access, 3, 0, "Allow") $aclObj.SetAccessRule($newAccessRule) } ## Finally commit the acls Set-Acl -Path $PathToSetAcls -AclObject $aclObj Write-Log "[$functionName] ACLs committed for '$PathToSetAcls'." } else { Write-Log "[$functionName] All required ACLs are already present." } Write-Log "[$functionName] Exiting." } function Set-ObsStoreRootFolderPath { [CmdletBinding()] Param ( [Parameter(Mandatory=$False)] [System.Management.Automation.SwitchParameter] $SetAcls ) $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)" $obsRootFolderRegKeyPath = "HKLM:\SOFTWARE\Microsoft\AzureStack\Observability" $obsRootFolderRegKeyName = "ObsRootFolderPath" $obsRootFolderPath = $null ## Check if ObsStorePath is present in registry if (Get-ItemProperty -Path $obsRootFolderRegKeyPath -Name $obsRootFolderRegKeyName -ErrorAction Ignore) { $pathFromRegKey = Get-ItemPropertyValue -Path $obsRootFolderRegKeyPath -Name $obsRootFolderRegKeyName -ErrorAction Ignore Write-Log "[$functionName] ObsRootFolderPath key found in registry - $pathFromRegKey" ## If key is present then check if the directory exists, if not, create a new directory and overwrite the new path in registry. if (Test-Path -Path $pathFromRegKey -PathType Container -ErrorAction Ignore) { $obsRootFolderPath = $pathFromRegKey } else { Write-Log "[$functionName] Directory not found for path - $pathFromRegKey." } } if (-not $obsRootFolderPath) { Write-Log "[$functionName] Either ObsRootFolderPath key not found in registry or the path doesn't exists. Create new path and store in registry - $obsRootFolderRegKeyPath." ## Create the registry key path if it does not exist. if (-not (Test-Path -Path $obsRootFolderRegKeyPath -ErrorAction Ignore)) { Write-Log "[$functionName] Registry key path not found. Creating registry key path - $obsRootFolderRegKeyPath" $out = New-Item -Path $obsRootFolderRegKeyPath -Force Write-Log "[$functionName] Registry key path created - $out" } ## Generate a unique ObsStorePath using last 4 characters of GUID. $guidStr = [System.Guid]::NewGuid().ToString() $last_4_chars = $guidStr.Substring($guidStr.Length - 4) $obsRootFolderPath = "$($env:SystemDrive)\Obs_$last_4_chars" ## Create the directory for the path. $out = New-Item -Path $obsRootFolderPath -ItemType Directory -Force Write-Log "[$functionName] Created directory for path - $out" ## Store the path in registry. $out = Set-ItemProperty -Path $obsRootFolderRegKeyPath -Name $obsRootFolderRegKeyName -Value $obsRootFolderPath Write-Log "[$functionName] Created registry key ($obsRootFolderRegKeyPath\$obsRootFolderRegKeyName) and stored ObsRootFolderPath value ($obsRootFolderPath) - $out." } if ($SetAcls) { ## Set ACLs for the Obs Store Path if it doesn't have the required permissions. Set-AclsForGivenPath -PathToSetAcls $obsRootFolderPath } Write-Log "[$functionName] Exiting. Returning { ObsRootFolderPath = $obsRootFolderPath }" return $obsRootFolderPath } function Get-ExtVersion { [CmdletBinding()] Param() $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering." ## Figure out the extension version from env variable. $ExtVersion = [System.Environment]::GetEnvironmentVariable("AZURE_GUEST_AGENT_EXTENSION_VERSION") if (-not $ExtVersion) { ## As a fallback, get the extension version from the extension folder path. Write-Log "[$functionName] AZURE_GUEST_AGENT_EXTENSION_VERSION environment variable not found. Setting it from extension folder path." $ExtVersion = Split-Path -Leaf $global:extensionRootLocation Write-Log "[$functionName] Extension version from extension folder path = $ExtVersion." } Write-Log "[$functionName] Exiting. Returning { ExtVersion = $ExtVersion }" return $ExtVersion } function Set-ObsNugetStorePath { [CmdletBinding()] Param ( [Parameter(Mandatory=$True)] [System.String] $ObsStoreRootPath, [Parameter(Mandatory=$False)] [System.String] $ExtVersion, [Parameter(Mandatory=$False)] [System.String] $WorkloadName, [Parameter(Mandatory=$False)] [System.Management.Automation.SwitchParameter] $CreatePathIfNotExists ) $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)" if (($null -ne $ExtVersion -and $ExtVersion -in $global:ObsNugetStorePath) -or ($null -ne $global:ObsNugetStorePath)) { Write-Log "[$functionName] ObsNugetStorePath already set. Exiting." return } $partialNugetStorePath = Join-Path -Path $ObsStoreRootPath -Child "Nugets" ## If ExtVersion is not passed, then figure out the extension version that is getting used. if (-not $ExtVersion) { $ExtVersion = Get-ExtVersion } ## Set the ObsNugetStorePath with the ext version specific nuget store. $global:ObsNugetStorePath = Join-Path -Path $partialNugetStorePath -ChildPath $ExtVersion Write-Log "[$functionName] Setting ObsNugetStorePath = $($global:ObsNugetStorePath)." ## Create the obsNugetStorePath directory if it does not exist. if ($CreatePathIfNotExists) { if (-not (Test-Path -Path $global:ObsNugetStorePath -PathType Container)) { $out = New-Item -Path $global:ObsNugetStorePath -ItemType Directory -Force Write-Log "[$functionName] Created directory for path - $out" } else { Write-Log "[$functionName] Directory already exists for path - $($global:ObsNugetStorePath)." } } else { ## Directory must exist for the path. Write-Log "[$functionName] Let's confirm whether directory for path exists or not. Path = $($global:ObsNugetStorePath)." if (Test-Path -Path $global:ObsNugetStorePath -PathType Container) { Write-Log "[$functionName] Confirmed directory exists - $($global:ObsNugetStorePath)." } elseif ($WorkloadName -eq "Update") { ## For Update script, if nuget store based path is not found, then we can fallback to the extension folder path. So for this case, we don't throw an error if NugetStorePath directory is not found. Write-Log "[$functionName] Expected directory for workload ($WorkloadName) not found for path - $($global:ObsNugetStorePath)." } else { ## Error message is updated with the path where the directory was supposed to be present. $WrapperConstants.Exception.NugetStorePathDirectoryNotFound.Message = $WrapperConstants.Exception.NugetStorePathDirectoryNotFound.Message -f $global:ObsNugetStorePath Write-Log "[$functionName] $($WrapperConstants.Exception.NugetStorePathDirectoryNotFound.Message)." throw $WrapperConstants.Exception.NugetStorePathDirectoryNotFound.Name } } Write-Log "[$functionName] Exiting." } function Confirm-PackageExists { [CmdletBinding()] Param ( [Parameter(Mandatory=$True)] [System.String] $PackageName, [Parameter(Mandatory=$True)] [System.String] $SourcePath, [Parameter(Mandatory=$False)] [System.String] $ProviderName = $WrapperConstants.NugetDetails.ProviderName, [Parameter(Mandatory=$False)] [System.Management.Automation.SwitchParameter] $ThrowIfNotExists ) $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)" $packageExists = Find-Package -Name $PackageName -ProviderName $ProviderName -Source $SourcePath -ErrorAction Ignore if (-not $packageExists) { if ($ThrowIfNotExists) { ## Error message is updated with the package name and path where the nupkg file supposed to be present. $WrapperConstants.Exception.NupkgFileNotFound.Message = $WrapperConstants.Exception.NupkgFileNotFound.Message -f $PackageName, $SourcePath Write-Log "[$functionName] $($WrapperConstants.Exception.NupkgFileNotFound.Message)" -Level "ERROR" throw $WrapperConstants.Exception.NupkgFileNotFound.Name } else { Write-Log "[$functionName] Nupkg file for ($PackageName) not found in source ($SourcePath)." return $false } } Write-Log "[$functionName] Exiting. Nupkg file with version $($packageExists.PackageFilename) found at path $($packageExists.Source)." return $true } function Extract-Package { [CmdletBinding()] Param ( [Parameter(Mandatory=$True)] [System.String] $PackageName, [Parameter(Mandatory=$True)] [System.String] $SourcePath, [Parameter(Mandatory=$True)] [System.String] $DestinationPath, [Parameter(Mandatory=$False)] [System.String] $ProviderName = $WrapperConstants.NugetDetails.ProviderName ) $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)" $output = Install-Package ` -Name $PackageName ` -Source $SourcePath ` -Destination $DestinationPath ` -ProviderName $ProviderName ` -Force Write-Log "[$functionName] Exiting. Successfully installed package. Result = $($output | Select-Object Name, Status, Source, FullPath)." } function Install-ObsPackages { [CmdletBinding()] Param() $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering." Set-NugetPackageProvider ## Check if ZDP package path exists or not. $obsZDPPackagePath = "$env:SystemDrive\ObsZDP" $zdpPathExists = Test-Path $obsZDPPackagePath -PathType Container -ErrorAction Ignore ## Install the nuget packages foreach($packageName in $WrapperConstants.RequiredObsPackageNames.Values) { Write-Log "[$functionName] Installing package: $packageName from source: $global:packageBinPath." if (Confirm-PackageExists -PackageName $packageName -SourcePath $global:packageBinPath -ThrowIfNotExists) { Extract-Package ` -PackageName $packageName ` -SourcePath $global:packageBinPath ` -DestinationPath $global:ObsNugetStorePath } if ($zdpPathExists) { Write-Log "[$functionName] Installing package: $packageName from source: $obsZDPPackagePath." ## Extract the ZDPd nugets to ObsNugetStorePath. ## Note: SourcePath value is different for ZDPd nugets. if (Confirm-PackageExists -PackageName $packageName -SourcePath $obsZDPPackagePath) { Extract-Package ` -PackageName $packageName ` -SourcePath $obsZDPPackagePath ` -DestinationPath $global:ObsNugetStorePath } } } Write-Log "[$functionName] Exiting. Successfully installed observability nuget packages." } function Uninstall-ObsPackages { [CmdletBinding()] Param() $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering." ## Navigate to the parent directory of ObsNugetStorePath $nugetStorePathParent = Split-Path -Parent $global:ObsNugetStorePath ## Get all ext versions specific nuget store paths. Sort them in descending order. ## For e.g. List all the version folders in the ObsNugetStorePathParent directory (i.e. C:\Obs_XXXX\Nugets\), like 1.0.6.0, 1.0.6.1, 1.0.7.0, etc. $allVersionsOfNugetStorePaths = Get-ChildItem -Path $nugetStorePathParent -Directory -Name | ForEach-Object { [System.Version] $_ } | Sort-Object -Descending Write-Log "[$functionName] AllVersionsOfNugetStorePaths = `n$($allVersionsOfNugetStorePaths | Out-String) ." ## Keep the latest two versions of the nuget store paths. $latestTwoVersions = $allVersionsOfNugetStorePaths | Select-Object -First 2 Write-Log "[$functionName] LatestTwoVersions = `n$($latestTwoVersions | Out-String) ." ## Delete N-2 and older versions of the nuget store paths. foreach ($version in $allVersionsOfNugetStorePaths) { if ($version -notin $latestTwoVersions) { $pathToDelete = Join-Path -Path $nugetStorePathParent -ChildPath $version.ToString() Write-Log "[$functionName] Deleting directory: $pathToDelete" try { ## Only when ErrorAction is set to SilentlyContinue, the error is saved in the ErrorVariable. For ErrorAction Ignore, the error is not saved in the ErrorVariable. Remove-Item -Path $pathToDelete -Recurse -Force ` -ErrorAction SilentlyContinue -ErrorVariable removeItemError if ($removeItemError) { ## Just log the error and continue with the next directory deletion. Write-Log "[$functionName] Error occured while deleting directory ($pathToDelete). ErrorDetails: $removeItemError" } else { Write-Log "[$functionName] Successfully deleted directory: $pathToDelete" } } catch { $exceptionDetails = Get-ExceptionDetails -ErrorObject $_ Write-Log "[$functionName] Failed to delete directory: $pathToDelete. Error: $exceptionDetails" -Level "ERROR" } } else { Write-Log "[$functionName] Skipping version ($version) as it is one of the latest two versions." } } Write-Log "[$functionName] Exiting. Successfully uninstalled observability nuget packages." } function Get-SetupScriptPath { [CmdletBinding()] Param() $functionName = $MyInvocation.MyCommand.Name if (-not (Get-Command Get-Package -ErrorAction Ignore)) { Write-Log "[$functionName] $($WrapperConstants.Exception.GetPackageCommandNotFound.Message)" -Level "ERROR" throw $WrapperConstants.Exception.GetPackageCommandNotFound.Name } $setupScriptsPackageObj = Get-Package ` -Name $WrapperConstants.RequiredObsPackageNames.ObsExtSetupScripts ` -Destination $global:ObsNugetStorePath ` -ProviderName $WrapperConstants.NugetDetails.ProviderName Write-Log "[$functionName] SetupScriptsPackageObj = $($setupScriptsPackageObj)" $setupScriptsPackageContentPath = Join-Path -Path ([System.IO.Path]::GetDirectoryName($setupScriptsPackageObj.Source)) -ChildPath "content" Write-Log "[$functionName] SetupScriptsPackageContentPath = $($setupScriptsPackageContentPath)" $scriptPath = Join-Path -Path $setupScriptsPackageContentPath -ChildPath "Setup-Extension.ps1" if (-not (Test-Path $scriptPath)) { $WrapperConstants.Exception.SetupScriptPathNotFound.Message = $WrapperConstants.Exception.SetupScriptPathNotFound.Message -f $scriptPath Write-Log "[$functionName] $($WrapperConstants.Exception.SetupScriptPathNotFound.Message)" -Level "ERROR" throw $WrapperConstants.Exception.SetupScriptPathNotFound.Name } Write-Log "[$functionName] Returning. Successfully found setup scripts path at = $scriptPath" return $scriptPath } function Get-FileLockProcess { [CmdletBinding()] Param( [Parameter(Mandatory=$True)] [System.String] $FilePath, [Parameter(Mandatory=$False)] [System.String] $LogFile ) $functionName = $MyInvocation.MyCommand.Name ##### BEGIN Variable/Parameter Transforms and PreRun Prep ##### if (! $(Test-Path $FilePath)) { Write-Log "[$functionName] The path $FilePath was not found! Halting!" -Level "WARNING" return } ##### END Variable/Parameter Transforms and PreRun Prep ##### ##### BEGIN Main Body ##### if ($PSVersionTable.PSEdition -eq "Desktop" -or $PSVersionTable.Platform -eq "Win32NT" -or $($PSVersionTable.PSVersion.Major -le 5 -and $PSVersionTable.PSVersion.Major -ge 3)) { $CurrentlyLoadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies() $AssembliesFullInfo = $CurrentlyLoadedAssemblies | Where-Object { $_.GetName().Name -eq "Microsoft.CSharp" -or $_.GetName().Name -eq "mscorlib" -or $_.GetName().Name -eq "System" -or $_.GetName().Name -eq "System.Collections" -or $_.GetName().Name -eq "System.Core" -or $_.GetName().Name -eq "System.IO" -or $_.GetName().Name -eq "System.Linq" -or $_.GetName().Name -eq "System.Runtime" -or $_.GetName().Name -eq "System.Runtime.Extensions" -or $_.GetName().Name -eq "System.Runtime.InteropServices" } $AssembliesFullInfo = $AssembliesFullInfo | Where-Object {$_.IsDynamic -eq $False} $ReferencedAssemblies = $AssembliesFullInfo.FullName | Sort-Object | Get-Unique $usingStatementsAsString = @" using Microsoft.CSharp; using System.Collections.Generic; using System.Collections; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Runtime; using System; using System.Diagnostics; "@ $TypeDefinition = @" $usingStatementsAsString namespace MyCore.Utils { static public class FileLockUtil { [StructLayout(LayoutKind.Sequential)] struct RM_UNIQUE_PROCESS { public int dwProcessId; public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime; } const int RmRebootReasonNone = 0; const int CCH_RM_MAX_APP_NAME = 255; const int CCH_RM_MAX_SVC_NAME = 63; enum RM_APP_TYPE { RmUnknownApp = 0, RmMainWindow = 1, RmOtherWindow = 2, RmService = 3, RmExplorer = 4, RmConsole = 5, RmCritical = 1000 } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct RM_PROCESS_INFO { public RM_UNIQUE_PROCESS Process; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)] public string strAppName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)] public string strServiceShortName; public RM_APP_TYPE ApplicationType; public uint AppStatus; public uint TSSessionId; [MarshalAs(UnmanagedType.Bool)] public bool bRestartable; } [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] static extern int RmRegisterResources(uint pSessionHandle, UInt32 nFiles, string[] rgsFilenames, UInt32 nApplications, [In] RM_UNIQUE_PROCESS[] rgApplications, UInt32 nServices, string[] rgsServiceNames); [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)] static extern int RmStartSession(out uint pSessionHandle, int dwSessionFlags, string strSessionKey); [DllImport("rstrtmgr.dll")] static extern int RmEndSession(uint pSessionHandle); [DllImport("rstrtmgr.dll")] static extern int RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[] rgAffectedApps, ref uint lpdwRebootReasons); /// <summary> /// Find out what process(es) have a lock on the specified file. /// </summary> /// <param name="path">Path of the file.</param> /// <returns>Processes locking the file</returns> /// <remarks>See also: /// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373661(v=vs.85).aspx /// http://wyupdate.googlecode.com/svn-history/r401/trunk/frmFilesInUse.cs (no copyright in code at time of viewing) /// /// </remarks> static public List<Int32> WhoIsLocking(string path) { // Console.WriteLine("Looking for process handles for file {0}.", path); uint handle; string key = Guid.NewGuid().ToString(); var processes = new List<Int32>(); int res = RmStartSession(out handle, 0, key); if (res != 0) throw new Exception("Could not begin restart session. Unable to determine file locker."); try { const int ERROR_MORE_DATA = 234; uint pnProcInfoNeeded = 0, pnProcInfo = 0, lpdwRebootReasons = RmRebootReasonNone; string[] resources = new string[] { path }; // Just checking on one resource. res = RmRegisterResources(handle, (uint)resources.Length, resources, 0, null, 0, null); if (res != 0) throw new Exception("Could not register resource."); //Note: there's a race condition here -- the first call to RmGetList() returns // the total number of process. However, when we call RmGetList() again to get // the actual processes this number may have increased. res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, null, ref lpdwRebootReasons); if (res == ERROR_MORE_DATA) { // Create an array to store the process results RM_PROCESS_INFO[] processInfo = new RM_PROCESS_INFO[pnProcInfoNeeded]; pnProcInfo = pnProcInfoNeeded; // Get the list res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons); if (res == 0) { processes = new List<Int32>((int)pnProcInfo); // Enumerate all of the results and add them to the // list to be returned for (int i = 0; i < pnProcInfo; i++) { try { processes.Add(processInfo[i].Process.dwProcessId); } // catch the error -- in case the process is no longer running catch (ArgumentException) { } } } else { var exceptionMessage = String.Format("Could not list processes locking file ({0}).", path); throw new Exception(exceptionMessage); } } else if (res != 0) { var exceptionMessage = String.Format("Could not list processes locking file ({0}). Failed to get size of result.", path); throw new Exception(exceptionMessage); } } finally { RmEndSession(handle); } return processes; } } } "@ $CheckMyCoreUtilsFileLockUtilLoaded = $CurrentlyLoadedAssemblies | Where-Object {$_.ExportedTypes -like "MyCore.Utils.FileLockUtil*"} if ($null -eq $CheckMyCoreUtilsFileLockUtilLoaded) { Add-Type -ReferencedAssemblies $ReferencedAssemblies -TypeDefinition $TypeDefinition } $Result = [MyCore.Utils.FileLockUtil]::WhoIsLocking($FilePath) } if ($null -ne $PSVersionTable.Platform -and $PSVersionTable.Platform -ne "Win32NT") { $lsofOutput = lsof $FilePath function Parse-lsofStrings ($lsofOutput, $Index) { $($lsofOutput[$Index] -split " " | foreach { if (![String]::IsNullOrWhiteSpace($_)) { $_ } }).Trim() } $lsofOutputHeaders = Parse-lsofStrings -lsofOutput $lsofOutput -Index 0 $lsofOutputValues = Parse-lsofStrings -lsofOutput $lsofOutput -Index 1 $Result = [pscustomobject]@{} for ($i=0; $i -lt $lsofOutputHeaders.Count; $i++) { $Result | Add-Member -MemberType NoteProperty -Name $lsofOutputHeaders[$i] -Value $lsofOutputValues[$i] } } return $Result ##### END Main Body ##### } Function Close-ProcessHandles { [CmdletBinding()] Param ( [Parameter(Mandatory=$True)] [System.String] $FolderPathToClean ) $functionName = $MyInvocation.MyCommand.Name Write-Log "[$functionName] Entering. Params: $($PSBoundParameters | ConvertTo-Json -Compress)" ## Get the process handles that are locking files of lower extension version and save the fileName and corresponding ProcessIDs. $filesLockedByProcessesDict = @{} foreach ($directory in (Get-ChildItem $FolderPathToClean)) { Write-Log "[$functionName] Checking process handles inside directory: $($directory.FullName)" Get-ChildItem -Path $directory.FullName -Recurse | Where-Object { ! $_.PSIsContainer } | ForEach-Object { $filePath = $_.FullName try { $processHandles = Get-FileLockProcess -FilePath $filePath if ($null -ne $processHandles -and $processHandles.Count -gt 0) { $filesLockedByProcessesDict[$filePath] = $processHandles } } catch { $exceptionDetails = Get-ExceptionDetails -ErrorObject $_ Write-Log "[$functionName] Exception occurred for file ($filePath). Exception is as follows: $exceptionDetails" } } } if ($filesLockedByProcessesDict.Keys.Count -eq 0) { Write-Log "[$functionName] No files found with locked process handles." } else { Write-Log "[$functionName] Files locked by Processes are as follows = $($filesLockedByProcessesDict | ConvertTo-Json -Compress)." $currentPID = [System.Diagnostics.Process]::GetCurrentProcess().Id Write-Log "[$functionName] Current PID is $currentPID" $returnStatusMessage = [System.String]::Empty ## Loop through the ProcessIDs and force stop them accordingly (if needed). Write-Log "[$functionName] Looping through locked file processes and force stop them accordingly (if needed)." ## Maintain a set of stopped processes so we don't stop the same one again $stoppedPID = [System.Collections.Generic.HashSet[string]]@() foreach ($currentFile in $filesLockedByProcessesDict.Keys) { foreach ($procId in $filesLockedByProcessesDict[$currentFile]) { if ($procId -eq $currentPID) { ## We do not want to stop the current process, so if the file is locked by the current process, we hope that the process will finish successfully and release the handle. Write-Log "[$functionName] Ignoring file $currentFile as it is used by PID ($procId) which is running the current script." } elseif ($stoppedPID.Contains($procId)) { ## We do not want to stop the same process again (and get "PID not found" error) Write-Log "[$functionName] Ignoring PID ($procId) as it was already stopped" } else { $procDetails = gwmi win32_process | Where-Object {$_.ProcessId -eq $procId} | Select-Object ProcessName, ExecutablePath, CommandLine $fileLockingProcDetails = @{ FilePath = $currentFile ProcId = $procId ProcessName = $procDetails.ProcessName ExecutablePath = $procDetails.ExecutablePath CommandLine = $procDetails.CommandLine } Write-Log "[$functionName] Details of file and its locking process = $($fileLockingProcDetails | ConvertTo-Json -Compress)" try { Write-Log "[$functionName] Stopping process $procId = $(Stop-Process -Id $procId -Force -PassThru | Out-String)" $stoppedPID.Add($procId) | Out-Null } catch{ $returnStatusMessage += "[$functionName] Cannot stop process $procId due to error $_" } } } } } Write-Log "[$functionName] Exiting. Return status message = $returnStatusMessage" return $returnStatusMessage } #endregion Observability Functions #region Misc Functions function Get-ExceptionDetails { [CmdLetBinding()] Param ( [Parameter(Mandatory=$True, ValueFromPipeline)] [System.Management.Automation.ErrorRecord] $ErrorObject ) return @{ Errormsg = $ErrorObject.ToString() Exception = $ErrorObject.Exception.ToString() Stacktrace = $ErrorObject.ScriptStackTrace Failingline = $ErrorObject.InvocationInfo.Line Positionmsg = $ErrorObject.InvocationInfo.PositionMessage PScommandpath = $ErrorObject.InvocationInfo.PSCommandPath Failinglinenumber = $ErrorObject.InvocationInfo.ScriptLineNumber Scriptname = $ErrorObject.InvocationInfo.ScriptName } | ConvertTo-Json ## The ConvertTo-Json will return the entire hashtable as string. } function Save-Exception { [CmdLetBinding()] Param ( [Parameter(Mandatory=$True)] [System.Management.Automation.ErrorRecord] $ErrorObject, [Parameter(Mandatory=$False)] [System.String] $CustomErrorMessage ) $functionName = $MyInvocation.MyCommand.Name $errorKey = $ErrorObject.ToString() $exceptionConstant = $null if ($global:ErrorConstants -and $global:ErrorConstants.Keys -contains $errorKey) { $exceptionConstant = $global:ErrorConstants[$errorKey] } elseif ($WrapperConstants.Exception.Keys -contains $errorKey) { $exceptionConstant = $WrapperConstants.Exception[$errorKey] } if ($exceptionConstant) { $customErrorMessage += $exceptionConstant.Message $errorCode = $exceptionConstant.Code } else { $customErrorMessage += "$($WrapperConstants.Exception.UnhandledException.Message) $errorKey" $errorCode = $WrapperConstants.Exception.UnhandledException.Code } $exceptionDetailsAsString = Get-ExceptionDetails -ErrorObject $ErrorObject ## Exception details are for logging only. Write-Log "[$functionName] $customErrorMessage : $exceptionDetailsAsString" -Level "ERROR" return $errorCode, $customErrorMessage } function Write-Log { [CmdletBinding()] Param ( [Parameter(Mandatory=$True, ValueFromPipeline)] [System.String] $Message, [Parameter(Mandatory=$False)] [ValidateSet("INFO","WARNING","ERROR","FATAL","DEBUG","VERBOSE")] [System.String] $Level = "INFO", [Parameter(Mandatory=$False)] [System.String] $LogFile, [Parameter(Mandatory=$false)] [System.Management.Automation.SwitchParameter] $WriteToConsole ) if ($global:LogFile -and ([System.String]::IsNullOrWhiteSpace($LogFile) -or [System.String]::IsNullOrEmpty($LogFile))) { $LogFile = $global:LogFile } $dateTimeStamp = [System.DateTime]::UtcNow.ToString("u") $formattedMessage = "$dateTimeStamp : $Level : $Message" if ($WriteToConsole -or (-not $LogFile)) { if (Get-Command Trace-Execution -ErrorAction Ignore) { Trace-Execution $formattedMessage } switch($Level.toUpper()) { "INFO" { Write-Host $formattedMessage break; } "DEBUG" { Write-Debug $formattedMessage break; } "VERBOSE" { Write-Verbose $formattedMessage break; } "WARNING" { Write-Warning $formattedMessage break; } "ERROR" { Write-Error $formattedMessage break; } "FATAL" { Write-Error $formattedMessage break; } } } if ($LogFile) { Out-File -FilePath $LogFile -InputObject $formattedMessage -Append -Encoding utf8 } } #endregion Misc Functions #region Legacy functions # Legacy functions are needed for compatability with Action plans that still call these functions function Get-GmaPackageContentPath { "$PSScriptRoot\Legacy" } ## This import is needed only for legacy support. Import-Module (Join-Path -Path (Get-GmaPackageContentPath) -ChildPath 'GMATenantJsonHelper.psm1') ` -DisableNameChecking ` -Verbose:$false ` -Global #endregion Legacy functions #endregion Functions #region Exports ## Handler functions Export-ModuleMember -Function Get-ConfigSequenceNumber Export-ModuleMember -Function Get-HandlerEnvInfo Export-ModuleMember -Function Get-HandlerHeartBeatFile Export-ModuleMember -Function Get-HandlerConfigSettings Export-ModuleMember -Function Get-LogFolderPath Export-ModuleMember -Function Get-StatusFolderPath Export-ModuleMember -Function Get-StatusFilePath Export-ModuleMember -Function Set-HandlerLogFile Export-ModuleMember -Function Get-HandlerLogFile Export-ModuleMember -Function Set-Status ## Observability functions Export-ModuleMember -Function Set-NugetPackageProvider Export-ModuleMember -Function Set-AclsForGivenPath Export-ModuleMember -Function Set-ObsStoreRootFolderPath Export-ModuleMember -Function Get-ExtVersion Export-ModuleMember -Function Set-ObsNugetStorePath Export-ModuleMember -Function Confirm-PackageExists Export-ModuleMember -Function Extract-Package Export-ModuleMember -Function Install-ObsPackages Export-ModuleMember -Function Uninstall-ObsPackages Export-ModuleMember -Function Get-SetupScriptPath Export-ModuleMember -Function Get-FileLockProcess Export-ModuleMember -Function Close-ProcessHandles ## Misc functions Export-ModuleMember -Function Get-ExceptionDetails Export-ModuleMember -Function Save-Exception Export-ModuleMember -Function Write-Log ## Legacy functions Export-ModuleMember -Function Get-GmaPackageContentPath #endregion Exports # SIG # Begin signature block # MIIoRgYJKoZIhvcNAQcCoIIoNzCCKDMCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAcP5CVZpyeRV3Y # oenZuNbrhlguCV2n+caxkP4XfLq9RqCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0 # Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz # NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo # DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3 # a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF # HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy # 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC # Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj # L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp # h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3 # cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X # dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL # E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi # u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1 # sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq # 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb # DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/ # V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # 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 # /Xmfwb1tbWrJUnMTDXpQzTGCGiYwghoiAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIN1nqx5mAr2SrLdbDpGTgaV/ # U4/Sv+gpnmbHwBgzh6JlMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAn04/LJqGsfEmTGTMRYLgC571dlHtY/6BQ22ulSpfb0t0vB2vwEYFxnMN # +z1OTIe/iu8WkrxxY8YEz6UOQyHHVt/auTWknIFcnsr02oV303WP25Th3jyoU9LP # u9CZ+bTmkku0URB9odXnscawgMHgOoMk25OOUttZBtzaQ+W4/t0bgXftJq8NWgzq # DYon/P/tuO8QcZBQYwQwNVI1OtOeN4LehxMmhgC+ymw5I7v1Q+MRKBq9DzRGgu0z # dx97sXVf5FH86uHR9KRAdzH/WSmqgNn3jgbuM8tVdwLIsTEWTtd9Gh0SZRNiD9CM # 8fTcU3qlSITzVEzBUbY8YuORc5K3ZKGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCC # F5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq # hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCCWwLn2itajAKjv+Tdmp0ryBmIjNhSnPq3W9PO4TV3B3gIGZ0oHGn7A # GBMyMDI0MTIwNDE1MDM1NC4xNDdaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo0MDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAAB/tCowns0IQsBAAEAAAH+MA0G # CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u # MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0 # MDcyNTE4MzExOFoXDTI1MTAyMjE4MzExOFowgdMxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w # ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjQwMUEt # MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvLwhFxWlqA43olsE4PCe # gZ4mSfsH2YTSKEYv8Gn3362Bmaycdf5T3tQxpP3NWm62YHUieIQXw+0u4qlay4AN # 3IonI+47Npi9fo52xdAXMX0pGrc0eqW8RWN3bfzXPKv07O18i2HjDyLuywYyKA9F # mWbePjahf9Mwd8QgygkPtwDrVQGLyOkyM3VTiHKqhGu9BCGVRdHW9lmPMrrUlPWi # YV9LVCB5VYd+AEUtdfqAdqlzVxA53EgxSqhp6JbfEKnTdcfP6T8Mir0HrwTTtV2h # 2yDBtjXbQIaqycKOb633GfRkn216LODBg37P/xwhodXT81ZC2aHN7exEDmmbiWss # jGvFJkli2g6dt01eShOiGmhbonr0qXXcBeqNb6QoF8jX/uDVtY9pvL4j8aEWS49h # KUH0mzsCucIrwUS+x8MuT0uf7VXCFNFbiCUNRTofxJ3B454eGJhL0fwUTRbgyCbp # LgKMKDiCRub65DhaeDvUAAJT93KSCoeFCoklPavbgQyahGZDL/vWAVjX5b8Jzhly # 9gGCdK/qi6i+cxZ0S8x6B2yjPbZfdBVfH/NBp/1Ln7xbeOETAOn7OT9D3UGt0q+K # iWgY42HnLjyhl1bAu5HfgryAO3DCaIdV2tjvkJay2qOnF7Dgj8a60KQT9QgfJfwX # nr3ZKibYMjaUbCNIDnxz2ykCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBRvznuJ9SU2 # g5l/5/b+5CBibbHF3TAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf # BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz # L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww # bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El # MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF # BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAiT4NUvO2lw+0 # dDMtsBuxmX2o3lVQqnQkuITAGIGCgI+sl7ZqZOTDd8LqxsH4GWCPTztc3tr8AgBv # sYIzWjFwioCjCQODq1oBMWNzEsKzckHxAzYo5Sze7OPkMA3DAxVq4SSR8y+TRC2G # cOd0JReZ1lPlhlPl9XI+z8OgtOPmQnLLiP9qzpTHwFze+sbqSn8cekduMZdLyHJk # 3Niw3AnglU/WTzGsQAdch9SVV4LHifUnmwTf0i07iKtTlNkq3bx1iyWg7N7jGZAB # RWT2mX+YAVHlK27t9n+WtYbn6cOJNX6LsH8xPVBRYAIRVkWsMyEAdoP9dqfaZzwX # GmjuVQ931NhzHjjG+Efw118DXjk3Vq3qUI1re34zMMTRzZZEw82FupF3viXNR3DV # OlS9JH4x5emfINa1uuSac6F4CeJCD1GakfS7D5ayNsaZ2e+sBUh62KVTlhEsQRHZ # RwCTxbix1Y4iJw+PDNLc0Hf19qX2XiX0u2SM9CWTTjsz9SvCjIKSxCZFCNv/zpKI # lsHx7hQNQHSMbKh0/wwn86uiIALEjazUszE0+X6rcObDfU4h/O/0vmbF3BMR+45r # AZMAETJsRDPxHJCo/5XGhWdg/LoJ5XWBrODL44YNrN7FRnHEAAr06sflqZ8eeV3F # uDKdP5h19WUnGWwO1H/ZjUzOoVGiV3gwggdxMIIFWaADAgECAhMzAAAAFcXna54C # m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp # Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy # MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH # EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV # BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B # AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51 # yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY # 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9 # cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN # 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua # Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74 # kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2 # K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5 # TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk # i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q # BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri # Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC # BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl # pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y # eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA # YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU # 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny # bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw # MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w # Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp # b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm # ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM # 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW # OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4 # FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw # xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX # fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX # VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC # onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU # 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG # ahC0HVUzWLOhcGbyoYIDWTCCAkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo0MDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAhGNHD/a7Q0bQLWVG9JuGxgLRXseggYMw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF # AAIFAOr6dCgwIhgPMjAyNDEyMDQwNjI0NDBaGA8yMDI0MTIwNTA2MjQ0MFowdzA9 # BgorBgEEAYRZCgQBMS8wLTAKAgUA6vp0KAIBADAKAgEAAgIQ8gIB/zAHAgEAAgIV # 3jAKAgUA6vvFqAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow # CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQB1bnVJedws # V71dgEZ2I4E+NstsP2iVqt75/DZdToSxB5h9UGlR2i/bd3UROjCj3/c8hNAdK0Qy # 3BC+X+XpnfVCxgNvAPw3AEyXEofTMj/JNKUyGSLG1HtX5K1P0VZAJsAWSRi5woTZ # z323wCn305Ce19xl2oIMchH2rOyXNgwVZ/cPSLv2ZV4sXvStBBvdm0h6Mml00Vu1 # QENrLXrrqEAjuP5kOAFFdNM4n0WHUN+5/S4dL8emUIwquAqFXagvBNhZ1JgVcHUx # PCjZd5DZx+xKKCsFS17PHKYqhKH7QDXCq07CWUXNvubuvINKlAkV123HcEhhvPSX # FC4ABSYo3jjCMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB # IDIwMTACEzMAAAH+0KjCezQhCwEAAQAAAf4wDQYJYIZIAWUDBAIBBQCgggFKMBoG # CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgJMd1To/D # 1a2Dxy3vx9/Kcg+bpCQs+j76CPxm/YH56gEwgfoGCyqGSIb3DQEJEAIvMYHqMIHn # MIHkMIG9BCARhczd/FPInxjR92m2hPWqc+vGOG1+/I0WtkCstyh0eTCBmDCBgKR+ # MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT # HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB/tCowns0IQsBAAEA # AAH+MCIEIIO2hrDY4UoAvVTRqJsMmPbvGrI93qiWHaxo8TjxftSCMA0GCSqGSIb3 # DQEBCwUABIICACrjW8QEXa6fgzoURDAXcKFaNHvJwgdC5kJePb4G9AiIDFR+ypcV # QI2sAnOuQAHZt4/J3d8NBTMpqdFKO7imvpOdU8/jGzHICre5bVWBSqvCEqoCwwvC # CBs/4cYdiJkkditPeN9+XO97d5ygRsmv4n2KA3MqEbmrHGxATZyUSamP7ZSomTwc # UbreleMO6aTGlmNqfOU08sED8JUqn9btnTX1YztkPbqO7eJPLJ/SwAUUiDfb/jgD # IpNQAIltZsnbUm0t76CpVjKbKlXXM8KUKeqTPBlD5eNksGLsaE1kf2tK5a12myf0 # XbwYmOIZ4vI9ul6nJzf3JbbdIrmQN3iz7CIFc/MKstPZV9g2s+7Va4n1vm3efuJa # 5gvwRkvg+l9DnHpDioqA492in+UiI+YSc5taMePtCfC1Rn6t0hkPpqFJN5UwUQas # Zme7ClQSXL7Ie7mJEAXqN40gHtvS9ThuM3mwQsD7VChkyPHmqKzIdUDMK+g+31bc # /XISfZWXAEaamHyqFUUqW6T+4ctUENbQYNV6H4FZLZfHlUgbtA2ngQkwEG7kMVl/ # 606ffZoXtIoehn04R2U6klH5zJctHmj+IxghuZPHJesFLw3gb2eZ10zn9M4veP+A # l76P4+nC7HaI3S0ZGO2uXitr02S80/Kk3F7yvCYVO1NjZ4QLS6DkKIad # SIG # End signature block |