WinfieldFallbackLogging.psm1

# Copyright (c) Microsoft Corporation. All rights reserved.
# WinfieldFallbackLogging.psm1 0.99.0 2023-08-12 16:46:57
# ASZ-ArcA-Deploy main Debug-x64

<#
.SYNOPSIS
    Sends diagnostics data using standalone observability pipeline. Intended for use in cases where
    standard log collection is unavailable.
.DESCRIPTION
    Accepts a directory location (DiagnosticLogPath) from which logs are sent via the standalone
    observability pipeline to be ingested into Kusto.
.PARAMETER ResourceGroupName
    Azure Resource group name where temporary Arc resource will be created. This can be same
    parameter as used in AzS Deployment.
.PARAMETER SubscriptionId
    Azure SubscriptionID where temporary Arc resource will be created. This can be same parameter
    as used in AzS Deployment.
.PARAMETER TenantId
    Azure TenantID where temporary Arc resource will be created. This can be same parameter as used
    in AzS Deployment.
.PARAMETER RegistrationWithDeviceCode
    Switch to use device code for authentication. This is the default if Service Principal
    credentials (-RegistrationWithCredential {creds}) is not provided.
.PARAMETER RegistrationWithCredential
    Service Principal credentials used for authentication to register ArcAgent.
.PARAMETER DiagnosticLogPath
    Diagnostic log path which will be parsed and sent to Microsoft.
.PARAMETER ObsRootFolderPath
    Optional. Observability root folder path where logs are output.
    Default: ${env:USERPROFILE}\Observability (e.g. C:\Users\Administrator\Observability)
.PARAMETER StampId
    Optional. The value set for the AEOStampId GUID used for tracking collected logs in Kusto.
    Default: The AEOStampId used will default to the first of the following options applicable:
       1. The StampId (if provided)
       2. $env:STAMP_GUID (if set)
       3. The host machine's UUID
.EXAMPLE
    # Interactive registration with device code (used by default)
    Send-WinfieldDiagnosticData -ResourceGroupName "xxxxx" `
                                -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
                                -TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
                                -DiagnosticLogPath "G:\ArcA\ArcADeployment\logs"
.EXAMPLE
    # Interactive registration with device code (declared explicitly)
    Send-WinfieldDiagnosticData -ResourceGroupName "xxxxx" `
                                -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
                                -TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
                                -RegistrationWithDeviceCode `
                                -DiagnosticLogPath "G:\ArcA\ArcADeployment\logs"
.EXAMPLE
    # Registration with Service Principal Credential
    Send-WinfieldDiagnosticData -ResourceGroupName "xxxxx" `
                                -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
                                -TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
                                -RegistrationWithCredential {$credential} `
                                -DiagnosticLogPath "C:\ObservabilityDiagnosticLogs\LogsToExport"
.EXAMPLE
    # Get-ObservabilityStampId pipes the Observability Id as the 'StampId' if available.
    Get-ObservabilityStampId | Send-WinfieldDiagnosticData -ResourceGroupName "xxxxx" `
                                                           -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
                                                           -TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
                                                           -DiagnosticLogPath "C:\ObservabilityDiagnosticLogs\LogsToExport"
