AzStackHciSBEHealth/AzStackHci.SBEHealth.Helpers.psm1
|
Import-LocalizedData -BindingVariable locSbeTxt -FileName AzStackHci.SBEHealth.Strings.psd1 function Get-ASArtifactPathLite { <# .SYNOPSIS Returns the nuget content path. Same as normal Get-ArtifactPath except it doesn't use Trace-Execution or try to locate on ProductVHD. .DESCRIPTION Calculates and returns the path to the nuget content folder for the specified nuget on the current infrastructure vm environment. All product artifacts are exposed to the infrastructure vms, however the location is not fixed, this method is used to find the desired content. .EXAMPLE $Path = Get-ASArtifactPathLite -NugetName "Microsoft.Diagnostics.Tracing.EventSource.Redist" .PARAMETER NugetName The full name of the nuget without version information. .PARAMETER Version The optional version number of the package. #> [CmdletBinding()] PARAM ( [Parameter(Position=0, Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $NugetName, [Parameter(Mandatory=$false)] [System.String] $Version = $null ) PROCESS { $VerbosePreference = [System.Management.Automation.ActionPreference]::Continue Import-Module PackageManagement -DisableNameChecking -Verbose:$false | Out-Null $nugetProvider = Get-PackageProvider | Where-Object { $_.Name -eq "Nuget" } if ($nugetProvider -eq $null) { Log-Info -Message "Attempting to install nuget package provider." Install-PackageProvider nuget -Force -ForceBootstrap } $drivePath = "$env:SystemDrive\NugetStore" if (Test-Path -Path $drivePath) { if ($Version) { $package = Get-Package -Name $NugetName -Destination $drivePath -ErrorAction Stop -RequiredVersion $Version -ProviderName Nuget } else { $package = Get-Package -Name $NugetName -Destination $drivePath -ErrorAction Stop -ProviderName Nuget } Log-Info -Message "Get-Package returned with Success:$($?)" } if ($package -eq $null) { throw "Could not find package $NugetName on source $drivePath." } Log-Info -Message "Found package $($package.Name) with version $($package.Version) at $($package.Source)." return [System.IO.Path]::GetDirectoryName($package.Source); } } function Copy-SBEContentLocalToNode { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string]$PackagePath, [Parameter(Mandatory=$true)] [string]$TargetNodeName, [Parameter(Mandatory=$true)] [string]$DestPath, [Parameter(Mandatory=$false)] [string[]]$ExcludeDirs, [Parameter(Mandatory=$false)] [string[]]$ExcludeFiles, [Parameter(Mandatory=$false)] [switch]$SkipNugetCopy, [PSCredential]$Credential ) $copyItems = @() # Note - this function only works on the seed node as only it will have NugetStore bootstrapped. $sbeConfig = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.SBEConfiguration" $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE" if ($Credential) { Log-Info ("$($MyInvocation.MyCommand.Name) - Username is '{0}'" -f $Credential.UserName) } else { # Credential is not needed if the target is the seed node (in this case it is copying to itself) Log-Info "$($MyInvocation.MyCommand.Name) - Credential was not provided" } # Check if the copy destination is the current node $targetIsCurrentNode = $false if ($env:ComputerName -eq $TargetNodeName) { Log-Info "$($MyInvocation.MyCommand.Name) - Current node ComputerName matched TargetNodeName" $targetIsCurrentNode = $true } else { $thisComputerName = $null $foundDnsModule = Get-Command -Module DnsClient -Name Register-DnsClient -ErrorAction SilentlyContinue if ($null -eq $foundDnsModule) { Import-Module -Name DnsClient -ErrorAction SilentlyContinue -Force | Out-Null } $dnsName = ((Resolve-DnsName -Name $TargetNodeName -ErrorAction SilentlyContinue) | Select-Object -First 1) if ($dnsName.NameHost) { # Case when IP is resolved $thisComputerName = ($dnsName.NameHost).Split('.')[0] if ($env:ComputerName -eq $thisComputerName) { Log-Info "$($MyInvocation.MyCommand.Name) - Current node ComputerName matched Resolve-DnsName by IP address" $targetIsCurrentNode = $true } } elseif ($dnsName.Name) { # Case when hostname is resolved $thisComputerName = ($dnsName.Name).Split('.')[0] if ($env:ComputerName -eq $thisComputerName) { Log-Info "$($MyInvocation.MyCommand.Name) - Current node ComputerName matched Resolve-DnsName by hostname" $targetIsCurrentNode = $true } } else { # No DNS match so try IP address instead [array]$myIP = (Get-NetIPAddress).IPAddress if ($TargetNodeName -in $myIP) { Log-Info "$($MyInvocation.MyCommand.Name) - TargetNodeName was matched in the current node myIP list" $targetIsCurrentNode = $true } } } if ($true -eq $targetIsCurrentNode) { $finalDestPath = $DestPath Log-Info "$($MyInvocation.MyCommand.Name) - Target node is the current node, so use local path as destination: $finalDestPath" } else { Log-Info "$($MyInvocation.MyCommand.Name) - Target node is a remote node, so need to map PSDrive(s)" Get-PSDrive -Name SBE -ErrorAction SilentlyContinue | Remove-PSDrive -Force if ($DestPath -match '^(\w):') { $destRoot = '\\' + $TargetNodeName + '\' + $Matches[1] + '$' } else { throw "Unable to determine proper path to copy SBE. Dest structure is unexpected '$DestPath'." } $systemDriveRoot = '\\' + $TargetNodeName + '\' + ($env:SystemDrive ).Replace(':','$') $retry = $true $maxRetry = 4 $attempt = 0 while ($true -eq $retry) { $attempt++ Log-Info "$($MyInvocation.MyCommand.Name) - Map New-PSDrive to '$($destRoot)', attempt '$($attempt)/$($maxRetry)'" try { $destDrv = New-PSDrive -Credential $Credential -Name SBECACHE -PSProvider FileSystem -Root $destRoot -ErrorAction SilentlyContinue } catch { $errMessage = $PSItem.Exception.Message Log-Info "$($MyInvocation.MyCommand.Name) - New-PSDrive failed with exception: $($errMessage)" } $found = Get-PSDrive -Name SBECACHE if ($found -and $found.Root -eq $destRoot) { $errMessage = '' $retry = $false } else { if ($attempt -ge $maxRetry) { throw "Failed to map New-PSDrive after '$($attempt)' attempts. Exception: '$($errMessage)'" $retry = $false } Start-Sleep -Seconds 15 } } # Change the destination path to use the mounted drive letter... $finalDestPath = $DestPath -replace '^\w:', $destDrv.Root Log-Info "$($MyInvocation.MyCommand.Name) - Changing DestPath from $DestPath to $finalDestPath." if ($destRoot -ne $systemDriveRoot -and $false -eq $SkipNugetCopy.IsPresent) { $retry = $true $maxRetry = 4 $attempt = 0 while ($true -eq $retry) { $attempt++ Log-Info "$($MyInvocation.MyCommand.Name) - Map New-PSDrive to '$($systemDriveRoot)', attempt '$($attempt)/$($maxRetry)'" try { $sysDrv = New-PSDrive -Credential $Credential -Name SBESYSROOT -PSProvider FileSystem -Root $systemDriveRoot -ErrorAction SilentlyContinue } catch { $errMessage = $PSItem.Exception.Message Log-Info "$($MyInvocation.MyCommand.Name) - New-PSDrive failed with exception: $($errMessage)" } $found = Get-PSDrive -Name SBESYSROOT if ($found -and $found.Root -eq $systemDriveRoot) { $errMessage = '' $retry = $false } else { if ($attempt -ge $maxRetry) { throw "Failed to map New-PSDrive after '$($attempt)' attempts. Exception: '$($errMessage)'" $retry = $false } Start-Sleep -Seconds 15 } } $sbeConfigDest = $sbeConfig.Replace($env:SystemDrive,$sysDrv.Root) $sbeRoleDest = $sbeRoleNuget.Replace($env:SystemDrive,$sysDrv.Root) } elseif ($false -eq $SkipNugetCopy.IsPresent) { Log-Info "$($MyInvocation.MyCommand.Name) - Using the destDrv mount to copy Config and Role Nugets." $sbeConfigDest = $sbeConfig.Replace($env:SystemDrive,$destDrv.Root) $sbeRoleDest = $sbeRoleNuget.Replace($env:SystemDrive,$destDrv.Root) } else { Log-Info "$($MyInvocation.MyCommand.Name) - Skipping sysDrv mount - we don't need to copy Config or Role Nugets." # This is typical of post-deploy OperationType like "Update" where the SBE.Role nuget is already available on all nodes and the SBEConfiguration nuget is not needed. } } $msg = "$($MyInvocation.MyCommand.Name) - " + ($locSbeTxt.WillCopyToPSDrive -f 'SBE package contents',$finalDestPath,$TargetNodeName) Log-Info -Message $msg $copyItems += @{Source=$PackagePath;Destination=$finalDestPath} if (-not([string]::IsNullOrWhitespace($sbeConfigDest))) { $msg = "$($MyInvocation.MyCommand.Name) - " + ($locSbeTxt.WillCopyToPSDrive -f 'SBEConfiguration',$sbeConfigDest,$TargetNodeName) Log-Info -Message $msg $copyItems += @{Source=$sbeConfig;Destination=$sbeConfigDest} } if (-not([string]::IsNullOrWhitespace($sbeRoleDest))) { $msg = "$($MyInvocation.MyCommand.Name) - " + ($locSbeTxt.WillCopyToPSDrive -f 'SBE.Role nuget',$sbeRoleDest,$TargetNodeName) Log-Info -Message $msg $copyItems += @{Source=$sbeRoleNuget;Destination=$sbeRoleDest} } [string]$exclude = "" if ($ExcludeFiles.Count -ne 0) { $exclude += " /XF $ExcludeFiles" } if ($ExcludeDirs.Count -ne 0) { $exclude += " /XD $ExcludeDirs" } foreach ($item in $copyItems) { try { $msg = "$($MyInvocation.MyCommand.Name) - " + ($locSbeTxt.CopySBEToNode -f $item.Source,$TargetNodeName,$item.Destination) Log-Info -Message $msg -Type Info $copyCmd = "robocopy.exe $($item.Source) $($item.Destination) *.* /MIR /NP /R:2 /W:10$exclude" $output = Invoke-Command -ScriptBlock { cmd.exe /c $copyCmd } # Check for exit code. If exit code is greater than 7, an error occurred while peforming the copy operation. if ($LASTEXITCODE -ge 8) { $msg = "$($MyInvocation.MyCommand.Name) - " + ($locSbeTxt.RobocopyFailed -f $LASTEXITCODE) Log-Info -Message $msg -ConsoleOut -Type Error $msg = "$($MyInvocation.MyCommand.Name) - " + ($output | Out-String).Trim() Log-Info -Message $msg -ConsoleOut -Type Info if ($destDrv) { $destDrv | Remove-PSDrive -ErrorAction SilentlyContinue } if ($sysDrv) { $sysDrv | Remove-PSDrive -ErrorAction SilentlyContinue } return $false } else { try { if ($true -eq (Test-Path -Path $item.Destination -PathType Container)) { Log-Info -Message "$($MyInvocation.MyCommand.Name) - Unblock-File for destination '$($item.Destination)'" -Type Info Get-ChildItem -Path $item.Destination -Recurse | ForEach-Object { Unblock-File -Path $PSItem.FullName -ErrorAction SilentlyContinue } } else { # Expected destination to exist as a folder Log-Info -Message "$($MyInvocation.MyCommand.Name) - Expected a folder for Unblock-File: $($item.Destination)" -Type Warning } } catch { # Ignore errors here Log-Info -Message "$($MyInvocation.MyCommand.Name) - Error occurred during Unblock-File: $($PSItem.Exception.Message)" -Type Warning } } } catch { Log-Info -Message ("$($MyInvocation.MyCommand.Name) - Copy operation failed with error: " + $PSItem.Exception.Message) -Type Error throw "Copy operation '$($copyCmd)' failed with error: $($PSItem.Exception.Message)" } finally { # Remove any mapped PSDrives if (Get-PSDrive -Name SBESYSROOT -ErrorAction SilentlyContinue) { Get-PSDrive -Name SBESYSROOT -ErrorAction SilentlyContinue | Remove-PSDrive -Force | Out-Null } if (Get-PSDrive -Name SBECACHE -ErrorAction SilentlyContinue) { Get-PSDrive -Name SBECACHE -ErrorAction SilentlyContinue | Remove-PSDrive -Force | Out-Null } } } if ($destDrv) { $destDrv | Remove-PSDrive -ErrorAction SilentlyContinue } if ($sysDrv) { $sysDrv | Remove-PSDrive -ErrorAction SilentlyContinue } return $true } function Get-SBEHealthCheckParams { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [CloudEngine.Configurations.EceInterfaceParameters] $ECEParameters, [String] $Tag, [Parameter(Mandatory=$true)] [string] $SBEMetadataPath ) $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE" Import-Module "$($sbeRoleNuget)\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null $sbePartnerProps = $null if ((Get-Command Get-SBEPartnerProperties).Parameters.Keys -contains "SBEMetadataPath") { $sbePartnerProps = Get-SBEPartnerProperties -SBERoleConfig $ECEParameters.Roles["SBE"].PublicConfiguration -SBEMetadataPath $SBEMetadataPath } else { Log-Info -Message "Get-SBEPartnerProperties does not support SBEMetadataPath parameter, so calling without it." -Type Info $sbePartnerProps = Get-SBEPartnerProperties -SBERoleConfig $ECEParameters.Roles["SBE"].PublicConfiguration } $sbeCredList = $null if ((Get-Command Get-SBECredentialList).Parameters.Keys -contains "SBEMetadataPath") { $sbeCredList = Get-SBECredentialList -Parameters $ECEParameters -SBEMetadataPath $SBEMetadataPath } else { Log-Info -Message "Get-SBECredentialList does not support SBEMetadataPath parameter, so calling without it." -Type Info $sbeCredList = Get-SBECredentialList -Parameters $ECEParameters } $sbeHostData = Get-AllNodesData -BareMetalConfig $ECEParameters.Roles["BareMetal"].PublicConfiguration $params = @{ CredentialList = $sbeCredList HostData = $sbeHostData PartnerProperties = $sbePartnerProps Tag = $Tag } return $params } function Get-ManifestMatchesModelandSKUResult { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [String] $SBEManifestFilePath, [Parameter(Mandatory=$true)] [Object] $EndpointContentsResult, [Parameter(Mandatory = $false)] [System.String] $ModelOverride = "", [Parameter(Mandatory = $false)] [System.String] $SKUOverride ) $modelValue = $ModelOverride if ([System.String]::IsNullOrEmpty($ModelOverride)) { try { $modelValue = (Get-ItemPropertyValue -Path "HKLM:\HARDWARE\DESCRIPTION\System\BIOS" -Name "SystemProductName").ToString() } catch { try { # Typically on need to do this on containerized build agents Trace-Execution "Unable to determine model from registry - falling back to Win32_ComputerSystem" $modelValue = (Get-WmiObject -ComputerName localhost -Namespace root\cimv2 -Class Win32_ComputerSystem | Select-Object Model).Model } catch { Trace-Execution "Unable to determine model from Win32_ComputerSystem" $modelValue = "Unknown" } } } $skuValue = $SKUOverride if ($null -eq $SKUOverride) { try { $skuValue = (Get-ItemPropertyValue -Path "HKLM:\HARDWARE\DESCRIPTION\System\BIOS" -Name "SystemSKU").ToString() } catch { try { Trace-Execution "Unable to determine SKU from registry - falling back to Win32_ComputerSystem" $skuValue = (Get-WmiObject -ComputerName localhost -Namespace root\cimv2 -Class Win32_ComputerSystem | Select-Object SystemSKUNumber).SystemSKUNumber } catch { Trace-Execution "Unable to determine SKU from Win32_ComputerSystem" } } } try { $manifestXML = New-Object -TypeName System.Xml.XmlDocument $manifestXML.PreserveWhitespace = $false $xmlTextReader = New-Object -TypeName System.Xml.XmlTextReader -ArgumentList $SBEManifestFilePath $manifestXML.Load($xmlTextReader) $xmlTextReader.Dispose() # Get all of the supported models entries in the manifest $suppportedModelsElements = $manifestXML.SelectNodes("//ApplicableUpdate/UpdateInfo/SupportedModels") if ([System.String]::IsNullOrEmpty($suppportedModelsElements)) { throw "Unable to locate any SupportedModels elements in manifest at $SBEManifestFilePath" } # test that the model is supported $modelSupported = $false $skuSupported = $false $supportedModels = $applicableUpdate.UpdateInfo.SupportedModels $noSpacesModel = $modelValue -replace '[\W]', '' $supportedModelTextList = @() $totalSupportedModels = @{} foreach ($supportedModels in $suppportedModelsElements) { foreach ($supportedModel in $supportedModels.SupportedModel) { $supportedModelValue = $supportedModel.InnerText if ([System.String]::IsNullOrEmpty($supportedModelValue)) { $supportedModelValue = $supportedModel } $totalSupportedModels[$supportedModelValue] = $true } } $supportedModelTextList = $totalSupportedModels.Keys $supportedModelListAsString = $supportedModelTextList -join ", " foreach ($supportedModels in $suppportedModelsElements) { foreach ($supportedModel in $supportedModels.SupportedModel) { $supportedModelValue = $supportedModel.InnerText if ([System.String]::IsNullOrEmpty($supportedModelValue)) { $supportedModelValue = $supportedModel } if ($noSpacesModel -like "$supportedModelValue*") { Trace-Execution "Model '$modelValue' is supported by SBE." $modelSupported = $true $supportedSKUs = $supportedModel.SupportedSKUs $notSupportedSKUS = $supportedModel.NotSupportedSKUs if ($null -eq $notSupportedSKUS -and $null -eq $supportedSKUs) { # no SKU restrictions $skuSupported = $true } else { $supportedSKUList = $supportedSKUs -split ";" $notSupportedSKUList = $notSupportedSKUS -split ";" $noSpaceSKU = $skuValue -replace '[\W]', '' foreach ($supportedSKU in $supportedSKUList) { if ([System.String]::IsNullOrWhiteSpace(($supportedSKU))) { continue } if ($noSpaceSKU -like "$supportedSKU*") { Trace-Execution "SKU '$skuValue' is supported by SBE." $skuSupported = $true break } } foreach ($notSupportedSKU in $notSupportedSKUList) { if ([System.String]::IsNullOrWhiteSpace(($notSupportedSKU))) { continue } if ($noSpaceSKU -like "$notSupportedSKU*") { Trace-Execution "SKU '$skuValue' is not supported by SBE." $skuSupported = $false break } } } } if ($modelSupported -and $skuSupported) { break } } } if ($modelSupported -and $skuSupported) { Trace-Execution "Model '$modelValue' and SKU '$skuValue' are supported by SBE." $EndpointContentsResult.Status = 'SUCCESS' $EndpointContentsResult.Description = "The current SBE discovery manifest endpoint at $sbeEndpoint has matching entries for the server model '$modelValue' and SKU '$skuValue'." } else { $msg = "System model '$modelValue' is not supported by SBE. Supported models: $supportedModelListAsString" Trace-Execution $msg $EndpointContentsResult.Status = 'FAILURE' $EndpointContentsResult.Description = $msg $EndpointContentsResult.Remediation = "The current SBE discovery manifest endpoint at $sbeEndpoint does not currently have any matching entries for the server model '$modelValue' and SKU '$skuValue'.`nTo assure your Azure Local instance can receive updates, review your hardware vendor documentation to confirm the endpoint override $sbeEndpoint is appropriate for your model server.`nTo reset the endpoint to the default value please use: Set-OverrideUpdateConfiguration -ResetDefaultOemUpdateUri" } } catch { $msg = "Failed to parse SBE manifest XML at $SBEManifestFilePath. Error: $($PSItem.Exception.Message)" Trace-Execution $msg $EndpointContentsResult.Status = 'FAILURE' $EndpointContentsResult.Description = $msg } return $EndpointContentsResult } function Test-SBEPropertiesValid { [CmdletBinding()] param ( [Parameter()] [CloudEngine.Configurations.EceInterfaceParameters] $ECEParameters, [Parameter(Mandatory=$false)] [System.String] $SBEMetadataPath ) $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE" Import-Module "$($sbeRoleNuget)\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null if ((Get-Command Get-SBEPartnerProperties).Parameters.Keys -contains "SBEMetadataPath") { $sbePartnerProps = Get-SBEPartnerProperties -SBERoleConfig $ECEParameters.Roles["SBE"].PublicConfiguration -SBEMetadataPath $SBEMetadataPath } else { Log-Info -Message "Get-SBEPartnerProperties does not support SBEMetadataPath parameter, so calling without it." -Type Info $sbePartnerProps = Get-SBEPartnerProperties -SBERoleConfig $ECEParameters.Roles["SBE"].PublicConfiguration } Log-Info -Message "Found '$($sbePartnerProps.Count)' PartnerProperties." -Type Info } function Test-SBECredentialsValid { [CmdletBinding()] param ( [Parameter()] [CloudEngine.Configurations.EceInterfaceParameters] $ECEParameters, [Parameter(Mandatory=$false)] [System.String] $SBEMetadataPath ) $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE" Import-Module "$($sbeRoleNuget)\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null if ((Get-Command Get-SBECredentialList).Parameters.Keys -contains "SBEMetadataPath") { $sbeCredList = Get-SBECredentialList -Parameters $ECEParameters -SBEMetadataPath $SBEMetadataPath } else { Log-Info -Message "Get-SBECredentialList does not support SBEMetadataPath parameter, so calling without it." -Type Info $sbeCredList = Get-SBECredentialList -Parameters $ECEParameters } } function Test-SolutionExtensionModule { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $PackagePath, [Parameter()] [System.Management.Automation.Runspaces.PSSession] $PsSession ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop $solExtModule = $null Log-Info -Message ($locSbeTxt.SBEPackagePath -f $PackagePath) -Type Info if ($PSSession) { $computername = $PsSession.ComputerName } else { $computername = $env:ComputerName } # Validate the SolutionExtension module using a function from the SBE Role Helper module $sbValidate = { param( [String] [parameter(Mandatory=$true)] $PackagePath, [String] [parameter(Mandatory=$true)] $SbeRoleNuget ) try { Import-Module "$SbeRoleNuget\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null $solExtModulePath = Join-Path -Path $PackagePath -ChildPath "Configuration\SolutionExtension" $solExtModule = Initialize-SolutionExtensionModule -SolExtFilePath $solExtModulePath -RequireTag "HealthServiceIntegration" -AssertCertificate return $solExtModule } catch { Write-Output "An exception occurred while validating the SolutionExtension module: " + ($PSItem | Format-List * | Out-String).Trim() } } $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE" $solExtModule = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sbValidate -ArgumentList @($PackagePath, $sbeRoleNuget) } else { Invoke-Command -ScriptBlock $sbValidate -ArgumentList @($PackagePath, $sbeRoleNuget) } if ($null -eq $solExtModule) { Log-Info -Message ($locSbeTxt.NoHeatlhChecks) -Type Info return $false } elseif ($solExtModule -match "An exception occurred") { throw $solExtModule } return $true } function Invoke-TestSBEContentIntegrity { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $SBEMetadataPath, [Parameter(Mandatory=$true)] [string] $SBEContentPath, [Parameter()] [System.Management.Automation.Runspaces.PSSession] $PsSession ) $sbIntegrity = { param( [String] [parameter(Mandatory=$true)] $SBEMetadataPath, [String] [parameter(Mandatory=$true)] $SBEContentPath, [String] [parameter(Mandatory=$true)] $SbeRoleNuget ) try { if (-not(Get-Command -Name Test-SBEContentIntegrity -ErrorAction SilentlyContinue)) { if (Test-Path -Path "$($SbeRoleNuget)\content\Helpers\SBEMetadataHelper.psm1" -PathType Leaf) { Write-Verbose "Importing SBEMetadataHelper module from $SbeRoleNuget for content integrity test." Import-Module "$($SbeRoleNuget)\content\Helpers\SBEMetadataHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null } else { Write-Verbose "Fallback to importing SBESolutionExtensionHelper module from $SbeRoleNuget for content integrity test." Import-Module "$($SbeRoleNuget)\content\Helpers\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null } } $skipDir = @("IntegratedContent") Test-SBEContentIntegrity -SBEMetadataDirPath $SBEMetadataPath -SBEContentPath $SBEContentPath -IgnoreTopLevelFolder $skipDir } catch { throw $PSItem } } $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE" $result = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sbIntegrity -ArgumentList @($SBEMetadataPath, $SBEContentPath, $sbeRoleNuget) } else { Invoke-Command -ScriptBlock $sbIntegrity -ArgumentList @($SBEMetadataPath, $SBEContentPath, $sbeRoleNuget) } return $result } function Import-SolutionExtensionModule { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string] $PackagePath, [Parameter()] [System.Management.Automation.Runspaces.PSSession] $PsSession ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop # Import the SolutionExtension module $solExtModule = (Join-Path -Path $PackagePath -ChildPath "Configuration\SolutionExtension\SolutionExtension.psd1") Log-Info -Message ($locSbeTxt.ModuleToImport -f $solExtModule) -Type Info $sbImport = { param( [String] [parameter(Mandatory=$true)] $SolExtModule ) try { Import-Module $SolExtModule -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null } catch { Write-Output "An error occurred while importing the SolutionExtension module: " + ($PSItem | Format-List * | Out-String).Trim() } } $result = if ($PsSession) { Invoke-Command -Session $PsSession -ScriptBlock $sbImport -ArgumentList @($solExtModule) } else { Invoke-Command -ScriptBlock $sbImport -ArgumentList @($solExtModule) } if ($result -match "An exception occurred") { throw $solExtModule } return $true } function New-SBEHealthResultObject { param ( [Parameter(Mandatory=$true)] [string]$TargetName, [Parameter()] [string]$TestName, [Parameter()] [ValidateSet('CRITICAL','WARNING','INFORMATIONAL')] [string]$Severity = 'INFORMATIONAL', [Parameter()] [ValidateSet('SUCCESS', 'FAILURE', 'ERROR')] [string]$Status, [Parameter()] [string]$Description, [Parameter()] [string]$Detail, [Parameter()] [bool]$PopulateAdditionalData = $true ) $name = 'AzStackHci_SBEHealth' $title = 'SBE' if (-not([string]::IsNullOrWhiteSpace($TestName))) { $name += "_$TestName" $title += (" " + $TestName.Replace("-", " ")) } $name += "_$TargetName" if (-not($title.EndsWith(" Health Check"))) { $title += " Health Check" } $params = @{ Name = $name Title = $title DisplayName = $title Severity = $Severity Description = $Description Tags = @{} Remediation = '' TargetResourceID = $TargetName TargetResourceName = $TargetName TargetResourceType = 'SBEHealth' Timestamp = "$([datetime]::UtcNow)" Status = $Status AdditionalData = @{ Source = $TargetName Resource = 'SBEHealth' Detail = $Detail Status = $Status Timestamp = "$([datetime]::UtcNow)" } HealthCheckSource = $ENV:EnvChkrId } $resultObj = New-AzStackHciResultObject @params return $resultObj } function Get-ResultObject { $resultObject = @{ "Name" = "" "DisplayName" = "" "Title"= "" "Description" = "" "Status" = "" "Severity" = "" "Timestamp" = "" "TargetResourceID" = "" "TargetResourceName" = "" "TargetResourceType" = "" "Tags" = @{} "AdditionalData" = @{} "HealthCheckSource" = "" "Remediation" = "" } return $resultObject } function Assert-ResponseSchemaValid { [CmdletBinding()] param ( [PSObject[]]$ResultObject ) $expectedSchema = Get-ResultObject foreach ($item in $ResultObject) { # Assert Name or Title must contain information if ([string]::IsNullOrWhiteSpace($item.Name) -and [string]::IsNullOrWhiteSpace($item.Title)) { $msg = "Both Name and Title properties of this result object are empty" Log-Info -Message $msg -Type Error $item.AdditionalData.NameTitleEmpty = $msg $item.Severity = 'CRITICAL' $item.Status = "Error" } elseif ([string]::IsNullOrWhiteSpace($item.Name)) { $item.Name = $item.Title } elseif ([string]::IsNullOrWhiteSpace($item.Title)) { $item.Title = $item.Name } # Assert response contains expected schema properties foreach ($expectedKey in $expectedSchema.Keys) { if (-not($item.ContainsKey($expectedKey))) { # TODO : Temporary special case to add DisplayName if missing due to this being added after partner communication if ($key -eq "DisplayName") { $item.DisplayName = $item.Title } else { Log-Info -Message "Expected result property '$($expectedKey)' was not found" -Type Warning $item.$expectedKey = "" # TODO : In the future, we should decide how to better handle these cases of missing properties } } } # Assert Status values if ($item.Status -notin @("Success", "Failure", "Error")) { $msg = "Unexpected Status: '$($item.Status)'" Log-Info -Message $msg $item.AdditionalData.StatusDiscrepancy = $msg $item.Status = "Error" } # Assert Severity values if ($item.Severity -notin @('CRITICAL', 'WARNING', 'INFORMATIONAL')) { $msg = "Unexpected Severity: '$($item.Severity)'" $item.AdditionalData.SeverityDiscrepancy = $msg if ($item.Status -eq "Success") { $item.Severity = 'WARNING' Log-Info -Message $msg -Type Warning } else { $item.Severity = 'CRITICAL' Log-Info -Message $msg -Type Error } } # Assert Timestamp is valid if (-not [string]::IsNullOrWhiteSpace($item.Timestamp)) { try { $null = [DateTime]$item.Timestamp } catch { Log-Info -Message "Invalid Timestamp: '$($item.Timestamp)'" -Type Warning if (-not [string]::IsNullOrWhiteSpace($item.AdditionalData.Timestamp)) { try { $null = [DateTime]$item.AdditionalData.Timestamp # AdditionalData.Timestamp is valid, so use it $item.Timestamp = $item.AdditionalData.Timestamp } catch { # Use current time $item.Timestamp = "$([datetime]::UtcNow)" } } else { # Use current time $item.Timestamp = "$([datetime]::UtcNow)" } } } if (-not [string]::IsNullOrWhiteSpace($item.AdditionalData.Timestamp)) { try { $null = [DateTime]$item.AdditionalData.Timestamp } catch { Log-Info -Message "Invalid Timestamp: '$($item.AdditionalData.Timestamp)'" -Type Warning # Timestamp must be valid now, so use it $item.AdditionalData.Timestamp = $item.Timestamp } } } return $ResultObject } function Test-SBEEndpointConnectivity { [CmdletBinding()] param ( [Parameter(Mandatory=$false)] [string] $SbeEndpointUri, [Parameter(Mandatory=$true)] [PSCustomObject] $EndpointAccessResult, [Parameter(Mandatory=$true)] [string] $ManifestFilePath ) if ([System.String]::IsNullOrWhiteSpace($SbeEndpointUri)) { Trace-Execution "SBE manifest endpoint not reported by Get-SolutionDiscoveryDiagnosticInfo." $EndpointAccessResult.Status = 'FAILURE' # don't want to block updates if there is a bug with Get-SolutionDiscoveryDiagnosticInfo reporting the endpoint $EndpointAccessResult.Severity = 'INFORMATIONAL' $EndpointAccessResult.Description = "SBE manifest endpoint not reported by Get-SolutionDiscoveryDiagnosticInfo." $EndpointAccessResult.Remediation = "Check the Get-SolutionDiscoveryDiagnosticInfo output for the SBE manifest endpoint. If it is missing, check the Solution Discovery service configuration." } else { $caughtThrow = $false try { Trace-Execution "Checking connectivity to SBE manifest endpoint: $SbeEndpointUri" Trace-Execution "Downloading SBE manifest to temporary file: $ManifestFilePath" # Need to use try/catch. Apparently Invoke-WebRequest doesn't really support SilentlyContinue $manifestResponse = Invoke-WebRequest -Uri $SbeEndpointUri -UseBasicParsing -ErrorAction SilentlyContinue -OutFile $ManifestFilePath -TimeoutSec 15 -PassThru Trace-Execution "SBE manifest response: $($manifestResponse.StatusCode)" } catch { $msg = "Failed to reach SBE manifest endpoint: $SbeEndpointUri. Error: $($PSItem.Exception.Message)" Trace-Execution $msg $EndpointAccessResult.Status = 'FAILURE' $EndpointAccessResult.Description = $msg $caughtThrow = $true } if ($null -eq $manifestResponse -and $false -eq $caughtThrow) { # no response object - should never get here as only way to have null is for an exception to be thrown now that we use -PassThru $msg = "Failed to reach SBE manifest endpoint: $SbeEndpointUri. No response received." Trace-Execution $msg $EndpointAccessResult.Status = 'FAILURE' $EndpointAccessResult.Description = $msg } elseif ($manifestResponse.StatusCode -ne 200) { $msg = "Failed to reach SBE manifest endpoint: $SbeEndpointUri. Response code: $($manifestResponse.StatusCode)" Trace-Execution $msg $EndpointAccessResult.Status = 'FAILURE' $EndpointAccessResult.Description = $msg } if ($EndpointAccessResult.Status -eq 'FAILURE') { $EndpointAccessResult.Remediation = "Check firewall rules to ensure the SBE manifest endpoint $SbeEndpointUri is reachable." if ($SbeEndpointUri -like "*aka.ms*") { $EndpointAccessResult.Remediation += " NOTE: Because aka.ms redirects, you will need to allow HTTPS(443) to aka.ms and to the target of $SbeEndpointUri. To determine the redirection target, browse to $SbeEndpointUri and note the URL that it redirects to in your browser address bar." } } } return $EndpointAccessResult } function Invoke-SBEHealthCheckWithPrerequisites { <# .SYNOPSIS Executes SBE health check with required prerequisite steps (integrity check and module import). .DESCRIPTION This function performs the following steps in sequence: 1. Verifies SBE content integrity (if enabled) 2. Imports the SolutionExtension module 3. Executes the specified health check function This function is designed to run within a remote PowerShell job for parallel execution. .EXAMPLE Invoke-SBEHealthCheckWithPrerequisites -FunctionName 'Get-SBEHealthCheckResultOnNode' -FunctionParams @{Tag='Deployment'} -SBEWorkingDir 'C:\CloudContent\...' -SBEMetadataPath 'C:\CloudContent\...' -RunFrom 'Local' -SkipIntegrityTest $false .PARAMETER FunctionName The name of the SBE health check function to execute. .PARAMETER FunctionParams Hashtable of parameters to pass to the health check function. .PARAMETER SBEWorkingDir Path to the SBE working directory containing the SolutionExtension module. .PARAMETER SBEMetadataPath Path to the SBE metadata directory used for integrity verification. .PARAMETER RunFrom Specifies where the check is running from ('Local' or 'CSV'). .PARAMETER SkipIntegrityTest If true, skips the SBE content integrity verification. .PARAMETER SbeRoleHelpersPath Optional pre-resolved path to the $sbeRoleNuget\content\Helpers directory. When provided, avoids calling Get-ASArtifactPathLite on each node, which eliminates the NuGet provider bootstrap cost. If not provided, falls back to resolving the path via Get-ASArtifactPathLite. #> [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string]$FunctionName, [Parameter(Mandatory=$true)] [hashtable]$FunctionParams, [Parameter(Mandatory=$true)] [string]$SBEWorkingDir, [Parameter(Mandatory=$true)] [string]$SBEMetadataPath, [Parameter(Mandatory=$true)] [string]$RunFrom, [Parameter(Mandatory=$false)] [bool]$SkipIntegrityTest = $false, [Parameter(Mandatory=$false)] [string]$SbeRoleHelpersPath = $null ) try { # Verify SBE content integrity (if enabled) if ("Local" -eq $RunFrom -and -not $SkipIntegrityTest) { # Use pre-resolved helpers path if provided to avoid per-node NuGet provider bootstrap cost. # Fall back to Get-ASArtifactPathLite if no path was supplied. if (-not [string]::IsNullOrEmpty($SbeRoleHelpersPath)) { $sbeHelpersDir = $SbeRoleHelpersPath } else { $sbeRoleNuget = Get-ASArtifactPathLite -NugetName "Microsoft.AzureStack.Role.SBE" $sbeHelpersDir = "$sbeRoleNuget\content\Helpers" } Import-Module "$sbeHelpersDir\SBESolutionExtensionHelper.psm1" -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null $skipDir = @("IntegratedContent") $integrityResult = Test-SBEContentIntegrity -SBEMetadataDirPath $SBEMetadataPath -SBEContentPath $SBEWorkingDir -IgnoreTopLevelFolder $skipDir if ($false -eq $integrityResult) { throw "SBE content integrity check found irregularities in the files at '$SBEWorkingDir' on '$env:COMPUTERNAME'" } } # Import the SolutionExtension module $solExtModule = Join-Path -Path $SBEWorkingDir -ChildPath "Configuration\SolutionExtension\SolutionExtension.psd1" Import-Module $solExtModule -Force -ErrorAction Stop -Verbose:$false -DisableNameChecking -Global | Out-Null # Execute the health check function & $FunctionName @FunctionParams } catch { # Return error information that can be captured by the parent throw $PSItem } } function Wait-SBECopyNodeBatch { <# .SYNOPSIS Processes results from a batch of parallel SBE content copy jobs. .DESCRIPTION Waits for a batch of PowerShell jobs that copy SBE content to nodes, processes their results, and returns a structured output object. On first failure, all remaining jobs are cleaned up (by design) to fail fast - their completed status is not captured. .PARAMETER Jobs Array of PowerShell jobs started by Start-Job for copying SBE content to nodes. .PARAMETER FunctionName The test name used when creating result objects for failures. .OUTPUTS Hashtable with keys: - Results : Array of result objects (populated on failure) - FirewallRulesChanged : Hashtable of node name to firewall rules changed by successful jobs - HasError : Boolean, true if any job failed - ErrorMessage : The error message from the first failure, if HasError is true #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [object[]] $Jobs, [Parameter(Mandatory = $true)] [string] $FunctionName ) $output = @{ Results = @() FirewallRulesChanged = @{} HasError = $false ErrorMessage = '' } $Jobs | Wait-Job | Out-Null foreach ($job in $Jobs) { $jobResult = Receive-Job -Job $job -ErrorAction SilentlyContinue $nodePrefix = if ($jobResult.ComputerName) { "[$($jobResult.ComputerName)]" } else { "[$($job.Location)]" } if ($null -ne $jobResult) { foreach ($msg in $jobResult.Messages) { Log-Info "$nodePrefix $msg" } if ($jobResult.Success) { Log-Info " Successfully copied SBE content to '$($jobResult.ComputerName)'" if ($jobResult.FirewallRulesChanged.Count -gt 0) { foreach ($node in $jobResult.FirewallRulesChanged.Keys) { $output.FirewallRulesChanged[$node] = $jobResult.FirewallRulesChanged[$node] } } } else { $errorMsg = $jobResult.Error if ([string]::IsNullOrEmpty($errorMsg)) { $errorMsg = "Unknown error during copy operation" } Log-Info -Message " An unhandled error occurred during 'Copy-SBEContentLocalToNode' to '$($jobResult.ComputerName)'" -Type Error -ConsoleOut Log-Info -Message (" The exception message was: $errorMsg") -Type Error -ConsoleOut $exceptionResult = New-SBEHealthResultObject -TestName $FunctionName -TargetName $jobResult.ComputerName -Status 'FAILURE' -Severity 'CRITICAL' -Description "Copy-SBEContentLocalToNode to '$($jobResult.ComputerName)'" $exceptionResult.AdditionalData.Detail = $errorMsg $output.Results += $exceptionResult # By design: on first failure, remove ALL jobs (including completed but unprocessed) # and return immediately. Remaining completed jobs' results are intentionally discarded. $Jobs | Remove-Job -Force -ErrorAction SilentlyContinue $output.HasError = $true $output.ErrorMessage = $errorMsg return $output } } else { Log-Info -Message " $nodePrefix No result received from job for node (job may have failed to start or return data)" -Type Warning -ConsoleOut } } $Jobs | Remove-Job -Force -ErrorAction SilentlyContinue return $output } Export-ModuleMember -Function Test-* Export-ModuleMember -Function New-SBEHealthResultObject Export-ModuleMember -Function Get-ASArtifactPathLite Export-ModuleMember -Function Get-ManifestMatchesModelandSKUResult Export-ModuleMember -Function Get-SBEHealthCheckParams Export-ModuleMember -Function Copy-SBEContentLocalToNode Export-ModuleMember -Function Import-SolutionExtensionModule Export-ModuleMember -Function Assert-ResponseSchemaValid Export-ModuleMember -Function Invoke-TestSBEContentIntegrity Export-ModuleMember -Function Invoke-SBEHealthCheckWithPrerequisites Export-ModuleMember -Function Wait-SBECopyNodeBatch # SIG # Begin signature block # MIIncQYJKoZIhvcNAQcCoIInYjCCJ14CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDsiLZysG79OVht # uZqmZA02OpNB2meRQpsM3BLAvNH2rqCCDMkwggYEMIID7KADAgECAhMzAAACHPrN # xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD # b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1 # OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD # VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB # DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP # oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC # /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf # rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j # qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT # xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B # Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O # BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL # EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT # DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw # YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w # cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy # bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl # MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC # AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN # rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK # 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK # Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY # BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu # uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE # msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz # 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6 # U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO # 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD # 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC # EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS # b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX # DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ # Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq # lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo # 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv # QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a # 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1 # FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO # GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7 # ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ # uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS # CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm # VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3 # SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E # BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX # LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB # Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP # oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv # TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw # TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv # TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC # AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D # 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY # nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI # vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6 # aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w # PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7 # RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK # /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK # YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw # YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT # Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghn+MIIZ+gIBATBu # MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc # +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwG # CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI # hvcNAQkEMSIEIKUwRhFY73z4D/LcwTOi0ecFGZN81TZHAIrzRcj1IsyWMEIGCisG # AQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAYP5flA7Q55hMGW4PtFRA # qxeS5AcV3vsqAfGJovGQ/8aG6fCOuYRLBdM/FlTzy2DiC7+aH/y4R0dEsNIaeiJw # rbBTE9y0W7jj2GA68ZLW/lOKE/sMVi5gxyEsqB3Jm8apWfDRdsz7muQd9aI4wXjC # 9OO4RXUCzAHc1YooZD9u1CqHGlX51889J3bSvaA4B5B+YntvV1MqMtdVLSm9ws7x # NVpPlzjE6qrzH6/w4uj7E4ojxcuBViciGeSCa5VR+Na1wvo3f3lcAAbS465X8jIK # XvkMbWIY3/IZPch7HbXdlfoiXn+NUkS0zMxcgIXFlLqJ7hdU7dZ9fXcMwCEFhx79 # EqGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kwgheF # AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFFMIIB # QQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCDu9huqwAk747X53RtV # +9qUo72kPenNl1H9C2ZORcyLIQIGaeugEigEGBMyMDI2MDUwMzE0MzExMC4xODVa # MASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0 # ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoyQTFBLTA1RTAtRDk0NzElMCMG # A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIFEKAD # AgECAhMzAAACEKvN5BYY7zmwAAEAAAIQMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m # dCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgxMloXDTI2MTExMzE4 # NDgxMlowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD # VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTAr # BgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUG # A1UECxMeblNoaWVsZCBUU1MgRVNOOjJBMUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxN # aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOC # Ag8AMIICCgKCAgEAjcc4q057ZwIgpKu4pTXWLejvYEduRf+1mIpbiJEMFWWmU2xp # ip+zK7xFxKGB1CclUXBU0/ZQZ6LG8H0gI7yvosrsPEI1DPB/XccGCvswKbAKckng # OuGTEPGk7K/vEZa9h0Xt02b7m2n9MdIjkLrFl0pDriKyz0QHGpdh93X6+NApfE1T # L24Vo0xkeoFGpL3rX9gXhIOF59EMnTd2o45FW/oxMgY9q0y0jGO0HrCLTCZr50e7 # TZRSNYAy2lyKbvKI2MKlN1wLzJvZbbc//L3s1q3J6KhS0KC2VNEImYdFgVkJej4z # ZqHfScTbx9hjFgFpVkJl4xH5VJ8tyJdXE9+vU0k9AaT2QP1Zm3WQmXedSoLjjI7L # WznuHwnoGIXLiJMQzPqKqRIFL3wzcrDrZeWgtAdBPbipglZ5CQns6Baj5Mb6a/EZ # C9G3faJYK5QVHeE6eLoSEwp1dz5WurLXNPsp0VWplpl/FJb8jrRT/jOoHu85qRcd # YpgByU9W7IWPdrthmyfqeAw0omVWN5JxcogYbLo2pANJHlsMdWnxIpN5YwHbGEPC # uosBHPk2Xd9+E/pZPQUR6v+D85eEN5A/ZM/xiPpxa8dJZ87BpTvui7/2uflUMJf2 # Yc9ZLPgEdhQQo0LwMDSTDT48y3sV7Pdo+g5q+MqnJztN/6qt1cgUTe9u+ykCAwEA # AaOCAUkwggFFMB0GA1UdDgQWBBSe42+FrpdF2avbUhlk86BLSH5kejAfBgNVHSME # GDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRw # Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1l # LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsG # AQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p # Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMB # Af8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDAN # BgkqhkiG9w0BAQsFAAOCAgEAvs4rO3oo8czOrxPqnnSEkUVq718QzlrIiy7/EW7J # mQXsJoFxHWUF0Ux0PDyKFDRXPJVv29F7kpJkBJJmcQg5HQV7blUXIMWQ1qX0KdtF # QXI/MRL77Z+pK5x1jX+tbRkA7a5Ft7vWuRoAEi02HpFH5m/Akh/dfsbx8wOpecJb # YvuHuy4aG0/tGzOWFCxMMNhGAIJ4qdV87JnY/uMBmiodlm+Gz357XWW5tg3HrtNZ # XuQ0tWUv26ud4nGKJo/oLZHP75p4Rpt7dMdYKUF9AuVFBwxYZYpvgk12tfK+/yOw # q84/fjXVCdM83Qnawtbenbk/lnbc9KsZom+GnvA4itAMUpSXFWrcRkqdUQLN+JrG # 6fPBoV8+D8U2Q2F4XkiCR6EU9JzYKwTuvL6t3nFuxnkLdNjbTg2/yv2j3WaDuCK5 # lSPgsndIiH6Bku2Ui3A0aUo6D9z9v+XEuBs9ioVJaOjf/z+Urqg7ESnxG0/T1dKc # i7vLQ2XNgWFYO+/OlDjtGoma1ijX4m14N9qgrXTuWEGwgC7hhBgp3id/LAOf9BST # WA5lBrilsEoexXBrOn/1wM3rjG0hIsxvF5/YOK78mVRGY6Y7zYJ+uXt4OTOFBwad # Pv8MklreQZLPnQPtiwop4rlLUYaPCiD4YUqRNbLp8Sgyo9g0iAcZYznTuc+8Q8ZI # rgwwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEB # CwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD # VQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAe # Fw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGm # TOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/H # ZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDc # wUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62A # W36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1w # jjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCG # MFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ # 1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP # 8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFz # ymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHz # NgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3 # xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsG # AQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/ # LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEG # DCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYB # BQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8G # A1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQw # VgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9j # cmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUF # BwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br # aS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQEL # BQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfC # cTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AF # vonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l # 9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn # 8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5m # O0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyx # TkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4 # S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9 # y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM # +Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhw # RNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCCAkEC # AQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0 # ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoyQTFBLTA1RTAtRDk0NzElMCMG # A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIa # AxUAOsyf2b6riPKnnXlIgIL2f53PUsKggYMwgYCkfjB8MQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1T # dGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO2hUrgwIhgPMjAyNjA1MDMw # NDUxMDRaGA8yMDI2MDUwNDA0NTEwNFowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA # 7aFSuAIBADAKAgEAAgIP2wIB/zAHAgEAAgITyzAKAgUA7aKkOAIBADA2BgorBgEE # AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag # MA0GCSqGSIb3DQEBCwUAA4IBAQC8BD/jbO+hYI1cLwigLLq4vh+KzNiSSPNrYpjK # 81EmkYyZs5aX5AMRAnKzZrP4zsNFkviAjAjcDcR62MFounzMKMdJW5L/Ak/LXwXt # M34DfDJQcdZIA42apu7Gxus4gBy4l6dU2LN+j4ltCPCRJhdMPexSSf+OQbx8kO01 # Je3+DWhdgn9pdujhIj8ifSldthlXtNLStB9fWFll8TzvJx6wr8KKMuvcau4DRbnp # b8VFIDpJYNDEwTAhl9aTGcKSTtPlNK0OXC56AgmwyMbDcy10gMlKbnqlX+/gE2Na # W74yfYadFJ7YPleyZRz57qTs137kpL9OZd3T9tyb2gqNzRj0MYIEDTCCBAkCAQEw # gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIQq83kFhjvObAA # AQAAAhAwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B # CRABBDAvBgkqhkiG9w0BCQQxIgQgx3nN7dbkLCUV3AAG6KRPHxc54ClIOOjR+Hk6 # CCBEoj0wgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDD1SHufsjzY59S1iHU # QY9hnsKSrJPg5a9Mc4YnGmPHxjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwAhMzAAACEKvN5BYY7zmwAAEAAAIQMCIEIIT70NF/aOTBRnL/Ym5d # CJI5L5fhiGpS2xzTAo0XyaBPMA0GCSqGSIb3DQEBCwUABIICADveX8AcCUYV2tya # QTQLFG0UZxCft4NKBBik07su+TTiegTTeNgkIJWQ63eOgttruUO75jmZfhsoN1LE # Dly6oGBtqgTovoetGj60VsL0gKuSMK7IPqlYiM8Hz2nX5hpXotp8xJBKZ0Y+XZxc # 5A/tnFeM3l1iaUNUY5iYLuu0p5UQLReer1NcrpvRtOj1ntANG0G44Yxz9Q4I/rsU # CpZL92DFS5aiQZdcZT8PPdP++/p8fj/LHctE5SncbQZWcfSY5DQ38+k7bwol/ZfN # KAEMLmQCWrnOtB06BwKyIK2eQo74KzxC1yKSEvm16M1v59M8oOr5VYi1cbb8PNpf # ziOQNIlVXbmYxfN6IZUJOK9XtNCaabkIA1HMY7z3sP+Ql/Mwo1Uh9KdASq6FGA// # kdSkCN35+doKAvRputekOa8cv/RCib+mfpz7MbkivGSQs78+tc33ZPcwsmElH8M0 # +I7MHRYswq2X/YvoacpIgmJG6dALGLpajhYnPxcSPuJ41zOeCICPK7Ugmp7CtpM8 # B9QvlmbvcQeBx9MshZ5IvGa+iMw+rw8ezaiqlGTmp53JrA2cLdVnE2FqLuJouJf9 # Dfrx+LYhIsdfLU2N2DWozyhMi8eOuHaF2T7OLM4NRaB9MMIAJyhqWQ7DlhJWYPYT # DRE3L1rBcKJMhjSxf+Dsc0BtPQTD # SIG # End signature block |