AzStackHciStandaloneObservability/package/bin/ObsDep/content/Powershell/ObservabilityHelpers.psm1

<###################################################
 # #
 # Copyright (c) Microsoft. All rights reserved. #
 # #
 ##################################################>


# Import Observability EventSource
$observabilityNugetPath = "$PSScriptRoot\..\.."
Add-Type -Path "$observabilityNugetPath\lib\net472\Microsoft.AzureStack.Observability.ObservabilityDeployment.dll"
Import-Module "$observabilityNugetPath\content\Powershell\ObservabilityConstants.psm1"


# Create and setup new Observability volume. Returns tuple with computer name and $true on success, or error information $_ on failure.
function Add-ObservabilityVolumeWithRetry
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    param (
        # Fully qualified path to the VHD file that should be created.
        # An exception is thwrown if that VHD file already exist. This approach is chosen
        # to prevent issues, should the VHD file be still mounted.
        [string]
        [Parameter(Mandatory=$true)] 
        $Path, 

        # Folder access path for mounted drive.
        [string] 
        [Parameter(Mandatory=$true)] 
        $AccessPath,
        
        # Volume name for the partition that is created within the VHD
        [string] 
        [Parameter(Mandatory=$true)] 
        $VolumeLabel,

        # Maximum size in bytes for the VHD file.
        [int64] 
        [Parameter(Mandatory=$true)] 
        $Size,

        # Optional. Specifies that the VHD should be static.
        [switch]
        [Parameter(Mandatory=$false)]
        $StaticSize,

        # Optional. Number of times to retry operation before failing.
        [int]
        [Parameter(Mandatory=$false)]
        $Retries = 5,

        # Optional. Seconds to sleep before retry
        [int]
        [Parameter(Mandatory=$false)]
        $RetrySleepTimeInSeconds = 10
    )

    if(Test-Path $Path)
    { 
        if((Get-DiskImage $Path).Attached)
        {
            Write-ObservabilityLog "VHD file is already mounted."
        }
        else
        {
            Write-ObservabilityLog "Existing unmounted VHD file found. Mounting $Path"
            Mount-VHD -Path $Path -NoDriveLetter -Verbose
        }
    }
    else
    {
        $retryAttempt = 0
        $success = $false
        $hostName = $env:COMPUTERNAME
        while(-not($success) -and ($retryAttempt -lt $Retries))
        {
            $retryAttempt = $retryAttempt + 1
            try
            {
                Write-ObservabilityLog "Trying to create Observability Volume. Attempt $retryAttempt of $Retries"
                if($StaticSize)
                {
                    New-ObservabilityVolume -Path $Path -AccessPath $AccessPath -VolumeLabel $VolumeLabel -Size $Size -StaticSize
                }
                else
                {
                    New-ObservabilityVolume -Path $Path -AccessPath $AccessPath -VolumeLabel $VolumeLabel -Size $Size
                }
                Write-ObservabilityVolumeCreationStopTelemetry `
                    -ComputerName $hostName `
                    -Message "Observability volume setup on host $hostName succeeded."
                $success = $true
            }
            catch
            {
                $exceptionMessage = $_.Exception.Message
                Write-ObservabilityVolumeCreationStopTelemetry `
                    -ComputerName $hostName `
                    -Message "Observability volume setup on host $hostName failed with exception $exceptionMessage." `
                    -ExceptionDetails $exceptionMessage
                if ($retryAttempt -lt $Retries)
                {
                    Write-ObservabilityErrorLog "Failure during VHD creation: '$exceptionMessage'. Retrying."
                }
                else
                {
                    # All retries failed.
                    throw $_
                }
                Start-Sleep -Seconds $RetrySleepTimeInSeconds
            }
        }
    }
}

# Function to create a data VHD file, which contains an empty, NTFS formatted partition. Returns $true on success, $false on failure
function New-ObservabilityVolume
{
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    param (
        # Fully qualified path to the VHD file that should be created.
        # An exception is thwrown if that VHD file already exist. This approach is chosen
        # to prevent issues, should the VHD file be still mounted.
        [string]
        [Parameter(Mandatory=$true)] 
        $Path, 
        
        # Folder access path for mounted drive.
        [string] 
        [Parameter(Mandatory=$true)] 
        $AccessPath,

        # Volume name for the partition that is created within the VHD.
        [string] 
        [Parameter(Mandatory=$true)] 
        $VolumeLabel,

        # Maximum size in bytes for the VHD file.
        [int64] 
        [Parameter(Mandatory=$true)] 
        $Size,

        # Optional. Specifies that the VHD should be static.
        [switch]
        [Parameter(Mandatory=$false)]
        $StaticSize
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    Import-Module Hyper-V
    $disk = $null

    try 
    {
        Write-ObservabilityLog ("Creating new VHD $Path with the following properties: Max size: $Size bytes, Static: $StaticSize.")
        if ($StaticSize)
        {
            New-VHD -Path $Path -SizeBytes $Size -Fixed -Verbose | Out-Null
        }
        else
        {
            New-VHD -Path $Path -SizeBytes $Size -Dynamic -Verbose | Out-Null
        }
    }
    catch
    {
        Remove-ObservabilityVolume -Path $Path -AccessPath $AccessPath
        throw ("Error creating the data VHD file at {0}. Error: {1}" -f $Path, $_)
    }

    try
    {
        Write-ObservabilityLog ("Mounting a data VHD {0}" -f $Path)
        $disk = Mount-VHD -Path $Path -Passthru -NoDriveLetter -Verbose

        Write-ObservabilityLog "Initializing mounted VHD for disk number $($disk.Number)"
        $null = Initialize-Disk -Number $disk.Number -Verbose

        Write-ObservabilityLog "Creating new partition. for disk number $($disk.Number)"
        $part = New-Partition -DiskNumber $disk.Number -UseMaximumSize -Verbose
            
        Write-ObservabilityLog "Formatting volume."
        $null = Format-Volume -Partition $part -FileSystem NTFS -NewFileSystemLabel $VolumeLabel -Force -Confirm:$false -Verbose

        Write-ObservabilityLog ("Setting attribute '{0}' to '{1}'." -f "NoDefaultDriveLetter", "true")
        Set-Partition $part.DiskNumber $part.PartitionNumber -NoDefaultDriveLetter $true -Verbose

        $diskString = Out-String -InputObject $disk
        Write-ObservabilityLog $diskString
    }
    catch
    {
        Remove-ObservabilityVolume -Path $Path -AccessPath $AccessPath
        throw New-Object -TypeName System.ApplicationException -ArgumentList ($("Mounting VHD and formatting the partition failed. Error: {0}" -f $_.Exception.Message), $_.Exception)
    }
}

# Dismount and Delete vhdx file at given path
function Remove-ObservabilityVolume
{
    param(
        [string]
        [Parameter(Mandatory=$true)] 
        $Path,

        [string]
        [Parameter(Mandatory=$true)] 
        $AccessPath
    )
    if(Test-Path $Path)
    {
        try
        {
            if((Get-DiskImage $Path).Attached)
            {
                Dismount-VHD -Path $Path -Verbose
            }
        }
        catch {}
        $null = Remove-Item -Path $Path -Force
    }
    if(Test-Path $AccessPath)
    {
        $null = Remove-Item -Path $AccessPath -Force
    }
}

# Tests if path is an empty directory(including hidden files) Note: Will return false if path is not a directory
function Test-DirectoryIsEmpty
{
    param(
        [string]
        [Parameter(Mandatory=$true)] 
        $Path
    )
    return (Get-ChildItem $Path -Force).Count -eq 0
}

# Create and set quotas for each subfolder of the Observability volume. Returns tuple with computer name and $true on success, or error information $_ on failure.
function New-VolumeFoldersAndPrunerWithRetry
{
    param (
        # Folder access path for mounted drive.
        [string]
        [parameter(Mandatory=$true)] 
        $AccessPath, 

        # For folders with enforced quota, when the folder is this percent full cleanup will be initiated.
        [int]
        [Parameter(Mandatory=$true)]
        $CleanupThresholdPercent,

        # For folders with enforced quota, then cleanup occurs files will be pruned until this fullness percentage is reached.
        [int]
        [Parameter(Mandatory=$true)]
        $FreeSpaceThresholdPercent,

        # Frequency for which each folder pruning scheduled task executes
        [int]
        [Parameter(Mandatory=$true)]
        $PurgeFolderFrequencyInMinutes,

        # Name of subfolder config file to read
        [string]
        [Parameter(Mandatory=$true)] 
        $SubFolderConfigFileName,
        
        # Optional. Number of times to retry operation before failing.
        [int]
        [Parameter(Mandatory=$false)]
        $Retries = 5,

        # Optional. Seconds to sleep before retry
        [int]
        [Parameter(Mandatory=$false)]
        $RetrySleepTimeInSeconds = 10
    )
    $retryAttempt = 0
    $success = $false
    while(-not($success) -and ($retryAttempt -lt $Retries))
    {
        $retryAttempt = $retryAttempt + 1
        try
        {
            Write-ObservabilityLog "Trying to setup Observability Folder quotas. Attempt $retryAttempt of $Retries"
            New-VolumeFoldersAndPruner `
                -AccessPath $AccessPath `
                -CleanupThresholdPercent $CleanupThresholdPercent `
                -FreeSpaceThresholdPercent $FreeSpaceThresholdPercent `
                -PurgeFolderFrequencyInMinutes $PurgeFolderFrequencyInMinutes `
                -SubFolderConfigFileName $SubFolderConfigFileName
            Write-ObservabilityLog "Observability volume folder and pruner setup on host $env:COMPUTERNAME succeeded."
            $success = $true
        }
        catch
        {
            if ($retryAttempt -lt $Retries)
            {
                $exceptionMessage = $_.Exception.Message
                Write-ObservabilityErrorLog "Failure during quota setup: '$exceptionMessage'. Retrying."
            }
            else
            {
                # All retries failed.
                throw $_
            }
            Start-Sleep -Seconds $RetrySleepTimeInSeconds
        }
    }
}

