Src/LabResource.ps1

function Test-LabResource {
<#
    .SYNOPSIS
        Tests whether a lab's resources are present.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## PowerShell DSC configuration document (.psd1) containing lab metadata.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab resource Id to test.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourceId,

        ## Lab resource path
        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.String] $ResourcePath
    )
    begin {

        if (-not $ResourcePath) {
            $hostDefaults = Get-ConfigurationData -Configuration Host;
            $ResourcePath = $hostDefaults.ResourcePath;
        }

    }
    process {

        if ($resourceId) {

            $resources = ResolveLabResource -ConfigurationData $ConfigurationData -ResourceId $ResourceId;
        }
        else {

            $resources = $ConfigurationData.NonNodeData.($labDefaults.ModuleName).Resource;
        }

        foreach ($resource in $resources) {

            $fileName = $resource.Id;
            if ($resource.Filename) { $fileName = $resource.Filename; }

            $testResourceDownloadParams = @{
                DestinationPath = Join-Path -Path $ResourcePath -ChildPath $fileName;;
                Uri = $resource.Uri;
            }

            if ($resource.Checksum) {

                $testResourceDownloadParams['Checksum'] = $resource.Checksum;
            }

            if (-not (TestResourceDownload @testResourceDownloadParams)) {

                return $false;
            }
        } #end foreach resource

        return $true;

    } #end process
} #end Test-LabResource


function TestLabResourceIsLocal {
<#
    .SYNOPSIS
        Test whether a lab resource is available locally
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## PowerShell DSC configuration document (.psd1) containing lab metadata.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab resource Id to test.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourceId,

        ## Node's target resource folder
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $LocalResourcePath
    )
    process {

        $resource = ResolveLabResource -ConfigurationData $ConfigurationData -ResourceId $ResourceId;

        if (($resource.Expand) -and ($resource.Expand -eq $true)) {

            ## Check the ResourceId folder is present
            $resourcePath = Join-Path -Path $LocalResourcePath -ChildPath $resourceId;
            $resourceExtension = [System.IO.Path]::GetExtension($resource.Filename);

            switch ($resourceExtension) {
                '.iso' {
                    $isPresent = Test-Path -Path $resourcePath -PathType Container;
                }
                '.zip' {
                    $isPresent = Test-Path -Path $resourcePath -PathType Container;
                }
                default {
                    throw ($localized.ExpandNotSupportedError -f $resourceExtension);
                }
            }
        }
        else {

            $resourcePath = Join-Path -Path $LocalResourcePath -ChildPath $resource.Filename;
            $isPresent = Test-Path -Path $resourcePath -PathType Leaf;
        }

        if ($isPresent) {

            WriteVerbose -Message ($localized.ResourceFound -f $resourcePath);
            return $true;
        }
        else {

            WriteVerbose -Message ($localized.ResourceNotFound -f $resourcePath);
            return $false;
        }

    } #end process
} #end function TestLabResourceIsLocal


