DSCResources/VE_XD7Common/VE_XD7Common.psm1

Import-LocalizedData -BindingVariable localized -FileName VE_XD7Common.Resources.psd1;

#region Private Functions

function AddInvokeScriptBlockCredentials {
<#
    .SYNOPSIS
        Adds the required Invoke-Command parameters for loopback processing with CredSSP.
#>

    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory)]
        [System.Collections.Hashtable] $Hashtable,

        [Parameter(Mandatory)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )
    process {

        $Hashtable['ComputerName'] = $env:COMPUTERNAME;
        $Hashtable['Credential'] = $Credential;
        $Hashtable['Authentication'] = 'Credssp';

    }
} #end function AddInvokeScriptBlockCredentials


function GetHostname {
    [CmdletBinding()]
    [OutputType([System.String])]
    param ( )
    process {

        $globalIpProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties();
        if ($globalIpProperties.DomainName) {
            return '{0}.{1}' -f $globalIpProperties.HostName, $globalIpProperties.DomainName;
        }
        else {
            return $globalIpProperties.HostName;
        }

    } #end process
} #end function GetHostname


function GetRegistryValue {
<#
    .SYNOPSIS
        Returns a registry string value.
    .NOTES
        This is an internal function and shouldn't be called from outside.
        This function enables registry calls to be unit tested.
#>

    [CmdletBinding(SupportsShouldProcess)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [OutputType([System.String])]
    param (
        # Registry key name/path to query.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Alias('Path')] [System.String] $Key,

        # Registry value to return.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name
    )
    process {

        $itemProperty = Get-ItemProperty -Path $Key -Name $Name -ErrorAction SilentlyContinue;
        if ($itemProperty.$Name) {
            return $itemProperty.$Name;
        }
        return '';

    }
} #end function GetRegistryValue


function StartWaitProcess {
<#
    .SYNOPSIS
        Starts and waits for a process to exit.
    .NOTES
        This is an internal function and shouldn't be called from outside.
#>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([System.Int32])]
    param (
        # Path to process to start.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $FilePath,

        # Arguments (if any) to apply to the process.
        [Parameter()]
        [AllowNull()]
        [System.String[]] $ArgumentList,

        # Credential to start the process as.
        [Parameter()]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        # Working directory
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $WorkingDirectory = (Split-Path -Path $FilePath -Parent)
    )
    process {

        $startProcessParams = @{
            FilePath = $FilePath;
            WorkingDirectory = $WorkingDirectory;
            NoNewWindow = $true;
            PassThru = $true;
        };
        $displayParams = '<None>';
        if ($ArgumentList) {
            $displayParams = [System.String]::Join(' ', $ArgumentList);
            $startProcessParams['ArgumentList'] = $ArgumentList;
        }
        Write-Verbose ($localized.StartingProcess -f $FilePath, $displayParams);
        if ($Credential) {
            Write-Verbose ($localized.StartingProcessAs -f $Credential.UserName);
            $startProcessParams['Credential'] = $Credential;
        }
        if ($PSCmdlet.ShouldProcess($FilePath, 'Start Process')) {
            $process = Start-Process @startProcessParams -ErrorAction Stop;
        }
        if ($PSCmdlet.ShouldProcess($FilePath, 'Wait Process')) {
            Write-Verbose ($localized.ProcessLaunched -f $process.Id);
            Write-Verbose ($localized.WaitingForProcessToExit -f $process.Id);
            $process.WaitForExit();
            $exitCode = [System.Convert]::ToInt32($process.ExitCode);
            Write-Verbose ($localized.ProcessExited -f $process.Id, $exitCode);
        }
        return $exitCode;

    } #end process
} #end function StartWaitProcess


function FindXDModule {
<#
    .SYNOPSIS
        Locates a module's manifest (.psd1) file.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name = 'Citrix.XenDesktop.Admin',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path = 'C:\Program Files\Citrix\XenDesktopPoshSdk\Module\Citrix.XenDesktop.Admin.V1'
    )
    process {

        $module = Get-ChildItem -Path $Path -Include "$Name.psd1" -File -Recurse;
        if (-not $module) {
            # If we have no .psd1 file, search for a .psm1 (for StoreFront)
            $module = Get-ChildItem -Path $Path -Include "$Name.psm1" -File -Recurse;
        }
        return $module.FullName;

    } #end process
} #end function FindModule


