Microsoft.RdInfra.RDPowershell.Migration.psm1

#################################################################
## Copyright (c) 2020 Microsoft Corporation. All rights reserved.
#################################################################

#Global Constants
#Schema for resource group level deployments
$Script:ResourceGroupSchema = "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#"

#Schema for subscription level deployments
$Script:SubscriptionSchema = "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#"
$Script:UserAssignmentRole = "1d18fff3-a72a-46b5-b4a9-0b38a3cd7e63"

#Guid for the "Desktop Virtualization User" role
$Script:UserAssignmentRole = "1d18fff3-a72a-46b5-b4a9-0b38a3cd7e63"

#Maximum number of role assignments allowed in a subscription
$Script:MaxUserAssignments = 2000

#Valid locations to migrate to
$Script:ValidMigrationLocations = @("eastus","eastus2","westus","westus2","northcentralus","southcentralus","westcentralus","centralus")

<#
    Writes the given string to Write-Host and forks to the output file, if it was specified
 
    Parameters:
        $s : string to write
 
    Returns:
        none
#>

function Write-ForkedOutput
{
Param(
    [Parameter(Mandatory=$true)]
    $s
)

    Write-Host $s

    if ($Script:OutputFile -ne $null)
    {
        $s | Out-File -FilePath $Script:OutputFile -Append
    }
}

<#
    Normalizes the given location string.
 
    Remove spaces, and convert to lowercase.
 
    Parameters:
        $Location : Location to normalize
 
    Returns:
        Normalized location
#>

function NormalizeLocation
{
Param(
    [Parameter(Mandatory=$true)]
    $Location
)

    return $Location.ToLower().Replace(' ','')
}

<#
    Checks all requirements to run the cmdlets.
 
    Parameters:
        $Location : Location for migration operation
 
    Returns:
        $true if requirements met
#>

function CheckAllRequirements
{
Param(
    [Parameter(Mandatory=$false)]
    $Location
)
    $result = CheckAzContext -and CheckRdsContext

    if ($result -eq $true -and $Location -ne $null)
    {
        $result = $false
        $normalizedLocation = NormalizeLocation $Location

        foreach ($validMigrationLocation in $Script:ValidMigrationLocations)
        {
            if ($normalizedLocation -eq $validMigrationLocation)
            {
                $result = $true
                break;
            }
        }

        if ($result -eq $false)
        {
            Write-ForkedOutput "Invalid location $Location"
            $locationString = $Script:ValidMigrationLocations -join ','
            Write-ForkedOutput "Location must be one of" $locationString
        }
    }

    return $result
}

<#
    Checks to see if we are properly authenticated with Azure
 
    Returns:
        $true if we are logged in properly
#>

function CheckAzContext
{
    $context = Get-AzContext

    if ($context -eq $null)
    {
        Write-ForkedOutput "Please authenticate with Azure using the Login-AzAccount cmdlet"
        return $false
    }

    return $true
}

<#
    Checks to see if we have the rds context set properly
 
    Returns:
        $true if we are logged in properly
#>

function CheckRdsContext
{
    if ($Global:rdMgmtContext -eq $null -or $Global:AdalContext -eq $null)
    {
        Write-ForkedOutput "Run Set-RdsMigrationContext before running this command"
        return $false
    }
}

<#
    Creates a valid resource group name from the passed in base name
    Resource Groups must match this pattern:
 
        Length: 1-90 characters
        Regex pattern: ^[-\w\._\(\)]+$
 
    Will truncate to 87 characters
    Spaces are not allowed.
    '/' is not allowed
    Will replace all disallowed characters with '_'
 
    Parameters:
        $BaseName : The base name to create the resource group from.
        $ResourceName : Optional resource name if there is a sub type that needs to be scoped, like hostpool
 
    Returns:
        Valid resource group name
#>

function CreateResourceGroupName
{
Param(
    [Parameter(Mandatory=$true)]
    $BaseName,
    [Parameter(Mandatory=$false)]
    $ResourceName
)

    if ($ResourceName -ne $null)
    {
        $formattedName = "{0}_{1}" -f $BaseName,$ResourceName
    }
    else
    {
        $formattedName = $BaseName
    }

    #Truncate to 87 characters so final length will be 90 or less
    if ($formattedName.Length -gt 87)
    {
        $msg = "Migration resource group name of {0}-rg exceeds maximum length of 90. Aborting migration"
        throw $msg
    }

    $resourceGroupName = "{0}-rg" -f $formattedName
    $resourceGroupName = $resourceGroupName -replace ' ','_'
    $resourceGroupName = $resourceGroupName -replace '/','_'

    return $resourceGroupName
}

<#
    Set up the user assignments saved in the $Script:UserAssignments hash table.
 
    If the user doesn't exist, an error is output and we continue on.
 
    Returns:
        None
#>

function SetupUserAssignments
{
    foreach ($scope in $Script:UserAssignments.Keys)
    {
        $userUpns = $Script:UserAssignments[$scope]
        foreach ($userUpn in $userUpns)
        {
            Write-ForkedOutput "Assigning user $userUpn to $scope"
            $user = Get-AzADUser -UserPrincipalName $userUpn
            if ($user -eq $null)
            {
                Write-ForkedOutput "User $userUpn not found, skipping"
            }
            else
            {
                $roleAssignment = Get-AzRoleAssignment -Scope $scope -RoleDefinitionId $Script:UserAssignmentRole -ObjectId $user.Id
                if ($roleAssignment -eq $null)
                {
                    try
                    {
                        if ($Script:ShouldProcess -eq $true)
                        {
                            $roleAssignment = New-AzRoleAssignment -Scope $scope -RoleDefinitionId $Script:UserAssignmentRole -ObjectId $user.Id
                        }
                    }
                    catch
                    {
                        Write-ForkedOutput "Unable to add role assignment"
                        Write-ForkedOutput $_.Exception.Message
                    }
                }
            }
        }
    }
}

<#
    Save the user assignment in the $Script:UserAssignments hash table.
 
    Parameters:
        $Scope : The resource ID of the Application Group that the user assignment is for
        $Upn : The UPN to be assigned to the Application Group
 
    Returns:
        None
#>

function SaveUserAssignment
{
Param(
    [Parameter(Mandatory=$true)]
    $Scope,
    [Parameter(Mandatory=$true)]
    $Upn
)
    $users = $Script:UserAssignments[$Scope]

    if ($users -eq $null)
    {
        $users = @()
    }

    if ($users.Contains($Upn) -eq $false)
    {
        $users += $Upn
        $Script:RequiredUserAssignments = $Script:RequiredUserAssignments + 1
    }

    $Script:UserAssignments[$Scope] = $users
}

<#
    Checks to see if the given subscription has enough role assignment capacity to hold
    the required user assigments.
 
    A subscription can only hold a certain number of them, defined in the $Script:MaxUserAssignments
    script variable
 
    Parameters:
        $Subscription : The subscription to check
 
    Returns:
        $true if the user assignments can be added to the subscription
