Microsoft.AVS.Management.psm1

<# Private Function Import #>
. $PSScriptRoot\AVSGenericUtils.ps1

<#
    .SYNOPSIS
    Manages the Tools Repository on vSAN datastores for VMware Tools deployment.

    .DESCRIPTION
    This function creates a GuestStore folder on each cluster's vSAN datastore and configures
    hosts to pull VMware Tools from their respective vSAN datastore. The 'gueststore-vmtools'
    file is required.

    When -Validate is specified, only reads and validates metadata.json files without making changes.
    When -Validate is NOT specified, uploads tools and configures hosts as normal.

    .PARAMETER ToolsURL
    A publicly available HTTP(S) URL to download the Tools zip file. Required when -Validate
    is NOT specified. Must be HTTPS or HTTP.

    .PARAMETER Validate
    Switch to enable validation-only mode. When set, the function reads metadata.json files
    to verify they are in sync, but makes no changes to the datastore or host configuration.

    .EXAMPLE
    # Upload tools to repositories
    Set-ToolsRepo -ToolsURL "https://example.com/tools.zip"

    .EXAMPLE
    # Validate existing repositories (no upload)
    Set-ToolsRepo -Validate
#>

function Set-ToolsRepo {
    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $false,
            HelpMessage = 'A publicly available HTTP(S) URL to download the Tools zip file.')]
        [ValidateNotNullOrEmpty()]
        [SecureString]
        $ToolsURL,

        [Parameter(Mandatory = $false)]
        [switch]$Validate
    )

    # Initialize variables
    $new_folder = 'GuestStore'
    $archive_path = '/vmware/apps/vmtools/windows64/'
    $normalizedArchivePath = if ($null -ne $archive_path) { $archive_path.Trim('/','\') } else { '' }
    $successfulDatastores = @()
    $failedDatastores = @()

    # Main execution wrapped in try-catch-finally
    try {
        Write-Verbose "Starting Set-ToolsRepo"

        # Check mutual exclusion: -Validate or -ToolsURL required
        if (-not $Validate -and $null -eq $ToolsURL) {
            throw "ToolsURL is required when -Validate is not specified."
        }

        if ($Validate) {
            Write-Information "Running in validation-only mode. No upload or configuration changes will be made." -InformationAction Continue

            $GetMetadataVersion = {
                param(
                    [Parameter(Mandatory = $true)]
                    $MetadataObject,

                    [Parameter(Mandatory = $true)]
                    [string]$LatestVersion
                )

                if ($null -eq $MetadataObject) {
                    return $null
                }

                $versionPattern = '(?i)vmtools-(\d+(?:\.\d+){1,3})'
                $installerFilePattern = '(?i)vmware-tools-(\d+(?:\.\d+){1,3})'
                $plainVersionPattern = '^\d+(?:\.\d+){1,3}$'
                $candidateVersions = @()

                if ($MetadataObject.PSObject.Properties.Name -contains 'installer' -and $null -ne $MetadataObject.installer) {
                    $installerObj = $MetadataObject.installer

                    if ($installerObj.PSObject.Properties.Name -contains 'version') {
                        $installerVersion = [string]$installerObj.version
                        if ($installerVersion -match $plainVersionPattern) {
                            $candidateVersions += $installerVersion
                        }
                        if ($installerVersion -match $versionPattern) {
                            $candidateVersions += $Matches[1]
                        }
                    }

                    if ($installerObj.PSObject.Properties.Name -contains 'file') {
                        $installerFile = [string]$installerObj.file
                        if ($installerFile -match $installerFilePattern) {
                            $candidateVersions += $Matches[1]
                        }
                    }
                }

                if ($MetadataObject.PSObject.Properties.Name -contains 'vmtools') {
                    $vmtoolsField = [string]$MetadataObject.vmtools
                    if ($vmtoolsField -match $versionPattern) {
                        $candidateVersions += $Matches[1]
                    }
                }

                foreach ($prop in $MetadataObject.PSObject.Properties) {
                    $nameCandidate = [string]$prop.Name
                    if ($nameCandidate -match $versionPattern) {
                        $candidateVersions += $Matches[1]
                    }

                    $valueCandidate = [string]$prop.Value
                    if ($valueCandidate -match $versionPattern) {
                        $candidateVersions += $Matches[1]
                    }
                }

                if ($candidateVersions.Count -gt 0) {
                    $uniqueCandidates = $candidateVersions | Select-Object -Unique
                    if ($uniqueCandidates -contains $LatestVersion) {
                        return $LatestVersion
                    }

                    $sortedCandidates = $uniqueCandidates |
                        Sort-Object {
                            try {
                                [version]$_
                            } catch {
                                [version]'0.0'
                            }
                        } -Descending

                    return [string]$sortedCandidates[0]
                }

                # Fallback: some metadata formats store only the tools version in a plain 'version' field.
                if ($MetadataObject.PSObject.Properties.Name -contains 'version') {
                    $versionField = [string]$MetadataObject.version
                    if ($versionField -match $versionPattern) {
                        return $Matches[1]
                    }
                    if ($versionField -match $plainVersionPattern -and $versionField -eq $LatestVersion) {
                        return $versionField
                    }
                }

                return $null
            }

            # Get vSAN datastores with error handling
            try {
                $datastores = @(Get-Datastore -ErrorAction Stop | Where-Object { $_.extensionData.Summary.Type -eq 'vsan' })

                if ($null -eq $datastores -or $datastores.Count -eq 0) {
                    throw "No vSAN datastores found in the environment"
                }

                Write-Information "Found $($datastores.Count) vSAN datastore(s)" -InformationAction Continue
            } catch {
                throw "Failed to retrieve vSAN datastores: $_"
            }

            foreach ($datastore in $datastores) {
                $ds_name = $datastore.Name
                $localMetadataTempDir = $null
                Write-Information "Validating datastore: $ds_name" -InformationAction Continue

                try {
                    if (Get-PSDrive -Name DS -ErrorAction SilentlyContinue) {
                        Remove-PSDrive -Name DS -Force -ErrorAction SilentlyContinue
                    }

                    try {
                        New-PSDrive -Location $datastore -Name DS -PSProvider VimDatastore -Root '\' -ErrorAction Stop | Out-Null
                    } catch {
                        throw "Failed to create PSDrive for datastore $ds_name : $_"
                    }

                    $baseDestPath = "DS:/$new_folder"
                    $destPath = if ([string]::IsNullOrEmpty($normalizedArchivePath)) {
                        $baseDestPath
                    } else {
                        Join-Path -Path $baseDestPath -ChildPath $normalizedArchivePath
                    }

                    if (-not (Test-Path -Path $destPath)) {
                        throw "GuestStore tools path not found on $ds_name : $destPath"
                    }

                    $existing_dirs = Get-ChildItem -Path $destPath -ErrorAction Stop |
                        Where-Object {
                            $_.PSIsContainer -and
                            $_.Name -match '^vmtools-\d'
                        }

                    if ($null -eq $existing_dirs -or $existing_dirs.Count -eq 0) {
                        throw "No vmtools-* version folders found on $ds_name under $destPath"
                    }

                    $highestVersionFolder = $null
                    $highestVersion = $null

                    foreach ($existing_dir in $existing_dirs) {
                        $ver = $existing_dir.Name -replace 'vmtools-', ''
                        try {
                            $parsedVersion = [version]$ver
                        } catch {
                            continue
                        }

                        if ($null -eq $highestVersion -or $parsedVersion -gt $highestVersion) {
                            $highestVersion = $parsedVersion
                            $highestVersionFolder = $existing_dir
                        }
                    }

                    if ($null -eq $highestVersionFolder) {
                        throw "No valid vmtools version folders could be parsed on $ds_name"
                    }

                    $latestDetectedVersionFolder = $highestVersionFolder.Name
                    $latestDetectedVersion = $latestDetectedVersionFolder -replace 'vmtools-', ''
                    Write-Host "Datastore $ds_name latest detected tools version: $latestDetectedVersionFolder"

                    $topLevelMetadataPath = Join-Path $destPath 'metadata.json'
                    $versionMetadataPath = Join-Path (Join-Path $destPath $latestDetectedVersionFolder) 'metadata.json'

                    if (-not (Test-Path -Path $topLevelMetadataPath)) {
                        throw "Top-level metadata.json not found on $ds_name at $topLevelMetadataPath"
                    }

                    if (-not (Test-Path -Path $versionMetadataPath)) {
                        throw "Version metadata.json not found on $ds_name at $versionMetadataPath"
                    }

                    # Copy metadata files locally because VimDatastore does not support Get-Content.
                    $localMetadataTempDir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ("avs-validate-metadata-{0}-{1}" -f (Get-Date -Format 'yyyyMMddHHmmssfff'), [guid]::NewGuid().ToString('N'))
                    New-Item -Path $localMetadataTempDir -ItemType Directory -ErrorAction Stop | Out-Null

                    $localTopLevelMetadataPath = Join-Path -Path $localMetadataTempDir -ChildPath 'top-level-metadata.json'
                    $localVersionMetadataPath = Join-Path -Path $localMetadataTempDir -ChildPath 'version-metadata.json'

                    try {
                        Copy-DatastoreItem -Item $topLevelMetadataPath -Destination $localTopLevelMetadataPath -Force -ErrorAction Stop
                    } catch {
                        throw "Failed to copy top-level metadata.json from $ds_name : $($_.Exception.Message)"
                    }

                    try {
                        Copy-DatastoreItem -Item $versionMetadataPath -Destination $localVersionMetadataPath -Force -ErrorAction Stop
                    } catch {
                        throw "Failed to copy version metadata.json from $ds_name : $($_.Exception.Message)"
                    }

                    try {
                        $topLevelMetadataObj = Get-Content -Path $localTopLevelMetadataPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
                        $versionMetadataObj = Get-Content -Path $localVersionMetadataPath -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
                    } catch {
                        throw "Failed to parse metadata.json content on $ds_name : $($_.Exception.Message)"
                    }

                    $topLevelMetadataVersion = & $GetMetadataVersion -MetadataObject $topLevelMetadataObj -LatestVersion $latestDetectedVersion
                    $versionFolderMetadataVersion = & $GetMetadataVersion -MetadataObject $versionMetadataObj -LatestVersion $latestDetectedVersion

                    Write-Host "Datastore $ds_name top-level metadata version: $topLevelMetadataVersion"
                    Write-Host "Datastore $ds_name version-folder metadata version: $versionFolderMetadataVersion"

                    $topLevelInSync = (-not [string]::IsNullOrEmpty($topLevelMetadataVersion)) -and ($topLevelMetadataVersion -eq $latestDetectedVersion)
                    $versionFolderInSync = (-not [string]::IsNullOrEmpty($versionFolderMetadataVersion)) -and ($versionFolderMetadataVersion -eq $latestDetectedVersion)

                    if ($topLevelInSync -and $versionFolderInSync) {
                        Write-Host "Datastore $ds_name validation result: SUCCESS - metadata is in sync."
                        $successfulDatastores += $ds_name
                    } else {
                        Write-Host "Datastore $ds_name validation result: FAILURE - metadata is not in sync."
                        if ([string]::IsNullOrEmpty($topLevelMetadataVersion)) {
                            Write-Warning "Unable to determine version from top-level metadata.json on $ds_name"
                        } elseif (-not $topLevelInSync) {
                            Write-Warning "top-level metadata.json version ($topLevelMetadataVersion) does not match latest detected version ($latestDetectedVersionFolder) on $ds_name"
                        }
                        if ([string]::IsNullOrEmpty($versionFolderMetadataVersion)) {
                            Write-Warning "Unable to determine version from version-folder metadata.json on $ds_name"
                        } elseif (-not $versionFolderInSync) {
                            Write-Warning "version-folder metadata.json version ($versionFolderMetadataVersion) does not match latest detected version ($latestDetectedVersionFolder) on $ds_name"
                        }
                        $failedDatastores += $ds_name
                    }
                } catch {
                    Write-Error "Validation failed for datastore $ds_name : $_"
                    $failedDatastores += $ds_name
                } finally {
                    if (-not [string]::IsNullOrEmpty($localMetadataTempDir) -and (Test-Path -Path $localMetadataTempDir)) {
                        Remove-Item -Path $localMetadataTempDir -Recurse -Force -ErrorAction SilentlyContinue
                    }
                    if (Get-PSDrive -Name DS -ErrorAction SilentlyContinue) {
                        Remove-PSDrive -Name DS -Force -ErrorAction SilentlyContinue
                    }
                }
            }

            Write-Information "`n=== Validation Summary ===" -InformationAction Continue
            if ($successfulDatastores.Count -gt 0) {
                Write-Information "List of Datastores with metadata in sync: $($successfulDatastores -join ', ')" -InformationAction Continue
            }
            if ($failedDatastores.Count -gt 0) {
                Write-Warning "List of Datastores with metadata out of sync or validation failure: $($failedDatastores -join ', ')"
            }

            if ($failedDatastores.Count -gt 0) {
                if ($failedDatastores.Count -eq @($datastores).Count) {
                    throw "Validation failed for all datastores."
                }

                throw "Validation failed for some datastores. Review failed datastore list above."
            }

            return
        }

        $failedDatastoreReasons = @{}

        # Convert SecureString to plain text for use with web requests
        $ToolsURLPlain = [System.Net.NetworkCredential]::new('', $ToolsURL).Password

        # Validate URL pattern (must be HTTP or HTTPS)
        if ($ToolsURLPlain -notmatch '^https?://') {
            throw "ToolsURL must be a valid HTTP or HTTPS URL."
        }

        # Validate URL accessibility
        try {
            $webResponse = Invoke-WebRequest -Uri $ToolsURLPlain -Method Head -TimeoutSec 30 -ErrorAction Stop
            if ($webResponse.StatusCode -ne 200) {
                throw "URL returned status code: $($webResponse.StatusCode)"
            }
        } catch {
            throw "Unable to access the provided URL: $_"
        }

        # Use current working directory (managed by agent)
        $tools_file = "./tools.zip"

        # Download the tools file
        try {
            Write-Information "Downloading tools..." -InformationAction Continue
            Invoke-WebRequest -Uri $ToolsURLPlain -OutFile $tools_file -ErrorAction Stop

            # Validate downloaded file
            if (-not (Test-Path -Path $tools_file)) {
                throw "Downloaded file not found at expected location"
            }

            $fileSize = (Get-Item $tools_file).Length
            if ($fileSize -eq 0) {
                throw "Downloaded file is empty"
            }

            Write-Verbose "Downloaded file size: $($fileSize / 1MB) MB"
        } catch {
            throw "Failed to download tools file: $_"
        }

        # Extract the archive
        try {
            Write-Information "Extracting tools archive..." -InformationAction Continue
            Expand-Archive -Path $tools_file -DestinationPath "." -Force -ErrorAction Stop
        } catch {
            throw "Failed to extract tools archive: $_"
        }

        # Locate windows64 directory in extracted archive
        $windows64_path = Join-Path -Path "." -ChildPath $normalizedArchivePath

        if (-not (Test-Path -Path $windows64_path)) {
            throw "windows64 directory not found in extracted archive at: $windows64_path"
        }

        Write-Information "windows64 directory located - will validate metadata.json files next" -InformationAction Continue

        # Build the path to windows64/metadata.json
        $windows64_top_metadata_path = Join-Path -Path $windows64_path -ChildPath "metadata.json"

        # Check if metadata.json exists in windows64
        if (-not (Test-Path -Path $windows64_top_metadata_path)) {
            throw "metadata.json not found in windows64 directory at: $windows64_top_metadata_path"
        }

        Write-Information "metadata.json found in windows64 directory: $windows64_top_metadata_path" -InformationAction Continue

        # Find the vmtools-xxx folder inside windows64
        $vmtools_folders = Get-ChildItem -Path $windows64_path -Directory | Where-Object { $_.Name -like "vmtools-*" }

        if ($null -eq $vmtools_folders -or $vmtools_folders.Count -eq 0) {
            throw "No vmtools folder found inside windows64 at: $windows64_path"
        }

        $vmtools_folder_path = $vmtools_folders[0].FullName

        Write-Information "Found vmtools folder: $($vmtools_folders[0].Name) at $vmtools_folder_path" -InformationAction Continue

        # Check if metadata.json exists inside the vmtools-xxx folder
        $vmtools_version_metadata_path = Join-Path -Path $vmtools_folder_path -ChildPath "metadata.json"

        if (-not (Test-Path -Path $vmtools_version_metadata_path)) {
            throw "metadata.json not found inside vmtools folder at: $vmtools_version_metadata_path"
        }

        Write-Information "metadata.json found inside vmtools folder: $vmtools_version_metadata_path" -InformationAction Continue

        # Validation success gate: both required metadata files were found
        Write-Information "Validation gate passed: windows64 metadata at $windows64_top_metadata_path and vmtools metadata at $vmtools_version_metadata_path. Proceeding to datastore operations." -InformationAction Continue

        # Use the already validated vmtools folder from Step 3
        $tools_version = Split-Path -Path $vmtools_folder_path -Leaf

        if ([string]::IsNullOrEmpty($tools_version) -or $tools_version -notlike 'vmtools-*') {
            throw "Invalid vmtools folder name detected at: $vmtools_folder_path"
        }

        $tools_short_version = $tools_version -replace 'vmtools-', ''
        Write-Information "Found tools version: $tools_version" -InformationAction Continue

        # Get vSAN datastores with error handling
        try {
            $datastores = @(Get-Datastore -ErrorAction Stop | Where-Object { $_.extensionData.Summary.Type -eq 'vsan' })

            if ($null -eq $datastores -or $datastores.Count -eq 0) {
                throw "No vSAN datastores found in the environment"
            }

            Write-Information "Found $($datastores.Count) vSAN datastore(s)" -InformationAction Continue
        } catch {
            throw "Failed to retrieve vSAN datastores: $_"
        }

        # Process each datastore
        foreach ($datastore in $datastores) {
            $ds_name = $datastore.Name
            Write-Information "Processing datastore: $ds_name" -InformationAction Continue

            try {
                # Ensure any existing PSDrive is removed
                if (Get-PSDrive -Name DS -ErrorAction SilentlyContinue) {
                    Remove-PSDrive -Name DS -Force -ErrorAction SilentlyContinue
                }

                # Create PS drive with error handling
                try {
                    New-PSDrive -Location $datastore -Name DS -PSProvider VimDatastore -Root '\' -ErrorAction Stop | Out-Null
                } catch {
                    throw "Failed to create PSDrive for datastore $ds_name : $_"
                }

                # Check if repo folder exists
                try {
                    $Dsbrowser = Get-View -Id $Datastore.Extensiondata.Browser -ErrorAction Stop
                    $spec = New-Object VMware.Vim.HostDatastoreBrowserSearchSpec
                    $spec.Query += New-Object VMware.Vim.FolderFileQuery
                    $datastoreRoot = "[{0}]" -f $ds_name
                    $searchResult = $dsBrowser.SearchDatastore($datastoreRoot, $spec)
                    $folderObj = $searchResult.File | Where-Object { $_.FriendlyName -eq $new_folder }
                } catch {
                    throw "Failed to browse datastore $ds_name : $_"
                }

                # Create folder if it doesn't exist
                if ($null -eq $folderObj) {
                    try {
                        New-Item -ItemType Directory -Path "DS:/$new_folder" -ErrorAction Stop | Out-Null
                        Write-Information "Created $new_folder directory on $ds_name" -InformationAction Continue
                    } catch {
                        throw "Failed to create $new_folder directory on $ds_name : $_"
                    }

                    # Verify folder creation
                    $searchResult = $dsBrowser.SearchDatastore($datastoreRoot, $spec)
                    $folderObj = $searchResult.File | Where-Object { $_.FriendlyName -eq $new_folder }

                    if ($null -eq $folderObj) {
                        throw "Folder verification failed after creation on $ds_name"
                    }
                }

                # Check existing tools versions to determine highest version
                $baseDestPath = "DS:/$new_folder"
                $destPath = if ([string]::IsNullOrEmpty($normalizedArchivePath)) {
                    $baseDestPath
                } else {
                    Join-Path -Path $baseDestPath -ChildPath $normalizedArchivePath
                }
                $highestExistingVersion = $null
                $shouldUpdateTopLevelMetadata = $false

                if (Test-Path -Path $destPath) {
                    try {
                        $existing_dirs = Get-ChildItem -Path $destPath -ErrorAction Stop |
                            Where-Object {
                                $_.PSIsContainer -and
                                $_.Name -match '^vmtools-\d'
                            }

                        foreach ($existing_dir in $existing_dirs) {
                            $ver = $existing_dir.Name -replace 'vmtools-', ''
                            if ($null -eq $highestExistingVersion -or [version]$ver -gt [version]$highestExistingVersion) {
                                $highestExistingVersion = $ver
                            }
                        }

                        if ($highestExistingVersion) {
                            Write-Information "Current highest version on $ds_name is $highestExistingVersion" -InformationAction Continue
                        }
                    } catch {
                        Write-Warning "Failed to check existing versions on $ds_name : $_"
                    }
                }

                # Determine if we should update the top-level metadata.json
                # Only update if new version is greater than the highest existing version
                if ($null -eq $highestExistingVersion -or [version]$tools_short_version -gt [version]$highestExistingVersion) {
                    $shouldUpdateTopLevelMetadata = $true
                    Write-Information "New version ($tools_short_version) is greater than existing ($highestExistingVersion). Top-level metadata.json will be updated." -InformationAction Continue
                } else {
                    Write-Information "New version ($tools_short_version) is not greater than existing ($highestExistingVersion). Top-level metadata.json will be preserved." -InformationAction Continue
                }

                # Always copy the new version (older versions are allowed)
                try {
                    Write-Information "Copying $tools_version to $ds_name..." -InformationAction Continue

                    # Use the discovered vmtools directory from the extracted archive as the source
                    $sourceDir = $vmtools_folder_path

                    # Ensure destination folder exists on the datastore
                    if (-not (Test-Path -Path $destPath)) {
                        New-Item -ItemType Directory -Path $destPath -Force -ErrorAction Stop | Out-Null
                    }

                    # Check if this version already exists on the datastore
                    $versionDestPath = Join-Path $destPath $tools_version
                    if (Test-Path -Path $versionDestPath) {
                        Write-Information "Version $tools_version already exists on $ds_name. Skipping copy." -InformationAction Continue
                    } else {
                        # Copy the vmtools-{version} folder itself (preserves folder structure)
                        Copy-DatastoreItem -Item $sourceDir -Destination $destPath -Recurse -Force -ErrorAction Stop

                        # Verify metadata.json exists in the copied version folder
                        $versionMeta = Get-ChildItem -Path $versionDestPath -Filter metadata.json -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
                        if (-not $versionMeta) { throw "metadata.json not found in copied version folder on $ds_name" }

                        Write-Information "Successfully copied $tools_version to $ds_name" -InformationAction Continue
                    }

                    # Ensure top-level GuestStore artifacts (for example, gueststore-vmtools) are present.
                    # Keep metadata.json handling separate below so preserve/update rules stay unchanged.
                    $topLevelSourceDir = Split-Path -Path $sourceDir -Parent
                    if (-not [string]::IsNullOrEmpty($topLevelSourceDir) -and (Test-Path -Path $topLevelSourceDir)) {
                        $topLevelFiles = Get-ChildItem -Path $topLevelSourceDir -File -ErrorAction SilentlyContinue |
                            Where-Object { $_.Name -ne 'metadata.json' }
                        foreach ($file in $topLevelFiles) {
                            $destFilePath = Join-Path -Path $destPath -ChildPath $file.Name
                            Copy-DatastoreItem -Item $file.FullName -Destination $destFilePath -Force -ErrorAction Stop
                        }
                        if ($topLevelFiles) {
                            Write-Information "Ensured top-level GuestStore artifacts are present on $ds_name" -InformationAction Continue
                        }
                    }

                    # Update top-level metadata.json only if new version is greater
                    if ($shouldUpdateTopLevelMetadata) {
                        $topLevelMetadataPath = Join-Path $destPath "metadata.json"
                        Copy-DatastoreItem -Item $windows64_top_metadata_path -Destination $topLevelMetadataPath -Force -ErrorAction Stop
                        Write-Information "Updated top-level metadata.json on $ds_name to version $tools_short_version" -InformationAction Continue
                    } else {
                        Write-Information "Top-level metadata.json on $ds_name preserved (not overwritten)" -InformationAction Continue
                    }
                } catch {
                    throw "Failed to copy tools to $ds_name : $_"
                }

                # Configure hosts
                $url = ($datastore.ExtensionData.Summary.Url) + "$new_folder"

                # Get hosts with proper error handling
                try {
                    $ds_id = $datastore.Id
                    $vmhosts = Get-VMHost -ErrorAction Stop | Where-Object {
                        $_.ExtensionData.Datastore.value -contains ($ds_id.Split('-', 2)[1])
                    }

                    if ($null -eq $vmhosts -or $vmhosts.Count -eq 0) {
                        throw "No hosts found for datastore $ds_name"
                    }

                    Write-Information "Configuring $($vmhosts.Count) host(s) for datastore $ds_name" -InformationAction Continue
                } catch {
                    throw "Failed to retrieve hosts for datastore $ds_name : $_"
                }

                # Configure each host
                $failedHosts = @()
                foreach ($vmhost in $vmhosts) {
                    try {
                        $esxcli = Get-EsxCli -V2 -VMHost $vmhost -ErrorAction Stop
                        Write-Verbose "Setting GuestStore repository for host: $vmhost"

                        $arguments = $esxcli.system.settings.gueststore.repository.set.CreateArgs()
                        $arguments.url = $url
                        $result = $esxcli.system.settings.gueststore.repository.set.invoke($arguments)

                        if ($result -eq $false) {
                            throw "ESXCLI command returned false"
                        }

                        Write-Information "Successfully configured host: $vmhost" -InformationAction Continue
                    } catch {
                        Write-Warning "Failed to configure host $vmhost : $_"
                        $failedHosts += $vmhost.Name
                    }
                }

                if ($failedHosts.Count -gt 0) {
                    throw "Failed to configure hosts for datastore $ds_name : $($failedHosts -join ', ')"
                }

                $successfulDatastores += $ds_name
            } catch {
                $failureMessage = $_.Exception.Message
                if ([string]::IsNullOrWhiteSpace($failureMessage)) {
                    $failureMessage = [string]$_
                }

                Write-Warning "Error processing datastore $ds_name : $failureMessage"
                $failedDatastores += $ds_name
                $failedDatastoreReasons[$ds_name] = $failureMessage
            } finally {
                # Always clean up PSDrive
                if (Get-PSDrive -Name DS -ErrorAction SilentlyContinue) {
                    Remove-PSDrive -Name DS -Force -ErrorAction SilentlyContinue
                }
            }
        }

        # Summary report
        Write-Information "`n=== Summary ===" -InformationAction Continue
        if ($successfulDatastores.Count -gt 0) {
            Write-Information "List of Successfully processed datastores: $($successfulDatastores -join ', ')" -InformationAction Continue
        }
        if ($failedDatastores.Count -gt 0) {
            Write-Warning "List of Failed datastores: $($failedDatastores -join ', ')"

            foreach ($failedDs in $failedDatastores) {
                $reason = $failedDatastoreReasons[$failedDs]
                if ([string]::IsNullOrWhiteSpace($reason)) {
                    $reason = "No detailed failure reason captured."
                }

                Write-Warning "Failure reason for datastore $failedDs : $reason"
            }
        }

        if ($failedDatastores.Count -gt 0) {
            if ($failedDatastores.Count -eq @($datastores).Count) {
                throw "All datastores failed to process."
            }

            throw "Some datastores failed to process. Review successful datastore list, failed datastore list, and failure reasons above."
        }
    } catch {
        Write-Error "Set-ToolsRepo failed: $_"
        throw
    } finally {
        # Ensure PSDrive is removed
        if (Get-PSDrive -Name DS -ErrorAction SilentlyContinue) {
            Remove-PSDrive -Name DS -Force -ErrorAction SilentlyContinue
        }
    }
}

<#
    .Synopsis
        This allows the customer to change DRS from the default setting to 1-4 with 4 being the least conservative.
    .PARAMETER Drs
        The DRS setting to apply to the cluster. 3 is the default setting, 2 is one step more conservative (meaning less agressive in moving VMs).
    .PARAMETER ClustersToChange
        The clusters to apply the DRS setting to. This can be a single cluster or a comma separated list of clusters or a wildcard.
    .EXAMPLE
        Set-CustomDRS -ClustersToChange "Cluster-1, Cluster-2" -Drs 2
        Set-CustomDRS -ClustersToChange "*" -Drs 3 # This returns it to the default setting
#>

function Set-CustomDRS {

    [AVSAttribute(15, UpdatesSDDC = $false)]
    param(
        [Parameter(Mandatory = $true)]
        [String]$ClustersToChange,
        [Parameter(Mandatory = $true,
            HelpMessage = "The DRS setting. Default of 3 or more conservative of 2 or less conservative 4.")]
        [ValidateRange(1, 4)]
        [int] $Drs
    )

    switch ($Drs) {
        4 { $drsChange = 2 }
        3 { $drsChange = 3 }
        2 { $drsChange = 4 }
        1 { $drsChange = 5 }
        Default { $drsChange = 3 }
    }

    # Settings for DRS
    $spec = New-Object VMware.Vim.ClusterConfigSpecEx
    $spec.DrsConfig = New-Object VMware.Vim.ClusterDrsConfigInfo
    $spec.DrsConfig.VmotionRate = $drsChange
    $spec.DrsConfig.Enabled = $true
    $spec.DrsConfig.Option = New-Object VMware.Vim.OptionValue[] (2)
    $spec.DrsConfig.Option[0] = New-Object VMware.Vim.OptionValue
    $spec.DrsConfig.Option[0].Value = '0'
    $spec.DrsConfig.Option[0].Key = 'TryBalanceVmsPerHost'
    $spec.DrsConfig.Option[1] = New-Object VMware.Vim.OptionValue
    $spec.DrsConfig.Option[1].Value = '1'
    $spec.DrsConfig.Option[1].Key = 'IsClusterManaged'
    $modify = $true
    # End DRS settings

    # $cluster is an array of cluster names or "*""
    foreach ($cluster_each in ($ClustersToChange.split(",", [System.StringSplitOptions]::RemoveEmptyEntries)).Trim()) {
        $Clusters += Get-Cluster -Name $cluster_each
    }

    foreach ($cluster in $clusters) {
        try {
            $_this = Get-View -Id $cluster.Id
            $_this.ReconfigureComputeResource_Task($spec, $modify)
            Write-Host "Successfully set DRS for cluster $($cluster.Name)."
        }
        catch {
            Write-Error "Failed to set DRS for cluster $($cluster.Name)."
        }
    }
}

function Remove-CustomRole {
    <#
    .DESCRIPTION
        This function allows customer to remove a custom role from the SDDC.
        Useful in case of roles created with greater privileges than Cloudadmin that can no longer be removed from the UI.
    #>


    [CmdletBinding()]
    [AVSAttribute(10, UpdatesSDDC = $false)]
    param (
        [Parameter(Mandatory = $true,
            HelpMessage = "The name of the role to remove, as displayed in the vCenter UI (case insensitive). This must be a custom role.")]
        [string]
        $roleInput
    )
    # Check if the role exists before attempting removal
    $roleToRemove = Get-VIRole | Where-Object { $_.Name -eq $roleInput}

    # Check if the role is in the protected names list or is a System role
    if ($roleToRemove.Count -eq 1) {
        if ((Test-AVSProtectedObjectName -Name $roleToRemove.Name) -or $roleToRemove.IsSystem -eq $true) {
            Write-Error "'$roleInput' is either System or Built-in. Removal not allowed."
        }
        else {
            try {
                Remove-VIRole -Role $roleToRemove -Confirm:$false -Force:$false
                Write-Host "The role '$roleInput' has been removed."
            }
            catch {
                Write-Error "Failed to remove the role '$roleInput'."
                Write-Error $_.Exception.Message
            }
        }
    }
    else {
        Write-Host "The role '$roleInput' was not found or can refer to several roles. No removal performed. Below the list of roles found:"
        foreach ($roleItem in $roleToRemove) {
            Write-Host "Role Name: $($roleItem.Name)"
            Write-Host "Role Description: $($roleItem.Description)"
        }
    }
}

function Get-EsxtopData {
    <#
    .SYNOPSIS
        Collects esxtop performance data from an ESXi host via the vCenter Esxtop service API.

    .DESCRIPTION
        Collects batch-mode esxtop snapshots from a single ESXi host via the vCenter ServiceManager
        API (no SSH) and uploads the resulting CSV to the cluster's vSAN datastore (or a
        customer-specified datastore via OutputDatastoreName).

    .PARAMETER ClusterName
        The name of the vSphere cluster containing the target ESXi host.

    .PARAMETER EsxiHostName
        The ESXi host name or prefix. The first connected host matching this prefix is used.

    .PARAMETER Iterations
        Number of FetchStats snapshots. Combined with IntervalSeconds, total spacing between the
        first and last sample must not exceed 30 seconds: (Iterations - 1) * IntervalSeconds <= 30.

    .PARAMETER IntervalSeconds
        Seconds to wait after each sample before the next (not applied after the last sample).
        Range 2-30. The minimum of 2 seconds aligns with esxtop's minimum sampling interval.

    .PARAMETER OutputDatastoreName
        Name of the datastore to upload the CSV to. When omitted, defaults to the first vSAN
        datastore on the cluster. Specify this to use a non-vSAN datastore or when automatic
        vSAN discovery does not find the desired target.

    .NOTES
        Get-View emits a non-fatal "Invalid property" error for ServiceManager and Esxtop service
        objects but still returns a usable object. ErrorAction SilentlyContinue suppresses the noise.
        The returned object is validated via Get-Member before use.

        The Esxtop SimpleCommand API (CounterInfo, FetchStats, FreeStats) is not covered in the
        official vSphere API reference. The approach used here is based on:
        - https://williamlam.com/2017/02/using-the-vsphere-api-in-vcenter-server-to-collect-esxtop-vscsistats-metrics.html
        - https://github.com/lamw/vmware-scripts/blob/master/powershell/Get-EsxtopAPI.ps1
    #>


    [CmdletBinding()]
    [AVSAttribute(30, UpdatesSDDC = $false)]
    param(
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Name of the vSphere cluster containing the target ESXi host.')]
        [ValidateNotNullOrEmpty()]
        [string]$ClusterName,

        [Parameter(
            Mandatory = $true,
            HelpMessage = 'ESXi host name or name prefix. The first matching host will be used.')]
        [ValidateNotNullOrEmpty()]
        [string]$EsxiHostName,

        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Number of FetchStats snapshots (spacing (Iterations-1)*IntervalSeconds must be <= 30s).')]
        [ValidateRange(1, 6)]
        [int]$Iterations = 6,

        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Seconds between snapshots (2-30; with Iterations, total spacing <= 30s).')]
        [ValidateRange(2, 30)]
        [int]$IntervalSeconds = 5,

        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Name of the datastore for CSV upload. Defaults to the first vSAN datastore on the cluster.')]
        [ValidateNotNullOrEmpty()]
        [string]$OutputDatastoreName
    )

    $EsxiHostName = Limit-WildcardsandCodeInjectionCharacters -String $EsxiHostName
    $ClusterName = Limit-WildcardsandCodeInjectionCharacters -String $ClusterName
    if ($PSBoundParameters.ContainsKey('OutputDatastoreName')) {
        $OutputDatastoreName = Limit-WildcardsandCodeInjectionCharacters -String $OutputDatastoreName
    }

    $samplingSpanSec = [Math]::Max(0, $Iterations - 1) * $IntervalSeconds
    if ($samplingSpanSec -gt 30) {
        throw ("Esxtop sampling is limited to 30 seconds between the first and last sample: " +
            "(Iterations-1)*IntervalSeconds must be <= 30. Current spacing is ${samplingSpanSec}s " +
            "(Iterations=$Iterations, IntervalSeconds=$IntervalSeconds).")
    }

    $cluster = Get-Cluster -Name $ClusterName -ErrorAction Stop
    $vmHost = $cluster | Get-VMHost |
        Where-Object { $_.Name -like "$EsxiHostName*" -and $_.ConnectionState -eq 'Connected' } |
        Select-Object -First 1

    if ($null -eq $vmHost) {
        throw "No connected ESXi host matching '$EsxiHostName' found in cluster '$ClusterName'."
    }

    Write-Host "Target host: $($vmHost.Name)"

    # Get ServiceManager via Get-View (emits non-fatal error but returns usable object)
    $serviceManager = Get-View ($global:DefaultVIServer.ExtensionData.Content.ServiceManager) -Property "" -ErrorAction SilentlyContinue
    if ($null -eq $serviceManager) {
        throw "Could not resolve ServiceManager via Get-View."
    }
    if (-not (Get-Member -InputObject $serviceManager -Name "QueryServiceList")) {
        throw "ServiceManager object is missing QueryServiceList method. MoRef may be invalid."
    }

    # Query services on the target host
    $locationString = "vmware.host." + $vmHost.Name
    $services = $serviceManager.QueryServiceList($null, $locationString)
    if (-not $services) {
        throw "No services found at location '$locationString'."
    }

    $esxtopService = $null
    foreach ($svc in $services) {
        if ($svc.ServiceName -eq "Esxtop") {
            $esxtopService = $svc
            break
        }
    }
    if ($null -eq $esxtopService) {
        $available = ($services | ForEach-Object { $_.ServiceName }) -join ', '
        throw "Esxtop service not found on host $($vmHost.Name). Available: $available"
    }

    $esxtopView = Get-View $esxtopService.Service -Property "" -ErrorAction SilentlyContinue
    if ($null -eq $esxtopView) {
        throw "Could not resolve Esxtop service view via Get-View."
    }
    if (-not (Get-Member -InputObject $esxtopView -Name "ExecuteSimpleCommand")) {
        throw "Esxtop service view is missing ExecuteSimpleCommand method. MoRef may be invalid."
    }

    # CounterInfo
    $esxtopView.ExecuteSimpleCommand("CounterInfo") | Out-Null

    # FetchStats loop — collect samples to local temp file, then upload to vSAN datastore
    $hostShort = $vmHost.Name.Split('.')[0]
    $runTimestamp = Get-Date -Format "yyyyMMdd_HHmmss"
    $csvFileName = "esxtop_${hostShort}_${runTimestamp}.csv"
    $tempCsv = Join-Path ([System.IO.Path]::GetTempPath()) $csvFileName

    Write-Host "Collecting $Iterations samples from $($vmHost.Name) (interval=${IntervalSeconds}s)..."
    '"Timestamp","SampleNumber","RawData"' | Out-File -FilePath $tempCsv -Encoding UTF8
    $totalBytes = 0

    for ($i = 1; $i -le $Iterations; $i++) {
        $stats = $esxtopView.ExecuteSimpleCommand("FetchStats")

        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        $escaped = $stats -replace '"', '""'
        $csvRow = '"' + $timestamp + '",' + $i + ',"' + $escaped + '"'
        $csvRow | Out-File -FilePath $tempCsv -Encoding UTF8 -Append
        $totalBytes += $stats.Length

        $pct = [math]::Round(($i / $Iterations) * 100)
        $dataKB = [math]::Round($totalBytes / 1024, 1)
        Write-Host "Sample $i/$Iterations (${pct}%) - ${dataKB} KB collected"

        if ($i -lt $Iterations) {
            Start-Sleep -Seconds $IntervalSeconds
        }
    }

    # FreeStats
    try {
        $esxtopView.ExecuteSimpleCommand("FreeStats") | Out-Null
    }
    catch {
        Write-Warning "FreeStats call failed: $($_.Exception.Message)"
    }

    # Upload CSV to datastore
    try {
        if ($PSBoundParameters.ContainsKey('OutputDatastoreName')) {
            $datastore = Get-Datastore -Name $OutputDatastoreName -ErrorAction Stop
        }
        else {
            $datastore = Get-Datastore -RelatedObject $cluster -ErrorAction SilentlyContinue |
                Where-Object { $_.Type -eq 'vsan' -or $_.Name -like '*vsan*' -or $_.Name -like '*vsanDatastore*' } |
                Select-Object -First 1
        }

        if ($null -eq $datastore) {
            Write-Warning ("No vSAN datastore found on cluster '$ClusterName'. CSV saved locally at $tempCsv. " +
                "Use -OutputDatastoreName to specify an accessible datastore.")
        }
        else {
            $driveName = "esxtopUpload"
            if (Get-PSDrive -Name $driveName -ErrorAction SilentlyContinue) {
                Remove-PSDrive -Name $driveName -Force -ErrorAction SilentlyContinue
            }
            New-PSDrive -Name $driveName -Location $datastore -PSProvider VimDatastore -Root "\" -ErrorAction Stop | Out-Null

            $destFolder = "${driveName}:\esxtop_output"
            if (-not (Test-Path $destFolder -ErrorAction SilentlyContinue)) {
                New-Item -Path $destFolder -ItemType Directory -ErrorAction Stop | Out-Null
                if (-not (Test-Path $destFolder -ErrorAction SilentlyContinue)) {
                    throw "Failed to create esxtop_output folder on datastore [$($datastore.Name)]."
                }
            }

            $destFile = "$destFolder\$csvFileName"
            Copy-DatastoreItem -Item $tempCsv -Destination $destFile -Force -ErrorAction Stop
            $fileSizeKB = [math]::Round((Get-Item $tempCsv).Length / 1024, 1)
            Write-Host "Uploaded ${fileSizeKB} KB to [$($datastore.Name)] esxtop_output/$csvFileName"
        }
    }
    catch {
        Write-Warning "Datastore upload failed: $($_.Exception.Message)"
    }

    Write-Host "Esxtop collection complete. $Iterations samples from $($vmHost.Name)."
}

