SDNExpress.ps1

# --------------------------------------------------------------
# Copyright © Microsoft Corporation. All Rights Reserved.
# Microsoft Corporation (or based on where you live, one of its affiliates) licenses this sample code for your internal testing purposes only.
# Microsoft provides the following sample code AS IS without warranty of any kind. The sample code arenot supported under any Microsoft standard support program or services.
# Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose.
# The entire risk arising out of the use or performance of the sample code remains with you.
# In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the code be liable for any damages whatsoever
# (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss)
# arising out of the use of or inability to use the sample code, even if Microsoft has been advised of the possibility of such damages.
# ---------------------------------------------------------------
<#
.SYNOPSIS
    Deploys and configures the Microsoft SDN infrastructure,
    including creation of the network controller, Software Load Balancer MUX
    and gateway VMs. Then the VMs and Hyper-V hosts are configured to be
    used by the Network Controller. When this script completes the SDN
    infrastructure is ready to be fully used for workload deployments.
.EXAMPLE
    .\SDNExpress.ps1 -ConfigurationDataFile .\MyConfig.psd1
    Reads in the configuration from a PSD1 file that contains a hash table
    of settings data.
.EXAMPLE
    .\SDNExpress -ConfigurationData $MyConfigurationData
    Uses the hash table that is passed in as the configuration data. This
    parameter set is useful when programatically generating the
    configuration data.
.EXAMPLE
    .\SDNExpress
    Displays a user interface for interactively defining the configuraiton
    data. At the end you have the option to save as a configuration file
    before deploying.
.NOTES
    Prerequisites:
    * All Hyper-V hosts must have Hyper-V enabled and the Virtual Switch
    already created.
    * All Hyper-V hosts must be joined to Active Directory.
    * The physical network must be preconfigured for the necessary subnets and
    VLANs as defined in the configuration data.
    * The VHD specified in the configuration data must be reachable from the
    computer where this script is run.
#>


param(
    [Parameter(Mandatory=$true,ParameterSetName="ConfigurationFile")]
    [String] $ConfigurationDataFile=$null,
    [Parameter(Mandatory=$true,ParameterSetName="ConfigurationData")]
    [object] $ConfigurationData=$null,
    [Switch] $SkipValidation,
    [Switch] $SkipDeployment,
    [PSCredential] $DomainJoinCredential = $null,
    [PSCredential] $NCCredential = $null,
    [PSCredential] $LocalAdminCredential = $null
    )    

function IsNotNullOrEmpty($value) {
    return -not [string]::IsNullOrEmpty($value)
}

function IsNotNullOrEmptyOrZero($value) {
    return -not ([string]::IsNullOrEmpty($value) -or ($value -eq 0))
}

function ValidatePrivateVIPSubnetConfigNotEmpty {
    if ([string]::IsNullOrEmpty($ConfigData.PrivateVIPGateway)) {
        throw "PrivateVIPGateway is required to create a PrivateVIPSubnet"
    }
    if ([string]::IsNullOrEmpty($ConfigData.PrivateVIPPoolStart)) {
        throw "PrivateVIPPoolStart is required to create a PrivateVIPSubnet"
    }
    if ([string]::IsNullOrEmpty($ConfigData.PrivateVIPPoolEnd)) {
        throw "PrivateVIPPoolEnd is required to create a PrivateVIPSubnet"
    }
}

function ValidatePublicVIPSubnetConfigNotEmpty {
    if ([string]::IsNullOrEmpty($ConfigData.PublicVIPGateway)) {
        throw "PublicVIPGateway is required to create a PublicVIPSubnet"
    }
    if ([string]::IsNullOrEmpty($ConfigData.PublicVIPPoolStart)) {
        throw "PublicVIPPoolStart is required to create a PublicVIPSubnet"
    }
    if ([string]::IsNullOrEmpty($ConfigData.PublicVIPPoolEnd)) {
        throw "PublicVIPPoolEnd is required to create a PublicVIPSubnet"
    }
}

<#
    This function is used to resolve the credentials to use for the deployment.
    If the credentials are not provided on the command line, the user is prompted.
#>


function ResolveCredentials
{
    param(
        [PSCredential] $Credential,
        [String] $Message
    )

    if ($null -eq $Credential) {
        write-sdnexpresslog "No credentials found on command line or in config file. Prompting."    
        $Credential = get-Credential -Message $Message

        # return secure string
        return $Credential.Password
    }

    if ($null -ne $Credential) {
        write-sdnexpresslog "Using credentials from the command line."    

        # return secure string
        return $Credential.Password
    }

    write-sdnexpresslog "Using credentials from config file."

    # this is no longer deemed safe, password and credentials may not be provided through config files.
    throw "Providing credentials through the configuration file is not considered secure. Please provide credentials directly when invoking sdnexpress "
    
}

function GetNextMacAddress
{
    param(
        [String] $MacAddress
    )

    return ("{0:X12}" -f ([convert]::ToInt64($MacAddress.ToUpper().Replace(":", "").Replace("-", ""), 16) + 1)).Insert(2, "-").Insert(5, "-").Insert(8, "-").Insert(11, "-").Insert(14, "-")
}

function ValidateMacPools
{

    $curInstallDirectory = $Global:installDirectory

    Import-Module Moc -ErrorAction SilentlyContinue
    if ($null -eq $(Get-Module -Name Moc)) 
    {
        Write-SDNExpressLog "MOC Module not found. Skipping check for conflicting MOC and SDN resources."
        return
    }

    try 
    {
        try 
        {
            $mocNotInstalled = (Get-mocconfig -ErrorAction SilentlyContinue).installState -eq "NotInstalled"
            if ($mocnotInstalled)
            {
                Write-SDNExpressLog "No MOC installation found. Skipping check for conflicting MOC and SDN resources."
                return
            }
        }
        catch
        {
            Write-SDNExpressLog "No MOC installation found. Skipping check for conflicting MOC and SDN resources."
            return
        }

        $loc = get-moclocation -ErrorAction SilentlyContinue
        if (-not $loc)
        {
            Write-SDNExpressLog "No MOC location found. Skipping check for conflicting MOC and SDN resources."
            return
        }
        
        $sdnStart = $ConfigData.SDNMacPoolStart -Replace "-",""
        $sdnEnd = $ConfigData.SDNMacPoolEnd -Replace "-",""

        foreach($l in $loc) 
        {
            $mocMacPool = Get-MocMacPool -location $l.Name
            if (-not $mocMacPool)
            {
                continue
            }

            try 
            {
                $mocStart = $mocMacPool.Properties.range.startmacaddress -Replace ":",""
                $mocEnd = $mocMacPool.Properties.range.endmacaddress -Replace ":",""
            }
            catch 
            {
                throw "Could not determine the MOC MAC pool start or end MAC address."
            }
            
            
            if ((IsMac1GreaterThanMac2 -mac1 $sdnStart -mac2 $mocStart) `
                    -and (IsMac1GreaterThanMac2 -mac1 $sdnStart -mac2 $mocEnd))
            {
                continue
            }
            if ((IsMac1GreaterThanMac2 -mac1 $mocStart -mac2 $sdnEnd) `
                    -and (IsMac1GreaterThanMac2 -mac1 $mocEnd -mac2 $sdnEnd))
            {
                continue
            }
            throw "The SDN MAC pool specified in the configuration file conflicts with the MOC MAC pools."
        }
    } 
    finally 
    {
        $Global:installDirectory = $curInstallDirectory
    }
}