function TestXDModule {
<#
    .SYNOPSIS
        Tests whether Powershell modules or Snapin are available/registered.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $Name = 'Citrix.XenDesktop.Admin',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path = 'C:\Program Files\Citrix\XenDesktopPoshSdk\Module\Citrix.XenDesktop.Admin.V1',

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

        if ($IsSnapin) {

            if (Get-PSSnapin -Name $Name -Registered -ErrorAction SilentlyContinue) {
                return $true;
            }
        }

        ## If testing a snap-in and it fails, try resolving it as a module (#18)
        if (FindXDModule -Name $Name -Path $Path) {
            return $true;
        }

        return $false;

    } #end process
} #end TestModule


function AssertXDModule {
<#
    .SYNOPSIS
        Asserts whether all the specified modules are present, throwing if not.
#>

    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String[]] $Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $Path = 'C:\Program Files\Citrix\XenDesktopPoshSdk\Module\Citrix.XenDesktop.Admin.V1',

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

        foreach ($moduleName in $Name) {

            if (-not (TestXDModule -Name $moduleName -Path $Path -IsSnapin:$IsSnapin)) {

                ThrowInvalidProgramException -ErrorId $moduleName -ErrorMessage $localized.XenDesktopSDKNotFoundError;
            }
        } #end foreach module

    } #end process
} #end function AssertXDModule


function Add-PSSnapin {
<#
    .SYNOPSIS
        Proxy function to load Citrix PowerShell snapins within a module
        at the global scope.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [System.String[]] $Name
    )
    process {

        foreach ($snapinName in $Name) {

            $modulePath = Join-Path -Path $PSScriptRoot -ChildPath "$snapinName.psm1";
            Import-Module -Name $modulePath -Global -Verbose:$false;

        } #end foreach snapin

    } #end process
} #end function Add-PSSnapin


function GetXDBrokerMachine {
<#
    .SYNOPSIS
        Searches for a registered Citrix XenDesktop machine by name.
#>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.String] $MachineName
    )
    process {

        if ($MachineName.Contains('.')) {
            ## Attempt to locate the machine by FQDN
            $brokerMachine = Get-BrokerMachine -DNSName $MachineName -ErrorAction SilentlyContinue;
        }
        elseif ($MachineName.Contains('\')) {
            ## Otherwise attempt to locate the machine by DomainName\NetBIOSName
            $brokerMachine = Get-BrokerMachine -MachineName $MachineName -ErrorAction SilentlyContinue;
        }
        else {
            ## Failing all else, perform a wildcard search
            $brokerMachine = Get-BrokerMachine -MachineName "*\$MachineName" -ErrorAction SilentlyContinue;
        }

        if ($null -eq $brokerMachine) {

            Write-Error -ErrorId 'MachineNotFound' -Message ($localized.MachineNotFoundError -f $Machine);
            return;
        }
        elseif (($brokerMachine).Count -gt 1) {

            Write-Error -ErrorId 'AmbiguousMachineReference' -Message ($localized.AmbiguousMachineReferenceError -f $MachineName);
            return;
        }
        else {

            return $brokerMachine;
        }

    } #end process
} #end function GetXDBrokerMachine


function TestXDMachineIsExistingMember {
<#
    .SYNOPSIS
        Tests whether a machine is an existing member of a list of FQDN machine members.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory)]
        [System.String] $MachineName,

        [Parameter()]
        [System.String[]] $ExistingMembers
    )
    process {

        if ((-not $MachineName.Contains('\')) -and (-not $MachineName.Contains('.'))) {

            Write-Warning -Message ($localized.MachineNameNotFullyQualifiedWarning -f $MachineName);
            $netBIOSName = $MachineName;
        }
        elseif ($MachineName.Contains('\')) {

            $netBIOSName = $MachineName.Split('\')[1];
        }

        if ($ExistingMembers -contains $MachineName) {
            return $true;
        }
        elseif ($ExistingMembers -like '{0}.*' -f $netBIOSName) {
            return $true;
        }
        else {
            return $false;
        }

    } #end process
} #end function TestXDMachine


function TestXDMachineMembership {
<#
    .SYNOPSIS
        Provides a centralised function to test whether machine membership of a Machine Catalog or
        Delivery Group are correct - evaluating FQDNs, DOMAINNAME\NETBIOS and NETBIOS name formats.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory)]
        [System.String[]] $RequiredMembers,

        [Parameter(Mandatory)]
        [ValidateSet('Present','Absent')]
        [System.String] $Ensure,

        [Parameter()]
        [System.String[]] $ExistingMembers
    )
    process {

        $isInCompliance = $true;

        foreach ($member in $RequiredMembers) {

            if (TestXDMachineIsExistingMember -MachineName $member -ExistingMembers $ExistingMembers) {

                if ($Ensure -eq 'Absent') {
                    Write-Verbose ($localized.SurplusMachineReference -f $member);
                    $isInCompliance = $false;
                }
            }
            else {

                if ($Ensure -eq 'Present') {
                    Write-Verbose ($localized.MissingMachineReference -f $member);
                    $isInCompliance = $false;
                }
            }

        } #end foreach member

        return $isInCompliance;

    } #end process
} #end function TestXDMachineMembers


