Classes/Public/DscConfigurationCompiler.ps1

<#
Copyright (c) 2018-2021 VMware, Inc. All rights reserved
 
The BSD-2 license (the "License") set forth below applies to all parts of the Desired State Configuration Resources for VMware project. You may not use this file except in compliance with the License.
 
BSD-2 License
 
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#>


<#
.DESCRIPTION
 
Used for parsing and compiling a DSC Configuration into a DSC object.
#>

class DscConfigurationCompiler {
    hidden [string] $ConfigName
    hidden [Hashtable] $Parameters
    hidden [Hashtable] $ConfigurationData
    hidden [Hashtable] $ResourceNameToInfo
    hidden [Hashtable] $CompositeResourceToScriptBlock
    hidden [bool] $IsNested

    DscConfigurationCompiler([string] $ConfigName, [Hashtable] $Parameters, [Hashtable] $ConfigurationData) {
        $this.ConfigName = $ConfigName
        $this.Parameters = $Parameters

        # configurationData gets cloned because it's state gets mutated during execution.
        if ($null -ne $ConfigurationData) {
            $this.ConfigurationData = $this.DeepCloneUtil($ConfigurationData)
        }

        $this.CompositeResourceToScriptBlock = @{}
        $this.ResourceNameToInfo = @{}
        $this.IsNested = $false
    }

    <#
    .DESCRIPTION
    Compiles the DSC Configuration and returns an configuration object
    #>

    [VmwDscConfiguration] CompileDscConfiguration([DscConfigurationBlock] $dscConfigurationBlock) {
        Write-Verbose "Starting compilation process"

        Write-Verbose "Validating ConfigurationData"

        # validate the configurationData
        $this.ValidateConfigurationData()

        # parse and compile the configuration
        $dscItems = $this.CompileDscConfigurationUtil($dscConfigurationBlock, $this.Parameters)

        Write-Verbose "Handling nodes"

        # combine nodes of same instanceName and bundle nodeless resources
        $dscNodes = $this.CombineNodes($dscItems)

        for ($i = 0; $i -lt $dscNodes.Length; $i++) {
            Write-Verbose ("Ordering DSC Resources of node: " + $dscNodes[$i].InstanceName)

            # parse the dsc resources and their dependencies into a sorted array
            $dscNodes[$i].Resources = $this.OrderResources($dscNodes[$i].Resources)
        }

        $dscConfigurationObject = [VmwDscConfiguration]::new(
            $this.ConfigName,
            $dscNodes
        )

        return $dscConfigurationObject
    }

    <#
    .DESCRIPTION
 
    Creates a deep clone of an object.
    #>

    hidden [PsObject] DeepCloneUtil([PsObject] $ObjectToClone) {
        $memStream = New-Object -TypeName 'IO.MemoryStream'
        $formatter = new-object -TypeName 'Runtime.Serialization.Formatters.Binary.BinaryFormatter'

        $formatter.Serialize($memStream, $ObjectToClone)

        $memStream.Position = 0

        $clonedObject = $formatter.Deserialize($memStream)

        return $clonedObject
    }

    <#
    .DESCRIPTION
    Ensures ConfigurationData is valid if present.
    Throws an exception if ConfigurationData is invalid.
    #>

    hidden [void] ValidateConfigurationData() {
        # if the configurationData is null nothing is validated
        if ($null -eq $this.ConfigurationData) {
            return
        }

        # must contain AllNodes key
        if (-not $this.ConfigurationData.ContainsKey('AllNodes')) {
            throw $script:ConfigurationDataDoesNotContainAllNodesException
        }

        # AllNodes key must be array
        if ($this.ConfigurationData['AllNodes'] -isnot [Array]) {
            throw $script:ConfigurationDataAllNodesKeyIsNotAnArrayException
        }

        # hashset for detecting entries with same nodeName property
        $duplicateNodeNameSet = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)

        # will contain the common nodes setting marked with '*'
        $commonNodesConfiguration = $null

        foreach ($nodeConfiguration in $this.ConfigurationData['AllNodes']) {
            # each node entry must be a hashtable
            if ($nodeConfiguration -isnot [Hashtable]) {
                throw $script:ConfigurationDataNodeEntryInAllNodesIsNotAHashtableException
            }

            # each node entry must have a NodeName property
            if (-not $nodeConfiguration.ContainsKey('NodeName')) {
                throw $script:ConfigurationDataNodeEntryInAllNodesDoesNotContainNodeNameException
            }

            # checks if nodeName is added to the hashset or if it's already in.
            $isNodeNameAdded = $duplicateNodeNameSet.Add($nodeConfiguration['NodeName'])

            if (-not $isNodeNameAdded) {
                throw $script:DuplicateEntryInAllNodesException
            }

            if($nodeConfiguration['NodeName'] -eq '*')
            {
                $commonNodesConfiguration = $nodeConfiguration
            }
        }

