Azs.TeamsIntegration.psm1

#Requires -Modules Azs.Update.Admin, Azs.Operator

Import-Module -Name Azs.Operator -Force -Verbose:$false
Import-Module -Name Azs.Update.Admin -Force -Verbose:$false

# Import modules
$Location = $PSScriptRoot
[System.Reflection.Assembly]::LoadFrom("$Location\Dependencies\Antlr4.Runtime.dll")
[System.Reflection.Assembly]::LoadFrom("$Location\Dependencies\Newtonsoft.Json.dll")
[System.Reflection.Assembly]::LoadFrom("$Location\Dependencies\Jurassic.dll")
[System.Reflection.Assembly]::LoadFrom("$Location\Dependencies\AdaptiveCards.dll")
[System.Reflection.Assembly]::LoadFrom("$Location\Dependencies\AdaptiveExpressions.dll")
[System.Reflection.Assembly]::LoadFrom("$Location\Dependencies\AdaptiveCards.Templating.dll")

Import-Module -Name $Location\Azs.TeamsIntegration.Update.psm1 -force -Global -Verbose:$false
Import-Module -Name $Location\Azs.TeamsIntegration.Utilities.psm1 -force -Global -DisableNameChecking -Verbose:$false
Import-LocalizedData -BindingVariable LocalizedText -Filename Azs.TeamsIntegration.Update.Strings.psd1 -ErrorAction SilentlyContinue