# SIG # Begin signature block
# MIInSAYJKoZIhvcNAQcCoIInOTCCJzUCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCruAu0r2LbdOtI
# p+c8Bhjx5x5Bkbvjb65eZWVXBpePNKCCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1
# OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8
# o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg
# 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4
# Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R
# X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk
# ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw
# HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg
# UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh
# MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9
# s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H
# VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3
# w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n
# 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs
# A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo
# Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb
# SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6
# 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z
# V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v
# 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs
# /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA
# AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow
# VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo
# MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh
# emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h
# KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd
# M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp
# yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t
# Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5
# REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs
# 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK
# Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5
# pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW
# eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ
# 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC
# NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI
# MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4
# NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh
# ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q
# hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU
# nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb
# H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z
# uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u
# vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW
# 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV
# DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghnkMIIZ4AIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK
# KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFPIvMSt
# oRTBwJTzqzdh6vsnj3FZ9C0kkiuHFg/0LKNlMEIGCisGAQQBgjcCAQwxNDAyoBSA
# EgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20w
# DQYJKoZIhvcNAQEBBQAEggEAaYGXIc5aDLklsu7Pdmu3ONBwjCGZ23o+AF3Gozzo
# 8KrBXZJQdV5yxC8rqYxKawHCsVKSiJ87YgROzq8UowoF1zoaXdyXHPEcxTTdQa2b
# rSPwJ3+hpUbsi9DYb1ZVfcZZCqXqq1Zrh5Zd43Q/3JUeqffTIJ1meofwgJeP8DhO
# Y4thQ8iBE7JFEr6LZty5XPqoxkvYvvuBPvN6RSMNYbqXGNkdnT1r6QCCOQtCKzuC
# 9hQ8d/pxNxiJrum+p466OTFn0Ul7WMwKrjqmtX93m7YyjnHTLmp7EgbOvlQxUUL3
# HIUFCn5mQJO9sSO9QaPHmo+yxCbasO4PQ6iVQhFoJAEJ+qGCF5YwgheSBgorBgEE
# AYI3AwMBMYIXgjCCF34GCSqGSIb3DQEHAqCCF28wghdrAgEDMQ8wDQYJYIZIAWUD
# BAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoD
# ATAxMA0GCWCGSAFlAwQCAQUABCBWboxqBSa3GPqUToWYA1dOMa8HKQWEx9Kt4ZNl
# UifDuAIGajGGs0haGBMyMDI2MDYxNjE5MjgwMy4xNjNaMASAAgH0oIHRpIHOMIHL
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxN
# aWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRT
# UyBFU046ODkwMC0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFNlcnZpY2WgghHsMIIHIDCCBQigAwIBAgITMwAAAiJB0vaq/8i1/wABAAAC
# IjANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAe
# Fw0yNjAyMTkxOTM5NTZaFw0yNzA1MTcxOTM5NTZaMIHLMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj
# YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODkwMC0wNUUw
# LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1ueKJukIuUsAAJo/AY5DZRqH7
# bhgv7CWGNlEdbRGoITrdE6Wsn57NaNu1BTdjBbFcv7Rfixte0x+HRvXSqsD+WeSX
# /6/y9wE0Mz+xRPTGIY20K7aQDa68OyzVyUeUCypyZC/gW/3ytO/ZOnU9H2ri77kJ
# P8ABrqyy1UxX/OseEgvHsj8yikWT0ARtrjWbXMHFzSOo5hQcfUmMXKqWWz6+N0+U
# ynhGy1n+doW4WZgpH8Y5W7hpSokWj1M/Lu4wi3o6Dz9vVWukcgUFGjLAl4YZpOha
# h7HuiC/alXImMQf8C3A8q/6/1hFoeIZB4UGkywxB/OSTOSsL6+39pDqzM7CgOpf4
# V799kN94yM9uXJI5T/SiA5MdIZIhEW0+bh85RqDh5YW3/oav54RPxw5OPlH64QV6
# KJkl0FIElMVoLNo8UWRQcMD179x7WASjC6LsaNZ7yK0qcESIsL1wiQmdfQBxcqrF
# CpIQfnmQFkOp9IyXUWqza8tmpz8E6aXg9b1eiAT3PVTgrOlPi/hYZCfPxX/6jGty
# Pjy1CiwOmJamohmSU//COAenfRT2G2HMRUpCX1zs+AmDmdQM1XRab4YSALLAlDzG
# CsgI77nnuJjoXAliJmv7NfrvWAcA5KqCUOWQ6kSPt5r28MfKXWJJpSXtFeS/MkDz
# Jy/iJRVyHcFy/B+MtwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFFkHwGoDJ5ZbEEiu
# 8KstiusqaozQMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1Ud
# HwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggr
# BgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIw
# MTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgw
# DgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBiAM+nqrpwG29txSXv
# 42o+CsTe2C4boaRfFju9JaWkLTHwq7pknNONL3n+UG3x/B083EKXiFYrAmul7BTH
# CGXU63/xRsZ2wj3ZmR0A4d9nf9saCJVm4juPVFBai/oktOOYH2j+1+zM70woN5on
# gB/pvy7X8AfY6JB4XPvb80Qz7fY5eddbnwjzg1sZhUPFbbcweWeACINrzqFK62mM
# eXKmhtufMraoogJeJXfWY3x4/pbubgENT3+pXT65203CPF9kfdKE7GKAIRYy3xkB
# TDvFd8dufjOpCn38nK6qMlVtnBjDhWQG0PM3E/oxBs5UBrI6pBYkmIHtbjifDquH
# T+ThaVV7xHc6InoSc3aNzX49JHUgQmuvDdMjLkbYXeA0/1q5IxSg2U+ycZBOvAi3
# udZPKhA5VzODjf/ucu/vFtXrYcRkmGKN3jujaK3/yMZi2Ju5NEL3ISWorwp7RjeZ
# g+JMIK0fosuVj+YCm5r64LH/D9QJDAj+XfZaNeFdv90K5A0QRRGP/poB9yTIVjEX
# j/uJzp8L4Dd44sAquqDOiHdkLgxfK8nPqpCSWPZ9G+RCPm85o9cAfxENtrSuOwcp
# yKzxsRCYCL+PK4+98orit9EVJ/LLoCeG+jLlj0KaD4Qy6sZe4rWMr1brQLosTBZN
# wFnXxNjInCWBd0i7is1yTS/4qTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkA
# AAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVow
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX
# 9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1q
# UoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8d
# q6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byN
# pOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2k
# rnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4d
# Pf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgS
# Uei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8
# QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6Cm
# gyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzF
# ER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQID
# AQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQU
# KqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1
# GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0
# bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMA
# QTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbL
# j+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1p
# Y3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwU
# tj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN
# 3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU
# 5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5
# KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGy
# qVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB6
# 2FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltE
# AY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFp
# AUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcd
# FYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRb
# atGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQd
# VTNYs6FwZvKhggNPMIICNwIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg
# T3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjg5MDAtMDVFMC1E
# OTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEw
# BwYFKw4DAhoDFQC7ycXVZx3bsDpJkr7VucgpksozuKCBgzCBgKR+MHwxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7dwFMjAiGA8y
# MDI2MDYxNjE3MjQwMloYDzIwMjYwNjE3MTcyNDAyWjB2MDwGCisGAQQBhFkKBAEx
# LjAsMAoCBQDt3AUyAgEAMAkCAQACAQ0CAf8wBwIBAAICElIwCgIFAO3dVrICAQAw
# NgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgC
# AQACAwGGoDANBgkqhkiG9w0BAQsFAAOCAQEAjCkvBujk0fJenfyTMx8kAxrjn/B+
# SF0SzD/dqJAio8PzTJXGmWCbSnKMBPzwiMtE4Jv5Eqjdz3DjCKcZHGR6pZym4QOz
# i5IWmhNqb7VPhau2+6/lyJx19EeZc0MoQoWHA8O3//8oSt3yCdjJPpM5RRPrZzEg
# Zc1rvkn5Az+e97FRZPSNHxbo1+/FPsVyNhtU72pa6xE7RT3kzsSNYqQPxH1G9un9
# DLPCs3xWJ3RCqLUCWSA+2P5sOUQVkqbdG/Qr6AQUVcsZIpCS9hB7SoMXKfJXwb02
# upWJL1iNTcQGr00R2TzWaw51umYlM2U1TL3KqSfhmaEWE425lIBoJwj8ODGCBA0w
# ggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACIkHS
# 9qr/yLX/AAEAAAIiMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYL
# KoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEILJaR5oeycxV2UZ3pQIsJJqo89A1
# g3XKij8LZ3Cxlwp1MIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB5DCBvQQgBWBdAQoE
# 58aCM2ySYM6ZtwQg6ccY3AD5BxG58NHkCRMwgZgwgYCkfjB8MQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBQQ0EgMjAxMAITMwAAAiJB0vaq/8i1/wABAAACIjAiBCDGGD7ZOZeD
# h+InR8QJK4ZNluGvaVn9Gwflb+OxWlmR3zANBgkqhkiG9w0BAQsFAASCAgBZAEyd
# lSta3dhAOpCTqiROs2kujjA94ExRV1XjvC6Le5HzOh2BGoRiInxrKvxjvvnrYQin
# UVWTawAOU9888yKxinNf0zUEBbU+DrvRGMYs1vVoWYKl3hjYzyiWTBPg/oiEVygd
# JjSZasIP5HQhJh0HpzeJN/14deZrDFBbFrXgEf5B1PXmn9V+qLm2dW7C5zSXVien
# cqRYsc9bekD8L/gZ/Ej1MzAENNyUBoA7eq3tWRY5vuAkbdpALb1w5b60KWB4cvSo
# +cJfNAtCphNNJdGV84J1fRm4BmTGbr8rZMPm3cC54gRGDmHOwVs5Q/ZwKreY9EhK
# IYbqVZZe5twiPVOyA3tidCxERCmiXaQlBA898fZNYTP5mEfW2L9dR+pYqeuKFFoB
# xmS6H0MYtGo0D85QLuWgjlnFBE6Xr1zCI8iNAADgweY1VQWRSUEFN8B9iABrrJs4
# LFVrlSNRQ2E9P9LeUxhMhlkEpX6xcY5o/XQBcYeLvqU6WqRNOjy8kk6bcY1Fgb8Z
# M4mz5b3fZcWY0uAjoH9pp8+2VDOYlCzRdBv2UXHzJ8eZ0HJjIl1LE4peyRc9jYyd
# VSpffZ67yA5EHaHhjEbzX3em6IPWPv3iNua889NC/sVU7XQN8S4OY1AP6adeY6HL
# 5t+gSBQE994h9VYTMItHhNTXBSDIxJK3fcBcmA==
# SIG # End signature block