        # remove the common node settings entry
        $this.ConfigurationData['AllNodes'] = $this.ConfigurationData['AllNodes'] | Where-Object { $_.NodeName -ne '*' }

        # add the common node settings entry properties to each node configuration
        if ($null -ne $commonNodesConfiguration) {
            foreach($nodeConfiguration in $this.ConfigurationData['AllNodes']) {
                foreach($nodeKey in $commonNodesConfiguration.Keys)
                {
                    if(-not $nodeConfiguration.ContainsKey($nodeKey))
                    {
                        $nodeConfiguration[$nodeKey] = $commonNodesConfiguration[$nodeKey]
                    }
                }
            }
        }
    }

    <#
    .DESCRIPTION
    Orders a collection of Dsc resources based on their DependsOn property and check for duplicate ids.
    When a duplicate is found an exception is thrown.
    #>

    hidden [VmwDscResource[]] OrderResources([VmwDscResource[]] $DscResources) {
        # place resource in an oredered hashtable in order to check for duplicates
        $dscResOrderedDict = [ordered]@{}
        foreach ($resource in $DscResources) {
            $key = $resource.GetId()

            if ($dscResOrderedDict.Contains($key)) {
                throw ($script:DuplicateResourceException -f $resource.InstanceName, $resource.ResourceType)
            }

            $dscResOrderedDict[$key] = $resource
        }

        # creates graph with the created ordered hashtable
        $resGraph = [VmwDscResourceGraph]::new($dscResOrderedDict)

        # sort the resources based on their dependencies
        $sortedDscResArr = $resGraph.TopologicalSort()

        $result = New-Object -TypeName 'System.Collections.ArrayList'

        # iterate the ordered resources to find composite resources and retrieve their inner resources
        for ($i = 0; $i -lt $sortedDscResArr.Count; $i++) {
            $resource = $sortedDscResArr[$i]

            if ($resource.GetIsComposite()) {
                $innerResources = $resource.GetInnerResources()

                # this sorts the composite resource dsc resources and handles inner DependsOn properties
                $parsedInnerResources = $this.OrderResources($innerResources)

                $resource.SetInnerResources($parsedInnerResources)
            }

            $result.Add($resource) | Out-Null
        }

        return $result.ToArray()
    }

    <#
    .DESCRIPTION
    Handles the main logic for compiling a dsc configuration
    #>

    hidden [DscItem[]] CompileDscConfigurationUtil([DscConfigurationBlock] $dscConfigurationBlock, [Hashtable] $Parameters) {
        $dscConfigurationParser = [DscConfigurationParser]::new()

        Write-Verbose "Parsing configuration block of $($dscConfigurationBlock.Name)"

        <#
            The Verbose preference is set to 'SilentlyContinue' to suppress the
            Verbose output of 'Import-DscResource' when importing the 'VMware.vSphereDSC' module.
        #>

        $savedVerbosePreference = $Global:VerbosePreference
        $Global:VerbosePreference = 'SilentlyContinue'

        # parse the configuration, run Import-DscResource statements and retrieve the found resources/nested configurations
        $parseResult = $dscConfigurationParser.ParseDscConfiguration($dscConfigurationBlock)

        $Global:VerbosePreference = $savedVerbosePreference

        Write-Verbose "Preparing functions for dsc resources and nodes"

        $resources = $parseResult.ResourceNameToInfo
        $foundDscResourcesList = New-Object -TypeName 'System.Collections.ArrayList'
        # divide the regular resources and composite/nested into separate groups
        foreach ($resourceName in $resources.Keys) {
            $this.ResourceNameToInfo[$resourceName] = $resources[$resourceName]

            $resourceInfo = $this.ResourceNameToInfo[$resourceName]

            if (($resourceInfo.ImplementedAs.ToString() -eq 'Composite' -or $resourceInfo.ImplementedAs.ToString() -eq 'Configuration') -and
                (-not $this.CompositeResourceToScriptBlock.ContainsKey($resourceName))) {
                $this.CompositeResourceToScriptBlock[$resourceName] = (Get-Command $resourceName -ErrorAction 'SilentlyContinue')
            } else {
                $foundDscResourcesList.Add($resourceName) | Out-Null
            }
        }

        # create functions for nodes and resources to be used in InvokeWithContext
        $functionsToDefine = $this.CreateFunctionsToDefine($foundDscResourcesList.ToArray())

        Write-Verbose "Preparing default variables and variables from ConfigurationData"

        # create variable objects for InvokeWithContext from ConfigurationData and common dsc configuration variables
        $variablesToDefine = $this.CreateVariablesToDefine()

        $configScriptBlock = $parseResult.ScriptBlock

        Write-Verbose "Executing Configuration scriptblock to extract resources and nodes"

        $dscItems = $configScriptBlock.InvokeWithContext($functionsToDefine, $variablesToDefine, $Parameters)

        return $dscItems
    }

    <#
    .DESCRIPTION
    Creates variables that will be used during the dsc configuration scriptblock execution
    #>

    hidden [Array] CreateVariablesToDefine() {
        $result = @(
            if ($null -ne $this.ConfigurationData) {
                New-Object -TypeName PSVariable -ArgumentList ('ConfigurationData', $this.ConfigurationData )
                New-Object -TypeName PSVariable -ArgumentList ('AllNodes', $this.ConfigurationData.AllNodes )
            }
        )

        return $result
    }

    <#
    .DESCRIPTION
    Combines nodes of same name and adds nodeless resources to a default localhost node.
    #>

    hidden [VmwDscNode[]] CombineNodes([DscItem[]] $DscItemsArr) {
        $nodeLessResources = New-Object -TypeName 'System.Collections.ArrayList'
        $connectionNodes = [ordered]@{}

        for ($i = 0; $i -lt $DscItemsArr.Count; $i++) {
            # can be a node or nodeless resource
            $dscItem = $DscItemsArr[$i]

            # keep nodeless resources in a separate array in order to add them to the default 'localhost' node
            if ($dscItem -is [VmwDscResource]) {
                $nodeLessResources.Add($dscItem) | Out-Null

                continue
            }

            if ($dscItem -is [VmwDscNode]) {
                if (-not $connectionNodes.Contains($dscItem.InstanceName)) {
                    $connectionNodes[$dscItem.InstanceName] = $dscItem
                } else {
                    $connectionNodes[$dscItem.InstanceName] += $dscItem
                }

                continue
            }

            foreach ($connection in $dscItem.Connections) {
                if (-not $connectionNodes.Contains($connection)) {
                    $nodeObject = New-Object -TypeName $dscItem.Type -ArgumentList $connection, $dscItem.Resources

                    $connectionNodes[$connection] = $nodeObject

                    continue
                }

                $connectionNodes[$connection].Resources += $dscItem.Resources
            }
        }

        # group nodeless resources
        if ($nodeLessResources.Count -gt 0) {
            $localHostConnection = 'localhost'

            if ($connectionNodes.Contains($localHostConnection)) {
                $connectionNodes[$localHostConnection].Resources += $nodeLessResources.ToArray()
            } else {
                $localhostNode = [VmwDscNode]::new(
                    $localHostConnection,
                    $nodeLessResources.ToArray()
                )

                $connectionNodes[$localHostConnection] = $localHostNode
            }
        }

        return @( $connectionNodes.Values )
    }

    <#
    .DESCRIPTION
    Creates functions for handling the dsc resources and dynamic keywords.
    #>

    hidden [Hashtable] CreateFunctionsToDefine([System.String[]] $foundDscResourcesArr) {
        $functionsToDefine = @{}

        $dscResourceScriptBlock = {
            Param(
                [string]
                $Name,
                [ScriptBlock]
                $Properties
            )

            $this.ParseDscResource($Name, $Properties)
        }

        foreach ($dscResourceName in $foundDscResourcesArr) {
            $functionsToDefine[$dscResourceName] = $dscResourceScriptBlock
        }

        $nestedConfigAndCompositeResScriptBlock = {
            Param(
                [string]
                $NestedConfigName,

                [ScriptBlock]
                $Properties
            )

            if ($this.IsNested) {
                throw $script:NestedMoreThanASingleLevelException
            }

            $this.IsNested = $true

            # parse prop scriptblock into a hashtable
            $propsAsText = $Properties.Ast.Extent.Text
            $propsAsText = $propsAsText.Insert(0, '@')
            $parsedProps = Invoke-Expression $propsAsText

            $dependsOn = $null

            # check if dependsOn property is present and removes it
            if ($parsedProps.ContainsKey('DependsOn')) {
                $dependsOn = $parsedProps['DependsOn']
                $parsedProps.Remove('DependsOn')
            }

            # get type of the configuration from the stack
            $configType = Get-PSCallStack | Select-Object -First 1 | Select-Object -ExpandProperty Command

            # retrieve internal resources
            $compositeDscResource = $this.CompositeResourceToScriptBlock[$configType]
            $dscConfigurationBlock = $this.MapCompositeDSCResourceToDscConfigurationBlock($compositeDscResource)
            $innerResources = $this.CompileDscConfigurationUtil($dscConfigurationBlock, $parsedProps)

            $compositeResourceProps = @{}

            # adds the dependsOn property on each resource in the composite resource
            if ($null -ne $dependsOn) {
                $compositeResourceProps['DependsOn'] = $dependsOn
            }

            $moduleName = $null
            if ($this.ResourceNameToInfo.ContainsKey($configType)) {
                $moduleName = @{
                    ModuleName = $this.ResourceNameToInfo[$configType].ModuleName
                    RequiredVersion = $this.ResourceNameToInfo[$configType].Version
                }
            }

            $compositeResource = [VmwDscResource]::new(
                $NestedConfigName,
                $configType,
                $moduleName,
                $compositeResourceProps,
                $innerResources
            )

            $this.IsNested = $false

            return $compositeResource
        }

        foreach ($configName in $this.CompositeResourceToScriptBlock.Keys) {
            $functionsToDefine[$configName] = $nestedConfigAndCompositeResScriptBlock
        }

        $nodeLogicScriptBlock = {
            Param (
                [string[]]
                $Connections,

                [ScriptBlock]
                $scriptBlock
            )

            if ($this.IsNested) {
                throw $script:NestedNodesAreNotSupportedException
            }

            $type = Get-PSCallStack | Select-Object -First 1 -ExpandProperty Command
            $vmwNodeResult = New-Object -TypeName 'System.Collections.ArrayList'

            # when multipe connections are specified for a node blocke
            # each connection gets made into a different node object
            foreach ($connection in $Connections) {
                $nodeObject = $null

                # resources are created here to avoid duplicates via reference
                $dscResources = . $scriptBlock

                if ($type -eq 'Node') {
                    $nodeObject = [VmwDscNode]::new($connection, $dscResources)
                } elseif ($type -eq 'vSphereNode') {
                    $nodeObject = [VmwVsphereDscNode]::new($connection, $dscResources)
                }

                $vmwNodeResult.Add($nodeObject) | Out-Null
            }

            $vmwNodeResult.ToArray()
        }

        $functionsToDefine['Node'] = $nodeLogicScriptBlock
        $functionsToDefine['vSphereNode'] = $nodeLogicScriptBlock

        return $functionsToDefine
    }

    <#
    .DESCRIPTION
 
    Maps the specified PowerShell ConfigurationInfo object to a DSC Configuration Block by retrieving the name of the DSC Configuration and the text
    information for it - start and end line and also the text of the DSC Configuration.
    #>

    hidden [DscConfigurationBlock] MapCompositeDSCResourceToDscConfigurationBlock([System.Management.Automation.ConfigurationInfo] $compositeDscResource) {
        $dscConfigurationBlock = [DscConfigurationBlock]::new()

        $dscConfigurationBlock.Name = $compositeDscResource.Name
        $dscConfigurationBlock.Extent = [DscConfigurationBlockExtent]::new()

        $dscConfigurationBlock.Extent.StartLine = $compositeDscResource.ScriptBlock.Ast.Extent.StartLineNumber
        $dscConfigurationBlock.Extent.EndLine = $compositeDscResource.ScriptBlock.Ast.Extent.EndLineNumber
        $dscConfigurationBlock.Extent.Text = $compositeDscResource.ScriptBlock.Ast.Extent.Text

        return $dscConfigurationBlock
    }

    <#
    .DESCRIPTION
    Handles the logic for dsc resources and composite dsc resources.
    #>

    hidden [VmwDscResource] ParseDscResource([string] $Name, [ScriptBlock] $Properties) {
        $moduleName = [string]::Empty

        # gets the resource type by getting the callStack and selecting the top first call which is this function call
        $resourceType = Get-PSCallStack | Select-Object -Skip 1 -First 1 -ExpandProperty Command

        if (-not $this.ResourceNameToInfo.ContainsKey($resourceType)) {
             # if the resource is not found throws exception
             throw ($script:DscResourceNotFoundException -f $resourceType)
        }

        $resourceInfo = $this.ResourceNameToInfo[$resourceType]
        $moduleName = @{
            ModuleName = $resourceInfo.Module.Name
            RequiredVersion = $resourceInfo.Module.Version
        }

        $propsAsText = $Properties.Ast.Extent.Text
        $propsAsText = $propsAsText.Insert(0, '@')
        $parsedProps = Invoke-Expression $propsAsText

        $result = [VmwDscResource]::new(
            $Name,
            $resourceType,
            $moduleName,
            $parsedProps
        )

        return $result
    }
}