function ResolveXDBrokerMachine {
<#
    .SYNOPSIS
        Returns a machine machine from an existing collection of Citrix XenDesktop
        machines assigned to a Machine Catalog or Delivery Group
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory)]
        [System.String] $MachineName,

        [Parameter(Mandatory)]
        [AllowNull()]
        [System.Object[]] $BrokerMachines
    )
    process {

        foreach ($machine in $brokerMachines) {
            ## Try matching on DNS name
            if (($machine.DNSName -eq $MachineName) -or ($machine.MachineName -eq $MachineName)) {

                return $machine;
            }
            elseif ((-not $MachineName.Contains('\')) -and ($machine.MachineName -match '^\S+\\{0}$' -f $MachineName)) {
                ## Try matching based on DOMAIN\NETBIOS name
                return $machine
            }

        } #end foreach machine

        return $null;

    } #end process
} #end function ResolveXDBrokerMachine


function ThrowInvalidOperationException {
<#
    .SYNOPSIS
        Throws terminating error of category NotInstalled with specified errorId and errorMessage.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.String] $ErrorId,

        [Parameter(Mandatory)]
        [System.String] $ErrorMessage
    )
    process {

        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument;
        $exception = New-Object -TypeName 'System.InvalidOperationException' -ArgumentList $ErrorMessage;
        $errorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList $exception, $ErrorId, $errorCategory, $null;
        throw $errorRecord;

    } #end process
} #end function ThrowInvalidOperationException


function ThrowInvalidProgramException {
<#
    .SYNOPSIS
        Throws terminating error of category NotInstalled with specified errorId and errorMessage.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.String] $ErrorId,

        [Parameter(Mandatory)]
        [System.String] $ErrorMessage
    )
    process {

        $errorCategory = [System.Management.Automation.ErrorCategory]::NotInstalled;
        $exception = New-Object -TypeName 'System.InvalidProgramException' -ArgumentList $ErrorMessage;
        $errorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList $exception, $ErrorId, $errorCategory, $null;
        throw $errorRecord;

    } #end process
} #end function ThrowInvalidProgramException


function ThrowOperationCanceledException {
<#
    .SYNOPSIS
        Throws terminating error of category InvalidOperation with specified errorId and errorMessage.
#>

    param(
        [Parameter(Mandatory)]
        [System.String] $ErrorId,

        [Parameter(Mandatory)]
        [System.String] $ErrorMessage
    )
    process {

        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation;
        $exception = New-Object -TypeName 'System.OperationCanceledException' -ArgumentList $ErrorMessage;
        $errorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList $exception, $ErrorId, $errorCategory, $null;
        throw $errorRecord;

    } #end process
} #end function ThrowOperationCanceledException


function TestXDInstalledRole {
<#
    .SYNOPSIS
        Tests whether a Citrix XenDesktop 7.x role is installed.
#>

    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        ## Citrix XenDesktop 7.x role to query.
        [Parameter(Mandatory)]
        [ValidateSet('Controller','Studio','Storefront','Licensing','Director','DesktopVDA','SessionVDA')]
        [System.String[]] $Role
    )
    process {

        $installedRoles = GetXDInstalledRole -Role $Role;
        foreach ($r in $Role) {

            if ($installedRoles -notcontains $r) {
                return $false;
            }
        }

        return $true;

    } #end process
} #end function TestXDRole