#>

function ValidateUserAssignments
{
Param(
    [Parameter(Mandatory=$true)]
    $Subscription
)
    $scope = "/subscriptions/{0}" -f $Subscription

    $currentRoleAssignments = Get-AzRoleAssignment -Scope $scope

    if ($Script:RequiredUserAssignments -gt ($Script:MaxUserAssignments - $currentRoleAssignments.Count))
    {
        return $false
    }

    return $true
}

<#
    Checks to see if the given workspace exists
 
    Parameters:
        $WorkspaceArmPath : The ID of the workspace to check
 
    Returns:
        $true if exists
        $false if not
#>

function WorkspaceExists
{
Param(
    [Parameter(Mandatory=$true)]
    $WorkspaceArmPath
)

    try
    {
        Get-AzResource -ResourceId $WorkspaceArmPath > $null
    }
    catch
    {
        return $false
    }

    return $true
}

<#
    Checks to see if the given workspace is in the correct location
 
    Parameters:
        $WorkspaceArmPath : The ID of the workspace to check
        $ExpectedLocation : The expected location
 
    Returns:
        $true if is in the correct location
        $false if not
#>

function WorkspaceIsInCorrectLocation
{
Param(
    [Parameter(Mandatory=$true)]
    $WorkspaceArmPath,
    [Parameter(Mandatory=$true)]
    $ExpectedLocation
)

    try
    {
        $ws = Get-AzResource -ResourceId $WorkspaceArmPath
        $wsLocationNormalized = NormalizeLocation -Location $ws.Location
        $expectedLocationNormalized = NormalizeLocation -Location $ExpectedLocation
        if ($expectedLocationNormalized -ne $wsLocationNormalized)
        {
            $status = "Workspace {0} is in location {1} rather than required location {2}" -f $workspaceArmPath,$ws.Location,$ExpectedLocation
            Write-ForkedOutput $status
            return $false
        }
    }
    catch
    {
        return $false
    }

    return $true
}

<#
    Creates a workspace based on the name of the Tenant.
 
    It will create it in the resource group $Tenant-RG, creating the resource group if it doesn't
    already exist.
 
    Parameters:
        $Subscription : The subscription to create the workspace in
        $Tenant : The tenant to create the workspace for
        $Location : The location to create the workspace in
 
    Returns:
        The resource ID of the workspace
#>

function CreateTenantWorkspace
{
Param(
    [Parameter(Mandatory=$true)]
    $Subscription,
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Location
)
    $resourceGroupName = CreateResourceGroupName $Tenant

    $resourceGroup = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue -ErrorVariable notpresent

    if ($resourceGroup -eq $null)
    {
        Write-ForkedOutput "Create $resourceGroupName to contain the workspace $Tenant"
        if ($Script:ShouldProcess)
        {
            $resourceGroup = New-AzResourceGroup -Name $resourceGroupName -Location $Location -Tags $Script:DefaultTags
        }
    }

    $workspaceArmPath = "/subscriptions/"+$Subscription+"/resourcegroups/"+$resourceGroupName+"/providers/Microsoft.DesktopVirtualization/workspaces/"+$Tenant

    $exists = WorkspaceExists $workspaceArmPath

    if ($exists -eq $true)
    {
        $isInCorrectLocation = WorkspaceIsInCorrectLocation -WorkspaceArmPath $workspaceArmPath -ExpectedLocation $Location
        if ($isInCorrectLocation -eq $false)
        {
            throw "Unable to create workspace $workspaceArmPath"
        }

        Write-ForkedOutput "Using workspace $workspaceArmPath for ApplicationGroups"
        return $workspaceArmPath
    }

    $rdsTenant = Get-RdsTenant -TenantName $Tenant

    $template = @{}
    $template["`$schema"] = $Script:ResourceGroupSchema
    $template["contentVersion"] = "1.0.0.0"
    $template["parameters"] = @{}
    $template["variables"] = @{}
    $template["outputs"] = @{}

    $properties = @{}
    $properties["Description"] = $rdsTenant.Description
    $properties["FriendlyName"] = $rdsTenant.FriendlyName

    $workspace = @{}
    $workspace["apiVersion"] = "2019-01-23-preview"
    $workspace["type"] = "Microsoft.DesktopVirtualization/workspaces"
    $workspace["name"] = $Tenant
    $workspace["location"] = $Location
    $workspace["tags"] = $Script:DefaultTags
    $workspace["properties"] = $properties

    $templateResources = @()
    $templateResources += $workspace

    $template["resources"] = $templateResources

    Write-ForkedOutput "Creating workspace $Tenant for ApplicationGroups"

    if ($Script:ShouldProcess)
    {
        $deployment = New-AzResourceGroupDeployment -TemplateObject $template -ResourceGroupName $resourceGroupName
        Write-ForkedOutput "Template deployment result"
        $s = $deployment | ConvertTo-Json
        Write-ForkedOutput $s
    }

    Write-ForkedOutput "Workspace $Tenant created"

    return $workspaceArmPath
}

<#
    Gets a list of HostPools
 
    It will either get all of the ones under a tenant, or just a single one
 
    Parameters:
        $Tenant : The tenant to get the HostPools from
        $HostPool : Optionally, fetch a specific host pool
 
    Returns:
        Array of HostPools
#>

function FetchHostPools
{
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$false)]
    $HostPool
)

    if ($HostPool -eq $null)
    {
        try
        {
            [array]$hostPools = Get-RdsHostPool -TenantName $Tenant -ShowHidden
        }
        catch
        {
            Write-ForkedOutput $_.Exception.Message
            throw "Error accessing Tenant $Tenant"
        }
    }
    else
    {
        try
        {
            [array]$hostPools = Get-RdsHostPool -TenantName $Tenant -Name $HostPool -ShowHidden
        }
        catch
        {
            Write-ForkedOutput $_.Exception.Message
            throw "Error accessing HostPool $HostPool"
        }
    }

    return $hostPools
}

<#
    Adds the Application Group references in $Script:AppGroupRefs to the given workspace
 
    Parameters:
        $WorkspaceArmPath : The arm path of the workspace to update
 
    Returns:
        None
#>

