Src/LabMedia.ps1

function NewLabMedia {
<#
    .SYNOPSIS
        Creates a new lab media object.
    .DESCRIPTION
        Permits validation of custom NonNodeData\Lability\Media entries.
#>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id = $(throw ($localized.MissingParameterError -f 'Id')),

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Filename = $(throw ($localized.MissingParameterError -f 'Filename')),

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Description = '',

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('x86','x64')]
        [System.String] $Architecture = $(throw ($localized.MissingParameterError -f 'Architecture')),

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

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('ISO','VHD')]
        [System.String] $MediaType = $(throw ($localized.MissingParameterError -f 'MediaType')),

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Uri = $(throw ($localized.MissingParameterError -f 'Uri')),

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

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

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Windows','Linux')]
        [System.String] $OperatingSystem = 'Windows',

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Collections.Hashtable] $CustomData = @{},

        [Parameter(ValueFromPipelineByPropertyName)]
        [AllowNull()]
        [System.Array] $Hotfixes
    )
    begin {

        ## Confirm we have a valid Uri
        try {

            $resolvedUri = New-Object -TypeName 'System.Uri' -ArgumentList $Uri;
            if ($resolvedUri.Scheme -notin 'http','https','file') {
                throw ($localized.UnsupportedUriSchemeError -f $resolvedUri.Scheme);
            }
        }
        catch {

            throw $_;
        }

    }
    process {

        $labMedia = [PSCustomObject] @{
            Id = $Id;
            Filename = $Filename;
            Description = $Description;
            Architecture = $Architecture;
            ImageName = $ImageName;
            MediaType = $MediaType;
            OperatingSystem = $OperatingSystem;
            Uri = [System.Uri] $Uri;
            Checksum = $Checksum;
            CustomData = $CustomData;
            Hotfixes = $Hotfixes;
        }

        ## Ensure any explicit product key overrides the CustomData value
        if ($ProductKey) {

            $CustomData['ProductKey'] = $ProductKey;
        }
        return $labMedia;

    } #end process
} #end function NewLabMedia


function ResolveLabMedia {
<#
    .SYNOPSIS
        Resolves the specified media using the registered media and configuration data.
    .DESCRIPTION
        Resolves the specified lab media from the registered media, but permitting the defaults to be overridden by configuration data.
 
        This also permits specifying of media within Configuration Data and not having to be registered on the lab host.
#>

    [CmdletBinding()]
    param (
        ## Media ID
        [Parameter(Mandatory, ValueFromPipeline)]
        [System.String] $Id,

        ## Lab DSC configuration data
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Collections.Hashtable]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformationAttribute()]
        $ConfigurationData
    )
    process {

        ## Avoid any $media variable scoping issues
        $media = $null;

        ## If we have configuration data specific instance, return that
        if ($PSBoundParameters.ContainsKey('ConfigurationData')) {

            $customMedia = $ConfigurationData.NonNodeData.$($labDefaults.ModuleName).Media.Where({ $_.Id -eq $Id });
            if ($customMedia) {

                $newLabMediaParams = @{};
                foreach ($key in $customMedia.Keys) {

                    $newLabMediaParams[$key] = $customMedia.$key;
                }
                $media = NewLabMedia @newLabMediaParams;
            }
        }

        ## If we have custom media, return that
        if (-not $media) {

            $media = Get-ConfigurationData -Configuration CustomMedia;
            $media = $media | Where-Object { $_.Id -eq $Id };
        }

        ## If we still don't have a media image, return the built-in object
        if (-not $media) {

            $media = Get-LabMedia -Id $Id;
        }

        ## We don't have any defined, custom or built-in media
        if (-not $media) {

            throw ($localized.CannotLocateMediaError -f $Id);
        }

        return $media;

    } #end process
} #end function ResolveLabMedia


