AzStackHciOSImageRecipe/AzStackHci.OSImageRecipe.Helpers.psm1
Import-LocalizedData -BindingVariable lswTxt -FileName AzStackHci.OSImageRecipe.Strings.psd1 $global:recipeFilePath = "$env:windir/System32/AzureLocalImage/OSImageRecipe.xml" $renderXMLvarScriptBloc = { <# .SYNOPSIS A script block that can be passed to another script block that happens to be running remotely. This is a cascaded script blocks concept. .DESCRIPTION The point of this script block is to validate that the Azure Local version is one that should have a local OS image recipe file on it. If it is and that file exists return a data structure that represents that XML file. #> Param($recipeFilePath) function isOfficialBuild { <# Return $true if the registry tells us this is an official build. Return $false if the registry tells us this is NOT an official build. Return $null if the we fail to find OFFICIAL_BUILD in the ComposedBuildInfo registry location. The shouldHaveRecipeFile() function should be called before isOfficialBuild(). This is assure that we at least expect the hosts is from a composed build and that we should expect the registry to contain this information. #> try { $rawIsOfficial = (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\ComposedBuildInfo\Parameters).OFFICIAL_BUILD } catch { $rawIsOfficial = $null } if ($null -eq $rawIsOfficial) { return $null } elseif ([int]$rawIsOfficial) { return $true } else { return $false } } function validateRecipeFileExistsAndIsNotCorrupt { <# Return the XML data structure if it renders correctly from the file. Return $false if the file simply does not exists OR if the file does not render a valid data structure and therefore considered corrupt. #> if (Test-Path -Path $recipeFilePath) { try { [xml]$recipeObj = Get-Content $recipeFilePath return $recipeObj } catch { # return False because although the XML file existed it appears to be corrupt return $false } } else { # return False because the xml should exist on this host, but it does not return $false } } function validateRecipeIsSigned { <# Validate that the signature in the recipe matches that particular file and that the file contents have not been manually manipulated. Return $true if the onbox xml recipe file is correctly signed. Return $false if the onbox xml recipe file is NOT correctly signed. isOfficialBuild() should be run before this function because only official builds contain signed recipe files and checking to see if it is an official build first guards the logic found in this func. #> Param($recipeFilePath) $recipeFilePath = $recipeFilePath -replace '\\|/', '=' [string]$recipeFilePath = [string]$recipeFilePath -replace '=', '\\' $contentPath = (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\EdgeArcBootstrapSetup\' -ErrorAction SilentlyContinue).ContentBinariesPath if (([System.String]::IsNullOrEmpty($contentPath)) -or (!(Test-Path $contentPath))) { $bootstrapDirectory = Get-ChildItem -Path "C:/windows/system32/Bootstrap" | Where-Object { $_.Name -like "*content*" } | Sort-Object -Property Name -Descending if ($null -ne $bootstrapDirectory) { $contentPath = $bootstrapDirectory[0].FullName } } if ([System.String]::IsNullOrEmpty($contentPath)) { return $false } $validatorDll = "$contentPath/Microsoft.AzureStack.UpdateService.BootstrapValidation" $validatorDll += "/lib/net472/Microsoft.AzureStack.Services.Update.ResourceProvider.UpdateService.Security.dll" Import-Module $validatorDll $validatorInstance = [Microsoft.AzureStack.Services.Update.ResourceProvider.UpdateService.Security.SignedXmlValidator]::new() if (Test-Path -Path $recipeFilePath) { $isSigned = $validatorInstance.ValidateAsync("$recipeFilePath").GetAwaiter().GetResult() if ($isSigned) { return $true } return $false } return $false } function getOSBuildVersion { <# .SYNOPSIS Return a version object that represents the live installed version. (HKLM:\SYSTEM\CurrentControlSet\Services\ComposedBuildInfo\Parameters).COMPOSED_BUILD_ID NOTE: this func is scoped within a scriptBlock called by a scriptBlock .DESCRIPTION Discover the 'COMPOSED_BUILD_ID' version string. Convert the string into a [version] module object. For example if the string is '10.2502.0.6250' the object looks like this: Major Minor Build Revision ----- ----- ----- -------- 10 2502 0 6205 #> $installedVersionString = $null try { $installedVersionString = (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\ComposedBuildInfo\Parameters).COMPOSED_BUILD_ID if (-not $installedVersionString) { throw 'COMPOSED_BUILD_ID is not set' } $installedVersionString = $installedVersionString.split('.')[0..3] -join'.' # make sure no more than 4 chunks separated by periods because [Version]::Parse() does not like it $installedVersionString = [regex]::Replace($installedVersionString, '-\d*', '') # remove any hyphenated subversion string because [Version]::Parse() does not like it #assume subversion are backward and forward compatible with versions of similar major version } catch { # if the registry entry does not exist then this HCI instance can be considered a # nonComposed one and thus just set the version to 0 # this will cause the downstream logic to skip the tests instead of fail them and # thus maintaining the integrity of the EnvironmentChecker for composed HCI # and the older nonComposed HCI. $installedVersionString = '0.0.0.0' } try { $installedVersionObj = [Version]::Parse($installedVersionString) } catch { $installedVersionString = '0.0.0.0' $installedVersionObj = [Version]::Parse($installedVersionString) } return $installedVersionObj } function shouldHaveRecipeFile { <# .SYNOPSIS Return True if the image running on the local host is an HCI image from an era that contained an embedded recipe XML file. NOTE: this func is scoped within a scriptBlock called by a scriptBlock .DESCRIPTION Check that the 'COMPOSED_BUILD_ID' version running on the local live host is >= 10.2502.0.3017. This minimum version is just a line in the sand when composed images started to contain the recipe xml file. Return True if the installed version is >= the minimum version. If True is returned the live host *should* have a valid recipe file. If False is returned then this live host should *not* have a recipe file. #> $minVersionString = '10.2502.0.0' $minVersionObj = [Version]::Parse($minVersionString) $installedVersionObj = getOSBuildVersion if ($installedVersionObj -ge $minVersionObj) { return $true } return $false } # The renderXMLvarScriptBloc can return 3 explicit things. # Each of these indicate a different outcome. # $recipeObj == This image should contain a recipe file # and the returned value is a data structure # representing the XML content in the recipe file. # $False == This image should contain a recipe file, but does NOT! # $Null == This image should NOT contain a recipe file, thus it # is OK to skip testing against it. # This Obj vs False vs Null response takes into account if the recipe file is signed # or not, but only if this live hosts was born from the official composed # image pipeline. If it came from the buddy pipeline do not take # into account if the recipe is signed or not. # Return False if this is an Official build and it is NOT signed. # This False response will correctly induce recipe validation test failures. if ($(shouldHaveRecipeFile)) { $isOfficial = $(isOfficialBuild) if ($isOfficial -or ($null -eq $isOfficial)) { if (-not $(validateRecipeIsSigned $recipeFilePath)) { # return False because this is an official build and the recipe file is not signed return $false } } return $(validateRecipeFileExistsAndIsNotCorrupt) } else { return $null } } # end renderXMLvarScriptBloc function TestResult { <# .SYNOPSIS Build up a params data structure to pass the New-AzStackHciResultObject function. .DESCRIPTION The New-AzStackHciResultObject function get information about the result of a test into the correct result files (log, json, etc.) #> Param([Parameter(Mandatory=$true,Position=0)] [array]$responses, [Parameter(Mandatory=$true,Position=1)] [string]$Name, [Parameter(Mandatory=$true,Position=2)] [string]$Title, [Parameter(Mandatory=$true,Position=3)] [string]$DisplayName, [Parameter(Mandatory=$true,Position=4)] [string]$Severity, [Parameter(Mandatory=$true,Position=5)] [string]$Description, [Parameter(Mandatory=$false,Position=6)] [string]$Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-install-os', [Parameter(Mandatory=$false,Position=7)] [string]$TargetResourceType = 'OSImageRecipe', [Parameter(Mandatory=$false,Position=8)] [string]$Resource = 'OS Image Recipe') $instanceResults = @() foreach ($response in $responses) { $detailString = $($response.details) -join '; ' foreach ($msg in $($response.logLines)) { $msgArray = $msg.split('|') Log-Info $msgArray[0] -Type $msgArray[1] } try { $Status = 'SUCCESS' if ($($response.rc)) { $Status = 'FAILURE' } $params = @{ Name = $Name Title = $Title DisplayName = $DisplayName Severity = $Severity Description = $Description Tags = @{} Remediation = $Remediation TargetResourceID = $($response.computername) TargetResourceName = $($response.computername) TargetResourceType = $TargetResourceType Timestamp = [datetime]::UtcNow Status = $status HealthCheckSource = $ENV:EnvChkrId AdditionalData = @{Source = $($response.computername) Resource = $Resource Detail = $detailString Status = $status TimeStamp = [datetime]::UtcNow}} $instanceResults += New-AzStackHciResultObject @params } catch { throw $_ } } return $instanceResults } function Test-InstalledPackages { <# .SYNOPSIS Validate that all the packages installed, including the bootStrap package, have the same version the OS image recipe requires. .DESCRIPTION Loop around the package defined in OSImageRecipe.xml and validate using 'Get-InstalledModule' that the version installed matches the version defined in the recipe XML file. This function used to use Get-Package to check to see if 'packages' as defined in the recipe were installed on a system with the correct version. Get-Package was used because it is a more generic cmdline tool than Get-InstalledModule. Get-Package would allow the user to see PowerShell modules install and other types of packages. As of 01/09/2025 the recipe does not contain any non-PowerShell packages other than The bootStrap one. BootStrap is already special cased in this test. Once there are non-PowerShell packages in the recipe we need to define what kind of package. #> [CmdletBinding()] Param([Parameter()] [System.Management.Automation.Runspaces.PSSession[]] $PsSession) $sb = { Param($recipeFilePath, $renderXML, $lswTxt, $resultSeverity) $rc = 0 $localHost = $ENV:ComputerName $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] $packageObj = New-Object System.Collections.Generic.List[System.Object] $innerSB = [ScriptBlock]::Create($renderXML) $XMLstructure = & $innerSB $recipeFilePath if ($XMLstructure) { if ($($XMLstructure.SelectNodes('/BuildInfo/Packages/Package'))) { $packageObj = $XMLstructure.BuildInfo.Packages.Package } } else { if ($false -eq $XMLstructure) { $rc += 1 $msg = $lswTxt.failTestMissingRecipe $res = $resultSeverity } else { $msg = $lswTxt.skipTestMissingRecipe $res = 'SUCCESS' } $detailList.Add($msg) $logLines.Add("${msg}|${res}") } # the only thing we skip for now is the EC itself since how could we be running this code # unless EC is installed. We will run the ver validation that it is the right EC ver # during build time, but in the field just skip validating that EC is installed for now # this was mainly done because otherwise the CI runs that use ALLNUGETSHARES as a way to # install a specific EC version will fail every time $packagesToSkip = @('AzStackHci.EnvironmentChecker') foreach ($pkg in $packageObj) { $name = $pkg.Name $ver = $pkg.Version try { $ver = $ver.split('.')[0..3] -join'.' # make sure no more than 4 chunks separated by periods because [Version]::Parse() does not like it $ver = [regex]::Replace($ver, '-\d*', '') # remove any hyphenated subversion string because [Version]::Parse() does not like it #assume subversion are backward and forward compatible with versions of similar major version } catch { $ver = 'N/A' } if ($packagesToSkip -contains $name) { continue } if ($name -notlike '*Bootstrap.Setup*') { try { $liveHostVer = ([string]((Get-InstalledModule -Name ${name} -ErrorAction SilentlyContinue).Version)).trim() $liveHostVer = $liveHostver.split('.')[0..3] -join'.' # make sure no more than 4 chunks separated by periods because [Version]::Parse() does not like it $liveHostver = [regex]::Replace($liveHostver, '-\d*', '') # remove any hyphenated subversion string because [Version]::Parse() does not like it } catch { $liveHostVer = 'N/A' } if (-not $liveHostVer) { $liveHostVer = 'N/A' } try { $liveHostVerObj = [Version]::Parse($liveHostVer) } catch { $liveHostVerObj = $null } try { $verObj = [Version]::Parse($ver) } catch { $verObj = $null } if ($liveHostVer -eq 'N/A') { $rc += 1 $msg = $lswTxt.InstalledPackagesNotInstalled -f $name, $localHost $logLines.Add("${msg}|${resultSeverity}") } elseif (-not $liveHostVerObj) { $rc += 1 $msg = $lswTxt.InstalledPackagesLiveHostVerConversionFail -f $name, $localHost $logLines.Add("${msg}|${resultSeverity}") } elseif (-not $verObj) { $rc += 1 $msg = $lswTxt.InstalledPackagesRecipeVerConversionFail -f $name, $localHost $logLines.Add("${msg}|${resultSeverity}") } elseif ($liveHostVerObj -lt $verObj) { $rc += 1 $msg = $lswTxt.InstalledPackagesFail -f $name, $localHost, $liveHostVer, $ver $logLines.Add("${msg}|${resultSeverity}") } else { # this assumes that ($liveHostVerObj -ge $verObj) $msg = $lswTxt.InstalledPackagesPass -f $name, $localHost, $liveHostVer, $ver $logLines.Add("${msg}|SUCCESS") } $detailList.Add($msg) } else { # checking both if the 'InstallCompleted' AND the version string exist # this is done because in the past I have seen bugs that prevented the bootstrap package from # successfully installing, but the version string was there. # It is best to validate that both bits of information exists and not just the version string! $outString = Get-Item -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\EdgeArcBootstrapSetup\' -ErrorAction SilentlyContinue |Out-String if (-not [regex]::match($outString, 'InstallCompleted\s+:\s+1').Success) { $rc += 1 $msg = $lswTxt.InstalledPackagesBootstrapInstalledFail -f $name, $localHost $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.InstalledPackagesBootstrapInstalledPass -f $name, $localHost $logLines.Add("${msg}|SUCCESS") } $detailList.Add($msg) if (-not [regex]::match($outString, $ver).Success) { $rc += 1 $msg = $lswTxt.InstalledPackagesBootstrapVersionFail -f $name, $localHost, $ver $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.InstalledPackagesBootstrapVersionPass -f $name, $localHost, $ver $logLines.Add("${msg}|SUCCESS") } $detailList.Add($msg) } } $response = "" | Select-Object -Property rc, details, computername, logLines $response.rc = $rc $response.details = $detailList $response.computername = $localHost $response.logLines = $logLines return $response } #endSB $resultSeverity = "CRITICAL" $responses = if ($psSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } else { Invoke-Command -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } $splat = @{responses = $responses Name = "AzStackHci_OSImageRecipeValidation_Package_Version" Title = "Installed Packages match recipe." DisplayName = "Installed Packages match recipe." Severity = $resultSeverity Description = "Validating that the packages installed on the host are the same versions defined in the OS image recipe."} return (TestResult @splat) } function Test-InstalledAdditionalFiles { <# .SYNOPSIS Validate that all the 'additional files' defined in the OS image recipe match those installed on the system. .DESCRIPTION Loop around the additionFiles defined in OSImageRecipe.xml and validate they exit on the system using '[System.IO.File]::Exists()'. #> [CmdletBinding()] Param([Parameter()] [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) $sb = { Param($recipeFilePath, $renderXML, $lswTxt, $resultSeverity) $rc = 0 $localHost = $ENV:ComputerName $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] $fileList = New-Object System.Collections.Generic.List[System.Object] $innerSB = [ScriptBlock]::Create($renderXML) $XMLstructure = & $innerSB $recipeFilePath if ($XMLstructure) { if ($($XMLstructure.SelectNodes('/BuildInfo/AdditionalFiles/File/DestinationPath'))) { $fileList = $XMLstructure.BuildInfo.AdditionalFiles.File.DestinationPath } } else { if ($false -eq $XMLstructure) { $rc += 1 $msg = $lswTxt.failTestMissingRecipe $res = ${resultSeverity} } else { $msg = $lswTxt.skipTestMissingRecipe $res = 'SUCCESS' } $detailList.Add($msg) $logLines.Add("${msg}|${res}") } $fileList |foreach-object { $localFilePath = $_ $filePath = "${localFilePath}" #$logLines.Add("Validate that additional file [${filePath}] is installed on host.|INFO") if (-not [System.IO.File]::Exists(${filePath})) { $rc += 1 $msg = $lswTxt.InstalledFilesFail -f $filePath, $localHost $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.InstalledFilesPass -f $filePath, $localHost $msg += ' :: ' $msg += ([System.IO.File]::GetCreationTime(${localFilePath}) |Out-string).trim() $msg += ' :: ' $msg += ((([System.IO.File]::GetAccessControl(${localFilePath})).Access)[0]).FileSystemRights $logLines.Add("${msg}|SUCCESS") } $detailList.Add($msg) } $response = "" | Select-Object -Property rc, details, computername, logLines $response.rc = $rc $response.details = $detailList $response.computername = $localHost $response.logLines = $logLines return $response } #endSB $resultSeverity = "CRITICAL" $responses = if ($psSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } else { Invoke-Command -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } $splat = @{responses = $responses Name = "AzStackHci_OSImageRecipeValidation_Additional_Installed_Files" Title = "Installed Files match recipe." DisplayName = "Installed Files match recipe." Severity = $resultSeverity Description = "Validating all additional files exist on the live host that are defined in the OS image recipe."} return (TestResult @splat) } function Test-BaseOSimage { <# .SYNOPSIS Validate that the system has a supported base OS version installed as defined in the OS image recipe file. .DESCRIPTION Validate that the Edition string and the Version string installed match what is in the OS image recipe. Use the following PS commands to perform this validation: Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' | Select-Object -Property LCUVer).LCUVer (Get-ComputerInfo | Select-Object WindowsEditionId).WindowsEditionId #> [CmdletBinding()] Param([Parameter()] [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) $sb = { Param($recipeFilePath, $renderXML, $lswTxt, $resultSeverity) $rc = 0 $localHost = $ENV:ComputerName $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] $recipeBaseOSverString = $null $recipeEditionString = $null $innerSB = [ScriptBlock]::Create($renderXML) $XMLstructure = & $innerSB $recipeFilePath if ($XMLstructure) { $recipeBaseOSverString = $XMLstructure.BuildInfo.SupportedVersions.Version.Build # validate only the first 3 chunks of the version string as the last chunk represents # the LCU (HotFixes) and those differ post composed image creation $recipeBaseOSverString = ($recipeBaseOSverString.Split('.')[0..2]) -join('.') $recipeEditionString = $XMLstructure.BuildInfo.SupportedVersions.Version.Edition } else { if ($false -eq $XMLstructure) { $rc += 1 $msg = $lswTxt.failTestMissingRecipe $res = $resultSeverity } else { $msg = $lswTxt.skipTestMissingRecipe $res = 'SUCCESS' } $detailList.Add($msg) $logLines.Add("${msg}|${res}") } if ($recipeBaseOSverString -and $recipeEditionString) { try { $installedBaseOSverString = [string](Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' | Select-Object -Property LCUVer).LCUVer } catch { $installedBaseOSverString = 'N.A.0.0' } # validate only the first 3 chunks of the version string as the last chunk represents the LCU (HotFixes) and those differ post composed image creation $installedBaseOSverString = ($installedBaseOSverString.Split('.')[0..2]) -join('.') try { $installedEditionString = (Get-ComputerInfo | Select-Object WindowsEditionId).WindowsEditionId } catch { $installedEditionString = 'N/A' } #$logLines.Add("Validate that the base OS installed on host [${installedBaseOSverString}] matches recipe version [${recipeBaseOSverString}].|INFO") if ($recipeBaseOSverString -ne $installedBaseOSverString) { $rc += 1 $msg = $lswTxt.BaseOSimageVersionFail -f $localHost, $recipeBaseOSverString, $installedBaseOSverString $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.BaseOSimageVersionPass -f $localHost, $recipeBaseOSverString, $installedBaseOSverString $logLines.Add("${msg}|SUCCESS") } $detailList.Add($msg) if ($recipeEditionString -ne $installedEditionString) { $rc += 1 $msg = $lswTxt.BaseOSimageEditionStringFail -f $localHost, $recipeEditionString, $installedEditionString $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.BaseOSimageEditionStringPass -f $localHost, $recipeEditionString, $installedEditionString $logLines.Add("${msg}|SUCCESS") } } $detailList.Add($msg) $response = "" | Select-Object -Property rc, details, computername, logLines $response.rc = $rc $response.details = $detailList $response.computername = $localHost $response.logLines = $logLines return $response } #endSB $resultSeverity = "CRITICAL" $responses = if ($psSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } else { Invoke-Command -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } $splat = @{responses = $responses Name = "AzStackHci_OSImageRecipeValidation_Base_OS" Title = "BaseOS matches recipe." DisplayName = "BaseOS matches recipe." Severity = $resultSeverity Description = "Validating that the base OS version string and edition string installed on the host match the OS image recipe."} return (TestResult @splat) } function Test-BuildId { <# .SYNOPSIS Validate that the system has a version that matches the one defined in the OS image recipe file. .DESCRIPTION Validate that the version burned into the registry matches what is in the OS image recipe. Use the following PS commands to perform this validation: (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\ComposedBuildInfo\Parameters).COMPOSED_BUILD_ID #> [CmdletBinding()] Param([Parameter()] [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) $sb = { Param($recipeFilePath, $renderXML, $lswTxt, $resultSeverity) $rc = 0 $localHost = $ENV:ComputerName $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] $recipeBuildId = $null $innerSB = [ScriptBlock]::Create($renderXML) $XMLstructure = & $innerSB $recipeFilePath if ($XMLstructure) { $recipeBuildId = $XMLstructure.BuildInfo.BuildId } else { if ($false -eq $XMLstructure) { $rc += 1 $msg = $lswTxt.failTestMissingRecipe $res = ${resultSeverity} } else { $msg = $lswTxt.skipTestMissingRecipe $res = 'SUCCESS' } $detailList.Add($msg) $logLines.Add("${msg}|${res}") } if ($recipeBuildId) { try { $installedBuildID = [string]((Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\ComposedBuildInfo\Parameters).COMPOSED_BUILD_ID).trim() } catch { $installedBuildID = 'N/A' } #$logLines.Add("Validate that OS image version [${recipeBuildId}] is installed on host.|INFO") if ($recipeBuildId -ne $installedBuildID) { $rc += 1 $msg = $lswTxt.BuildIdFail -f $localHost, $recipeBuildId, $installedBuildID $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.BuildIdPass -f $localHost, $recipeBuildId, $installedBuildID $logLines.Add("${msg}|SUCCESS") } $detailList.Add($msg) } $detailList.Add($msg) $response = "" | Select-Object -Property rc, details, computername, logLines $response.rc = $rc $response.details = $detailList $response.computername = $localHost $response.logLines = $logLines return $response } #endSB $resultSeverity = "CRITICAL" $responses = if ($psSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } else { Invoke-Command -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } $splat = @{responses = $responses Name = "AzStackHci_OSImageRecipeValidation_Version" Title = "OS image version matches recipe." DisplayName = "OS image version matches recipe." Severity = $resultSeverity Description = "Validating that the OS image version installed on the host matches the OS image recipe."} return (TestResult @splat) } function Test-ComposedBuildVersion { <# .SYNOPSIS Validate that the version running on the existing Azure Local node matches the version running on a node this is about to be added. The passed in PSsession is the session to the node that is about to be added. .DESCRIPTION Validate that the version burned into the registry on the existing node matches what is burned into the registry on the node that is about to be added. Use the following PS commands to perform this validation: (Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\ComposedBuildInfo\Parameters).COMPOSED_BUILD_ID #> [CmdletBinding()] Param([Parameter()] [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) $sb = { Param($existingNodeInstalledBuildID, $existingNodeName, $lswTxt, $resultSeverity) $rc = 0 $localHost = $ENV:ComputerName $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] if ($existingNodeName -eq $localHost) { # if by chance you happen to have remote PS session to the localhost then skip this test $msg = $lswTxt.SkipTestNotNodeAdd $res = 'SUCCESS' $logLines.Add("${msg}|${res}") } else { try { $newNodeInstalledBuildID = [string]((Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\ComposedBuildInfo\Parameters).COMPOSED_BUILD_ID).trim() } catch { $newNodeInstalledBuildID = 'N/A' } #$logLines.Add("Validate that OS image version [${existingNodeInstalledBuildID}] is installed on the node you want to add to the cluster.|INFO") if ($newNodeInstalledBuildID -ne $existingNodeInstalledBuildID) { $rc += 1 $msg = $lswTxt.NodeAddBuildIdFail -f $existingNodeName, $localHost, $existingNodeInstalledBuildID, $newNodeInstalledBuildID $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.NodeAddBuildIdPass -f $existingNodeName, $localHost, $existingNodeInstalledBuildID, $newNodeInstalledBuildID $logLines.Add("${msg}|SUCCESS") } } $detailList.Add($msg) $response = "" | Select-Object -Property rc, details, computername, logLines $response.rc = $rc $response.details = $detailList $response.computername = $localHost $response.logLines = $logLines return $response } #endSB $resultSeverity = "CRITICAL" # this is where we get the buildID on the existing cluster, the buildID of the node you want to add is done in the scriptBlock Invoke-Command call below $existingNodeInstalledBuildID = [string]((Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\ComposedBuildInfo\Parameters).COMPOSED_BUILD_ID).trim() $responses = "" | Select-Object -Property rc, details, computername, logLines $existingNodeName = $ENV:ComputerName if ($PsSession) { $responses = Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList (,$existingNodeInstalledBuildID, $existingNodeName, $lswTxt, $resultSeverity) } else { # if a remote PS session is not passed in then this is NOT a nodeAdd test, so skip $rc = 0 $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] $msg = $lswTxt.SkipTestNotNodeAdd $res = 'SUCCESS' $detailList.Add($msg) $logLines.Add("${msg}|${res}") $responses = "" | Select-Object -Property rc, details, computername, logLines $responses.rc = $rc $responses.details = $detailList $responses.computername = $existingNodeName $responses.logLines = $logLines } $splat = @{responses = $responses Name = "AzStackHci_OSImageRecipeValidation_NodeAdd_Version" Title = "Cluster OS image version matches the version on the node to add." DisplayName = "Cluster OS image version matches the version on the node to add." Severity = $resultSeverity Description = "Validating that the OS image version installed on the cluster matches the version on the host to add to the cluster."} return (TestResult @splat) } function Test-SolutionVersion { <# .SYNOPSIS Validate that the system has a solution version that matches the one defined in the OS image recipe file. .DESCRIPTION Validate that the solution version burned into the registry matches what is in the OS image recipe. Use the following PS commands to perform this validation: (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\EdgeArcBootstrapSetup').VSRInfo #> [CmdletBinding()] Param([Parameter()] [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) $sb = { Param($recipeFilePath, $renderXML, $lswTxt, $resultSeverity) $rc = 0 $localHost = $ENV:ComputerName $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] $recipeSolutionVersion = $null $innerSB = [ScriptBlock]::Create($renderXML) $XMLstructure = & $innerSB $recipeFilePath if ($XMLstructure) { $recipeSolutionVersion = $XMLstructure.BuildInfo.VSRVersion } else { if ($false -eq $XMLstructure) { $rc += 1 $msg = $lswTxt.failTestMissingRecipe $res = ${resultSeverity} } else { $msg = $lswTxt.skipTestMissingRecipe $res = 'SUCCESS' } $detailList.Add($msg) $logLines.Add("${msg}|${res}") } if ($recipeSolutionVersion) { #$logLines.Add("Validate that solution version [${recipeSolutionVersion}] is installed on host.|INFO") try { $regPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\VSR' if (-not (Test-Path $regPath)) { $regPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\EdgeArcBootstrapSetup' } $vsrInfoJson = [string](Get-ItemProperty -Path $regPath).VSRInfo $installedSolutionVersion = (($vsrInfoJson | ConvertFrom-Json | Select-Object -Property SolutionVersion).SolutionVersion).trim() } catch { $installedSolutionVersion = 'N/A' } if (($recipeSolutionVersion -ne $installedSolutionVersion) -or ($installedSolutionVersion -eq 'N/A')) { $rc += 1 $msg = $lswTxt.SolutionVersionFail -f $localHost, $recipeSolutionVersion, $installedSolutionVersion $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.SolutionVersionPass -f $localHost, $recipeSolutionVersion, $installedSolutionVersion $logLines.Add("${msg}|SUCCESS") } $detailList.Add($msg) } else { $msg = $lswTxt.SkipTestMissingVSRVersion -f $localHost, $recipeSolutionVersion, $installedSolutionVersion $logLines.Add("${msg}|SUCCESS") $detailList.Add($msg) } $response = "" | Select-Object -Property rc, details, computername, logLines $response.rc = $rc $response.details = $detailList $response.computername = $localHost $response.logLines = $logLines return $response } #endSB $resultSeverity = "CRITICAL" $responses = if ($psSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } else { Invoke-Command -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } $splat = @{responses = $responses Name = "AzStackHci_OSImageRecipeValidation_Solution_Version" Title = "Solution version matches recipe." DisplayName = "Solution version matches recipe." Severity = $resultSeverity Description = "Validating that the solution version installed on the host matches the OS image recipe."} return (TestResult @splat) } function Test-FeaturesOnDemand { <# .SYNOPSIS Validate that the system has the correct FOD enabled as defined in the OS image recipe file. .DESCRIPTION Validate that the correct version of the FODs are installed. Use the following PS command to perform this validation: ((Get-WindowsCapability -Online -Name '${name}*').Name.split('~')[-1]).TrimEnd() #> [CmdletBinding()] Param([Parameter()] [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) $sb = { Param($recipeFilePath, $renderXML, $lswTxt, $resultSeverity) $rc = 0 $localHost = $ENV:ComputerName $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] $recipeFODlist = New-Object System.Collections.Generic.List[System.Object] $innerSB = [ScriptBlock]::Create($renderXML) $XMLstructure = & $innerSB $recipeFilePath if ($XMLstructure) { if ($($XMLstructure.SelectNodes('/BuildInfo/FeatureOnDemands/Feature'))) { $recipeFODlist = $XMLstructure.BuildInfo.FeatureOnDemands.Feature } } else { if ($false -eq $XMLstructure) { $rc += 1 $msg = $lswTxt.failTestMissingRecipe $res = ${resultSeverity} } else { $msg = $lswTxt.skipTestMissingRecipe $res = 'SUCCESS' } $detailList.Add($msg) $logLines.Add("${msg}|${res}") } $recipeFODlist |foreach-object { $fullNameString = $_.Name $name = ($fullNameString.split('~')[0]).TrimEnd() $ver = ($fullNameString.split('~')[-1]).TrimEnd() #$logLines.Add("Validate that FOD [${name}] is installed as version [${ver}] on host.|INFO") try { $liveHostVer = $(((Get-WindowsCapability -Online -Name "${name}*").Name.split('~')[-1]).TrimEnd()) } catch { $liveHostVer = 'N/A' } if ($liveHostVer -ne $ver) { $rc += 1 $msg = $lswTxt.FeaturesOnDemandFail -f $name, $localHost, $ver, $liveHostVer $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.FeaturesOnDemandPass -f $name, $localHost, $ver, $liveHostVer $logLines.Add("${msg}|SUCCESS") } $detailList.Add($msg) } $response = "" | Select-Object -Property rc, details, computername, logLines $response.rc = $rc $response.details = $detailList $response.computername = $localHost $response.logLines = $logLines return $response } #endSB $resultSeverity = "CRITICAL" $responses = if ($psSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } else { Invoke-Command -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } $splat = @{responses = $responses Name = "AzStackHci_OSImageRecipeValidation_FOD" Title = "Features on Demand match recipe." DisplayName = "Features on Demand match recipe." Severity = $resultSeverity Description = "Validating that the FODs installed on the host match the FODs defined in the OS image recipe."} return (TestResult @splat) } function Test-LatestCumulativeUpdate { <# .SYNOPSIS Validate that the system has the correct LCU as defined in the OS image recipe file. .DESCRIPTION Validate that the correct LCU version is installed. Use the following PS command to perform this validation: ((Get-HotFix -Id ${ver}).HotFixID).TrimEnd() #> [CmdletBinding()] Param([Parameter()] [System.Management.Automation.Runspaces.PSSession[]] $PsSession ) $sb = { Param($recipeFilePath, $renderXML, $lswTxt, $resultSeverity) $rc = 0 $localHost = $ENV:ComputerName $detailList = New-Object System.Collections.Generic.List[System.Object] $logLines = New-Object System.Collections.Generic.List[System.Object] $recipeLCUlist = New-Object System.Collections.Generic.List[System.Object] $innerSB = [ScriptBlock]::Create($renderXML) $XMLstructure = & $innerSB $recipeFilePath if ($XMLstructure) { if ($($XMLstructure.SelectNodes('/BuildInfo/LCUs/LCU'))) { $recipeLCUlist = $XMLstructure.BuildInfo.LCUs.LCU } } else { if ($false -eq $XMLstructure) { $rc += 1 $msg = $lswTxt.failTestMissingRecipe $res = ${resultSeverity} } else { $msg = $lswTxt.skipTestMissingRecipe $res = 'SUCCESS' } $detailList.Add($msg) $logLines.Add("${msg}|${res}") } # for example: 9B is overridden by 12B $overriddenKBs = @('KB5043080') $recipeLCUlist |foreach-object { $name = $_.Name $_.Msu | foreach-Object { [string]$recipeHotFixID = ($_).split('-')[1] if (-not ($overriddenKBs -contains $recipeHotFixID)) { #$logLines.Add("Validate that LCU [${name}] as version [${recipeHotFixID}] is installed on host.|INFO.|INFO") $liveHotFixID = $null $liveHotFixID = (Get-HotFix -Id ${recipeHotFixID} -ErrorAction SilentlyContinue).HotFixID if (-not $liveHotFixID -or $liveHotFixID.TrimEnd() -ne $recipeHotFixID) { $rc += 1 $msg = $lswTxt.LatestCumulativeUpdateFail -f $name, $localHost, $recipeHotFixID, $liveHotFixID if (-not $liveHotFixID) { $msg += " Installed Hot Fixes: [" $msg += [string]((get-hotfix | Select-Object -Property HotFixID).HotFixID) -join(',') $msg += "]." } $logLines.Add("${msg}|${resultSeverity}") } else { $msg = $lswTxt.LatestCumulativeUpdatePass -f $name, $localHost, $recipeHotFixID, $liveHotFixID $logLines.Add("${msg}|SUCCESS") } } else { $msg = $lswTxt.LatestCumulativeUpdateSkip -f $name, $recipeHotFixID, $localHost $logLines.Add("${msg}|INFO") } $detailList.Add($msg) } } $response = "" | Select-Object -Property rc, details, computername, logLines $response.rc = $rc $response.details = $detailList $response.computername = $localHost $response.logLines = $logLines return $response } #endSB $resultSeverity = "CRITICAL" $responses = if ($psSession) { Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } else { Invoke-Command -ScriptBlock $sb -ArgumentList (,$global:recipeFilePath, $renderXMLvarScriptBloc, $lswTxt, $resultSeverity) } $splat = @{responses = $responses Name = "AzStackHci_OSImageRecipeValidation_LCU" Title = "Latest Cumulative Update matches recipe." DisplayName = "Latest Cumulative Update matches recipe." Severity = $resultSeverity Description = "Validating that the LCUs installed on the host match the LCUs defined in the OS image recipe."} return (TestResult @splat) } Export-ModuleMember -Function Test-* # SIG # Begin signature block # MIIoRgYJKoZIhvcNAQcCoIIoNzCCKDMCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDBaqtmH6fnyoU9 # vSSYUbaPmr/UGVIUrLynSXZpAz40FaCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0 # 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 # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIK2MwKYcFWFWry+mMgOfuwUv # 1OdPLB9oKLHHNhOUJRbcMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAJRNNTQKc4/oALTgckeHcKH1tzZlyOnLaYj1dA+v87ywlodCNoTyG7+Y5 # AP9Oejc/x6ZuWsbL6G+RymgK+Ygs+XTmAPlH+6HhBIzV9po5JnL2cT9ZJbiY4/JQ # 1eUyk+29k7douWqtceSELs6iCgKEVkqhC8MiQ06Rw23i2+ZHYHayuIrjEQl1Pvlg # Qhx0SYUCEscRLare1XDQ74GHFHkjP2ChFAMl9jVYYyB0rKsblz+cYNyjNsXg1Ojx # Cw00QWNVcEgYe/Ss384r3sWMDhpCszOPyueHrYFHaq/VZ650XG28hcwFUakLQ1JA # WSNiWgDXMSfuEScc5A9ZOONCijz7jKGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCC # F5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq # hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCArMr/uunKjVBIWnnuw7uYmah7wX8vGBZ+tSRcEHv9Z7AIGaC5MEqeo # GBMyMDI1MDYxMDE1NDgzNS4zMDdaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo1NTFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAACAdFFWZgQzEJPAAEAAAIBMA0G # CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u # MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0 # MDcyNTE4MzEyMloXDTI1MTAyMjE4MzEyMlowgdMxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w # ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjU1MUEt # MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtWrf+HzDu7sk50y5YHhe # CIJG0uxRSFFcHNek+Td9ZmyJj20EEjaU8JDJu5pWc4pPAsBI38NEAJ1b+KBnlStq # U8uvXF4qnEShDdi8nPsZZQsTZDKWAgUM2iZTOiWIuZcFs5ZC8/+GlrVLM5h1Y9nf # Mh5B4DnUQOXMremAT9MkvUhg3uaYgmqLlmYyODmba4lXZBu104SLAFsXOfl/TLhp # ToT46y7lI9sbI9uq3/Aerh3aPi2knHvEEazilXeooXNLCwdu+Is6o8kQLouUn3Kw # UQm0b7aUtsv1X/OgPmsOJi6yN3LYWyHISvrNuIrJ4iYNgHdBBumQYK8LjZmQaTKF # acxhmXJ0q2gzaIfxF2yIwM+V9sQqkHkg/Q+iSDNpMr6mr/OwknOEIjI0g6ZMOymi # vpChzDNoPz9hkK3gVHZKW7NV8+UBXN4G0aBX69fKUbxBBLyk2cC+PhOoUjkl6UC8 # /c0huqj5xX8m+YVIk81e7t6I+V/E4yXReeZgr0FhYqNpvTjGcaO2WrkP5XmsYS7I # vMPIf4DCyIJUZaqoBMToAJJHGRe+DPqCHg6bmGPm97MrOWv16/Co6S9cQDkXp9vM # SSRQWXy4KtJhZfmuDz2vr1jw4NeixwuIDGw1mtV/TdSI+vpLJfUiLl/b9w/tJB92 # BALQT8e1YH8NphdOo1xCwkcCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBSwcq9blqLo # PPiVrym9mFmFWbyyUjAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf # BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz # L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww # bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El # MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF # BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAOjQAyz0cVztT # FGqXX5JLRxFK/O/oMe55uDqEC8Vd1gbcM28KBUPgvUIPXm/vdDN2IVBkWHmwCp4A # Icy4dZtkuUmd0fnu6aT9Mvo1ndsLp2YJcMoFLEt3TtriLaO+i4Grv0ZULtWXUPAW # /Mn5Scjgn0xZduGPBD/Xs3J7+get9+8ZvBipsg/N7poimYOVsHxLcem7V5XdMNsy # tTm/uComhM/wgR5KlDYTVNAXBxcSKMeJaiD3V1+HhNkVliMl5VOP+nw5xWF55u9h # 6eF2G7eBPqT+qSFQ+rQCQdIrN0yG1QN9PJroguK+FJQJdQzdfD3RWVsciBygbYaZ # lT1cGJI1IyQ74DQ0UBdTpfeGsyrEQ9PI8QyqVLqb2q7LtI6DJMNphYu+jr//0spr # 1UVvyDPtuRnbGQRNi1COwJcj9OYmlkFgKNeCfbDT7U3uEOvWomekX60Y/m5utRcU # PVeAPdhkB+DxDaev3J1ywDNdyu911nAVPgRkyKgMK3USLG37EdlatDk8FyuCrx4t # iHyqHO3wE6xPw32Q8e/vmuQPoBZuX3qUeoFIsyZEenHq2ScMunhcqW32SUVAi5oZ # 4Z3nf7dAgNau21NEPwgW+2wkrNqDg7Hp8yHyoOKbgEBu6REQbvSfZ5Kh4PV+S2gx # f2uq6GoYDnlqABOMYwz309ISi0bPMh8wggdxMIIFWaADAgECAhMzAAAAFcXna54C # 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 # Tjo1NTFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaIjCgEBMAcGBSsOAwIaAxUA1+26cR/yH100DiNFGWhuAv2rYBqggYMw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF # AAIFAOvyffYwIhgPMjAyNTA2MTAwOTQ4MzhaGA8yMDI1MDYxMTA5NDgzOFowdzA9 # BgorBgEEAYRZCgQBMS8wLTAKAgUA6/J99gIBADAKAgEAAgIGlQIB/zAHAgEAAgIU # CjAKAgUA6/PPdgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow # CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQAfi+MGnunW # ybtUkrkwLDZX//z34ylWV1dRMzJyaqBTNnbPDaXxEl3FZpXbHM9+w+jLusEyONCH # Pwrs1MBQ4pH7XAYyXbUhfq2mhK1ehRuSJLE8mq7MWv0Hz6Ry3VhHAIpuxXTFzy0Z # nra2oeVxOzWdLwGrnAUyZJ0cBvz4ws5W28cVozT9zNEOj4LnHHFVZoOSEZYoDhzP # DuGohvJsym+INRclCuWzfMbWyI9U352DlHXIcvkrjaOjN6T6zMB2g2UOYEFnRSGz # y3TRIVLQCUGAl3BFmoeuM1zwRGod1LUEl1R6nROtHQobv/pmGT1NaNp/nTnHZQg2 # BKCIu1C+ohavMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB # IDIwMTACEzMAAAIB0UVZmBDMQk8AAQAAAgEwDQYJYIZIAWUDBAIBBQCgggFKMBoG # CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgx1Pgk0u1 # ITeFX9UG3cHheYczP0aqaKwl3DcveJ60OHYwgfoGCyqGSIb3DQEJEAIvMYHqMIHn # MIHkMIG9BCBYa7I6TJQRcmx0HaSTWZdJgowdrl9+Zrr0pIdqHtc4IzCBmDCBgKR+ # MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT # HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACAdFFWZgQzEJPAAEA # AAIBMCIEIGXf2ArpJzaxZZF6XoFIb/jMjZpoRUaPtqukZ0JzHWTvMA0GCSqGSIb3 # DQEBCwUABIICAChFn4Fc9hmC2xzpck53sxKQLOJIgTFbouu/GXE4KHK3hLlFlBsl # VognB6aVrnDeG+uH3FAJAvpLKaI+r+YT8aHZWdeKWRSx2EkXkwMLh5pTojZVr1OZ # SV5MQ/2sZgjcPJZZnth2/o9Crd52ryHuUMuxxj9KHXwRx1JpKgPGDVMeajq2OU27 # xBrMJirJ2SqbjZe4vPZ0+Tc1YMwJuVIdb0d70idzcNUgPBa3CbnBxBmqd7jRU4QV # LA0YPReeBz+am4GtjYBI3kf5BEMkMaoU1GVmb1tKt4FmeSHTXhYXvy9giXKzy9Ai # 9NILxTQJylSbNtq6q7Iy6IO3i9T9YPCWx2/ZviYcxtEyVSl8jflhCH/FM0eXz0/I # rBf05gAThLujBq2TH0Jkx+eCyzhg9ex7coduStoaZDGWayqioIRbHXbRCd5HM2oR # +o0JJ2fIdhiFPMl0EgFpN6CiFdPDHhlKDNFU6JAPuzLPhb5X4IbOuBjx+QlYYI73 # N1r8a9qzIPH59a1IKS7ExkekQ1Ovwa/F5RG2V6WUpFefMUNjezL/xmSBh4qApm1g # veQFduuaPTrwnAat9eeLbE8ObPRAOW3rsXdCoiOaXDqvYg+F93K9q7iM0qg0KeA2 # amyfccwrIOFxfFnGBzF14RtigzqjG9u/A8OsG++rthrKdQX1Po4DTzyA # SIG # End signature block |