function UpdateWorkspaceAppGroupRefs
{
Param(
    [Parameter(Mandatory=$true)]
    [string]$WorkspaceArmPath
)
    Write-ForkedOutput "Updating workspace $WorkspaceArmPath to include hybrid application groups"

    if ($Script:ShouldProcess -eq $false)
    {
        foreach ($appgroupref in $Script:AppGroupRefs)
        {
            Write-ForkedOutput "Adding $appgroupref"
        }

        return
    }

    $ws = Get-AzResource -ResourceId $WorkspaceArmPath

    $updatedAppGroupRefs = @()

    foreach ($appgroupRef in $ws.Properties.applicationGroupReferences)
    {
        if ($appgroupRef -ne $null)
        {
            Write-ForkedOutput "Adding $appgroupref"
            $updatedAppGroupRefs += $appgroupRef
        }
    }

    foreach ($appgroupref in $Script:AppGroupRefs)
    {
        if ($ws.Properties.applicationGroupReferences -notcontains $appgroupRef)
        {
            $updatedAppGroupRefs += $appgroupRef
        }
    }

    $template = @{}
    $template["`$schema"] = $Script:ResourceGroupSchema
    $template["contentVersion"] = "1.0.0.0"
    $template["parameters"] = @{}
    $template["variables"] = @{}
    $template["outputs"] = @{}

    $properties = @{}
    $properties["applicationGroupReferences"] = $updatedAppGroupRefs

    $workspace = @{}
    $workspace["apiVersion"] = "2019-01-23-preview"
    $workspace["type"] = "Microsoft.DesktopVirtualization/workspaces"
    $workspace["name"] = $ws.Name
    $workspace["location"] = $ws.Location
    $workspace["tags"] = $ws.Tags
    $workspace["properties"] = $properties

    $templateResources = @()
    $templateResources += $workspace

    $template["resources"] = $templateResources

    Write-ForkedOutput "Deploying Workspace update"

    $deployment = New-AzResourceGroupDeployment -TemplateObject $template -ResourceGroupName $ws.ResourceGroupName

    Write-ForkedOutput "Template deployment result"
    $s = $deployment | ConvertTo-Json
    Write-ForkedOutput $s

    Write-ForkedOutput "Workspace update complete"
}

<#
    Creates the resource section for the given Application Group
 
    Parameters:
        $HostPoolName : The name of the host pool
        $ApplicationGroup : The V1 application group object
        $AppGroupArmPath : The arm path of the Application Group to create
        $CopyUserAssignments : Boolean to indicate that user assignments should be copied
 
    Returns:
        Hash table of the resource section
#>

function CreateApplicationGroupJson
{
Param(
    [Parameter(Mandatory=$true)]
    $HostPoolName,
    [Parameter(Mandatory=$true)]
    $ApplicationGroup,
    [Parameter(Mandatory=$true)]
    $AppGroupArmPath,
    [Parameter(Mandatory=$true)]
    $CopyUserAssignments
)
    $Script:AppGroupRefs += $AppGroupArmPath

    $properties = @{}
    $migrationRequest = @{}
    $migrationRequest["Operation"] = "Start"
    $properties["MigrationRequest"] = $migrationRequest

    $applicationGroupFragment = @{}
    $applicationGroupFragment["apiVersion"] = "2019-01-23-preview"
    $applicationGroupFragment["type"] = "Microsoft.DesktopVirtualization/applicationGroups"
    $applicationGroupFragment["name"] = $ApplicationGroup.AppGroupName
    $applicationGroupFragment["location"] = $Location
    $applicationGroupFragment["tags"] = $Script:DefaultTags
    $applicationGroupFragment["properties"] = $properties
    $dependsOn = @()
    $dependsOn += "[concat('Microsoft.DesktopVirtualization/hostpools/', '$HostPoolName')]"
    $applicationGroupFragment["dependsOn"] = $dependsOn

    if ($CopyUserAssignments -eq $true)
    {
        [array]$users = Get-RdsAppGroupUser -TenantName $ApplicationGroup.TenantName -HostPoolName $ApplicationGroup.HostPoolName -AppGroupName $ApplicationGroup.AppGroupName

        foreach ($user in $users)
        {
            SaveUserAssignment -Scope $AppGroupArmPath -Upn $user.UserPrincipalName
        }
    }

    return $applicationGroupFragment
}

<#
    Creates the resource section for the given Resource Group
 
    Parameters:
        $ResourceGroupName : The name of the resource group
        $Location : Location of the resource group
 
    Returns:
        Hash table of the resource section
#>

function CreateResourceGroupJson
{
Param(
    [Parameter(Mandatory=$true)]
    $ResourceGroupName,
    [Parameter(Mandatory=$true)]
    $Location
)

    $properties = @{}

    $fragment = @{}
    $fragment["apiVersion"] = "2018-05-01"
    $fragment["type"] = "Microsoft.Resources/resourceGroups"
    $fragment["name"] = $ResourceGroupName
    $fragment["location"] = $Location
    $fragment["tags"] = $Script:DefaultTags
    $fragment["properties"] = $properties

    return $fragment
}

<#
    Creates the resource section for the given host pool
 
    Parameters:
        $HostPoolArmPath : Arm path of the created host pool
        $HostPool : V1 host pool object
        $Location : Location for the created host pool
        $Operation : Operation to take. Start, Revoke, Complete, Hide, or Unhide
 
    Returns:
        Hash table of the resource section
#>

function CreateHostPoolResourceJson
{
Param(
    [Parameter(Mandatory=$true)]
    $HostPool,
    [Parameter(Mandatory=$true)]
    $Location,
    [Parameter(Mandatory=$true)]
    [ValidateSet("Start","Revoke","Complete","Hide","Unhide")]
    $Operation
)
    $migrationPath = "TenantGroups/"+$HostPool.TenantGroupName+"/Tenants/"+$HostPool.TenantName+"/HostPools/"+$HostPool.HostPoolName

    $properties = @{}
    $migrationRequest = @{}
    $migrationRequest["Operation"] = $Operation
    $migrationRequest["MigrationPath"] = $migrationPath
    $properties["MigrationRequest"] = $migrationRequest

    $hostPoolFragment = @{}
    $hostPoolFragment["apiVersion"] = "2019-01-23-preview"
    $hostPoolFragment["type"] = "Microsoft.DesktopVirtualization/hostpools"
    $hostPoolFragment["name"] = $HostPool.HostPoolName
    $hostPoolFragment["location"] = $Location
    $hostPoolFragment["tags"] = $Script:DefaultTags
    $hostPoolFragment["properties"] = $properties

    return $hostPoolFragment
}

<#
    Creates the resource section for the host pool and associated application groups
 
    Parameters:
        $Tenant : V1 tenant name
        $Subscription : Subscription to create the host pool in
        $Location : Location for the created host pool
        $HostPool : V1 host pool object
        $CopyUserAssignments : Set to $true will copy the user assignments
        $Operation : Operation to take. Start, Revoke, Complete, Hide, or Unhide
 
    Returns:
        Hash table of the resource section
#>