function Send-AzsUpdate {
    <#
    .SYNOPSIS
        Send Update status to operators and audiences.
    .DESCRIPTION
        Retrieves update status and post a webrequest to operator and audience URIs, optionally as an adaptive card for teams.
    .EXAMPLE
        PS C:\> $OperatorUri = "https://outlook.office.com/webhook/<etc>"
        PS C:\> $AudienceUri = "https://outlook.office.com/webhook/<etc>"
        PS C:\> $Bridge = "https://teams.microsoft.com/l/meetup-join/<etc>"
        PS C:\> $stamp = "Prod"
        PS C:\> Send-AzsUpdate -AudienceUri $AudienceUri -OperatorUri $OperatorUri -Stamp $stamp -BridgeInformation $Bridge
        Updates audience every 10 minutes and operators (default) every 5 minutes for stamp "Prod"
    .NOTES
        Operators are assumed to have access to the audience channel and thus do not need rich data on the update.
        Operators recieve update status and duration and messages about the execution of retrieving and building status updates for all.
        OperatorFrequency must be less than or equal AudienceFrequency, it is assumes operators need update status quicker than audiences.
    .PARAMETER OperatorFrequency
        How often (approximately) the monitor the update for Operators. Must be 5, 10, 15, 20, 30 or 60
    .PARAMETER AudienceFrequency
        How often (approximately) the monitor the update for Audiences. Must be 5, 10, 15, 20, 30 or 60
    .PARAMETER AudienceUri
        Uri(s) to send Audience status updates
    .PARAMETER OperatorUri
        Uri(s) to send Operator status updates
    .PARAMETER Stamp
        An Azs.Operator construct that is passed to PEP and ARM connections. Stamps must be onboarded in Azs.Management using Add-Stamp.
    .PARAMETER PepCredential
        An Azs.Operator construct that is passed to PEP connections. Cloud Admin credential for PEP connections.
    .PARAMETER UpdateStatus
        NOT YET IMPLEMENT. FOR OFFLINE UPDATES
    .PARAMETER StampInformation
        NOT YET IMPLEMENT. FOR OFFLINE UPDATES
    .PARAMETER DisposePEP
        Close the PEP Session after every iteration.
    .PARAMETER Brief
        Messages will not be delivered if the previous message content was the same.
    .PARAMETER LogAction
        Logs write to console and disk by default, valid values 'File','Console','WebHook' where Webhook is the operator webhook by default
    .PARAMETER LogPath
        Saves zip of diagnostic data for troubleshooting in $home\.Azs.TeamsIntegration by default.
    .PARAMETER RetrySeconds
        Wait in seconds for retries connecting to ARM and PEP. Default is 30
    .PARAMETER HideEnvironmentDetail
        Optionally hide environment on teams cards
    .NOTES
        v0.1.1 - Send-AzsUpdate errors when Get-AzsUpdate returns nothing
        v0.1.1 Not using -log can lead to errors
     #>

    [CmdletBinding()]
    [Alias("Send-AzsUpdateStatus")]
    param (

        [Parameter(Mandatory = $false, HelpMessage = 'Will override connectionUri in StampDefinition')]
        [System.Uri[]]
        $AudienceUri,

        [Parameter(Mandatory = $false, HelpMessage = 'Send issues to a seperate Operator URI')]
        [System.Uri[]]
        $OperatorUri,

        [Parameter(Mandatory = $true, ParameterSetName = 'Online')]
        [ArgumentCompleter( { (Get-Stamp).Name | Sort-Object })]
        [string]
        $Stamp,

        [Parameter(Mandatory = $false, ParameterSetName = 'Online')]
        [pscredential]
        $PepCredential,

        [Parameter(Mandatory = $true, ParameterSetName = 'Offline', HelpMessage = 'Use offline XML status of update')]
        [xml]
        $UpdateStatus,

        [Parameter(Mandatory = $false, HelpMessage = 'Messages will not be delivered if the previous message content was the same.')]
        [switch]$Brief,

        [Parameter(Mandatory = $true, ParameterSetName = 'Offline', HelpMessage = 'Use offline Stamp Information JSON status of update')]
        [psobject]
        $StampInformation,

        [Parameter(Mandatory = $false, ParameterSetName = 'Online', HelpMessage = 'Force PEP disconnect after every run.')]
        [switch]$DisposePEP,

        [Parameter(Mandatory = $false, HelpMessage = "Logs write to console and disk by default, valid values 'File','Console','WebHook' where Webhook is the operator webhook by default")]
        [ValidateSet('Console','File','WebHook')]
        [string[]]$LogAction = @('File','Console'),

        [Parameter(Mandatory = $false, HelpMessage = 'Customize log path')]
        [string]$logPath = (Join-Path "$HOME" (".AzsTeamsIntegration\{0}" -f (Get-Date).ToString('yyyyMMddHHmmss'))),

        [Parameter(Mandatory = $false, ParameterSetName = 'Online', HelpMessage = 'Wait in seconds for retries connecting to ARM and PEP. Default is 30')]
        [int]$retrySeconds = 30,

        [Parameter(Mandatory = $false, HelpMessage = 'Optionally include bridge information on the card')]
        $BridgeInformation,

        [Parameter(Mandatory = $false, HelpMessage = 'Optionally hide environment on teams cards')]
        [switch]$HideEnvironmentDetail
    )
    try {
        $GLOBAL:logPath = Initialize-LogPath -LogPath $logPath
        $GLOBAL:LogAction = $LogAction
        $GLOBAL:Stamp = $Stamp

        #region online/live & offline read for update info
        if ($PSCmdlet.ParameterSetName -eq 'Online') {
            Write-CustomLog "[$Stamp] Connecting PEP"
            $pepsession = Get-PepSessionWithRetries -stamp $stamp -retrySeconds $retrySeconds -PepCredential $PepCredential ###### ONLINE
            if (-not $pepSession) {
                throw ($localizedText.NoPepSession -f $stamp)
            }

            if (-not $StampInformation) {
                Write-CustomLog "[$Stamp] Getting Stamp Information"
                $StampInformation = Invoke-PepCommand -PepSession $pepSession -scriptBlock { Get-AzureStackStampInformation } ###### ONLINE
            }

            # get update status from URP and determine latest update state
            $AllUpdates = Get-AzsUpdateWithRetries -stamp $stamp -retrySeconds $retrySeconds                             ###### ONLINE
            $UpdateInFlight = ($AllUpdates | Where-Object State -in 'Installing', 'Preparing')
            if ($UpdateInFlight) {
                $TrackingUpdateRun = Get-AzsUpdateRunWithRetries -Stamp $Stamp -retrySeconds $retrySeconds -UpdateName $UpdateInFlight.Name  ####### ONLINE
                # if the update is installing and the audience needs an update we can get the detail from the PEP
                # if the audience doesn't need an update, just the operators, then ARM calls will be enough.
                if ($UpdateInFlight.state -eq 'Installing' -and $AudienceUri) {
                    Write-CustomLog "[$Stamp] Running Get-AzureStackUpdateStatus"
                    [xml]$updateStatus = Invoke-PepCommand -PepSession $pepSession -ScriptBlock { Get-AzureStackUpdateStatus }
                }
            }
            else {
                Write-CustomLog "[$Stamp] No active update run"
            }
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'Offline') {
            $OfflineLogPath = Join-Path $logPath 'offline'
            Expand-Archive -Path $zipFile -DestinationPath $OfflineLogPath

            # Stamp Info
            $StampInformation = Get-Content (Join-Path $OfflineLogPath\StampInformation.json) -raw -ErrorAction Continue | ConvertFrom-Json

            # All updates
            $AllUpdates = Get-Content (Join-Path $OfflineLogPath\AllUpdates.json) -raw -ErrorAction Continue | ConvertFrom-Json
            $UpdateInFlight = ($AllUpdates | Where-Object State -in 'Installing', 'Preparing')

            # Current Run
            $TrackingUpdateRun = Get-Content (Join-Path $OfflineLogPath\UpdateRun.json) -raw -ErrorAction Continue | ConvertFrom-Json

            # Current Update status
            [xml]$updateStatus = Get-Content (Join-Path $OfflineLogPath\UpdateStatus.xml) -raw -ErrorAction Continue

            Remove-Item -path $OfflineLogPath -Recurse -force
        }
        else {
            throw "Unexpected Error. Unknown parameterset"
        }
        #endregion

        # if there's an update running, get duration, and save name for later.
        if ($UpdateInFlight) {
            Write-CustomLog "[$Stamp] Update $($UpdateInFlight.DisplayName) is $($UpdateInFlight.state)"
            $Banner = "$($UpdateInFlight.DisplayName) - $($UpdateInFlight.State.ToString())"
            $GLOBAL:TrackingUpdateRunUniqueName = $TrackingUpdateRun.Name
            $UpdateCardTemplate = $GLOBAL:UpdateInProgressTemplate
            $totalDuration = Format-TimeSpan $TrackingUpdateRun.duration
        }
        else {
            $latestUpdate = $AllUpdates | Select-Object -first 1
            if ($latestUpdate) {
                $Banner = "$($latestUpdate.DisplayName) - $($latestUpdate.State.ToString())"
            }
            else {
                $Banner = 'Azure Stack Hub Update Status'
            }

            Write-CustomLog "[$Stamp] No Updates in progress according URP. (Can be transient)"
            $UpdateCardTemplate = $GLOBAL:UpdateNotRunningTemplate
        }

        # if the update is preparing, we can get the preparation steps in progress
        if ($UpdateInFlight.State -eq 'Preparing') {
            $PrepProgress = $TrackingUpdateRun | Select-Object -expand ProgressStep | Select-Object Name, Status
            Write-CustomLog "[$Stamp] Steps in progress:"
            $PrepProgress | ForEach-Object { Write-CustomLog (("[$Stamp] {0}" -f $_.Name).PadRight(75) + $_.Status) }
            Write-CustomLog "[$Stamp] Update Duration: $totalDuration"
        }

        # the previous run errored, we want logs but we only want to once them once.
        # TO DO, make these logs targetted
        if ($latestUpdate.State -in 'PreparationFailed', 'InstallationFailed') {
            $collectAzsLogs = $true
        }
        else {
            $GLOBAL:AzsLogsSent = $false
        }

        # Get previous duration if the update is installed
        if ($latestUpdate.State -eq 'Installed') {
            $latestUpdateRun = Get-AzsUpdateRunWithRetries -Stamp $Stamp -retrySeconds $retrySeconds -UpdateName $latestUpdate.Name
            $totalDurationFinal = Format-TimeSpan $latestUpdateRun.duration
        }
        #endregion

        # formatting last few bits
        $updateHistory = $AllUpdates | Select-Object Location, DisplayName, @{label = 'State'; Expression = { $_.State.tostring() } }, VersionNumber
        $UpdateSummary = Get-UpdateSummary -UpdateStatus $updateStatus

        # Gather cmdlet used and version
        $moduleVersion = $MyInvocation.MyCommand.Module.Version
        $moduleName = $MyInvocation.MyCommand.Module.Name
        $commandletName = (Get-PSCallStack)[-2].Command

        # Create PSObject of progress data in "i hate nulls and ints" mode
        $UpdateData = New-Object -TypeName PSObject -Property @{
            ShowScopedRepair  = $null -ne $UpdateProgress.ShowScopedRepair
            showPrepProgress  = $null -ne $PrepProgress
            ShowStampInfo     = $null -ne $StampInformation
            showDuration      = $null -ne $totalDuration
            ShowHistory       = $null -ne $updateHistory
            ShowProgress      = $null -ne $UpdateSummary.updateProgress
            ShowBridge        = $null -ne $BridgeInformation
            Banner            = if ($Banner) { $Banner } else { "Azure Stack Update" }
            totalDuration     = if ($totalDurationFinal) {$totalDurationFinal} else {$totalDuration}
            Stamp             = New-Object -TypeName PSObject -Property @{
                AdminPortal   = $StampInformation.AdminExternalEndpoints.AdminPortal
                TenantPortal  = $StampInformation.TenantExternalEndpoints.TenantPortal
                Prefix        = $StampInformation.Prefix
                Hardware      = $StampInformation.HardwareOEM
                NumberOfNodes = "$($StampInformation.NumberOfNodes)"
                Version       = $StampInformation.StampVersion
                CloudId       = $StampInformation.CloudId
                Name          = $Stamp
            }
            Update            = New-Object -TypeName PSObject -Property @{
                Duration       = if ($totalDuration) { $totalDuration } else { '' }
                Status         = if ($UpdateInFlight.State) { $UpdateInFlight.State.ToString() } else { '' }
                Prep           = if ($PrepProgress) {$PrepProgress} else {''}
                Summary        = if ($UpdateSummary.summary) { $UpdateSummary.summary } else { '' }
                UpdateProgress = if ($UpdateSummary.updateProgress) { $UpdateSummary.updateProgress } else { '' }
                UpdateHistory  = if ($updateHistory) { $UpdateHistory } else { '' }
                UpdateName     = if ($TrackingUpdateRun.Name) { $TrackingUpdateRun.Name } else { '' }
            }
            BridgeInformation = $BridgeInformation
            commandletName    = if ($commandletName) { $commandletName } else { '' }
            moduleVersion     = if ($moduleVersion) { "$moduleVersion" } else { '' }
            moduleName        = if ($moduleName) { $moduleName } else { '' }
            ShowEnvDetail     = if ($HideEnvironmentDetail) { 'false' } else { 'true' }
            ShowNoEnvDetail   = if ($HideEnvironmentDetail) { 'true' } else { 'false' }
            Brief             = [bool]$Brief
        }

        # Get Update Detail Adaptive Card Template if there's an audience.
        if ($AudienceUri) {
            $TeamsMessageContent = Format-TeamsMessage -JsonTemplate $UpdateCardTemplate -AdaptiveCardData $UpdateData
            if ($TeamsMessageContent -ne $GLOBAL:previousAudienceMessage -or -not $Brief) {
                Send-MessageContent -uri $AudienceUri -MessageContent $TeamsMessageContent
                $GLOBAL:previousAudienceMessage = $TeamsMessageContent
            }
            else {
                Write-CustomLog ($localizedText.SameAsPreviousMessage -f 'Audience', $Brief)
            }
        }
        else {
                Write-CustomLog "[$Stamp] Skipping Adaptive card. No Audience."
        }

        if ($OperatorUri) {
            $OperatorText = Get-OperatorUpdate -updateData $UpdateData
            $OperatorUpdateMessage = Format-OperatorMessage -Message $OperatorText
            if ($GLOBAL:PreviousOperatorMessage -ne $OperatorUpdateMessage -or -not $Brief) {
                foreach ($Uri in $OperatorUri) {
                    Invoke-RestMethod -Uri $Uri -Method Post -Body $OperatorUpdateMessage -ContentType 'application/json' | out-Null
                }
            }
            else {
                Write-CustomLog ($localizedText.SameAsPreviousMessage -f 'Operator', $Brief)
            }
            $GLOBAL:PreviousOperatorMessage = $OperatorUpdateMessage
        }
    }
    catch {
        $OperatorCatchMessage = "We hit a problem sending data from the stamp to teams: `nError: `n{0}. `nUpdate Data: `n {1}" -f $_.exception.message, $OperatorUpdateMessage
        Write-CustomLog "[$Stamp] $OperatorCatchMessage"
        Send-TextMessage -uri $OperatorUri -message $OperatorCatchMessage
    }
    finally {
        if ($pepSession -and $disposePEP) {
            Write-CustomLog "[$Stamp] Closing Pepsession"
            Close-AzsPepSession -Stamp $Stamp
        }
        else {
            Write-CustomLog "[$Stamp] Leaving Pepsession open."
        }

        if ($collectAzsLogs -and -not $GLOBAL:azsLogsSent) {
            Write-CustomLog "[$Stamp] Sending Azure Stack Logs."
            $pepsession = Get-PepSessionWithRetries -stamp $stamp -retrySeconds $retrySeconds -PepCredential $PepCredential
            Invoke-PepCommand -PepSession $pepSession -ScriptBlock { Send-AzureStackDiagnosticLog -FilterByRole SeedRingServices } -AsJob
            $GLOBAL:azsLogsSent = $true
            Send-TextMessage -uri $OperatorUri -message "[$Stamp] Sending Azure Stack Logs."
        }

        if ($updatesFromURP) { $updatesFromURP | ConvertTo-Json -depth 15 -WarningAction SilentlyContinue | Out-File $logPath\AllUpdates.json }
        if ($StampInformation) { $StampInformation | ConvertTo-Json -depth 15 -WarningAction SilentlyContinue | Out-File $logPath\StampInformation.json }
        if ($UpdateStatus) { $UpdateStatus | Out-File $logPath\UpdateStatus.xml }
        if ($UpdateData) { $UpdateData | ConvertTo-Json -Depth 20 -WarningAction SilentlyContinue | Out-File $logPath\UpdateData.json }
        if ($latestUpdateRun) { $latestUpdateRun | ConvertTo-Json -depth 15 -WarningAction SilentlyContinue | Out-File $logPath\UpdateRun.json }
        if ($TeamsMessageContent) { $TeamsMessageContent | Out-File $logPath\TeamsAudienceMessageContent.json }
        if ($OperatorUpdateMessage) { $OperatorUpdateMessage | Out-File $logPath\TeamsOperatorContent.json}
        if ($OperatorCatchMessage) { $GLOBAL:previousAudienceMessage = 'ERROR'; Write-CustomLog "Forcing card next time" }

        if ('Webhook' -in $GLOBAL:LogActionlog) {
        # send operator the log if logging on, highlight important messaging
            $TeamsMessageContent = Format-OperatorMessage -message (Get-Content (Join-Path $logPath AzsMgmtTeams.log))
            foreach ($connector in $OperatorUri) {
                Invoke-RestMethod -Uri $connector -Method Post -Body $TeamsMessageContent -ContentType 'application/json'
            }
        }

        $zipFile = "{0}\AzsTeamsIntegrationLogs_{1}.zip" -f ((Split-Path $logPath -Parent), (Split-Path $logPath -Leaf))
        Write-CustomLog "[$Stamp] Zipping logs. $zipFile"
        Compress-Archive -Path $logPath -DestinationPath $zipFile
        Write-CustomLog "[$Stamp] Removing $logPath"
        Remove-Item -Path $logPath -Recurse -Force -ErrorAction SilentlyContinue
    }
}