.EXAMPLE
    # Copy-WinfieldDiagnosticData pipes the 'DiagnosticLogPath' (always) and the Observability Id as the 'StampId' if available.
    Copy-WinfieldDiagnosticData | Send-WinfieldDiagnosticData -ResourceGroupName "xxxxx" `
                                                              -SubscriptionId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
                                                              -TenantId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
.NOTES
    Requires Support VM to have stable Internet connectivity and Azure Powershell installed:
       https://learn.microsoft.com/en-us/powershell/azure/install-azure-powershell?view=azps-10.1.0
    By default the AEOStampId used for tracking logs reported to Kusto will be set to the first of
    the following options applicable:
       1. StampId (if provided)
       2. $env:STAMP_GUID (if set)
       3. The host machine's UUID
    In order for helper functions 'Get-ObservabilityStampId' or 'Copy-WinfieldDiagnosticData' to
    successfully retrieve the Observability ID as the StampId value, Observability needs to be
    set-up on the IRVM prior to the function call. (Setting the AEOStampId to the Observability ID
    may be helpful as this ID will match any other logs that may have been reported as part of
    standard log collection.)
#>

function Send-WinfieldDiagnosticData {
    [CmdletBinding(PositionalBinding = $false, DefaultParameterSetName = "Interactive")]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String] $ResourceGroupName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String] $SubscriptionId,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String] $TenantId,

        [Parameter(Mandatory = $false, ParameterSetName = "Interactive")]
        [Switch] $RegistrationWithDeviceCode,

        [Parameter(Mandatory = $true, ParameterSetName = "ServicePrincipal")]
        [PSCredential] $RegistrationWithCredential,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path -Path $_ -PathType Container })]
        [System.String] $DiagnosticLogPath,

        [Parameter(Mandatory = $false)]
        [System.String] $ObsRootFolderPath = "${env:USERPROFILE}\Observability",

        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [System.Guid] $StampId
    )

    $ErrorActionPreference = "Stop"

    # Used as a safeguard for piped helper functions (standard parameter validation may not catch)
    if ([string]::IsNullOrEmpty($DiagnosticLogPath)) {
        $DiagnosticLogPath = Read-Host "DiagnosticLogPath is mandatory, please provide a value (a directory path, no quotes)"
        if (-not (Test-Path -Path $DiagnosticLogPath -PathType Container)) {
            throw "DiagnosticLogPath '$DiagnosticLogPath' does not resolve to a valid directory."
        }
    }

    $cloud = "AzureCloud"
    $registrationRegion = "eastus"

    Set-FallbackLoggingRegistryKey

    if ($PSBoundParameters.ContainsKey('StampId') -and ([System.Guid]::Empty -ne $StampId)) {
        $env:STAMP_GUID = $StampId
        Trace-Execution "Set `$env:STAMP_GUID to $env:STAMP_GUID. This will be used as the AEOStampID for log tracking in Kusto."
    }

    $package = Find-Package AzStackHci.EnvironmentChecker
    $observabilityStandalonePath = Join-Path -Path $ObsRootFolderPath -ChildPath "ObservabilityStandalone"
    Get-StandaloneObservabilityScripts `
        -DestinationPath $observabilityStandalonePath `
        -NugetName $package.Name `
        -NugetVersion $package.Version | Out-Null
    $standaloneScriptsPath = Join-Path -Path $observabilityStandalonePath -ChildPath "$($package.Name)\AzStackHciStandaloneObservability\package\scripts" -Resolve
    try {
        if ($PSCmdlet.ParameterSetName -eq "Interactive") {
            Trace-Execution "Installing the StandaloneObservability pipeline using interactive registration with device code..."
            & $standaloneScriptsPath\Install-StandaloneObservability.ps1 `
                -ResourceGroupName $ResourceGroupName `
                -SubscriptionId $SubscriptionId `
                -TenantId $TenantId `
                -Interactive `
                -FactoryLogShare $DiagnosticLogPath `
                -ObsRootFolderPath $ObsRootFolderPath `
                -Cloud $cloud `
                -RegistrationRegion $registrationRegion `
                -GcsRegion $registrationRegion -ParseOnce | Out-Null
        }
        else {
            Trace-Execution "Installing the StandaloneObservability pipeline using registration with service principal credentials..."
            & $standaloneScriptsPath\Install-StandaloneObservability.ps1 `
                -ResourceGroupName $ResourceGroupName `
                -SubscriptionId $SubscriptionId `
                -TenantId $TenantId `
                -RegistrationSPCredential $RegistrationWithCredential `
                -FactoryLogShare $DiagnosticLogPath `
                -ObsRootFolderPath $ObsRootFolderPath `
                -Cloud $cloud `
                -RegistrationRegion $registrationRegion `
                -GcsRegion $registrationRegion -ParseOnce | Out-Null
        }

        Wait-ShowProgress -TimeSpan (New-TimeSpan -Minutes 10)
    }
    catch {
        $exception = $_
        $errorMessage = @"
$OperationType failed with exception:
$exception
$($exception.ScriptStackTrace)
"@

        Write-Error $errorMessage
        throw $exception
    }
    finally {
        Trace-Execution "Uninstalling the StandaloneObservability pipeline..."
        if ($PSCmdlet.ParameterSetName -eq "ServicePrincipal") {
            & $standaloneScriptsPath\Uninstall-StandaloneObservability.ps1 `
                -RegistrationSPCredential $RegistrationWithCredential `
                -ErrorAction SilentlyContinue | Out-Null
        }
        else {
            $token = Get-AzAccessToken
            & $standaloneScriptsPath\Uninstall-StandaloneObservability.ps1 `
                -AccessToken $token.Token `
                -ErrorAction SilentlyContinue | Out-Null
        }

        Trace-Execution "StandaloneObservability uninstalled successfully."
        Remove-FallbackLoggingRegistryKey

        $aeoStampIdUsed = if ($null -eq $env:STAMP_GUID) { (Get-CimInstance -Class Win32_ComputerSystemProduct).UUID } else { $env:STAMP_GUID }
        $message = @"
Log reporting complete.
----------------------------------------------------------------------------------------------------------------------------
AEOStampID '$aeoStampIdUsed' used for log tracking in Kusto (registration region: '$registrationRegion').
 
To clean up log reporting artifacts, terminate this PowerShell session and in a new session run:
Remove-Item -Path "$ObsRootFolderPath" -Recurse -Force
----------------------------------------------------------------------------------------------------------------------------
"@

        Trace-Execution $message
    }
}

<#
.SYNOPSIS
    Copies diagnostic logs from the mounted IRVM to the given destination location.
.DESCRIPTION
    Stops the IRVM, mounts the VHDs and copies diagnostic logs from the mounted VHDs to the given
    destination location.
.PARAMETER DiagnosticLogPath
    Optional. Destination for copied logs.
    Default: "${env:USERPROFILE}\Observability\ObservabilityDiagnosticLogs"
.OUTPUTS
    A custom object: [DiagnosticLogPath = {value}] or, if the Observability Stamp Id is available,
    a custom object: [DiagnosticLogPath = {value}, StampId = {value}] where the DiagnosticLogPath
    value is the path to the directory containing the copied logs and the StampId value is the
    Observability Stamp Id (GUID) retrieved from the IRVM Knowledge Store.
.EXAMPLE
    Copy-WinfieldDiagnosticData
.EXAMPLE
    Copy-WinfieldDiagnosticData -DiagnosticLogPath "C:/my/custom/location"
#>

function Copy-WinfieldDiagnosticData {
    [OutputType([PSCustomObject])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [System.String] $DiagnosticLogPath = "${env:USERPROFILE}\Observability\ObservabilityDiagnosticLogs"
    )

    Invoke-StopIRVMAndMountVHDs
    [System.Collections.Hashtable] $driveLetterDict = Get-IRVMMountedVHDDriveLetterDictionary
    $obsStampId = Get-ObservabilityStampIdInternal -DriveLetterDictionary $driveLetterDict

    New-Item -Path $DiagnosticLogPath -ItemType Directory -Force | Out-Null
    $exportLogsPath = Join-Path -Path $DiagnosticLogPath -ChildPath "LogsToExport"
    $robocopyLogPath = Join-Path -Path $DiagnosticLogPath -ChildPath "RobocopyLog.log"
    Copy-IRVMMountedVHDLogs -DriveLetterDictionary $driveLetterDict -DestinationDirectory $exportLogsPath -RobocopyLogPath $robocopyLogPath
    Invoke-DismountVHDsAndStartIRVM

    # Return the nested folder location (LogsToExport) as the new DiagnosticLogPath
    # â"Å“â"€â"€ $DiagnosticLogPath (original input)
    # â"‚ â"Å“â"€â"€ LogsToExport ($exportLogsPath)
    # â"‚ â"‚ â"Å“â"€â"€ { logs collected from mounted IRVM VHDs }
    # â"‚ â"Å“â"€â"€ RobocopyLog.log (details of copy action)
    if ([System.Guid]::Empty -eq $obsStampId) {
        return [PSCustomObject]@{ DiagnosticLogPath = $exportLogsPath }
    }
    else {
        # And (if found) also return the observability stamp ID
        return [PSCustomObject]@{ DiagnosticLogPath = $exportLogsPath; StampId = $obsStampId }
    }
}

<#
.SYNOPSIS
    Gets the observability stamp ID if successful; otherwise returns null.
.DESCRIPTION
    Gets the observability stamp ID if successful; otherwise returns null.
.OUTPUTS
    The observability stamp ID (type GUID) in a custom object: [StampId = {GUID}] if successful;
    otherwise $null.
.EXAMPLE
    Get-ObservabilityStampId
.NOTES
    To be successful, Observability must have been setup on the IRVM prior to calling this function
    and the ID must be found in the Knowledge Store on the mounted IRVM.
#>

function Get-ObservabilityStampId {
    [OutputType([PSCustomObject])]
    [CmdletBinding()]
    param ()

    Invoke-StopIRVMAndMountVHDs
    [System.Collections.Hashtable] $driveLetterDict = Get-IRVMMountedVHDDriveLetterDictionary
    $obsStampId = Get-ObservabilityStampIdInternal -DriveLetterDictionary $driveLetterDict
    Invoke-DismountVHDsAndStartIRVM

    return [PSCustomObject]@{ StampId = $obsStampId }
}

<#
.SYNOPSIS
    Gets data required to set the fallback logging registry key.
.DESCRIPTION
    Gets data required to set the fallback logging registry key. This registry key is a flag for
    used in the standalone observability tool to enable log collection in the Winfield scenario.
#>

function Get-ArcARegKeyData {
    return @{
        Path         = 'HKLM:\Software\Microsoft\ArcA\'
        Name         = 'IsArcAEnv'
        PropertyType = 'DWORD'
        Value        = 1
    }
}

<#
.SYNOPSIS
    Downloads the AzStackHci.EnvironmentChecker NuGet from PSGallery to access the required
    StandaloneObservability scripts.
.DESCRIPTION
    Downloads the AzStackHci.EnvironmentChecker NuGet from PSGallery to use the contained
    StandaloneObservability scripts in \AzStackHciStandaloneObservability\package\scripts.
#>

function Get-StandaloneObservabilityScripts {
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DestinationPath,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String] $NugetName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String] $NugetVersion
    )

    New-Item -Path $DestinationPath -ItemType Directory -Force | Out-Null

    $nugetExtractedPath = Join-Path -Path $DestinationPath -ChildPath $NugetName
    if (Test-Path -Path "$nugetExtractedPath\AzStackHciStandaloneObservability\package\scripts") {
        Trace-Execution "Standalone Observability scripts already exist in $nugetExtractedPath, skipping download."
    }
    else {
        Install-Package -Source (Get-PSRepository).SourceLocation `
            -Name $NugetName `
            -RequiredVersion $NugetVersion `
            -ProviderName "nuget" `
            -Destination $DestinationPath -ExcludeVersion -Force -Verbose | Out-Null

        Trace-Execution "Successfully installed package $NugetName, version: $NugetVersion to '$nugetExtractedPath'."
    }
}

<#
.SYNOPSIS
    Sets the registry key required to enable fallback logging in the Winfield environment.
.DESCRIPTION
    Sets the registry key required to enable fallback logging in the Winfield environment.
#>

function Set-FallbackLoggingRegistryKey {
    $itemProperty = $null
    $arcARegKey = Get-ArcARegKeyData

    $totalRetries = 0
    $maxNumRetries = 3
    do {
        $totalRetries++
        if (!(Test-Path $arcARegKey.Path)) {
            New-Item -Path $arcARegKey.Path -Force | Out-Null
        }

        New-ItemProperty -Path $arcARegKey.Path `
            -Name $arcARegKey.Name `
            -PropertyType $arcARegKey.PropertyType `
            -Value $arcARegKey.Value -Force | Out-Null

        $itemProperty = Get-ItemProperty -Path $arcARegKey.Path -Name $arcARegKey.Name -ErrorAction SilentlyContinue
        if ($null -eq $itemProperty) {
            $failureMessage = "Failed to confirm fallback logging registry key is set on retry $totalRetries/$maxNumRetries."
            if ($totalRetries -lt $maxNumRetries) {
                $secondsSleepInterval = 10
                Write-Warning "$failureMessage Waiting $secondsSleepInterval seconds and retrying..."
                Start-Sleep -Seconds $secondsSleepInterval
            }
            else {
                throw $failureMessage
            }
        }
        else {
            Trace-Execution "Fallback logging registry key set. $(($itemProperty | Out-String).TrimEnd())"
        }
    } while (($null -eq $itemProperty) -and ($totalRetries -lt $maxNumRetries))
}