function CreateHostPoolResourceJsonArray
{
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Subscription,
    [Parameter(Mandatory=$false)]
    $Location,
    [Parameter(Mandatory=$true)]
    $HostPool,
    [Parameter(Mandatory=$true)]
    $CopyUserAssignments,
    [Parameter(Mandatory=$true)]
    [ValidateSet("Start","Revoke","Complete","Hide","Unhide")]
    $Operation
)
    if ($Location -eq $null)
    {
        $hostpoolArmPath = GetHostPoolArmPath `
            -Tenant $Tenant `
            -Subscription $Subscription `
            -HostPool $hostPool

        $Location = GetArmObjectLocation -ResourceId $hostpoolArmPath

        # The host pool does not exist in ARM, return empty array
        if ($Location -eq $null)
        {
            return @()
        }
    }

    $resourceGroupName = CreateResourceGroupName $Tenant $HostPool.HostPoolName

    $resourceGroup = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue -ErrorVariable notpresent

    if ($resourceGroup -eq $null)
    {
        if ($Operation -eq "Start")
        {
            $status = "Creating resource group $resourceGroupName for HostPool {0}" -f $HostPool.HostPoolName
            Write-ForkedOutput $status
        }
        $resourceGroupLocation = $Location
    }
    else
    {
        if ($Operation -eq "Start")
        {
            $status = "Using resource group $resourceGroupName for HostPool {0}" -f $HostPool.HostPoolName
            Write-ForkedOutput $status
        }
        $resourceGroupLocation = $resourceGroup.Location
    }

    $resources = @()

    #First resource is the resource group for the HostPool and other objects
    $resources += CreateResourceGroupJson `
        -ResourceGroupName $resourceGroupName `
        -Location $resourceGroupLocation

    #Second resource is a nested deployment that deploys the HostPool and associated applicationgroups to
    #the new resource group.
    #First, create the nested Template, and then create the deployment resource

    $template = @{}
    $template["`$schema"] = $Script:ResourceGroupSchema
    $template["contentVersion"] = "1.0.0.0"
    $template["parameters"] = @{}
    $template["variables"] = @{}
    $template["outputs"] = @{}

    $hostpoolArmPath = "/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}" -f `
        $Subscription, `
        $resourceGroupName, `
        $HostPool.HostPoolName

    if ($Operation -eq "Start")
    {
        $status = "Migrate HostPool '{0}' to $resourceGroupName" -f $HostPool.HostPoolName
        Write-ForkedOutput $status
    }

    $hostPoolFragment = CreateHostPoolResourceJson `
        -HostPool $HostPool `
        -Location $Location `
        -Operation $Operation

    $templateResources = @()
    $templateResources += $hostPoolFragment

    if ($Operation -eq "Start")
    {
        [array]$appgroups = Get-RdsAppGroup -TenantName $HostPool.TenantName -HostPoolName $HostPool.HostPoolName -ShowHidden
        foreach ($appgroup in $appgroups)
        {
            $appgroupArmPath = "/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.DesktopVirtualization/applicationgroups/{2}" -f `
                $Subscription, `
                $resourceGroupName, `
                $appgroup.AppGroupName

            $status = "Migrate AppGroup '{0}' to resource group {1}" -f $appgroup.AppGroupName,$resourceGroupName
            Write-ForkedOutput $status

            $applicationGroupFragment = CreateApplicationGroupJson `
                -HostPoolName $HostPool.HostPoolName `
                -ApplicationGroup $appgroup `
                -AppGroupArmPath $appgroupArmPath `
                -CopyUserAssignments $CopyUserAssignments
            $templateResources += $applicationGroupFragment
        }
    }

    $randGuid = (New-Guid).ToString()

    $template["resources"] = $templateResources

    $deployment = @{}
    $deployment["apiVersion"] = "2018-05-01"
    $deployment["type"] = "Microsoft.Resources/deployments"
    $deployment["name"] = "deploy-{0}" -f $randGuid
    $deployment["resourceGroup"] = $resourceGroupName
    $dependsOn = @()
    $dependsOn += "[concat('Microsoft.Resources/resourceGroups/', '$resourceGroupName')]"
    $deployment["dependsOn"] = $dependsOn
    $deploymentProperties = @{}
    $deploymentProperties["mode"] = "Incremental"
    $deploymentProperties["template"] = $template
    $deployment["properties"] = $deploymentProperties

    $resources += $deployment

    return $resources
}

<#
    Creates the template to migrate the list of host pool and associated application groups
 
    Parameters:
        $Tenant : V1 tenant name
        $Subscription : Subscription to create the host pool in
        $HostPools : List of V1 host pool object
        $Location : Location for the created host pool
        $Operation : Operation to take. Start, Revoke, Complete, Hide, or Unhide
        $CopyUserAssignments : Set to $true will copy the user assignments
 
    Returns:
        Full template
#>

function CreateMigrationTemplate
{
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Subscription,
    [Parameter(Mandatory=$true)]
    $HostPools,
    [Parameter(Mandatory=$false)]
    $Location,
    [Parameter(Mandatory=$true)]
    $Operation,
    [Parameter(Mandatory=$false)]
    $CopyUserAssignments = $true
)
    $template = @{}
    $template["`$schema"] = $Script:SubscriptionSchema
    $template["contentVersion"] = "1.0.0.0"

    $resources = @()

    foreach ($hostPool in $HostPools)
    {
        [array]$hostPoolResources = CreateHostPoolResourceJsonArray  `
            -Tenant $Tenant `
            -Subscription $Subscription `
            -Location $Location `
            -HostPool $hostPool `
            -CopyUserAssignments $CopyUserAssignments `
            -Operation $Operation

        foreach ($resource in $hostPoolResources)
        {
            $resources += $resource
        }
    }

    $template["resources"] = $resources

# $json = ConvertTo-Json $template -Depth 10
# $fixedJson = $json -replace "\\u0027", "'"

# return $fixedJson

    return $template
}

<#
    Returns a formatted resource ID for the given V1 HostPool
 
    Parameters:
        $Tenant : V1 tenant name
        $Subscription : Subscription the host pools is in
        $HostPool : V1 host pool object
 
    Returns:
        string
#>

function GetHostPoolArmPath
{
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Subscription,
    [Parameter(Mandatory=$true)]
    $HostPool
)
    $resourceGroupName = CreateResourceGroupName $Tenant $HostPool.HostPoolName

    $hostpoolArmPath = "/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}" -f `
        $Subscription, `
        $resourceGroupName, `
        $HostPool.HostPoolName

    return $hostpoolArmPath
}

<#
    Returns the location of the given resource
 
    Parameters:
        $ResourceId : resource ID of the object
 
    Returns:
        string
#>

function GetArmObjectLocation
{
Param(
    [Parameter(Mandatory=$true)]
    $ResourceId
)
    try
    {
        $resource = Get-AzResource -ResourceId $ResourceId

        return $resource.Location
    }
    catch
    {
        return $null
    }
}

<#
    Returns a formatted resource ID for the given V1 appgroup
 
    Parameters:
        $Tenant : V1 tenant name
        $Subscription : Subscription the host pools is in
        $HostPool : V1 host pool object
        $Appgroup : V1 appgroup object
 
    Returns:
        string
#>

function GetAppGroupArmPath
{
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Subscription,
    [Parameter(Mandatory=$true)]
    $HostPool,
    [Parameter(Mandatory=$true)]
    $Appgroup
)
    $resourceGroupName = CreateResourceGroupName $Tenant $HostPool.HostPoolName

    $appgroupArmPath = "/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.DesktopVirtualization/applicationgroups/{2}" -f `
        $Subscription, `
        $resourceGroupName, `
        $Appgroup.AppGroupName

    return $appgroupArmPath
}

<#
    Gets a list of the ArmObjects that were created during a Start Migration
 
    Parameters:
        $Tenant : V1 tenant name
        $Subscription : Subscription the host pools are in
        $HostPools : List of V1 host pool objects
 
    Returns:
        List of strings
#>

function GetArmObjectList
{
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Subscription,
    [Parameter(Mandatory=$true)]
    $HostPools
)

    $armObjectList = @()

    foreach ($hostPool in $HostPools)
    {
        [array]$appgroups = Get-RdsAppGroup -TenantName $HostPool.TenantName -HostPoolName $HostPool.HostPoolName -ShowHidden
        foreach ($appgroup in $appgroups)
        {
            $appgroupArmPath = GetAppGroupArmPath `
                -Tenant $Tenant `
                -Subscription $Subscription `
                -HostPool $HostPool `
                -Appgroup $appgroup

            $armObjectList += $appgroupArmPath
        }

        $hostpoolArmPath = GetHostPoolArmPath `
            -Tenant $Tenant `
            -Subscription $Subscription `
            -HostPool $hostPool

        $armObjectList += $hostpoolArmPath
    }

    return $armObjectList
}

<#
    Deletes the HostPools and ApplicationGroups that were created during a Start Migration
 
    Parameters:
        $Tenant : V1 tenant name
        $Subscription : Subscription the host pool is in
        $HostPools : List of V1 host pool object
#>

function DeleteRevertedObjects
{
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Subscription,
    [Parameter(Mandatory=$true)]
    $HostPools
)

    $armObjectsToDelete = GetArmObjectList `
        -Tenant $Tenant `
        -Subscription $Subscription `
        -HostPools $HostPools

    foreach ($armObjectToDelete in $armObjectsToDelete)
    {
        Remove-AzResource -ResourceId $armObjectToDelete -Force > $null
    }
}

function Start-RdsHostPoolMigration
{
    <#
    .synopsis
        Start migrating a legacy HostPool or HostPools to the arm model
    .example
        Start-RdsHostPoolMigration -Tenant Contoso -HostPool Office -CopyUserAssignments -Location EastUS
        Start-RdsHostPoolMigration -Tenant Contoso -Location WestEU
 
    .description
        This initiates migrating the given HostPool, or all HostPools in a Tenant to V2 of WVD
    #>

[cmdletbinding(SupportsShouldProcess=$true,PositionalBinding=$false)]
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Location,
    [Parameter(Mandatory=$false)]
    $HostPool,
    [Parameter(Mandatory=$false)]
    $Workspace,
    [Parameter(Mandatory=$false)]
    $OutputFile,
    [Parameter(Mandatory=$false)]
    [Switch]$CopyUserAssignments
)
    $ErrorActionPreference = "Stop"

    if ($OutputFile -ne $null)
    {
        $Script:OutputFile = $OutputFile
    }

    $requirementsMet = CheckAllRequirements -Location $Location

    if ($requirementsMet -eq $false)
    {
        return $false
    }

    if ($HostPool -ne $null)
    {
        if ($PSCmdlet.ShouldProcess("$Tenant/$HostPool"))
        {
            $Script:ShouldProcess = $true
        }
        else
        {
            $Script:ShouldProcess = $false
        }
    }
    else
    {
        if ($PSCmdlet.ShouldProcess("$Tenant"))
        {
            $Script:ShouldProcess = $true
        }
        else
        {
            $Script:ShouldProcess = $false
        }
    }

    if ($CopyUserAssignments -eq $false)
    {
        Write-ForkedOutput "You did not choose the -CopyUserAssignments switch."
        Write-ForkedOutput "This will migrate your host pools and application groups"
        Write-ForkedOutput "This will also assign your application groups to the specified workspace (or default tenant workspace)"
        Write-ForkedOutput "No users will be able to access WVD resources till you have assigned them to their application groups"
    }
    else
    {
        Write-ForkedOutput "You chose the -CopyUserAssignments switch."
        Write-ForkedOutput "This will migrate your host pools and application groups"
        Write-ForkedOutput "This will also assign your application groups to the specified workspace (or default tenant workspace)"
        Write-ForkedOutput "Individual user assignments will be copied from the Legacy to ARM application groups"
        Write-ForkedOutput "Each user assignment will use up a Role Assignment slot in your subscription. Role assignments"
        Write-ForkedOutput "are a limited resource in the subscription, and using a large number of them for this purpose"
        Write-ForkedOutput "will make your subscription less manageable."
        Write-ForkedOutput "See https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-list-portal#list-number-of-role-assignments"
        do
        {
            $response = Read-Host -Prompt "Do you wish to continue y/n?"
        }
        while (($response -ne "y") -and ($response -ne "n"))

        if ($response -eq "n")
        {
            Write-ForkedOutput "Aborting command"
            return $false
        }
    }

    $Script:AppGroupRefs = @()
    $Script:DefaultTags = @{}
    $Script:DefaultTags["WVD Migration"] = "true"
    $Script:RequiredUserAssignments = 0
    $Script:UserAssignments = @{}

    [array]$hostPools = FetchHostPools -Tenant $Tenant -HostPool $HostPool

    if ($hostPools.Count -eq 0)
    {
        Write-ForkedOutput "No HostPools found"
        return $false
    }

    $context = Get-AzContext
    $subscription = $context.Subscription.Id

    if ($Workspace -eq $null)
    {
        $Workspace = CreateTenantWorkspace `
            -Subscription $subscription `
            -Tenant $Tenant `
            -Location $Location
    }
    else
    {
        $exists = WorkspaceExists $Workspace
        if ($exists -eq $false)
        {
            Write-ForkedOutput "Workspace does not exist with resource ID $Workspace"
            Write-ForkedOutput "When using the -Workspace parameter, the value must be the full resource ID of the workspace you wish to use"
            Write-ForkedOutput "Aborting command"
            return $false
        }

        $isInCorrectLocation = WorkspaceIsInCorrectLocation -WorkspaceArmPath $Workspace -ExpectedLocation $Location
        if ($isInCorrectLocation -eq $false)
        {
            Write-ForkedOutput "Workspace with resource ID $Workspace is not in location $Location"
            Write-ForkedOutput "Aborting command"
            return $false
        }
    }

    Write-ForkedOutput "Creating conversion template"

    $template = CreateMigrationTemplate `
        -Tenant $Tenant `
        -Subscription $subscription `
        -HostPools $hostPools `
        -Location $Location `
        -Operation "Start" `
        -CopyUserAssignments $CopyUserAssignments

    Write-ForkedOutput "Conversion template created"

    if ($CopyUserAssignments -eq $true)
    {
        $userAssignmentsPossible = ValidateUserAssignments `
            -Subscription $subscription

        if ($userAssignmentsPossible -eq $false)
        {
            Write-ForkedOutput "Insufficient role assignment quota to copy user assignments"
            Write-ForkedOutput "Rerun command without the -CopyUserAssignments switch to migrate"
            Write-ForkedOutput "See https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-list-portal#list-number-of-role-assignments"
            return $false
        }
    }

    Write-ForkedOutput "Converting HostPools and associated ApplicationGroups to hybrid management mode"

    if ($Script:ShouldProcess)
    {
        $deployment = New-AzDeployment -TemplateObject $template -Location $Location
        Write-ForkedOutput "Template deployment result"
        $s = $deployment | ConvertTo-Json
        Write-ForkedOutput $s
    }

    Write-ForkedOutput "Conversion complete"

    UpdateWorkspaceAppGroupRefs -WorkspaceArmPath $Workspace

    if ($CopyUserAssignments -eq $true)
    {
        SetupUserAssignments
    }

    Write-ForkedOutput "Start-RdsHostPoolMigration complete"

    return $true
}