# Create each subfolder of observability volume and set folder quota if specified.
function New-VolumeFoldersAndPruner
{
    param (
        # Folder access path for mounted drive.
        [string]
        [parameter(Mandatory=$true)] 
        $AccessPath, 

        # When a folder is this percent full cleanup will be initiated.
        [int]
        [Parameter(Mandatory=$true)]
        $CleanupThresholdPercent,

        # For folders with enforced quota, then cleanup occurs files will be pruned until this fullness percentage is reached.
        [int]
        [Parameter(Mandatory=$true)]
        $FreeSpaceThresholdPercent,

        # Frequency for which each folder cleanup scheduled task executes
        [int]
        [Parameter(Mandatory=$true)]
        $PurgeFolderFrequencyInMinutes,

        # Name of subfolder config file to read
        [string]
        [Parameter(Mandatory=$true)] 
        $SubFolderConfigFileName
    )

    $observabilityNugetPath = "$PSScriptRoot\..\.."
    $pruneObservabilityPath = "$observabilityNugetPath\content\Powershell\PruneObservabilityVolume.ps1"
    $subFolders = Get-ObservabilityFolders -SubFolderConfigFileName $SubFolderConfigFileName
    foreach ($subFolder in $subFolders)
    {
        $subFolderPath = Join-Path -Path $AccessPath -ChildPath $subFolder.Name
        if(-not (Test-Path $subFolderPath)){
            Write-ObservabilityLog ("Creating empty directory at $subFolderPath.")
            New-Item -Path $subFolderPath -ItemType "Directory" -Verbose | Out-Null
        }
    }

    # Create Scheduled task to prune observability volume
    $taskName = "ObservabilityVolumePruner"
    if(Test-ScheduledTaskExists -TaskName $taskName)
    {
        Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -Verbose | Out-Null
    }

    Write-ObservabilityLog "Creating new scheduled task $taskName."
    $frequency = New-TimeSpan -Minutes $PurgeFolderFrequencyInMinutes
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount
    $trigger = New-ScheduledTaskTrigger -RepetitionInterval $frequency -Once -At "12:00 AM"
    $action = New-ScheduledTaskAction `
        -Execute "powershell.exe"  `
        -Argument "-Command $pruneObservabilityPath -AccessPath $AccessPath -CleanupThresholdPercent $CleanupThresholdPercent -FreeSpaceThresholdPercent $FreeSpaceThresholdPercent -SubFolderConfigFileName $SubFolderConfigFileName"
    $settings = New-ScheduledTaskSettingsSet 
    $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal
    Register-ScheduledTask -TaskName $taskName -TaskPath "Microsoft\AzureStack\Observability" -InputObject $task
    Write-ObservabilityLog "Creating new scheduled task $taskName succeeded."

    Add-VolumeFolderQuotaStatisticsScheduledTask -AccessPath $AccessPath -SubFolderConfigFileName $SubFolderConfigFileName
}

function Set-FolderQuotas
{
    param (
        # Folder access path for mounted drive.
        [string]
        [parameter(Mandatory=$true)] 
        $AccessPath, 

        # Name of subfolder config file to read
        [string]
        [Parameter(Mandatory=$true)] 
        $SubFolderConfigFileName
    )
    $subFolders = Get-ObservabilityFolders -SubFolderConfigFileName $SubFolderConfigFileName
    foreach($subFolder in $subFolders)
    {
        $subFolderPath = Join-Path -Path $AccessPath -ChildPath $subFolder.Name
        if(Test-Path $subFolderPath){
            if(Test-PathHasQuota -Path $subFolderPath)
            {
                Remove-FsrmQuota -Path $subFolderPath -Confirm:$false -Verbose | Out-Null
            }
        }
        else
        {
            Write-ObservabilityLog ("Creating empty directory at $subFolderPath.")
            New-Item -Path $subFolderPath -ItemType "Directory" -Verbose | Out-Null
        }

        $subFolderSizeInMB = [int64]::Parse($subFolder.SizeInMB)
        $subFolderSizeInBytes = $subFolderSizeInMB * 1MB
        Write-ObservabilityLog "Creating new FSRM quota at path $subFolderPath of size $subFolderSizeInBytes bytes."
        New-FsrmQuota -Path $subFolderPath -Size $subFolderSizeInBytes -ErrorAction Stop -Verbose | Out-Null
        Write-ObservabilityLog "Creating FSRM quota at path $subFolderPath succeeded."
    }
}

# Returns true if a given task exists and false otherwise.
function Test-ScheduledTaskExists
{
    param(
        # Absolute path of folder to test
        [string]
        [Parameter(Mandatory=$true)] 
        $TaskName
    )
    try
    {
        if(Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop)
        {
            return $true
        }
    }
    catch {}
    return $false
}

# Returns true if a given folder has a quota and false otherwise.
function Test-PathHasQuota
{
    param(
        # Absolute path of folder to test
        [string]
        [Parameter(Mandatory=$true)] 
        $Path
    )
    $quotas = Get-FsrmQuota -ErrorAction Stop -Verbose
    if ($quotas -is [System.Array])
    {
        foreach ($quota in $quotas)
        {
            if ($quota.Path -eq $Path)
            {
                return $true
            }
        }
        return $false
    }
    else
    {
        return $quotas.Path -eq $Path
    }
}