<#
.SYNOPSIS
    Removes the registry key required for enabling fallback logging in the Winfield environment.
.DESCRIPTION
    Removes the registry key required for enabling fallback logging in the Winfield environment.
#>

function Remove-FallbackLoggingRegistryKey {
    $arcARegKey = Get-ArcARegKeyData
    if (Test-Path $arcARegKey.Path) {
        Remove-Item -Path $arcARegKey.Path -Force -Verbose
        Trace-Execution "Removed fallback logging registry key."
    }
    else {
        Trace-Execution "Fallback logging registry key not found: no action needed."
    }
}

<#
.SYNOPSIS
    Waits for the given amount of time while displaying the % progress of elapsed time.
.DESCRIPTION
    Waits for the given amount of time while displaying the % progress of elapsed time.
#>

function Wait-ShowProgress {
    param ([TimeSpan] $TimeSpan)

    $totalSecondsToWait = $TimeSpan.TotalSeconds
    $message = "Waiting $($TimeSpan.ToString("mm\:ss")) minutes for MA to flush the cache folder..."
    Trace-Execution $message

    $secondsWaited = 0
    $secondsSleepInterval = 5
    Write-Progress -Activity $message -Status "0% Complete:" -PercentComplete 0
    while (($secondsWaited + $secondsSleepInterval) -le $totalSecondsToWait) {
        Start-Sleep -Seconds $secondsSleepInterval
        $secondsWaited += $secondsSleepInterval
        $progress = [System.Math]::Round(($secondsWaited / $totalSecondsToWait) * 100)
        Write-Progress -Activity $message -Status "$progress% Complete:" -PercentComplete $progress
    }

    Start-Sleep -Seconds ($totalSecondsToWait % $secondsSleepInterval)
    Write-Progress -Activity $message -Status "100% Complete:" -PercentComplete 100 -Completed
}