function Invoke-LabResourceDownload {
<#
    .SYNOPSIS
        Starts a download of all required lab resources.
    .DESCRIPTION
        When a lab configuration is started, Lability will attempt to download all the required media and resources.
 
        In some scenarios you many need to download lab resources in advance, e.g. where internet access is not
        readily available or permitted. The `Invoke-LabResourceDownload` cmdlet can be used to manually download
        all required resources or specific media/resources as needed.
    .PARAMETER ConfigurationData
        Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
    .PARAMETER All
        Specifies all media, custom and DSC resources should be downloaded.
    .PARAMETER MediaId
        Specifies the specific media IDs to download.
    .PARAMETER ResourceId
        Specifies the specific custom resource IDs to download.
    .PARAMETER Media
        Specifies all media IDs should be downloaded.
    .PARAMETER Resources
        Specifies all custom resource IDs should be downloaded.
    .PARAMETER DSCResources
        Specifies all defined DSC resources should be downloaded.
    .PARAMETER Moduless
        Specifies all defined PowerShell modules should be downloaded.
    .PARAMETER Force
        Forces a download of all resources, overwriting any existing resources.
    .PARAMETER DestinationPath
        Specifies the target destination path of downloaded custom resources (not media or DSC resources).
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -All
 
        Downloads all required lab media, any custom resources and DSC resources defined in the 'MyLab.psd1' configuration.
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -MediaId 'WIN10_x64_Enterprise_EN_Eval'
 
        Downloads only the 'WIN10_x64_Enterprise_EN_Eval' media.
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -ResourceId 'MyCustomResource'
 
        Downloads only the 'MyCustomResource' resource defined in the 'MyLab.psd1' configuration.
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -Media
 
        Downloads only the media defined in the 'MyLab.psd1' configuration.
    .EXAMPLE
        Invoke-LabResourceDownload -ConfigurationData ~\Documents\MyLab.psd1 -Resources -DSCResources
 
        Downloads only the custom file resources and DSC resources defined in the 'MyLab.psd1' configuration.
#>

    [CmdletBinding(DefaultParameterSetName = 'All')]
    param (
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
         $ConfigurationData = @{ },

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'All')]
        [System.Management.Automation.SwitchParameter] $All,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'MediaId')]
        [System.String[]] $MediaId,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ResourceId')]
        [System.String[]] $ResourceId,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Media')]
        [System.Management.Automation.SwitchParameter] $Media,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Resources')]
        [System.Management.Automation.SwitchParameter] $Resources,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'DSCResources')]
        [System.Management.Automation.SwitchParameter] $DSCResources,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Modules')]
        [System.Management.Automation.SwitchParameter] $Modules,

        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Resources')]
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'ResourceId')]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,

        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    begin {

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        if (-not $DestinationPath) {
            $DestinationPath = $hostDefaults.ResourcePath;
        }

    }
    process {

        if ($PSCmdlet.ParameterSetName -in 'MediaId','Media','All') {

            if (-not $MediaId) {

                WriteVerbose ($Localized.DownloadingAllRequiredMedia);
                $uniqueMediaIds = @();
                $ConfigurationData.AllNodes.Where({ $_.NodeName -ne '*' }) | ForEach-Object {
                    $id = (Resolve-NodePropertyValue -NodeName $_.NodeName -ConfigurationData $ConfigurationData).Media;
                    if ($uniqueMediaIds -notcontains $id) { $uniqueMediaIds += $id; }
                }
                $MediaId = $uniqueMediaIds;
            }

            if ($MediaId) {

                foreach ($id in $MediaId) {

                    $labMedia = ResolveLabMedia -ConfigurationData $ConfigurationData -Id $id;
                    InvokeLabMediaImageDownload -Media $labMedia -Force:$Force;

                    WriteVerbose $Localized.DownloadingAllRequiredHotfixes;
                    if ($labMedia.Hotfixes.Count -gt 0) {
                        foreach ($hotfix in $labMedia.Hotfixes) {
                            InvokeLabMediaHotfixDownload -Id $hotfix.Id -Uri $hotfix.Uri;
                        }
                    }
                    else {
                        WriteVerbose ($localized.NoHotfixesSpecified);
                    }
                }

            }
            else {
                WriteVerbose ($localized.NoMediaDefined);
            }

        } #end if MediaId or MediaOnly

        if ($PSCmdlet.ParameterSetName -in 'ResourceId','Resources','All') {

            if (-not $ResourceId) {

                WriteVerbose ($Localized.DownloadingAllDefinedResources);
                $ResourceId = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Resource.Id;
            }

            if (($ResourceId.Count -gt 0) -and (-not $MediaOnly)) {

                foreach ($id in $ResourceId) {

                    $resource = ResolveLabResource -ConfigurationData $ConfigurationData -ResourceId $id;
                    if (($null -eq $resource.IsLocal) -or ($resource.IsLocal -eq $false)) {

                        $fileName = $resource.Id;
                        if ($resource.Filename) { $fileName = $resource.Filename; }
                        $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $fileName;
                        $invokeResourceDownloadParams = @{
                            DestinationPath = $resourceDestinationPath;
                            Uri = $resource.Uri;
                            Checksum = $resource.Checksum;
                            Force = $Force;
                        }
                        [ref] $null = InvokeResourceDownload @invokeResourceDownloadParams;
                        Write-Output (Get-Item -Path $resourceDestinationPath);
                    }
                }
            }
            else {

                WriteVerbose ($localized.NoResourcesDefined);
            }

        } #end if ResourceId or ResourceOnly

        if ($PSCmdlet.ParameterSetName -in 'DSCResources','All') {

            $dscResourceDefinitions = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).DSCResource;
            if (($null -ne $dscResourceDefinitions) -and ($dscResourceDefinitions.Count -gt 0)) {

                ## Invokes download of DSC resource modules into the module cache
                WriteVerbose ($Localized.DownloadingAllDSCResources);
                InvokeModuleCacheDownload -Module $dscResourceDefinitions -Force:$Force;
            }
            else {
                WriteVerbose ($localized.NoDSCResourcesDefined);
            }
        } #end if DSC resource

        if ($PSCmdlet.ParameterSetName -in 'Modules','All') {

            $moduleDefinitions = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Module;
            if (($null -ne $moduleDefinitions) -and ($moduleDefinitions.Count -gt 0)) {

                ## Invokes download of PowerShell modules into the module cache
                WriteVerbose ($Localized.DownloadingAllPowerShellModules);
                InvokeModuleCacheDownload -Module $moduleDefinitions -Force:$Force;
            }
            else {
                WriteVerbose ($localized.NoPowerShellModulesDefined);
            }

        } #end PowerShell module

    } #end process
} #end function Invoke-LabResourceDownload