# Remove oldest files from folder until free space threshold is reached
function Remove-ObservabilityFolderOldestFiles
{
    param(
        # Folder access path for mounted drive.
        [string]
        [parameter(Mandatory=$true)] 
        $AccessPath, 

        # When a folder is this percent full cleanup will be initiated.
        [int]
        [Parameter(Mandatory=$true)]
        $CleanupThresholdPercent,

        # When cleanup occurs files will be pruned until this fullness percentage is reached.
        [int]
        [Parameter(Mandatory=$true)]
        $FreeSpaceThresholdPercent,

        # Name of subfolder config file to read
        [string]
        [Parameter(Mandatory=$true)] 
        $SubFolderConfigFileName
    )
    
    $subFolders = Get-ObservabilityFolders -SubFolderConfigFileName $SubFolderConfigFileName
    foreach($subFolder in $subFolders)
    {
        $subFolderPath = Join-Path -Path $AccessPath -ChildPath $subFolder.Name
        $subFolderSizeInMB = [int64]::Parse($subFolder.SizeInMB)
        $quotaSize = $subFolderSizeInMB * 1MB
        $files = Get-ChildItem -Path $subFolderPath -Recurse -Attributes "!Directory" | Sort-Object -Property "LastWriteTime"
        $directorySize = ($files | Measure-Object -Property "Length" -Sum).Sum
        $freeSpaceSize = $quotaSize * $FreeSpaceThresholdPercent / 100
        $cleanupThresholdSize = $quotaSize * $CleanupThresholdPercent / 100
        if($directorySize -gt $cleanupThresholdSize)
        {
            Write-ObservabilityLog "Cleanup Trigger Threshold Percent of $CleanupThresholdPercent reached."
            Write-ObservabilityLog "Cleanup of oldest files from $subFolderPath started."
            foreach ($file in $files)
            {
                $filePath = $file.FullName
                $fileSize = $file.Length
                $exceptionDetails = ""
                $success = $false
                try
                {
                    Write-ObservabilityLog "Removing file at $filePath of size $fileSize bytes."
                    Remove-Item -Path $filePath -Force
                    $directorySize = $directorySize - $fileSize
                    $success = $true
                }
                catch
                {
                    # Next activation of scheduled task will retry delete.
                    $exceptionDetails = $_
                    $success = $false
                }
                finally
                {
                    Write-ObservabilityVolumePrunerFileDeletionTelemetry `
                        -ComputerName $ENV:COMPUTERNAME `
                        -DirectoryPath $subFolderPath `
                        -DirectorySize $directorySize `
                        -FilePath $filePath `
                        -FileSize $fileSize `
                        -Success $success `
                        -ExceptionDetails $exceptionDetails
                }

                if ($directorySize -lt $freeSpaceSize)
                {
                    Write-ObservabilityLog "Acceptable volume size of $freeSpaceSize reached. Stopping cleanup."
                    break
                }
            }
            Write-ObservabilityLog "Cleanup of oldest files from $subFolderPath completed."
        }
    }
}

# Returns object array of the subfolders within the Observability volume.
# Folders should include name and folder size as percentage of volume size.
function Get-ObservabilityFolders
{
    param(
        # Name of subfolder config file to read
        [string]
        [Parameter(Mandatory=$true)] 
        $SubFolderConfigFileName
    )
    $observabilityNugetPath = "$PSScriptRoot\..\.."
    $observabilityFoldersFilePath = "$observabilityNugetPath\content\Configuration\$SubFolderConfigFileName"
    return Get-Content $observabilityFoldersFilePath | ConvertFrom-Json
}

# Creates a scheduled task to mount the Observability volume on startup, and to attempt to do so every hour.
function Add-MountVolumeScheduledTask
{
    param (
        # Fully qualified path to the VHD file that should be created.
        # An exception is thrown if that VHD file already exist. This approach is chosen
        # to prevent issues, should the VHD file be still mounted.
        [string]
        [Parameter(Mandatory=$true)] 
        $Path,

        # Credential under which the scheduled task should run. If not supplied, the system account is used.
        [PSCredential]
        [Parameter(Mandatory=$false)]
        $Credential
    )
    $taskName = "MountObservabilityVolume"
    if(Test-ScheduledTaskExists -TaskName $taskName)
    {
        Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -Verbose | Out-Null
    }

    # Setup scheduled task to mount VHD on startup
    $observabilityNugetPath = "$PSScriptRoot\..\.."
    $mountScriptPath = "$observabilityNugetPath\content\Powershell\MountObservabilityVolume.ps1"
    Write-ObservabilityLog "Creating new scheduled task $taskName."

    $frequency = New-TimeSpan -Minutes 30
    $hourlyTrigger = New-ScheduledTaskTrigger -RepetitionInterval $frequency -Once -At "12:00 AM"
    $startupTrigger= New-ScheduledTaskTrigger -AtStartup
    $triggers = @($startupTrigger, $hourlyTrigger)
    $action = New-ScheduledTaskAction `
        -Execute "powershell.exe"  `
        -Argument "-Command $mountScriptPath -Path $Path"
    $settings = New-ScheduledTaskSettingsSet 
    if($Credential)
    {
        $principal = New-ScheduledTaskPrincipal -LogonType S4U -UserId $Credential.UserName
        $task = New-ScheduledTask -Action $action -Trigger $triggers -Settings $settings -Principal $principal
        Register-ScheduledTask `
            -TaskName $taskName `
            -TaskPath "Microsoft\AzureStack\Observability" `
            -InputObject $task `
            -User $Credential.UserName `
            -Password $Credential.GetNetworkCredential().Password
    }
    else
    {
        $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount
        $task = New-ScheduledTask -Action $action -Trigger $triggers -Settings $settings -Principal $principal
        Register-ScheduledTask -TaskName $taskName -TaskPath "Microsoft\AzureStack\Observability" -InputObject $task
    }
    Write-ObservabilityLog "Creating new scheduled task $taskName succeeded."
}

function Add-VolumeAccessPath
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $AccessPath,
    
        [string]
        [Parameter(Mandatory=$true)]
        $VolumePath
    )
    Write-ObservabilityLog ("Adding access path at $AccessPath.")
    if(-not (Get-DiskImage $VolumePath).Attached)
    {
        Mount-VHD -Path $VolumePath -Verbose -ErrorAction Stop
    }
    $diskNumber = (Get-DiskImage $VolumePath).Number

    $part = Get-Partition -DiskNumber $diskNumber -PartitionNumber 2

    if (Test-Path $AccessPath)
    {
        if (Test-AccessPathExists -AccessPath $AccessPath -VolumeFilePath $VolumePath)
        {
            Write-ObservabilityLog ("Access path at $AccessPath already exists. Noop.")
        }
        elseif(-Not(Test-DirectoryIsEmpty -Path $AccessPath))
        {
            throw "$AccessPath is not an empty directory. It cannot be used as a folder mount point."
        }
    }
    else
    {
        Write-ObservabilityLog ("Creating empty directory at $AccessPath.")
        New-Item -Path $AccessPath -ItemType "Directory" -Verbose | Out-Null

        if (Test-AccessPathExists -AccessPath $AccessPath -VolumeFilePath $VolumePath)
        {
            Write-ObservabilityLog ("Access path at $AccessPath already exists. Removing access path $AccessPath from $VolumePath.")
            Remove-Partitionaccesspath -InputObject $part -AccessPath $AccessPath -Verbose | Out-null
        }

        Add-PartitionAccessPath -InputObject $part -AccessPath $AccessPath -Verbose | Out-Null
        Write-ObservabilityLog ("Successfully added access path at $AccessPath.")
    }
}

# For a given mounted volume, return true if it has the specified access path and false if not.
function Test-AccessPathExists
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $AccessPath,

        [string]
        [Parameter(Mandatory=$true)]
        $VolumeFilePath
    )
    $disk = Get-VHD $VolumeFilePath
    $part = Get-Partition -Disknumber $disk.Number -PartitionNumber 2
    $matchedAccessPath = $part.AccessPaths | Where-Object {$_ -clike "$AccessPath*"}
    if($matchedAccessPath)
    {
        return $true
    }
    else
    {
        return $false
    }
}

function Write-ObservabilityLog
{
    param(
        [string]
        [Parameter(Mandatory=$true)] 
        $Message
    )
    if (Get-Command Trace-Execution -ErrorAction SilentlyContinue)
    {
        Trace-Execution $Message
    }
    [Microsoft.AzureStack.Observability.ObservabilityEventSource]::Log.WriteInformational($Message)
}

function Write-ObservabilityErrorLog
{
    param(
        [string]
        [Parameter(Mandatory=$true)] 
        $Message
    )
    if (Get-Command Trace-Execution -ErrorAction SilentlyContinue)
    {
        Trace-Execution $Message
    }
    [Microsoft.AzureStack.Observability.ObservabilityEventSource]::Log.WriteError($Message)
}