<#
.SYNOPSIS
    Stops the IRVM and mounts the VHDs.
.DESCRIPTION
    Stops the IRVM and mounts the VHDs.
#>

function Invoke-StopIRVMAndMountVHDs {
    if ((Get-VM -Name IRVM01).State -ne "Off") {
        Trace-Execution "Stopping the IRVM..."
        Stop-VM -Name IRVM01
    }
    else {
        Trace-Execution "IRVM is already in a stopped state."
    }

    $vhdxPaths = (Get-VM -VMName IRVM01 | Select-Object VMId | Get-VHD | Select-Object Path).Path
    foreach ($vhdxPath in $vhdxPaths) {
        Trace-Execution "Attempting to mount VHD '$vhdxPath'..."
        Mount-VHD -Path $vhdxPath
        if (!(Test-VHD -Path $vhdxPath)) {
            Write-Warning "VHD '$vhdxPath' is reported to be in a bad state."
        }
    }
}

<#
.SYNOPSIS
    Dismounts all mounted IRVM VHDs and attempts to start the IRVM.
.DESCRIPTION
    Dismounts all mounted IRVM VHDs and attempts to start the IRVM.
#>

function Invoke-DismountVHDsAndStartIRVM {
    $vhdxPaths = (Get-VM -VMName IRVM01 | Select-Object VMId | Get-VHD | Select-Object Path).Path
    foreach ($vhdxPath in $vhdxPaths) {
        Trace-Execution "Attempting to dismount VHD '$vhdxPath'..."
        Dismount-VHD -Path $vhdxPath
        if (!(Test-VHD -Path $vhdxPath)) {
            Write-Warning "VHD '$vhdxPath' is reported to be in a bad state."
        }
    }

    Trace-Execution "Starting the IRVM..."
    Start-VM -Name IRVM01

    $numRetries = 6
    $retryCount = 0
    $secondsWait = 5
    while (($retryCount -lt $numRetries) -and (Get-VM -Name IRVM01).State -ne 'Running') {
        Trace-Execution "Waiting $(($numRetries - $retryCount) * $secondsWait) seconds for the IRVM to be in running state..."
        Start-Sleep -Seconds $secondsWait
        $retryCount++
    }

    $irvm = Get-VM -Name IRVM01
    if ($irvm.State -eq 'Running') {
        Trace-Execution "IRVM started successfully."
    }
    else {
        Write-Warning "IRVM failed to start after $($numRetries * $secondsWait) seconds: $($irvm | Out-String)".Trim()
    }
}