function Revert-RdsHostPoolMigration
{
    <#
    .synopsis
        Revert an inprogress migration of a legacy HostPool or HostPools to the arm model
    .example
        Revert-RdsHostPoolMigration -Tenant Contoso -HostPool Office -Location EastUS
        Revert-RdsHostPoolMigration -Tenant Contoso -Location WestEU
    .description
        This revokes an inprogress migration of the given HostPool, or all HostPools in a Tenant.
    #>

[cmdletbinding(SupportsShouldProcess=$true,PositionalBinding=$false)]
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Location,
    [Parameter(Mandatory=$false)]
    $HostPool,
    [Parameter(Mandatory=$false)]
    $OutputFile
)
    $ErrorActionPreference = "Stop"

    if ($OutputFile -ne $null)
    {
        $Script:OutputFile = $OutputFile
    }

    $requirementsMet = CheckAllRequirements -Location $Location

    if ($requirementsMet -eq $false)
    {
        return $false
    }

    if ($HostPool -ne $null)
    {
        if ($PSCmdlet.ShouldProcess("$Tenant/$HostPool"))
        {
            $Script:ShouldProcess = $true
        }
        else
        {
            $Script:ShouldProcess = $false
        }
    }
    else
    {
        if ($PSCmdlet.ShouldProcess("$Tenant"))
        {
            $Script:ShouldProcess = $true
        }
        else
        {
            $Script:ShouldProcess = $false
        }
    }

    [array]$hostPools = FetchHostPools -Tenant $Tenant -HostPool $HostPool

    $context = Get-AzContext
    $subscription = $context.Subscription.Id

    $template = CreateMigrationTemplate `
        -Tenant $Tenant `
        -Subscription $subscription `
        -HostPools $hostPools `
        -Operation "Revoke"

    Write-ForkedOutput "Reverting migration of HostPools and associated ApplicationGroups"

    if ($Script:ShouldProcess)
    {
        $deployment = New-AzDeployment -TemplateObject $template -Location $Location
        Write-ForkedOutput "Template deployment result"
        $s = $deployment | ConvertTo-Json
        Write-ForkedOutput $s

        DeleteRevertedObjects `
            -Tenant $Tenant `
            -Subscription $subscription `
            -HostPools $hostPools
    }

    Write-ForkedOutput "Revert-RdsHostPoolMigration complete"

    return $true
}

function Complete-RdsHostPoolMigration
{
    <#
    .synopsis
        Complete an inprogress migration of a legacy HostPool or HostPools to the arm model
    .example
        Complete-RdsHostPoolMigration -Tenant Contoso -HostPool Office -Location EastUS
        Complete-RdsHostPoolMigration -Tenant Contoso -Location WestEU
    .description
        This completes an inprogress migration of the given HostPool, or all HostPools in a Tenant.
    #>

[cmdletbinding(SupportsShouldProcess=$true,PositionalBinding=$false)]
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Location,
    [Parameter(Mandatory=$false)]
    $HostPool,
    [Parameter(Mandatory=$false)]
    $OutputFile
)
    $ErrorActionPreference = "Stop"

    if ($OutputFile -ne $null)
    {
        $Script:OutputFile = $OutputFile
    }

    $requirementsMet = CheckAllRequirements -Location $Location

    if ($requirementsMet -eq $false)
    {
        return $false
    }

    if ($HostPool -ne $null)
    {
        if ($PSCmdlet.ShouldProcess("$Tenant/$HostPool"))
        {
            $Script:ShouldProcess = $true
        }
        else
        {
            $Script:ShouldProcess = $false
        }
    }
    else
    {
        if ($PSCmdlet.ShouldProcess("$Tenant"))
        {
            $Script:ShouldProcess = $true
        }
        else
        {
            $Script:ShouldProcess = $false
        }
    }

    [array]$hostPools = FetchHostPools -Tenant $Tenant -HostPool $HostPool

    $context = Get-AzContext
    $subscription = $context.Subscription.Id

    $template = CreateMigrationTemplate `
        -Tenant $Tenant `
        -Subscription $subscription `
        -HostPools $hostPools `
        -Location $null `
        -Operation "Complete"

    Write-ForkedOutput "Completing migration of HostPools and associated ApplicationGroups"

    if ($Script:ShouldProcess)
    {
        $deployment = New-AzDeployment -TemplateObject $template -Location $Location
        Write-ForkedOutput "Template deployment result"
        $s = $deployment | ConvertTo-Json
        Write-ForkedOutput $s
    }

    Write-ForkedOutput "Complete-RdsHostPoolMigration complete"

    return $true
}

