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