<#
.SYNOPSIS
    Generates and returns a dictionary of IRVM mounted VHD volumes and assigned drive letters.
.DESCRIPTION
    Generates and returns a dictionary of IRVM mounted VHD volumes and assigned drive letters.
#>

function Get-IRVMMountedVHDDriveLetterDictionary {
    # Dictionary of mounted IRVM assigned drive letters
    [System.Collections.Hashtable] $driveLetterDict = @{}

    Trace-Execution "Retrieving available drive letters for mounted IRVM VHD volumes..."
    $vhdxPaths = (Get-VM -VMName IRVM01 | Select-Object VMId | Get-VHD | Select-Object Path).Path
    foreach ($vhdxPath in $vhdxPaths) {
        $vhdVolumes = Get-VHD -Path $vhdxPath | Get-Disk | Get-Partition | Get-Volume
        if ((Split-Path $vhdxPath -Leaf).StartsWith("IRVM01")) {
            # The IRVM01{...}.vhdx does not have a file system label, assigning it 'IRVM01'.
            # The only other volume in this VHD is FileSystemType FAT32 which is not used for log collection.
            $vhdVolume = $vhdVolumes | Where-Object { $_.FileSystemType -like "NTFS" -and [string]::IsNullOrWhiteSpace($_.FileSystemLabel) }
            Trace-Execution "Adding drive '$($vhdVolume.DriveLetter)' under file system label 'IRVM01'"
            $driveLetterDict.Add("IRVM01", $vhdVolume.DriveLetter)
        }
        else {
            foreach ($volume in $vhdVolumes) {
                if ([string]::IsNullOrWhiteSpace($volume.FileSystemLabel)) {
                    Trace-Execution "No file system label found for drive '$($volume.DriveLetter)', skipping..."
                }
                else {
                    Trace-Execution "Adding drive '$($volume.DriveLetter)' for '$($volume.FileSystemLabel)'"
                    $driveLetterDict.Add($volume.FileSystemLabel, $volume.DriveLetter)
                }
            }
        }
    }

    Trace-Execution "Retrieval of drive letters complete."
    return $driveLetterDict
}

<#
.SYNOPSIS
    Copies logs from the IRVM mounted VHDs to the given DriveLetterDictionary and outputs copy
    details to the given RobocopyLogPath (file).
.DESCRIPTION
    Copies logs from the IRVM mounted VHDs to the given DriveLetterDictionary and outputs copy
    details to the given RobocopyLogPath (file).
#>