function Get-LabMedia {
<#
    .SYNOPSIS
        Gets registered lab media.
    .DESCRIPTION
        The Get-LabMedia cmdlet retrieves all built-in and registered custom media.
    .PARAMETER Id
        Specifies the specific media Id to return.
    .PARAMETER CustomOnly
        Specifies that only registered custom media are returned.
#>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param (
        ## Media ID
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id,

        ## Only return custom media
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $CustomOnly
    )
    process {

        ## Retrieve built-in media
        if (-not $CustomOnly) {

            $defaultMedia = Get-ConfigurationData -Configuration Media;
        }
        ## Retrieve custom media
        $customMedia = @(Get-ConfigurationData -Configuration CustomMedia);
        if (-not $customMedia) {

            $customMedia = @();
        }

        ## Are we looking for a specific media Id
        if ($Id) {

            ## Return the custom media definition first (if it exists)
            $media = $customMedia | Where-Object { $_.Id -eq $Id };
            if ((-not $media) -and (-not $CustomOnly)) {

                ## We didn't find a custom media entry, return a default entry (if it exists)
                $media = $defaultMedia | Where-Object { $_.Id -eq $Id };
            }
        }
        else {

            ## Return all custom media
            $media = $customMedia;
            if (-not $CustomOnly) {

                foreach ($mediaEntry in $defaultMedia) {

                    ## Determine whether the media is present in the custom media, i.e. make sure
                    ## we don't override a custom entry with the default one.
                    $defaultMediaEntry = $customMedia | Where-Object { $_.Id -eq $mediaEntry.Id }
                    ## If not, add it to the media array to return
                    if (-not $defaultMediaEntry) {

                        $media += $mediaEntry;
                    }
                } #end foreach default media
            } #end if not custom only
        }

        foreach ($mediaObject in $media) {

            $mediaObject.PSObject.TypeNames.Insert(0, 'VirtualEngine.Lability.Media');
            Write-Output -InputObject $mediaObject;
        }

    } #end process
} #end function Get-LabMedia


function Test-LabMedia {
<#
    .SYNOPSIS
        Tests whether lab media has already been successfully downloaded.
    .DESCRIPTION
        The Test-LabMedia cmdlet will check whether the specified media Id has been downloaded and its checksum is correct.
    .PARAMETER Id
        Specifies the media Id to test.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id
    )
    process {

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        $media = Get-LabMedia -Id $Id;
        if ($media) {

            if (-not $hostDefaults.DisableLocalFileCaching) {

                $testResourceDownloadParams = @{
                    DestinationPath = Join-Path -Path $hostDefaults.IsoPath -ChildPath $media.Filename;
                    Uri = $media.Uri;
                    Checksum = $media.Checksum;
                }
                return TestResourceDownload @testResourceDownloadParams;
            }
            else {

                ## Local file resource caching is disabled
                return $true;
            }
        }
        else {

            return $false;
        }

    } #end process
} #end function Test-LabMedia