Export-ModuleMember -Function Send-AzsUpdate

function Watch-AzsUpdate {
    <#
    .SYNOPSIS
        Continually monitor Updates for changes and invoke updates to operators and audiences.
    .DESCRIPTION
        Runs in loop every minute for (configurable) totalMinutes, default 7200.
        Each iteration checks if the operators or the audiences should updated as per OperatorFreqency and AudienceFrequency respectively.
        If Operators should be updated, the status of the most recent update is retrieved and changes (name, duration, status) are determined.
        If Audiences should be updated and changes are detected, both audiences and operators receive an update.
        If Audiences are not to be updated, i.e. update has changed but AudienceFrequency (a update) isn't due, the operators get a short status update.
    .EXAMPLE
        PS C:\> $OperatorUri = "https://outlook.office.com/webhook/<etc>"
        PS C:\> $AudienceUri = "https://outlook.office.com/webhook/<etc>"
        PS C:\> $Bridge = "https://teams.microsoft.com/l/meetup-join/<etc>"
        PS C:\> $stamp = "Prod"
        PS C:\> Watch-AzsUpdate -AudienceUri $AudienceUri -OperatorUri $OperatorUri -Stamp $stamp -BridgeInformation $Bridge -AudienceFrequency 30
        Updates audience every 30 minutes and operators (default) every 15 minutes for stamp "Prod"
    .PARAMETER OperatorFrequency
        How often (approximately) the monitor the update for Operators. Must be 5, 10, 15 (default) or 30.
    .PARAMETER AudienceFrequency
        How often (approximately) the monitor the update for Audiences. Must be 30 or 60.
    .PARAMETER AudienceUri
        Uri(s) to send Audience status updates
    .PARAMETER OperatorUri
        Uri(s) to send Operator status updates
    .PARAMETER Stamp
        An Azs.Operator construct that is passed to PEP and ARM connections. Stamps must be onboarded in Azs.Management using Add-AzsStamp.
    .PARAMETER PepCredential
        An Azs.Operator construct that is passed to PEP connections. Cloud Admin credential for PEP connections.
    .PARAMETER UpdateStatus
        NOT YET IMPLEMENT. FOR OFFLINE UPDATES
    .PARAMETER StampInformation
        NOT YET IMPLEMENT. FOR OFFLINE UPDATES
    .PARAMETER DisposePEP
        Close the PEP Session after every iteration.
    .PARAMETER Brief
        Messages will not be delivered if the previous message content was the same.
    .PARAMETER Log
        Saves zip of diagnostic data for troubleshooting
    .PARAMETER LogPath
        Saves zip of diagnostic data for troubleshooting
    .PARAMETER RetrySeconds
        Wait in seconds for retries connecting to ARM and PEP. Default is 30
    .PARAMETER TotalMinutes
        Total runtime in minutes. Defaults to 7200 minutes (5 days)
    .PARAMETER BridgeInformation
        Optionally include bridge information on the card
    .PARAMETER HideEnvironmentDetail
        Optionally hide environment on teams cards.
    .NOTES
        To be run on a workstation, server or jumpbox with PowerShell 7 and has connectivity WINRM to PriviledgedEndpoint, and HTTPS connectivity Azure Resource Manager on Azure Stack Stack Hub and HTTPS connectivity the target webhook.
        Operators are assumed to have access to the audience channel and thus do not need rich data on the update.
        Operators recieve update status and duration and messages about the execution of retrieving and building status updates for all.
        OperatorFrequency must be less than or equal AudienceFrequency, it is assumes operators need update status quicker than audiences.
    #>

    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'PEP')]
        [ArgumentCompleter( { (Get-Stamp).Name | Sort-Object })]
        [string]
        $Stamp,

        [Parameter(Mandatory = $false, ParameterSetName = 'PEP')]
        [pscredential]
        $PepCredential,

        [Parameter(Mandatory = $false, HelpMessage = 'Uri(s) to send Audience status updates')]
        [System.Uri[]]
        $AudienceUri,

        [Parameter(Mandatory = $false, HelpMessage = 'Uri(s) to send Audience status updates')]
        [System.Uri[]]
        $OperatorUri,

        [Parameter(Mandatory = $false, HelpMessage = 'How often (approximately) the monitor the update for Operators. Must be 5, 10 or 15')]
        [ValidateSet(10, 15, 30)]
        $OperatorFrequency = 15,

        [Parameter(Mandatory = $false, HelpMessage = 'How often (approximately) to send the update to the audiences. Must be 30 or 60')]
        [ValidateSet(10, 30, 60)]
        $AudienceFrequency = 30,

        [Parameter(Mandatory = $false, HelpMessage = 'Optionally include bridge information on the card')]
        $BridgeInformation,

        [Parameter(Mandatory = $false, HelpMessage = 'Messages will not be delivered if the previous message content was the same.')]
        [switch]$Brief,

        [Parameter(Mandatory = $true, ParameterSetName = 'File', HelpMessage = 'Use offline Stamp Information JSON status of update')]
        [psobject]
        $StampInformation,

        [Parameter(Mandatory = $false, ParameterSetName = 'PEP', HelpMessage = 'Force PEP disconnect after every run.')]
        [switch]$DisposePEP,

        [Parameter(Mandatory = $false, HelpMessage = "Logs write to console and disk by default, valid values 'File','Console','WebHook' where Webhook is the operator webhook by default")]
        [ValidateSet('Console','File','WebHook')]
        [string[]]$LogAction = @('File','Console'),

        [Parameter(Mandatory = $false, HelpMessage = 'Customize log path')]
        [string]$LogPath = (Join-Path "$HOME" ".AzsMgmtTeams"),

        [Parameter(Mandatory = $false, HelpMessage = 'Wait in seconds for retries connecting to ARM and PEP. Default is 30')]
        [int]$RetrySeconds = 30,

        [Parameter(Mandatory = $false, HelpMessage = 'Total runtime in minutes. Defaults to 7200 minutes (5 days)')]
        $totalMinutes = 7200,

        [Parameter(Mandatory = $false, HelpMessage = 'Optionally hide environment on teams cards')]
        [switch]$HideEnvironmentDetail
    )

    $GLOBAL:previousAudienceMessage = 'PLACEHOLDER'
    $GLOBAL:previousOperatorMessage = 'PLACEHOLDER'
    $i = 0
    $opCount = 0
    $audCount = 0

    try {

        # Check for module update
        Test-ModuleUpdate

        if (-not $PepCredential -and -not (Get-AzsStamp -Stamp $Stamp).CloudAdminUser.VaultName) {
            Write-Warning $localizedText.NoPepCredentialsWarning
        }

        if (-not $AudienceUri -and -not $OperatorUri) {
            throw $localizedText.NoUriWarning
        }

        # Validate PEP connectivity
        try {
            $pepSession = Connect-AzsPepSession -Stamp $Stamp -PepCredential $PepCredential -errorAction Stop
        }
        catch {
            $pepError = $_.exception.message
        }

        if ($pepSession.State -ne 'Opened') {
            throw $localizedText.NoPepConnection -f $pepError
        }

        # check ARM connectivity
        try {
            Connect-AzsArmEndpoint -Stamp $Stamp
            $validateUpdates = Get-AzsUpdate
        }
        catch {
            throw $localizedText.NoARMConnection -f $_.exception.message
        }

        while ($i -le $totalMinutes) {
            try {
                # determine if operators and audiences should get an update
                $totalPercentage = (($totalMinutes - $i) / $totalMinutes) * 100
                $showOperator = if (($i % $OperatorFrequency) -eq 0) {
                    $opMinus = 0
                    $opMinusPercent = 0
                    $opCount = 0
                    $OperatorUri
                }
                else {
                    $opCount++;
                    $opMinus = $OperatorFrequency - $opCount
                    $opMinusPercent = ($opMinus / $OperatorFrequency) * 100
                    $null
                }
                $showAudience = if (($i % $AudienceFrequency) -eq 0) {
                    $audMinus = 0
                    $audMinusPercent = 0
                    $audCount = 0
                    $AudienceUri
                }
                else {
                    $audCount++
                    $audMinus = $AudienceFrequency - $audCount
                    $audMinusPercent = ($audMinus / $AudienceFrequency) * 100
                    $null
                }

                Write-Progress -id 1 -activity "Forwarding Azure Stack Update Status" -status "Total Monitoring Runtime" -percentComplete $totalPercentage
                Write-Progress -id 2 -parentId 1 -activity "Audience Forwarding" -status "Audience Update in $audMinus minutes" -percentComplete $audMinusPercent
                Write-Progress -id 3 -parentId 1 -activity "Operator Forwarding" -status "Operator Update in $opMinus minutes" -percentComplete $opMinusPercent


                if ($showAudience -or $showOperator) {
                    $PSBoundParameters['AudienceUri'] = $showAudience
                    $PSBoundParameters['OperatorUri'] = $showOperator
                    $PSBoundParameters.Remove('OperatorFrequency') | Out-Null
                    $PSBoundParameters.Remove('AudienceFrequency') | Out-Null
                    Send-AzsUpdate @PSBoundParameters
                }
                Start-Sleep -Seconds 60
                $i++
            }
            catch {
                Write-Warning ($localizedText.Exception -f $($_.exception.message))
            }
        }
    }
    catch {
        throw ("Watch-AzsUpdate had a problem: Error: {0}" -f $_.exception.message)
    }
    finally {
        Close-AzsPepSession -Stamp $Stamp -ErrorAction SilentlyContinue
    }
}