function IsMac1GreaterThanMac2
{
    param(
        [string]$mac1,
        [string]$mac2
    )

    $mac1Bytes = [System.Net.NetworkInformation.PhysicalAddress]::Parse($mac1).GetAddressBytes()
    $mac2Bytes = [System.Net.NetworkInformation.PhysicalAddress]::Parse($mac2).GetAddressBytes()

    $i = 0
    while ($i -lt $mac2Bytes.Count)
    {
        if ($mac1Bytes[$i] -gt $mac2Bytes[$i])
        {
            return $true
        }
        if ($mac2Bytes[$i] -gt $mac1Bytes[$i])
        {
            return $false
        }
        $i++
    }
    return $false
}
# returns a simple lexicographical based comparison of two OS versions
function IsOSVersionGreaterOrEqual
{
    param(
        [string] $MinVersion,
        [string] $TestVersion
    )
    return [string]::Compare($TestVersion, $MinVersion, [System.StringComparison]::Ordinal) -ge 0
}

# Feature specific checks for supported versions and functionality
function ValidateSdnConfiguration
{
    param(
        [hashtable] $config, 
        [switch] $SkipValidation
    )
    $hostInfo = Invoke-Command -ComputerName $config.hypervhosts[0] -ScriptBlock { Get-ComputerInfo }
    if($SkipValidation)
    {
        Write-SDNExpressLog "Skipping validation checks."
        return
    }
    # Feature specific checks for supported versions and functionality
    # -- subjectName on < 22H2
    if($config.UseCertBySubject -eq $true) 
    {
        if (-not (IsOSVersionGreaterOrEqual -MinVersion "23H2" -TestVersion $hostInfo.OSDisplayVersion))
        {
            throw "Subject name credential object is not supported on anything below 23H2. Please upgrade your target cluster"
        }
    }
}