function Get-RdsHostPoolMigrationMapping
{
    <#
    .synopsis
        Outputs what the results of doing a hostpool migration will be. It will be output in CSV format, and
        thus readable via excel, or other spread sheet program.
    .example
        Get-RdsHostPoolMigrationMapping -Tenant Contoso -HostPool Office -Location EastUS -OutputFile mapping.csv
        Get-RdsHostPoolMigrationMapping -Tenant Contoso -Location WestUS -OutputFile mapping.csv
    .description
        Outputs what the results of doing a hostpool migration will be
    #>

[cmdletbinding(PositionalBinding=$false)]
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$false)]
    $HostPool,
    [Parameter(Mandatory=$true)]
    $OutputFile
)
    $ErrorActionPreference = "Stop"

    $requirementsMet = CheckAllRequirements

    if ($requirementsMet -eq $false)
    {
        return $false
    }

    [array]$hostPools = FetchHostPools -Tenant $Tenant -HostPool $HostPool

    $context = Get-AzContext
    $subscription = $context.Subscription.Id

    $header = "Tenant,Hostpool,ApplicationGroup,ResourceId"
    $header | Out-File -Encoding ascii $OutputFile
    foreach ($hostPool in $hostPools)
    {
        $resourceGroupName = CreateResourceGroupName $hostPool.TenantName $hostPool.HostPoolName
        $armPath = "/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}" -f $subscription,$resourceGroupName,$hostPool.HostPoolName
        $row = "{0},{1},,{2}" -f $hostPool.TenantName,$hostPool.HostPoolName,$armPath
        $row | Out-File -Append -Encoding ascii $OutputFile

        [array]$appgroups = Get-RdsAppGroup -TenantName $hostPool.TenantName -HostPoolName $hostPool.HostPoolName -ShowHidden
        foreach ($appgroup in $appgroups)
        {
            $armPath = "/subscriptions/{0}/resourcegroups/{1}/providers/Microsoft.DesktopVirtualization/hostpools/{2}" -f $subscription,$resourceGroupName,$appgroup.AppGroupName
            $row = "{0},{1},{2},{3}" -f $hostPool.TenantName,$hostPool.HostPoolName,$appgroup.AppGroupName,$armPath
            $row | Out-File -Append -Encoding ascii $OutputFile
        }
    }

    return $true
}