function InvokeLabMediaImageDownload {
<#
    .SYNOPSIS
        Downloads ISO/WIM/VHDX media resources.
    .DESCRIPTION
        Initiates a download of a media resource. If the resource has already been downloaded and the checksum is
        correct, it won't be re-downloaded. To force download of a ISO/VHDX use the -Force switch.
    .NOTES
        ISO media is downloaded to the default IsoPath location. VHD(X) files are downloaded directly into the
        ParentVhdPath location.
#>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        ## Lab media object
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [System.Object] $Media,

        ## Force (re)download of the resource
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    process {
        $hostDefaults = Get-ConfigurationData -Configuration Host;

        $invokeResourceDownloadParams = @{
            DestinationPath = Join-Path -Path $hostDefaults.IsoPath -ChildPath $media.Filename;
            Uri = $media.Uri;
            Checksum = $media.Checksum;
        }
        if ($media.MediaType -eq 'VHD') {

            $invokeResourceDownloadParams['DestinationPath'] = Join-Path -Path $hostDefaults.ParentVhdPath -ChildPath $media.Filename;
        }

        $mediaUri = New-Object -TypeName System.Uri -ArgumentList $Media.Uri;
        if ($mediaUri.Scheme -eq 'File') {

            ## Use a bigger buffer for local file copies..
            $invokeResourceDownloadParams['BufferSize'] = 1MB;
        }

        if ($media.MediaType -eq 'VHD') {

            ## Always download VHDXs regardless of Uri type
            [ref] $null = InvokeResourceDownload @invokeResourceDownloadParams -Force:$Force;
        }
        elseif (($mediaUri.Scheme -eq 'File') -and ($media.MediaType -eq 'WIM') -and $hostDefaults.DisableLocalFileCaching)
        ## TODO: elseif (($mediaUri.Scheme -eq 'File') -and $hostDefaults.DisableLocalFileCaching)
        {
            ## NOTE: Only WIM media can currently be run from a file share (see https://github.com/VirtualEngine/Lab/issues/28)
            ## Caching is disabled and we have a file resource, so just return the source URI path
            WriteVerbose ($localized.MediaFileCachingDisabled -f $Media.Id);
            $invokeResourceDownloadParams['DestinationPath'] = $mediaUri.LocalPath;
        }
        else {

            ## Caching is enabled or it's a http/https source
            [ref] $null = InvokeResourceDownload @invokeResourceDownloadParams -Force:$Force;
        }
        return (Get-Item -Path $invokeResourceDownloadParams.DestinationPath);

    } #end process
} #end InvokeLabMediaImageDownload


function InvokeLabMediaHotfixDownload {
<#
    .SYNOPSIS
        Downloads resources.
    .DESCRIPTION
        Initiates a download of a media resource. If the resource has already been downloaded and the checksum
        is correct, it won't be re-downloaded. To force download of a ISO/VHDX use the -Force switch.
    .NOTES
        ISO/WIM media is downloaded to the default IsoPath location. VHD(X) files are downloaded directly into the
        ParentVhdPath location.
#>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Id,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Uri,

        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Checksum,

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

        $hostDefaults = Get-ConfigurationData -Configuration Host;
        $destinationPath = Join-Path -Path $hostDefaults.HotfixPath -ChildPath $Id;
        $invokeResourceDownloadParams = @{
            DestinationPath = $destinationPath;
            Uri = $Uri;
        }
        if ($Checksum) {

            [ref] $null = $invokeResourceDownloadParams.Add('Checksum', $Checksum);
        }

        [ref] $null = InvokeResourceDownload @invokeResourceDownloadParams -Force:$Force;
        return (Get-Item -Path $destinationPath);

    } #end process
} #end function InvokeLabMediaHotfixDownload