try {
    # Script version, should be matched with the config files
    $ScriptVersion = "4.0"

    if ((get-wmiobject win32_operatingsystem).caption.Contains("Windows 10")) {
        get-windowscapability -name rsat.NetworkController.Tools* -online | Add-WindowsCapability -online
    } else {
        $feature = get-windowsfeature "RSAT-NetworkController"
        if ($null -eq $feature) {
            throw "SDN Express requires Windows Server 2016 or later."
        }
        if (!$feature.Installed) {
            add-windowsfeature "RSAT-NetworkController"
        }
    }
    import-module .\SDNExpress.psm1 -force
    write-SDNExpressLog "*** Begin SDN Express Deployment ***"
    write-SDNExpressLog "ParameterSet: $($psCmdlet.ParameterSetName)" 
    write-SDNExpressLog " -ConfigurationDataFile: $ConfigurationDataFile"
    write-SDNExpressLog " -ConfigurationData: $ConfigurationData"
    write-SDNExpressLog " -SkipValidation: $SkipValidation"
    write-SDNExpressLog " -SkipDeployment: $SkipValidation"
    Write-SDNExpressLog "Version info follows: $($PSVersionTable | out-string)"

    if ($psCmdlet.ParameterSetName -eq "ConfigurationFile") {
        write-sdnexpresslog "Using configuration file passed in by parameter."    
        $configdata = [hashtable] (Invoke-Expression (Get-Content $ConfigurationDataFile | out-string))
    } elseif ($psCmdlet.ParameterSetName -eq "ConfigurationData") {
        write-sdnexpresslog "Using configuration data object passed in by parameter."    
        $configdata = $configurationData 
    }

    ValidateSdnConfiguration -config $configdata -SkipValidation $SkipValidation

    $IsL2ForwardingEnabled = $false
    if ($ConfigData.IsL2ForwardingEnabled) {

        $IsL2ForwardingEnabled = $true
    }
    write-sdnexpresslog "IsL2ForwardingEnabled : $IsL2ForwardingEnabled"

    # if FCNC is enabled, load the modules
    if ($configdata.UseFCNC) {
    if(-not [string]::IsNullOrEmpty($Global:FCNC_MODULE_PATH_ROOT)) {
        ipmo  (Join-Path $Global:FCNC_MODULE_PATH_ROOT -ChildPath NetworkControllerFc.psd1) -Force -Scope Global
    } else {
        import-Module NetworkControllerFc -ErrorAction SilentlyContinue
        if ($null -eq (Get-Module NetworkControllerFc)) {
        ipmo ..\NetworkControllerFc\NetworkControllerFc.psd1 -Force -Scope Global
        }
    }  

    # rename and copy package
    if([string]::IsNullOrEmpty($configdata.FCNCPackage) -eq $false) {    
        write-sdnexpresslog "looking for FCNC package $($configdata.FCNCPackage)"
        # check if the package exists
        if (Test-Path $configdata.FCNCPackage) {
        write-sdnexpresslog "FCNC package found"
        $configdata.FCNCBins = $configdata.FCNCPackage
        } else {
        write-sdnexpresslog "FCNC package not found"
        throw "FCNC package not found"
        }

        # copy the nuget to a temp file, rename to zip , decompress it and delete the temp file
        write-sdnexpresslog "copying FCNC package to $($configdata.FCNCBins)"
        Copy-Item $configdata.FCNCPackage "$($configdata.FCNCPackage).zip" -Verbose
        $configdata.FCNCBins = $configdata.FCNCPackage.Replace(".nupkg", ".zip")
            
        Copy-Item $configdata.FCNCPackage $configdata.FCNCBins -Force
        write-sdnexpresslog "unzipping FCNC package"
        Expand-Archive -Path $configdata.FCNCBins -DestinationPath $configdata.FCNCBins.Replace(".zip", "") -Force
        $configdata.FCNCBins = $configdata.FCNCBins.Replace(".zip", "")
    }
    }

    if ($Configdata.ScriptVersion -ne $scriptversion) {
        write-error "Configuration file version $($ConfigData.ScriptVersion) is not compatible with this version of SDN express. Please update your config file to match the version $scriptversion example."
        return
    }

    if ($DomainJoinCredential -eq $null)
    {
        throw "Domain Join credentials are required."
    }

    if($null -eq $NCCredential) {
        throw "NC credentials are required."
    }

    if($null -eq $LocalAdminCredential) {
        throw "Local Admin credentials are required."
    }
    
    # grab the secure passwords from the credentials
    $DomainJoinPassword = $DomainJoinCredential.Password
    $NCPassword = $NCCredential.Password
    $LocalAdminPassword = $LocalAdminCredential.Password

    $credential = $DomainJoinCredential

    if (![string]::IsNullOrEmpty($ConfigData.ManagementSubnet)) {
        $ManagementSubnetBits = $ConfigData.ManagementSubnet.Split("/")[1]
    }

    if ([string]::IsNullOrEmpty($ConfigData.PASubnet)) {
        if ($ConfigData.Muxes.Count -gt 0) {
            throw "Load Balancer Mux configuration requires a PA Subnet."
        }
        if ($ConfigData.Gateways.Count -gt 0) {
            throw "Gateway configuration requires a PA Subnet."
        }
    }

    # check for installed features
    Install-SdnWindowsFeatures -hyperVHosts $configdata.hypervhosts -credential $credential -isFCNC $configdata.UseFCNC

    $hasPrivateVIPSubnet = IsNotNullOrEmpty($ConfigData.PrivateVIPSubnet)
    $hasPrivateVIPGateway = IsNotNullOrEmpty($ConfigData.PrivateVIPGateway)
    $hasPrivateVIPPoolStart = IsNotNullOrEmpty($ConfigData.PrivateVIPPoolStart)
    $hasPrivateVIPPoolEnd = IsNotNullOrEmpty($ConfigData.PrivateVIPPoolEnd)
    
    if($hasPrivateVIPSubnet -and ($IsL2ForwardingEnabled -or $hasPrivateVIPGateway -or $hasPrivateVIPPoolStart -or $hasPrivateVIPPoolEnd)) {
        ValidatePrivateVIPSubnetConfigNotEmpty
    }
    
    $hasPublicVIPSubnet = IsNotNullOrEmpty($ConfigData.PublicVIPSubnet)
    $hasPublicVIPGateway = IsNotNullOrEmpty($ConfigData.PublicVIPGateway)
    $hasPublicVIPPoolStart = IsNotNullOrEmpty($ConfigData.PublicVIPPoolStart)
    $hasPublicVIPPoolEnd = IsNotNullOrEmpty($ConfigData.PublicVIPPoolEnd)
    
    if($hasPublicVIPSubnet -and ($IsL2ForwardingEnabled -or $hasPublicVIPGateway -or $hasPublicVIPPoolStart -or $hasPublicVIPPoolEnd)) {
        ValidatePublicVIPSubnetConfigNotEmpty
    }

    if($IsL2ForwardingEnabled -and (IsNotNullOrEmptyOrZero($ConfigData.PrivateVIPVLANID)) -and (IsNotNullOrEmptyOrZero($ConfigData.PublicVIPVLANID)) -and ($ConfigData.PrivateVIPVLANID -eq $ConfigData.PublicVIPVLANID)) {
        throw "Private/Public VIP subnet with non-zero VlanIds can not be same."
    }

    if (($ConfigData.Muxes.count -gt 0) -or ($ConfigData.Gateways.count -gt 0)) {
        $PASubnetBits = $ConfigData.PASubnet.Split("/")[1]
    }

    ValidateMacPools

    if ($ConfigData.DomainJoinUserName -ne $null)
    {
        $DomainJoinUserNameDomain = $ConfigData.DomainJoinUserName.Split("\")[0]
        $DomainJoinUserNameName = $ConfigData.DomainJoinUserName.Split("\")[1]
    }
    if ($ConfigData.LocalAdminDomainUser -ne $null)
    {
        $LocalAdminDomainUserDomain = $ConfigData.LocalAdminDomainUser.Split("\")[0]
        $LocalAdminDomainUserName = $ConfigData.LocalAdminDomainUser.Split("\")[1]
    }

    if ($null -eq $ConfigData.VMProcessorCount) {$ConfigData.VMProcessorCount = 8}
    if ($null -eq $ConfigData.VMMemory) {$ConfigData.VMMemory = 8GB}
    if ($null -eq $ConfigData.DisableIPv6DHCP) {$ConfigData.DisableIPv6DHCP = $false}
    if ([string]::IsNullOrEmpty($ConfigData.PoolName)) {$ConfigData.PoolName = "DefaultAll"}

    write-SDNExpressLog "STAGE 1: Create VMs"

    $createparams = @{
        'ComputerName'='';
        'VMLocation'=$ConfigData.VMLocation;
        'VMName'='';
        'VHDSrcPath'=$ConfigData.VHDPath;
        'VHDName'=$ConfigData.VHDFile;
        'VMMemory'=$ConfigData.VMMemory;
        'VMProcessorCount'=$ConfigData.VMProcessorCount;
        'Nics'=@();
        'CredentialDomain'=$DomainJoinUserNameDomain;
        'CredentialUserName'=$DomainJoinUserNameName;
        'CredentialPassword'=$DomainJoinPassword;
        'JoinDomain'=$ConfigData.JoinDomain;
        'LocalAdminPassword'=$LocalAdminPassword;
        'DomainAdminDomain'=$LocalAdminDomainUserDomain;
        'DomainAdminUserName'=$LocalAdminDomainUserName;
        'SwitchName'=$ConfigData.SwitchName;
        'DisableIPv6DHCP'=$ConfigData.DisableIPv6DHCP
    }

    if (![String]::IsNullOrEmpty($ConfigData.ProductKey)) {
        $createparams.ProductKey = $ConfigData.ProductKey
    }
    if (![String]::IsNullOrEmpty($ConfigData.Locale)) {
        $createparams.Locale = $ConfigData.Locale
    }
    if (![String]::IsNullOrEmpty($ConfigData.TimeZone)) {
        $createparams.TimeZone = $ConfigData.TimeZone
    }

    write-SDNExpressLog "STAGE 1.0.1: Enable VFP"
    foreach ($h in $ConfigData.hypervhosts) {
     
        write-SDNExpressLog "Enabling VFP on $($h) $($ConfigData.SwitchName)"
        $s = New-SdnExpressPsSession -ComputerName $h $credential
        invoke-command -Session $s {
            param(
                [String] $VirtualSwitchName
                )
            Enable-VmSwitchExtension -VMSwitchName $VirtualSwitchName -Name "Microsoft Azure VFP Switch Extension"
        } -ArgumentList $ConfigData.SwitchName

        $s = New-SdnExpressPsSession -ComputerName $h $credential
        invoke-command -Session $s {
          Set-Service -Name NCHostAgent  -StartupType Automatic; Start-Service -Name NCHostAgent 
        }
    }

    $HostNameIter = 0
    
    $useCertBySubject = $false

    if ($ConfigData.UseCertBySubject) { 
        $useCertBySubject = $true
    }

    if (-not $ConfigData.UseFCNC) {
        foreach ($NC in $ConfigData.NCs) {
            if ([string]::IsNullOrEmpty($nc.macaddress)) {
                $nc.macaddress = $ConfigData.SDNMacPoolStart
                $configdata.SDNMacPoolStart = GetNextMacAddress($ConfigData.SDNMacPoolStart)
            }

            if ([string]::IsNullOrEmpty($nc.HostName)) {
                $nc.HostName = $ConfigData.HyperVHosts[$HostNameIter]
                $HostNameIter = ($HostNameIter + 1) % $ConfigData.HyperVHosts.Count
            }
        }
    }


    foreach ($Mux in $ConfigData.Muxes) {
        if ([string]::IsNullOrEmpty($Mux.HostName)) {
            $Mux.HostName = $ConfigData.HyperVHosts[$HostNameIter]
            $HostNameIter = ($HostNameIter + 1) % $ConfigData.HyperVHosts.Count
        }
        # In Bgpless(L2Forwarding) mode PAIPaddress will be created later at SLB configuration stage, so initializing Mux.PAIPAddress only if L2Forwarding is disabled
        if (-not $IsL2ForwardingEnabled -and [string]::IsNullOrEmpty($Mux.PAIPAddress)) {
            $Mux.PAIPAddress = $ConfigData.PAPoolStart
            $ConfigData.PAPoolStart = Get-IPAddressInSubnet -Subnet $ConfigData.PAPoolStart -Offset 1
        }
    }
    #Allocate GW management MACs from outside of SDN pool
    foreach ($gateway in $ConfigData.Gateways) {
        if ([string]::IsNullOrEmpty($Gateway.macaddress)) {
            $gateway.macaddress = $ConfigData.SDNMacPoolStart
            $configdata.SDNMacPoolStart = GetNextMacAddress($ConfigData.SDNMacPoolStart)
        }
        if ([string]::IsNullOrEmpty($Gateway.HostName)) {
            $Gateway.HostName = $ConfigData.HyperVHosts[$HostNameIter]
            $HostNameIter = ($HostNameIter + 1) % $ConfigData.HyperVHosts.Count
        }        
    }
    #Allocate GW FE & BE macs, FE IP from within SDN mac and PA pools
    $nextmac = $configdata.SDNMacPoolStart
    $PAOffset = 0
    foreach ($gateway in $ConfigData.Gateways) {
        if ([string]::IsNullOrEmpty($Gateway.FrontEndMac)) {
            $gateway.FrontEndMac = $nextmac
            $nextmac = GetNextMacAddress($nextmac)
        }
        if ([string]::IsNullOrEmpty($Gateway.BackEndMac)) {
            $gateway.BackEndMac = $nextmac
            $nextmac = GetNextMacAddress($nextmac)
        }
        if ([string]::IsNullOrEmpty($Gateway.FrontEndIP)) {
            $Gateway.FrontEndIP = Get-IPAddressInSubnet -Subnet $ConfigData.PAPoolStart -Offset $PAOffset
            $PAOffset += 1
        }
    }

    if (-not $ConfigData.UseFCNC) {
        write-SDNExpressLog "STAGE 1.1: Create NC VMs"
        foreach ($NC in $ConfigData.NCs) {
            $createparams.ComputerName=$NC.HostName;
            $createparams.VMName=$NC.ComputerName;
            if ([string]::IsNullOrEmpty($NC.ManagementIP)) {
                $createparams.Nics=@(
                    @{Name="Management"; MacAddress=$NC.MacAddress; VLANID=$ConfigData.ManagementVLANID; SwitchName=$NC.ManagementSwitch}
                )
            } else {
                $createparams.Nics=@(
                    @{Name="Management"; MacAddress=$NC.MacAddress; IPAddress="$($NC.ManagementIP)/$ManagementSubnetBits"; Gateway=$ConfigData.ManagementGateway; DNS=$ConfigData.ManagementDNS; VLANID=$ConfigData.ManagementVLANID; SwitchName=$NC.ManagementSwitch}
                )
            }
            $createparams.Roles=@("NetworkController","NetworkControllerTools")
            New-SDNExpressVM @createparams
        }
    }

    write-SDNExpressLog "STAGE 1.2: Create Mux VMs"

    foreach ($Mux in $ConfigData.Muxes) {
        $createparams.ComputerName=$mux.HostName;
        $createparams.VMName=$mux.ComputerName;
        if ([string]::IsNullOrEmpty($Mux.ManagementIP)) {
            $createparams.Nics=@(
                @{Name="Management"; MacAddress=$Mux.MacAddress; VLANID=$ConfigData.ManagementVLANID; SwitchName=$Mux.ManagementSwitch}
            )
        } else {
            $createparams.Nics=@(
                @{Name="Management"; MacAddress=$Mux.MacAddress; IPAddress="$($Mux.ManagementIP)/$ManagementSubnetBits"; Gateway=$ConfigData.ManagementGateway; DNS=$ConfigData.ManagementDNS; VLANID=$ConfigData.ManagementVLANID; SwitchName=$Mux.ManagementSwitch}
            )
        }

        # In BgpLess Mux (L2Forwarding) mode HNVPA adapter will be created later during SLB configuration stage, so only HNVPA adapter if L2Forwarding is disabled
        if (-not $IsL2ForwardingEnabled) {
            $createparams.Nics += @{Name="HNVPA"; MacAddress=$Mux.PAMacAddress; IPAddress="$($Mux.PAIPAddress)/$PASubnetBits"; VLANID=$ConfigData.PAVLANID; IsMuxPA=$true}
        }

        $createparams.Roles=@("SoftwareLoadBalancer")

        New-SDNExpressVM @createparams
    }

    if ($ConfigData.NCs.count -gt 0 -or $ConfigData.UseFCNC) {
        write-SDNExpressLog "STAGE 2: Network Controller Configuration"
        $NCNodes = @()

        
        if ($ConfigData.UseFCNC) {

            if ([string]::IsNullOrEmpty($ConfigData.FCNCBins))
            {
                $ConfigData.FCNCBins = "C:\Windows\NetworkController"
            }

            $NCNodes = $ConfigData.HyperVHosts

            $params = @{
                'Credential'=$Credential
                'RestName'=$ConfigData.RestName
                'RestIpAddress'=$ConfigData.RestIpAddress
                'ComputerNames'=$NCNodes
                'FCNCBins' = $ConfigData.FCNCBins
                'FCNCDBs' = $ConfigData.FCNCDBs
                'ClusterNetworkName' = $ConfigData.ClusterNetworkName
                'UseCertBySubject' = $useCertBySubject
                'CertificatePassword' = $NCPassword
            }
            
            New-FCNCNetworkController @params

        } else {
            foreach ($NC in $ConfigData.NCs) {
                $NCNodes += $NC.ComputerName
            }

            WaitforComputerToBeReady -ComputerName $NCNodes -Credential $Credential

            $params = @{
                'Credential'=$Credential
                'RestName'=$ConfigData.RestName
                'RestIpAddress'=$ConfigData.RestIpAddress
                'ComputerNames'=$NCNodes
                'UseCertBySubject' = $useCertBySubject
                'CertificatePassword' = $NCPassword
            }

            if (![string]::IsNullOrEmpty($ConfigData.ManagementSecurityGroup)) {
                $params.ManagementSecurityGroupName = $ConfigData.ManagementSecurityGroup
                $params.ClientSecurityGroupName = $ConfigData.ClientSecurityGroup
            }
            New-SDNExpressNetworkController @params
        }


        write-SDNExpressLog "STAGE 2.1: Getting REST cert thumbprint in order to find it in local root store."

        # Check through nodes until we find a node that was originally set up with
        $NCHostCertThumb = $null
        $nodeIdx = 0
        while ($null -eq $NCHostCertThumb -and $nodeIdx -lt $NCNodes.length) {
            $s = New-SdnExpressPsSession -ComputerName $NCNodes[$nodeIdx] $credential
            $NCHostCertThumb = invoke-command -Session $s {
                param(
                    $RESTName,
                    [String] $funcDefGetSdnCert
                )
                . ([ScriptBlock]::Create($funcDefGetSdnCert))
                $Cert = GetSdnCert -subjectName $RestName.ToUpper()
                return $cert.Thumbprint        
            } -ArgumentList $ConfigData.RestName, $Global:fdGetSdnCert

            $nodeIdx++
        }

        $NCHostCert = get-childitem "cert:\localmachine\root\$NCHostCertThumb"

        $params = @{
            'RestName' = $ConfigData.RestName;
            'MacAddressPoolStart' = $ConfigData.SDNMacPoolStart;
            'MacAddressPoolEnd' = $ConfigData.SDNMacPoolEnd;
            'NCHostCert' = $NCHostCert
            'NCUsername' = $ConfigData.NCUsername;
            'NCPassword' = $NCPassword
            'UseCertBySubject' = $useCertBySubject
        }
        New-SDNExpressVirtualNetworkManagerConfiguration @Params -Credential $Credential

        if (![string]::IsNullOrEmpty($ConfigData.PASubnet)) {
            $params = @{
                'RestName' = $ConfigData.RestName;
                'AddressPrefix' = $ConfigData.PASubnet;
                'VLANID' = $ConfigData.PAVLANID;
                'DefaultGateways' = $ConfigData.PAGateway;
                'IPPoolStart' = $ConfigData.PAPoolStart;
                'IPPoolEnd' = $ConfigData.PAPoolEnd
            }
            Add-SDNExpressVirtualNetworkPASubnet @params -Credential $Credential
        } else {
            write-SDNExpressLog "PA subnets not specified in configuration, skipping Virtual Network PA configuration."
        }
    } 
    else 
    {
        $NCHostCert = GetSdnCert -subjectName $configdata.RestName -store "cert:\localmachine\root" 

        if ($null -eq $NCHostCert) {
            $ErrorText = "Network Controller cert with CN=$($configdata.RestName) not found on $(hostname) in cert:\localmachine\root"
            write-SDNExpressLog $ErrorText
            throw $ErrorText
        }        
    }

    $useFcNc = $false
    if ($ConfigData.UseFCNC)
    { 
        $useFcNc = $true
    } 

    if ($ConfigData.Muxes.Count -gt 0) {
        write-SDNExpressLog "STAGE 3: SLB Configuration"

        if (![string]::IsNullOrEmpty($ConfigData.PrivateVIPSubnet)) {
            $params = @{
                'RestName' = $ConfigData.RestName;
                'PrivateVIPPrefix' = $ConfigData.PrivateVIPSubnet;
                'PublicVIPPrefix' = $ConfigData.PublicVIPSubnet;
                'IsL2ForwardingEnabled' = $IsL2ForwardingEnabled;
            }

            if ($hasPrivateVIPGateway) {
                $params.Add('PrivateVIPGateway', $ConfigData.PrivateVIPGateway)
            }
            if ($hasPrivateVIPPoolStart) {
                $params.Add('PrivateVIPPoolStart', $ConfigData.PrivateVIPPoolStart)
            }
            if ($hasPrivateVIPPoolEnd) {
                $params.Add('PrivateVIPPoolEnd', $ConfigData.PrivateVIPPoolEnd)
            }
            if (IsNotNullOrEmpty($ConfigData.PrivateVIPVLANID)) {
                $params.Add('PrivateVIPVLANID', $ConfigData.PrivateVIPVLANID)
            }
            if ($hasPublicVIPGateway) {
                $params.Add('PublicVIPGateway', $ConfigData.PublicVIPGateway)
            }
            if ($hasPublicVIPPoolStart) {
                $params.Add('PublicVIPPoolStart', $ConfigData.PublicVIPPoolStart)
            }
            if ($hasPublicVIPPoolEnd) {
                $params.Add('PublicVIPPoolEnd', $ConfigData.PublicVIPPoolEnd)
            }
            if (IsNotNullOrEmpty($ConfigData.PublicVIPVLANID)) {
                $params.Add('PublicVIPVLANID', $ConfigData.PublicVIPVLANID)
            }

            New-SDNExpressLoadBalancerManagerConfiguration @Params -Credential $Credential
        } else {
            write-SDNExpressLog "VIP subnets not specified in configuration, skipping load balancer manager configuration."
        }

        WaitforComputerToBeReady -ComputerName $ConfigData.Muxes.ComputerName -Credential $Credential

        foreach ($Mux in $ConfigData.muxes) {
            if($IsL2ForwardingEnabled) {
                $params = @{
                    'RestName'=$ConfigData.RestName
                    'ComputerName'=$Mux.computername
                    'JoinDomain'=$ConfigData.JoinDomain
                    'InternalNicLogicalNetworkName' = "HNVPA" 
                    'InternalNicAddressPrefix' = $ConfigData.PASubnet
                }
    
                $Result = Initialize-SDNExpressMuxNic @params -Credential $Credential
                $Mux.ExtMacAddress = $Result.ExternalMac
                $Mux.PAIPAddress = $Result.InternalIP
                $Mux.PAMacAddress = $Result.InternalMac

                $params = @{
                    'ComputerName' = $mux.HostName;
                    'VMName' = $mux.ComputerName;
                    'SwitchName' = $ConfigData.SwitchName;
                    'Nics' = @(
                                @{Name="HNVPA"; MacAddress=$Mux.PAMacAddress; InstanceId="{$([Guid]::Empty)}"; VlanId=$ConfigData.PAVLANID; IsMuxPA=$true},
                                @{Name="External"; MacAddress=$Mux.ExtMacAddress; InstanceId="{$($Result.ExternalNicInstanceId)}"; IsMuxExt=$true}
                             );
                }

                Add-SDNExpressMuxNic @params -Credential $Credential

                $params = @{
                    'ComputerName' = $mux.ComputerName;
                    'MacAddress' = $Mux.PAMacAddress;
                    'IPAddress' = $Mux.PAIPAddress;
                    'AddressPrefix' = $ConfigData.PASubnet.Split("/") | Select-Object -Last 1;
                }

                # Configuring the HNVPA IPAddress on mux HNVPA adapter
                Add-IPAddress @params -Credential $Credential
            }

            $params = @{
                'ComputerName' = $Mux.ComputerName
                'PAMacAddress' = $Mux.PAMacAddress
                'ExtMacAddress' = $Mux.ExtMacAddress
                'PAGateway' = $ConfigData.PAGateway
                'LocalPeerIP' = $Mux.PAIPAddress
                'MuxASN' = $ConfigData.SDNASN
                'Routers' = $ConfigData.Routers
                'RestName' = $ConfigData.RestName
                'NCHostCert' = $NCHostCert
                'Credential' = $Credential
                'IsFC' = $useFcNc
                'IsL2ForwardingEnabled' = $IsL2ForwardingEnabled
            }

            Add-SDNExpressMux @params
        }
    }


    write-SDNExpressLog "STAGE 4: Host Configuration"
    $params = @{}

    if (![string]::IsNullOREmpty($ConfigData.PASubnet)) {
        $params.HostPASubnetPrefix = $ConfigData.PASubnet;
    }

    foreach ($h in $ConfigData.hypervhosts) {
        if($null -ne $ConfigData.Port -and $ConfigData.Port -ne 0) {
            write-SDNExpressLog "Using port $($ConfigData.Port) for host $h"
            $params.Port = $ConfigData.Port
        }

        $ncNodes = $ConfigData.NCs.ComputerName
        Add-SDNExpressHost @params -ComputerName $h `
                                -RestName $ConfigData.RestName `
                                -NCHostCert $NCHostCert `
                                -Credential $Credential `
                                -VirtualSwitchName $ConfigData.SwitchName `
                                -IsFC $useFcNc `
                                -AddToFcCluster $false `
                                -CertificatePassword $NCPassword `
                                -NCNodes $ncNodes
    }

    if ($ConfigData.Gateways.Count -gt 0) {
        write-SDNExpressLog "STAGE 5.1: Create Gateway VMs"

        foreach ($Gateway in $ConfigData.Gateways) {
            $params = @{
                'RestName'=$ConfigData.RestName
                'ComputerName'=$gateway.computername
                'HostName'=$gateway.Hostname
                'JoinDomain'=$ConfigData.JoinDomain
                'FrontEndLogicalNetworkName'='HNVPA'
                'FrontEndAddressPrefix'=$ConfigData.PASubnet
            }
    
            $Result = Initialize-SDNExpressGateway @params -Credential $Credential
    
            $Gateway.FrontEndMac = $Result.FrontEndMac
            $Gateway.FrontEndIP = $Result.FrontEndIP
            $Gateway.BackEndMac = $Result.BackEndMac

            $createparams.ComputerName=$Gateway.HostName;
            $createparams.VMName=$Gateway.ComputerName;
            if ([string]::IsNullOrEmpty($Gateway.ManagementIP)) {
                $createparams.Nics=@(
                    @{Name="Management"; MacAddress=$Gateway.MacAddress; VLANID=$ConfigData.ManagementVLANID; SwitchName=$Mux.ManagementSwitch}
                    @{Name="FrontEnd"; MacAddress=$Gateway.FrontEndMac; IPAddress="$($Gateway.FrontEndIp)/$PASubnetBits"; VLANID=$ConfigData.PAVLANID},
                    @{Name="BackEnd"; MacAddress=$Gateway.BackEndMac; VLANID=$ConfigData.PAVLANID}
                );
            } else {
                $createparams.Nics=@(
                    @{Name="Management"; MacAddress=$Gateway.MacAddress; IPAddress="$($Gateway.ManagementIP)/$ManagementSubnetBits"; Gateway=$ConfigData.ManagementGateway; DNS=$ConfigData.ManagementDNS; VLANID=$ConfigData.ManagementVLANID; SwitchName=$Mux.ManagementSwitch}
                    @{Name="FrontEnd"; MacAddress=$Gateway.FrontEndMac; IPAddress="$($Gateway.FrontEndIp)/$PASubnetBits"; VLANID=$ConfigData.PAVLANID},
                    @{Name="BackEnd"; MacAddress=$Gateway.BackEndMac; VLANID=$ConfigData.PAVLANID}
                );
            }
            $createparams.Roles=@("RemoteAccess", "RemoteAccessServer", "RemoteAccessMgmtTools", "RemoteAccessPowerShell", "RasRoutingProtocols", "Web-Application-Proxy")
    
            New-SDNExpressVM @createparams
        }
    
        write-SDNExpressLog "STAGE 5.3: Configure Gateways"

        if ([String]::IsNullOrEmpty($ConfigData.RedundantCount)) {
            $ConfigData.RedundantCount = 1
        } 

        if ([string]::IsNullOrEmpty($configdata.GatewayPoolType) -or ($configdata.GatewayPoolType -eq "All")) {
            write-SDNExpressLog "Gateway pool type is All."
            New-SDNExpressGatewayPool -IsTypeAll -PoolName $ConfigData.PoolName -Capacity $ConfigData.Capacity -GreSubnetAddressPrefix $ConfigData.GreSubnet -RestName $ConfigData.RestName -Credential $Credential -RedundantCount $ConfigData.RedundantCount
        } elseif ($configdata.GatewayPoolType -eq "GRE") {
            write-SDNExpressLog "Gateway pool type is GRE."
            New-SDNExpressGatewayPool -IsTypeGRE -PoolName $ConfigData.PoolName -Capacity $ConfigData.Capacity -GreSubnetAddressPrefix $ConfigData.GreSubnet -RestName $ConfigData.RestName -Credential $Credential -RedundantCount $ConfigData.RedundantCount
        } elseif ($configdata.GatewayPoolType -eq "Forwarding") {
            write-SDNExpressLog "Gateway pool type is Forwarding."
            New-SDNExpressGatewayPool -IsTypeForwarding -PoolName $ConfigData.PoolName -Capacity $ConfigData.Capacity -RestName $ConfigData.RestName -Credential $Credential -RedundantCount $ConfigData.RedundantCount
        } elseif ($configdata.GatewayPoolType -eq "IPSec") {
            write-SDNExpressLog "Gateway pool type is IPSec."
            New-SDNExpressGatewayPool -IsTypeIPSec -PoolName $ConfigData.PoolName -Capacity $ConfigData.Capacity -RestName $ConfigData.RestName -Credential $Credential -RedundantCount $ConfigData.RedundantCount
        } else {
            write-SDNExpressLog "Gateway pool type is Invalid."
            throw "Invalid GatewayPoolType specified in config file."
        } 

        WaitforComputerToBeReady -ComputerName $ConfigData.Gateways.ComputerName -Credential $Credential

        foreach ($G in $ConfigData.Gateways) {
            $params = @{
                'RestName'=$ConfigData.RestName
                'ComputerName'=$g.computername
                'HostName'=$g.Hostname
                'NCHostCert'= $NCHostCert
                'PoolName'=$ConfigData.PoolName
                'FrontEndIp'=$G.FrontEndIP
                'FrontEndLogicalNetworkName'='HNVPA'
                'FrontEndAddressPrefix'=$ConfigData.PASubnet
                'FrontEndMac'=$G.FrontEndMac
                'BackEndMac'=$G.BackEndMac
                'Routers'=$ConfigData.Routers 
                'PAGateway'=$ConfigData.PAGateway
                'ManagementRoutes'=$ConfigData.ManagementRoutes
                'LocalASN'=$ConfigData.SDNASN
            }

            if ($ConfigData.UseGatewayFastPath -eq $true) {
                New-SDNExpressGateway @params  -Credential $Credential -UseFastPath -IsFC $useFcNc
            } else {
                New-SDNExpressGateway @params  -Credential $Credential -IsFC $useFcNc
            }
        }

    }

    [bool] $testMux = ($configdata.Muxes.Count -gt 0 )
    [bool] $testGateway = ($configData.Gateways.Count -gt 0)
    Test-SdnExpressHealth -restname $ConfigData.RestName `
        -Credential $Credential `
        -testmux $testMux `
        -testgateway $testgateway 

}
catch
{
    try {
        write-SDNExpressLog "An error occurred during the deployment. $_"
    } 
    catch
    {
        Write-Host "Unhandled error: $_"
    }
    $pscmdlet.throwterminatingerror($PSItem)
}

write-SDNExpressLog "SDN Express deployment complete."

# SIG # Begin signature block
# MIIoQwYJKoZIhvcNAQcCoIIoNDCCKDACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCkDs3SddqYXS2r
# UPxN/iA9XaZm9FdR722XVVTmNq9Ya6CCDXYwggX0MIID3KADAgECAhMzAAAEBGx0
# Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz
# NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo
# DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3
# a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF
# HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy
# 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC
# Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj
# L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp
# h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3
# cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X
# dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL
# E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi
# u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1
# sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq
# 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb
# DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/
# V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGiMwghofAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIIv2MhBCLYBnVi+8RiWMShyv
# kunNTIdEjUxJsY3OhMBwMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAJIYs/VLq9YcBY3bAv7rc4MWOhfAtBpDjHJ47bRVlXbKOaD29/H+MRlCV
# vRDZR71BeVRnd6e42ueXDkJNIqr5HnZBtNCe2Sovir18LHsXDH5gKHoLWt/cVS6A
# 9rLB6P2lnF769H/mVlHt+0RnbYLD6ipOseAo/F3HVA/cn6Gt4AP1iPVRq115kcs9
# TkVasZOhYl3HLnrBi/S905cj/8SN8PqB980jWQCbIjg3PQfe6B5c1A6v1ontj7Fq
# jyMueJOBS1JqyxpXwpnuJ11pJpKBYh9gLkadnDG4i+70NANGQvikD4kMBo3g/gSr
# eRmxeakYp+GEg/zeqGoDpR04Ikxd/KGCF60wghepBgorBgEEAYI3AwMBMYIXmTCC
# F5UGCSqGSIb3DQEHAqCCF4YwgheCAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq
# hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCCkScE/8CKsfMuigCwjV5S5SY4Zb7Z64mr7XJkxnIlFTAIGZ0hn9eo3
# GBMyMDI0MTIwNTAzMDk1MS4zODZaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# Tjo2QjA1LTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaCCEfswggcoMIIFEKADAgECAhMzAAAB9oMvJmpUXSLBAAEAAAH2MA0G
# CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0
# MDcyNTE4MzEwNFoXDTI1MTAyMjE4MzEwNFowgdMxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w
# ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjZCMDUt
# MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0UJeLMR/N9WPBZhuKVFF
# +eWJZ68Wujdj4X6JR05cxO5CepNXo17rVazwWLkm5AjaVh19ZVjDChHzimxsoaXx
# Nu8IDggKwpXvpAAItv4Ux50e9S2uVwfKv57p9JKG+Q7VONShujl1NCMkcgSrPdmd
# /8zcsmhzcNobLomrCAIORZ8IwhYy4siVQlf1NKhlyAzmkWJD0N+60IiogFBzg3yI
# SsvroOx0x1xSi2PiRIQlTXE74MggZDIDKqH/hb9FT2kK/nV/aXjuo9LMrrRmn44o
# YYADe/rO95F+SG3uuuhf+H4IriXr0h9ptA6SwHJPS2VmbNWCjQWq5G4YkrcqbPMa
# x7vNXUwu7T65E8fFPd1IuE9RsG4TMAV7XkXBopmPNfvL0hjxg44kpQn384V46o+z
# dQqy5K9dDlWm/J6vZtp5yA1PyD3w+HbGubS0niEQ1L6wGOrPfzIm0FdOn+xFo48E
# Rl+Fxw/3OvXM5CY1EqnzEznPjzJc7OJwhJVR3VQDHjBcEFTOvS9E0diNu1eocw+Z
# Ckz4Pu/oQv+gqU+bfxL8e7PFktfRDlM6FyOzjP4zuI25gD8tO9zJg6g6fRpaZc43
# 9mAbkl3zCVzTLDgchv6SxQajJtvvoQaZxQf0tRiPcbr2HWfMoqqd9uiQ0hTUEhG4
# 4FBSTeUPZeEenRCWadCW4G8CAwEAAaOCAUkwggFFMB0GA1UdDgQWBBRIwZsJuOcJ
# fScPWcXZuBA4B89K8jAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf
# BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz
# L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww
# bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El
# MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
# BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEA13kBirH1cHu1
# WYR1ysj125omGtQ0PaQkEzwGb70xtqSoI+svQihsgdTYxaPfp2IVFdgjaMaBi81w
# B8/nu866FfFKKdhdp3wnMZ91PpP4Ooe7Ncf6qICkgSuwgdIdQvqE0h8VQ5QW5sDV
# 4Q0Jnj4f7KHYx4NiM8C4jTw8SQtsuxWiTH2Hikf3QYB71a7dB9zgHOkW0hgUEeWO
# 9mh2wWqYS/Q48ASjOqYw/ha54oVOff22WaoH+/Hxd9NTEU/4vlvsRIMWT0jsnNI7
# 1jVArT4Q9Bt6VShWzyqraE6SKUoZrEwBpVsI0LMg2X3hOLblC1vxM3+wMyOh97aF
# Os7sFnuemtI2Mfj8qg16BZTJxXlpPurWrG+OBj4BoTDkC9AxXYB3yEtuwMs7pRWL
# yxIxw/wV9THKUGm+x+VE0POLwkrSMgjulSXkpfELHWWiCVslJbFIIB/4Alv+jQJS
# KAJuo9CErbm2qeDk/zjJYlYaVGMyKuYZ+uSRVKB2qkEPcEzG1dO9zIa1Mp32J+zz
# W3P7suJfjw62s3hDOLk+6lMQOR04x+2o17G3LceLkkxJm41ErdiTjAmdClen9yl6
# HgMpGS4okjFCJX+CpOFX7gBA3PVxQWubisAQbL5HgTFBtQNEzcCdh1GYw/6nzzNN
# t+0GQnnobBddfOAiqkzvItqXjvGyK1QwggdxMIIFWaADAgECAhMzAAAAFcXna54C
# m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp
# Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy
# MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51
# yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY
# 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9
# cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN
# 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua
# Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74
# kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2
# K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5
# TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk
# i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q
# BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri
# Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC
# BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl
# pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y
# eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA
# YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw
# MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp
# b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm
# ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM
# 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW
# OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4
# FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw
# xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX
# fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX
# VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC
# onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU
# 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG
# ahC0HVUzWLOhcGbyoYIDVjCCAj4CAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# Tjo2QjA1LTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAFU9eSpdxs0a06JFIuGFHIj/I+36ggYMw
# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF
# AAIFAOr7d+0wIhgPMjAyNDEyMDUwMDUzMDFaGA8yMDI0MTIwNjAwNTMwMVowdDA6
# BgorBgEEAYRZCgQBMSwwKjAKAgUA6vt37QIBADAHAgEAAgIhMzAHAgEAAgITqzAK
# AgUA6vzJbQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQCig6lcmMUfXIl5
# iSzFtIAptVd/NhEzOXjI07lIrLEnanXcu2jnTe1yEqmI6i0DnVBa1C6t9I7yEr/s
# 1ZJRCKXrpERTC4Vsbv95zwngVzGGIfktqj/tUzWs4xBN8XYGiQ/mx8xxXP4GBd9u
# QtxAkvjB7pwXe+DcUBT0Z+O6PBf5b1UPDQ0gm8A4vBG/BZ027wQhx/h0T8hITReM
# Qs4avGnYaABIXck35Dt15hb5RXcLoDCgC/CKcw0tp9WRajDAKQunQfmju5BGiQxe
# 7pUVzt/gHx843AgIQawpVeZY2ERVPD+vPP+Nj+VMckSJkyivG73naHzRJjYZoP5T
# ZRA5sZFLMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAH2gy8malRdIsEAAQAAAfYwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqG
# SIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgdBObx/ZUUPqP
# 2BOLdHnwhwKlR9dDcEQMdNpCTf962AIwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHk
# MIG9BCArYUzxlF6m5USLS4f8NXL/8aoNEVdsCZRmF+LlQjG2ojCBmDCBgKR+MHwx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p
# Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB9oMvJmpUXSLBAAEAAAH2
# MCIEICSuDnfSw5e7YwVc/IF+6nHIfxtUbMhsU2Zb+L5pwWDtMA0GCSqGSIb3DQEB
# CwUABIICACVR0UUfjL2oTe1dzLwP2/ssasXwrgHec+gi1H4BV3EGLxJ6qLfOPdjY
# IhciLTiirr5N8QPy+RHSTX2Dq+BaniSo1SLmh39QO54R6cNScy75gcZz2aKSLvT1
# Z2l7Y8kJcDjFPcAOiAnP/CJgJX9EYXT/sEV2tiT+AYA+vq6YigX+IYAgxVtgE2Um
# M+2oyYohz86IlzeAJTXRoz54H1flV/carL8+G8wG8bj5PZZWIkkJFQHDJsuO53tF
# t9uqnq+7kYrWqEU9waGnOZqHwOTEHaKE8kQKnDrOCIB/csdViPmYpSkyN+LHECa2
# yzfbeXA1NGvrqrKi70QnHLind35x2dDiPzwGXZ9OmEhxU/37uCxd1IS2hpSg2r1z
# aZ/S1j76iE+8+U3qpqRYFztPXrC8LPaT3+/ZmeEKYCVyG8xqwCtlmTacxha0V/ok
# Yjtb+TdSGhdu9l8QTKiVv+riUFro2y0ohv3i1rmR2/HQ2cDUjXkc6yDThCK8Ss+C
# m1/bYkLJtkg1HCShP6fBNN8MC0yy6qW3h9eE03VWUeF+Vq6Vgvnq8Y7//1tb2F9U
# ftr0ca+0kSuisJM+wfNYR1+v7OzW2AwmfCvn7qai9ldcG2tL1yTVXITmMtgQkquI
# dNciIMJXTevC8N2OAx30gWlyflhSHVMjPkdFGSfs8DnolFuWPDbI
# SIG # End signature block