function GetXDInstalledRole {
<#
    .SYNOPSIS
        Returns installed Citrix XenDesktop 7.x installed products.
#>

    [CmdletBinding()]
    [OutputType([System.String[]])]
    param (
        ## Citrix XenDesktop 7.x role to query.
        [Parameter(Mandatory)]
        [ValidateSet('Controller','Studio','Storefront','Licensing','Director','DesktopVDA','SessionVDA')]
        [System.String[]] $Role
    )
    process {

        $installedProducts = Get-ItemProperty 'HKLM:\SOFTWARE\Classes\Installer\Products\*' -ErrorAction SilentlyContinue |
            Where-Object { $_.ProductName -like '*Citrix*' -and $_.ProductName -notlike '*snap-in' } |
                Select-Object -ExpandProperty ProductName;

        $installedRoles = @();
        foreach ($r in $Role) {

            switch ($r) {

                'Controller' {
                    $filter = 'Citrix Broker Service';
                }
                'Studio' {
                    $filter = 'Citrix Studio';
                }
                'Storefront' {
                    $filter = 'Citrix Storefront$';
                }
                'Licensing' {
                    $filter = 'Citrix Licensing';
                }
                'Director' {
                    $filter = 'Citrix Director(?!.VDA Plugin)';
                }
                'DesktopVDA' {
                    $filter = 'Citrix Virtual Desktop Agent';
                }
                'SessionVDA' {
                    $filter = 'Citrix Virtual Desktop Agent';
                }
            }

            $result = $installedProducts -match $filter;
            if ([System.String]::IsNullOrEmpty($result)) {

            }
            elseif ($result) {
                $installedRoles += $r;
            }

        }

        return $installedRoles;

    } #end process
} #end function GetXDInstalledProduct


function ResolveXDSetupMedia {
<#
    .SYNOPSIS
        Resolve the correct installation media source for the
        local architecture depending on the role.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        ## Citrix XenDesktop 7.x role to install/uninstall.
        [Parameter(Mandatory)]
        [ValidateSet('Controller','Studio','Storefront','Licensing','Director','DesktopVDA','SessionVDA')]
        [System.String[]] $Role,

        ## Citrix XenDesktop 7.x installation media path.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $SourcePath
    )
    process {

        $architecture = 'x86';
        if ([System.Environment]::Is64BitOperatingSystem) {
            $architecture = 'x64';
        }

        if ($Role -contains 'DesktopVDA') {
            $installMedia = 'XenDesktopVdaSetup.exe';
        }
        elseif ($Role -contains 'SessionVDA') {
            $installMedia = 'XenDesktopVdaSetup.exe';
        }
        else {
            $installMedia = 'XenDesktopServerSetup.exe';
        }

        $sourceArchitecturePath = Join-Path -Path $SourcePath -ChildPath $architecture;
        $installMediaPath = Get-ChildItem -Path $sourceArchitecturePath -Filter $installMedia -Recurse -File;

        if (-not $installMediaPath) {
            throw ($localized.NoValidSetupMediaError -f $installMedia, $sourceArchitecturePath);
        }

        return $installMediaPath.FullName;

    } #end process
} #end function ResolveXDSetupMedia