function ResolveLabResource {
<#
    .SYNOPSIS
        Resolves a lab resource by its ID
#>

    param (
        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab resource ID
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourceId
    )
    process {

        $resource = $ConfigurationData.NonNodeData.($labDefaults.ModuleName).Resource | Where-Object Id -eq $ResourceId;
        if ($resource) {

            return $resource;
        }
        else {

            throw ($localized.CannotResolveResourceIdError -f $resourceId);
        }

    } #end process
} #end function ResolveLabResource


function ExpandLabResource {
<#
    .SYNOPSIS
        Copies files, e.g. EXEs, ISOs and ZIP file resources into a lab VM's mounted VHDX differencing disk image.
    .NOTES
        VHDX should already be mounted and passed in via the $DestinationPath parameter
        Can expand ISO and ZIP files if the 'Expand' property is set to $true on the resource's properties.
#>

    param (
        ## Specifies a PowerShell DSC configuration document (.psd1) containing the lab configuration.
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData,

        ## Lab VM name
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name,

        ## Destination mounted VHDX path to expand resources into
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,

        ## Source resource path
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourcePath
    )
    begin {

        if (-not $ResourcePath) {

            $hostDefaults = Get-ConfigurationData -Configuration Host;
            $ResourcePath = $hostDefaults.ResourcePath;
        }

    }
    process {

        ## Create the root destination (\Resources) container
        if (-not (Test-Path -Path $DestinationPath -PathType Container)) {

            [ref] $null = New-Item -Path $DestinationPath -ItemType Directory -Force -Confirm:$false;
        }

        $node = Resolve-NodePropertyValue -NodeName $Name -ConfigurationData $ConfigurationData -ErrorAction Stop;
        foreach ($resourceId in $node.Resource) {

            WriteVerbose ($localized.AddingResource -f $resourceId);
            $resource = ResolveLabResource -ConfigurationData $ConfigurationData -ResourceId $resourceId;

            ## Default to resource.Id unless there is a filename property defined!
            $resourceSourcePath = Join-Path $resourcePath -ChildPath $resource.Id;

            if ($resource.Filename) {

                $resourceSourcePath = Join-Path $resourcePath -ChildPath $resource.Filename;
                if ($resource.IsLocal) {

                    $resourceSourcePath = Resolve-Path -Path $resource.Filename;
                }
            }

            if (-not (Test-Path -Path $resourceSourcePath) -and (-not $resource.IsLocal)) {

                $invokeLabResourceDownloadParams = @{
                    ConfigurationData = $ConfigurationData;
                    ResourceId = $resourceId;
                }
                [ref] $null = Invoke-LabResourceDownload @invokeLabResourceDownloadParams;
            }

            if (-not (Test-Path -Path $resourceSourcePath)) {

                throw ($localized.CannotResolveResourceIdError -f $resourceId);
            }

            $resourceItem = Get-Item -Path $resourceSourcePath;
            $resourceDestinationPath = $DestinationPath;

            if ($resource.DestinationPath -and (-not [System.String]::IsNullOrEmpty($resource.DestinationPath))) {

                $destinationDrive = Split-Path -Path $DestinationPath -Qualifier;
                $resourceDestinationPath = Join-Path -Path $destinationDrive -ChildPath $resource.DestinationPath;

                ## We can't create a drive-rooted folder!
                if (($resource.DestinationPath -ne '\') -and (-not (Test-Path -Path $resourceDestinationPath))) {

                    [ref] $null = New-Item -Path $resourceDestinationPath -ItemType Directory -Force  -Confirm:$false;
                }
            }
            elseif ($resource.IsLocal -and ($resource.IsLocal -eq $true)) {

                $relativeLocalPath = ($resource.Filename).TrimStart('.');
                $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $relativeLocalPath;
            }

            if (($resource.Expand) -and ($resource.Expand -eq $true)) {

                if ([System.String]::IsNullOrEmpty($resource.DestinationPath)) {

                    ## No explicit destination path, so expand into the <DestinationPath>\<ResourceId> folder
                    $resourceDestinationPath = Join-Path -Path $DestinationPath -ChildPath $resource.Id;
                }

                if (-not (Test-Path -Path $resourceDestinationPath)) {

                    [ref] $null = New-Item -Path $resourceDestinationPath -ItemType Directory -Force -Confirm:$false;
                }

                switch ([System.IO.Path]::GetExtension($resourceSourcePath)) {

                    '.iso' {

                        ExpandIso -Path $resourceItem.FullName -DestinationPath $resourceDestinationPath;
                    }

                    '.zip' {

                        WriteVerbose ($localized.ExpandingZipResource -f $resourceItem.FullName);
                        $expandZipArchiveParams = @{
                            Path = $resourceItem.FullName;
                            DestinationPath = $resourceDestinationPath;
                            Verbose = $false;
                        }
                        [ref] $null = ExpandZipArchive @expandZipArchiveParams;
                    }

                    Default {

                        throw ($localized.ExpandNotSupportedError -f $resourceItem.Extension);
                    }

                } #end switch
            }
            else {

                WriteVerbose ($localized.CopyingFileResource -f $resourceDestinationPath);
                $copyItemParams = @{
                    Path = "$($resourceItem.FullName)";
                    Destination = "$resourceDestinationPath";
                    Force = $true;
                    Recurse = $true;
                    Verbose = $false;
                    Confirm = $false;
                }
                Copy-Item @copyItemParams;
            }

        } #end foreach ResourceId

    } #end process
} #end function ExpandLabResource

# SIG # Begin signature block
# MIIXtwYJKoZIhvcNAQcCoIIXqDCCF6QCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUl59Jqo4Yq5ok600drQoSCsQr
# S92gghLqMIID7jCCA1egAwIBAgIQfpPr+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
# NwIBFTAjBgkqhkiG9w0BCQQxFgQUwJn84E30qOJjMkjx3UUT+DJoIG0wDQYJKoZI
# hvcNAQEBBQAEggEAoP6ffUcLL7sbasY9KGHjJlRQumaJMk5sI09+Bih+3UW77ecw
# x2Njl/eHpGviAKnNTGOGOxpw9Ww39XDbisfHXAQbED4pywqXlmqwMQLW79tzwVBy
# xsA/7YuuQ6McVcelHI1q0FfV11yrLT3pmYauqt+qdO5IiGRp5SvvEobDX0MXoSvU
# cl76S/obnFzBTBMzJ60inJLSKN7A37TOiKQ6N3uOnoAzucmFgWmjsjFvM/BUTAJB
# Vz/IVq05oHqA3TLla1PWXEVSWz02FbL4WFrgG+1JilGbh09RV3oM7mUgsL8mPCuF
# iqB73jF0ZxS3HGOFusW/K4lEe31yIoLHdJaPqqGCAgswggIHBgkqhkiG9w0BCQYx
# ggH4MIIB9AIBATByMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBD
# b3Jwb3JhdGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2
# aWNlcyBDQSAtIEcyAhAOz/Q4yP6/NW4E2GqYGxpQMAkGBSsOAwIaBQCgXTAYBgkq
# hkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNzA1MDExNDE3
# NTNaMCMGCSqGSIb3DQEJBDEWBBTRDWfojSzcLjmqwQ1oXs0YakLwNzANBgkqhkiG
# 9w0BAQEFAASCAQAkGQVxgRTKIePxdQcuYm7MqyPDQ9bYGPozat2rZt2cMyUFS4AR
# AYMJWVWX2DIjv2ixLT0CKZ2Hpo73G787YUnKrJd1fPJQSVCpJ+GJjymo/p1Ipckv
# 0tbxEL+Ta0lytoX3QUdEMKxLKqQGjY84uF2N9kF3CCRhGux0VA214znqsXYZItPM
# iXvKcLtx82pRSqpL1m/BXbFn2RNYfYJ13pzO7cF7q9wAoF4E4sIPA/J3YV351fpE
# zigWkbW908EhQvqbekNYZaOT9w7dZjuUjP0nafmNv6yQX0LWdXrGrUNcVAfw9FYj
# tUzC9nuX3cX2M0/7m4l8pRWVc6dr1eOhb692
# SIG # End signature block