Obs/bin/ObsAgent/lib/Scripts/LogCollectionHelper.psm1
<##############################################################
# # # Copyright (C) Microsoft Corporation. All rights reserved. # # # ##############################################################> Import-Module $PSScriptRoot\GenericHelper.psm1 -Force -Verbose:$false function Invoke-ScriptBlockWithRetries { param ( [Parameter(Mandatory = $true)] [ScriptBlock] $ScriptBlock, [Parameter(Mandatory = $false)] [Object] $Argument, [Parameter(Mandatory = $true)] [int] $MaxTries, [Parameter(Mandatory = $false)] [int] $IntervalInSeconds = 30 ) $functionName = "$($MyInvocation.MyCommand.Name)" Trace-Progress "$functionName : Retrying max $MaxTries interval [$IntervalInSeconds] ScriptBlock = [$ScriptBlock]" $attempt = 1 $success = $false do { Trace-Progress "$functionName : Starting attempt $attempt" try { $result = Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $Argument -ErrorAction Stop $success = $true } catch { $message = "Exception occurred while trying to execute scriptblock command:" + $_.Exception.ToString() Trace-Progress "$functionName : $message" if ($attempt -ge $MaxTries) { throw } Start-Sleep -Seconds $IntervalInSeconds } finally { Trace-Progress "$functionName : Completed attempt $attempt; Status = $success" $attempt++ } } while (!$success) return $result } function Get-WindowsEventLog { Param ( [parameter(Mandatory=$true)] [string[]] $ComputerNames, [parameter(Mandatory=$false)] [HashTable] [ValidateNotNull()] $ComputerPSSessions, [parameter(Mandatory=$true)] [string[]] $LogPattern, [parameter(Mandatory=$false)] [DateTime] $EventsFromDate = (Get-Date).AddHours(-1), [parameter(Mandatory=$false)] [DateTime] $EventsToDate = (Get-Date), [Parameter(Mandatory=$false)] [REF] $ExcludedEndpoints, [parameter(Mandatory=$true)] [PSObject] $Roles, [parameter(Mandatory=$true)] [string] $CurrentRole, [parameter(Mandatory=$true)] [string] $DestPathWithRoleName, [parameter(Mandatory=$true)] [bool] $LocalMode ) $functionName = "$($MyInvocation.MyCommand.Name)_$CurrentRole" # Get the time span in milliseconds function Get-TimeSpan($Date) { $timeSpan = New-TimeSpan -Start $Date -End (Get-Date) return [Math]::Round($timeSpan.TotalMilliseconds) } # Calculate number of milliseconds and prepare the WEvtUtil parameter to filter based on date/time $toSpan = Get-TimeSpan -Date $EventsToDate $fromSpan = Get-TimeSpan -Date $EventsFromDate $exportLogJobs = @() # Copy logs from remote machine to local machine foreach($computerName in $ComputerNames) { $session = $null $machineName = $computerName.Split('.')[0] Trace-Progress "$functionName : computername = [$computerName] machinename = [$machineName]" if (!$LocalMode) { if ($ComputerPSSessions) { Trace-Progress "$functionName :Checking if the session for $computerName session is valid in ComputerPSSessions array" if ($ComputerPSSessions.ContainsKey($computerName)) { $session = $ComputerPSSessions[$computerName] if (($null -ne $session) -and ( ($session.State -ne "Opened") -or ($session.Availability -ne "Available") ) ) { if($session) { #if we had opened the session previously, close it before overwriting this variable with new session. Remove-PSSession -Session $session -ErrorAction SilentlyContinue } Trace-Progress "$functionName :The session for $computerName went into state = [$($session.state)] , availabilty = [$($session.Availability)], ! Reinitializing!" $session = Initialize-PSSession -ComputerPSSessions $ComputerPSSessions -ComputerFqdn $computerName -ExcludedEndpoints ([REF]$ExcludedEndpoints.Value) if ($null -ne $session) { $ComputerPSSessions[$computerName] = $session } } } else { Trace-Progress "$functionName :$computerName session not found in ComputerPSSessions[] array, unable to collect event logs " } } else { Trace-Progress "$functionName :Creating a PSSession to [$computerName] as ComputerPSSessions[] array is null" $session = New-PSSession -ComputerName $computerName -ErrorAction SilentlyContinue # $ComputerPsSessions are not provided and we are opening a new session for each computername, we need to close these before we leave. } } if ($LocalMode -or (($null -ne $session) -and ($session.State -eq "Opened") -and ($session.Availability -eq "Available"))) { if ($LocalMode) { $logPath = "$($env:TEMP)WinEvents$CurrentRole\" } else { $logPath = Invoke-Command -Session $session {$tmp = "$($env:TEMP)WinEvents$using:CurrentRole\" ; $tmp = $tmp.ToLower().Replace("c:","\\$($env:ComputerName)\c$"); return $tmp} } Trace-Progress "$functionName :Log path computer = $logPath -- for $computerName " $initblock = [ScriptBlock]::Create("Import-Module -Name '$PSScriptRoot\LogCollectionHelper.psm1' -Force; Import-Module -Name '$PSScriptRoot\GenericHelper.psm1' -Force") # Collect logs on remote machine if ($LocalMode) { $exportLogJobs += Start-Job -ScriptBlock { Collect-WindowsEventLogs -LogFolder $using:logPath -FromSpan $using:fromSpan -ToSpan $using:ToSpan -LogPattern $using:LogPattern } -InitializationScript $initblock -ErrorAction Continue } else { Invoke-Command -Session $session $initBlock $exportLogJobs += Invoke-Command -AsJob -Session $session { Collect-WindowsEventLogs -LogFolder $using:logPath -FromSpan $using:fromSpan -ToSpan $using:ToSpan -LogPattern $using:LogPattern } -ErrorAction Continue } } else { if($null -eq $session) { Trace-Progress "$functionName :Could not establish a PS session with the computer [$computerName]." -Warning } else { Trace-Progress "$functionName :Session with the computer [$computerName] is stale - Session state = [$($session.State)], Session Availability =[$($session.Availability)]" -Warning } } } Trace-Progress "$functionName :Kicked off $($exportLogJobs.count) jobs to collect windows events" try { $ProgressPreference = "SilentlyContinue" $exportLogJobOutput = $exportLogJobs | Wait-job | Receive-Job Trace-Progress "$functionName :Finished waiting for jobs count = [$($exportLogJobs.Count)]" Write-Output $exportLogJobs # dont change to trace-progress Trace-Progress "$functionName :DestPathWithRoleName = [$DestPathWithRoleName]" foreach ($o in $exportLogJobOutput) { Trace-Progress "$functionName :Job retruned logpath = [$($o.logPath)] from computer = $($o.ComputerName)" if (-not [string]::IsNullOrEmpty($o.logPath)) { Trace-Progress "$functionName :Copying from Source: $($o.logPath) to Destination: $DestPathWithRoleName" try { $windowsEventFiles = Get-ChildItem -Path $o.logPath -File -Recurse } catch { Trace-Progress -Message "$functionName :Failed to get files from $($o.logPath) on $($o.computerName). Error: $_" -Warning } if (($null -ne $windowsEventFiles ) -or ($windowsEventFiles.Count -gt 0)) { $destPath = Join-Path -Path $DestPathWithRoleName -ChildPath $o.ComputerName try { Trace-Progress "$functionName :Creating new directory $destPath" New-ASPath -Path $destPath -Type Directory Copy-Item -Path $o.logPath -Destination $destPath -Force -Recurse } catch { Trace-Progress "$functionName :Failed to copy logs from $($o.logPath). Error: $_" -Warning } Remove-Item $o.logPath -Force -Recurse -ErrorAction SilentlyContinue } } else { Trace-Progress "$functionName :No logs copied as path on remote machine was empty. $($o.logPath)" } } Trace-Progress "$functionName :all evtx logs from all role vm's complete." $allEvtxCollectionSuccess = $true } finally { Trace-Progress "$functionName :In Finally block" if(!$allEvtxCollectionSuccess) { Trace-Progress "$functionName :Finally block- Unclean Exit detected, stopping all export jobs, if in progress" $exportLogJobs | Stop-Job $exportLogJobs | Receive-Job } Trace-Progress "$functionName :In Finally block, removing all job" $exportLogJobs | remove-job -ErrorAction SilentlyContinue } } function Collect-WindowsEventLogs { Param ( [parameter(Mandatory=$true)] [string] $LogFolder, [parameter(Mandatory=$true)] [double] $FromSpan, [parameter(Mandatory=$true)] [double] $ToSpan, [parameter(Mandatory=$true)] [string[]] $LogPattern ) if (-not (Test-Path $logFolder)) { $null = New-Item -ItemType Directory -Path $logFolder } $timestamps = @{} $qParameter = "*[System[TimeCreated[timediff(@SystemTime) <= $fromSpan] and TimeCreated[timediff(@SystemTime) >=$toSpan]]]" $reservedChannels = @("Microsoft.AzureStack.LCMController.EventSource/Admin") foreach ($lp in $logPattern) { $eventLogs = Get-WinEvent -ListLog $lp -Force -ErrorAction SilentlyContinue if (!$eventLogs.count) { $timestamps.$lp = @{} } else { $eventLogs | Foreach-Object { $fileSuffix = "Event_"+$_.LogName.Replace("/","-")+".EVTX" $logFile = $logFolder + $fileSuffix $locale = (Get-Culture).Name # Export log file using the WEvtUtil command-line tool # For Analytical and Debug log: disable => export => enable, as export cannot be performed over an enabled direct channel. $directChannel = $false $allLatestTimeCreated = $null if ($_.LogType -in @('Analytical','Debug')) { if ($_.IsEnabled) { $directChannel = $true # Disable Logs WEvtUtil.exe sl /e:false $_.LogName } } else { # We cant collect latest time in O(1) for Analytical and Debug Log, so leave it as null # Here are are collecting the latest time for Regular Logs only $allLatestTimeCreated = Get-WinEvent -logname $_.LogName -MaxEvents 1 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "TimeCreated" } # Export logs based on query to file with overwrite if ($reservedChannels -icontains $lp) { WEvtUtil.exe epl $_.LogName $logFile /ow:true } else { WEvtUtil.exe epl $_.LogName $logFile /q:$qParameter /ow:true } $allOldestTimeCreated = Get-WinEvent -logname $_.LogName -oldest -MaxEvents 1 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "TimeCreated" if ($directChannel -eq $true) { # Enable Logs echo y | WEvtUtil.exe sl /e:true $_.LogName | out-null } # Archive logs (saves all locale specific information to allow reading of events without publisher) # WEvtUtil.exe al $logFile /l:$locale $copiedLatestTimeCreated = Get-WinEvent -path $logFile -MaxEvents 1 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "TimeCreated" $copiedOldestTimeCreated = Get-WinEvent -path $logFile -oldest -MaxEvents 1 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty "TimeCreated" if ($allOldestTimeCreated -or $copiedOldestTimeCreated) { $timestamps.($_.LogName) = @{} } if ($allOldestTimeCreated) { $timestamps.($_.LogName).all = @{ oldestTimeCreated = $allOldestTimeCreated; latestTimeCreated = $allLatestTimeCreated } } if ($copiedOldestTimeCreated) { $timestamps.($_.LogName).copied = @{ oldestTimeCreated = $copiedOldestTimeCreated; latestTimeCreated = $copiedLatestTimeCreated } } } } } # Return the computerName and the logFolder @{ ComputerName = $env:ComputerName VMName = $null logPath = $logFolder timestamps = $timestamps } } # # For security reasons we strictly restrict the files to be included to certain extentions # function Get-FileLog { Param ( [parameter(Mandatory=$true, ParameterSetName='File')] [string[]] $ComputerNames, [parameter(Mandatory=$false, ParameterSetName='File')] [HashTable] [ValidateNotNull()] $ComputerPSSessions, [parameter(Mandatory=$true)] [string[]] $SourceLogFilePaths, [parameter(Mandatory=$false)] [DateTime] $FilesFromDate = (Get-Date).AddHours(-1), [parameter(Mandatory=$false)] [DateTime] $FilesToDate = (Get-Date), [parameter(Mandatory=$true, ParameterSetName='CSV')] [string] $CSVLogsFolderName, [parameter(Mandatory=$true)] [string] $Role, [Parameter(Mandatory=$false, ParameterSetName='File')] [REF]$ExcludedEndpoints, [parameter(Mandatory=$true)] [string] $DestPathWithRoleName, [parameter(Mandatory=$true)] [bool] $LocalMode, [parameter(Mandatory=$false)] [bool] $IsArcA = $false, [parameter(Mandatory=$true)] [bool] $SaveToPathSelected ) Trace-EnteringMethod $functionName = "$($MyInvocation.MyCommand.Name)_$Role" $CSVLogsCopied = @() $ProgressPreference = "SilentlyContinue" foreach($logPath in $SourceLogFilePaths) { if ($logPath.Contains('$')) { # The path might contain environment variables, hence expanding it to actual path. # Example for valid environment variables: $env:WinDir, $env:SystemDrive, $env:ProgramData. # Avoid using environment variables that are different per user ex. $env:temp. $logPath = $ExecutionContext.InvokeCommand.ExpandString($logPath) } # Copy-Item -FromSession has a bug where it does not respect wild card over remote, as well as failing to copy some logs due to file locks. # Manually copying the files by mapping the drive. $logPathLeaf = Split-Path -Path $logPath -Leaf $logPathParent = Split-Path -Path $logPath -Parent if ($PsCmdlet.ParameterSetName -eq "CSV") { try { if ($logPath -notin $CSVLogsCopied) { Trace-Progress "$functionName :Copying from $logPath" $CSVLogDestRelativePath = $CSVLogsFolderName $items = $null if (Test-Path $logPath -ErrorAction SilentlyContinue) { Trace-Progress "$functionName : Copying csv logs from Source: $logPathParent to Destination: $CSVLogDestRelativePath" $items = Get-FilteredChildItem -Path $logPath -FromDate $FilesFromDate -ToDate $FilesToDate -IsArcA $IsArcA if (($null -ne $items.filteredItems) -and ($items.filteredItems.Count -gt 0)) { Copy-FilteredChildItem -Items $items.filteredItems -Source $logPathParent -ChildFolder $CSVLogDestRelativePath -DestPathWithRoleName $DestPathWithRoleName # If we are sending the logs to an SMB Share, then we want all files compressed if (-not $SaveToPathSelected) { Extract-CompressedFiles -DestPathWithRoleName $DestPathWithRoleName -ChildFolder $CSVLogDestRelativePath } } else { Trace-Progress "$functionName : Skipping Copy-FilteredChildItem and checking for cab files, as items.FilteredItems is null." } <# if ($items.filesToSkipCompression.Count -gt 0) { Trace-Progress "$functionName : total files to skip compression = $($items.filesToSkipCompression.Count)" Copy-FilteredChildItem -Items $items.filesToSkipCompression -Source $logPathParent -ChildFolder $CSVLogDestRelativePath -ZipPipeline $UncompressedPipeline } #> $CSVLogsCopied += $logPath } else { Trace-Progress "$functionName :Folder $logPath does not exist. Logs from '$logPath' were not collected." -Warning } } } catch { Trace-Progress "$functionName : Failed to copy CSV logs at log path : $logpath. Error : $_" -Error } } elseif ($PsCmdlet.ParameterSetName -eq "File") { # Copy logs from remote machine to local machine foreach($computerName in $ComputerNames) { try { $session = $null $machineName = $computerName.Split('.')[0] $destRelativePath = $machineName if (!$LocalMode) { if ($ComputerPSSessions) { if ($ComputerPSSessions.ContainsKey($computerName)) { $session = $ComputerPSSessions[$computerName] if (($null -ne $session) -and ($session.State -ne "Opened")) { Trace-Progress "$functionName :The session for $computerName went into $($session.state) state! Reinitializing!" $session = Initialize-PSSession -ComputerPSSessions $ComputerPSSessions -ComputerFqdn $computerName -ExcludedEndpoints ([REF]$ExcludedEndpoints.Value) if ($null -ne $session) { $ComputerPSSessions[$computerName] = $session } } } } else { Trace-Progress "$functionName :Creating a PSSession to $computerName" $session = New-PSSession -ComputerName $computerName -ErrorAction SilentlyContinue } } if (!$LocalMode -and (($null -eq $session) -or ($session.State -ne "Opened"))) { Trace-progress -Message "$functionName :Could not establish a PS session with the computer. Logs were not copied from this computer." -Warning } else { Trace-Progress "$functionName : Copying from $logPath" if (!$LocalMode) { $logPathRoot = ("\\$computerName\$($logPathParent -replace ':', '$')").TrimEnd('\') $mappedDriveName = "Remote" + $machineName $mappedDrive = Get-PSDrive $mappedDriveName -ErrorAction SilentlyContinue if ((-not $mappedDrive)) { Trace-Progress "$functionName : Creating mapped drive : $mappedDriveName" $mappedDrive = New-PSDrive -Name $mappedDriveName -PSProvider FileSystem -Root $logPathRoot -ErrorVariable DriveError -ErrorAction SilentlyContinue if ($DriveError.count -gt 0) { $err = $DriveError[0] $errorMessage = $err.Exception.Message Trace-Progress "$functionName : Error creating mapped drive : $errorMessage" -Warning } } else { Trace-Progress "$functionName : Mapped drive for $mappedDriveName exists" } } if ($LocalMode -or $mappedDrive) { if ($LocalMode) { $logPathRoot = $logPathParent.TrimEnd('\') $newLogPath = $logPath } else { $newLogPath = $mappedDriveName + ':' + $logPathLeaf } Trace-Progress "$functionName : newLogPath = [$newLogPath]" $items = $null if (Test-Path $newLogPath -ErrorAction Continue) { if ($LocalMode) { Trace-Progress "$functionName :Copying file logs from Source: $newLogPath to Destination: $DestPathWithRoleName $destRelativePath" } else { Trace-Progress "$functionName :Copying file logs from Source: $newLogPath (Remote is mapped drive for $logPathRoot) to Destination: $DestPathWithRoleName $destRelativePath" } $items = Get-FilteredChildItem -Path $newLogPath -FromDate $FilesFromDate -ToDate $FilesToDate -IsArcA $IsArcA if (($null -ne $items.filteredItems) -and ($items.filteredItems.Count -gt 0)) { Copy-FilteredChildItem -Items $items.filteredItems -Source $logPathRoot -DestPathWithRoleName $DestPathWithRoleName -ChildFolder $destRelativePath -ComputerName $machineName # If we are sending the logs to an SMB Share, then we want all files compressed if (-not $SaveToPathSelected) { Extract-CompressedFiles -DestPathWithRoleName $DestPathWithRoleName -ChildFolder $CSVLogDestRelativePath } Trace-Progress "$functionName :Completed Copy-FilteredChildItems" } else { Trace-Progress "$functionName : Skipping Copy-FilteredChildItem and checking for cab files, as items.FilteredItems is null." } } else { Trace-Progress "$functionName :Folder $newLogPath does not exist on $computerName. Logs from '$logPath' were not collected." -Warning } if (!$LocalMode) { Trace-Progress "$functionName :Removing PS Drive $mappedDrive" Remove-PSDrive $mappedDrive -Verbose } } } } catch { Trace-Progress "$functionName : Failure to collect logs for log path : $LogPath on computer : $ComputerName. Error: $_" -Error if ($mappedDrive) { Remove-PSDrive $mappedDrive } } } if (-not $ComputerPSSessions -and ($null -ne $session)) { Remove-PSSession -Session $session -ErrorAction SilentlyContinue } } else{ # We should never see this, if we see this means error in role xml. Trace-Progress "$functionName :Folder [$logPath] - [$logPathParent] is neither file log or CSV log check..." -Warning } } } # # Gets files according to filtered extensions and date range. # returns the child items based on the filtered criteria # function Get-FilteredChildItem { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] $Path, [Parameter(Mandatory=$true)] [DateTime] $FromDate, [Parameter(Mandatory=$true)] [DateTime] $ToDate, [parameter(Mandatory=$false)] [switch] $IncludeDumpFile, [parameter(Mandatory=$false)] [bool] $IsArcA = $false ) Trace-EnteringMethod $functionName = $($MyInvocation.MyCommand.Name) $allowedFileExtensions = '*.txt','*.log','*.etl','*.out','*.xml','*.htm','*.html','*.mta','*.evtx','*.tsf','*.json','*.zip','*.csv','*.err','*.cab' # Note following file extensions are omitted - '*.blg', ,'*.trace', '*.bin' if ($isArcA) { $allowedFileExtensions = $allowedFileExtensions + "*.dtr", "*.bin", "*.trace" } if ($IncludeDumpFile) { $allowedFileExtensions = $allowedFileExtensions + "*.dmp" } $dateFilterExt = @('*.bin') $excludedFiles = @('*unattend.xml') #$skipCompressionFileExtensions = @('*.bin', '*.zip', '*.cab') $reservedFiles = @("CBS.log", "ActionList.xml", "DeviceInventory.xml") $reservedPaths = @("TestObservability", "$env:SystemDrive\CloudDeployment\Logs", "$env:SystemDrive\Observability\ECE", "$env:SystemDrive\Observability\ECEAgent", "MasLogs", "$env:windir\logs\DISM", "$env:SystemDrive\Observability\Download\UdiSessions\Scan-*\udiapi.log", "$env:SystemDrive\Observability\Download\UdiSessions\Download-*\udiapi.log", "$env:windir\Logs\MoSetup\udiapi-osget*.log", "$env:windir\Logs\MoSetup\UpdateAgent*.log" ) #$reservedPattern = @('MonAgentHost', 'AzureStack_Validation') $filesToSkipCompression = @() Trace-Progress "$functionName :Path : $Path" try { if (Test-Path -Path $Path -PathType leaf) { Trace-Progress "$functionName testpath success - path is a leaf = $Path" $unfilteredItems = Get-ChildItem -Path $Path -Force -ErrorAction stop } else { Trace-Progress "$functionName testpath is not a leaf = $Path" $unfilteredItems = Get-ChildItem -Path $Path -Recurse -Force -ErrorAction stop } Trace-Progress "$functionName : Found $($unfilteredItems.Count) unfiltered items in $Path." # Apply special filtering for bin files based on date range as logs get added to these files incrementally, so we cannot depend on creation/modification date. if ($allowedFileExtensions | Where-Object {$dateFilterExt -Contains $_}) { Trace-Progress "$functionName allowedFileExtentions $allowedFileExtensions - dateFilterExt = $dateFilterExt" $items1 = @() try { $childItems = Get-ChildItem -Path $Path -Include $dateFilterExt -Recurse -Force -ErrorAction stop Trace-Progress "$functionName childItems = $($childItems.count) , childItems = $($childItems -join ',')" } catch { Trace-Progress "$functionName : Failed to Get-ChildItem for Path : $Path, Powershell Exception: $_" -Error } $directories = $childItems | Group-Object Directory Trace-Progress "$functionName : Obtained $($directories.count) directories" foreach ($directory in $directories) { Trace-Progress "$functionName : Processing direcotry = $directory " $files = @($directory.Group | Sort-Object CreationTime,Name) if ($files.Count -le 2) { $items1 += $files } elseif (($FromDate -le $ToDate) -and ($ToDate -ge $files[0].CreationTime)) { # Start from the first file modified after FromDate $filesModifiedAfterFromDate = $files | Where-Object {$_.LastWriteTime -ge $FromDate} # If there is less then 3 files which were modified after from date, just get the last 3 files to help investigation if ($filesModifiedAfterFromDate.Count -gt 2) { $fromFile = $filesModifiedAfterFromDate[0] } else { # Get last 3 log files if there is no log written in specific time range $fromFile = $files[0 - [math]::min($files.Count, 3)] } # End at the first file modifed after the ToDate. $filesModifiedAfterToDate = $files | Where-Object {$_.LastWriteTime -ge $ToDate} if ($filesModifiedAfterToDate.Count -gt 0) { $toFile = $filesModifiedAfterToDate[0] } else { $toFile = $files[-1] } $fromIndex = [array]::IndexOf($files, $fromFile) $toIndex = [array]::IndexOf($files, $toFile) if($fromIndex -ne '-1' -and $toIndex -ne '-1' -and $fromIndex -le $toIndex) { $items1 += $files[$fromIndex..$toIndex] } } } <# # by disabling this, all the files will be in item1 [System.Array]$tmp = @($items1 | ForEach-Object {$r=@()} {$t=$_; $skipCompressionFileExtensions | ForEach-Object {if ($t -like $_){$r+=$t}}} {$r}) $filesToSkipCompression = $tmp Trace-Progress "$functionName : Zipping skipped for files with dateFilterExt are : $tmp" $items1 = $items1 | Where-Object { $_ -NotIn $filesToSkipCompression } #> } }catch { Trace-Progress "$functionName : Failed while parsing for bin files $_" -Error Trace-Progress -Message "$functionName : StackTrace : $($PSItem.ScriptStackTrace)" -Error } # Rest of files, ex.("*.etl","*.txt","*.log", ..etc) are filtered based on creation/modification date range, except reserved folders/files. # Adding try catch block because powershell throws .net terminating exception which is not ignored by powershell with “ErrorAction SilentlyContinue” try { $ext = $allowedFileExtensions | Where-Object { $_ -notin $dateFilterExt} $items2 = @() # Handles possible arrays of files/folders $pathItemResult = Get-Item $Path foreach ($pathItem in $pathItemResult) { if($pathItem -is [System.IO.DirectoryInfo]) { # Get items recursively for folders $items2 += Get-ChildItem -Path $pathItem -Include $ext -Exclude $excludedFiles -Recurse -Force -ErrorVariable Item2Errors -ErrorAction Continue } elseif ($pathItem -is [System.IO.FileInfo]) { # Get items non recursively for files $items2 += Get-ChildItem -Path $pathItem -Include $ext -Exclude $excludedFiles -Force -ErrorVariable Item2Errors -ErrorAction Continue } else { Trace-Progress "$functionName : Failed to handle '$pathItem' item type: $($pathItem.GetType().FullName)" } } } catch [UnauthorizedAccessException] { Trace-Progress "$functionName : Failed to Get-ChildItem for Path : $Path, .Net Exception: $_" -Error # This is a temporary workaround to handle the issue in Bug 4780610, where accessing (by Get-ChildItem above) some of the .blg files copied to our SF clusters' # diagnostic shares result in an AccessDenied error. Since we already have the unfiltered list of items, as a fallback, we will perform the filtering directly # against that list instead of relying on Get-ChildItem. $items2 = Get-ItemsByExtension -UnfilteredItems $unfilteredItems -Include $ext -Exclude $excludedFiles -ErrorAction SilentlyContinue } catch { Trace-Progress "$functionName : Failed to Get-ChildItem for Path : $Path, Powershell Exception: $_" -Error } Trace-Progress -Message "$functionName : item2 count = $($items2.count)" $items2 = $items2 | Where-Object {((($_.CreationTime -ge $FromDate) -or ($_.LastWriteTime -ge $FromDate)) -and $_.CreationTime -le $ToDate)} Trace-Progress -Message "FromDate $($FromDate.ToString()) ToDate = $($ToDate.ToString()) " # since we use -ErrorAction continue, most errors will not hit the catch block. See if there were any errors in getting $items2 # Note: Found a bug where Get-ChildItem did not finish getting items if a file is not found (likely because it was pruned or zipped). Solution is to use # -ErrorAction Continue instead of -ErrorAction Stop. foreach ($err in $Item2Errors) { $errorMessage = $err.Exception.Message if ($errorMessage -like "Could not find item *") { $fileNotFound = $errorMessage.Split()[-1].Trim('.') # if file not found is .etl or .blg, check if it was zipped if (($fileNotFound.endswith(".etl")) -or ($fileNotFound.endswith(".blg"))) { $extension = $fileNotFound.Substring($fileNotFound.length - 3) $zippedFileName = $fileNotFound + ".zip" $srcFile = $null try { # This is the new zip file that needs to be copied in lieu of original etl or blg $srcFile = Get-Item -Path $zippedFileName -ErrorAction Stop } catch { Trace-Progress -Message "$functionName : Failed to Fetch the zip file in the abscence of $($extension) [$zippedFileName]" -Error } if ($srcFile -ne $null) { $items2 += $srcFile Trace-Progress -Message "$functionName : Successfully added [$zippedFileName] to list of items to copy" } } else { if (($fileNotFound).EndsWith('.zip')) { # assume the file was pruned, so make it a warning Trace-Progress -Message "$functionName : $errorMessage" -warning } else { Trace-Progress -Message "$functionName : $errorMessage" -Error } } } else { Trace-Progress -Message "$functionName : $errorMessage" -Error } } # Contents of reserved paths are always copied. # NOTE: # using $path.contains looks for substring which can be faulty, To match the full folder name should use EndsWith # e.g. of faulty comparision is when SDN matches folders 'SDN' and 'SDNDiagnostics' cause all files from both folders to be picked up. $items3 = @() $pathInReservedPaths = $false foreach ($reservedPath in $reservedPaths) { if ($path.ToLower().Trim("\").EndsWith($reservedPath.ToLower().Trim("\"))) { $pathInReservedPaths = $true break } } if ($pathInReservedPaths) { Trace-Progress "Path $path is in reserved paths. Getting all files at this path, regardless of log collection time range." $items3 = Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { !$_.PSIsContainer } } # Reserved files are always copied. $items4 = Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue | Where-Object {$_.Name -in $reservedFiles} <# $items5 = @() if (($reservedPattern | ForEach-Object {$Path.Contains($_)}) -contains $true) { $items5 = Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue } #> # [System.Array]$tmp1 = @(@($items1) + @($items2) | ForEach-Object {$r=@()} {$t=$_; $skipCompressionFileExtensions | ForEach-Object {if ($t -like $_){$r+=$t}}} {$r}) # $filesToSkipCompression += $tmp1 Trace-Progress "$functionName : adding items1.count = $($items1.count) and items2.count = $($items2.count) and items3.count = $($items3.count) after applying time filter" $filteredItems = @($items1) + @($items2) + @($items3) + @($items4) | Sort-Object -Property FullName -Unique Trace-Progress "$functionName : Returning unfilteredItems ($($unfilteredItems.count)), filteredItems ($($filteredItems.count)), filesToSkipCompression ($($filesToSkipCompression.count))" return @{ unfilteredItems = @($unfilteredItems) filteredItems = @($filteredItems) filesToSkipCompression = @($filesToSkipCompression) } } function Get-TimestampsHelper { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [AllowEmptyCollection()] [System.IO.FileSystemInfo[]] $Items ) if ($Items.count -eq 0) { return @{} } $oldestCreationTime = $Items[0].CreationTimeUtc $latestCreationTime = $Items[0].CreationTimeUtc $oldestLastWriteTime = $Items[0].LastWriteTimeUtc $latestLastWriteTime = $Items[0].LastWriteTimeUtc foreach ($item in $Items) { if ($null -ne $item.CreationTimeUtc) { if ($null -eq $oldestCreationTime -or $oldestCreationTime -gt $item.CreationTimeUtc) { $oldestCreationTime = $item.CreationTimeUtc } if ($null -eq $latestCreationTime -or $latestCreationTime -lt $item.CreationTimeUtc) { $latestCreationTime = $item.CreationTimeUtc } } if ($null -ne $item.LastWriteTimeUtc) { if ($null -eq $oldestLastWriteTime -or $oldestLastWriteTime -gt $item.LastWriteTimeUtc) { $oldestLastWriteTime = $item.LastWriteTimeUtc } if ($null -eq $latestLastWriteTime -or $latestLastWriteTime -lt $item.LastWriteTimeUtc) { $latestLastWriteTime = $item.LastWriteTimeUtc } } } return @{ oldestCreationTime = $oldestCreationTime latestCreationTime = $latestCreationTime oldestLastWriteTime = $oldestLastWriteTime latestLastWriteTime = $latestLastWriteTime } } function Get-Timestamps { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [AllowNull()] [AllowEmptyCollection()] [System.IO.FileSystemInfo[]] $all, [Parameter(Mandatory=$true)] [AllowNull()] [AllowEmptyCollection()] [System.IO.FileSystemInfo[]] $copied ) $newDetails = @{} if ($all.count) { $allTimestamps = Get-TimestampsHelper $all $newDetails.all = $allTimestamps $newDetails.all.count = $all.count } if ($copied.count) { $copiedTimestamps = Get-TimestampsHelper $copied $newDetails.copied = $copiedTimestamps $newDetails.copied.count = $copied.count } return $newDetails } # # Copy filtered files recursively by re-creating the folder structure at the destination to match the source. # function Copy-FilteredChildItem { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [AllowNull()] [AllowEmptyCollection()] [Object[]] $Items, [Parameter(Mandatory=$true)] [string] $Source, [Parameter(Mandatory=$true)] [string] $ChildFolder, [Parameter(Mandatory=$true)] [string] $DestPathWithRoleName, [Parameter(Mandatory=$false)] [string] $ComputerName ) $functionName = $($MyInvocation.MyCommand.Name) # Handle paths with wildcard(s); set Source to deepest non-wildcard parent that resolves to a full directory. if ($Source -ne $env:SystemDrive) { $sourceItem = Get-Item $Source while ($sourceItem -isnot [System.IO.DirectoryInfo]) { $Source = Split-Path $Source -Parent $sourceItem = Get-Item $Source } } else { # handle exception when path is $env:systemdrive, in that case get-item $source returns current path not c:/ $Source += "\" $sourceItem = Get-Item $Source } # Catches cases where string path may not match the resolved file path, e.g. # 'C:\Users\ADMINI~1\AppData' (string) vs. C:\Users\Administrator (${item}.FullName) # Also resolves wildcard paths into valid expanded paths. $Source = $sourceItem.FullName.TrimEnd('\') Trace-Progress "$functionName DestPathWithRoleName = $DestPathWithRoleName" foreach ($item in $Items) { $Destination = Join-Path -Path $DestPathWithRoleName -ChildPath $ChildFolder $itemDir = $item.DirectoryName $itemName = $item.FullName if (($null -eq $itemDir) -or ($null -eq $itemName)) { Trace-Progress "$functionName : Null directory or fullname found. Item $item, Directory $itemDir, ItemName $itemName" -Warning # Skip processing this item continue } $dir = $itemDir.Replace($Source, $Destination) $target = $itemName.Replace($Source, $Destination) if (!(Test-Path $dir -ErrorAction Continue)) { Trace-Progress "$functionName Creating new directory: $dir" $null = New-Item $dir -Type Directory } if ($ComputerName) { if ($item.Extension -in @('.bin','.etl')) { $parent = Split-Path $target -Parent $leaf = Split-Path $target -Leaf $target = "$($parent)\$($computerName)_$($leaf)" } } if (!(Test-Path $target -ErrorAction Continue)) { try { Trace-Progress -Message "$functionName : Copying item $($item.FullName) to [$target]" Copy-Item -Path $item.FullName -Destination $target -Force -ErrorAction Stop } catch [System.Management.Automation.ItemNotFoundException], [System.IO.FileNotFoundException] { $errorHResult = "0x$('{0:x8}' -f $_.Exception.HResult)" # Prepare the error message but dont trace immediately $actualErrorMessage = "$functionName : Failed to copy [$($item.FullName)] to $target. HResult : $errorHResult. Error: $_" # Does the file that failed to copy end with .etl or .blg? if yes, maybe it just got converted to .zip, so attempt to copy zip instead. # If copy of that fails too, then trace the original error - $actualErrorMessage if(($item.FullName).EndsWith('.etl') -or ($item.FullName).EndsWith('.blg')) { # This is best case attempt when etl or blg file just got converted to zip file. $extension = ($item | select Extension).Extension $zippedFileName = $item.FullName + ".zip" $target = $target + ".zip" $srcFile = $null try { # This is the new zip file that needs to be copied in lieu of original etl or blg $srcFile = Get-Item -Path $zippedFileName -ErrorAction Stop } catch { Trace-Progress -Message $actualErrorMessage -Error Trace-Progress -Message "Failed to Fetch the zip file in the abscence of $($extension) [$zippedFileName]" } if($srcFile) { try { Copy-Item -Path $zippedFileName -Destination $target -Force -ErrorAction Ignore Trace-Progress -Message "$functionName : attempting to copy ZIP file instead of $($extension) file succeeded [$zippedFileName] to [$target]" } catch { # this is not the original error, we found matching zip file and copying of that failed # this is a best case effort, if this fails trace original error. Trace-Progress -Message "$functionName : copying ZIP file instead of $($extension) file failed as well [$zippedFileName] to [$target]" # also add the original error into the error list. Trace-Progress -Message $actualErrorMessage -Error } } } else { # the file that failed to copy is not an etl or blg, so we dont have an alternative to that. if (($item.FullName).EndsWith('.zip')) { # assume the file was pruned, so make it a warning Trace-Progress -Message $actualErrorMessage -warning } else { Trace-Progress -Message $actualErrorMessage -Error } } } catch { $errorHResult = "0x$('{0:x8}' -f $_.Exception.HResult)" Trace-Progress -Message "$functionName : Failed to copy $($item.FullName) to $target. HResult : $errorHResult. Error: $_" -Error # On failure, display the size of the directories in system drive if($target.StartsWith($env:systemdrive[0])) { $sysDrive = Get-PSDrive $env:systemdrive[0] Trace-Progress -Message "SystemDrive = $($sysDrive.Name), UsedSpace = $($($sysDrive.Used)/1GB), FreeSpace = $($($sysDrive.Free)/1GB) " } #if we are copying to a user specified destination folder, HRESULT will have error incase of diskfull, no need to print folder sizes } } } Trace-Progress "$functionName Complete.." } function Extract-CompressedFiles { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] $DestPathWithRoleName, [Parameter(Mandatory=$false)] [string] $ChildFolder ) $functionName = $($MyInvocation.MyCommand.Name) Trace-Progress "$functionName : DestPathWithRoleName = $DestPathWithRoleName ChildFolder = $ChildFolder" $searchFolder = Join-Path -Path $DestPathWithRoleName -ChildPath $ChildFolder $count = 0 do { $zipFiles = Get-CompressedFiles -SearchFolder $searchFolder -CompressionType "zip" $cabFiles = Get-CompressedFiles -SearchFolder $searchFolder -CompressionType "cab" $tarFiles = Get-CompressedFiles -SearchFolder $searchFolder -CompressionType "tar" $count += 1 foreach ($zipFile in $zipFiles) { try { Extract-ZipFile -ZipFile $zipFile } catch { Trace-Progress "$functionName : ZIP file $($zipFile.FullName) exception during processing: $($_.Exception.ToString())" Trace-Progress "$functionName : ZIP file $($zipFile.FullName) exception during processing: $($_.Exception.Message)" -Error } } foreach ($cabFile in $cabFiles) { try { Extract-CabFile -CabFile $cabFile } catch { Trace-Progress "$functionName CAB file $($cabFile.FullName) exception during processing: $($_.Exception.ToString())" Trace-Progress "$functionName CAB file $($cabFile.FullName) exception during processing: $($_.Exception.Message)" -Error } } foreach ($tarFile in $tarFiles) { try { Extract-TarFile -TarFile $tarFile } catch { Trace-Progress "$functionName TAR file $($tarFile.FullName) exception during processing: $($_.Exception.ToString())" Trace-Progress "$functionName TAR file $($tarFile.FullName) exception during processing: $($_.Exception.Message)" -Error } } } # Break after 10 round of while loop, since if compressed file cannot be deleted for some reason, loop would go on forever. while (($zipFiles.Count -gt 0 -or $cabFiles.Count -gt 0 -or $tarFiles.Count -gt 0) -and $count -lt 10) Trace-Progress "$functionName Complete.." } # # Search for zip files in the input directory and extract the content in to <filename>_zip directory and delete the zip files. # function Extract-ZipFile { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [System.IO.FileInfo] $ZipFile ) $functionName = $($MyInvocation.MyCommand.Name) Trace-Progress "$functionName : Processing ZIP file $($zipFile.FullName)" $zipDirectoryPath = Join-Path -Path $zipFile.Directory -ChildPath ($zipFile.BaseName+"_ZIP") Trace-Progress "$functionName : Going to create extract folder $zipDirectoryPath for ZIP file $($zipFile.FullName)" Expand-Archive -LiteralPath $zipFile.FullName -DestinationPath $zipDirectoryPath $ZipFileOpened = [System.IO.Compression.ZipFile]::Open($zipFile.FullName,[System.IO.Compression.ZipArchiveMode]::Read) $internalFile = $ZipFileOpened.Entries | Where-Object {-not [string]::IsNullOrEmpty($_.Name)} $internalFileCount = $internalFile.Count $ZipFileOpened.Dispose() $extractedFiles = Get-ChildItem -Path $zipDirectoryPath -Filter "*.*" -File -Recurse Trace-Progress "$functionName : ZIP file $($zipFile.FullName) Internal File count $internalFileCount extracted file count $($extractedFiles.Count)" Remove-Item $zipFile.FullName -Force -Recurse } # # Search for cab files in the input directory and extract the content in to <filename>_cab directory and delete the cab files. # function Extract-CabFile { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [System.IO.FileInfo] $CabFile ) $functionName = $($MyInvocation.MyCommand.Name) Trace-Progress "$functionName Processing CAB file $($cabFile.FullName)" Add-Type -Path "$PSScriptRoot\..\Microsoft.Deployment.Compression.Cab.dll" -ErrorAction Ignore -Verbose:$false | Out-Null $cabObject = New-Object -TypeName "Microsoft.Deployment.Compression.Cab.CabInfo" -ArgumentList $cabFile.FullName $cabDirectoryPath = Join-Path -Path $cabFile.Directory -ChildPath ($cabFile.BaseName+"_CAB") Trace-Progress "$functionName Going to create extract folder $cabDirectoryPath for CAB file $($cabFile.FullName)" $temp = New-Item -Path $cabDirectoryPath -ItemType Directory $cabObject.Unpack($cabDirectoryPath) $internalFileCount = $cabObject.GetFiles().Count $extractedFiles = Get-ChildItem -Path $cabDirectoryPath -Filter "*.*" -File -Recurse Trace-Progress "$functionName CAB file $($cabFile.FullName) Internal File count $internalFileCount extracted file count $($extractedFiles.Count)" $cabObject.Delete() } # # Search for tar files in the input directory and extract the content in to <filename>_tar directory and delete the tar files. # function Extract-TarFile { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [System.IO.FileInfo] $TarFile ) $functionName = $($MyInvocation.MyCommand.Name) Trace-Progress "$functionName Processing TAR file $($tarFile.FullName)" $tarFileBaseName = $tarFile.BaseName.Replace(".", "") $tarDirectoryPath = Join-Path -Path $tarFile.Directory -ChildPath ($tarFileBaseName+"_TAR") Trace-Progress "$functionName Going to create extract folder $tarDirectoryPath for TAR file $($tarFile.FullName)" $temp = New-Item -Path $tarDirectoryPath -ItemType Directory tar.exe -xf $tarFile.FullName --directory $tarDirectoryPath $internalFileCount = (tar tvf $tarFile.FullName).Count $extractedFiles = Get-ChildItem -Path $tarDirectoryPath -Recurse Trace-Progress "$functionName TAR file $($tarFile.FullName) Internal File count $internalFileCount (includes directories). Extracted file count $($extractedFiles.Count). (Includes directories)" $tarFile.Delete() } function Get-CompressedFiles { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string] $SearchFolder, [Parameter(Mandatory=$true)] [string] $CompressionType ) $functionName = $($MyInvocation.MyCommand.Name) Trace-Progress "$functionName Going to search $CompressionType files under searchFolder = $searchFolder" $filter = "*." + $CompressionType if ($CompressionType -ieq "tar") { $filter += "*" } $compressedFiles = Get-ChildItem -Path $searchFolder -Filter $filter -File -Recurse -ErrorAction Ignore if ($CompressionType -ieq "zip") { $compressedFiles = $compressedFiles | Where-Object {-not $_.FullName.EndsWith("etl.zip")} } Trace-Progress "$functionName $CompressionType files count $($compressedFiles.Count) under searchFolder = $searchFolder" return $compressedFiles } # # Creates a PowerShell Session if needed. # function Initialize-PSSession { [CmdletBinding()] param( [parameter(Mandatory=$false)] [HashTable] [ValidateNotNull()] $ComputerPSSessions, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [string] $ComputerFqdn, [Parameter(Mandatory=$false)] [REF]$ExcludedEndpoints ) $functionName = $($MyInvocation.MyCommand.Name) if ($ComputerPSSessions) { if ($ComputerPSSessions.ContainsKey($ComputerFqdn)) { $session = $ComputerPSSessions[$ComputerFqdn] if (($null -ne $session) -and ($session.State -ne "Opened")) { Trace-Progress "$functionName : The session for $ComputerFqdn went into $($session.state) state! Reinitializing!" } } } if ($null -eq $session -or $session.State -ne "Opened") { # Client call for new PS session can hang forever when server side WSMan layer is not responding.To unblock log collection, # we are testing the PS session creation in different thread using start-job if the monitoring job doesn’t return the PS session object in 2 min we declare the server to be in a bad state. $scriptBlock = [ScriptBlock]::Create(${function:Test-PSSession}) $psSessionObject = Invoke-ScriptBlockCommand -ScriptBlock $scriptBlock -ArgumentList $ComputerFqdn -TimeOutInSec 120 # Validate if the server is connectable if (($null -eq $psSessionObject -or $psSessionObject.State -ne 'Opened') -or (!(Test-Connection -ComputerName $ComputerFqdn -Quiet))) { Trace-Progress -Message "$functionName : Computer $ComputerFqdn is unreachable, Could not establish a PS session earlier. Will not retry" -Error $ExcludedEndpoints.Value += $ComputerFqdn return $null } <# New-PSSessionOption paramter: .IdleTimeout : Determines how long the session stays open if the computer does not receive any communication. This includes the heartbeat signal {It means if no operation is happening, session will be open as long as session connection is established and it will help us from create PS session timeout} .OperationTimeout - Determines the maximum time that any operation in the session can run. {This prevent very large file like +25GB copy operation and help us from diskspace issue} .MaxConnectionRetryCount :Specifies the number of times that PowerShell attempts to make a connection to a target machine if the current attempt fails due to network issues. #> $sessionOptions = New-PSSessionOption -OperationTimeout ([timespan]"00:10:00").TotalMilliseconds -MaxConnectionRetryCount 1 -IdleTimeout 600000 $session = New-PSSession -ComputerName $ComputerFqdn -SessionOption $sessionOptions -ErrorAction Continue if ($null -eq $session) { $ExcludedEndpoints.Value += $ComputerFqdn Trace-Progress -Message "$functionName : Could not establish a PS session with the computer $ComputerFqdn." -error } } return $session } <# .SYNOPSIS This is the generic function to execute the command or function as script block in separate powershell thread using start-job and return the job output object. #> function Invoke-ScriptBlockCommand { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [ScriptBlock]$ScriptBlock, [Parameter(Mandatory=$false)] $ArgumentList, [Parameter(Mandatory=$false)] [int]$TimeOutInSec = 120 ) $functionName = $($MyInvocation.MyCommand.Name) $jobOutput = $null # Start and get the monitoring job result try { $monitoringJob = Start-Job -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -Verbose $jobOutput = $monitoringJob | Wait-Job -Timeout $TimeOutInSec | Receive-Job $monitoringJob | Stop-Job $monitoringJob | Remove-Job } catch { Trace-Progress "$functionName : ScriptBlock - $ScriptBlock, failed with an error: $_ " -Error } return $jobOutput } function Test-PSSession { [CmdletBinding()] param( [Parameter(Mandatory=$false)] [string] $ComputerFqdn, [Parameter(Mandatory=$false)] [PSCredential] $LocalAdminCredential ) if($LocalAdminCredential) { $session = New-PSSession -ComputerName $ComputerFqdn -Credential $LocalAdminCredential -ErrorAction Continue } else { $session = New-PSSession -ComputerName $ComputerFqdn -ErrorAction Continue } return $session } <# .SYNOPSIS Return items that match the provided filter conditions for file extensions to include/exclude. #> function Get-ItemsByExtension { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [Object[]] $UnfilteredItems, [Parameter(Mandatory=$true)] [string[]] $Include, [Parameter(Mandatory=$true)] [string[]] $Exclude ) $filteredItems = New-Object System.Collections.Generic.List[System.Object] foreach ($unfilteredItem in $UnfilteredItems) { $excludedItem = $false foreach ($extensionToExclude in $Exclude) { if ($unfilteredItem -like $extensionToExclude) { $excludedItem = $true } } if (-not $excludedItem) { foreach ($extensionToInclude in $Include) { if ($unfilteredItem -like $extensionToInclude) { $filteredItems.Add($unfilteredItem) } } } } return $filteredItems } function Get-ContainerStateLog { param ( [parameter(Mandatory=$false)] [DateTime] $FilesFromDate = (Get-Date).AddHours(-1), [parameter(Mandatory=$false)] [DateTime] $FilesToDate = (Get-Date), [parameter(Mandatory=$true)] [string] $Role, [parameter(Mandatory=$true)] [string] $DestPathWithRoleName ) $containerStateLogDirPath = Join-Path -Path $DestPathWithRoleName -ChildPath "ContainerStateLogs" $containerStateErrorLogPath = Join-Path -Path $containerStateLogDirPath -ChildPath "ContainerStateCollectionErrors.txt" Trace-Progress -Message "Start container state log collection of $Role to $containerStateLogDirPath." New-Item $containerStateLogDirPath -ItemType Directory -Force | Out-Null # Collect HCS state $hcsStateLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "HcsState.txt" Invoke-ExpressionWithTracing -Expression "hcsdiag list" -TraceFilePath $hcsStateLogFilePath # Collect HNS state $hnsNetworksLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "HnsState_Networks.txt" $hnsEndpointsLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "HnsState_Endpoints.txt" $hnsPolicyListLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "HnsState_PolicyList.txt" $hnsNetworksCommands = @( "Get-HnsNetwork | select Name, Type, ActivityId, ID, @{Name='Subnets'; Expression={ `$_.Subnets | select AddressPrefix, GatewayAddress, ID }} | Out-String", "Get-HnsNetwork | ForEach-Object { Get-HnsNetwork -Id `$_.ID -Detailed } | ConvertTo-Json -Depth 20" ) $hnsEndpointsCommands = @( "Get-HnsEndpoint | select ActivityId, ID, IpAddress, MacAddress, State | Format-Table | Out-String", "Get-HnsEndpoint | ConvertTo-Json -Depth 20" ) foreach ($hnsNetworksCommand in $hnsNetworksCommands) { Invoke-ExpressionWithTracing -Expression $hnsNetworksCommand -TraceFilePath $hnsNetworksLogFilePath } foreach ($hnsEndpointsCommand in $hnsEndpointsCommands) { Invoke-ExpressionWithTracing -Expression $hnsEndpointsCommand -TraceFilePath $hnsEndpointsLogFilePath } Invoke-ExpressionWithTracing -Expression "Get-HnsPolicyList | ConvertTo-Json -Depth 20" -TraceFilePath $hnsPolicyListLogFilePath # Collect Docker engine state $dockerStateEngineLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "DockerState-Engine.txt" $dockerEngineStateCommands = @( "docker version", "docker info", "docker ps -sa", "docker images", "docker volume ls", "docker system df -v", "docker network ls" ) foreach ($dockerEngineStateCommand in $dockerEngineStateCommands) { Invoke-ExpressionWithTracing -Expression $dockerEngineStateCommand -TraceFilePath $dockerStateEngineLogFilePath } $networkIds = docker network ls -q foreach ($networkId in $networkIds) { Invoke-ExpressionWithTracing -Expression "docker inspect $networkId" -TraceFilePath $dockerStateEngineLogFilePath } # Collect container specific diagnostics $allContainerIds = docker ps -aq [System.Collections.Generic.HashSet[string]]$runningContainerIds = docker ps -q # List of SF environment variables to include in the output. Other SF environment variable names starting with "Fabric" will be redacted. $sfEnvironmentVariablesToInclude = [System.Collections.Generic.HashSet[string]]@( "Fabric_ApplicationHostId", "Fabric_ApplicationHostType", "Fabric_ApplicationId", "Fabric_ApplicationName", "Fabric_CodePackageName", "Fabric_Endpoint_InstanceEndpoint", "Fabric_Endpoint_IPOrFQDN_InstanceEndpoint", "Fabric_Folder_App_Log", "Fabric_Folder_App_Temp", "Fabric_Folder_App_Work", "Fabric_Folder_Application", "Fabric_Folder_Application_OnHost", "Fabric_IsContainerHost", "Fabric_NodeId", "Fabric_NodeIPOrFQDN" "Fabric_NodeName" "Fabric_PartitionId", "Fabric_ServiceName", "Fabric_ServicePackageActivationId", "Fabric_ServicePackageName", "Fabric_ServicePackageVersionInstance", "Fabric_ContainerName", "FabricCodePath", "FabricLogRoot" ) foreach ($containerId in $allContainerIds) { try { $dockerInspectOutput = docker inspect $containerId | ConvertFrom-Json for ($i = 0; $i -lt $dockerInspectOutput.Config.Env.Count; $i++) { $envVariablePair = $dockerInspectOutput.Config.Env[$i] -split '=', 2 if ($envVariablePair.Length -eq 2) { $envVariableName = $envVariablePair[0] if ($envVariableName -ieq "AZS_DEPLOYMENT_APPLICATION_NAME") { $applicationName = $envVariablePair[1] -replace "/", "+" } elseif ($envVariableName -ieq "AZS_DEPLOYMENT_SERVICE_NAME") { $serviceName = $envVariablePair[1] } if ($envVariableName.StartsWith("Fabric") -and (-not $sfEnvironmentVariablesToInclude.Contains($envVariableName))) { $redactedEnvVariable = "$envVariableName=[redacted]" $dockerInspectOutput.Config.Env[$i] = $redactedEnvVariable } } else { # Unable to parse environment variable string, so will redact it completely to be safe (i.e., by avoiding leaking sensitive information). $dockerInspectOutput.Config.Env[$i] = "[redacted]" } } $containerStateLogFilePath = Join-Path -Path $containerStateLogDirPath -ChildPath "DockerState-${applicationName}_${serviceName}_${containerId}.txt" Add-Content $containerStateLogFilePath "docker inspect $containerId" Add-Content $containerStateLogFilePath $($dockerInspectOutput | ConvertTo-Json -Depth 10) } catch { Add-Content $containerStateErrorLogPath "Error while collecting docker inspect output of $containerId. ExceptionMessage: $($_.Exception.Message), ExceptionType: $($_.Exception.GetType().Name)" } # Collect running container specific diagnostics. if ($runningContainerIds.Contains($containerId)) { $containerStateCommands = @( "docker top $containerId", "docker stats $containerId --no-stream" ) foreach ($containerStateCommand in $containerStateCommands) { Invoke-ExpressionWithTracing -Expression $containerStateCommand -TraceFilePath $containerStateLogFilePath } } } Trace-Progress -Message "Finished container state log collection." } function Invoke-ExpressionWithTracing { param ( [parameter(Mandatory=$true)] [string] $Expression, [parameter(Mandatory=$true)] [string] $TraceFilePath ) try { Add-Content $TraceFilePath $Expression Invoke-Expression $Expression *>&1 | Add-Content -Path $TraceFilePath Add-Content $TraceFilePath "`n" } catch { Add-Content $containerStateErrorLogPath "Error executing '$Expression'. ExceptionMessage: $($_.Exception.Message), ExceptionType: $($_.Exception.GetType().Name)" } } function Get-ServiceFabricLog { [CmdletBinding()] param( [parameter(Mandatory=$false)] [string] $LogPath = "$env:SystemDrive\ServiceFabricLogs", [parameter(Mandatory=$false)] [DateTime] $FromDate = (Get-Date).AddHours(-4), [parameter(Mandatory=$false)] [DateTime] $ToDate = (Get-Date) ) $packagePath = Join-Path $env:SystemDrive -ChildPath "ServiceFabric\Tools\Microsoft.Azure.ServiceFabric.WindowsServer.SupportPackage.zip" # Expand SF Support Package. $toolsDir = Join-Path $env:SystemDrive -ChildPath "ServiceFabric\Tools" $collectorPath = Join-Path $toolsDir "StandaloneLogCollector.exe" if (-not (Test-Path($collectorPath))) { Trace-Progress "$functionName : Unzipping tool to:$collectorPath" Expand-Archive $packagePath -DestinationPath $toolsDir -Force } $timeStamp = $((Get-Date).ToString('yyyyMMddHHmmss')) $outputPath = "$env:SystemDrive\MASLogs\StandaloneLogCollector_StdOut_$timeStamp.txt" # Perform the log directory cleanup only in default path case to prevent security vulnerability if (Test-Path $LogPath) { Trace-Progress "$functionName : Removing existing ServiceFabric logs in:$LogPath" $null = Remove-Item -Path $LogPath -Recurse -Force } #Bugfix: StandaloneLogCollector.exe Fails With "Unable to load DLL 'FabricClient.dll'" if ( $env:Path -notlike "*C:\Program Files\Microsoft Service Fabric\bin\fabric\fabric.code*") { $env:Path = $env:Path + ";C:\Program Files\Microsoft Service Fabric\bin\fabric\fabric.code" } . $collectorPath -Output $LogPath -Mode Collect -StartUtcTime $FromDate.ToUniversalTime() -EndUtcTime $ToDate.ToUniversalTime() -IncludeLeaseLogs > $outputPath 2>&1 Copy-Item -Path $outputPath -Destination $LogPath\ -Force -ErrorAction Continue $miscellaneousLogsZipPath = Join-Path $LogPath "miscellaneousLogs.zip" $miscellaneousLogsPath = Join-Path $LogPath "miscellaneousLogs" Trace-Progress "$functionName : Unzipping $miscellaneousLogsZipPath" Expand-Archive -Path $miscellaneousLogsZipPath -DestinationPath $miscellaneousLogsPath -Force Trace-Progress "$functionName : Removing $miscellaneousLogsZipPath after unzipping" $null = Remove-Item -Path $miscellaneousLogsZipPath -Force # Clean up the duplicate EventLogs folder as it separately collected along with Event_Application, Event_System $miscellaneousEventLogsPath = Join-Path $miscellaneousLogsPath "EventLogs" Trace-Progress "$functionName : Removing $miscellaneousEventLogsPath after unzipping" $null = Remove-Item -Path $miscellaneousEventLogsPath -Force -Recurse } Function Get-RoleLogs { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [PSCustomObject] $argumentsObject, [Parameter(Mandatory=$true)] [String] $role ) $OutputPath = $argumentsObject.OutputPath $FromDateG = $argumentsObject.FromDateG $ToDateG = $argumentsObject.ToDateG $FromDate = $argumentsObject.FromDate $ToDate = $argumentsObject.ToDate $roles = $argumentsObject.roles $domain = $argumentsObject.domain $destPath = $argumentsObject.destPath $roleNames = $argumentsObject.roleNames $nodeNames = $argumentsObject.nodeNames $filterByNode = $argumentsObject.FilterByNode $vmRoleNames = $argumentsObject.vmRoleNames $FilterByLogType = $argumentsObject.FilterByLogType $allClusterInfo = $argumentsObject.allClusterInfo $localMode = $argumentsObject.localMode $isArcA = $argumentsObject.isArcAEnv $saveToPathSelected = $argumentsObject.SaveToPathSelected $functionName = "$($MyInvocation.MyCommand.Name)_$role" $perfRoleStartDate = Get-Date $roleLogDetails = @{"role" = $role; "StartDate" = $perfRoleStartDate} try { Write-Output "`r" Trace-Progress "$functionName : Collecting logs for role: $role" #Trace-InvokingProcessStats -Role ($role+"_Start") if (!$localMode) { $endpointPSSessions = @{} $ExcludedEndpoints = @() } # TODOTODO Override the nodes with node names passed #$nodes = $roles[$role].Nodes $nodes = @() $currentRoleData = $roles[$role] if ("PhysicalMachines" -in $currentRoleData.Nodes) { $nodes += $nodeNames } if ("AllVms" -in $currentRoleData.Nodes -and $vmRoleNames["AllVms"].count -gt 0) { $nodes += $vmRoleNames["AllVms"] } elseif ($role -in $currentRoleData.Nodes -and $vmRoleNames[$role].count -gt 0) { $nodes += $vmRoleNames[$role] } Trace-Progress "$functionName : Nodes to collect logs from for role [$role] = [$($nodes -join ', ')]. Note that this is before applying node filter." <# Thave above elseif should resolve to following code, if there are more specialized roles get added and their rolename is not same as the defined in Get-InfraVMNames() we need to remove the above elseif and update below cases. elseif ("NC" -in $currentRoleData.Nodes) { $nodes += $vmRoleNames["NC"] } elseif ("SLB" -in $currentRoleData.Nodes) { $nodes += $vmRoleNames["SLB"] } elseif ("GWY" -in $currentRoleData.Nodes) { $nodes += $vmRoleNames["GWY"] }#> $logsTobeCollected = (($currentRoleData.FileLog.count -gt 0) -or ($currentRoleData.CSVLog.count ) -or ($currentRoleData.WindowsEventLog.count ) -or ($null -eq $currentRoleData.ScriptExecution)) # $rolePublicInfoLogs -- This is the xml node will <Logs></Logs> if ($logsTobeCollected) { $roleLogDetails.logsAvailable = $true Trace-Progress "$functionName : Destination path : $OutputPath" $destinationFolderPath = Join-Path -Path $OutputPath -ChildPath $role # Iterate over each end-point and collect logs if ($localMode) { $node = $env:ComputerName $endpoint = if ($null -eq $domain) { $node } else { "$node.$domain" } $endpoints = @($endpoint) } else { $endpoints = @() } if ($filterByNode) { Trace-Progress "$functionName : Node filter list = $($filterByNode -join ', ')" $nodes = $nodes | Where-Object { $_ -in $filterByNode} Trace-Progress "$functionName : Node list after applying node filter = $($nodes -join ', ')" } else { Trace-Progress "$functionName : No node filter specified" } if (!$localMode) { foreach ($node in $nodes) { $session = $null $endpoint = $node + ".$domain" Trace-Progress "$functionName : Creating a PSSession to $endpoint" if ($ExcludedEndpoints -contains $endpoint) { Trace-Progress -Message "$functionName : Could not establish a PS session earlier with the computer $endpoint. Will not retry." -Error } else { $session = Initialize-PSSession -ComputerPSSessions $endpointPSSessions -ComputerFqdn $endpoint -ExcludedEndpoints ([REF]$ExcludedEndpoints) if ($null -ne $session) { $endpointPSSessions[$endpoint] = $session $endpoints += $endpoint } } } Trace-Progress "$functionName : nodescount = [$($nodes.count)] endpointPSSessions count = [ $($endpointPSSessions.Count)], endpoints Count = [$($endpoints.Count)]" } if ($role -in "ServiceFabric") { if ($isArcA -and $localMode) { # the sf log collector only supports max path length of 35, so we collect logs in temp short path dir and then move it. $SFLogTempPath = "$env:SystemDrive\MASLogs\ServiceFabricLogs" Trace-Progress -Message "$functionName : ServiceFabric logs will be temporarily output to:$SFLogTempPath" Get-ServiceFabricLog -LogPath $SFLogTempPath -FromDate $FromDate -ToDate $ToDate Trace-Progress -Message "$functionName : Move ServiceFabric logs from $SFLogTempPath to $OutputPath" Move-Item $SFLogTempPath $OutputPath } } # $endpoints is an empty array if there are no endpoints, it is not null. if($endpoints -gt 0) { # Collecting Windows event logs if ($FilterByLogType -contains 'WindowsEvent') { if ($currentRoleData.WindowsEventLog) { $logPattern = $currentRoleData.WindowsEventLog Trace-Progress -Message "$functionName : Collecting windows event logs with log patterns: $($logPattern -join ', '), with date range: from $FromDateG until $ToDateG, from machines $($endpoints -join ', ')" try { if ($localMode) { Get-WindowsEventLog -ComputerNames $endpoints -LogPattern $logPattern -EventsFromDate $FromDate -EventsToDate $ToDate -Roles $roles -CurrentRole $role ` -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode } else { Get-WindowsEventLog -ComputerNames $endpoints -ComputerPSSessions $endpointPSSessions -LogPattern $logPattern -EventsFromDate $FromDate -EventsToDate $ToDate ` -ExcludedEndpoints ([REF]$ExcludedEndpoints) -Roles $roles -CurrentRole $role -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode } Trace-Progress "$functionName : Successfully dumped and copied all the windows event log from individual machines to $destinationFolderPath" } catch { Trace-Progress "$functionName : Failed during windows event log collection $($_.Exception.Message)" -Error } } } else { Trace-Progress -Message "$functionName : Skipping WindowsEventLog collection." } # Collecting log files. if ($FilterByLogType -contains 'File') { if ($currentRoleData.FileLog) { $sourceLogPaths = foreach($entry in $currentRoleData.FileLog) { $entry Trace-Progress -Message "$functionName : Collecting files from '$($entry)'." } try { if ($localMode) { Get-FileLog -ComputerNames $endpoints -SourceLogFilePaths $sourceLogPaths -FilesFromDate $FromDate -FilesToDate $ToDate -Role $role ` -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode -IsArcA $isArcA -SaveToPathSelected $saveToPathSelected } else { Get-FileLog -ComputerNames $endpoints -ComputerPSSessions $endpointPSSessions -SourceLogFilePaths $sourceLogPaths -FilesFromDate $FromDate -FilesToDate $ToDate ` -Role $role -ExcludedEndpoints ([REF]$ExcludedEndpoints) -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode -IsArcA $isArcA -SaveToPathSelected $saveToPathSelected } } catch { Trace-Progress "$functionName : Failed during File log collection $($_.Exception.Message)" -Error } } } else { Trace-Progress -Message "$functionName : Skipping FileLog collection." } # Collecting container state. if ($FilterByLogType -contains 'ContainerState') { try { if ($isArcA -and $role -eq "MASLogs") { Get-ContainerStateLog -FilesFromDate $FromDate -FilesToDate $ToDate -Role $role -DestPathWithRoleName $destinationFolderPath } else { Trace-Progress -Message "$functionName : Skipping ContainerState collection for non-ArcA MASLogs." } } catch { Trace-Progress "$functionName : Failed during ContainerState collection $($_.Exception.Message)" -Error } } else { Trace-Progress -Message "$functionName : Skipping ContainerState collection." } if (!$localMode) { #Remove PSSessions Trace-Progress -Message "$functionName : Role : $role, Removing PS Sessions." foreach ($psSession in $endpointPSSessions.Values) { if ($null -ne $psSession) { Remove-PSSession -Session $psSession -ErrorAction SilentlyContinue } } } } if ($FilterByLogType -contains 'CSV') { if ($currentRoleData.CSVLog) { if ($localMode) { $isPrimaryNode = $false # if LocalMode, each node is doing it's own log collection in parallel. Only want the primary node to collect CSV logs. Trace-Progress -Message "$functionName : In Local Mode. Determining primary node, so that only primary node collects CSV logs" try { $nodes = Get-ClusterNode | Where-Object {$_.State -ieq "Up" } | Sort-Object -Property Name $primaryNode = $nodes[0].Name.ToLower() $isPrimaryNode = $primaryNode -eq ($env:COMPUTERNAME).ToLower() if ($isPrimaryNode) { Trace-Progress -message "$functionName : This is the primary node. This node will collect CSV logs." } else { Trace-Progress -message "$functionName : This is not the primary node. This node will not collect CSV logs." } } catch { # If we can't get primary node, it is likely deployment failed before cluster creation. In this case, # there would be no CSV Logs in cluster storage, as cluster storage is not available. # Even if there were race conditions in copying over CSV Logs, it would not cause log collection to fail. Trace-Progress -message "$functionName : Error getting primary node : $_ Will collect CSV logs on this node." $isPrimaryNode = $true } } if (!$localMode -or $isPrimaryNode) { $sourceLogPaths = foreach($entry in $currentRoleData.CSVLog) { $entry Trace-Progress -Message "$functionName : Collecting CSV files from '$entry'." } try { Get-FileLog -SourceLogFilePaths $sourceLogPaths -FilesFromDate $FromDate -FilesToDate $ToDate -Role $role -CSVLogsFolderName "CSVLogs" ` -DestPathWithRoleName $destinationFolderPath -LocalMode $localMode -IsArcA $isArcA -SaveToPathSelected $saveToPathSelected } catch { Trace-Progress "$functionName : Failed during CSV log collection $($_.Exception.Message)" -Error } } } } else { Trace-Progress -Message "$functionName : Skipping CSV Log collection." } if (($FilterByLogType -contains 'Script') -and ($null -ne $currentRoleData.ScriptExecution)) { Trace-Progress -Message "$functionName : $role defined ScriptExecution, call the script to collect log." # Get the PS1 file location. $ScriptInfo = $currentRoleData.ScriptExecution $scriptPath = $ScriptInfo.ScriptPath if ($false -eq [string]::IsNullOrEmpty($scriptInfo.NugetName)) { # The script execution specified NugetName, try to get it from t The command exported by CloudCommon module. $nugetRootPath = Get-ASArtifactPath -NugetName $scriptInfo.NugetName -ErrorAction Ignore $scriptPath = Join-Path $nugetRootPath $ScriptInfo.ScriptPath } Trace-Progress -Message "$functionName : Got $role log collecting script location: $scriptPath " # check the script file exists, and the script only support PS1 file # We expect the script has 3 parameters: # [Parameter(Mandatory = $true, HelpMessage = "The current role of logCollection; in script, we pass in for log purpose.")] # [string] $Role, # [parameter(Mandatory = $true, HelpMessage = "The Destination path to save the logCollection output.")] # [string] $DestPathWithRoleName, # [Parameter(Mandatory = $true, HelpMessage = "augumentsObject contains log collection parameters")] # [PSCustomObject] $argumentsObject if ($true -eq (Test-Path -Path $scriptPath -PathType Leaf) -and (".ps1" -eq [System.IO.Path]::GetExtension($scriptPath)) ) { $scriptParameter = @{ RoleName = $role LogOutputPathWithRoleName = $destinationFolderPath argumentsObject = $argumentsObject } # Save the script transcript to destination folder. $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($scriptPath) $transcriptPath = "$destinationFolderPath\Script_Output_$scriptName.log" $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() $outputContent = "" try { Trace-Progress -Message "$functionName : Calling script [$scriptPath] to save logs to $destinationFolderPath." Start-Transcript -Path $transcriptPath -Verbose # call the script. with target parameters. # TODOTODO: The customized script may hang, we need to limit the execution time and space usage. $output = & $scriptPath @scriptParameter $outputContent = $output | ConvertTo-Json } catch { $exceptionContent = $_.Exception.ToString() Trace-Progress -Message "$functionName : Failed to call script [$scriptPath] for $role. Exception [$exceptionContent]" -Warning } finally { $stopWatch.stop() Stop-Transcript Add-Content -Path $transcriptPath -Value "Script Output: `n $outputContent" Trace-Progress -Message "$functionName : Executing script [$scriptPath] for $role finished, used $($stopwatch.Elapsed.TotalSeconds) seconds." } } else { Trace-Progress -Message "$functionName : Didn't find script at $scriptPath for $role, or it is not a valid ps1 file, skipping script collection." -Warning } } else { Trace-Progress -Message "$functionName : No Script need to run for $role, Skipping script collection." -Warning } } else { $roleLogDetails.logsAvailable = $false Trace-Progress -Message "$functionName : No logs collected for this role as none is specified in input configuration file." } $normalTermination = $true } catch { Trace-Progress -Message "$functionName : Collecting logs failed with error: $_" -Error Trace-Progress -Message "$functionName : StackTrace : $($PSItem.ScriptStackTrace)" -Error $normalTermination = $true } finally { if (!$localMode) { Trace-Progress -Message "$functionName : Role: $role cleaningup endpointPSSessions, current opened sessions count = [$($endpointPSSessions.Values.Count)] " foreach ($psSession in $endpointPSSessions.Values) { if ($null -ne $psSession) { Trace-Progress -Message "$functionName : Removing session = [$psSession] " Remove-PSSession -Session $psSession -ErrorAction SilentlyContinue } } } if ($normalTermination -ne $true) { Trace-Progress -Message "$functionName : $role : unclean exit detected " -Error Trace-Progress -Message "$functionName : $role : Wait 30 seconds for child jobs to complete" Start-Sleep 30 # incase of an unclean exit, give time for sub jobs to complete before exiting parent job #[environment]::Exit(0) #$ZippingJobs.Values | Remove-Job -force } Write-ErrorsIfExist -Role $role #Trace-InvokingProcessStats -Role ($role+"_End") $roleLogCollectionTime = ((Get-Date) - $perfRoleStartDate).TotalMinutes.ToString("0.0##") Trace-Progress -Message "$functionName : Time taken to collect role $role is [$roleLogCollectionTime] Minutes" } } # This method prints the $global:errorList in the calling process (each role and resource provider collection runs as a separate Process) # Ensure this is called almost at the end of the job/role collection function Write-ErrorsIfExist { Param ( [parameter(Mandatory=$true)] [string] $Role ) $functionName = $($MyInvocation.MyCommand.Name) + "_$Role" # this variable is created when any trace-progress with -error is invoked. # Each role runs in its own process, so we can clear the $error automatic variable as well. if (((Test-Path variable:global:errorList) -and $Global:errorList -ne "") -or $Error.Count -gt 0) { Trace-Progress -Message "$functionName : Total entries in Global error list = $($Global:errorList.count)" $errorMessage = "ErrorList: `n" + $Global:errorList + ($Error | Get-Unique | Out-String) Write-Host $errorMessage -ForegroundColor "Red" #Dont change this to trace-progress Trace-Progress -Message $errorMessage $Error.Clear() } else { Trace-Progress -Message "$functionName : No Errors during role $Role" } } function Get-FreeSpace { Param ( [parameter(Mandatory=$true)] [string]$RelativePath ) $destinationFolder = Get-Item -Path (Split-Path $RelativePath -Parent) $fsobuild = new-Object -comobject Scripting.FileSystemObject $destinationFolderObj = $fsobuild.GetFolder($destinationFolder) $freeSpaceBytes = $destinationFolderObj.Drive.FreeSpace $freeSpaceKb = $freeSpaceBytes / 1024 return $freeSpaceKb } Export-ModuleMember -Function Get-FreeSpace Export-ModuleMember -Function Invoke-ScriptBlockWithRetries Export-ModuleMember -Function Write-ErrorsIfExist Export-ModuleMember -Function Get-RoleLogs Export-ModuleMember -Function Get-WindowsEventLog Export-ModuleMember -Function Collect-WindowsEventLogs Export-ModuleMember -Function Get-FileLog Export-ModuleMember -Function Get-FilteredChildItem Export-ModuleMember -Function Copy-FilteredChildItem Export-ModuleMember -Function Initialize-PSSession Export-ModuleMember -Function Test-PSSession Export-ModuleMember -Function Invoke-ScriptBlockCommand # SIG # Begin signature block # MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAL344ag4x4DX/i # EOqcMUn6WYScoxPxrZyrLBSpw+z4nKCCDXYwggX0MIID3KADAgECAhMzAAADrzBA # DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA # hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG # 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN # xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL # go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB # tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd # mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ # 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY # 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp # XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn # TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT # e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG # OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O # PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk # ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx # HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt # CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # 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 # /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIKBT1xuXN/QWVboDEQk5etnw # wpU2WDnCfsugnSB5FcZ+MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAZxw+KKOcGrNC0SfN8IrlEtJ/UkjXfQdWjV2rIRmQZwjn1sXGHe/1+f1o # J7aBFrb3dAd0ezNmuhhewJFLj+jJzrC3dhYu92k6MqExQRCyfkBvBPQxQ1qgsSRx # le/WkLKVgXws8lR975SjwLpjq15+t/hwUuamFPRDihcY9DA+WcQDCTYR6F/cY/zN # hemF1KQL4N/mkC5Ml2aw/QLp7Y5bb79gO5vWiC0wW6hUKwuEViJ+VszGSSSb3gTw # Uf3DP/i7nh6AAdcg4xT55TBmwOLtH4GKMyhKXkiOqKgocEtghQcixtXlg8pnJ9Oj # 4DY9R6Hl3fLffVbpKKYX3SLJR7xGRKGCFykwghclBgorBgEEAYI3AwMBMYIXFTCC # FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq # hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCBapgV/6fp7GE8TBYQV9vE/K3UQxsYjrkk/rpNCdMOE9gIGZjOt3LML # GBMyMDI0MDUxNjE4NDQ0Ni43MDhaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO # OkZDNDEtNEJENC1EMjIwMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT # ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAHimZmV8dzjIOsAAQAAAeIwDQYJ # KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx # MDEyMTkwNzI1WhcNMjUwMTEwMTkwNzI1WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl # cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpGQzQxLTRC # RDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVjtZhV+kFmb8cKQpg2mzis # DlRI978Gb2amGvbAmCd04JVGeTe/QGzM8KbQrMDol7DC7jS03JkcrPsWi9WpVwsI # ckRQ8AkX1idBG9HhyCspAavfuvz55khl7brPQx7H99UJbsE3wMmpmJasPWpgF05z # ZlvpWQDULDcIYyl5lXI4HVZ5N6MSxWO8zwWr4r9xkMmUXs7ICxDJr5a39SSePAJR # IyznaIc0WzZ6MFcTRzLLNyPBE4KrVv1LFd96FNxAzwnetSePg88EmRezr2T3HTFE # lneJXyQYd6YQ7eCIc7yllWoY03CEg9ghorp9qUKcBUfFcS4XElf3GSERnlzJsK7s # /ZGPU4daHT2jWGoYha2QCOmkgjOmBFCqQFFwFmsPrZj4eQszYxq4c4HqPnUu4hT4 # aqpvUZ3qIOXbdyU42pNL93cn0rPTTleOUsOQbgvlRdthFCBepxfb6nbsp3fcZaPB # fTbtXVa8nLQuMCBqyfsebuqnbwj+lHQfqKpivpyd7KCWACoj78XUwYqy1HyYnStT # me4T9vK6u2O/KThfROeJHiSg44ymFj+34IcFEhPogaKvNNsTVm4QbqphCyknrwBy # qorBCLH6bllRtJMJwmu7GRdTQsIx2HMKqphEtpSm1z3ufASdPrgPhsQIRFkHZGui # hL1Jjj4Lu3CbAmha0lOrAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQURIQOEdq+7Qds # lptJiCRNpXgJ2gUwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD # VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j # cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG # CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw # MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD # CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAORURDGrVRTbnulf # sg2cTsyyh7YXvhVU7NZMkITAQYsFEPVgvSviCylr5ap3ka76Yz0t/6lxuczI6w7t # Xq8n4WxUUgcj5wAhnNorhnD8ljYqbck37fggYK3+wEwLhP1PGC5tvXK0xYomU1nU # +lXOy9ZRnShI/HZdFrw2srgtsbWow9OMuADS5lg7okrXa2daCOGnxuaD1IO+65E7 # qv2O0W0sGj7AWdOjNdpexPrspL2KEcOMeJVmkk/O0ganhFzzHAnWjtNWneU11WQ6 # Bxv8OpN1fY9wzQoiycgvOOJM93od55EGeXxfF8bofLVlUE3zIikoSed+8s61NDP+ # x9RMya2mwK/Ys1xdvDlZTHndIKssfmu3vu/a+BFf2uIoycVTvBQpv/drRJD68eo4 # 01mkCRFkmy/+BmQlRrx2rapqAu5k0Nev+iUdBUKmX/iOaKZ75vuQg7hCiBA5xIm5 # ZIXDSlX47wwFar3/BgTwntMq9ra6QRAeS/o/uYWkmvqvE8Aq38QmKgTiBnWSS/uV # PcaHEyArnyFh5G+qeCGmL44MfEnFEhxc3saPmXhe6MhSgCIGJUZDA7336nQD8fn4 # y6534Lel+LuT5F5bFt0mLwd+H5GxGzObZmm/c3pEWtHv1ug7dS/Dfrcd1sn2E4gk # 4W1L1jdRBbK9xwkMmwY+CHZeMSvBMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ # mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT # Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh # dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1 # WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB # BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK # NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg # fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp # rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d # vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9 # 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR # Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu # qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO # ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb # oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6 # bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t # AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW # BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb # UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku # aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA # QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2 # VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu # bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw # LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93 # d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt # MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q # XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6 # U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt # I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis # 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp # kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0 # sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e # W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ # sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7 # Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0 # dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ # tB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh # bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpG # QzQxLTRCRDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy # dmljZaIjCgEBMAcGBSsOAwIaAxUAFpuZafp0bnpJdIhfiB1d8pTohm+ggYMwgYCk # fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF # AOnwn2IwIhgPMjAyNDA1MTYyMzA2MTBaGA8yMDI0MDUxNzIzMDYxMFowdDA6Bgor # BgEEAYRZCgQBMSwwKjAKAgUA6fCfYgIBADAHAgEAAgIYmzAHAgEAAgITVDAKAgUA # 6fHw4gIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID # B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAISJNra7Ggw4B/8Tn4yp # x+hXDg8/G8srkcJl7vwzRVbuS6W9fR0YCSMS6fkjFayeozoQe5FrKcY1U7cZw3XC # UhSsQGfYXh1matPvCIPXbjvnphAuKz0TdujD/q+eE6Gqa++ey0goT0AYhcfDRwvx # gxxBejaOpgYnpl40jU7s7YN9MYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt # U3RhbXAgUENBIDIwMTACEzMAAAHimZmV8dzjIOsAAQAAAeIwDQYJYIZIAWUDBAIB # BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx # IgQgYTx/m7EnhKIQb7RmkryxKwUK4vKWVHds0K/a9ZqT0F8wgfoGCyqGSIb3DQEJ # EAIvMYHqMIHnMIHkMIG9BCAriSpKEP0muMbBUETODoL4d5LU6I/bjucIZkOJCI9/ # /zCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB4pmZ # lfHc4yDrAAEAAAHiMCIEILplb72oW4+73iQROSoFPO5HtCiiSLulx8XotCt2EMse # MA0GCSqGSIb3DQEBCwUABIICAAajFu1RLbb5qf/Gz0lydB9miP4dFMz/LlkSSvV2 # cTuoG/cmUIHE5aS3aIvma//70yWGLWFrSeB3ILIcHH7esqQAJ/tGRVZw3RWr3uV+ # 9xMZSdlXNdDvqmn3xZzsX5z6l9jCEpTynsWJ3isoZAVqqnYPK4DBBqWsrqyVlgn9 # NxdbrKbxvXj1miZKnXpxuoxhBV1FCIo6lHaJKZ3UOqupaJHGynjG+maV82hXMMQf # Ct2d70jJHx1Dk+xw9vX+yPahO7HqS5IqyaKWih8IkUUl0qGMRstzA46XOREmPaIu # DO1vQgTrL5VoxLb3zoqVcsxtZkOIJXoWrjTkBqe+V0tqGCdGJopUSzSBwT4lB8Cc # aVcnFAL2AmdXN+fKunWWX+2hE7ljUl2SpTInx4A1JfLaKqwHc2h+7lhstCAlAIbq # H0/qQPchlmlKftg8Dsc/9DI5DBJb+e9+ZmbiTd7tn02HDv5TXSnG0E4/sC7vUwBH # B8DLD6m6X8u4KaP42yGvBuAL62u0unz5SJp5cX3mopYo0t6okYb0Cn6zaQsKE/zR # ndkvdyHt+fuGQlbgnpOOHgsE0Vku5JrqnF8M42lMbACJ9r8bzzY+ygW58Pm/Lm6W # L+L3jgUgfKx280NO2Dz+W7Z1wCbLsZ7semtKwE3chkZZi0a0JQ3pGimYv0z3rt2Q # t5YW # SIG # End signature block |