# SIG # Begin signature block
# MIIgFQYJKoZIhvcNAQcCoIIgBjCCIAICAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBPZhOw73KHv209
# /oWn8Zd3bW+vcJMlweMvkTERaksrKaCCD8swggTMMIIDtKADAgECAhBdqtQcwalQ
# C13tonk09GI7MA0GCSqGSIb3DQEBCwUAMH8xCzAJBgNVBAYTAlVTMR0wGwYDVQQK
# ExRTeW1hbnRlYyBDb3Jwb3JhdGlvbjEfMB0GA1UECxMWU3ltYW50ZWMgVHJ1c3Qg
# TmV0d29yazEwMC4GA1UEAxMnU3ltYW50ZWMgQ2xhc3MgMyBTSEEyNTYgQ29kZSBT
# aWduaW5nIENBMB4XDTE4MDgxMzAwMDAwMFoXDTIxMDkxMTIzNTk1OVowZDELMAkG
# A1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcMCVBhbG8gQWx0
# bzEVMBMGA1UECgwMVk13YXJlLCBJbmMuMRUwEwYDVQQDDAxWTXdhcmUsIEluYy4w
# ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuswYfqnKot0mNu9VhCCCR
# vVcCrxoSdB6G30MlukAVxgQ8qTyJwr7IVBJXEKJYpzv63/iDYiNAY3MOW+Pb4qGI
# bNpafqxc2WLW17vtQO3QZwscIVRapLV1xFpwuxJ4LYdsxHPZaGq9rOPBOKqTP7Jy
# KQxE/1ysjzacA4NXHORf2iars70VpZRksBzkniDmurvwCkjtof+5krxXd9XSDEFZ
# 9oxeUGUOBCvSLwOOuBkWPlvCnzEqMUeSoXJavl1QSJvUOOQeoKUHRycc54S6Lern
# 2ddmdUDPwjD2cQ3PL8cgVqTsjRGDrCgOT7GwShW3EsRsOwc7o5nsiqg/x7ZmFpSJ
# AgMBAAGjggFdMIIBWTAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDArBgNVHR8E
# JDAiMCCgHqAchhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNybDBhBgNVHSAEWjBY
# MFYGBmeBDAEEATBMMCMGCCsGAQUFBwIBFhdodHRwczovL2Quc3ltY2IuY29tL2Nw
# czAlBggrBgEFBQcCAjAZDBdodHRwczovL2Quc3ltY2IuY29tL3JwYTATBgNVHSUE
# DDAKBggrBgEFBQcDAzBXBggrBgEFBQcBAQRLMEkwHwYIKwYBBQUHMAGGE2h0dHA6
# Ly9zdi5zeW1jZC5jb20wJgYIKwYBBQUHMAKGGmh0dHA6Ly9zdi5zeW1jYi5jb20v
# c3YuY3J0MB8GA1UdIwQYMBaAFJY7U/B5M5evfYPvLivMyreGHnJmMB0GA1UdDgQW
# BBTVp9RQKpAUKYYLZ70Ta983qBUJ1TANBgkqhkiG9w0BAQsFAAOCAQEAlnsx3io+
# W/9i0QtDDhosvG+zTubTNCPtyYpv59Nhi81M0GbGOPNO3kVavCpBA11Enf0CZuEq
# f/ctbzYlMRONwQtGZ0GexfD/RhaORSKib/ACt70siKYBHyTL1jmHfIfi2yajKkMx
# UrPM9nHjKeagXTCGthD/kYW6o7YKKcD7kQUyBhofimeSgumQlm12KSmkW0cHwSSX
# TUNWtshVz+74EcnZtGFI6bwYmhvnTp05hWJ8EU2Y1LdBwgTaRTxlSDP9JK+e63vm
# SXElMqnn1DDXABT5RW8lNt6g9P09a2J8p63JGgwMBhmnatw7yrMm5EAo+K6gVliJ
# LUMlTW3O09MbDTCCBVkwggRBoAMCAQICED141/l2SWCyYX308B7KhiowDQYJKoZI
# hvcNAQELBQAwgcoxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5j
# LjEfMB0GA1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMp
# IDIwMDYgVmVyaVNpZ24sIEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTFF
# MEMGA1UEAxM8VmVyaVNpZ24gQ2xhc3MgMyBQdWJsaWMgUHJpbWFyeSBDZXJ0aWZp
# Y2F0aW9uIEF1dGhvcml0eSAtIEc1MB4XDTEzMTIxMDAwMDAwMFoXDTIzMTIwOTIz
# NTk1OVowfzELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0
# aW9uMR8wHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMTAwLgYDVQQDEydT
# eW1hbnRlYyBDbGFzcyAzIFNIQTI1NiBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqG
# SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXgx4AFq8ssdIIxNdok1FgHnH24ke021hN
# I2JqtL9aG1H3ow0Yd2i72DarLyFQ2p7z518nTgvCl8gJcJOp2lwNTqQNkaC07BTO
# kXJULs6j20TpUhs/QTzKSuSqwOg5q1PMIdDMz3+b5sLMWGqCFe49Ns8cxZcHJI7x
# e74xLT1u3LWZQp9LYZVfHHDuF33bi+VhiXjHaBuvEXgamK7EVUdT2bMy1qEORkDF
# l5KK0VOnmVuFNVfT6pNiYSAKxzB3JBFNYoO2untogjHuZcrf+dWNsjXcjCtvanJc
# YISc8gyUXsBWUgBIzNP4pX3eL9cT5DiohNVGuBOGwhud6lo43ZvbAgMBAAGjggGD
# MIIBfzAvBggrBgEFBQcBAQQjMCEwHwYIKwYBBQUHMAGGE2h0dHA6Ly9zMi5zeW1j
# Yi5jb20wEgYDVR0TAQH/BAgwBgEB/wIBADBsBgNVHSAEZTBjMGEGC2CGSAGG+EUB
# BxcDMFIwJgYIKwYBBQUHAgEWGmh0dHA6Ly93d3cuc3ltYXV0aC5jb20vY3BzMCgG
# CCsGAQUFBwICMBwaGmh0dHA6Ly93d3cuc3ltYXV0aC5jb20vcnBhMDAGA1UdHwQp
# MCcwJaAjoCGGH2h0dHA6Ly9zMS5zeW1jYi5jb20vcGNhMy1nNS5jcmwwHQYDVR0l
# BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIBBjApBgNVHREE
# IjAgpB4wHDEaMBgGA1UEAxMRU3ltYW50ZWNQS0ktMS01NjcwHQYDVR0OBBYEFJY7
# U/B5M5evfYPvLivMyreGHnJmMB8GA1UdIwQYMBaAFH/TZafC3ey78DAJ80M5+gKv
# MzEzMA0GCSqGSIb3DQEBCwUAA4IBAQAThRoeaak396C9pK9+HWFT/p2MXgymdR54
# FyPd/ewaA1U5+3GVx2Vap44w0kRaYdtwb9ohBcIuc7pJ8dGT/l3JzV4D4ImeP3Qe
# 1/c4i6nWz7s1LzNYqJJW0chNO4LmeYQW/CiwsUfzHaI+7ofZpn+kVqU/rYQuKd58
# vKiqoz0EAeq6k6IOUCIpF0yH5DoRX9akJYmbBWsvtMkBTCd7C6wZBSKgYBU/2sn7
# TUyP+3Jnd/0nlMe6NQ6ISf6N/SivShK9DbOXBd5EDBX6NisD3MFQAfGhEV0U5eK9
# J0tUviuEXg+mw3QFCu+Xw4kisR93873NQ9TxTKk/tYuEr2Ty0BQhMIIFmjCCA4Kg
# AwIBAgIKYRmT5AAAAAAAHDANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQDEyBNaWNyb3NvZnQgQ29kZSBW
# ZXJpZmljYXRpb24gUm9vdDAeFw0xMTAyMjIxOTI1MTdaFw0yMTAyMjIxOTM1MTda
# MIHKMQswCQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNV
# BAsTFlZlcmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAyMDA2IFZl
# cmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMT
# PFZlcmlTaWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBB
# dXRob3JpdHkgLSBHNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8k
# CAgpejWeYAyq50s7Ttx8vDxFHLsr4P4pAvlXCKNkhRUn9fGtyDGJXSLoKqqmQrOP
# +LlVt7G3S7P+j34HV+zvQ9tmYhVhz2ANpNje+ODDYgg9VBPrScpZVIUm5SuPG5/r
# 9aGRwjNJ2ENjalJL0o/ocFFN0Ylpe8dw9rPcEnTbe11LVtOWvxV3obD0oiXyrxyS
# Zxjl9AYE75C55ADk3Tq1Gf8CuvQ87uCL6zeL7PTXrPL28D2v3XWRMxkdHEDLdCQZ
# IZPZFP6sKlLHj9UESeSNY0eIPGmDy/5HvSt+T8WVrg6d1NFDwGdz4xQIfuU/n3O4
# MwrPXT80h5aK7lPoJRUCAwEAAaOByzCByDARBgNVHSAECjAIMAYGBFUdIAAwDwYD
# VR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCAYYwHQYDVR0OBBYEFH/TZafC3ey78DAJ
# 80M5+gKvMzEzMB8GA1UdIwQYMBaAFGL7CiFbf0NuEdoJVFBr9dKWcfGeMFUGA1Ud
# HwROMEwwSqBIoEaGRGh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3By
# b2R1Y3RzL01pY3Jvc29mdENvZGVWZXJpZlJvb3QuY3JsMA0GCSqGSIb3DQEBBQUA
# A4ICAQCBKoIWjDRnK+UD6zR7jKKjUIr0VYbxHoyOrn3uAxnOcpUYSK1iEf0g/T9H
# BgFa4uBvjBUsTjxqUGwLNqPPeg2cQrxc+BnVYONp5uIjQWeMaIN2K4+Toyq1f75Z
# +6nJsiaPyqLzghuYPpGVJ5eGYe5bXQdrzYao4mWAqOIV4rK+IwVqugzzR5NNrKSM
# B3k5wGESOgUNiaPsn1eJhPvsynxHZhSR2LYPGV3muEqsvEfIcUOW5jIgpdx3hv08
# 44tx23ubA/y3HTJk6xZSoEOj+i6tWZJOfMfyM0JIOFE6fDjHGyQiKEAeGkYfF9sY
# 9/AnNWy4Y9nNuWRdK6Ve78YptPLH+CHMBLpX/QG2q8Zn+efTmX/09SL6cvX9/zoc
# Qjqh+YAYpe6NHNRmnkUB/qru//sXjzD38c0pxZ3stdVJAD2FuMu7kzonaknAMK5m
# yfcjKDJ2+aSDVshIzlqWqqDMDMR/tI6Xr23jVCfDn4bA1uRzCJcF29BUYl4DSMLV
# n3+nZozQnbBP1NOYX0t6yX+yKVLQEoDHD1S2HmfNxqBsEQOE00h15yr+sDtuCjqm
# a3aZBaPxd2hhMxRHBvxTf1K9khRcSiRqZ4yvjZCq0PZ5IRuTJnzDzh69iDiSrkXG
# GWpJULMF+K5ZN4pqJQOUsVmBUOi6g4C3IzX0drlnHVkYrSCNlDGCD6Awgg+cAgEB
# MIGTMH8xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3JhdGlv
# bjEfMB0GA1UECxMWU3ltYW50ZWMgVHJ1c3QgTmV0d29yazEwMC4GA1UEAxMnU3lt
# YW50ZWMgQ2xhc3MgMyBTSEEyNTYgQ29kZSBTaWduaW5nIENBAhBdqtQcwalQC13t
# onk09GI7MA0GCWCGSAFlAwQCAQUAoIGWMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3
# AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMCoGCisGAQQBgjcCAQwx
# HDAaoRiAFmh0dHA6Ly93d3cudm13YXJlLmNvbS8wLwYJKoZIhvcNAQkEMSIEIKUc
# 2O7ChualkOy1BZ7JLR8ddL7M0fPDLsnGsQdUJOQyMA0GCSqGSIb3DQEBAQUABIIB
# AIoWM/O7vi0tglZrQPWiJHYXHAyB1uSu6LNlFOuM1+WeH9luiHQHuTrFhrnmVM/D
# NyJvD44tRgi3FGOwJer+Yisc4zvM3L3K/kde199cZd+djzVrJTrenL45rr8XgMk9
# 5ZFSICnRoWBElf5P//6ijiSjMJmSM6u0bZ5FGg64PRfVC9xYp9QkGYXl3fx0Td1h
# nq+/Nq/zkGD9PJ9CXcs5b297bX1XBx3NGEfhrmnKzUCMd4sscsXFYRt6hQGkW6I9
# NyTI/S4AiQXY1l7RSGzB0oBdWyrhYhPtcISNyCxTb0UvNqfXe6BsQb3RvLH4Lv4G
# tcBq/ls84JuARJIovRA4iQOhgg1EMIINQAYKKwYBBAGCNwMDATGCDTAwgg0sBgkq
# hkiG9w0BBwKggg0dMIINGQIBAzEPMA0GCWCGSAFlAwQCAQUAMHcGCyqGSIb3DQEJ
# EAEEoGgEZjBkAgEBBglghkgBhv1sBwEwMTANBglghkgBZQMEAgEFAAQgT1zsz0cH
# 3UjvMkm0yUq75ar6PRRCHg2VZqT83P585vgCECsqrGcocCZ83k2tZ6QSLV0YDzIw
# MjEwMjI0MDkwMDU1WqCCCjcwggT+MIID5qADAgECAhANQkrgvjqI/2BAIc4UAPDd
# MA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD
# ZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwHhcNMjEwMTAxMDAw
# MDAwWhcNMzEwMTA2MDAwMDAwWjBIMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xIDAeBgNVBAMTF0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIxMIIB
# IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwuZhhGfFivUNCKRFymNrUdc6
# EUK9CnV1TZS0DFC1JhD+HchvkWsMlucaXEjvROW/m2HNFZFiWrj/ZwucY/02aoH6
# KfjdK3CF3gIY83htvH35x20JPb5qdofpir34hF0edsnkxnZ2OlPR0dNaNo/Go+Ev
# Gzq3YdZz7E5tM4p8XUUtS7FQ5kE6N1aG3JMjjfdQJehk5t3Tjy9XtYcg6w6OLNUj
# 2vRNeEbjA4MxKUpcDDGKSoyIxfcwWvkUrxVfbENJCf0mI1P2jWPoGqtbsR0wwptp
# grTb/FZUvB+hh6u+elsKIC9LCcmVp42y+tZji06lchzun3oBc/gZ1v4NSYS9AQID
# AQABo4IBuDCCAbQwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYDVR0l
# AQH/BAwwCgYIKwYBBQUHAwgwQQYDVR0gBDowODA2BglghkgBhv1sBwEwKTAnBggr
# BgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMB8GA1UdIwQYMBaA
# FPS24SAd/imu0uRhpbKiJbLIFzVuMB0GA1UdDgQWBBQ2RIaOpLqwZr68KC0dRDbd
# 42p6vDBxBgNVHR8EajBoMDKgMKAuhixodHRwOi8vY3JsMy5kaWdpY2VydC5jb20v
# c2hhMi1hc3N1cmVkLXRzLmNybDAyoDCgLoYsaHR0cDovL2NybDQuZGlnaWNlcnQu
# Y29tL3NoYTItYXNzdXJlZC10cy5jcmwwgYUGCCsGAQUFBwEBBHkwdzAkBggrBgEF
# BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME8GCCsGAQUFBzAChkNodHRw
# Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEVGlt
# ZXN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4IBAQBIHNy16ZojvOca5yAO
# jmdG/UJyUXQKI0ejq5LSJcRwWb4UoOUngaVNFBUZB3nw0QTDhtk7vf5EAmZN7Wmk
# D/a4cM9i6PVRSnh5Nnont/PnUp+Tp+1DnnvntN1BIon7h6JGA0789P63ZHdjXyNS
# aYOC+hpT7ZDMjaEXcw3082U5cEvznNZ6e9oMvD0y0BvL9WH8dQgAdryBDvjA4VzP
# xBFy5xtkSdgimnUVQvUtMjiB2vRgorq0Uvtc4GEkJU+y38kpqHNDUdq9Y9YfW5v3
# LhtPEx33Sg1xfpe39D+E68Hjo0mh+s6nv1bPull2YYlffqe0jmd4+TaY4cso2luH
# poovMIIFMTCCBBmgAwIBAgIQCqEl1tYyG35B5AXaNpfCFTANBgkqhkiG9w0BAQsF
# ADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL
# ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElE
# IFJvb3QgQ0EwHhcNMTYwMTA3MTIwMDAwWhcNMzEwMTA3MTIwMDAwWjByMQswCQYD
# VQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGln
# aWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGlt
# ZXN0YW1waW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvdAy
# 7kvNj3/dqbqCmcU5VChXtiNKxA4HRTNREH3Q+X1NaH7ntqD0jbOI5Je/YyGQmL8T
# vFfTw+F+CNZqFAA49y4eO+7MpvYyWf5fZT/gm+vjRkcGGlV+Cyd+wKL1oODeIj8O
# /36V+/OjuiI+GKwR5PCZA207hXwJ0+5dyJoLVOOoCXFr4M8iEA91z3FyTgqt30A6
# XLdR4aF5FMZNJCMwXbzsPGBqrC8HzP3w6kfZiFBe/WZuVmEnKYmEUeaC50ZQ/ZQq
# LKfkdT66mA+Ef58xFNat1fJky3seBdCEGXIX8RcG7z3N1k3vBkL9olMqT4UdxB08
# r8/arBD13ays6Vb/kwIDAQABo4IBzjCCAcowHQYDVR0OBBYEFPS24SAd/imu0uRh
# pbKiJbLIFzVuMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMBIGA1Ud
# EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
# BwMIMHkGCCsGAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln
# aWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j
# b20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3J0MIGBBgNVHR8EejB4MDqgOKA2
# hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290
# Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRB
# c3N1cmVkSURSb290Q0EuY3JsMFAGA1UdIARJMEcwOAYKYIZIAYb9bAACBDAqMCgG
# CCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAsGCWCGSAGG
# /WwHATANBgkqhkiG9w0BAQsFAAOCAQEAcZUS6VGHVmnN793afKpjerN4zwY3QITv
# S4S/ys8DAv3Fp8MOIEIsr3fzKx8MIVoqtwU0HWqumfgnoma/Capg33akOpMP+LLR
# 2HwZYuhegiUexLoceywh4tZbLBQ1QwRostt1AuByx5jWPGTlH0gQGF+JOGFNYkYk
# h2OMkVIsrymJ5Xgf1gsUpYDXEkdws3XVk4WTfraSZ/tTYYmo9WuWwPRYaQ18yAGx
# uSh1t5ljhSKMYcp5lH5Z/IwP42+1ASa2bKXuh1Eh5Fhgm7oMLSttosR+u8QlK0cC
# CHxJrhO24XxCQijGGFbPQTS2Zl22dHv1VjMiLyI2skuiSpXY9aaOUjGCAk0wggJJ
# AgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAX
# BgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIg
# QXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0ECEA1CSuC+Ooj/YEAhzhQA8N0wDQYJ
# YIZIAWUDBAIBBQCggZgwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqG
# SIb3DQEJBTEPFw0yMTAyMjQwOTAwNTVaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYE
# FOHXgqjhkb7va8oWkbWqtJSmJJvzMC8GCSqGSIb3DQEJBDEiBCATYX2ZKX4EXe4p
# Fikl9mkc2Ru3CJK5Up8frjmBZKVv1jANBgkqhkiG9w0BAQEFAASCAQC/QrX4o69D
# bE/BDbeLmA5TqBWIKV/pvBzj0hy/hEhzOdj83Et9/Y0S44ASLaNGi7Bsmda5MHnv
# 6yaXQVUyqiS1kVBOMRJ+FNDji2qyqj1ZDglwT5gOaZ47O0Z1wJdMrw3lddxs7kMZ
# OLIClH5hM2y4Tf9HKVLvm2UyjUiU3oa0IL7a+s2USga2aPWULfAjZKukinGL82qL
# CaujExjkSUuFKZIr+soPhzVmYzW0PFt7ihaZVYD3xosQzyHY+hH9LMZeH9m+PWI9
# KxUsbPU+S0+rBXbqysnz3yEdKZfXctNWiLYUiD6EgS+pEKsSBsbWd4R+tloX/wQD
# 3Sl/IzkNK6WV
# SIG # End signature block