Functions/Public/New-AzUpgradeModulePlan.ps1

function New-AzUpgradeModulePlan
{
    <#
    .SYNOPSIS
        Generates a new upgrade plan for migrating to the Az PowerShell module.
 
    .DESCRIPTION
        Generates a new upgrade plan for migrating to the Az PowerShell module. The upgrade plan details the specific file/offset points that require changes when moving from AzureRM commands to Az commands.
 
    .PARAMETER FromAzureRmVersion
        Specifies the AzureRM module version used in your existing PowerShell scripts(s) or modules.
 
    .PARAMETER ToAzVersion
        Specifies the Az module version to upgrade to. Currently, only Az version 6.1.0 is supported.
 
    .PARAMETER FilePath
        Specifies the path to a single PowerShell file.
 
    .PARAMETER DirectoryPath
        Specifies the path to a folder where PowerShell scripts or modules reside.
 
    .PARAMETER AzureRmCmdReference
        Specifies the AzureRM command references output from the Find-AzUpgradeCommandReference cmdlet.
 
    .PARAMETER AzureRmModuleSpec
        Specifies an optional parameter to provide a pre-loaded AzureRM module spec, returned from Get-AzUpgradeCmdletSpec.
 
    .PARAMETER AzModuleSpec
        Specifies an optional parameter to provide a pre-loaded Az module spec, returned from Get-AzUpgradeCmdletSpec.
 
    .PARAMETER AzAliasMappingSpec
        Specifies an optional parameter to provide a pre-loaded Az cmdlet alias mapping table, returned from Get-AzUpgradeAliasSpec.
 
    .EXAMPLE
        The following example generates a new Az module upgrade plan for the script file 'C:\Scripts\my-azure-script.ps1'.
 
        New-AzUpgradeModulePlan -FromAzureRmVersion 6.13.1 -ToAzVersion 6.1.0 -FilePath 'C:\Scripts\my-azure-script.ps1'
 
    .EXAMPLE
        The following example generates a new Az module upgrade plan for the script and module files located under C:\Scripts.
 
        New-AzUpgradeModulePlan -FromAzureRmVersion 6.13.1 -ToAzVersion 6.1.0 -DirectoryPath 'C:\Scripts'
 
    .EXAMPLE
        The following example generates a new Az module upgrade plan for the script and module files under C:\Scripts.
 
        $references = Find-AzUpgradeCommandReference -DirectoryPath 'C:\Scripts' -AzureRmVersion '6.13.1'
        New-AzUpgradeModulePlan -ToAzVersion 6.1.0 -AzureRmCmdReference $references
 
    .EXAMPLE
        The following example generates a new Az module upgrade plan for the script and module files under several directories.
        Module specs are pre-loaded here to avoid re-loading the spec each time a plan is generated.
 
        # pre-load specifications
        $armSpec = Get-AzUpgradeCmdletSpec -ModuleName "AzureRM" -ModuleVersion "6.13.1"
        $azSpec = Get-AzUpgradeCmdletSpec -ModuleName "Az" -ModuleVersion "6.1.0"
        $azAliases = Get-AzUpgradeAliasSpec -ModuleVersion "6.1.0"
 
        # execute a batch of module upgrades
        $plan1 = New-AzUpgradeModulePlan -DirectoryPath 'C:\Scripts1' -FromAzureRmVersion '6.13.1' -ToAzVersion '6.1.0' -AzureRmModuleSpec $armSpec -AzModuleSpec $azSpec -AzAliasMappingSpec $azAliases
        $plan2 = New-AzUpgradeModulePlan -DirectoryPath 'C:\Scripts2' -FromAzureRmVersion '6.13.1' -ToAzVersion '6.1.0' -AzureRmModuleSpec $armSpec -AzModuleSpec $azSpec -AzAliasMappingSpec $azAliases
        $plan3 = New-AzUpgradeModulePlan -DirectoryPath 'C:\Scripts3' -FromAzureRmVersion '6.13.1' -ToAzVersion '6.1.0' -AzureRmModuleSpec $armSpec -AzModuleSpec $azSpec -AzAliasMappingSpec $azAliases
    #>

    [CmdletBinding()]
    Param
    (
        [Parameter(
            Mandatory=$true,
            ParameterSetName="FromReferences",
            HelpMessage='Specify the AzureRM command references collection output from the Find-AzUpgradeCommandReference cmdlet.')]
        [CommandReference[]]
        $AzureRmCmdReference,

        [Parameter(
            Mandatory=$true,
            ParameterSetName="FromNewSearchByFile",
            HelpMessage='Specify the Az module version to upgrade to.')]
        [Parameter(
            Mandatory=$true,
            ParameterSetName="FromNewSearchByDirectory",
            HelpMessage='Specify the Az module version to upgrade to.')]
        [System.String]
        [ValidateSet('6.13.1')]
        $FromAzureRmVersion,

        [Parameter(
            Mandatory=$true,
            ParameterSetName="FromNewSearchByFile",
            HelpMessage="Specify the path to a single PowerShell file.")]
        [System.String]
        [ValidateNotNullOrEmpty()]
        $FilePath,

        [Parameter(
            Mandatory=$true,
            ParameterSetName="FromNewSearchByDirectory",
            HelpMessage="Specify the path to the folder where PowerShell scripts or modules reside.")]
        [System.String]
        [ValidateNotNullOrEmpty()]
        $DirectoryPath,

        [Parameter(
            Mandatory=$true,
            HelpMessage='Specify the Az module version to upgrade to.')]
        [System.String]
        [ValidateSet('6.1.0')]
        $ToAzVersion,

        [Parameter(Mandatory=$false)]
        [System.Collections.Generic.Dictionary[System.String, CommandDefinition]]
        $AzureRmModuleSpec,

        [Parameter(Mandatory=$false)]
        [System.Collections.Generic.Dictionary[System.String, CommandDefinition]]
        $AzModuleSpec,

        [Parameter(Mandatory=$false)]
        [System.Collections.Generic.Dictionary[System.String, System.String]]
        $AzAliasMappingSpec
    )
    Process
    {
        $cmdStarted = Get-Date

        # if an existing set of command references was not provided
        # then call the Find cmdlet to search for those references.

        if ($PSCmdlet.ParameterSetName -eq 'FromNewSearchByFile')
        {
            if ($PSBoundParameters.ContainsKey('AzureRmModuleSpec'))
            {
                Write-Verbose -Message "Searching for commands to upgrade, by file, with pre-loaded module spec."
                $AzureRmCmdReference = Find-AzUpgradeCommandReference -FilePath $FilePath -AzureRmModuleSpec $AzureRmModuleSpec
            }
            else
            {
                Write-Verbose -Message "Searching for commands to upgrade, by file."
                $AzureRmCmdReference = Find-AzUpgradeCommandReference -FilePath $FilePath -AzureRmVersion $FromAzureRmVersion
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'FromNewSearchByDirectory')
        {
            if ($PSBoundParameters.ContainsKey('AzureRmModuleSpec'))
            {
                Write-Verbose -Message "Searching for commands to upgrade, by directory, with pre-loaded module spec."
                $AzureRmCmdReference = Find-AzUpgradeCommandReference -DirectoryPath $DirectoryPath -AzureRmModuleSpec $AzureRmModuleSpec
            }
            else
            {
                Write-Verbose -Message "Searching for commands to upgrade, by directory."
                $AzureRmCmdReference = Find-AzUpgradeCommandReference -DirectoryPath $DirectoryPath -AzureRmVersion $FromAzureRmVersion
            }
        }

        # we can't generate an upgrade plan without some cmdlet references, so quit early here if required.

        if ($AzureRmCmdReference -eq $null -or $AzureRmCmdReference.Count -eq 0)
        {
            Write-Verbose -Message "No AzureRm command references were found. No upgrade plan will be generated."
            return
        }
        else
        {
            Write-Verbose -Message "$($AzureRmCmdReference.Count) AzureRm command reference(s) were found. Upgrade plan will be generated."
        }

        if ($PSBoundParameters.ContainsKey('AzModuleSpec') -eq $false)
        {
            Write-Verbose -Message "Importing cmdlet spec for Az $ToAzVersion"
            $AzModuleSpec = Get-AzUpgradeCmdletSpec -ModuleName "Az" -ModuleVersion $ToAzVersion
        }
        else
        {
            Write-Verbose -Message "Az module spec was provided at runtime, skipping module spec import."
        }

        if ($PSBoundParameters.ContainsKey('AzAliasMappingSpec') -eq $false)
        {
            Write-Verbose -Message "Importing alias mapping spec for Az $ToAzVersion"
            $AzAliasMappingSpec = Get-AzUpgradeAliasSpec -ModuleVersion $ToAzVersion
        }
        else
        {
            Write-Verbose -Message "Az alias mapping spec was provided at runtime, skipping alias spec import."
        }

        $defaultParamNames = @("Debug", "ErrorAction", "ErrorVariable", "InformationAction", "InformationVariable", "OutVariable", "OutBuffer", "PipelineVariable", "Verbose", "WarningAction", "WarningVariable", "WhatIf", "Confirm")

        # synchronous results output instead of async. the reason for this is that
        # we need to sort the object results before returning them to the caller.

        $planSteps = New-Object -TypeName 'System.Collections.Generic.List[UpgradePlan]'
        $planWarningSteps = New-Object -TypeName 'System.Collections.Generic.List[UpgradePlan]'
        $planErrorSteps = New-Object -TypeName 'System.Collections.Generic.List[UpgradePlan]'

        foreach ($rmCmdlet in $AzureRmCmdReference)
        {
            Write-Verbose -Message "Checking upgrade potential for instance of $($rmCmdlet.CommandName)"

            if ($AzAliasMappingSpec.ContainsKey($rmCmdlet.CommandName) -eq $false)
            {
                $errorResult = New-Object -TypeName UpgradePlan
                $errorResult.UpgradeType = [UpgradeStepType]::Cmdlet
                $errorResult.SourceCommand = $rmCmdlet
                $errorResult.FullPath = $rmCmdlet.FullPath
                $errorResult.StartOffset = $rmCmdlet.StartOffset
                $errorResult.Location = $rmCmdlet.Location
                $errorResult.Original = $rmCmdlet.CommandName
                $errorResult.PlanResultReason = "No matching upgrade alias found. Command cannot be automatically upgraded."
                $errorResult.PlanResult = [PlanResultReasonCode]::ErrorNoUpgradeAlias
                $errorResult.PlanSeverity = [DiagnosticSeverity]::Error

                $planErrorSteps.Add($errorResult)

                continue
            }

            $resolvedCommandName = $AzAliasMappingSpec[$rmCmdlet.CommandName]

            if ($AzModuleSpec.ContainsKey($resolvedCommandName) -eq $false)
            {
                $errorResult = New-Object -TypeName UpgradePlan
                $errorResult.UpgradeType = [UpgradeStepType]::Cmdlet
                $errorResult.SourceCommand = $rmCmdlet
                $errorResult.FullPath = $rmCmdlet.FullPath
                $errorResult.StartOffset = $rmCmdlet.StartOffset
                $errorResult.Location = $rmCmdlet.Location
                $errorResult.Original = $rmCmdlet.CommandName
                $errorResult.PlanResultReason = "No Az cmdlet spec found for $resolvedCommandName. Command cannot be automatically upgraded."
                $errorResult.PlanResult = [PlanResultReasonCode]::ErrorNoModuleSpecMatch
                $errorResult.PlanSeverity = [DiagnosticSeverity]::Error

                $planErrorSteps.Add($errorResult)

                continue
            }

            $cmdletUpgrade = New-Object -TypeName UpgradePlan
            $cmdletUpgrade.Original = $rmCmdlet.CommandName
            $cmdletUpgrade.Replacement = $resolvedCommandName
            $cmdletUpgrade.UpgradeType = [UpgradeStepType]::Cmdlet
            $cmdletUpgrade.SourceCommand = $rmCmdlet
            $cmdletUpgrade.FullPath = $rmCmdlet.FullPath
            $cmdletUpgrade.StartOffset = $rmCmdlet.StartOffset
            $cmdletUpgrade.Location = $rmCmdlet.Location

            $cmdletUpgrade.PlanResultReason = "Command can be automatically upgraded."
            $cmdletUpgrade.PlanResult = [PlanResultReasonCode]::ReadyToUpgrade
            $cmdletUpgrade.PlanSeverity = [DiagnosticSeverity]::Information
            $planSteps.Add($cmdletUpgrade)

            # check if parameters need to be updated

            if ($rmCmdlet.Parameters.Count -gt 0)
            {
                $resolvedAzCommand = $AzModuleSpec[$resolvedCommandName]

                foreach ($rmParam in $rmCmdlet.Parameters)
                {
                    if ($defaultParamNames -contains $rmParam.Name)
                    {
                        # direct match to a built-in default parameter
                        # no changes required.
                        continue
                    }

                    $matchedDirectName = $resolvedAzCommand.Parameters | Where-Object -FilterScript { $_.Name -eq $rmParam.Name }

                    if ($matchedDirectName -ne $null)
                    {
                        # direct match to the upgraded cmdlet's parameter name.
                        # no changes required.
                        continue
                    }

                    $matchedAliasName = $resolvedAzCommand.Parameters | Where-Object -FilterScript { $_.Aliases -contains $rmParam.Name }

                    if ($matchedAliasName -ne $null)
                    {
                        # alias match to the upgraded cmdlet's parameter name.
                        # we should add an upgrade step to swap to use the non-aliased name.

                        $paramUpgrade = New-Object -TypeName UpgradePlan
                        $paramUpgrade.Original = $rmParam.Name
                        $paramUpgrade.Replacement = $matchedAliasName.Name
                        $paramUpgrade.UpgradeType = [UpgradeStepType]::CmdletParameter
                        $paramUpgrade.SourceCommand = $rmCmdlet
                        $paramUpgrade.FullPath = $rmCmdlet.FullPath
                        $paramUpgrade.StartOffset = $rmParam.StartOffset
                        $paramUpgrade.SourceCommandParameter = $rmParam
                        $paramUpgrade.Location = $rmParam.Location
                        $paramUpgrade.PlanResultReason = "Command parameter can be automatically upgraded."
                        $paramUpgrade.PlanResult = [PlanResultReasonCode]::ReadyToUpgrade
                        $paramUpgrade.PlanSeverity = [DiagnosticSeverity]::Information

                        $planSteps.Add($paramUpgrade)

                        continue
                    }

                    # no direct match and no alias match?
                    # this could mean a breaking change that requires manual adjustments, or it could be a dynamic parameter.

                    if ($resolvedAzCommand.SupportsDynamicParameters)
                    {
                        $paramWarning = New-Object -TypeName UpgradePlan
                        $paramWarning.Original = $rmParam.Name
                        $paramWarning.UpgradeType = [UpgradeStepType]::CmdletParameter
                        $paramWarning.SourceCommand = $rmCmdlet
                        $paramWarning.FullPath = $rmCmdlet.FullPath
                        $paramWarning.StartOffset = $rmParam.StartOffset
                        $paramWarning.SourceCommandParameter = $rmParam
                        $paramWarning.Location = $rmParam.Location
                        $paramWarning.PlanResultReason = "Parameter was not found in $resolvedCommandName or its aliases, but this may not be a problem because this cmdlet also supports dynamic parameters."
                        $paramWarning.PlanResult = [PlanResultReasonCode]::WarningDynamicParameter
                        $paramWarning.PlanSeverity = [DiagnosticSeverity]::Warning

                        $planErrorSteps.Add($paramWarning)
                    }
                    else
                    {
                        $paramError = New-Object -TypeName UpgradePlan
                        $paramError.Original = $rmParam.Name
                        $paramError.UpgradeType = [UpgradeStepType]::CmdletParameter
                        $paramError.SourceCommand = $rmCmdlet
                        $paramError.FullPath = $rmCmdlet.FullPath
                        $paramError.StartOffset = $rmParam.StartOffset
                        $paramError.SourceCommandParameter = $rmParam
                        $paramError.Location = $rmParam.Location
                        $paramError.PlanResultReason = "Parameter was not found in $resolvedCommandName or its aliases. This may indicate a breaking change that requires attention."
                        $paramError.PlanResult = [PlanResultReasonCode]::ErrorParameterNotFound
                        $paramError.PlanSeverity = [DiagnosticSeverity]::Error
    
                        $planErrorSteps.Add($paramError)
                    }
                }
            }
        }

        # send metrics, if enabled (and while the collections are still seperated)

        Send-MetricsIfDataCollectionEnabled -Operation Plan `
            -ParameterSetName $PSCmdlet.ParameterSetName `
            -Duration ((Get-Date) - $cmdStarted) `
            -Properties ([PSCustomObject]@{
                ToAzureModuleName = "Az"
                ToAzureModuleVersion = $ToAzVersion
                UpgradeStepsCount = $planSteps.Count
                PlanWarnings = $planWarningSteps
                PlanErrors = $planErrorSteps
            })

        # join the collections into one output result.

        $planSteps.AddRange($planWarningSteps)
        $planSteps.AddRange($planErrorSteps)

        # sort the upgrade steps to by file, then offset descending.
        # the reason for this is updates must be made in descending offset order
        # otherwise file positions will change for subsequent swaps.

        $filter1 = @{ Expression = 'FullPath'; Ascending = $true }
        $filter2 = @{ Expression = 'StartOffset'; Descending = $true }

        $planSteps = $planSteps | Sort-Object -Property $filter1, $filter2

        # now that we have a sorted collection, add in the step order number
        # for extra clarity in the upgrade plan.

        for ([int]$i = 0; $i -lt $planSteps.Count; $i++)
        {
            $planSteps[$i].Order = ($i + 1)
        }

        Write-Output -InputObject $planSteps
    }
}
# SIG # Begin signature block
# MIIjkgYJKoZIhvcNAQcCoIIjgzCCI38CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAoANgmS8TPrvBi
# CZuyat0WOryxMfbvpxqpHUNXsfNYw6CCDYEwggX/MIID56ADAgECAhMzAAAB32vw
# LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn
# s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw
# PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS
# yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG
# 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh
# EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH
# tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS
# 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp
# TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok
# t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4
# b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao
# mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD
# Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt
# VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G
# CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+
# Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82
# oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVZzCCFWMCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgd3GIAJ/3
# lMDmM0cbu1LjNsTqpXlbbgmhMtDYafbDPEUwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQAybLYXxORVl9tod/kMr/Opl1vSTYQl0wPIp1QZogt3
# tIUWKCEg00Tb6YtbzViOMLxaZCHidXsbkb9QcTDNj6lEdsTWRRwpDcDLXYC19kIb
# Cmy8p/USBL8N9T+yxEanL1QXa3ikCRIY4RDY8QZvxBLGLP9gZ1NCDawR+QRbEelb
# n7uuBibU7kARsWUzmHL+eMKwoSYkoFsBSaQJZX0WgTdykwrv1d7CqicQnHOAVBfF
# KAn00PIudTylHOAWqBHhrZGZ8A7gYyPDG7iwQEEXOM4ZlTbZyBykCOYvde8YAv66
# jH4Rt+1sV3GQ896jd5Ve9TuEKUUApItcUfkJkds9CRwzoYIS8TCCEu0GCisGAQQB
# gjcDAwExghLdMIIS2QYJKoZIhvcNAQcCoIISyjCCEsYCAQMxDzANBglghkgBZQME
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEID4NV9jtqlp/DSR1o1OkYyE/zzIXRlj73nhE5DFz
# g/IIAgZg04/RWVMYEzIwMjEwNzA1MDg0NTUzLjYwMlowBIACAfSggdSkgdEwgc4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjpGODdBLUUzNzQtRDdCOTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCDkQwggT1MIID3aADAgECAhMzAAABY4tkxsmFlmV2AAAA
# AAFjMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIxMDExNDE5MDIyM1oXDTIyMDQxMTE5MDIyM1owgc4xCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpGODdB
# LUUzNzQtRDdCOTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK1xF/YSncl0YpL/qN2F
# nfwjf0i8a+C4ELz5UZy3JOU54XH+rHv1y3LgKYGu3wrtNSEY4Hz5z6PRlEJvv7aK
# 2tm7WvFSes7iLFhQ08DV4hVx5zF6ll5uN2ti2fJNZ6JDjMSVYuY/waYdNFo7N4l8
# x87/1STIob3PDiaqAoEZ1hEbmuRr44EKP/3RDgo/AY0o01zAF4k5Hvyrfz03GaJI
# Z6EIIgbYbE6E2LX2cJZ963aNYPZLYVbNnTviO7p2eGHtaAkn08QrzW9pz1aGCTUl
# DLRULnMiQVLNigaU1v8OTzv7alAInTlRfFLvPIV0JJ2SPq+wVLxPGhiVswErX98/
# szUCAwEAAaOCARswggEXMB0GA1UdDgQWBBQJNcrxdnJn7j8xWp9Gx5A+1989KTAf
# BgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBH
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNU
# aW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0
# YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsG
# AQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQACiEGCB9eO4lPOjjICYfsTqam9IqdR
# tMj20UBBVLhufvP9xvloI8LZ9wOPq4sCoJSdhMLawUWZd67vFlM/iBP+7Xkq109T
# aeQSE4Nc9ueM15flEvao4ZtzGoWTcxpC+alYY0kVGIj6SxBSxnCkoZesT44WVITB
# QL/43PmHxVAFD0C1cDzza5nv1CSiDvnZ4qNxpP6af9IYfKbJB4bJxBq52FZVQqR4
# dA6Na7H4sThh1AY/qYc6kzmSphUvEzCq5xPZ8+TlsoNNZYz6TAR6qnefT2D/3Dsn
# 7XmO+wNjIi6AEWQJHaqwB7R5OWO7QJ7p07Rl/4TvkNMzvZl8BBSfX7YjMIIGcTCC
# BFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv
# b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcN
# MjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIw
# DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0
# VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEw
# RA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQe
# dGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKx
# Xf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4G
# kbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEA
# AaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7
# fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0g
# AQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYB
# BQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUA
# bQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOh
# IW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS
# +7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlK
# kVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon
# /VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOi
# PPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/
# fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCII
# YdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0
# cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7a
# KLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQ
# cdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+
# NR4Iuto229Nfj950iEkSoYIC0jCCAjsCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBP
# cGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpG
# ODdBLUUzNzQtRDdCOTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUA7SxgHt1J3SqTTSqzLcrMGZQBYe+ggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOSNNxIwIhgPMjAyMTA3MDUxMTQ1MjJaGA8yMDIxMDcwNjExNDUyMlowdzA9Bgor
# BgEEAYRZCgQBMS8wLTAKAgUA5I03EgIBADAKAgEAAgIYnwIB/zAHAgEAAgIRwDAK
# AgUA5I6IkgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAAy5RnUBAoslmUue
# s9sgwlgrh8m2nE2cUCI6BFZyzy0be2icSnGuhl97lfRkxH64K9Edfq/PdPKWFpOM
# iXgMQJVW8JzLAyfxtulHFcUySroivYDSxwil5bKcZ9D9+65FOC35bETJKnjHbMX5
# 84H+Ugvgh59zlWtAbQUGMmC0RsshMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAFji2TGyYWWZXYAAAAAAWMwDQYJYIZIAWUD
# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQgmcplsptt3a3+yvQaKi0CVRZnZ0PVVGFihAfzr7aZORYwgfoGCyqGSIb3
# DQEJEAIvMYHqMIHnMIHkMIG9BCCcWd2XHaFjoSikKbi4y9AYBIpLBy9Rb16ns1Gr
# EfQjajCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB
# Y4tkxsmFlmV2AAAAAAFjMCIEIMa9/DFPSMlBsvXkWetp7dJAQKumyv7Q9/Vs7RKa
# IwrdMA0GCSqGSIb3DQEBCwUABIIBAB+soezfSeR6ypLluO2UerFUPBJVWnylN3I1
# h4HeQk9Ead+WWcJFHZVmIZAjvcILBq/e3TBe5WN51yTW3W14NrYAZe3hNMI90lbJ
# p3Y+qZbQsHYPyFhqK2CEp1mzxSmGAGd/OMDc+f52t5QRGZic9ZMF/g3h6jNV1LoX
# Zbk9+lWUxN3z7czMvXjSCENOJ8itq3Q3HAIlsFXYVjddlgrkE+LoS6G7vlgO/6EH
# 5p6NVJ8SPmrsLbsZmpSsQ1ktXXE8+rAMhT/0TzujM6xwmyt6sncmRf7DG6NsnEsv
# BQw+cqJbBnvCcHLo4ksKmLCGVtUGbjnwSDQP1Nu/vEyvZcz0DP0=
# SIG # End signature block