function Write-ObservabilityVolumeCreationStartTelemetry
{
    param(
        [string]
        [Parameter(Mandatory=$true)] 
        $ComputerNames,

        [string]
        [Parameter(Mandatory=$true)] 
        $VolumeFilePath,

        [string]
        [Parameter(Mandatory=$true)] 
        $VolumeAccessPath,

        [string]
        [Parameter(Mandatory=$true)] 
        $VolumeLabel,

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

    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.ObservabilityVolumeCreationStart(
        $ComputerNames,
        $VolumeFilePath,
        $VolumeAccessPath,
        $VolumeLabel,
        $VolumeSize
    )
    $volumeInfo = @"
        Starting Observability Volume Setup
        ComputerNames: $ComputerNames
        Volume path: $volumeFilePath
        Volume folder mount access path: $VolumeAccessPath
        Volume label: $VolumeLabel
        Volume size in bytes: $VolumeSize
"@

    Write-ObservabilityLog -Message $volumeInfo
}

function Write-ObservabilityVolumeCreationStopTelemetry
{
    param(
        [string]
        [Parameter(Mandatory=$true)] 
        $ComputerName,

        [string]
        [Parameter(Mandatory=$true)] 
        $Message,

        [string]
        [Parameter(Mandatory=$false)] 
        $ExceptionDetails = ""
    )

    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.ObservabilityVolumeCreationStop($ComputerName, $Message, $ExceptionDetails)

    if($ExceptionDetails)
    {
        Write-ObservabilityErrorLog -Message $Message
    }
    else
    {
        Write-ObservabilityLog -Message $Message
    }
}

function Write-ObservabilityVolumeMountedByScheduledTaskTelemetry
{
    param(
        [string]
        [Parameter(Mandatory=$true)] 
        $ComputerName,

        [string]
        [Parameter(Mandatory=$true)] 
        $VolumeFilePath,

        [bool]
        [Parameter(Mandatory=$true)]
        $Success,

        [Parameter(Mandatory=$false)]
        $ExceptionDetails = ""
    )

    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.ObservabilityVolumeMountedByScheduledTask(
        $ComputerName,
        $VolumeFilePath,
        $Success,
        $ExceptionDetails
    )
    $message = @"
        Observability Volume Mounted by Scheduled Task
        ComputerName: $ComputerName
        Volume file path: $VolumeFilePath
        Success: $Success
        Exception Details (if applicable): $ExceptionDetails
"@

    if($ExceptionDetails)
    {
        Write-ObservabilityErrorLog -Message $Message
    }
    else
    {
        Write-ObservabilityLog -Message $Message
    }
}

function Write-ObservabilityVolumePrunerFileDeletionTelemetry
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $ComputerName,

        [string]
        [Parameter(Mandatory=$true)]
        $DirectoryPath,

        [int64]
        [Parameter(Mandatory=$true)]
        $DirectorySize,

        [string]
        [Parameter(Mandatory=$true)]
        $FilePath,

        [int64]
        [Parameter(Mandatory=$true)]
        $FileSize,

        [bool]
        [Parameter(Mandatory=$true)]
        $Success,

        [Parameter(Mandatory=$false)]
        $ExceptionDetails = ""
    )
    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.ObservabilityVolumePrunerFileDeletion(
        $ComputerName,
        $DirectoryPath,
        $DirectorySize,
        $FilePath,
        $FileSize,
        $Success,
        $ExceptionDetails
    )
        $message = @"
        Observability Volume Pruner File Deletion
        ComputerName: $ComputerName
        Directory path: $DirectoryPath
        Directory size in bytes: $DirectorySize
        File path: $FilePath
        File Size in bytes: $FileSize
        Success: $Success
        Exception Details (if applicable): $ExceptionDetails
"@

    if($success)
    {
        Write-ObservabilityLog $message
    }
    else
    {
        Write-ObservabilityErrorLog $message
    }
}

function Write-BootstrapNodeIdAndHardwareIdHashTelemetry
{
    param(
        # Node ID assigned to node before cluster joining
        [string]
        [parameter(Mandatory=$true)]
        $BootstrapNodeId
    )
    $hardwareHashId = Get-HardwareIdHash
    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.BootstrapNodeIdAndHardwareIdHash(
        $BootstrapNodeId,
        $hardwareHashId
    )
    Write-ObservabilityLog "Bootstrap Node ID: $BootstrapNodeId Hardware Hash ID: $hardwareHashId"
}

function Add-VolumeFolderQuotaStatisticsScheduledTask
{
    param(
        # Folder access path for observability drive
        [string]
        [parameter(Mandatory=$true)] 
        $AccessPath,

        # Name of subfolder config file to read
        [string]
        [Parameter(Mandatory=$true)] 
        $SubFolderConfigFileName
    )

    $taskName = "ObservabilityVolumeQuotaStatistics"
    if(Test-ScheduledTaskExists -TaskName $taskName)
    {
        Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -Verbose | Out-Null
    }

    # Setup scheduled task to mount VHD on startup
    $observabilityNugetPath = "$PSScriptRoot\..\.."
    $scriptPath = "$observabilityNugetPath\content\Powershell\WriteObservabilityVolumeQuotaStatistics.ps1"
    Write-ObservabilityLog "Creating new scheduled task $taskName."

    $frequency = New-TimeSpan -Hours 6
    $trigger = New-ScheduledTaskTrigger -RepetitionInterval $frequency -Once -At "12:00 AM"
    $action = New-ScheduledTaskAction `
        -Execute "powershell.exe"  `
        -Argument "-Command $scriptPath -AccessPath $AccessPath -SubFolderConfigFileName $SubFolderConfigFileName"
    $settings = New-ScheduledTaskSettingsSet 
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount
    $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal
    Register-ScheduledTask -TaskName $taskName -TaskPath "Microsoft\AzureStack\Observability" -InputObject $task
    Write-ObservabilityLog "Creating new scheduled task $taskName succeeded."
}

function Install-ArcForServerAgent
{
    param(
        [string]
        [parameter(Mandatory=$true)] 
        $AgentMsiPath,

        [string]
        [Parameter(Mandatory=$true)] 
        $AccessToken,

        [string]
        [Parameter(Mandatory=$true)] 
        $SubscriptionId,

        [string]
        [Parameter(Mandatory=$true)] 
        $TenantId,

        [string]
        [Parameter(Mandatory=$true)] 
        $ResourceGroupName,

        [string]
        [Parameter(Mandatory=$true)] 
        $ResourceName,

        [string]
        [Parameter(Mandatory=$true)] 
        $EnvironmentName,

        [string]
        [Parameter(Mandatory=$true)] 
        $Region
    )
    # NOTE: This Add-Type will not work if called from TelemetryAndDiagnostics ARC extension
    $observabilityNugetPath = Get-ASArtifactPath -NugetName "Microsoft.AzureStack.Observability.ObservabilityDeployment"
    Add-Type -Path "$observabilityNugetPath\lib\net472\Microsoft.AzureStack.Observability.ObservabilityCommon.dll"

    $timestamp = [DateTime]::Now.ToString("yyyyMMdd-HHmmss")
    $logPath = (New-Item -Path "$env:LocalRootFolderPath" -ItemType Directory -Force).FullName
    $logFile = Join-Path -Path $logPath -ChildPath "ArcForServerInstall_${timestamp}.txt"

    $AgentWebLink = $ObservabilityConfigConstants.ArcForServerAgentWebLink
    $AgentMsiPath = Join-Path -Path $AgentMsiPath -ChildPath $ObservabilityConfigConstants.ArcForServerMsiFileName
    $programFiles = [Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFiles)
    $AgentExePath = Join-Path -Path $programFiles -ChildPath $ObservabilityConfigConstants.ArcForServerExePath

    Write-ObservabilityLog "$env:COMPUTERNAME Starting Arc-for-server agent install AgentWebLink: $AgentWebLink AgentMsiPath: $AgentMsiPath AgentExePath: $AgentExePath logs: $logFile"
    $res = $false

    try {
        $arcContext = New-Object Microsoft.AzureStack.Observability.ObservabilityCommon.ArcForServer.ArcContext
        $arcContext.AccessToken = $AccessToken
        $arcContext.SubscriptionId = $SubscriptionId
        $arcContext.ResourceGroup = $ResourceGroupName
        $arcContext.Location = $Region
        $arcContext.Cloud = $EnvironmentName
        $arcContext.ResourceName = $ResourceName
        $arcContext.TenantId = $TenantId

        Write-ObservabilityLog "AgentWebLink: $AgentWebLink AgentMsiPath: $AgentMsiPath AgentExePath: $AgentExePath"
        $arcAgent = New-Object Microsoft.AzureStack.Observability.ObservabilityCommon.ArcForServer.ArcAgent
        $res = $arcAgent.Onboard($arcContext, $AgentWebLink, $AgentMsiPath, $logFile, $AgentExePath)
    }
    catch {
        Write-ObservabilityErrorLog "$env:COMPUTERNAME Failed to install/configure ArcAgent Exception: $_"
    }

    Write-ObservabilityLog "Arc-for-server agent install $env:COMPUTERNAME. Status $res"
    if($res -eq $false )
    {
        throw "Arc Agent failed to install or connect. Please check the Observability eventlog on the host"
    }
}