function ResolveXDServerSetupArguments {
<#
    .SYNOPSIS
        Resolve the installation arguments for the associated XenDesktop role.
#>

    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        ## Citrix XenDesktop 7.x role to install/uninstall.
        [Parameter(Mandatory)]
        [ValidateSet('Controller','Studio','Storefront','Licensing','Director')]
        [System.String[]] $Role,

        ## Citrix XenDesktop 7.x installation media path.
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String] $LogPath = (Join-Path -Path $env:TMP -ChildPath '\Citrix\XenDesktop Installer'),

        ## Uninstall Citrix XenDesktop 7.x product.
        [Parameter()]
        [System.Management.Automation.SwitchParameter] $Uninstall
    )
    process {

        $arguments = New-Object -TypeName System.Collections.ArrayList -ArgumentList @();
        $arguments.AddRange(@('/QUIET', '/LOGPATH', "`"$LogPath`"", '/NOREBOOT', '/COMPONENTS'));

        $components = @();
        foreach ($r in $Role) {

            switch ($r) {
                ## Install/uninstall component names by role
                'Controller' {
                    $components += 'CONTROLLER';
                }
                'Studio' {
                    $components += 'DESKTOPSTUDIO';
                }
                'Storefront' {
                    $components += 'STOREFRONT';
                }
                'Licensing' {
                    $components += 'LICENSESERVER';
                }
                'Director' {
                    $components += 'DESKTOPDIRECTOR';
                }
            } #end switch Role
        }

        $componentString = [System.String]::Join(',', $components);
        [ref] $null = $arguments.Add($componentString);

        if ($Uninstall) {
            [ref] $null = $arguments.Add('/REMOVE');
        }
        else {
            ## Additional install parameters per role
            if ($Role -contains 'Controller') {
                [ref] $null = $arguments.Add('/NOSQL');
            }
            [ref] $null = $arguments.Add('/CONFIGURE_FIREWALL');

        }

        return [System.String]::Join(' ', $arguments.ToArray());

    } #end process
} #end function ResolveXDSetupArguments

#endregion Private Functions

# SIG # Begin signature block
# MIIcawYJKoZIhvcNAQcCoIIcXDCCHFgCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUCXsXxhzf35a5z6B56tJucdWe
# yNyggheaMIIFIzCCBAugAwIBAgIQAsbTxa4q6RSRmx0hkVyicTANBgkqhkiG9w0B
# AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz
# c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTE5MDcwMzAwMDAwMFoXDTIxMTEw
# MzEyMDAwMFowYDELMAkGA1UEBhMCR0IxDzANBgNVBAcTBkxvbmRvbjEfMB0GA1UE
# ChMWVmlydHVhbCBFbmdpbmUgTGltaXRlZDEfMB0GA1UEAxMWVmlydHVhbCBFbmdp
# bmUgTGltaXRlZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/s4NME
# JLA1Aji4EHJ85uwyEEAepndYn1X8pRnGkOlTzVHITAeH7BQnehjbNwCj7MHUPTSM
# zSucXhyfaMZthCNYtugyZ2uU4uVjB1f3xdmXXFX+aukMYgCk1ZQFbQMBqbzRY4Cl
# DwlLNGVEjDJeLBUL6ciIETqDc27YLg772WLpuvIXne13EYXN422Y83XRqEMf4v9S
# 398S8MRk5qdasRtxYZY6GciZZQnAL/XObpXDM3tDFgcQuyGcZttRuXVZXEj+mlY8
# gUIzkSJ0aJn1pVVTsa+tCvAZuJMJwdPhyM7NUa7Ysm7n9qdF7BvcrWBmaYRfDyya
# lLwRoOcI2HVodX8CAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl6nED
# wGD5LfZldQ5YMB0GA1UdDgQWBBSDFWXl70FjVfl8IBwATpE46qvGeDAOBgNVHQ8B
# Af8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOgMYYv
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmww
# NaAzoDGGL2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3Mt
# ZzEuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0
# dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcB
# AQR4MHYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggr
# BgEFBQcwAoZCaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hB
# MkFzc3VyZWRJRENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZI
# hvcNAQELBQADggEBAJ1VZZNMAy8KyQLBqwRWAWDqcOxjVly6FTIeqO2/ul9rEYm8
# B9mNx60/AL+TbTbUwBzia2pwBuIin70eClZHFstvQcASBbB0k14R/rs+jestfFRm
# rsEz272POc6vsKce3TOlqBc2rtvVyuUPRvI2yQm1WYTpOgQnnp3ix2LBd+fgRANs
# P9yurvnGdEFFzToFDXFVkFHBQ9Pr5tAb4i7ZkSFC52BtB7NVuoiH83lx07SyjIxU
# 11ELEDZBpO3HiTsTzbhPAEw4CP++ONK8fieWZevDK9DFEiNIC0gWL/DH1+c7eihO
# oJdJqRAT9wkAMIjcskZ5LObGvMst/hqwBewpLzYwggUwMIIEGKADAgECAhAECRgb
# X9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD
# VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi
# BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAw
# MDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp
# Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERp
# Z2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqG
# SIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE
# 9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvsp
# J8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWu
# HEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel0
# 5iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4P
# waLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHN
# MIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUE
# DDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6
# Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0f
# BHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNz
# dXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG
# /WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ
# UzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYD
# VR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEB
# AD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh
# 9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6R
# Ffu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEM
# j7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutm
# Q9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUu
# kpHqaGxEMrJmoecYpJpkUe8wggZqMIIFUqADAgECAhADAZoCOv9YsWvW1ermF/Bm
# MA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IEFzc3VyZWQgSUQgQ0EtMTAeFw0xNDEwMjIwMDAwMDBaFw0yNDEwMjIwMDAw
# MDBaMEcxCzAJBgNVBAYTAlVTMREwDwYDVQQKEwhEaWdpQ2VydDElMCMGA1UEAxMc
# RGlnaUNlcnQgVGltZXN0YW1wIFJlc3BvbmRlcjCCASIwDQYJKoZIhvcNAQEBBQAD
# ggEPADCCAQoCggEBAKNkXfx8s+CCNeDg9sYq5kl1O8xu4FOpnx9kWeZ8a39rjJ1V
# +JLjntVaY1sCSVDZg85vZu7dy4XpX6X51Id0iEQ7Gcnl9ZGfxhQ5rCTqqEsskYnM
# Xij0ZLZQt/USs3OWCmejvmGfrvP9Enh1DqZbFP1FI46GRFV9GIYFjFWHeUhG98oO
# jafeTl/iqLYtWQJhiGFyGGi5uHzu5uc0LzF3gTAfuzYBje8n4/ea8EwxZI3j6/oZ
# h6h+z+yMDDZbesF6uHjHyQYuRhDIjegEYNu8c3T6Ttj+qkDxss5wRoPp2kChWTrZ
# FQlXmVYwk/PJYczQCMxr7GJCkawCwO+k8IkRj3cCAwEAAaOCAzUwggMxMA4GA1Ud
# DwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMI
# MIIBvwYDVR0gBIIBtjCCAbIwggGhBglghkgBhv1sBwEwggGSMCgGCCsGAQUFBwIB
# FhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIIBZAYIKwYBBQUHAgIwggFW
# HoIBUgBBAG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBm
# AGkAYwBhAHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0
# AGEAbgBjAGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAv
# AEMAUABTACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0
# AHkAIABBAGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAg
# AGwAaQBhAGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBw
# AG8AcgBhAHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBu
# AGMAZQAuMAsGCWCGSAGG/WwDFTAfBgNVHSMEGDAWgBQVABIrE5iymQftHt+ivlcN
# K2cCzTAdBgNVHQ4EFgQUYVpNJLZJMp1KKnkag0v0HonByn0wfQYDVR0fBHYwdDA4
# oDagNIYyaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElE
# Q0EtMS5jcmwwOKA2oDSGMmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dEFzc3VyZWRJRENBLTEuY3JsMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURDQS0xLmNydDANBgkq
# hkiG9w0BAQUFAAOCAQEAnSV+GzNNsiaBXJuGziMgD4CH5Yj//7HUaiwx7ToXGXEX
# zakbvFoWOQCd42yE5FpA+94GAYw3+puxnSR+/iCkV61bt5qwYCbqaVchXTQvH3Gw
# g5QZBWs1kBCge5fH9j/n4hFBpr1i2fAnPTgdKG86Ugnw7HBi02JLsOBzppLA044x
# 2C/jbRcTBu7kA7YUq/OPQ6dxnSHdFMoVXZJB2vkPgdGZdA0mxA5/G7X1oPHGdwYo
# FenYk+VVFvC7Cqsc21xIJ2bIo4sKHOWV2q7ELlmgYd3a822iYemKC23sEhi991VU
# QAOSK2vCUcIKSK+w1G7g9BQKOhvjjz3Kr2qNe9zYRDCCBs0wggW1oAMCAQICEAb9
# +QOWA63qAArrPye7uhswDQYJKoZIhvcNAQEFBQAwZTELMAkGA1UEBhMCVVMxFTAT
# BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEk
# MCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTA2MTExMDAw
# MDAwMFoXDTIxMTExMDAwMDAwMFowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERp
# Z2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMY
# RGlnaUNlcnQgQXNzdXJlZCBJRCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
# MIIBCgKCAQEA6IItmfnKwkKVpYBzQHDSnlZUXKnE0kEGj8kz/E1FkVyBn+0snPgW
# Wd+etSQVwpi5tHdJ3InECtqvy15r7a2wcTHrzzpADEZNk+yLejYIA6sMNP4YSYL+
# x8cxSIB8HqIPkg5QycaH6zY/2DDD/6b3+6LNb3Mj/qxWBZDwMiEWicZwiPkFl32j
# x0PdAug7Pe2xQaPtP77blUjE7h6z8rwMK5nQxl0SQoHhg26Ccz8mSxSQrllmCsSN
# vtLOBq6thG9IhJtPQLnxTPKvmPv2zkBdXPao8S+v7Iki8msYZbHBc63X8djPHgp0
# XEK4aH631XcKJ1Z8D2KkPzIUYJX9BwSiCQIDAQABo4IDejCCA3YwDgYDVR0PAQH/
# BAQDAgGGMDsGA1UdJQQ0MDIGCCsGAQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMG
# CCsGAQUFBwMEBggrBgEFBQcDCDCCAdIGA1UdIASCAckwggHFMIIBtAYKYIZIAYb9
# bAABBDCCAaQwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3Nz
# bC1jcHMtcmVwb3NpdG9yeS5odG0wggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5
# ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABl
# ACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAg
# AG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABh
# AG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwBy
# AGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBp
# AGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABl
# AGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJ
# YIZIAYb9bAMVMBIGA1UdEwEB/wQIMAYBAf8CAQAweQYIKwYBBQUHAQEEbTBrMCQG
# CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKG
# N2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv
# b3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9j
# cmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwHQYD
# VR0OBBYEFBUAEisTmLKZB+0e36K+Vw0rZwLNMB8GA1UdIwQYMBaAFEXroq/0ksuC
# MS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUAA4IBAQBGUD7Jtygkpzgdtlspr1LP
# UukxR6tWXHvVDQtBs+/sdR90OPKyXGGinJXDUOSCuSPRujqGcq04eKx1XRcXNHJH
# hZRW0eu7NoR3zCSl8wQZVann4+erYs37iy2QwsDStZS9Xk+xBdIOPRqpFFumhjFi
# qKgz5Js5p8T1zh14dpQlc+Qqq8+cdkvtX8JLFuRLcEwAiR78xXm8TBJX/l/hHrwC
# Xaj++wc4Tw3GXZG5D2dFzdaD7eeSDY2xaYxP+1ngIw/Sqq4AfO6cQg7Pkdcntxbu
# D8O9fAqg7iwIVYUiuOsYGk38KiGtSTGDR5V3cdyxG0tLHBCcdxTBnU8vWpUIKRAm
# MYIEOzCCBDcCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0
# IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNl
# cnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBDQQIQAsbTxa4q6RSRmx0h
# kVyicTAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkq
# hkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGC
# NwIBFTAjBgkqhkiG9w0BCQQxFgQUsenUF029HPJNBWpfKAjJXTtolacwDQYJKoZI
# hvcNAQEBBQAEggEADlkRVu44VUYLZ9BQsHLcgQ6qQfWE+UfS+I5Nt9WuUvlma9H5
# +nNqRiNCH1PX7PbdKJjbSPKPqEGvqsgpMJAxzZ4UT8VXv6Ma273WuIukB0ygww9h
# 9qC8wWT2FoZKWd3V2XFb5ShqjIT4KnWoyTG+t/2FMqsFG8Of+QuvTbwlBQ4Peous
# 2tmQouQc4dchQGIoLPoX0LcHW4ulC40lhVgi7YoHjvi2/3oxRTDjSYc4zJrKHEdj
# kjmIik/ytuH4Vji0Yg9INSMMMi/g41T4YUMcuWeButCDh6LgDK9VP8wzi5b54SiU
# Jjeb6cqyDKIeGfaNkQKAk46qIqICQSSQRm2+HaGCAg8wggILBgkqhkiG9w0BCQYx
# ggH8MIIB+AIBATB2MGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IEFzc3VyZWQgSUQgQ0EtMQIQAwGaAjr/WLFr1tXq5hfwZjAJBgUrDgMCGgUAoF0w
# GAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTkwNzE5
# MjAzMDQ2WjAjBgkqhkiG9w0BCQQxFgQU43L9hjg8WE4jhaFxmCX2vQhxfeYwDQYJ
# KoZIhvcNAQEBBQAEggEAAIwqHiECVEcGsDMspfqXUNzFhRRav6c5UWWE2gOazV9s
# VaZm/OBivgOtm5I7Ad3aGK8DfswwI/4di3pPhaML1uIs6f/Xb0q9sgg5ifu3wJ0X
# iRhNGHFy+kGugQooVgdRd44l09ytJsHAXyeZbrCgJ2xwnC4jW2HuhB8EUq6Ukwz3
# YHDV6+/RbnwOf8dEQ8h9Bg92QxmWTISHpWxMt7xzs0/4DiL9EISlARclc1tpG360
# 6tjP47eLmG+erYMUhYzOs1Eq1HwPlUJw+jOgE25QZEWn36/17A3h8nt6NQ+vEYfT
# trYPxiS4DTcsa8e3Py8VkJatkl7bmQDeVdCKodMT9Q==
# SIG # End signature block