function Set-RdsHostPoolHidden
{
    <#
    .synopsis
        Hides or unhides a legacy HostPool that is being migrated
    .example
        Set-RdsHostPoolHidden -Tenant Contoso -HostPool Office -Hidden $true -Location EastUS
        Set-RdsHostPoolHidden -Tenant Contoso -Hidden $false -Location WestEU
    .description
        Hides or unhides a legacy HostPool that is being migrated
    #>

[cmdletbinding(SupportsShouldProcess=$true,PositionalBinding=$false)]
Param(
    [Parameter(Mandatory=$true)]
    $Tenant,
    [Parameter(Mandatory=$true)]
    $Location,
    [Parameter(Mandatory=$true)]
    $HostPool,
    [Parameter(Mandatory=$true)]
    $Hidden,
    [Parameter(Mandatory=$false)]
    $OutputFile
)
    $ErrorActionPreference = "Stop"

    if ($OutputFile -ne $null)
    {
        $Script:OutputFile = $OutputFile
    }

    $requirementsMet = CheckAllRequirements -Location $Location

    if ($requirementsMet -eq $false)
    {
        return $false
    }

    if ($PSCmdlet.ShouldProcess("$Tenant/$HostPool"))
    {
        $Script:ShouldProcess = $true
    }
    else
    {
        $Script:ShouldProcess = $false
    }

    [array]$hostPools = FetchHostPools -Tenant $Tenant -HostPool $HostPool

    $context = Get-AzContext
    $subscription = $context.Subscription.Id

    if ($Hidden -eq $true)
    {
        Write-ForkedOutput "Hiding HostPool and associated ApplicationGroups"
        $Operation = "Hide"
    }
    else
    {
        Write-ForkedOutput "Unhiding HostPool and associated ApplicationGroups"
        $Operation = "Unhide"
    }

    $template = CreateMigrationTemplate `
        -Tenant $Tenant `
        -Subscription $subscription `
        -HostPools $hostPools `
        -Location $Location `
        -Operation $Operation

    if ($Script:ShouldProcess)
    {
        $deployment = New-AzDeployment -TemplateObject $template -Location $Location
        Write-ForkedOutput "Template deployment result"
        $s = $deployment | ConvertTo-Json
        Write-ForkedOutput $s
    }

    Write-ForkedOutput "Set-RdsHostPoolHidden complete"

    return $true
}

function Set-RdsMigrationContext
{
    <#
    .synopsis
        Sets the RDS Context and Adal Context to be used for migration
    .example
        Set-RdsMigrationContext -RdsContext <rdscontext> -AdalContext <adalcontext>
    .description
        Sets the RDS Context and Adal Context to be used for migration
        Create the RDS Context by running the Add-RdsAccount cmdlet
        The RDS Context is stored in the global variable $rdMgmtContext
        The Adal Context is stored in the global variable $AdalContext
    #>

[cmdletbinding(SupportsShouldProcess=$true,PositionalBinding=$false)]
Param(
    [Parameter(Mandatory=$true)]
    $RdsContext,
    [Parameter(Mandatory=$true)]
    $AdalContext
)
    if ($PSCmdlet.ShouldProcess("Set RdsContext and AdalContext"))
    {
        $Global:rdMgmtContext = $RdsContext
        $Global:AdalContext = $AdalContext
    }

    return $true
}

# SIG # Begin signature block
# MIIjkgYJKoZIhvcNAQcCoIIjgzCCI38CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDsmAE6WMuS+dD9
# 67D/0074LPqQvTVBHZvpgcZhDhR1DqCCDYEwggX/MIID56ADAgECAhMzAAACUosz
# qviV8znbAAAAAAJSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMjU5WhcNMjIwOTAxMTgzMjU5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDQ5M+Ps/X7BNuv5B/0I6uoDwj0NJOo1KrVQqO7ggRXccklyTrWL4xMShjIou2I
# sbYnF67wXzVAq5Om4oe+LfzSDOzjcb6ms00gBo0OQaqwQ1BijyJ7NvDf80I1fW9O
# L76Kt0Wpc2zrGhzcHdb7upPrvxvSNNUvxK3sgw7YTt31410vpEp8yfBEl/hd8ZzA
# v47DCgJ5j1zm295s1RVZHNp6MoiQFVOECm4AwK2l28i+YER1JO4IplTH44uvzX9o
# RnJHaMvWzZEpozPy4jNO2DDqbcNs4zh7AWMhE1PWFVA+CHI/En5nASvCvLmuR/t8
# q4bc8XR8QIZJQSp+2U6m2ldNAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUNZJaEUGL2Guwt7ZOAu4efEYXedEw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDY3NTk3MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFkk3
# uSxkTEBh1NtAl7BivIEsAWdgX1qZ+EdZMYbQKasY6IhSLXRMxF1B3OKdR9K/kccp
# kvNcGl8D7YyYS4mhCUMBR+VLrg3f8PUj38A9V5aiY2/Jok7WZFOAmjPRNNGnyeg7
# l0lTiThFqE+2aOs6+heegqAdelGgNJKRHLWRuhGKuLIw5lkgx9Ky+QvZrn/Ddi8u
# TIgWKp+MGG8xY6PBvvjgt9jQShlnPrZ3UY8Bvwy6rynhXBaV0V0TTL0gEx7eh/K1
# o8Miaru6s/7FyqOLeUS4vTHh9TgBL5DtxCYurXbSBVtL1Fj44+Od/6cmC9mmvrti
# yG709Y3Rd3YdJj2f3GJq7Y7KdWq0QYhatKhBeg4fxjhg0yut2g6aM1mxjNPrE48z
# 6HWCNGu9gMK5ZudldRw4a45Z06Aoktof0CqOyTErvq0YjoE4Xpa0+87T/PVUXNqf
# 7Y+qSU7+9LtLQuMYR4w3cSPjuNusvLf9gBnch5RqM7kaDtYWDgLyB42EfsxeMqwK
# WwA+TVi0HrWRqfSx2olbE56hJcEkMjOSKz3sRuupFCX3UroyYf52L+2iVTrda8XW
# esPG62Mnn3T8AuLfzeJFuAbfOSERx7IFZO92UPoXE1uEjL5skl1yTZB3MubgOA4F
# 8KoRNhviFAEST+nG8c8uIsbZeb08SeYQMqjVEmkwggd6MIIFYqADAgECAgphDpDS
# 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
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAlKLM6r4lfM52wAAAAACUjAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgcrH1UPZO
# /h+1OLSrZszW6htiQncO2RF1VuEf6fwmD60wQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQA/dhOcT58qzAftYNxHv24ESqPHO8RmsXpwPoPF7sff
# 0MfWTGAZFrmW/Bag9BPEc4NAyVY0n/QxLNrdL/EFDw3Uvqat1Sqb+lps43k/+4To
# ch7VeWv/SkqP7xs47MN0jYO6fyI7cO6aCIcT/0hqD4aTL6+iyCBm1ECaX4Bo+Lr6
# oKKme7RHtUajWmn0IC20lS1cchXL/unKQP4jFn9Hi7MnwaxZ/3cX2TAubSZI6WCa
# gAOJpg/3Zoh6FpFQTXJHxiTfd+dC/MS3z0yz8IuHvB66aShbcEHcpYxk6jMg4eGf
# XnssCceH9v8MfMxH8HzQd3vtDLgTo6ltKY/nQVumwIe1oYIS8TCCEu0GCisGAQQB
# gjcDAwExghLdMIIS2QYJKoZIhvcNAQcCoIISyjCCEsYCAQMxDzANBglghkgBZQME
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIGXORBLc03RPiccYZv5sj6PpBrqOJa9kc3H0Riwz
# oczVAgZh8DL5B+cYEzIwMjIwMTI2MTIyNTEwLjQ0MVowBIACAfSggdSkgdEwgc4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjpGN0E2LUUyNTEtMTUwQTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCDkQwggT1MIID3aADAgECAhMzAAABWZ/8fl8s6vJDAAAA
# AAFZMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIxMDExNDE5MDIxNVoXDTIyMDQxMTE5MDIxNVowgc4xCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpGN0E2
# LUUyNTEtMTUwQTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK54xGHJZ8SHREtNIoBo
# 9AG6Mro8gEZCt8WgV/mNdIt2tMOP3zVYU4+sRsImxTwfzJEDBWaTc7LxlEy/1302
# fRmd/R2pwnY7pyT90yvZAmQQLZ6D+faGBwwhi5rre/tmBJdbAXFZ8qL2JDc4txBn
# 30Mr1C8DFBdrIjwbP+i2RdAOaSwIs/xQsMeZAz3v5j9VEdwq8+iM6YcLcqKrYAwP
# +OE58371ST5kj2f7quToeTXhSvDczKYrVokL3Zn0+KNAnbpp4rH1tXymmgXQcgVC
# z1E/Ey8NEsvZ1FjV5QP6ovDMT8YAo7KzaYvT4Ix+xMVvW+1/1MnYaaoR8bLnQxmT
# ZOMCAwEAAaOCARswggEXMB0GA1UdDgQWBBT20KmFRryt+uTrJ9eIwjyy6Tdj5zAf
# BgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBH
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNU
# aW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0
# YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsG
# AQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQCNkVQS6A+BhrfGOCAWo3KcuUa4estp
# zyn+ZLlkh0pJmAJp4EUDrLWsieYCf2oyoc8KjVMC+NHFFVvHLrSMhWnR5FtY6l3Z
# 6Ur9ITBSz64j5wTRRE8vIpQiHVYjRVNPGR2tiqG5nKP5+sD0rZI464OFNz4n7erD
# JOpV7Im1L/sAwfX+GHoc4j5rfuAuQTFY82sdYvtHM4LTxwV997uhlFs52oHapdFW
# 1KXt6vMxEXnSX8soQfUd+M+Yq3J7udc6R941Guxfd6A0vecV56JjvmpCng4jRkqu
# Aeyf/dKmQUaR1fKvALBRAmZkAUtWijS/3MkeQv/lUvHVo7GPFzJ/O3wJMIIGcTCC
# 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
# N0E2LUUyNTEtMTUwQTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAKnbLAI8fhO58SCWrpZnXvXEZshGggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOWbWjcwIhgPMjAyMjAxMjYwOTI3MTlaGA8yMDIyMDEyNzA5MjcxOVowdzA9Bgor
# BgEEAYRZCgQBMS8wLTAKAgUA5ZtaNwIBADAKAgEAAgIlAAIB/zAHAgEAAgIRVzAK
# AgUA5ZyrtwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAI/SVXu9pLSCeJeG
# OMUPonE5PMT8DsoC48jmRDc2qdu+Tb18hnj4G91T1O18rJ2WOXsQHCvEIkIBjOK5
# 6cm9BOYYQ1BbO145VdvnKfa4UYpUXXAskmjcEagUmJyh/ucuR6Sv2VpTPU/6RA38
# SYYgWYTsZYeOLz9x49XJBpvV2H0rMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAFZn/x+Xyzq8kMAAAAAAVkwDQYJYIZIAWUD
# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQgjb8YG9qAu3MyeQXEnBOX6aSOsR4+njXnyrN83MgiKrcwgfoGCyqGSIb3
# DQEJEAIvMYHqMIHnMIHkMIG9BCABWBvPvzDmfNeSzmJT4+dGA+uj/qq7/fKkUn36
# rxND6DCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB
# WZ/8fl8s6vJDAAAAAAFZMCIEIL3K6pSTnkWvgYT9XAlfgyfAb31Et3khgh7aieDP
# hTaZMA0GCSqGSIb3DQEBCwUABIIBACOcN7iaqb0YZpp0uNkGKxhYhqjj5ebliXyg
# Et9cO4qLrJ+uodDgvx1jK5YkTHuG7GYp3oq1wJ8t/hXZwqW3Xk4TzNMkLjjzmF4a
# NtqCHg0aCVEyIMikMQzTq1566wk7KyXBswUnoEooxjzybpWNvdAp9dlsSXibIwvH
# ZeUulR13IGpBRR5XHnLIoe5rJpbyp3Uy/Ckf6OrmzqGaXclJMTI/DvNiC8UUp6Fi
# XP8z83UTw1k9Hc/snDVAf44LyyLBPaTJqlDfYIhA3d3S1XbWk6oEzDpdn9W/sDcY
# POJHf18kEoXZugyBEYNai0fRXpXI/ezc2FazFfSIrx1+vauzvLE=
# SIG # End signature block