modules/AzStack.Utilities/AzStack.Utilities.psm1
<################################################################
# # # Copyright (C) Microsoft Corporation. All rights reserved. # # # ################################################################> Using module .\AzStack.Utilities.Helper.psm1 Import-Module $PSScriptRoot\AzStack.Utilities.Helper.psm1 Import-LocalizedData -BindingVariable 'msg' -BaseDirectory "$PSScriptRoot\locale" -UICulture "en-US" # due to issue with how snap-ins work with JEA endpoints # need to remove it, then re-import the Microsoft.PowerShell.Utility module so we can expose the functions such as Import-PowerShellDataFile if (Get-PSSnapin | Where-Object { $_.Name -eq 'Microsoft.PowerShell.Utility' }) { Remove-PSSnapin -Name Microsoft.PowerShell.Utility } Import-Module Microsoft.PowerShell.Utility <# ┌──────────────────────────────────────────────────────────────────────────┐ │ Here our HCI helper functions start. │ └──────────────────────────────────────────────────────────────────────────┘ #> function Trace-CheckResult() { param ( [string] $checkName, [CheckStatus] $checkState, [string] $desc, [string] $details, [string] $url ) $color = [ConsoleColor]::DarkBlue # we now support different states (besides pass / fail) switch ($checkState) { ([CheckStatus]::Pass) { $color = [ConsoleColor]::Green; $details = "Validation successfull" } ([CheckStatus]::Fail) { $color = [ConsoleColor]::Red } ([CheckStatus]::Warning) { $color = [ConsoleColor]::DarkYellow } ([CheckStatus]::Info) { $color = [ConsoleColor]::Gray } Default {} } Write-Host -NoNewline ("[" + $checkState + "] ") -ForegroundColor $color Write-Host ("[" + $checkName + "]") -ForegroundColor Gray Write-Host $desc -ForegroundColor White Write-Host -NoNewline "Details: " -ForegroundColor Gray Write-Host $details -ForegroundColor White # we only post the URL when it is specified. Some error messages dont have public docs. if(($null -ne $url) -and ($url -ne "")) { Write-Host -NoNewline "Documentation: " -ForegroundColor Gray Write-Host $url -ForegroundColor Yellow } Write-Host # Blank line for spacing. } <# ┌──────────────────────────────────────────────────────────────────────────┐ │ THIS HAS BEEN PORTED OVER FROM HUB. WE DONT KNOW WHAT WE TRULY NEED. TO │ │ BE CLEANED UP IN THE │ │ FUTURE │ └──────────────────────────────────────────────────────────────────────────┘ #> # create a script variable that is accessible to all the functions within the AzStack.Utilities.psm1 module # this can be used to store configuration type data, only available within the script scope $configurationData = Import-PowerShellDataFile -Path "$PSScriptRoot\AzStack.Utilities.Config.psd1" New-Variable -Name 'AzsSupport_Utilities' -Scope Local -Force -Value @{ Cache = @{ FilesExcludedFromCleanup = @() TraceFilePath = $null WorkingDirectory = $null } Config = $configurationData } function New-WorkingDirectory { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [System.String]$Path = $script:AzsSupport_Utilities.Config.WorkingDirectory ) try { # create the working directory and set the global cache if (-NOT (Test-Path -Path $Path -PathType Container)) { $null = New-Item -Path $Path -ItemType Directory -Force } # create the trace file New-TraceOutputFile } catch { "{0}`n{1}" -f $_.Exception, $_.ScriptStackTrace | Trace-Output -Level:Error } } function Get-FormattedDateTimeUTC { param ( [Switch]$NoSpace ) if ($NoSpace) { return ([DateTime]::UtcNow.ToString('yyyyMMddHHmmss')) } return ([DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss')) } # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. function Get-TraceOutputFile { return [System.String]$script:AzsSupport_Utilities.Config.TraceFilePath } function Get-AzsSupportTraceEvent { <# .SYNOPSIS Gets the trace events from Get-AzsSupportTraceFilePath. .PARAMETER FunctionName The function name that you want to filter on .PARAMETER Level The log level you want to filter on .PARAMETER IncludeDebugEvents Return debug level events from trace file .EXAMPLE Get-AzsSupportTraceEvent -FunctionName 'New-AzsSupportTraceFilePath' -Level Verbose #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $FakeboundParameter) $possibleValues = (Import-Csv -Path (Get-TraceOutputFile)).FunctionName | Select-Object -Unique | Sort-Object $possibleValues | Where-Object { $_ -like "$wordToComplete*" } })] [string]$FunctionName, [Parameter(Mandatory = $false)] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $FakeboundParameter) $possibleValues = (Import-Csv -Path (Get-TraceOutputFile)).Level | Select-Object -Unique | Sort-Object $possibleValues | Where-Object { $_ -like "$wordToComplete*" } })] [string]$Level, [Parameter(Mandatory = $false)] [switch]$IncludeDebugEvents ) try { $traceEvents = Import-Csv -Path (Get-TraceOutputFile) if ($PSBoundParameters['FunctionName']) { $traceEvents = $traceEvents | Where-Object { $_.FunctionName -eq $FunctionName } } if ($PSBoundParameters['Level']) { $traceEvents = $traceEvents | Where-Object { $_.Level -eq $Level } } if (!$IncludeDebugEvents) { $traceEvents = $traceEvents | Where-Object { $_.Level -ne 'Debug' } } return $traceEvents } catch { "{0}`n{1}" -f $_.Exception, $_.ScriptStackTrace | Trace-Output -Level:Error } } function Get-FormattedException { <# .SYNOPSIS Extracts details from an exception that is used to format the error message in a consistent manner. Does not capture the exception message as this might contain PII .PARAMETER Exception An exception thrown by the CLR or with the throw keyword .EXAMPLE PS> try { 10 / 0 } catch { Get-FormattedException -Exception $_.Exception | Write-Host -ForegroundColor Red } .NOTES We return compressed json as this information sent to the telemetry endpoint if customer has it enabled #> param( [System.Exception]$Exception ) $outerTypeName = $null $innerTypeName = $null if ($null -ne $Exception) { $outerTypeName = ($Exception | Get-Member)[0].TypeName } if ($null -ne $Exception.InnerException) { $innerTypeName = ($Exception.InnerException | Get-Member)[0].TypeName } return (@{ ErrorRecord = @{ ScriptStackTrace = $Exception.ErrorRecord.ScriptStackTrace InvocationInfo = $Exception.ErrorRecord.InvocationInfo } OuterException = @{ TypeName = $outerTypeName Source = $Exception.Source StackTrace = $Exception.StackTrace } InnerException = @{ TypeName = $innerTypeName Source = $Exception.InnerException.Source StackTrace = $Exception.InnerException.StackTrace } }) | ConvertTo-Json -Depth 3 -Compress } function Trace-Output { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [System.String]$Message, [Parameter(Mandatory = $false)] [TraceLevel]$Level ) begin { if (!$PSBoundParameters['Level']) { $Level = [TraceLevel]::Information } $traceFile = (Get-TraceOutputFile) if ([string]::IsNullOrEmpty($traceFile)) { New-WorkingDirectory $traceFile = (Get-TraceOutputFile) } } process { # create custom object for formatting purposes $traceEvent = [PSCustomObject]@{ Computer = $env:COMPUTERNAME.ToUpper().ToString() TimestampUtc = [DateTime]::UtcNow.ToString('yyyy-MM-dd HH-mm-ss') FunctionName = (Get-PSCallStack)[1].Command Level = $Level.ToString() Message = $Message } # write the message to the console switch ($Level) { 'Error' { "{0}" -f $traceEvent.Message | Write-Error } 'Exception' { "{0}" -f $traceEvent.Message | Write-Host -ForegroundColor:Red } 'Success' { "{0}" -f $traceEvent.Message | Write-Host -ForegroundColor:Green } 'Verbose' { if ($VerbosePreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue) { "{0}" -f $traceEvent.Message | Write-Verbose } } 'Warning' { "{0}" -f $traceEvent.Message | Write-Warning } 'Important' { "====[ {0} ]====" -f $traceEvent.Message | Write-Host -ForegroundColor:White -BackgroundColor:DarkGray } 'Detail' { "{0}" -f $traceEvent.Message | Write-Host -ForegroundColor:White } 'Unknown' { "{0}" -f $traceEvent.Message | Write-Host -ForegroundColor:DarkGray } default { "{0}" -f $traceEvent.Message | Write-Host -ForegroundColor:DarkCyan } } # write the event to trace file to be used for debugging purposes $mutexInstance = Wait-OnMutex -MutexId 'AzsSupport_TraceLogging' -ErrorAction Continue if ($mutexInstance) { $traceEvent | Export-Csv -Append -NoTypeInformation -Path $traceFile } } end { if ($mutexInstance) { $mutexInstance.ReleaseMutex() } } } function Wait-OnMutex { param ( [Parameter(Mandatory = $true)] [System.String]$MutexId ) try { $MutexInstance = New-Object System.Threading.Mutex($false, $MutexId) if ($MutexInstance.WaitOne(3000)) { return $MutexInstance } else { throw New-Object -TypeName System.TimeoutException($msg.TimeoutException) } } catch [System.Threading.AbandonedMutexException] { $MutexInstance = New-Object System.Threading.Mutex($false, $MutexId) return (Wait-OnMutex -MutexId $MutexId) } catch { $MutexInstance.ReleaseMutex() $_ | Write-Error } } function Get-WorkingDirectory { # check to see if the working directory has been configured into cache # otherwise set the cache based on what we have defined within our configuration file if ([String]::IsNullOrEmpty($script:AzsSupport_Utilities.Cache.WorkingDirectory)) { $script:AzsSupport_Utilities.Cache.WorkingDirectory = ($script:AzsSupport_Utilities.Config.WorkingDirectory -f (Get-FormattedDateTimeUTC -NoSpace)) } return [System.String]$script:AzsSupport_Utilities.Cache.WorkingDirectory } function Get-AzsSupportWorkingDirectory { return (Get-WorkingDirectory) } function New-TraceOutputFile { try { # make sure that directory path exists, else create the folder structure required $workingDir = Get-WorkingDirectory if (-NOT (Test-Path -Path $workingDir -PathType Container)) { $null = New-Item -Path $workingDir -ItemType Directory -Force } # build the trace file path and set global variable [System.String]$fileName = "AzsSupport_TraceOutput_{0}.csv" -f (Get-Date).ToString('yyyyMMdd') [System.IO.FileInfo]$filePath = Join-Path -Path $workingDir -ChildPath $fileName Set-TraceOutputFile -Path $filePath.FullName # configure the cache to not cleanup the trace file $script:AzsSupport_Utilities.Cache.FilesExcludedFromCleanup += $filePath.Name "TraceFile: {0}" -f $filePath.FullName | Trace-Output -Level:Verbose } catch { $_.Exception | Write-Error } } function Get-TraceOutputFile { return [System.String]$script:AzsSupport_Utilities.Cache.TraceFilePath } function Set-TraceOutputFile { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String]$Path ) $script:AzsSupport_Utilities.Cache.TraceFilePath = $Path } function Convert-FileSystemPathToUNC { <# .SYNOPSIS Converts a local file path to a computer specific admin UNC path, such as C:\temp\myfile.txt to \\$COMPUTERNAME\c$\temp\myfile.txt .PARAMETER ComputerName The computer name to inject into the unc path .PARAMETER Path The local file system path, such as C:\temp\myfile.txt #> param( [String]$ComputerName, [String]$Path ) $newPath = $path.Replace((Split-Path $Path), (Split-Path $Path).Replace(':', '$')) return ("\\{0}\{1}" -f $ComputerName, $newPath) } function Get-UserInputValues { <# .SYNOPSIS Used to capture information from user and generate a psobject .PARAMETER Properties The psobject properties you want to prompt user to provide .EXAMPLE PS> $results = Get-UserInputValues -Properties "Destination,Port,RetryAttempts" Destination: microsoft.com Port: 80 RetryAttempts: 3 PS> $results Destination Port RetryAttempts ----------- ---- ------------- microsoft.com 80 3 #> param ( [Parameter(Mandatory = $true)] [string]$Properties ) $object = foreach ($property in ($Properties.Split(','))) { $property = $property.Trim() New-Object -TypeName PSCustomObject -Property @{ $($property) = (Get-UserInput -Message "$($property): ").Trim() } } return $object } function Clear-AzsSupportDirectory { <# .SYNOPSIS Clears the contents of the directory .PARAMETER ComputerName Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. .PARAMETER Credential Specifies a user account that has permission to perform this action. The default is the current user. Type a user name, such as User01 or Domain01\User01, or enter a PSCredential object generated by the Get-Credential cmdlet. If you type a user name, you're prompted to enter the password. .PARAMETER Path Specifies a path of the items being removed. Wildcard characters are permitted. If ommitted, defaults to (Get-WorkingDirectory). .PARAMETER Recurse Indicates that this cmdlet deletes the items in the specified locations and in all child items of the locations. .PARAMETER Force Forces the cmdlet to remove items that cannot otherwise be changed, such as hidden or read-only files or read-only aliases or variables. .EXAMPLE PS> Clear-AzsSupportDirectory .EXAMPLE PS> Clear-AzsSupportDirectory -ComputerName PREFIX-NC01 -Path 'C:\Temp\SDN2' .EXAMPLE PS> Clear-AzsSupportDirectory -ComputerName PREFIX-NC01,PREFIX-SLB01 -Credential (Get-Credential) .EXAMPLE PS> Clear-AzsSupportDirectory -Force -Recurse .EXAMPLE PS> Clear-AzsSupportDirectory -Path 'C:\Temp\Azs.Support\Path1','C:\Temp\Azs.Support\Path2' -Force -Recurse #> [CmdletBinding(DefaultParameterSetName = 'Local')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] [System.String[]]$ComputerName, [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] [Parameter(Mandatory = $false, ParameterSetName = 'Local')] [System.String[]]$Path = (Get-WorkingDirectory), [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] [Parameter(Mandatory = $false, ParameterSetName = 'Local')] [Switch]$Recurse, [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] [Parameter(Mandatory = $false, ParameterSetName = 'Local')] [Switch]$Force ) function Clear-WorkingDirectory { [CmdletBinding()] param ( [System.String[]]$Path, [bool]$Recurse, [bool]$Force ) $filteredPaths = @() foreach ($obj in $Path) { # if the path does not exist, lets skip if (-NOT (Test-Path -Path $obj)) { continue } # enumerate through the allowed folder paths for cleanup to make sure the paths specified can be cleaned up foreach ($allowedFolderPath in $Script:AzsSupport_Utilities.Config.FolderPathsAllowedForCleanup) { if ($obj -ilike $allowedFolderPath) { $filteredPaths += $obj } } } if ($filteredPaths) { $msg.FileSystemRemove -f ($filteredPaths -join ', ') | Trace-Output -Level:Verbose Remove-Item -Path $filteredPaths -Exclude $Script:AzsSupport_Utilities.Cache.FilesExcludedFromCleanup -Force:$Force -Recurse:$Recurse -ErrorAction Continue } } $params = @{ Path = $Path Recurse = $Recurse.IsPresent Force = $Force.IsPresent } try { if ($PSCmdlet.ParameterSetName -eq 'Remote') { Invoke-PSRemoteCommand -ComputerName $ComputerName -Credential $Credential -ScriptBlock { param([Parameter(Position = 1)]$Path, [Parameter(Position = 2)]$Recurse, [Parameter(Position = 3)]$Force) Clear-SdnWorkingDirectory -Path $Path -Recurse:$Recurse -Force:$Force } -ArgumentList @($params.Path, $params.Recurse, $params.Force) } else { Clear-WorkingDirectory @params } } catch { "{0}`n{1}" -f $_.Exception, $_.ScriptStackTrace | Trace-Output -Level:Error } } function Test-Is23H2Version() { if(($null -ne $Global:AzsSupport.EnvironmentInfo) -AND ($Global:AzsSupport.EnvironmentInfo.WindowsProductName -eq "Azure Stack HCI") -AND ($Global:AzsSupport.EnvironmentInfo.OSDisplayVersion -eq "23H2")) { return $true } else { return $false } } #Internal Only Functions function Get-ComputerNameCanonicalized { <# .SYNOPSIS This function will take a computer name and returns the FQDN .PARAMETER Addresses The ComputerName you want to canonicalize .OUTPUTS Returns an FQDN of the computer name #> param( [string[]]$ComputerName ) try { #Trace-AzsSupportCommand -Event OnEntry # Load the required assembly Add-Type -AssemblyName System.DirectoryServices # Get the domain name $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name $results = foreach ($computer in $ComputerName) { if (!$computer.EndsWith($domain)) { $computer = $computer + "." + $domain } New-Object -TypeName PSCustomObject -Property @{ Name = $computer.ToLower() } } Trace-Output -Level:Verbose -Message ($msg.UtilitiesCompCanon -f $($results.Name -join ', ')) #Trace-AzsSupportCommand -Event OnExit return $results.Name } catch { #$formattedException = Get-FormattedException -Exception $_.Exception $_.Exception.Message | Trace-Output -Level:Exception #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException } } function Test-AzsSupportQuickPing { <# .SYNOPSIS Check ComputerName Given Is Online .DESCRIPTION Uses System.Net.NetworkInformation.Ping to test resource is online .PARAMETER Addresses The ComputerName you want to test .EXAMPLE Test-AzsSupportQuickPing -ComputerName contoso-n01 .OUTPUTS Returns a list of computers that are online #> param ( [Parameter(Mandatory = $true)] [string[]]$ComputerName, [Parameter(Mandatory = $false)] [ArgumentCompleter( { "SuccessOnly", "FailureOnly" })] [ValidateScript( { $_ -in "SuccessOnly", "FailureOnly" })] [String]$Status ) try { $PingResult = @() #Trace-AzsSupportCommand -Event OnEntry ForEach ($Computer in $ComputerName) { $TestRun = (New-Object -TypeName System.Net.NetworkInformation.Ping).SendPingAsync($Computer, 250) switch -Wildcard ($Status) { 'SuccessOnly' { $Output = [pscustomobject]@{ Name = $Computer Result = $TestRun.Result.Status } $PingResult += $Output Clear-Variable Output, TestRun } 'FailureOnly' { $Output = [pscustomobject]@{ Name = $Computer Result = $TestRun.Result.Status } $PingResult += $Output Clear-Variable Output, TestRun } Default { $Output = [pscustomobject]@{ Name = $Computer Result = $TestRun.Result.Status } $PingResult += $Output Clear-Variable Output, TestRun } } # End of switch Status } #Trace-AzsSupportCommand -Event OnExit return $PingResult } catch { #$formattedException = Get-FormattedException -Exception $_.Exception $_.Exception.Message | Trace-Output -Level:Exception #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException } } function New-AzsSupportPSSession { <# .SYNOPSIS Creates a persistent powershell session to an infrastructure node. .DESCRIPTION Creates a persistent powershell session to an infrastructure node to avoid creating a new session for each command. .PARAMETER ComputerName The computer that you want to create remote pssession to. Will be transformed to FQDN by default. .PARAMETER NetBIOS Skips the FQDN transformation and will use NetBIOS name. .PARAMETER Force Re-creates the persistent session. .EXAMPLE PS> New-AzsSupportPSSession -ComputerName "Azs-Node01" .EXAMPLE PS> New-AzsSupportPSSession -ComputerName "Azs-XRP01" -Force .EXAMPLE PS> New-AzsSupportPSSession -ComputerName "Azs-XRP01" -NetBIOS -Force #> [CmdletBinding()] param ( [Parameter(Mandatory=$true)] [string[]]$ComputerName, [Parameter(Mandatory=$false)] [switch]$NetBIOS, [Parameter(Mandatory=$false)] [switch]$Force, [Parameter(Mandatory=$false)] [Int32]$Timeout = 15000 ) try { #Trace-AzsSupportCommand -Event OnEntry -SuppressParameterTracing $remoteSessions = [System.Collections.ArrayList]::new() if(!$NetBIOS.IsPresent) { $ComputerName = Get-ComputerNameCanonicalized -ComputerName $ComputerName } # return a list of current sessions on the computer # return only the sessions that are opened and available as this will allow new sessions to be opened # without having to wait for existing sessions to move from Busy -> Available $currentActiveSessions = Get-PSSession | Where-Object {$_.State -ieq 'Opened' -and $_.Availability -ieq "Available"} $remoteSessions = [System.Collections.ArrayList]::new() foreach($computer in $ComputerName){ if(!(Test-Connection -ComputerName $computer -Quiet -Count 1)) { # Writing this to verbose as this is a highly utilized function and we'll flood the screen # We also have several other commands that test for VM/Host availability Trace-Output -Level:Warning -Message ($msg.UtilitiesPSSessionFail -f $computer) continue } $session = $null # check to see if session is already opened # if no session already exists or Force is defined, then create a new remote session if($currentActiveSessions.ComputerName -contains $computer -and !$Force){ $session = ($currentActiveSessions | Where-Object {$_.ComputerName -eq $computer})[0] } else { Trace-Output -Level:Verbose -Message ($msg.UtilitiesPSSessionTimeoutValue -f $computer, $Timeout) $session = New-PSSession -ComputerName $computer -ErrorAction SilentlyContinue -SessionOption (New-PSSessionOption -OpenTimeout $Timeout) if($error[0].Exception -is [System.Management.Automation.Remoting.PSRemotingTransportException]) { Trace-Output -Level:Warning -Message ($msg.UtilitiesPSSessionTimeoutExceeded -f $computer, $error[0].Exception) } } # add the session to the array if($session){ [void]$remoteSessions.Add($session) } } # send back the results Trace-Output -Level:Verbose -Message ($msg.UtilitiesPSSessionRemoteSessions -f $($remoteSessions.Name -join ', ')) #Trace-AzsSupportCommand -Event OnExit return $remoteSessions } catch { #$formattedException = Get-FormattedException -Exception $_.Exception $_.Exception.Message | Trace-Output -Level:Exception #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException } } function Wait-AzsSupportJob { <# .SYNOPSIS Monitors jobs intitiate jobs. .DESCRIPTION Monitors jobs to ensure they complete or terminate if any particular job is taking too long. .PARAMETER JobName The job name to monitor. .PARAMETER Activity Description of the job that is being performed. .PARAMETER PassThru Return the results of the jobs to the console. .PARAMETER ExecutionTimeOut Total period to wait for jobs to complete before stopping jobs and progressing forward in scripts. If omitted, defaults to 900 seconds. .PARAMETER PollingInterval How often you want to query job status. If omitted, defaults to 5 seconds. .EXAMPLE Invoke-Command -ComputerName $InfraNodes -ScriptBlock {Get-PsDrive C} -AsJob -JobName ($Id = "$([guid]::NewGuid().Guid)") | Out-Null Wait-AzsSupportJob -JobName $Id -Activity "Get-PsDrive" -ExecutionTimeOut 30 -PollingInterval 5 $Disks = Get-Job -Name $Id | Receive-Job -Keep Invoke-Command -Session $InfraNodes -ScriptBlock {netsh trace start capture=yes} -AsJob -JobName ($Id = "$([guid]::NewGuid().Guid)") Wait-AzsSupportJob -JobName $Id -Activity "Enable network traces" -ExecutionTimeOut 300 -PollingInterval 1 #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$JobName, [Parameter(Mandatory = $false)] [System.String]$Activity = (Get-PSCallStack)[1].Command, [Parameter(Mandatory = $false)] [switch]$PassThru, [Parameter(Mandatory = $false)] [int]$ExecutionTimeOut = 900, [Parameter(Mandatory = $false)] [int]$PollingInterval = 5 ) try { #Trace-AzsSupportCommand -Event OnEntry Trace-Output -Level:Verbose -Message ($msg.UtilitiesWaitJobDetails -f $JobName, $PollingInterval, $ExecutionTimeOut) $startTime = $(get-date) # Loop while there are running jobs while ((Get-Job -Name $JobName).State -eq "Running") { # get the job details and write progress $job = Get-Job -Name $JobName $runningchildjobs = $job.ChildJobs | Where-Object { $_.State -eq "Running" } $jobcount = $job.ChildJobs.Count $runningjobcount = $runningchildjobs.Count $percent = (($jobcount - $runningjobcount) / $jobcount * 100) -as [int] Write-Progress -Activity $Activity -Status ($msg.UtilitiesWaitJobProgress -f $percent,$($runningchildjobs.Location -join ", ")) -PercentComplete $percent -Id $job.Id # check the stopwatch and break out of loop if we hit execution timeout limit if ((new-timespan $startTime $(get-date)).Seconds -ge $ExecutionTimeOut) { Trace-Output -Level:Warning -Message ($msg.UtilitiesWaitJobTimeout) Get-Job -Name $JobName | Stop-Job -Confirm:$false } # pause the loop per polling interval value Start-Sleep -Seconds $PollingInterval } $job = Get-Job -Name $JobName # Ensure that we complete all jobs for write-progress to clear the progress bars Write-Progress -Activity $Activity -Id $job.Id -Completed # Output results of the job status to the operator if ($job.State -ne "Completed") { Trace-Output -Level:Warning -Message ($msg.UtilitiesWaitJobFailed -f $JobName, $job.State, $((new-timespan $startTime $(get-date)).seconds)) # Identify all failed child jobs and present to the operator $failedChildJobs = $job.ChildJobs | Where-Object { $_.State -ne "Completed" } foreach ($failedChildJob in $failedChildJobs) { Trace-Output -Level:Warning -Message ($msg.UtilitiesWaitJobFailedDetails -f $JobName, $failedChildJob.Location, $failedChildJob.State, $failedChildJob.StatusMessage) } Trace-Output -Level:Error -Message ($msg.UtilitiesWaitJobFailedState -f $JobName, $job.State, $job.StatusMessage) } else { Trace-Output -Level:Verbose -Message ($msg.UtilitiesWaitJobElapsed -f $JobName, $job.State, $((new-timespan $startTime $(get-date)).seconds)) } # if the user defined PassThru, then get the results of the jobs and return to the console # use the -Keep to ensure the results are saved if want to receive the results again outside of this function # if not defined, return the job name itself back to the console #Trace-AzsSupportCommand -Event OnExit if ($PassThru) { return ((Get-Job -Name $JobName).ChildJobs.Output) } else { return (Get-Job -Name $JobName) } } catch { #$formattedException = Get-FormattedException -Exception $_.Exception $_.Exception.Message | Trace-Output -Level:Exception #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException } } function Write-Colour { <# .SYNOPSIS Controls the output and spacing in a uniformed way for validation results .DESCRIPTION Allows quick ability to change colour for validation results .EXAMPLE PS C:\> Write-Colour "$PSitem".PadRight(50), '[', ' FAIL ', ']', `n -ForeGroundColor Yellow, White, Red, White .PARAMETER Text Text to output .PARAMETER ForeGroundColor Color of the text to output to screen for Write-Host .PARAMETER ValidationResult The result data from $Result #> Param ( [Parameter(Mandatory = $False)] [String[]]$Text, [Parameter(Mandatory = $False)] [ConsoleColor[]]$ForeGroundColor, [Parameter(Mandatory = $False)] [Array]$ValidationResult ) try { #Trace-AzsSupportCommand -Event OnEntry for ($i = 0; $i -lt $Text.Length; $i++) { $Color = @{ } if ($ForeGroundColor) { $Color = @{ ForegroundColor = $ForeGroundColor[$i % ($ForeGroundColor.count)] } } Write-Host $Text[$i] @color -NoNewline } #Trace-AzsSupportCommand -Event OnExit } catch { #$formattedException = Get-FormattedException -Exception $_.Exception $_.Exception.Message | Trace-Output -Level:Exception #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException } } function Invoke-AzsSupportCommand { <# .SYNOPSIS Runs commands on local and remote computers. .PARAMETER ComputerName Specifies the computers on which the command runs. .PARAMETER ScriptBlock Specifies the commands to run. Enclose the commands in braces ({ }) to create a script block. When using Invoke-Command to run a command remotely, any variables in the command are evaluated on the remote computer. .PARAMETER HideComputerName Indicates that this cmdlet omits the computer name of each object from the output display. By default, the name of the computer that generated the object appears in the display. .PARAMETER ArgumentList Supplies the values of parameters for the scriptblock. The parameters in the script block are passed by position from the array value supplied to ArgumentList. This is known as array splatting. .PARAMETER AsJob Indicates that this cmdlet runs the command as a background job on a remote computer. Use this parameter to run commands that take an extensive time to finish. .PARAMETER Wait Waits for the commands to complete. Once completed, returns the job details to console. .PARAMETER PassThru Returns the results back from the command after the command has completed. .PARAMETER Activity Allows you to define the name of the activity in the banner when waiting for jobs to complete. .PARAMETER ExecutionTimeout Total period to wait for jobs to complete before stopping jobs and progressing forward in scripts. If omitted, defaults to 900 seconds #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String[]]$ComputerName, [Parameter(Mandatory = $true)] [ScriptBlock]$ScriptBlock, [Parameter(Mandatory = $false)] [Switch]$HideComputerName, [Parameter(Mandatory = $false)] [Object]$ArgumentList, [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')] [Switch]$AsJob, [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')] [Switch]$Wait, [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')] [Switch]$PassThru, [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')] [System.String]$Activity = 'Invoke-AzsSupportCommand', [Parameter(Mandatory = $false, ParameterSetName = 'AsJob')] [int]$ExecutionTimeout = 900 ) try { #Trace-AzsSupportCommand -Event OnEntry $session = New-AzsSupportPSSession -ComputerName $ComputerName if ($session) { Trace-Output -Level:Verbose -Message ("$($msg.UtilitiesCommandScriptBlock)" -f ($session.ComputerName -join ', '), $ScriptBlock.ToString()) # need to go based on if the variables are $true and not on the parameter set name # due to calling functions that may leverage .IsPresent, which even though may be $false, will cause the AsJob parameter set name to trigger incorrectly if ($AsJob -or $PassThru -or $Wait) { if ($ArgumentList) { "Arguments: {0}" -f ($ArgumentList | ConvertTo-Json).ToString() | Trace-Output -Level:Verbose $result = Invoke-Command -Session $session -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -AsJob -JobName $([guid]::NewGuid().Guid) -HideComputerName:($HideComputerName.IsPresent) } else { $result = Invoke-Command -Session $session -ScriptBlock $ScriptBlock -AsJob -JobName $([guid]::NewGuid().Guid) -HideComputerName:($HideComputerName.IsPresent) } if ($PassThru -or $Wait) { $result = Wait-AzsSupportJob -JobName $result.Name -ExecutionTimeOut $ExecutionTimeout -PassThru:($PassThru.IsPresent) -Activity $Activity } } else { if ($ArgumentList) { Trace-Output -Level:Verbose -Message ("$($msg.UtilitiesCommandArgs)" -f ($($ArgumentList | ConvertTo-Json).ToString())) $result = Invoke-Command -Session $session -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -HideComputerName:($HideComputerName.IsPresent) } else { $result = Invoke-Command -Session $session -ScriptBlock $ScriptBlock -HideComputerName:($HideComputerName.IsPresent) } } #Trace-AzsSupportCommand -Event OnExit return $result } #Trace-AzsSupportCommand -Event OnExit } catch { #$formattedException = Get-FormattedException -Exception $_.Exception $_.Exception.Message | Trace-Output -Level:Exception #Trace-AzsSupportCommand -Event OnExit -Status Exception -StatusMessage $formattedException } } # SIG # Begin signature block # MIIoRQYJKoZIhvcNAQcCoIIoNjCCKDICAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCK3AvU1RjacorP # NhrNo1bBC0VkRF79cLH/f+3MW+TLLKCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0 # Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz # NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo # DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3 # a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF # HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy # 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC # Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj # L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp # h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3 # cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X # dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL # E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi # u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1 # sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq # 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb # DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/ # V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # 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 # /Xmfwb1tbWrJUnMTDXpQzTGCGiUwghohAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIC9ivAnE4c5XYxzj3cIAmX5R # vEFcZoHmStI5xvx/kreEMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAODlIZMvcur7IQOTsXhdkdW9lubQB4GIXGlj0vut3mOOyRNXPY1U10Yik # 9yaCbInTJNUvSBqjiEo7elBNowmwzjjnAoh7DeyjgU1UqzGwvD+k+rP/ESS1X2of # +4N1hozWlWhtbOawdpLeF0wqDIPHWaxo2v4WyHmIk+x8WngplcAdKOM8fPFgXD5G # 4mIMrirRINs1pzZ+V/VAfqrGwvbMA4m1SYp0C/3lPKQJ9WGzjKsMNRb8Qj1qpNrO # 0CPCFG/G8Ncw8WFZMFaBzYJ60s/l4biA2m9blfuG5yrd5ASYdoKDhr5pJ7IKqnn4 # U8zAUi3Q9b9TxGWAD61uE03B0udLdqGCF68wgherBgorBgEEAYI3AwMBMYIXmzCC # F5cGCSqGSIb3DQEHAqCCF4gwgheEAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq # hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCBp4p8AazC07wcXkluMu+xOTC6Tn+dtYIQOgxgpk5fqrAIGZ7/IjL7P # GBMyMDI1MDIyNzEzNDQ1NS41MDlaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo2RjFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaCCEf0wggcoMIIFEKADAgECAhMzAAAB/Bigr8xpWoc6AAEAAAH8MA0G # CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u # MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0 # MDcyNTE4MzExNFoXDTI1MTAyMjE4MzExNFowgdMxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w # ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjZGMUEt # MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp1DAKLxpbQcPVYPHlJHy # W7W5lBZjJWWDjMfl5WyhuAylP/LDm2hb4ymUmSymV0EFRQcmM8BypwjhWP8F7x4i # O88d+9GZ9MQmNh3jSDohhXXgf8rONEAyfCPVmJzM7ytsurZ9xocbuEL7+P7EkIwo # OuMFlTF2G/zuqx1E+wANslpPqPpb8PC56BQxgJCI1LOF5lk3AePJ78OL3aw/Ndlk # vdVl3VgBSPX4Nawt3UgUofuPn/cp9vwKKBwuIWQEFZ837GXXITshd2Mfs6oYfxXE # tmj2SBGEhxVs7xERuWGb0cK6afy7naKkbZI2v1UqsxuZt94rn/ey2ynvunlx0R6/ # b6nNkC1rOTAfWlpsAj/QlzyM6uYTSxYZC2YWzLbbRl0lRtSz+4TdpUU/oAZSB+Y+ # s12Rqmgzi7RVxNcI2lm//sCEm6A63nCJCgYtM+LLe9pTshl/Wf8OOuPQRiA+stTs # g89BOG9tblaz2kfeOkYf5hdH8phAbuOuDQfr6s5Ya6W+vZz6E0Zsenzi0OtMf5RC # a2hADYVgUxD+grC8EptfWeVAWgYCaQFheNN/ZGNQMkk78V63yoPBffJEAu+B5xlT # PYoijUdo9NXovJmoGXj6R8Tgso+QPaAGHKxCbHa1QL9ASMF3Os1jrogCHGiykfp1 # dKGnmA5wJT6Nx7BedlSDsAkCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBSY8aUrsUaz # hxByH79dhiQCL/7QdjAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf # BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz # L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww # bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El # MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF # BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAT7ss/ZAZ0bTa # FsrsiJYd//LQ6ImKb9JZSKiRw9xs8hwk5Y/7zign9gGtweRChC2lJ8GVRHgrFkBx # ACjuuPprSz/UYX7n522JKcudnWuIeE1p30BZrqPTOnscD98DZi6WNTAymnaS7it5 # qAgNInreAJbTU2cAosJoeXAHr50YgSGlmJM+cN6mYLAL6TTFMtFYJrpK9TM5Ryh5 # eZmm6UTJnGg0jt1pF/2u8PSdz3dDy7DF7KDJad2qHxZORvM3k9V8Yn3JI5YLPuLs # o2J5s3fpXyCVgR/hq86g5zjd9bRRyyiC8iLIm/N95q6HWVsCeySetrqfsDyYWStw # L96hy7DIyLL5ih8YFMd0AdmvTRoylmADuKwE2TQCTvPnjnLk7ypJW29t17Yya4V+ # Jlz54sBnPU7kIeYZsvUT+YKgykP1QB+p+uUdRH6e79Vaiz+iewWrIJZ4tXkDMmL2 # 1nh0j+58E1ecAYDvT6B4yFIeonxA/6Gl9Xs7JLciPCIC6hGdliiEBpyYeUF0ohZF # n7NKQu80IZ0jd511WA2bq6x9aUq/zFyf8Egw+dunUj1KtNoWpq7VuJqapckYsmvm # mYHZXCjK1Eus7V1I+aXjrBYuqyM9QpeFZU4U01YG15uWwUCaj0uZlah/RGSYMd84 # y9DCqOpfeKE6PLMk7hLnhvcOQrnxP6kwggdxMIIFWaADAgECAhMzAAAAFcXna54C # m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp # Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy # MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH # EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV # BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B # AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51 # yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY # 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9 # cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN # 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua # Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74 # kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2 # K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5 # TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk # i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q # BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri # Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC # BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl # pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y # eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA # YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU # 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny # bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw # MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w # Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp # b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm # ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM # 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW # OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4 # FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw # xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX # fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX # VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC # onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU # 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG # ahC0HVUzWLOhcGbyoYIDWDCCAkACAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo2RjFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaIjCgEBMAcGBSsOAwIaAxUATkEpJXOaqI2wfqBsw4NLVwqYqqqggYMw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF # AAIFAOtqRwUwIhgPMjAyNTAyMjcwMjA1NTdaGA8yMDI1MDIyODAyMDU1N1owdjA8 # BgorBgEEAYRZCgQBMS4wLDAKAgUA62pHBQIBADAJAgEAAgFbAgH/MAcCAQACAg5R # MAoCBQDra5iFAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAI # AgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBACC0pz5MNb95 # 2D2p/Rr52laF1dE8IUqrRkFvjUCF3KQs5sg/RGCwjcA8ES0a2nZHChn7+MHFTHq/ # N8gq4HoPjoZH5z6n7/fedv06VG9xAXcqG88ntkPRKUP5bmADb8b5WuQYYT9cyXve # uZTTWrPLPP82rMFFbvsSXSKS2WTw/5BXJmQAk5CmEX9K7j2dN6Ih7Pdws9nSwjvT # O3Y+Ff5Z52waDqtsDHhlkIgkelyWewD6O9ShoyBeFgweP9ZWjD2W2bys7LNRSXRQ # OeStA9XTAMpPmmJTz0YHNeCW/XrZvgxIE+snwfKmuzndUWd7WuReOgrTo69WyrWZ # BLcmhIG1tggxggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg # MjAxMAITMwAAAfwYoK/MaVqHOgABAAAB/DANBglghkgBZQMEAgEFAKCCAUowGgYJ # KoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCDLGO+IczfU # gdl+/XTOqFY969HX4eCc/ITNp3OiEMYrhjCB+gYLKoZIhvcNAQkQAi8xgeowgecw # geQwgb0EIJVCr5C77+H8E5U/jDB5TBse4JSGH5PuGrd3kwJo0S1iMIGYMIGApH4w # fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd # TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAH8GKCvzGlahzoAAQAA # AfwwIgQgHDNE75stsaBuIwfFlbbwDljB5pl+elTc+s7twc087Z8wDQYJKoZIhvcN # AQELBQAEggIANSlxBBvEJNmX5cxYk27vdgiJIP5FkJJXWkpZBUjhKuYhOhyuBYu7 # I0vd59yWd8GXxbb29sBiD8hc6uKk7tSh734dz15sBWfAhuVE5dApf16epKs3+4gL # cGS9wqXZqBV29Q7ZY8Y+Fc+lJvaLpvmYDsElc43VDBt6h0PCc3eEUJSL+IhfKYiN # ho5WC8C+pKv921X6HMCB8ACREHIRgvUp6x0eM+V/XwLYmdSQAng6NoBVtgsdlfah # 8wPbWAeg19/kjPWGddl3cGMhxt19UioIy57+9rJz31ZG7WzvCVvQcKZ0W4lN4JLq # xNTFzPkB1sFaR1MMUg2O7ijT47PBSILveHRotCHmdOC6xxiprcdL24q/RnOmUdSP # ZqqaPqVUqFYBR9wNJkMadnTnuxAXBZBL/wHGKhjJ50FSPaLj9x91YfQn3JD9sdgT # inC6ZDkSrtS60e4Sodor38kWY/UwHelKcoMc24hwqrV2+6fUis7Jsvv3ezEhbH7b # FcMEmHZsed8RtINdrkxX69h6cy0+NBcWq8SOC4WUJwURJkhNDDut22iEBH0vun58 # qHB5hZHMIShRAAtkD6yD+4inDHC8Shpav2ju18GaQxOwp3pwczPGy2ustF6WCL4o # QXQzV94xYTRxzaYrm+QC6MbbosqSAx0+jyS/+DziUkgcKQWUHk+M3Uk= # SIG # End signature block |