function Uninstall-ArcForServerAgent
{
    param(
        [string]
        [parameter(Mandatory=$true)]
        $AgentMsiPath,

        [string]
        [Parameter(Mandatory=$true)]
        $AccessToken
    )
    
    # NOTE: This Add-Type will not work if called from TelemetryAndDiagnostics ARC extension
    $observabilityNugetPath = Get-ASArtifactPath -NugetName "Microsoft.AzureStack.Observability.ObservabilityDeployment"
    Add-Type -Path "$observabilityNugetPath\lib\net472\Microsoft.AzureStack.Observability.ObservabilityCommon.dll"

    $timestamp = [DateTime]::Now.ToString("yyyyMMdd-HHmmss")
    $logPath = (New-Item -Path "$env:LocalRootFolderPath" -ItemType Directory -Force).FullName
    $logFile = Join-Path -Path $logPath -ChildPath "ArcForServerUninstall_${timestamp}.txt"

    $AgentMsiPath = Join-Path -Path $AgentMsiPath -ChildPath $ObservabilityConfigConstants.ArcForServerMsiFileName
    $programFiles = [Environment]::GetFolderPath([System.Environment+SpecialFolder]::ProgramFiles)
    $AgentExePath = Join-Path -Path $programFiles -ChildPath $ObservabilityConfigConstants.ArcForServerExePath

    Write-ObservabilityLog "Starting Arc-for-server agent uninstall on $env:COMPUTERNAME. AgentMsiPath: $AgentMsiPath AgentExePath: $AgentExePath logs: $logFile"
    $arcContext = New-Object Microsoft.AzureStack.Observability.ObservabilityCommon.ArcForServer.ArcContext
    $arcContext.AccessToken = $AccessToken

    Write-ObservabilityLog "AgentMsiPath: $AgentMsiPath AgentExePath: $AgentExePath"
    $arcAgent = New-Object Microsoft.AzureStack.Observability.ObservabilityCommon.ArcForServer.ArcAgent
    $res = $arcAgent.Offboard($arcContext, $logFile, $AgentExePath, $AgentMsiPath)
    Write-ObservabilityLog "Arc-for-server agent uninstall $env:COMPUTERNAME. Status $res"

    return $res
}

function Install-ExtensionPreRequisites
{
    Write-ObservabilityLog "$env:COMPUTERNAME Installing pre-requisites for Arc-for-server extension."

    try
    {
        Write-Verbose -Message "$env:COMPUTERNAME Installing NuGet package provider." -Verbose
        Install-PackageProvider -Name NuGet -Force

        Write-Verbose -Message "$env:COMPUTERNAME Installing PowershellGet." -Verbose
        Install-Module -Name PowershellGet -Force -Confirm:$false -SkipPublisherCheck

        if(-not (Get-Module -Name "Az.Accounts" -ListAvailable))
        {
            Write-Verbose "[$commandName] Installing Az.Accounts module"
            Install-Module "Az.Accounts" -Force
        }

        if(-not (Get-Module -Name "Az.ConnectedMachine" -ListAvailable))
        {
            Write-Verbose "[$commandName] Installing Az.ConnectedMachine module"
            Install-Module "Az.ConnectedMachine" -Force
        }
    }
    catch
    {
        Write-ObservabilityErrorLog "$env:COMPUTERNAME Failed to install pre-requisites for Obervability Extension Exception: $_"
        throw $_
    }

    Write-ObservabilityLog "$env:COMPUTERNAME Finished pre-requisites Arc-for-server extension."
}

function Install-ArcForServerExtensions
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $AccessToken,

        [string]
        [Parameter(Mandatory=$true)]
        $SubscriptionId,

        [string]
        [Parameter(Mandatory=$true)]
        $TenantId,

        [string]
        [Parameter(Mandatory=$true)]
        $AccountId,

        [string]
        [Parameter(Mandatory=$true)]
        $ResourceGroupName,

        [string]
        [Parameter(Mandatory=$true)]
        $ResourceName,

        [string]
        [Parameter(Mandatory=$true)]
        $EnvironmentName,

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

    Write-ObservabilityLog "$env:COMPUTERNAME Starting Arc-for-server extension install."

    Write-ObservabilityLog "$env:COMPUTERNAME Install required modules."
    Install-ExtensionPreRequisites

    Import-Module Az.Accounts -Force
    Import-Module Az.ConnectedMachine -Force

    Write-ObservabilityLog "$env:COMPUTERNAME Authenticating to Azure with Access token. $($AccessToken.Length)"
    Connect-AzAccount -AccessToken $AccessToken -SubscriptionId $SubscriptionId -AccountId $AccountId

    $publisher = $ObservabilityConfigConstants.ExtensionPublisher
    $type = $ObservabilityConfigConstants.ExtensionType

    Write-ObservabilityLog "$env:COMPUTERNAME Checking if Observability extension is already installed."
    $extension = Get-AzConnectedMachineExtension -Name $type `
                                    -ResourceGroupName $ResourceGroupName `
                                    -MachineName $ResourceName `
                                    -ErrorAction SilentlyContinue
    # Install the extension only if it not present or publisher doesnt match
    if($null -eq $extension -or $extension.Publisher -ne $publisher)
    {
        Write-ObservabilityLog "$env:COMPUTERNAME Going to install Observability extension."
        New-AzConnectedMachineExtension -Name $type `
                                        -ResourceGroupName $ResourceGroupName `
                                        -MachineName $ResourceName `
                                        -Location $Region `
                                        -Publisher $publisher `
                                        -ExtensionType $type `
                                        -ErrorAction Stop

        Write-ObservabilityLog "$env:COMPUTERNAME Checking the installed Observability extension version."
        $extension = Get-AzConnectedMachineExtension -Name $type `
                                        -ResourceGroupName $ResourceGroupName `
                                        -MachineName $ResourceName `
                                        -ErrorAction SilentlyContinue
        if($null -eq $extension -or $extension.ProvisioningState -ne "Succeeded" -or $extension.Publisher -ne $publisher)
        {
            throw "$env:COMPUTERNAME extension installation failed. $($extension.Name) ProvisioningState: $($extension.ProvisioningState) Publisher: $($extension.Publisher)"
        }
    }

    # Fail the interface if extension is not found in good state
    if($null -eq $extension -or $extension.ProvisioningState -ne "Succeeded" -or $extension.Publisher -ne $publisher)
    {
        throw "$env:COMPUTERNAME Extension installation found failed. $($extension.Name) ProvisioningState: $($extension.ProvisioningState) Publisher: $($extension.Publisher)"
    }

    Write-ObservabilityLog "$env:COMPUTERNAME Finished Arc-for-server extension install. Status $($extension.ProvisioningState)"
}