function Register-LabMedia {
<#
    .SYNOPSIS
        Registers a custom media entry.
    .DESCRIPTION
        The Register-LabMedia cmdlet allows adding custom media to the host's configuration. This circumvents the requirement of having to define custom media entries in the DSC configuration document (.psd1).
 
        You can use the Register-LabMedia cmdlet to override the default media entries, e.g. you have the media hosted internally or you wish to replace the built-in media with your own implementation.
 
        To override a built-in media entry, specify the same media Id with the -Force switch.
    .LINK
        Get-LabMedia
        Unregister-LabMedia
#>

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param (
        ## Specifies the media Id to register. You can override the built-in media if required.
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String] $Id,

        ## Specifies the media's type.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateSet('VHD','ISO','WIM')]
        [System.String] $MediaType,

        ## Specifies the source Uri (http/https/file) of the media.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.Uri] $Uri,

        ## Specifies the architecture of the media.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [ValidateSet('x64','x86')]
        [System.String] $Architecture,

        ## Specifies a description of the media.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Description,

        ## Specifies the image name containing the target WIM image. You can specify integer values.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ImageName,

        ## Specifies the local filename of the locally cached resource file.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Filename,

        ## Specifies the MD5 checksum of the resource file.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Checksum,

        ## Specifies custom data for the media.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Collections.Hashtable] $CustomData,

        ## Specifies additional Windows hotfixes to install post deployment.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Collections.Hashtable[]] $Hotfixes,

        ## Specifies the media type. Linux VHD(X)s do not inject resources.
        [Parameter(ValueFromPipelineByPropertyName)]
        [ValidateSet('Windows','Linux')]
        [System.String] $OperatingSystem = 'Windows',

        ## Specifies that an exiting media entry should be overwritten.
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.SwitchParameter] $Force
    )
    process {

        ## Validate Linux VM media type is VHD
        if (($OperatingSystem -eq 'Linux') -and ($MediaType -ne 'VHD')) {

            throw ($localized.InvalidOSMediaTypeError -f $MediaType, $OperatingSystem);
        }

        ## Validate ImageName when media type is ISO/WIM
        if (($MediaType -eq 'ISO') -or ($MediaType -eq 'WIM')) {

            if (-not $PSBoundParameters.ContainsKey('ImageName')) {

                throw ($localized.ImageNameRequiredError -f '-ImageName');
            }
        }

        ## Resolve the media Id to see if it's already been used
        $media = ResolveLabMedia -Id $Id -ErrorAction SilentlyContinue;
        if ($media -and (-not $Force)) {

            throw ($localized.MediaAlreadyRegisteredError -f $Id, '-Force');
        }

        ## Get the custom media list (not the built in media)
        $existingCustomMedia = @(Get-ConfigurationData -Configuration CustomMedia);
        if (-not $existingCustomMedia) {

            $existingCustomMedia = @();
        }

        $customMedia = [PSCustomObject] @{
            Id = $Id;
            Filename = $Filename;
            Description = $Description;
            Architecture = $Architecture;
            ImageName = $ImageName;
            MediaType = $MediaType;
            OperatingSystem = $OperatingSystem;
            Uri = $Uri;
            Checksum = $Checksum;
            CustomData = $CustomData;
            Hotfixes = $Hotfixes;
        }

        $hasExistingMediaEntry = $false;
        for ($i = 0; $i -lt $existingCustomMedia.Count; $i++) {

            if ($existingCustomMedia[$i].Id -eq $Id) {

                WriteVerbose ($localized.OverwritingCustomMediaEntry -f $Id);
                $hasExistingMediaEntry = $true;
                $existingCustomMedia[$i] = $customMedia;
            }
        }

        if (-not $hasExistingMediaEntry) {

            ## Add it to the array
            WriteVerbose ($localized.AddingCustomMediaEntry -f $Id);
            $existingCustomMedia += $customMedia;
        }

        WriteVerbose ($localized.SavingConfiguration -f $Id);
        Set-ConfigurationData -Configuration CustomMedia -InputObject @($existingCustomMedia);
        return $customMedia;

    } #end process
} #end function Register-LabMedia


function Unregister-LabMedia {
<#
    .SYNOPSIS
        Unregisters a custom media entry.
    .DESCRIPTION
        The Unregister-LabMedia cmdlet allows removing custom media entries from the host's configuration.
    .LINK
        Get-LabMedia
        Register-LabMedia
#>

    [CmdletBinding(SupportsShouldProcess)]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSProvideDefaultParameterValue', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    param (
        ## Specifies the custom media Id to unregister. You cannot unregister the built-in media.
        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        [System.String] $Id
    )
    process {

        ## Get the custom media list
        $customMedia = Get-ConfigurationData -Configuration CustomMedia;
        if (-not $customMedia) {

            ## We don't have anything defined
            WriteWarning ($localized.NoCustomMediaFoundWarning -f $Id);
            return;
        }
        else {

            ## Check if we have a matching Id
            $media = $customMedia | Where-Object { $_.Id -eq $Id };
            if (-not $media) {
                ## We don't have a custom matching Id registered
                WriteWarning ($localized.NoCustomMediaFoundWarning -f $Id);
                return;
            }
        }

        $shouldProcessMessage = $localized.PerformingOperationOnTarget -f 'Unregister-LabMedia', $Id;
        $verboseProcessMessage = $localized.RemovingCustomMediaEntry -f $Id;
        if ($PSCmdlet.ShouldProcess($verboseProcessMessage, $shouldProcessMessage, $localized.ShouldProcessWarning)) {

            $customMedia = $customMedia | Where-Object { $_.Id -ne $Id };
            WriteVerbose ($localized.SavingConfiguration -f $Id);
            Set-ConfigurationData -Configuration CustomMedia -InputObject @($customMedia);
            return $media;
        }

    } #end process
} #end function Unregister-LabMedia


