Src/LabHostConfiguration.ps1
function GetLabHostSetupConfiguration { <# .SYNOPSIS Returns an array of hashtables defining the desired host configuration. .DESCRIPTION The GetLabHostSetupConfiguration function returns an array of hashtables used to determine whether the host is in the desired configuration. .NOTES The configuration is passed to avoid repeated calls to Get-LabHostDefault and polluting verbose output. #> [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( ) process { [System.Boolean] $isDesktop = (Get-CimInstance -ClassName Win32_OperatingSystem -Verbose:$false).ProductType -eq 1; ## Due to the differences in client/server deployment for Hyper-V, determine the correct method before creating the host configuration array. $labHostSetupConfiguration = @(); if ($isDesktop) { Write-Debug -Message 'Implementing desktop configuration.'; $labHostSetupConfiguration += @{ UseDefault = $true; Description = 'Hyper-V role'; ModuleName = 'PSDesiredStateConfiguration'; ResourceName = 'MSFT_WindowsOptionalFeature'; Prefix = 'WindowsOptionalFeature'; Parameters = @{ Ensure = 'Enable'; Name = 'Microsoft-Hyper-V-All'; } }; } else { Write-Debug -Message 'Implementing server configuration.'; $labHostSetupConfiguration += @{ UseDefault = $true; Description = 'Hyper-V Role'; ModuleName = 'PSDesiredStateConfiguration'; ResourceName = 'MSFT_RoleResource'; Prefix = 'WindowsFeature'; Parameters = @{ Ensure = 'Present'; Name = 'Hyper-V'; IncludeAllSubFeature = $true; } }; $labHostSetupConfiguration += @{ UseDefault = $true; Description = 'Hyper-V Tools'; ModuleName = 'PSDesiredStateConfiguration'; ResourceName = 'MSFT_RoleResource'; Prefix = 'WindowsFeature'; Parameters = @{ Ensure = 'Present'; Name = 'RSAT-Hyper-V-Tools'; IncludeAllSubFeature = $true; } }; } #end Server configuration $labHostSetupConfiguration += @{ ## Check for a reboot before continuing UseDefault = $false; Description = 'Pending reboot'; ModuleName = 'xPendingReboot'; ResourceName = 'MSFT_xPendingReboot'; Prefix = 'PendingReboot'; Parameters = @{ Name = 'TestingForHypervReboot'; SkipCcmClientSDK = $true; } }; return $labHostSetupConfiguration; } #end process } #end function GetLabHostSetupConfiguration function Get-LabHostConfiguration { <# .SYNOPSIS Retrieves the current lab host's configuration default values. .LINK Test-LabHostConfiguration Start-LabHostConfiguration #> [CmdletBinding()] [OutputType([System.Management.Automation.PSObject])] param ( ) process { $labHostSetupConfiguation = GetLabHostSetupConfiguration; foreach ($configuration in $labHostSetupConfiguation) { $importDscResourceParams = @{ ModuleName = $configuration.ModuleName; ResourceName = $configuration.ResourceName; Prefix = $configuration.Prefix; UseDefault = $configuration.UseDefault; } ImportDscResource @importDscResourceParams; $resource = GetDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters; $resource['Resource'] = $configuration.ResourceName; Write-Output -InputObject ([PSCustomObject] $resource); } } #end process } #end function Get-LabHostConfiguration function Test-LabHostConfiguration { <# .SYNOPSIS Tests the lab host's configuration. .DESCRIPTION The Test-LabHostConfiguration tests the current configuration of the lab host. .PARAMETER IgnorePendingReboot Specifies a pending reboot does not fail the test. .LINK Get-LabHostConfiguration Test-LabHostConfiguration #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ## Skips pending reboot check [Parameter()] [System.Management.Automation.SwitchParameter] $IgnorePendingReboot ) process { WriteVerbose $localized.StartedHostConfigurationTest; ## Test folders/directories $hostDefaults = GetConfigurationData -Configuration Host; foreach ($property in $hostDefaults.PSObject.Properties) { if (($property.Name.EndsWith('Path')) -and (-not [System.String]::IsNullOrEmpty($property.Value))) { WriteVerbose ($localized.TestingPathExists -f $property.Value); $resolvedPath = ResolvePathEx -Path $property.Value; if (-not (Test-Path -Path $resolvedPath -PathType Container)) { WriteVerbose -Message ($localized.PathDoesNotExist -f $resolvedPath); return $false; } } } $labHostSetupConfiguration = GetLabHostSetupConfiguration; foreach ($configuration in $labHostSetupConfiguration) { $importDscResourceParams = @{ ModuleName = $configuration.ModuleName; ResourceName = $configuration.ResourceName; Prefix = $configuration.Prefix; UseDefault = $configuration.UseDefault; } ImportDscResource @importDscResourceParams; WriteVerbose ($localized.TestingNodeConfiguration -f $Configuration.Description); if (-not (TestDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters)) { if ($configuration.Prefix -eq 'PendingReboot') { WriteWarning $localized.PendingRebootWarning; if (-not $IgnorePendingReboot) { return $false; } } else { return $false; } } } #end foreach labHostSetupConfiguration WriteVerbose $localized.FinishedHostConfigurationTest; return $true; } #end process } #end function Test-LabHostConfiguration function Start-LabHostConfiguration { <# .SYNOPSIS Invokes the configuration of the lab host. .DESCRIPTION The Start-LabHostConfiguration cmdlet invokes the configuration of the local host computer. .LINK Test-LabHostConfiguration Get-LabHostConfiguration #> [CmdletBinding()] [OutputType([System.Boolean])] param ( ) process { WriteVerbose $localized.StartedHostConfiguration; ## Create required directory structure $hostDefaults = GetConfigurationData -Configuration Host; foreach ($property in $hostDefaults.PSObject.Properties) { if (($property.Name.EndsWith('Path')) -and (-not [System.String]::IsNullOrEmpty($property.Value))) { [ref] $null = NewDirectory -Path $(ResolvePathEx -Path $Property.Value) -ErrorAction Stop; } } # Once all the path are created, check if the hostdefaults.Json file in the $env:ALLUSERSPROFILE is doesn't have entries with %SYSTEMDRIVE% in it # Many subsequent call are failing to Get-LabImage, Test-LabHostConfiguration which do not resolve the "%SYSTEMDRIVE%" in the path for Host defaults foreach ($property in $($hostDefaults.PSObject.Properties | Where-Object -Property TypeNameOfValue -eq 'System.String')) { if ($property.Value.Contains('%')) { # if the Path for host defaults contains a '%' character then resolve it $resolvedPath = ResolvePathEx -Path $Property.Value; # update the hostdefaults Object $hostDefaults.($property.Name) = $resolvedPath; $hostdefaultupdated = $true; } } if ($hostdefaultupdated) { # Write the changes back to the json file in the $env:ALLUSERSPROFILE $hostDefaults | ConvertTo-Json | Out-File -FilePath $(ResolveConfigurationDataPath -Configuration host) } $labHostSetupConfiguation = GetLabHostSetupConfiguration; foreach ($configuration in $labHostSetupConfiguation) { ImportDscResource -ModuleName $configuration.ModuleName -ResourceName $configuration.ResourceName -Prefix $configuration.Prefix -UseDefault:$configuration.UseDefault; WriteVerbose ($localized.TestingNodeConfiguration -f $Configuration.Description); [ref] $null = InvokeDscResource -ResourceName $configuration.Prefix -Parameters $configuration.Parameters; ## TODO: Need to check for pending reboots.. } WriteVerbose $localized.FinishedHostConfiguration; } #end process } #end function Start-LabHostConfiguration function Export-LabHostConfiguration { <# .SYNOPSIS Backs up the current lab host configuration. .LINK Import-LabHostConfiguration #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='Path')] [OutputType([System.IO.FileInfo])] param ( # Specifies the export path location. [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias("PSPath")] [System.String] $Path, # Specifies a literal export location path. [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $LiteralPath, ## Do not overwrite an existing file [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $NoClobber ) process { $now = [System.DateTime]::UtcNow; $configuration = [PSCustomObject] @{ Author = $env:USERNAME; GenerationHost = $env:COMPUTERNAME; GenerationDate = '{0} {1}' -f $now.ToShortDateString(), $now.ToString('hh:mm:ss'); ModuleVersion = (Get-Module -Name $labDefaults.ModuleName).Version.ToString(); HostDefaults = [PSCustomObject] (GetConfigurationData -Configuration Host); VMDefaults = [PSCustomObject] (GetConfigurationData -Configuration VM); CustomMedia = @([PSCustomObject] (GetConfigurationData -Configuration CustomMedia)); } if ($PSCmdlet.ParameterSetName -eq 'Path') { # Resolve any relative paths $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path); } else { $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LiteralPath); } if ($NoClobber -and (Test-Path -Path $Path -PathType Leaf -ErrorAction SilentlyContinue)) { $errorMessage = $localized.FileAlreadyExistsError -f $Path; $ex = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage; $errorCategory = [System.Management.Automation.ErrorCategory]::ResourceExists; $errorRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'FileExists', $errorCategory, $Path; $PSCmdlet.WriteError($errorRecord); } else { $verboseMessage = GetFormattedMessage -Message ($localized.ExportingConfiguration -f $labDefaults.ModuleName, $Path); $operationMessage = $localized.ShouldProcessOperation -f 'Export', $Path; $setContentParams = @{ Path = $Path; Value = ConvertTo-Json -InputObject $configuration -Depth 5; Force = $true; Confirm = $false; } if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) { try { ## Set-Content won't actually throw a terminating error?! Set-Content @setContentParams -ErrorAction Stop; Write-Output -InputObject (Get-Item -Path $Path); } catch { throw $_; } } } } #end process } #end function Export-LabHostConfiguration function Import-LabHostConfiguration { <# .SYNOPSIS Restores the lab host configuration from a backup. .LINK Export-LabHostConfiguration #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName='Path')] [OutputType([System.Management.Automation.PSCustomObject])] param ( # Specifies the export path location. [Parameter(Mandatory, ParameterSetName = 'Path', ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [Alias("PSPath")] [System.String] $Path, # Specifies a literal export location path. [Parameter(Mandatory, ParameterSetName = 'LiteralPath', ValueFromPipeline, ValueFromPipelineByPropertyName)] [ValidateNotNullOrEmpty()] [System.String] $LiteralPath, ## Restores only the lab host default settings [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Host, ## Restores only the lab VM default settings [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $VM, ## Restores only the lab custom media default settings [Parameter(ValueFromPipelineByPropertyName)] [System.Management.Automation.SwitchParameter] $Media ) process { if ($PSCmdlet.ParameterSetName -eq 'Path') { # Resolve any relative paths $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path); } else { $Path = $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($LiteralPath); } if (-not (Test-Path -Path $Path -PathType Leaf -ErrorAction SilentlyContinue)) { $errorMessage = $localized.InvalidPathError -f 'Import', $Path; $ex = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage; $errorCategory = [System.Management.Automation.ErrorCategory]::ResourceUnavailable; $errorRecord = New-Object System.Management.Automation.ErrorRecord $ex, 'FileNotFound', $errorCategory, $Path; $PSCmdlet.WriteError($errorRecord); return; } WriteVerbose -Message ($localized.ImportingConfiguration -f $labDefaults.ModuleName, $Path); $configurationDocument = Get-Content -Path $Path -Raw -ErrorAction Stop; try { $configuration = ConvertFrom-Json -InputObject $configurationDocument -ErrorAction Stop; } catch { $errorMessage = $localized.InvalidConfigurationError -f $Path; throw $errorMessage; } if ((-not $PSBoundParameters.ContainsKey('Host')) -and (-not $PSBoundParameters.ContainsKey('VM')) -and (-not $PSBoundParameters.ContainsKey('Media'))) { ## Nothing specified to load 'em all! $VM = $true; $Host = $true; $Media = $true; } WriteVerbose -Message ($localized.ImportingConfigurationSettings -f $configuration.GenerationDate, $configuration.GenerationHost); if ($Host) { $verboseMessage = GetFormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'Host'); $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'Host'; if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) { [ref] $null = Reset-LabHostDefault -Confirm:$false; $hostDefaultObject = $configuration.HostDefaults; $hostDefaults = ConvertPSObjectToHashtable -InputObject $hostDefaultObject; Set-LabHostDefault @hostDefaults -Confirm:$false; WriteVerbose -Message ($localized.ConfigurationRestoreComplete -f 'Host'); } } #end if restore host defaults if ($VM) { $verboseMessage = GetFormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'VM'); $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'VM'; if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) { [ref] $null = Reset-LabVMDefault -Confirm:$false; $vmDefaultObject = $configuration.VMDefaults; $vmDefaults = ConvertPSObjectToHashtable -InputObject $vmDefaultObject; ## Boot order is exposed externally $vmDefaults.Remove('BootOrder'); Set-LabVMDefault @vmDefaults -Confirm:$false; WriteVerbose -Message ($localized.ConfigurationRestoreComplete -f 'VM'); } } #end if restore VM defaults if ($Media) { $verboseMessage = GetFormattedMessage -Message ($localized.RestoringConfigurationSettings -f 'Media'); $operationMessage = $localized.ShouldProcessOperation -f 'Import', 'Media'; if ($PSCmdlet.ShouldProcess($verboseMessage, $operationMessage, $localized.ShouldProcessActionConfirmation)) { [ref] $null = Reset-LabMedia -Confirm:$false; foreach ($mediaObject in $configuration.CustomMedia) { $customMedia = ConvertPSObjectToHashtable -InputObject $mediaObject -IgnoreNullValues; Write-Output (Register-LabMedia @customMedia -Force); } WriteVerbose -Message ($localized.ConfigurationRestoreComplete -f 'Media'); } } #end if restore custom media } #end process } #end function # SIG # Begin signature block # MIIXtwYJKoZIhvcNAQcCoIIXqDCCF6QCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUo/zjlxbPfGKXczcWVXc/UADO # T7WgghLqMIID7jCCA1egAwIBAgIQfpPr+3zGTlnqS5p31Ab8OzANBgkqhkiG9w0B # AQUFADCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIG # A1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhh # d3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcg # Q0EwHhcNMTIxMjIxMDAwMDAwWhcNMjAxMjMwMjM1OTU5WjBeMQswCQYDVQQGEwJV # UzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xMDAuBgNVBAMTJ1N5bWFu # dGVjIFRpbWUgU3RhbXBpbmcgU2VydmljZXMgQ0EgLSBHMjCCASIwDQYJKoZIhvcN # AQEBBQADggEPADCCAQoCggEBALGss0lUS5ccEgrYJXmRIlcqb9y4JsRDc2vCvy5Q # WvsUwnaOQwElQ7Sh4kX06Ld7w3TMIte0lAAC903tv7S3RCRrzV9FO9FEzkMScxeC # i2m0K8uZHqxyGyZNcR+xMd37UWECU6aq9UksBXhFpS+JzueZ5/6M4lc/PcaS3Er4 # ezPkeQr78HWIQZz/xQNRmarXbJ+TaYdlKYOFwmAUxMjJOxTawIHwHw103pIiq8r3 # +3R8J+b3Sht/p8OeLa6K6qbmqicWfWH3mHERvOJQoUvlXfrlDqcsn6plINPYlujI # fKVOSET/GeJEB5IL12iEgF1qeGRFzWBGflTBE3zFefHJwXECAwEAAaOB+jCB9zAd # BgNVHQ4EFgQUX5r1blzMzHSa1N197z/b7EyALt0wMgYIKwYBBQUHAQEEJjAkMCIG # CCsGAQUFBzABhhZodHRwOi8vb2NzcC50aGF3dGUuY29tMBIGA1UdEwEB/wQIMAYB # Af8CAQAwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NybC50aGF3dGUuY29tL1Ro # YXd0ZVRpbWVzdGFtcGluZ0NBLmNybDATBgNVHSUEDDAKBggrBgEFBQcDCDAOBgNV # HQ8BAf8EBAMCAQYwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMTEFRpbWVTdGFtcC0y # MDQ4LTEwDQYJKoZIhvcNAQEFBQADgYEAAwmbj3nvf1kwqu9otfrjCR27T4IGXTdf # plKfFo3qHJIJRG71betYfDDo+WmNI3MLEm9Hqa45EfgqsZuwGsOO61mWAK3ODE2y # 0DGmCFwqevzieh1XTKhlGOl5QGIllm7HxzdqgyEIjkHq3dlXPx13SYcqFgZepjhq # IhKjURmDfrYwggSjMIIDi6ADAgECAhAOz/Q4yP6/NW4E2GqYGxpQMA0GCSqGSIb3 # DQEBBQUAMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3Jh # dGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2aWNlcyBD # QSAtIEcyMB4XDTEyMTAxODAwMDAwMFoXDTIwMTIyOTIzNTk1OVowYjELMAkGA1UE # BhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTQwMgYDVQQDEytT # eW1hbnRlYyBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIFNpZ25lciAtIEc0MIIBIjAN # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAomMLOUS4uyOnREm7Dv+h8GEKU5Ow # mNutLA9KxW7/hjxTVQ8VzgQ/K/2plpbZvmF5C1vJTIZ25eBDSyKV7sIrQ8Gf2Gi0 # jkBP7oU4uRHFI/JkWPAVMm9OV6GuiKQC1yoezUvh3WPVF4kyW7BemVqonShQDhfu # ltthO0VRHc8SVguSR/yrrvZmPUescHLnkudfzRC5xINklBm9JYDh6NIipdC6Anqh # d5NbZcPuF3S8QYYq3AhMjJKMkS2ed0QfaNaodHfbDlsyi1aLM73ZY8hJnTrFxeoz # C9Lxoxv0i77Zs1eLO94Ep3oisiSuLsdwxb5OgyYI+wu9qU+ZCOEQKHKqzQIDAQAB # o4IBVzCCAVMwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAO # BgNVHQ8BAf8EBAMCB4AwcwYIKwYBBQUHAQEEZzBlMCoGCCsGAQUFBzABhh5odHRw # Oi8vdHMtb2NzcC53cy5zeW1hbnRlYy5jb20wNwYIKwYBBQUHMAKGK2h0dHA6Ly90 # cy1haWEud3Muc3ltYW50ZWMuY29tL3Rzcy1jYS1nMi5jZXIwPAYDVR0fBDUwMzAx # oC+gLYYraHR0cDovL3RzLWNybC53cy5zeW1hbnRlYy5jb20vdHNzLWNhLWcyLmNy # bDAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQVGltZVN0YW1wLTIwNDgtMjAdBgNV # HQ4EFgQURsZpow5KFB7VTNpSYxc/Xja8DeYwHwYDVR0jBBgwFoAUX5r1blzMzHSa # 1N197z/b7EyALt0wDQYJKoZIhvcNAQEFBQADggEBAHg7tJEqAEzwj2IwN3ijhCcH # bxiy3iXcoNSUA6qGTiWfmkADHN3O43nLIWgG2rYytG2/9CwmYzPkSWRtDebDZw73 # BaQ1bHyJFsbpst+y6d0gxnEPzZV03LZc3r03H0N45ni1zSgEIKOq8UvEiCmRDoDR # EfzdXHZuT14ORUZBbg2w6jiasTraCXEQ/Bx5tIB7rGn0/Zy2DBYr8X9bCT2bW+IW # yhOBbQAuOA2oKY8s4bL0WqkBrxWcLC9JG9siu8P+eJRRw4axgohd8D20UaF5Mysu # e7ncIAkTcetqGVvP6KUwVyyJST+5z3/Jvz4iaGNTmr1pdKzFHTx/kuDDvBzYBHUw # ggUZMIIEAaADAgECAhADViTO4HBjoJNSwH9//cwJMA0GCSqGSIb3DQEBCwUAMHIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJ # RCBDb2RlIFNpZ25pbmcgQ0EwHhcNMTUwNTE5MDAwMDAwWhcNMTcwODIzMTIwMDAw # WjBgMQswCQYDVQQGEwJHQjEPMA0GA1UEBxMGT3hmb3JkMR8wHQYDVQQKExZWaXJ0 # dWFsIEVuZ2luZSBMaW1pdGVkMR8wHQYDVQQDExZWaXJ0dWFsIEVuZ2luZSBMaW1p # dGVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqLQmabdimcQtYPTQ # 9RSjv3ThEmFTRJt/MzseYYtZpBTcR6BnSfj8RfkC4aGZvspFgH0cGP/SNJh1w67b # iX9oT5NFL9sUJHUsVdyPBA1LhpWcF09PP28mGGKO3oQHI4hTLD8etiIlF9qFantd # 1Pmo0jdqT4uErSmx0m4kYGUUTa5ZPAK0UZSuAiNX6iNIL+rj/BPbI3nuPJzzx438 # oHYkZGRtsx11+pLA6hIKyUzRuIDoI7JQ0nZ0MkCziVyc6xGfS54JVLaVCEteTKPz # Gc4yyvCqp6Tfe9gs8UuxJiEMdH5fvllTU4aoXbm+W8tonkE7i/19rv8S1A2VPiVV # xNLbpwIDAQABo4IBuzCCAbcwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1 # DlgwHQYDVR0OBBYEFP2RNOWYipdNCSRVb5jIcyRp9tUDMA4GA1UdDwEB/wQEAwIH # gDATBgNVHSUEDDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYv # aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmww # QgYDVR0gBDswOTA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93 # d3cuZGlnaWNlcnQuY29tL0NQUzCBhAYIKwYBBQUHAQEEeDB2MCQGCCsGAQUFBzAB # hhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wTgYIKwYBBQUHMAKGQmh0dHA6Ly9j # YWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURDb2RlU2ln # bmluZ0NBLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCclXHR # DhDyJr81eiD0x+AL04ryDwdKT+PooKYgOxc7EhRn59ogxNO7jApQPSVo0I11Zfm6 # zQ6K6RPWhxDenflf2vMx7a0tIZlpHhq2F8praAMykK7THA9F3AUxIb/lWHGZCock # yD/GQvJek3LSC5NjkwQbnubWYF/XZTDzX/mJGU2DcG1OGameffR1V3xODHcUE/K3 # PWy1bzixwbQCQA96GKNCWow4/mEW31cupHHSo+XVxmjTAoC93yllE9f4Kdv6F29H # bRk0Go8Yn8WjWeLE/htxW/8ruIj0KnWkG+YwmZD+nTegYU6RvAV9HbJJYUEIfhVy # 3DeK5OlY9ima2sdtMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1b5VQCDANBgkq # hkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBB # c3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgxMDIyMTIwMDAw # WjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL # ExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3Vy # ZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB # CgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLXcep2nQUut4/6 # kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSRI5aQd4L5oYQj # ZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXiTWAYvqrEsq5w # MWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5Ng2Q7+S1TqSp # 6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8vYWxYoNzQYIH # 5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYDVR0TAQH/BAgw # BgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwMweQYI # KwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j # b20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6 # Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmww # OqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJ # RFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUH # AgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZIAYb9bAMwHQYD # VR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaAFEXroq/0ksuC # MS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPzItEVyCx8JSl2 # qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRupY5a4l4kgU4Q # pO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKNJK4kxscnKqEp # KBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmifz0DLQESlE/Dm # ZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN3fYBIM6ZMWM9 # CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKyZqHnGKSaZFHv # MYIENzCCBDMCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0 # IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNl # cnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBDQQIQA1YkzuBwY6CTUsB/ # f/3MCTAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkq # hkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGC # NwIBFTAjBgkqhkiG9w0BCQQxFgQUjNDQ1HMZk2yRs6fl2/JAIaNvYfgwDQYJKoZI # hvcNAQEBBQAEggEAYbAxSWPaIMRs2+wDRuBctcnUGewwZ7kafRXskCHSQ4XdCm1b # 76OTxae2fp6ye8h1RY2LduCsAs1yKK3x1Wu5U08e80qv6893JdE7cnqRkxj9E4mK # Co+w9upyN0uer3nX9Vl1zH7ryPGAec3BkGQF7nWHvgN3Cqd63RrE6PnHX+cS2YM2 # i6+qaeJqc+8nESFPG3l02u632GetqQfcQLgomqGSWsRIWWh+fC4WPq8pj3Td7S3a # L1iqo8n9lYEHOnMumrgS3ggwvRguOK4Z7UBKLcKEegijlbYT1RZnt2wxZgDXlg6R # xDWkgvDQxCI4Wj1vHesfZrWtnTdS93iLQcVEd6GCAgswggIHBgkqhkiG9w0BCQYx # ggH4MIIB9AIBATByMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBD # b3Jwb3JhdGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2 # aWNlcyBDQSAtIEcyAhAOz/Q4yP6/NW4E2GqYGxpQMAkGBSsOAwIaBQCgXTAYBgkq # hkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MTMxMzMz # MjFaMCMGCSqGSIb3DQEJBDEWBBT8/RAn5xTBGumjLj7zxuGHAJdfgzANBgkqhkiG # 9w0BAQEFAASCAQCOaMD4yKWEC7GinexSgH/HsMHzY7uekZJC1ElyQlcg7O6HrEJ7 # zEO2LSpikXS3+QxO/JRx/I31qpQnIRSNhpwI32BtKymMzpqhpZXfg72j38DqPiyu # ET0KDiFfoPxjqmtku0h0lh+TlAxR/q/cFGjvFiqz8BsgGrhSJ99ZUclTRbm60GjD # weZl/uRln31Xj6/aUbt7qspnOojuQ2MZgm8gul2SXOBLApxKd/2baN/vXyYOU6Mp # MdH/JK7ea27nytqmyDWBzMycl+ob9VYf5/KSNpHStL6RI4YbqZ+dQ3wS9dTkao4W # E+HJklDJxUGp8AOJcyqmb0cS+KdNmRL+Mly/ # SIG # End signature block |