function Uninstall-ArcForServerExtensions
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $AccessToken,

        [string]
        [Parameter(Mandatory=$true)]
        $SubscriptionId,

        [string]
        [Parameter(Mandatory=$true)]
        $AccountId,

        [string]
        [Parameter(Mandatory=$true)]
        $ResourceGroupName,

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

    Write-ObservabilityLog "$env:COMPUTERNAME Arc-for-server agent extension uninstall start"

    Import-Module Az.Accounts -Force
    Import-Module Az.ConnectedMachine -Force

    $ResourceName = $env:COMPUTERNAME + "-" + $CloudId
    Write-ObservabilityLog "$env:COMPUTERNAME Authenticating to Azure with Access token."
    Connect-AzAccount -AccessToken $AccessToken -SubscriptionId $SubscriptionId -AccountId $AccountId
    $extensionName = $ObservabilityConfigConstants.ExtensionType

    Write-ObservabilityLog "$env:COMPUTERNAME Checking if $extensionName extension is already installed on resource $ResourceName."
    $extension = Get-AzConnectedMachineExtension -Name $extensionName `
                                    -ResourceGroupName $ResourceGroupName `
                                    -MachineName $ResourceName `
                                    -ErrorAction SilentlyContinue

    if($null -ne $extension)
    {
        Write-ObservabilityLog "$env:COMPUTERNAME Starting $extensionName extension uninstallation on resource $ResourceName"
        Remove-AzConnectedMachineExtension -MachineName $ResourceName `
                                           -Name $extensionName `
                                           -ResourceGroupName $ResourceGroupName `
                                           -SubscriptionId $SubscriptionId

        Write-ObservabilityLog "$env:COMPUTERNAME Checking if $extensionName extension if it is uninstalled on resource $ResourceName."
        $extension = Get-AzConnectedMachineExtension -Name $extensionName `
                                        -ResourceGroupName $ResourceGroupName `
                                        -MachineName $ResourceName `
                                        -ErrorAction SilentlyContinue
        if($null -ne $extension)
        {
            throw "$env:COMPUTERNAME $extensionName extension uninstallation failed. Name: $($extension.Name) ProvisioningState: $($extension.ProvisioningState) Publisher: $($extension.Publisher)"
        }
    }

    Write-ObservabilityLog "$env:COMPUTERNAME Arc-for-server agent extension uninstall complete"
}

function Invoke-CachedTelemetryFilesParsing
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $LogOrchestratorNugetPath,

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

    $result = $false
    try
    {
        $transcriptFileName = "{0}.{1:yyyy-MM-dd}.log" -f "Invoke-CachedTelemetryFilesParsing", $(Get-Date)
        $transcriptFilePath = Join-Path -Path "$env:LocalRootFolderPath" -ChildPath $transcriptFileName
        Start-Transcript -Path $transcriptFilePath -Append | Out-Null

        Write-ObservabilityLog "$env:COMPUTERNAME Cached Telemetry Files Parsing start"

        $parsingEnginePath = Join-Path -Path $LogOrchestratorNugetPath -ChildPath $ObservabilityConfigConstants.ParsingEngineInternalPath
        $configFilePath = Join-Path -Path $observabilityNugetPath -ChildPath $ObservabilityConfigConstants.TelemetryCacheConfigurationInternalPath
        Write-ObservabilityLog "$env:COMPUTERNAME CachedTelemetryFilesParsing parsingEnginePath: $parsingEnginePath configFilePath: $configFilePath"

        $telemetryCompnentsCache = Get-Content $configFilePath | ConvertFrom-Json

        Write-ObservabilityLog "$env:COMPUTERNAME Found Components count $($telemetryCompnentsCache.Components) from $configFilePath."
        foreach($component in $telemetryCompnentsCache.Components)
        {
            $cacheLogPath = $component.CachePath
            $exceptionMsg = ""
            $pathFound = $false
            try
            {
                Write-ObservabilityLog "$env:COMPUTERNAME Processing Telemetry cache for Component Name: $($component.Name) RegistryPath: $($component.RegistryPath) Path $cacheLogPath."
                if(-not [string]::IsNullOrEmpty($component.RegistryPath))
                {
                    Write-ObservabilityLog "$env:COMPUTERNAME Going to resolve registry path for Component Name: $($component.Name) RegistryPath: $($component.RegistryPath)."
                    $registryProperty = Get-ItemProperty -Path $ObservabilityConfigConstants.TelemetryRegistryPath -Name $component.RegistryPath -ErrorAction SilentlyContinue
                    if($registryProperty)
                    {
                        $registryLogPath = $registryProperty.$($component.RegistryPath)
                    }

                    Write-ObservabilityLog "$env:COMPUTERNAME Going to resolve registry path for Component Name: $($component.Name) RegistryPath: $($component.RegistryPath) Resolved Path $registryLogPath."
                    $cacheLogPath = $registryLogPath
                }

                Write-CachedTelemetryParsingStartTelemetry -ComponentName $component.Name -TelemetryCachePath $cacheLogPath -RegistryPath $component.RegistryPath
                Write-ObservabilityLog "$env:COMPUTERNAME Processing Telemetry cache for Component Name: $($component.Name) Path: $cacheLogPath."
                $cachePathJson = ConvertTo-Json -InputObject $cacheLogPath
                $parsingEngineArg = 'IngestionInfo:{"GenerateTelemetryEvents":"true","LogDirectoryPath":' + $cachePathJson + ',"TracingContext":"' + $CorrelationId + '"}'

                $pathFound = if (($cacheLogPath) -and (Test-Path -Path $cacheLogPath) ) { $true } else { $false }
                if($pathFound)
                {
                    $currentDir = [System.IO.Path]::GetDirectoryName($parsingEnginePath)
                    Write-ObservabilityLog "$env:COMPUTERNAME Going to start ParsingEngine $parsingEnginePath with Arguments: $parsingEngineArg CurrentDirectory: $currentDir."
                    Start-Process -FilePath $parsingEnginePath -NoNewWindow -Wait -ArgumentList $parsingEngineArg -Passthru -WorkingDirectory $currentDir
                    Write-ObservabilityLog "$env:COMPUTERNAME ParsingEngine finished execution for component $($component.Name) Going to read result file from $currentDir"

                    $resultFilePath = Join-Path -Path $currentDir -ChildPath $ObservabilityConfigConstants.ParsingEngineResultFileName
                    Write-ObservabilityLog "$env:COMPUTERNAME ParsingEngine execution result file for component $component is $resultFilePath"
                    $parsingEngineResultStr = Get-Content $resultFilePath
                    Write-ObservabilityLog "$env:COMPUTERNAME ParsingEngine execution result for component $($component.Name) is $parsingEngineResultStr"
                    $parsingEngineResult = $parsingEngineResultStr | ConvertFrom-Json
                    $result = $parsingEngineResult.Failed -eq $false
                }
                else
                {
                    $result = $true
                    Write-ObservabilityLog "$env:COMPUTERNAME Component $($component.Name) Telemetry cache path $cacheLogPath not found. So skipping parsing"
                }
            }
            catch
            {
                $exceptionMsg = $_.Exception.Message.ToString()
            }
            finally
            {
                Write-CachedTelemetryParsingStopTelemetry -ComponentName $component.Name `
                                                          -TelemetryCachePath $cacheLogPath `
                                                          -RegistryPath $component.RegistryPath `
                                                          -PathFound $pathFound `
                                                          -ParserResult $parsingEngineResultStr `
                                                          -ExceptionDetails $exceptionMsg
            }
        }
    }
    finally
    {
        Stop-Transcript | Out-Null
        Write-ObservabilityLog "$env:COMPUTERNAME Cached Telemetry Files Parsing end Result: $result"
    }

    return $result
}

function Write-ArcForServerInstallationStartTelemetry
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $ComputerNames
    )

    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.ArcForServerInstallationStart(
        $ComputerNames
    )
    $info = @"
        Starting Arc-for-Server agent installation
        ComputerNames: $ComputerNames
"@

    Write-ObservabilityLog -Message $info
}

function Write-ArcForServerInstallationStopTelemetry
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $ComputerName,

        [string]
        [Parameter(Mandatory=$true)]
        $Message,

        [string]
        [Parameter(Mandatory=$false)]
        $ExceptionDetails = ""
    )

    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.ArcForServerInstallationStop($ComputerName, $Message, $ExceptionDetails)

    if($ExceptionDetails)
    {
        Write-ObservabilityErrorLog -Message $Message
    }
    else
    {
        Write-ObservabilityLog -Message $Message
    }
}

function Get-Sha256Hash
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $ClearString
    )

    $hasher = [System.Security.Cryptography.HashAlgorithm]::Create('sha256')
    $hash = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($ClearString))
    $hashString = [System.BitConverter]::ToString($hash)
    $hashString = $hashString.Replace('-', '')
    return $hashString
}

function Get-HardwareIdHash
{
    $computerInfo = Get-ComputerInfo
    $manufacturer = $computerInfo.CsManufacturer
    $model = $computerInfo.CsModel
    $serialNumber = $computerInfo.BiosSeralNumber
    return (Get-Sha256Hash -ClearString "$manufacturer-$model-$serialNumber").toLower()
}