function Reset-LabMedia {
<#
    .SYNOPSIS
        Reset the lab media entries to default settings.
    .DESCRIPTION
        The Reset-LabMedia removes all custom media entries, reverting them to default values.
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.Management.Automation.PSCustomObject])]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns','')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess','')]
    param ( )
    process {

        Remove-ConfigurationData -Configuration CustomMedia;
        Get-Labmedia;

    }
} #end function Reset-LabMedia

# SIG # Begin signature block
# MIIXtwYJKoZIhvcNAQcCoIIXqDCCF6QCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU49Ma5IDTVqsckespn78BOIvj
# P1ugghLqMIID7jCCA1egAwIBAgIQfpPr+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
# NwIBFTAjBgkqhkiG9w0BCQQxFgQUXyADhb5kEqdW4pf5mx06usCPoYIwDQYJKoZI
# hvcNAQEBBQAEggEATRIr6Itu4DtfG3Tm6GT5j3ek6E1LgKhT1w1HNHz9IFua0jY6
# AgkiJoa9YPW2Aw39v2JxhtwBpMfHgvYJVMYxYo0sbTgUaokvFEIALCIKKeqIqnYL
# a5e4YaVDk+lG0BtEZbgM2lrBfn0ACSfGktrS/gOhzZpWFoMp/mWgGwl97vS5Gx+d
# 08f2Dt5zvBlVRLJtDTrbCe0gE46vw+jiCzzqu9iMSaSSYcaloHVIHN+qzaFOYs6G
# 5cDceyr5bIOG2e18xwVxMCH9JVA7WNLm1dOgLYkf+6HX9PSi1IT4X89aF103Bf2m
# QmdAx9VsSRGucxtMgZzhoPodl/Jl7yhQlL0BFKGCAgswggIHBgkqhkiG9w0BCQYx
# ggH4MIIB9AIBATByMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBD
# b3Jwb3JhdGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2
# aWNlcyBDQSAtIEcyAhAOz/Q4yP6/NW4E2GqYGxpQMAkGBSsOAwIaBQCgXTAYBgkq
# hkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNzA1MDExNDE3
# NTJaMCMGCSqGSIb3DQEJBDEWBBQqSUQcYAQbwSaWdw2qUn1OyqPnljANBgkqhkiG
# 9w0BAQEFAASCAQBwMB6jdojy/VRungrqRnTkV26p6FqNx28gV5XnBzxjbhFTrOqv
# NEnYnomU0w4oCyxXhjg+XB35DYTTT+U9GgK9xtVQK5M8cBwU8Y/t0SaELwkKia6i
# YjwzhIreNuw1XPiXy5oxNuZuM5/jO5rR+xSMEdpLKrnviccmNS6Sfav79QNY2lrp
# qaaWqbY4GTuceAW2QGIqPIzuajUBF4Y+nAkrVflYdrkwUSLM1w80ZSiboOgnZ9RP
# FynF/yLvx1wPoV23tm8BsdJH/QYtEGPcfP6HpndYdNFIqmtmVJd5khQh4IPeTzD2
# to5GToUqbERBd1BEhB8tGxTe9s9HkYjlthEE
# SIG # End signature block