function Copy-IRVMMountedVHDLogs {
    param (
        [System.Collections.Hashtable] $DriveLetterDictionary,
        [System.String] $DestinationDirectory,
        [System.String] $RobocopyLogPath
    )

    $irvmDriveLetter = $DriveLetterDictionary["IRVM01"]
    $ephemeralDriveLetter = $DriveLetterDictionary["Ephemeral"]

    $logParsingEngineExt = @(
        '*.txt'
        '*.log'
        '*.etl'
        '*.out'
        '*.xml'
        '*.htm'
        '*.html'
        '*.evtx'
        '*.json'
        '*.zip'
        '*.csv'
        '*.err'
        '*.cab'
        '*.dtr'
        '*.bin'
    )

    $relativePath = "ProgramData\AzureConnectedMachineAgent\Log"
    Invoke-Robocopy -Source "$(Join-Path -Path "$irvmDriveLetter`:" -ChildPath $relativePath)" `
        -Destination "$(Join-Path -Path $DestinationDirectory -ChildPath $relativePath)" `
        -File $logParsingEngineExt `
        -RobocopyLogPath $RobocopyLogPath

    $relativePath = "GMACache\MonAgentHostCache\Configuration"
    Invoke-Robocopy -Source "$(Join-Path -Path "$irvmDriveLetter`:" -ChildPath $relativePath)" `
        -Destination "$(Join-Path -Path $DestinationDirectory -ChildPath $relativePath)" `
        -File "*.log" `
        -RobocopyLogPath $RobocopyLogPath

    $relativePath = "GMACache\TelemetryCache\Configuration"
    Invoke-Robocopy -Source "$(Join-Path -Path "$irvmDriveLetter`:" -ChildPath $relativePath)" `
        -Destination "$(Join-Path -Path $DestinationDirectory -ChildPath $relativePath)" `
        -File "*.log" `
        -RobocopyLogPath $RobocopyLogPath

    $relativePath = "GMACache\DiagnosticsCache\Configuration"
    Invoke-Robocopy -Source "$(Join-Path -Path "$irvmDriveLetter`:" -ChildPath $relativePath)" `
        -Destination "$(Join-Path -Path $DestinationDirectory -ChildPath $relativePath)" `
        -File "*.log" `
        -RobocopyLogPath $RobocopyLogPath

    $relativePath = "Diagnostics"
    Invoke-Robocopy -Source "$(Join-Path -Path "$ephemeralDriveLetter`:" -ChildPath $relativePath)" `
        -Destination "$(Join-Path -Path $DestinationDirectory -ChildPath $relativePath)" `
        -File $logParsingEngineExt `
        -ExcludeDirectories "$ephemeralDriveLetter`:\Diagnostics\FabricRingArcA\PerfCounters" `
        -RobocopyLogPath $RobocopyLogPath

    $relativePath = "Logs"
    $pathsToExclude = @(
        "$ephemeralDriveLetter`:\Logs\sflogs\_sf_docker_logs"
        "$ephemeralDriveLetter`:\Logs\sflogs\work"
    )
    Invoke-Robocopy -Source "$(Join-Path -Path "$ephemeralDriveLetter`:" -ChildPath $relativePath)" `
        -Destination "$(Join-Path -Path $DestinationDirectory -ChildPath $relativePath)" `
        -File $logParsingEngineExt `
        -ExcludeDirectories $pathsToExclude `
        -RobocopyLogPath $RobocopyLogPath
}

<#
.DESCRIPTION
    Encapsulates the call to Robocopy.exe, see below for additional robocopy details:
    https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy
.NOTES
    Each file ($Files) and /xd ($ExcludeDirectories) must be added as its own $robocopyParams entry
    to be read correctly as a separate entity. Otherwise all contents of list will be considered a
    single entry, e.g. a single file extension "*.log *.etl" rather than two: "*.log" and "*.etl".
    This is the case with or without surrounding quotes added to each entry in the list.
#>

function Invoke-Robocopy {
    param (
        [System.String] $Source,
        [System.String] $Destination,
        [System.String] $RobocopyLogPath,
        [System.String[]] $Files = @(),
        [System.String[]] $ExcludeDirectories = @()
    )

    if (Test-Path $Source) {
        $robocopyParams = [System.Collections.ArrayList]@(
            "`"$Source`""
            "`"$Destination`""
        )

        foreach ($file in $Files) {
            $robocopyParams.Add("`"$file`"") | Out-Null
        }

        if ($ExcludeDirectories.Count -gt 0) {
            $robocopyParams.Add("/xd") | Out-Null
            foreach ($dir in $ExcludeDirectories) {
                $robocopyParams.Add("`"$dir`"") | Out-Null
            }
        }

        $robocopyParams.Add("/s") | Out-Null
        $robocopyParams.Add("/r:1") | Out-Null
        $robocopyParams.Add("/w:5") | Out-Null
        $robocopyParams.Add("/log+:$RobocopyLogPath") | Out-Null
        $robocopyParams.Add("/tee") | Out-Null

        Trace-Execution "Copying relevant files from '$Source' using call: 'robocopy $robocopyParams'"
        robocopy $robocopyParams | ForEach-Object {
            $data = $_.Split([char]9)
            if (("$($data[2])" -ne "") -and (Test-Path $data[2])) {
                $src = "$($data[2])"
            }
            if (("$($data[4])" -ne "") -and ("$($data[4])" -notmatch '^[\*.]')) {
                $file = "$($data[4])"
                Write-Progress -Activity "Copying logs from: $src..." -CurrentOperation $file -ErrorAction SilentlyContinue
            }
        }
        Write-Progress -Activity "Copying logs from: $Source complete" -Completed

        $robocopyExitCode = $LASTEXITCODE
        if ($robocopyExitCode -eq 1) {
            Trace-Execution "Robocopy completed successfully with exit code: $robocopyExitCode"
        }
        else {
            Write-Warning "Robocopy completed with exit code: $robocopyExitCode. See '$RobocopyLogPath' for details."
        }
    }
    else {
        Write-Warning "Path not found: '$Source', skipping robocopy call. Logs expected at this location may not have been generated."
    }
}

<#
.SYNOPSIS
    Gets the Observability stamp ID if successful; otherwise returns null.
.DESCRIPTION
    Gets the Observability stamp ID if successful; otherwise returns null.
.OUTPUTS
    The Observability stamp ID (type GUID) if successful; otherwise null.
.NOTES
    To be successful, Observability must have been setup on the IRVM prior to calling this function
    and the Observability stamp ID must be found in the Knowledge Store on the mounted IRVM.
#>

function Get-ObservabilityStampIdInternal {
    [OutputType([System.Guid])]
    param (
        [System.Collections.Hashtable] $DriveLetterDictionary
    )

    $ksStampIdPath = Get-KSStampIdPath -DriveLetterDictionary $DriveLetterDictionary
    if ([string]::IsNullOrEmpty($ksStampIdPath)) {
        return [System.Guid]::Empty
    }

    try {
        # Get the highest versioned DesiredStateRecord if multiple exist.
        if ($ksStampIdPath -is [System.Array]) {
            $ksStampIdPath = ($ksStampIdPath | Sort-Object -Descending)[0]
        }

        $data = Get-Content $ksStampIdPath | ConvertFrom-Json
        $obsStampId = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($data.Content))
        Trace-Execution "Retrieved Observability Stamp ID: $obsStampId."

        # KS stores the stamp GUID with surrounding quotes
        return [System.Guid]::Parse($obsStampId.Trim('"'))
    }
    catch {
        Write-Warning "Failed to retrieve Observability Stamp ID. Observability may not have been setup on the IRVM."
    }

    return [System.Guid]::Empty
}

<#
.SYNOPSIS
    Gets the path to the Stamp ID in the Knowledge Store if successful, otherwise returns null.
.DESCRIPTION
    Gets the path to the Stamp ID in the Knowledge Store from the mounted IRVM if successful,
    otherwise returns null.
#>

function Get-KSStampIdPath {
    [OutputType([string])]
    param (
        [System.Collections.Hashtable] $DriveLetterDictionary
    )

    $ksStampIdFile = "DesiredStateRecord.*.json"
    $ksStampIdRelativePath = "KnowledgeStore\Globals\StampId"

    $ksDriveLetter = $DriveLetterDictionary["IRVM01"]
    if ($null -eq $ksDriveLetter) {
        Write-Warning "The mounted IRVM01 VHD volume is not available on this stamp: unable to retrieve the Observability Stamp ID."
        return $null
    }

    $ksStampIdPath = Join-Path -Path "$ksDriveLetter`:\$ksStampIdRelativePath" -ChildPath $ksStampIdFile -Resolve -ErrorAction Ignore
    if ([string]::IsNullOrEmpty($ksStampIdPath)) {
        Write-Warning "Stamp ID location '$(Join-Path -Path "$ksDriveLetter`:\$ksStampIdRelativePath" -ChildPath $ksStampIdFile)' not found: unable to retrieve the Observability Stamp ID."
        return $null
    }
    else {
        return $ksStampIdPath
    }
}

<#
.SYNOPSIS
    Formats and logs messages as verbose output with timestamp.
.DESCRIPTION
    Formats and logs messages as verbose output with timestamp.
#>

function Trace-Execution([string] $message) {
    $caller = (Get-PSCallStack)[1]
    Write-Verbose -Message "[$([DateTime]::UtcNow.ToString('u'))][$($caller.Command)] $message" -Verbose
}

Export-ModuleMember Send-WinfieldDiagnosticData
Export-ModuleMember Copy-WinfieldDiagnosticData
Export-ModuleMember Get-ObservabilityStampId

# SIG # Begin signature block
# MIIoLQYJKoZIhvcNAQcCoIIoHjCCKBoCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCkYhtI1rB321+9
# 78hwz7I22CR3SnPoFnKHwdCe8qKCgaCCDXYwggX0MIID3KADAgECAhMzAAADTrU8
# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU
# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1
# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm
# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa
# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq
# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk
# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31
# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2
# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d
# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM
# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh
# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX
# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir
# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8
# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A
# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H
# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGg0wghoJAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIC4rvHSlE0ZsKRkW0eXCFpuR
# pIJ7e77qw5T4JUAzu2C8MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAODDseBxgA5XbZkL/WOQzf/CNsTxhBeq4JI0WbZrTDesgUffAbKF6/Mt6
# nE+sauuf85k+VlyHzljCmLp9r/5RYpqgnZNK1+fP8UaRU4gfBS2ixPPqYvwXfCBR
# 87xVeS6VNITCTCXJ5B7cug6uiih97iXqgL1/xab/QzkMPYT5/5ld1g+PH23yh9VB
# Gto4dc+DqJlAMRP72qzM+ul5YS5eIGQoZkdcK3OUQ4HdwaQjxMps6AoDCqs+fKnd
# wnRH40/0Y87KYAliVtN/yaIIXOiwgofwDUiQzCtrRVTzMUxtkepKOxTSWVgDnq6T
# pxuXDyJr25occ5NVV8Al3I/lzsmyeaGCF5cwgheTBgorBgEEAYI3AwMBMYIXgzCC
# F38GCSqGSIb3DQEHAqCCF3AwghdsAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCC3e0zzSiiNuRPqf6EKMon/lGJ51CuQKDyykfl3T8qBKgIGZNTIpURR
# GBMyMDIzMDgxMjE2NDg0OS4yNzNaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHtMIIHIDCCBQigAwIBAgITMwAAAc9SNr5xS81IygABAAABzzANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzA1MjUxOTEy
# MTFaFw0yNDAyMDExOTEyMTFaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTIwMC0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQC4Pct+15TYyrUje553lzBQodgmd5Bz7WuH8SdHpAoW
# z+01TrHExBSuaMKnxvVMsyYtas5h6aopUGAS5WKVLZAvUtH62TKmAE0JK+i1hafi
# CSXLZPcRexxeRkOqeZefLBzXp0nudMOXUUab333Ss8LkoK4l3LYxm1Ebsr3b2OTo
# 2ebsAoNJ4kSxmVuPM7C+RDhGtVKR/EmHsQ9GcwGmluu54bqiVFd0oAFBbw4txTU1
# mruIGWP/i+sgiNqvdV/wah/QcrKiGlpWiOr9a5aGrJaPSQD2xgEDdPbrSflYxsRM
# dZCJI8vzvOv6BluPcPPGGVLEaU7OszdYjK5f4Z5Su/lPK1eST5PC4RFsVcOiS4L0
# sI4IFZywIdDJHoKgdqWRp6Q5vEDk8kvZz6HWFnYLOlHuqMEYvQLr6OgooYU9z0A5
# cMLHEIHYV1xiaBzx2ERiRY9MUPWohh+TpZWEUZlUm/q9anXVRN0ujejm6OsUVFDs
# sIMszRNCqEotJGwtHHm5xrCKuJkFr8GfwNelFl+XDoHXrQYL9zY7Np+frsTXQpKR
# NnmI1ashcn5EC+wxUt/EZIskWzewEft0/+/0g3+8YtMkUdaQE5+8e7C8UMiXOHkM
# K25jNNQqLCedlJwFIf9ir9SpMc72NR+1j6Uebiz/ZPV74do3jdVvq7DiPFlTb92U
# KwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFDaeKPtp0eTSVdG+gZc5BDkabTg4MB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBQgm4pnA0xkd/9uKXJMzdMYyxUfUm/ZusU
# Ba32MEZXQuMGp20pSuX2VW9/tpTMo5bkaJdBVoUyd2DbDsNb1kjr/36ntT0jvL3A
# oWStAFhZBypmpPbx+BPK49ZlejlM4d5epX668tRRGfFip9Til9yKRfXBrXnM/q64
# IinN7zXEQ3FFQhdJMzt8ibXClO7eFA+1HiwZPWysYWPb/ZOFobPEMvXie+GmEbTK
# bhE5tze6RrA9aejjP+v1ouFoD5bMj5Qg+wfZXqe+hfYKpMd8QOnQyez+Nlj1ityn
# OZWfwHVR7dVwV0yLSlPT+yHIO8g+3fWiAwpoO17bDcntSZ7YOBljXrIgad4W4gX+
# 4tp1eBsc6XWIITPBNzxQDZZRxD4rXzOB6XRlEVJdYZQ8gbXOirg/dNvS2GxcR50Q
# dOXDAumdEHaGNHb6y2InJadCPp2iT5QLC4MnzR+YZno1b8mWpCdOdRs9g21QbbrI
# 06iLk9KD61nx7K5ReSucuS5Z9nbkIBaLUxDesFhr1wmd1ynf0HQ51Swryh7YI7TX
# T0jr81mbvvI9xtoqjFvIhNBsICdCfTR91ylJTH8WtUlpDhEgSqWt3gzNLPTSvXAx
# XTpIM583sZdd+/2YGADMeWmt8PuMce6GsIcLCOF2NiYZ10SXHZS5HRrLrChuzedD
# RisWpIu5uTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNQ
# MIICOAIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjkyMDAtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQDq
# 8xzVXwLguauAQj1rrJ4/TyEMm6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6IHp8jAiGA8yMDIzMDgxMjExMjIy
# NloYDzIwMjMwODEzMTEyMjI2WjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDogeny
# AgEAMAoCAQACAiVzAgH/MAcCAQACAhOWMAoCBQDogztyAgEAMDYGCisGAQQBhFkK
# BAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJ
# KoZIhvcNAQELBQADggEBAJ4W3QPmFCxq3++aEMIoERn2tN1HijV0TpT4Ay/Scx5O
# lKmR1uTw/RRLrhJhn8oAbL0RoIfufTD6Tt17MmJXJHvUBKhbI+gGstksH1hvL2DF
# ZbzQAgo+u2A7MECPM6tP8+tN2++Z2jVmPo9J4DSzT6jBPjEzZ2jT8dcxTvcb9F6P
# 4F1l75+21B8l81e+hq4LfhIHv7nPF7kQC1EuBJHEmsah17/Dg1V+aW5TTuPc9mcT
# /AjHQdsixmLQxgVHR1ibT7iFgFddVxHjnGJ6lcqZSeUftk59Te6SdTOJRgq7fZq7
# +1Dn49rYZKLFsd1vHodNjcEi4q1AmKHJPncYkqg+xVMxggQNMIIECQIBATCBkzB8
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1N
# aWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAc9SNr5xS81IygABAAAB
# zzANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEE
# MC8GCSqGSIb3DQEJBDEiBCB1pTxf6z9WL3Kn3lbi/h919zVl4gjPsi5AyM24gKNS
# nzCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EILPpsLqeNS4NuYXE2VJlMuvQ
# eWVA80ZDFhpOPjSzhPa/MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAHPUja+cUvNSMoAAQAAAc8wIgQgjvPsxOroiKgFL/EFHWhd9Jv6
# p2bzR5v9iryv+tOeA04wDQYJKoZIhvcNAQELBQAEggIAlM7gENwVPxlU9N0Ef1u5
# F+mvNXrYdwf2S3z/3/83+8cPDckejModvFekOosXbnVUlMMsDfI4HUG1+UDmljTq
# mgR58Z2NIz+q66FshmUexuJabF8rQHXPw5vEfF39cKochC6LjoDz70ns7P263lO4
# sQA43G0qmM2TvHJHFluy/eoL4vqjKtBPiea/3dNFmlgeH9YdfZVQOf94xMpCC+Le
# e7uAnC1O7wGJ50TdkoEdWs+n3qWTvCBe3wfAsqCsHIso+w2P3Tfc5MEsrJJFoOc7
# swXhHtpScGxPi7PPwo3zT7xPrgPg8vNZVk8k2eGcCouHZdmYQT3BszBrtSk8ZxT6
# lzf1G67rjihJcKCmpiL0FSNjS/mnMyJ1WqMwQu7RcFzhX4mW0nK0o45FR3NAwZpB
# UPs6j8xjK0B14C+6xva62G5UA3Hyj3b16/TrCs+8xEG4gC+8P0p56bAWx+Ew4df3
# 2j04XJ/yP6MvlGwOi5aBX90IqNVxCQi61mY1eagwRHlyg5O3pK7bZoo5PlGPm89Q
# 6mWsh03yK0D0sC7y/3IzitK47dUzWY1Hu02U7IaS3IRRfUocYYf7Hp0bldPxZOIo
# lFJBYbhcVw/1YSh8PqxN5ykIBHv7t2GYkXfvKTv5ZvD+POBgmjPWhvC5GP/0ySWF
# V78xeKGcy6XwSB82+advuKI=
# SIG # End signature block