function Set-GMAScenarioRegistryKeyToBootstrap
{
    # Import GMATenantJsonHelper
    $gmaPackageContentPath = Join-Path $(Get-ASArtifactPath -NugetName "Microsoft.AzureStack.Observability.GenevaMonitoringAgent") -ChildPath "content"
    Import-Module "$gmaPackageContentPath\GMATenantJsonHelper.psm1" -DisableNameChecking

    # $MiscConstants imported from GMATenantJsonHelper
    $gmaScenarioRegKeyPath = $MiscConstants.GMAScenarioRegKey.Path
    $gmaScenarioRegKeyName = $MiscConstants.GMAScenarioRegKey.Name
    $gmaScenarioBootstrapValue = $MiscConstants.GMAScenarioRegKey.Bootstrap
    $gmaScenarioRegKeyPropertyType = $MiscConstants.GMAScenarioRegKey.PropertyType
    if(-not (Test-Path $gmaScenarioRegKeyPath))
    {
        Write-ObservabilityLog "Creating GMAScenario registry path at $gmaScenarioRegKeyPath"
        New-Item -Path $gmaScenarioRegKeyPath -Force
    }
    New-ItemProperty `
        -Path $gmaScenarioRegKeyPath `
        -Name $gmaScenarioRegKeyName `
        -PropertyType $gmaScenarioRegKeyPropertyType `
        -Value $gmaScenarioBootstrapValue `
        -Force
    Write-ObservabilityLog "Set $gmaScenarioRegKeyName at $gmaScenarioRegKeyPath to $gmaScenarioBootstrapValue"
}

function Write-CachedTelemetryParsingStartTelemetry
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $ComponentName,

        [string]
        [Parameter(Mandatory=$false)]
        $TelemetryCachePath,

        [string]
        [Parameter(Mandatory=$false)]
        $RegistryPath

    )

    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.CachedTelemetryParsingStart(
        $ComponentName,
        $TelemetryCachePath
    )
    $info = @"
        Starting Cached Telemetry file parsing
        ComponentName: $ComponentName
        TelemetryCachePath: $TelemetryCachePath
        RegistryPath: $RegistryPath
"@

    Write-ObservabilityLog -Message $info
}

function Write-CachedTelemetryParsingStopTelemetry
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $ComponentName,

        [string]
        [Parameter(Mandatory=$false)]
        $TelemetryCachePath,

        [string]
        [Parameter(Mandatory=$false)]
        $RegistryPath,

        [bool]
        [Parameter(Mandatory=$true)]
        $PathFound,

        [string]
        [Parameter(Mandatory=$false)]
        $ParserResult,

        [string]
        [Parameter(Mandatory=$false)]
        $ExceptionDetails = ""
    )

    [Microsoft.AzureStack.Observability.ObservabilityConfigTelemetryEventSource]::Log.CachedTelemetryParsingStop($ComponentName, $TelemetryCachePath, $PathFound, $ParserResult, $ExceptionDetails)

        $info = @"
        Finished Cached Telemetry file parsing
        ComponentName: $ComponentName
        TelemetryCachePath: $TelemetryCachePath
        RegistryPath: $RegistryPath
        PathFound: $PathFound
        ParserResult: $ParserResult
        ExceptionDetails: $ExceptionDetails
"@


    if($ExceptionDetails)
    {
        Write-ObservabilityErrorLog -Message $info
    }
    else
    {
        Write-ObservabilityLog -Message $info
    }
}

function Get-ArcResourceId
{
    param(
        [string]
        [Parameter(Mandatory=$true)]
        $SubscriptionId,

        [string]
        [Parameter(Mandatory=$true)]
        $ResourceGroupName,

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

    $resourceName = Get-ArcResourceName

    Write-ObservabilityLog "Generating Arc resource id with SubscriptionId $SubscriptionId ResourceGroupName $ResourceGroupName ResourceName $resourceName"

    $arcResourceIdFormat = $ObservabilityConfigConstants.ArcForServerResourceIdFormat
    $arcResourceId = [string]::Format($arcResourceIdFormat, $SubscriptionId, $ResourceGroupName, $resourceName)
    Write-ObservabilityLog "Get-ArcResourceId returning $arcResourceId"

    return $arcResourceId
}

function Get-ArcResourceName
{
    # Resouce name has to be host name for device registration to work
    $resourceName = hostname
    Write-ObservabilityLog "Get-ArcResourceName returning $resourceName"

    return $resourceName
}

Export-ModuleMember -Function Add-MountVolumeScheduledTask
Export-ModuleMember -Function Add-ObservabilityVolumeWithRetry
Export-ModuleMember -Function Add-VolumeAccessPath
Export-ModuleMember -Function Add-VolumeFolderQuotaStatisticsScheduledTask
Export-ModuleMember -Function Get-ObservabilityFolders
Export-ModuleMember -Function Get-Sha256Hash
Export-ModuleMember -Function Remove-ObservabilityFolderOldestFiles
Export-ModuleMember -Function Set-FolderQuotas
Export-ModuleMember -Function Set-GMAScenarioRegistryKeyToBootstrap
Export-ModuleMember -Function New-VolumeFoldersAndPrunerWithRetry
Export-ModuleMember -Function Write-ObservabilityErrorLog
Export-ModuleMember -Function Write-ObservabilityLog
Export-ModuleMember -Function Write-BootstrapNodeIdAndHardwareIdHashTelemetry
Export-ModuleMember -Function Write-ObservabilityVolumeCreationStartTelemetry
Export-ModuleMember -Function Write-ObservabilityVolumeCreationStopTelemetry
Export-ModuleMember -Function Write-ObservabilityVolumeMountedByScheduledTaskTelemetry
Export-ModuleMember -Function Write-ObservabilityVolumePrunerFileDeletionTelemetry
Export-ModuleMember -Function Install-ArcForServerAgent
Export-ModuleMember -Function Uninstall-ArcForServerAgent
Export-ModuleMember -Function Uninstall-ArcForServerExtensions
Export-ModuleMember -Function Install-ArcForServerExtensions
Export-ModuleMember -Function Write-ArcForServerInstallationStartTelemetry
Export-ModuleMember -Function Write-ArcForServerInstallationStopTelemetry
Export-ModuleMember -Function Get-ArcResourceId
Export-ModuleMember -Function Get-ArcResourceName
Export-ModuleMember -Function Invoke-CachedTelemetryFilesParsing
# SIG # Begin signature block
# MIInpAYJKoZIhvcNAQcCoIInlTCCJ5ECAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD4mWnO5oE4UfbN
# x6dE0j2v5GtOLZihJvlpy3/LCzeezKCCDYUwggYDMIID66ADAgECAhMzAAADTU6R
# phoosHiPAAAAAANNMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI4WhcNMjQwMzE0MTg0MzI4WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDUKPcKGVa6cboGQU03ONbUKyl4WpH6Q2Xo9cP3RhXTOa6C6THltd2RfnjlUQG+
# Mwoy93iGmGKEMF/jyO2XdiwMP427j90C/PMY/d5vY31sx+udtbif7GCJ7jJ1vLzd
# j28zV4r0FGG6yEv+tUNelTIsFmmSb0FUiJtU4r5sfCThvg8dI/F9Hh6xMZoVti+k
# bVla+hlG8bf4s00VTw4uAZhjGTFCYFRytKJ3/mteg2qnwvHDOgV7QSdV5dWdd0+x
# zcuG0qgd3oCCAjH8ZmjmowkHUe4dUmbcZfXsgWlOfc6DG7JS+DeJak1DvabamYqH
# g1AUeZ0+skpkwrKwXTFwBRltAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUId2Img2Sp05U6XI04jli2KohL+8w
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwMDUxNzAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# ACMET8WuzLrDwexuTUZe9v2xrW8WGUPRQVmyJ1b/BzKYBZ5aU4Qvh5LzZe9jOExD
# YUlKb/Y73lqIIfUcEO/6W3b+7t1P9m9M1xPrZv5cfnSCguooPDq4rQe/iCdNDwHT
# 6XYW6yetxTJMOo4tUDbSS0YiZr7Mab2wkjgNFa0jRFheS9daTS1oJ/z5bNlGinxq
# 2v8azSP/GcH/t8eTrHQfcax3WbPELoGHIbryrSUaOCphsnCNUqUN5FbEMlat5MuY
# 94rGMJnq1IEd6S8ngK6C8E9SWpGEO3NDa0NlAViorpGfI0NYIbdynyOB846aWAjN
# fgThIcdzdWFvAl/6ktWXLETn8u/lYQyWGmul3yz+w06puIPD9p4KPiWBkCesKDHv
# XLrT3BbLZ8dKqSOV8DtzLFAfc9qAsNiG8EoathluJBsbyFbpebadKlErFidAX8KE
# usk8htHqiSkNxydamL/tKfx3V/vDAoQE59ysv4r3pE+zdyfMairvkFNNw7cPn1kH
# Gcww9dFSY2QwAxhMzmoM0G+M+YvBnBu5wjfxNrMRilRbxM6Cj9hKFh0YTwba6M7z
# ntHHpX3d+nabjFm/TnMRROOgIXJzYbzKKaO2g1kWeyG2QtvIR147zlrbQD4X10Ab
# rRg9CpwW7xYxywezj+iNAc+QmFzR94dzJkEPUSCJPsTFMIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGXUwghlxAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAANNTpGmGiiweI8AAAAA
# A00wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIBh/
# s5fzwaSmSBFVEmJVDeQnrglI3dBjYZJuIBq273scMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEASmmD7jROw6su+SC4RRMx2shkB9x0SAllR+jB
# kMoSN2ibbQU/VBeKSDQQ3YZzbvJPI55SSG/VFozTjLoK/3ByoqUT/FLIdMTwv0JL
# YjE6w+I+Sv4zz91vzdoqizRxhHOxnc0kzhv6KulHJ73kcUik55Lo+FNj4LnBuqQ/
# nDueT4LGgGno2XIKTpSm/UcDbiRhiX9mwP14J4t2bJa7hKQaot6HKDgeXflBf/Fj
# fK9kOVuCwh2Ubumf5AKTsa2MSJia2l4RX9sUZH1zLt5Q9ZcUl/OYanzUqiIdbGVo
# SdhBF6A01P6qDP5g4IcqkMm+7RW/HkkfrKAKISXpHFtay4mu1KGCFv8wghb7Bgor
# BgEEAYI3AwMBMYIW6zCCFucGCSqGSIb3DQEHAqCCFtgwghbUAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFRBgsqhkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCAf5CkHYNQoSUdOQnAFC/sV5ernGS/Mcv2z
# KJYZxY1L+wIGZK/4evteGBMyMDIzMDcxMzE5MjU0Ni40NDdaMASAAgH0oIHQpIHN
# MIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL
# ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjpENkJELUUzRTctMTY4NTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCEVYwggcMMIIE9KADAgECAhMzAAABx/sAoEpb8ifcAAEA
# AAHHMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIyMTEwNDE5MDEzNVoXDTI0MDIwMjE5MDEzNVowgcoxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVy
# aWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkQ2QkQtRTNF
# Ny0xNjg1MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIC
# IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr0LcVtnatNFMBrQTtG9P8ISA
# PyyGmxNfhEzaOVlt088pBUFAIasmN/eOijE6Ucaf3c2bVnN/02ih0smSqYkm5P3Z
# wU7ZW202b6cPDJjXcrjJj0qfnuccBtE3WU0vZ8CiQD7qrKxeF8YBNcS+PVtvsqhd
# 5YW6AwhWqhjw1mYuLetF5b6aPif/3RzlyqG3SV7QPiSJends7gG435Rsy1HJ4Xnq
# ztOJR41I0j3EQ05JMF5QNRi7kT6vXTT+MHVj27FVQ7bef/U+2EAbFj2X2AOWbvgl
# YaYnM3m/I/OWDHUgGw8KIdsDh3W1eusnF2D7oenGgtahs+S1G5Uolf5ESg/9Z+38
# rhQwLgokY5k6p8k5arYWtszdJK6JiIRl843H74k7+QqlT2LbAQPq8ivQv0gdclW2
# aJun1KrW+v52R3vAHCOtbUmxvD1eNGHqGqLagtlq9UFXKXuXnqXJqruCYmfwdFMD
# 0UP6ii1lFdeKL87PdjdAwyCiVcCEoLnvDzyvjNjxtkTdz6R4yF1N/X4PSQH4Flgs
# lyBIXggaSlPtvPuxAtuac/ITj4k0IRShGiYLBM2Dw6oesLOoxe07OUPO+qXXOcJM
# VHhE0MlhhnxfN2B1JWFPWwQ6ooWiqAOQDqzcDx+79shxA1Cx0K70eOBplMog27gY
# oLpBv7nRz4tHqoTyvA0CAwEAAaOCATYwggEyMB0GA1UdDgQWBBQFUNLdHD7BAF/V
# U/X/eEHLiUSSIDAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNV
# HR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Ny
# bC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYI
# KwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAy
# MDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0G
# CSqGSIb3DQEBCwUAA4ICAQDQy5c8ogP0y8xAsLVca07wWy1mT+nqYgAFnz2972kN
# O+KJ7AE4f+SVbvOnkeeuOPq3xc+6TS8g3FuKKYEwYqvnRHxX58tjlscZsZeKnu7f
# GNUlpNT9bOQFHWALURuoXp8TLHhxj3PEq9jzFYBP2YNMLol70ojY1qpze3nMMJfp
# durdBBpaOLlJmRNTLhxd+RJGJQbY1XAcx6p/FigwqBasSDUxp+0yFPEBB9uBE3KI
# LAtq6fczGp4EMeon6YmkyCGAtXMKDFQQgdP/ITe7VghAVbPTVlP3hY1dFgc+t8YK
# 2obFSFVKslkASATDHulCMht+WrIsukclEUP9DaMmpq7S0RLODMicI6PtqqGOhdna
# RltA0d+Wf+0tPt9SUVtrPJyO7WMPKbykCRXzmHK06zr0kn1YiUYNXCsOgaHF5ImO
# 2ZwQ54UE1I55jjUdldyjy/UPJgxRm9NyXeO7adYr8K8f6Q2nPF0vWqFG7ewwaAl5
# ClKerzshfhB8zujVR0d1Ra7Z01lnXYhWuPqVZayFl7JHr6i6huhpU6BQ6/VgY0cB
# iksX4mNM+ISY81T1RYt7fWATNu/zkjINczipzbfg5S+3fCAo8gVB6+6A5L0vBg39
# dsFITv6MWJuQ8ZZy7fwlFBZE4d5IFbRudakNwKGdyLGM2otaNq7wm3ku7x41UGAm
# kDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQEL
# BQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNV
# BAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4X
# DTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh
# bXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM
# 57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm
# 95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzB
# RMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBb
# fowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCO
# Mcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYw
# XE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW
# /aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/w
# EPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPK
# Z6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2
# BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfH
# CBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYB
# BAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8v
# BO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYM
# KwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0
# LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEF
# BQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBW
# BgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUH
# AQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtp
# L2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsF
# AAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518Jx
# Nj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+
# iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2
# pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefw
# C2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7
# T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFO
# Ry3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhL
# mm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3L
# wUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5
# m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE
# 0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggLNMIICNgIB
# ATCB+KGB0KSBzTCByjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UE
# CxMdVGhhbGVzIFRTUyBFU046RDZCRC1FM0U3LTE2ODUxJTAjBgNVBAMTHE1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAOIASP0JSbv5
# R23wxciQivHyckYooIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTAwDQYJKoZIhvcNAQEFBQACBQDoWnbzMCIYDzIwMjMwNzEzMjExMzIzWhgPMjAy
# MzA3MTQyMTEzMjNaMHYwPAYKKwYBBAGEWQoEATEuMCwwCgIFAOhadvMCAQAwCQIB
# AAIBEQIB/zAHAgEAAgIRyDAKAgUA6FvIcwIBADA2BgorBgEEAYRZCgQCMSgwJjAM
# BgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEB
# BQUAA4GBAB1FSRPydnvEFFfS60yPFo/F0snKBYdo/suq0DveDx1PND4wIaakYkeE
# 5Z92fDfIwBpb0ozlzxDuc1Xb15Zy05G/mE1iAYGL83U9jDVCUiGpB8vaU7mzeX+X
# +TNZd4Av+Lj9BJ+SpvihXCYrRqgG7DLLm9HF7Bvij0gGW1UhyW+RMYIEDTCCBAkC
# AQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAHH+wCgSlvy
# J9wAAQAAAccwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG
# 9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgs3iP4RRvH3QMySKzbHZTXdpCxMwwOrjO
# uyN9cgU41lkwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCBH5+Xb4lKyRQs5
# 5Vgtt4yCTsd0htESYCyPC1zLowmSyTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFBDQSAyMDEwAhMzAAABx/sAoEpb8ifcAAEAAAHHMCIEIHgkbe5TwCZ1EPqy
# IWn3w1iAUlqH9tQPaST/ZdO6j7HQMA0GCSqGSIb3DQEBCwUABIICAHPnLNRKwxJe
# dgceu3AtkwjMszI4NCgCxqzwL+8RZ4ppNqS37GnwRYHpODCbJ3TqBFrMqUUVRly9
# uhvNhumEP7tMYZGFvze/5qBwbLhJ6BSGrQlwqIOdlYDYg/DytM1r+iJTaqeKwKPs
# jUp0HBGmdd79OWrRzCJLmWzJrWDLrEGJKPoGZ0csXlcy2kE7zWkVIbV/vQOLZoyX
# O6S6uq7obd5dz+lFeMF2tYUTfMg2pvfl2CD/jro6Z/EbXW1MasvkI9vlpDGzGk2L
# hYYb+HqHazolHxKe6nwImtSNc6SCIwAXy/lvcXKbv6xA1zXT4GTx/RYBw3tOxnJm
# LclP5DssG3XwqYdrvdqNjLKvV38wPI1Y591T1K3hEw51cKZru9lFrBhtYSvtmYkm
# 0O2HFpX8ZmB+YQf9q9x9guqTVxyYjsfe4g5XU9rqFYv7nvUKF/i5O5Y7e42amTWe
# SrFFhyFcyaZ6Rf6cycrLnFKCAMPBHwkV393MLvwgSejIz7rBzPzwTQ5fmJxbQhQZ
# EN0EPkxD6opsOgh1Lw2i69gidKdCQI1JDEOxbVX3m/xHSYBN0HJ084JzqEhC6zqB
# 0VvT8sdlOrtva8G3uoVAT87kxnV1OVdqPX0kVdvHf/HhcwIs1ndBVSIoylkK+Ova
# nDhY2dHO6xCZS3RofRBWvpr6Y+lECbFH
# SIG # End signature block