function Test-ModuleUpdate {
    param()
    try {
        Write-CustomLog "Looking for module updates"
        $ModuleOnline = Find-Module -Name $MyInvocation.MyCommand.Module.Name -Repository PSGallery -AllowPrerelease -ErrorAction SilentlyContinue
        if ([system.version]$ModuleOnline.Version.replace('-preview','') -gt $MyInvocation.MyCommand.Module.Version) {
            Write-Warning ($localizedText.CurrentVersion -f $MyInvocation.MyCommand.Module.Name, $MyInvocation.MyCommand.Module.Version)
            Write-Warning ($localizedText.UpdateToVersion -f $ModuleOnline.Version)
            Start-Sleep -seconds 10
        }
        else {
            Write-Verbose ($localizedText.CurrentVersion -f $MyInvocation.MyCommand.Module.Name, $MyInvocation.MyCommand.Module.Version)
        }
    }
    catch {
        Write-Verbose ($localizedText.Exception -f $_.exception.message)
    }
}

Export-ModuleMember -Function Watch-AzsUpdate

# SIG # Begin signature block
# MIIjkgYJKoZIhvcNAQcCoIIjgzCCI38CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDu8B0BFUqSyz6H
# eE3z8ZjvZPaUspdA+l+nZwk7V3nCJ6CCDYEwggX/MIID56ADAgECAhMzAAAB32vw
# LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn
# s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw
# PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS
# yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG
# 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh
# EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH
# tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS
# 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp
# TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok
# t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4
# b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao
# mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD
# Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt
# VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G
# CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+
# Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82
# oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVZzCCFWMCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgOZ7Wkfu+
# QT/B9eCBSQ7vOCTQd8sHezwO87S2UQE9YMIwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQCbrbD7ts0Ysk4dMpflYkXv1D7Bbamhz4TZAiFVnKb2
# IsYLuBrhlm++5EW4nZCXmQBqOEtBN240un0zoY1yA/QAHtfoFrr8f1AMgdm+aClu
# isk8B7oDHVRqU8rQsGHGIIxZ5mVhGyEHuEVfiBl/BZZGpXv8QfyVlZDiuA8/IWLw
# L/Ay7YTBOSm8QceaSwCmMfL7E7qmoj5tq2lsNH3FmKGq144er71PIC+8j8ZRXR0f
# WfPrzgDyguBlmxCyB+CQhFGR45niCXgl+ac6e2Bkq6jwGbNxVDhRNQNXtnKWwkIC
# acTOkvk0LH9HGEhVj6fbtDJYqvKkrMnogyIiCsgLyg54oYIS8TCCEu0GCisGAQQB
# gjcDAwExghLdMIIS2QYJKoZIhvcNAQcCoIISyjCCEsYCAQMxDzANBglghkgBZQME
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIL0iVE/pacd7SyO6f083xQ3qGWWFOLOsDGOqy31Y
# RhDLAgZgPPIkFm8YEzIwMjEwMzExMTUyNTAwLjIxNlowBIACAfSggdSkgdEwgc4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjozMkJELUUzRDUtM0IxRDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCDkQwggT1MIID3aADAgECAhMzAAABYtD+AvMB5c1JAAAA
# AAFiMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIxMDExNDE5MDIyMloXDTIyMDQxMTE5MDIyMlowgc4xCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjozMkJE
# LUUzRDUtM0IxRDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO+GodT2ucL3Mr2DQsv2
# ELNbSvKyBpYdUKtUBWiZmFVy18pG/pucgkrc5i9tu8CY7GpWV/CQNmHG2mVeSHMJ
# vbwCc/AAv7JP3bFCt6Zg75IbVSNOGA1eqLbmQiC6UAfSKXLN3dHtQ5diihb3Ymzp
# NP9K0cVPZfv2MXm+ZVU0RES8cyPkXel7+UEGE+kqdiBNDdb8yBXd8sju+90+V4nz
# YC+ZWW7SFJ2FFZlASpVaHpjv+eGohXlQaSBvmM4Q0xe3LhzQM8ViGz9cLeFSKgFf
# SY7qizL7wUg+eqYvDUyjPX8axEQHmk0th23wWH5p0Wduws43qNIo0OQ0mRotBK71
# nykCAwEAAaOCARswggEXMB0GA1UdDgQWBBTLxEoRYEpDtzp84B5WlZN2kP4qazAf
# BgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBH
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNU
# aW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0
# YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsG
# AQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQAtQa3DoXYbW/cXACbcVSFGe4gC8GXs
# FxSHT3JgwFU/NdJOcbkcFTVvTp6vlmTvHm6sIjknRBB0Xi1NBTqPw20u6u/T7Cnc
# /z0gT6mf9crI0VR9C+R1CtjezYKZEdZZ7fuNQWjsyftNDhQy+Rqnqryt0VoezLal
# heiinHzZD/4Y4hZYPf0u8TSv1ZfKtdBweWG3QU0Lp/I9SbIoemDG97RULMcPvq2u
# fhUp3OMiYQGL1WqkykSnqRJsM2IcA4l4dmoPNP6dLg5Dr7NVoYKIMInaQVZjSwDM
# ZhWryvfizX0SrzyLgkMPhLMVkfLxQQSQ37NeFk7F1RfeAkNWAh6mCORBMIIGcTCC
# BFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJv
# b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcN
# MjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIw
# DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0
# VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEw
# RA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQe
# dGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKx
# Xf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4G
# kbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEA
# AaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7
# fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0g
# AQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYB
# BQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUA
# bQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOh
# IW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS
# +7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlK
# kVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon
# /VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOi
# PPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/
# fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCII
# YdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0
# cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7a
# KLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQ
# cdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+
# NR4Iuto229Nfj950iEkSoYIC0jCCAjsCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBP
# cGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjoz
# MkJELUUzRDUtM0IxRDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAmrP6Chrbz0ax7s57n5Pop3VC8gyggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOP0n5EwIhgPMjAyMTAzMTExNzU0MjVaGA8yMDIxMDMxMjE3NTQyNVowdzA9Bgor
# BgEEAYRZCgQBMS8wLTAKAgUA4/SfkQIBADAKAgEAAgIr8QIB/zAHAgEAAgIRVzAK
# AgUA4/XxEQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAAIDMp0N05uhZQRe
# 6PS1v/iJoUlZCpQ804lWd+lATCq/HO5jwF6vo7D+ka2jwrYZkNAGakI6eoHRajdc
# WdI1XEfMdukNpH1aTHuk6Ss6s7GZi2zgmBNVYRyH6/uo/r2VAmhN58g9QcBGBNAq
# dP4aOpyoA854av9ZSzXjxpT4E5PEMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAFi0P4C8wHlzUkAAAAAAWIwDQYJYIZIAWUD
# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQgEMFlqBbgn/fqY6aomyLQi8kNgYIJ3SQIgNHYPLEw+0kwgfoGCyqGSIb3
# DQEJEAIvMYHqMIHnMIHkMIG9BCCKqhiV+zwNDrpU7DRB7Mi57xi6GBNYsGjgZqq2
# qVMKMjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB
# YtD+AvMB5c1JAAAAAAFiMCIEIFIoFNvxLtJimrsLTxAvErioEIQbE+YKajRWVcZo
# m5SyMA0GCSqGSIb3DQEBCwUABIIBAM3B26Ti984LR59/vDwukOlZ3LHs9Q+nGMxt
# 2GgvqSLQmu+sKar96YO/pC1lVAUOwpOiAQ4QUO9PDJ2sRHAS0gA6GWQzUjhzCZ35
# kjVIDzk5PKIEWspjcF4SJtuneQIUFUcDzSECk7jAyNtIdRH3F1JuwzT6utd1XcLl
# hzyUtClMziqFYdoA+ESWvXz2tcJ0f35R2mHea7wRqBJrl9Eu2PEy4ZEcyMtkJblJ
# nttUr8M/Yv1QBzahl6tJ2yemID1nW7542shSYQ9Gc60ptCtcY+KSe6AXId9+lpqH
# NzhVevIJGrDbaazdestxS5tZHAKLe0Rz3W8boigUzphSEgAzMkU=
# SIG # End signature block