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 |