Microsoft.AzStack.CSSTools.AppService.psm1
function Get-AzsSupportAppServiceLogs { <# .SYNOPSIS Gather Azure Stack Hub AppService Resource Provider logs and optionally upload them to a SASUri or a remote network share .DESCRIPTION Gather Azure Stack Hub AppService Resource Provider logs and optionally upload them to a SASUri or a remote network share This script should be done from one of the CN-VMs and requires the Worker Admin credentials to be provided .PARAMETER WorkerCred Credentials to access worker nodes (Only required when worker role logs are collected) .PARAMETER FilterByRole Collect only specified roles (Default is all roles) .PARAMETER FilterByNode Collect only specified nodes by IP address (Defaults to all nodes) .PARAMETER TimeOutInMinutes Each step will only run up to the number of minutes specificed by this parameter .PARAMETER OutputSharePath Used to define a network share path that you want to send the file(s) to. It will execute New-AzsSupportNetworkShare to create a new mapped network drive .PARAMETER OutputSharePathCreds Credentials used to access the network share location .PARAMETER OutputShareDriveLetter The drive letter that you want to use to map the network drive to .PARAMETER OutputSasUri The SAS URI token for the Azure or Azure Stack blob storage account you want to upload the file(s) to .EXAMPLE PS> Get-AzsSupportAppServiceLogs -WorkerCred $WorkerCred # Collect the logs locally by providing the Worker Credentials using a variable, allowing for the 1 hour default timeout time and allowing for the default 14 days worth of logs .EXAMPLE PS> Get-AzsSupportAppServiceLogs -WorkerCred $WorkerCred TimeOutInMinutes 120 -FromDate (Get-Date).AddMonths(-1) # Collect the logs locally by providing the Worker Credentials using a variable, specifying a 30 minutes of timeout time and 1 month worth of logs .EXAMPLE PS> Get-AzsSupportAppServiceLogs -LogBundlePath c:\Temp\AppService-12121212121 -OutputSharePath "\\xx.xx.xx.xx\share" -OutputSharePathCreds (Get-Credential) -OutputShareDriveLetter X # Send the logs collected to a remote share .EXAMPLE PS> Get-AzsSupportAppServiceLogs -LogBundlePath c:\Temp\AppService-12121212121 -OutputSasUri "https://azsdiagprdlocalwestus.blob.core.windows.net/a6a797f70d734aldkhfaknoaghngloransfkjuewrl;jsdjkfnb" # Send the logs collected to a remote SASUri # This option requires AzCopy to be available and that can be installed using the -InstallAzCopy parameter set .EXAMPLE PS> Get-AzsSupportAppServiceLogs -InstallAzCopy # Install azcopy locally to be used by the -OutputSasUri parameter set #> [CmdletBinding(DefaultParameterSetName = "CollectLogs")] param ( [Parameter( ParameterSetName = "CollectLogs", Mandatory = $false )] [PSCredential]$workerCred, [Parameter( ParameterSetName = "CollectLogs", Mandatory = $false )] [ArgumentCompleter( { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) try { $items = @(Get-AppServiceServer -ErrorAction Ignore | Select-Object -ExpandProperty Role | Sort-Object -Unique) } catch {} if (!$items) { return $null } if ([string]::IsNullOrEmpty($WordToComplete)) { return $items | Sort-Object } return $items | Where-Object { $_ -like "*$WordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } })] [ValidateScript( { try { $roleNames = @(Get-AppServiceServer | Select-Object -ExpandProperty Role | Sort-Object -Unique) } catch { throw "Unable to validate roles provided" } foreach ($providedRole in $_) { if ($providedRole -inotin $roleNames) { throw "Role '$providedRole' is not valid" } } return $true })] [System.String[]]$FilterByRole = @(Get-AppServiceServer | Select-Object -ExpandProperty Role | Sort-Object -Unique), [Parameter( ParameterSetName = "CollectLogs", Mandatory = $false )] [ArgumentCompleter( { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) try { $items = @(Get-AppServiceServer -ErrorAction Ignore | Select-Object -ExpandProperty Name | Sort-Object -Unique) } catch {} if (!$items) { return $null } if ([string]::IsNullOrEmpty($WordToComplete)) { return $items | Sort-Object } return $items | Where-Object { $_ -like "*$WordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } })] [ValidateScript( { try { $nodeNames = @(Get-AppServiceServer | Select-Object -ExpandProperty Name | Sort-Object -Unique) } catch { throw "Unable to validate nodes provided" } foreach ($providedNode in $_) { if ($providedNode -inotin $nodeNames) { throw "Node '$providedNode' is not valid" } } return $true })] [System.Net.IPAddress[]]$FilterByNode, [Parameter( ParameterSetName = "CollectLogs", Mandatory = $false )] [System.Int16]$TimeOutInMinutes = 60, [Parameter( ParameterSetName = "CollectLogs", Mandatory = $false )] [System.DateTime]$FromDate = (Get-Date).AddDays(-14), [Parameter( ParameterSetName = "CollectLogs", Mandatory = $false )] [System.DateTime]$ToDate = (Get-Date), [Parameter( ParameterSetName = "CollectLogs", Mandatory = $false )] [System.Int16]$MaxZipSizeInMB = 100, [Parameter( ParameterSetName = "CollectLogs", Mandatory = $false )] [System.Int16]$MaxParallelJobs = 5, [Parameter( ParameterSetName = "InstallAzCopy", Mandatory = $true )] [Switch]$InstallAzCopy, [Parameter( ParameterSetName = "SendLogsNetworkShare", Mandatory = $true )] [Parameter( ParameterSetName = "SendLogsSasUri", Mandatory = $true )] [System.IO.FileInfo]$LogBundlePath, [Parameter( ParameterSetName = "SendLogsNetworkShare", Mandatory = $true )] [System.IO.FileInfo]$OutputSharePath, [Parameter( ParameterSetName = "SendLogsNetworkShare", Mandatory = $true )] [System.Management.Automation.PSCredential]$OutputSharePathCreds, [Parameter( ParameterSetName = "SendLogsNetworkShare", Mandatory = $false )] [char]$OutputShareDriveLetter, [Parameter( ParameterSetName = "SendLogsSasUri", Mandatory = $true )] [System.String]$OutputSasUri ) $scriptVersion = "2022.04.07.1" enum Status { Undetermined OK Failed TimedOut Incomplete NotDone } class MachineStatus { [String]$Role [String]$Name [String]$IPAddress [String]$ServerState [String]$workerSizeName [String]$ComputeMode [Boolean]$Reachable [Status]$CollectStatus [Status]$CopyStatus [Status]$CleanupStatus MachineStatus() { $this.Reachable = $false $this.CollectStatus = [Status]::Undetermined $this.CopyStatus = [Status]::Undetermined $this.CleanupStatus = [Status]::Undetermined } } $StatusColors = @{ Undetermined = [ConsoleColor]::Cyan OK = [ConsoleColor]::Green Failed = [ConsoleColor]::Red TimedOut = [ConsoleColor]::Red Incomplete = [ConsoleColor]::Yellow NotDone = [ConsoleColor]::Yellow } function New-AzsSupportNetworkShare { <# .SYNOPSIS Creates a new network share directory for AzS Hub CSS to leverage .DESCRIPTION Creates a new network share that can be leveraged to copy files in/out of the Azure Stack Hub environment to customer's datacenter .PARAMETER DriveLetter Specify the drive letter mapping that you want to create .PARAMETER RemoteSharePath Specify the network share that is accessible from the Privileged Endpoint via TCP Port 445 .PARAMETER RemoteShareCredentials Specify the credentials that are used to access the network share location .EXAMPLE New-AzsSupportNetworkShare -DriveLetter z -RemoteSharePath "\\192.168.0.100\share" -RemoteShareCredentials (Get-Credential) #> [CmdletBinding()] param ( [Parameter()] [string]$DriveLetter, [Parameter()] [ValidateNotNullOrEmpty()] [string]$RemoteSharePath, [Parameter()] [ValidateNotNullOrEmpty()] [System.Management.Automation.PSCredential]$RemoteShareCredentials ) try { $TotalAttempts = 0 $MaxRetry = 3 if ([string]::IsNullOrEmpty($DriveLetter)) { $DriveLetter = 'z' "No DriveLetter was specified. Using {0}" -f $DriveLetter | Write-Host -ForegroundColor Cyan } do { $TotalAttempts++ "Attempting to map network drive {0} to {1}" -f $DriveLetter, $RemoteSharePath | Write-Host -ForegroundColor Cyan Try { New-PSDrive -Name $DriveLetter -Description "Network Drive used by AppServiceLogs" -PSProvider FileSystem -Root $RemoteSharePath -Credential $RemoteShareCredentials -Persist -Scope Global -ErrorAction Stop | Out-Null } # Catch and looking for exceptions to be able to gracefully prompt user for required information Catch [System.Runtime.InteropServices.ExternalException] { Switch -Wildcard ($_.Exception) { '*The user name or password is incorrect*' { "Ensure that the credentials provided to {0} are correct" -f $RemoteSharePath | Write-Host -ForegroundColor Yellow $RemoteShareCredentials = Get-Credential -Message "Credentials used to access the remote share" } '*The specified network password is not correct*' { "Ensure that the credentials provided to {0} are correct" -f $RemoteSharePath | Write-Host -ForegroundColor Yellow $RemoteShareCredentials = Get-Credential -Message "Credentials used to access the remote share" } '*The network path was not found*' { "Ensure that the network share path is correct and accessible from {0}" -f $env:COMPUTERNAME | Write-Host -ForegroundColor Yellow $userInputValues = Get-UserInputValues -Properties "NetworkSharePath" $RemoteSharePath = $userInputValues.NetworkSharePath } '*The network resource or device is no longer available*' { "Ensure that the network share path is correct and accessible from {0}" -f $env:COMPUTERNAME | Write-Host -ForegroundColor Yellow $userInputValues = Get-UserInputValues -Properties "NetworkSharePath" $RemoteSharePath = $userInputValues.NetworkSharePath } '*The local device name is already in use*' { "{0} drive is already in use. Please confirm if you want to remove existing mapped drive" -f $DriveLetter | Write-Host -ForegroundColor Yellow Get-PSDrive -PSProvider FileSystem | Format-Table Name, DisplayRoot, CurrentLocation, Description try { $choice = Get-UserInput -Message "Do you want to remove the existing network drive? [Y/N]:" switch ($choice) { Y { "User has opted to remove existing network drive" | Write-Host -ForegroundColor Cyan Get-PSDrive -Name $DriveLetter | Remove-PSDrive } N { throw "User has opted to not remove the existing network drive {0}" -f $DriveLetter } Default { throw "Invalid response" } } } catch { throw $_ } } '*The local device name has a remembered connection to another network resource*' { # in this scenario, we have two seperate powershell sessions where there might be a mapped drive to same resource # psdrive functions do not detect these, and instead need to leverage net use "{0} drive is already defined as persistent connection or connected on another session. Please confirm if you want to remove existing mapped connection" -f $DriveLetter | Write-Host -ForegroundColor Yellow net use try { $choice = Get-UserInput -Message "Do you want to remove the existing network drive? [Y/N]:" switch ($choice) { Y { "User has opted to remove existing network drive" | Write-Host -ForegroundColor Cyan net use "${DriveLetter}:" /delete } N { throw "User has opted to not remove the existing network drive {0}" -f $DriveLetter } Default { throw "Invalid response" } } } catch { throw $_ } } default { "Provide network share and credentials again" | Write-Host -ForegroundColor Yellow $userInputValues = Get-UserInputValues -Properties "DriveLetter,NetworkSharePath" $DriveLetter = $userInputValues.DriveLetter $RemoteSharePath = $userInputValues.NetworkSharePath $RemoteShareCredentials = Get-Credential -Message "Credentials used to access the remote share" } } } catch { throw $_ } } until((Test-Path -Path "$($DriveLetter):") -or $TotalAttempts -gt $MaxRetry) # Inform the operator if we were able to map the network drive if (Test-Path "$($DriveLetter):") { "Succesfully mapped network drive" | Write-Host -ForegroundColor Green } } catch { $_.Exception.Message | Write-Error } } 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 #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Properties ) try { $object = New-Object PSObject foreach ($property in ($Properties.Split(','))) { $property = $property.Trim() $object | Add-Member -MemberType NoteProperty -Name $property -Value (Get-UserInput -Message "$($property): ").Trim() } return $object } catch { $_.Exception.Message | Write-Error } } function Get-UserInput { <# .SYNOPSIS Used in scenarios where you need to prompt the user for input .PARAMETER Message The message that you want to display to the user .EXAMPLE $choice = Get-UserInput -Message "Do you want to proceed with operation? [Y/N]: " Switch($choice){ 'Y' {Do action} 'N' {Do action} default {Do action} } #> param ( [Parameter(Position = 0, ValueFromPipeline = $true)] [string]$Message, [string]$BackgroundColor = "Black", [string]$ForegroundColor = "Yellow" ) Write-Host -ForegroundColor:$ForegroundColor -BackgroundColor:$BackgroundColor -NoNewline $Message; return Read-Host } function Confirm-UserInput { param( [Parameter(Position = 0, ValueFromPipeline = $true)] [string]$Message = "Do you want to continue with this operation? [Y/N]: ", [string]$BackgroundColor = "Black", [string]$ForegroundColor = "Yellow" ) try { Write-Host -ForegroundColor:$ForegroundColor -BackgroundColor:$BackgroundColor -NoNewline $Message $answer = Read-Host return ($answer -ieq 'y') } catch { $_.Exception.Message | Write-Error } } function Test-Admin { [CmdletBinding(DefaultParameterSetName = "Default")] Param( ) Write-Verbose -Message "Starting '$($MyInvocation.MyCommand)'" $CurrentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent()) Write-Verbose -Message "Logged on user - $($CurrentUser.Identity.Name) " Write-Verbose -Message "Checking for elevation ... " if (($CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) -eq $false) { Write-Verbose -Message "In NON admin mode." return $false } else { Write-Verbose -Message "In admin mode." return $true } } function Get-JobsExecution { param( $jobPrefix, $roleServers, [System.Collections.ArrayList]$serversStatus, [SYstem.Int16]$jobsCompletedCount, [SYstem.Int16]$jobsFailedCount, [SYstem.Int16]$MaxParallelJobs, $startTime, $timer, $FilterByRole, $Step, [Switch]$Wait, [Switch]$Cleanup ) while (` (!$Cleanup -and ($activeJobs = @(Get-Job | Where-Object Name -Like ("{0}-*" -f $jobPrefix))).Count -ge $MaxParallelJobs -or $Wait) ` -or ($Cleanup -and ($activeJobs = @(Get-Job | Where-Object Name -Like ("{0}-*" -f $jobPrefix)))) ` -and ($timer -ge 0) ) { $activeJobsAtStartCount = $activeJobs.Count if ($jobsCompleted = $activeJobs | Where-Object State -ilike "Completed") { $jobsCompleted | ForEach-Object { $ipaddress = ($_.Name -split "-")[-1] "[{0}] -`t- Background job for {1} completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ipaddress | Write-Host -ForegroundColor Cyan if ($serverStatus = $serversStatus | Where-Object IPAddress -eq $ipaddress) { $serverStatus.$Step = [Status]::OK $_ | Receive-Job $jobsCompletedCount++ $_ | Remove-Job $activeJobsAtStartCount-- $Wait = $false } } } elseif ($jobsFailed = $activeJobs | Where-Object State -ilike "Failed" ) { $jobsFailed | ForEach-Object { $ipaddress = ($_.Name -split "-")[-1] "[{0}] -`t- Background job for {1} failed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ipaddress | Write-Host -ForegroundColor Red if ($serverStatus = $serversStatus | Where-Object IPAddress -eq $ipaddress) { $serverStatus.$Step = [Status]::Failed $_ | Receive-Job $jobsFailedCount++ $_ | Remove-Job $activeJobsAtStartCount-- $Wait = $false } } } $message = "[{0}] - `tJobs - Completed {1} - Running {2} - Failed {3} - Pending {4} - Process will timeout in {5} - Elapsed time {6}" -f ` (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ` $jobsCompletedCount, ` $activeJobsAtStartCount, ` $jobsFailedCount, ` ($roleServers.Count - $jobsCompletedCount - $jobsFailedCount - $activeJobsAtStartCount), ` ((Get-Date).AddSeconds(($timer) * 15) - (Get-Date)).ToString("hh\:mm\:ss"), ` ((Get-Date) - $startTime).ToString("hh\:mm\:ss") if (($activeJobs = @(Get-Job | Where-Object Name -like ("{0}-*" -f $jobPrefix))).Count -gt 0) { "{0} - Waiting 15 seconds" -f $message | Write-Host -Foreground Cyan Start-Sleep -Seconds 15 $timer-- } else { $message | Write-Host -Foreground Cyan } } if (($activeJobs = @(Get-Job | Where-Object Name -Like ("{0}-*" -f $jobPrefix))) -and $timer -le 0) { "[{0}] - `tCleaning up background jobs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $activeJobs | Format-Table -AutoSize | Out-String | Write-Verbose $activeJobs | ForEach-Object { $ipaddress = ($_.Name -split "-")[-1] "[{0}] -`t- Cleaning up background job for {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ipaddress | Write-Host -ForegroundColor Cyan if ($serverStatus = $serversStatus | Where-Object IPAddress -eq $ipaddress) { $serverStatus.$Step = [Status]::TimedOut $_ | Remove-Job -Force } } } return $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount } $mydocuments = [environment]::getfolderpath("mydocuments") $dir = "$mydocuments\AppServiceLogs" New-Item -Path $dir -ItemType Directory -Force | Out-Null $inst = "azcopy_windows_amd64_10.zip" $azcopy = "$azcopypath\AZcopy.exe" $Activity = "Azure Stack Hub - App Service Logs - Uploading" $Id = 1 $TotalSteps = 2 $Step = 1 switch ($PSCmdlet.ParameterSetName) { "CollectLogs" { if (!(Test-Admin)) { "This command requires an admin elevated session" | Write-Error return } $newFilterByRole = [System.Collections.ArrayList]::new() $roleNames = @(Get-AppServiceServer | Select-Object -ExpandProperty Role | Sort-Object -Unique) foreach ($providedRole in $FilterByRole) { if ($newRoleName = $roleNames -like "*$providedRole*") { $newFilterByRole.add([String]$newRolename) | Out-Null } } $FilterByRole = $newFilterByRole if ($FilterByRole -icontains "WebWorker" -and !$workerCred) { if (!($workerCred = Get-Credential -Message "Enter credentials for Worker Admin")) { Write-Error "Worker Admin credentials are required" return } } $startTime = Get-Date [String]$currentDate = (Get-Date -Date $startTime -Format "yyyyMMdd-HHmmss").ToString() $logDirectory = "c:\temp\AppServiceLogs_{0}" -f $currentDate $localLogDirectory = "c:\temp\AppServiceLogs_Local" do { try { $exitStatus = Stop-Transcript -ErrorAction Ignore "Cleaning zombie transcript" | Write-Warning } catch { $exitStatus = $null } } until (!$exitStatus) if ($zombieJobs = Get-Job -Name "AppService-*") { "Removing zombie jobs" | Write-Warning $zombieJobs | Remove-Job -Force } New-Item -Path $logDirectory -ItemType Directory -Force -ErrorAction Ignore | Out-Null $transcriptFileName = "{0}-CompleteAppRPLogCollection_{1}.txt" -f $env:COMPUTERNAME, $currentDate Start-Transcript -Path (Join-Path -Path $logDirectory -ChildPath $transcriptFileName) | Write-Host -ForegroundColor Yellow $collectionScript = { param( [Parameter(Mandatory = $true)] $NodeName, [Parameter(Mandatory = $true)] $NodeIPAddress, [Parameter(Mandatory = $true)] $localLogDirectory, [Parameter(Mandatory = $true)] $MaxZipSizeInMB, [Parameter(Mandatory = $true)] $FromDate, [Parameter(Mandatory = $true)] $ToDate, [Parameter(Mandatory = $true)] $currentDate ) function Compress-MultipleArchivesBasedOnMaxSize { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [String]$NodeName, [Parameter(Mandatory = $true)] [String]$NodeIPAddress, [Parameter(Mandatory = $true)] [String]$ZipOutputPath, [Parameter(Mandatory = $true)] [Object[]]$FilesToAdd, [Parameter(Mandatory = $false)] [Object[]]$ZipBasepath, [Parameter(Mandatory = $false)] [Decimal]$MaxZipSizeInMB = 100 ) $zipTimeStamp = Get-Date -Format "yyyyMMddHHmmss" $zipIndex = 0 Add-Type -Assembly System.IO.Compression.FileSystem [Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem') | Out-Null "[{0}|{1}|{2}] - `tMax zip file {3}MB" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $MaxZipSizeInMB | Write-Host -ForegroundColor Cyan $zipFileName = [System.IO.Path]::Combine($ZipOutputPath, ("{0}-{1}-{2}-{3}.zip" -f $NodeName, $NodeIPAddress, $zipTimeStamp, $zipIndex)) "[{0}|{1}|{2}] - `tCreating new zip file '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $zipFileName | Write-Host -ForegroundColor Cyan $zipObj = [System.IO.Compression.ZipFile]::Open($zipFileName, "Create") foreach ($fileName in ($NewFilesToAdd | Where-Object Name -notlike ("^$($NodeName)-$($NodeIPAddress)-\d{14}-\d+\.zip$"))) { if ((($zipFileNameInfo = [System.IO.FileInfo]$zipFileName).Length) -and $zipFileNameInfo.Length -gt ($MaxZipSizeInMB * 1MB)) { $zipObj.Dispose() $zipIndex++ $zipFileName = [System.IO.Path]::Combine($ZipOutputPath, ("{0}-{1}-{2}-{3}.zip" -f $NodeName, $NodeIPAddress, $zipTimeStamp, $zipIndex)) "[{0}|{1}|{2}] - `tCreating new zip file '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $zipFileName | Write-Host -ForegroundColor Cyan $zipObj = [System.IO.Compression.ZipFile]::Open($zipFileName, "Create") } if ($ZipBasepath) { $entryName = $fileName.FullName -replace ("^{0}\\" -f [Regex]::Escape($ZipBasepath)) } else { $entryName = $fileName.FullName -replace "^.:\\" } if (Test-Path -Path $FileName.FullName -PathType Leaf) { "[{0}|{1}|{2}] - `tAdding entry {3} for file '{4}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $entryName, $fileName.FullName | Write-Verbose if ($fileName.FullName -like "*.zip") { $compressionLevel = [System.IO.Compression.CompressionLevel]::NoCompression } else { $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal } [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipObj, $fileName.FullName, $entryName, $compressionLevel) | Out-Null } else { "[{0}|{1}|{2}] - `tSkipping. Does not exist. {3} for file '{4}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $entryName, $fileName | Write-Verbose } } $zipObj.Dispose() } function Copy-FilesToArchive { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [String]$NodeName, [Parameter(Mandatory = $true)] [String]$NodeIPAddress, [Parameter(Mandatory = $true)] [String]$SourceDirectory, [Parameter(Mandatory = $true)] [String]$SourceDirectoryDescription, [Parameter(Mandatory = $true)] [String]$DestinationDirectory, [Parameter(Mandatory = $false)] [System.DateTime]$FromDate = (Get-Date).AddDays(-14), [Parameter(Mandatory = $false)] [System.DateTime]$ToDate = (Get-Date) ) Set-StrictMode -Version 1.0 "[{0}|{1}|{2}] - Collect '{3}' directory '{4}' started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $SourceDirectoryDescription, $SourceDirectory | Write-Host -ForegroundColor Cyan "[{0}|{1}|{2}] - Considering files between {3} and {4}" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $FromDate, $ToDate | Write-Host -ForegroundColor Cyan if (Test-Path -Path $SourceDirectory) { Get-ChildItem -Path $SourceDirectory -Recurse -File | Where-Object { $_.LastWriteTime -ge $FromDate -and $_.LastWriteTime -le $ToDate } | ForEach-Object { $destinationFile = [System.IO.Path]::Combine($DestinationDirectory, ([System.IO.DirectoryInfo]$SourceDirectory).Name, ($_.FullName -replace ("^{0}" -f [Regex]::Escape($SourceDirectory)))) $destinationDirectoryTree = Split-Path -Path $destinationFile -Parent if (!(Test-Path -Path $destinationDirectoryTree -PathType Container)) { "[{0}|{1}|{2}] -- Creating destination directory '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $destinationDirectoryTree | Write-Verbose New-Item -Path $destinationDirectoryTree -ItemType Directory | Out-Null } "[{0}|{1}|{2}] -- Copying '{3}' to '{4}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_.FullName, $destinationFile | Write-Verbose Copy-Item -Path $_.FullName -Destination $destinationFile -Force } "[{0}|{1}|{2}] - Collect '{3}' directory '{4}' Completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $SourceDirectoryDescription, $SourceDirectory | Write-Host -ForegroundColor Green } else { "[{0}|{1}|{2}] - Collect '{3}' directory '{4}' does not exist" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $SourceDirectoryDescription, $SourceDirectory | Write-Host -ForegroundColor Yellow } } $VerbosePreference = $using:VerbosePreference $dataCollectionDir = [System.IO.Path]::Combine($localLogDirectory, "FilesCollected") try { #Remove AppServiceLogs_Local directory if already exists if (Test-Path -Path $localLogDirectory -PathType Container) { "[{0}|{1}|{2}] - Removing local log directory '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $localLogDirectory | Write-Host -ForegroundColor Cyan Remove-Item -Path $localLogDirectory -Recurse -Force -Confirm:$false | Out-Null } #Create AppServiceLogs_Local directory "[{0}|{1}|{2}] - Creating local log directory '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $dataCollectionDir | Write-Host -ForegroundColor Cyan New-Item -Path $dataCollectionDir -ItemType Directory -Force | Out-Null $transcriptFileName = [System.IO.Path]::Combine($localLogDirectory, ("{0}-{1}-AppRPLogCollection-{2}.txt" -f $NodeName, $NodeIPAddress, $currentDate)) "[{0}|{1}|{2}] - Starting transcript to '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $transcriptFileName | Write-Host -ForegroundColor Cyan Start-Transcript -Path $transcriptFileName | Write-Host -ForegroundColor Cyan "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - Starting log collection" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White $sharename = "CSS" #Logs to collect $collectGuestLogsPath = "C:\WindowsAzure\GuestAgent*\" $collectGuestLogsPath2 = "C:\WindowsAzure\Packages\" $httplogdirectory = "C:\DWASFiles\Log\" $ftpLogDirectory = "C:\inetpub\logs\LogFiles\" $websitesInstalldir = "C:\WebsitesInstall\" $windowsEventLogdir = "C:\Windows\System32\winevt\Logs" $webPILogdir = "C:\Program Files\IIS\Microsoft Web farm framework\roles\resources\antareslogs" $packagesdir = "C:\Packages" #Create CSS share New-SmbShare -Name $sharename -Path $dataCollectionDir -FullAccess "$env:UserDomain\$env:UserName" -ErrorAction Ignore | Out-Null #Starting CollectGuestLogs "[{0}|{1}|{2}] - Collect Guest Logs (CollectGuestLogs.exe) started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan if (Test-Path -Path $collectGuestLogsPath -PathType Any) { Start-Process "$collectGuestLogsPath\CollectGuestLogs.exe" -Verb runAs -WorkingDirectory $collectGuestLogsPath -Wait ; Move-Item $collectGuestLogsPath\*.zip -Destination $dataCollectionDir\ -Force Move-Item $collectGuestLogsPath\*.zip.json -Destination $dataCollectionDir\ -Force Get-Item $dataCollectionDir\*.zip.json | Rename-Item -NewName { $_.name -replace ".zip.json", ".json" } } elseif (Test-Path -Path $collectGuestLogsPath2 -PathType Any) { Start-Process "$collectGuestLogsPath2\CollectGuestLogs.exe" -Verb runAs -WorkingDirectory $collectGuestLogsPath2 -Wait ; Move-Item $collectGuestLogsPath2\*.zip -Destination $dataCollectionDir\ -Force Move-Item $collectGuestLogsPath2\*.zip.json -Destination $dataCollectionDir\ -Force Get-Item $dataCollectionDir\*.zip.json | Rename-Item -NewName { $_.name -replace ".zip.json", ".json" } } "[{0}|{1}|{2}] - Collect Guest Logs completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green if ($NodeName -notlike "CN*") { #Collecting IIS logs Copy-FilesToArchive -NodeName $NodeName -NodeIPAddress $NodeIPAddress -SourceDirectory $httplogdirectory -DestinationDirectory ([System.IO.Path]::Combine($dataCollectionDir, "HTTPLogs")) -SourceDirectoryDescription "IIS logs" -FromDate $FromDate -ToDate $ToDate #Collecting WFF logs Copy-FilesToArchive -NodeName $NodeName -NodeIPAddress $NodeIPAddress -SourceDirectory $webPILogdir -DestinationDirectory $dataCollectionDir -SourceDirectoryDescription "WFF logs" -FromDate $FromDate -ToDate $ToDate } #Collect FTP logs on Publisher servers if ($NodeName -like "FTP*") { #Collecting FTP logs Copy-FilesToArchive -NodeName $NodeName -NodeIPAddress $NodeIPAddress -SourceDirectory $ftpLogDirectory -DestinationDirectory ([System.IO.Path]::Combine($dataCollectionDir, "FTPLogs")) -SourceDirectoryDescription "FTP logs" -FromDate $FromDate -ToDate $ToDate } #Collecting Event logs "[{0}|{1}|{2}] - Collect Event logs (WebSites logs) started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan Copy-Item $windowsEventLogdir\Microsoft-Windows-WebSites%4Administrative.evtx -Destination $dataCollectionDir\ -Force Copy-Item $windowsEventLogdir\Microsoft-Windows-WebSites%4Operational.evtx -Destination $dataCollectionDir\ -Force Copy-Item $windowsEventLogdir\Microsoft-Windows-WebSites%4Verbose.evtx -Destination $dataCollectionDir\ -Force "[{0}|{1}|{2}] - Collect Event logs completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green #Collecting WebsitesInstall logs "[{0}|{1}|{2}] - Collect WebsitesInstall logs '{3}' started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $websitesInstalldir | Write-Host -ForegroundColor Cyan # WORK AROUND TO ADDRESS THE FACT THAT WINDOWS PATCHES PACKAGES (WITH ABOUT 4GB IN SIZE) WERE ALSO BEING COLLECTED $dataCollectionDir = Get-Item -Path $dataCollectionDir "[{0}|{1}|{2}] -- excluding files with extension like .msi or .msu" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan Get-ChildItem -Path $websitesInstalldir -Recurse | Where-Object { $_.psIsContainer -eq $false -and $_.Extension -notin (".msi", ".msu", ".exe") } | ForEach-Object { $destinationFile = Join-Path -Path $dataCollectionDir -ChildPath (Join-Path -Path ([System.IO.DirectoryInfo]$websitesInstalldir).Name -ChildPath ($_.FullName -replace ("^{0}" -f [Regex]::Escape($websitesInstalldir)))) $destinationDirectory = Split-Path -Path $destinationFile -Parent if (!(Test-Path -Path $destinationDirectory -PathType Container)) { "[{0}|{1}|{2}] -- Creating destination directory '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $destinationDirectory | Write-Verbose New-Item -Path $destinationDirectory -ItemType Directory | Out-Null } "[{0}|{1}|{2}] -- Copying '{3}' to '{4}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_.FullName, $destinationFile | Write-Verbose Copy-Item -Path $_.FullName -Destination $destinationFile -Force } "[{0}|{1}|{2}] - Collect WebsitesInstall logs completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green #Collecting C:\Packages "[{0}|{1}|{2}] - Collect C:\Packages started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan Copy-Item $packagesdir\ -Recurse -Destination $dataCollectionDir\ -Force "[{0}|{1}|{2}] - Collect C:\Packages completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}|{1}|{2}] - Finding files to compress started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $NewFilesToAdd = Get-ChildItem $dataCollectionDir -Recurse -File "[{0}|{1}|{2}] - Finding files to compress completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}|{1}|{2}] - Compressing Files started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan Compress-MultipleArchivesBasedOnMaxSize -NodeName $NodeName -NodeIPAddress $NodeIPAddress -ZipOutputPath $localLogDirectory -FilesToAdd $NewFilesToAdd -ZipBasepath $dataCollectionDir -MaxZipSizeInMB $MaxZipSizeInMB "[{0}|{1}|{2}] - Compressing Files completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}|{1}|{2}] - Cleaning up Files on directory {3} started" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $dataCollectionDir | Write-Host -ForegroundColor Cyan Remove-Item -Path $dataCollectionDir -Recurse -Force -Confirm:$false -ErrorAction Ignore "[{0}|{1}|{2}] - Cleaning up Files on directory {3} completed" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $dataCollectionDir | Write-Host -ForegroundColor Green Stop-Transcript | Write-Host -ForegroundColor Green } catch { "[{0}|{1}|{2}] - Failure found '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $PSItem.Exception.Message | Write-Host -ForegroundColor Red throw $PSItem } "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - Ending log collection" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White } $cleanupScript = { param( [Parameter(Mandatory = $true)] $NodeName, [Parameter(Mandatory = $true)] $NodeIPAddress, [Parameter(Mandatory = $true)] $localLogDirectory ) "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - Starting cleanup" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - Starting log cleanup" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan try { $sharename = "CSS" #Remove CSS share Get-SmbShare -Name $sharename | Remove-SmbShare -Confirm:$false -Force -ErrorAction Ignore | Out-Null #Remove AppServiceLogs_Local directory if already exists if (Test-Path -Path $localLogDirectory -PathType Any) { Remove-Item -Path $localLogDirectory -Recurse -Force -Confirm:$false -ErrorAction Ignore | Out-Null } } catch { "[{0}|{1}|{2}] - Failure found '{3}'" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $PSItem.Exception.Message | Write-Host -ForegroundColor Red throw $PSItem } "[{0}|{1}|{2}] - Completed log cleanup" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - Ending cleanup" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White "[{0}|{1}|{2}] - ******************************************************************************************************" -f $NodeName, $NodeIPAddress, (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor White } if (!$PSBoundParameters.ContainsKey("ToDate")) { "[{0}] - No ToDate parameter specified. Increasing the default value {1} by ading the TimeOutInMinutes {2}. Final value {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ToDate.ToString(), $TimeOutInMinutes, $ToDate.AddMinutes($TimeOutInMinutes) | Write-Host -ForegroundColor Cyan $ToDate = $ToDate.AddMinutes($TimeOutInMinutes) } "[{0}] - Script Version {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $scriptVersion | Write-Host -ForegroundColor Cyan "[{0}] - Parameters:" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan "[{0}] - `tFilterByRole {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($FilterByRole -join ",") | Write-Host -ForegroundColor Cyan "[{0}] - `tFilterByNode {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($FilterByNode -join ",") | Write-Host -ForegroundColor Cyan "[{0}] - `tTimeout in {1} minutes" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $TimeOutInMinutes | Write-Host -ForegroundColor Cyan "[{0}] - `tMaxParallelJobs {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $MaxParallelJobs | Write-Host -ForegroundColor Cyan "[{0}] - `tMaxZipSizeInMB {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $MaxZipSizeInMB | Write-Host -ForegroundColor Cyan "[{0}] - `tFromDate {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $FromDate | Write-Host -ForegroundColor Cyan "[{0}] - `tNumToDate {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $ToDate | Write-Host -ForegroundColor Cyan "[{0}] - `tTimeframe {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($ToDate - $FromDate) | Write-Host -ForegroundColor Cyan $timer = ($TimeOutInMinutes * 60) / 15 $originalTimer = $timer "[{0}] - Collecting logs for roles {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($FilterByRole -join ",") | Write-Host -ForegroundColor Cyan $roleServers = Get-AppServiceServer | Where-Object Role -In $FilterByRole | Sort-Object @{E = { [Version]$_.Name } } if ($PSBoundParameters.ContainsKey("FilterByNode")) { "[{0}] - Filtering by node(s) '{1}'" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), ($FilterByNode -join "','") | Write-Host -ForegroundColor Yellow $roleServers = $roleServers | Where-Object Name -in $FilterByNode } "[{0}] - Starting role server information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan Get-AppServiceServer | Select-Object Name, Status, Role, CpuPercentage, MemoryPercentage, ServerState, PlatformVersion | Format-Table | Out-File (Join-Path -Path $logDirectory -ChildPath "Get-AppServiceServer.txt") Get-AppServiceServer | ConvertTo-Json | Out-File $logDirectory"\AppServiceServer.json" "[{0}] - Completed role server information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Starting Config Global information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $appServiceConfigGlobal = Get-AppServiceConfig -Type Global "[{0}] - Redacting secrets on Config Global information" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $appServiceConfigGlobal.BitbucketClientId = "[REDACTED]" $appServiceConfigGlobal.BitbucketClientSecret = "[REDACTED]" $appServiceConfigGlobal.BitbucketNextClientId = "[REDACTED]" $appServiceConfigGlobal.BitbucketNextClientSecret = "[REDACTED]" $appServiceConfigGlobal.BitbucketProdClientId = "[REDACTED]" $appServiceConfigGlobal.BitbucketProdClientSecret = "[REDACTED]" $appServiceConfigGlobal.BitbucketStageClientId = "[REDACTED]" $appServiceConfigGlobal.BitbucketStageClientSecret = "[REDACTED]" $appServiceConfigGlobal.CertificatePassword = "[REDACTED]" $appServiceConfigGlobal.GitHubClientId = "[REDACTED]" $appServiceConfigGlobal.GitHubClientSecret = "[REDACTED]" $appServiceConfigGlobal.InfrastructureClientCertificatePassword = "[REDACTED]" $appServiceConfigGlobal.InfrastructureClientId = "[REDACTED]" $appServiceConfigGlobal.ManagementServerCertificatePassword = "[REDACTED]" $appServiceConfigGlobal.PublisherServerCertificatePassword = "[REDACTED]" $appServiceConfigGlobal.TokenRequestCertificatePassword = "[REDACTED]" $appServiceConfigGlobal.UsageStorageAccountConnString = "[REDACTED]" $appServiceConfigGlobal | ConvertTo-Json | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceConfigGlobal.json") $appServiceConfigGlobal | Format-List | Out-String | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceConfigGlobal.txt") "[{0}] - Completed Config Global information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Starting app server event collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan Get-AppServiceEvent -StartTime $FromDate -EndTime $ToDate | ConvertTo-Json | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceEvent.json") "[{0}] - Completed app server event collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Starting ActiveController operation information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $appServiceOperationActiveController = Get-AppServiceOperation -OperatorName ActiveController $appServiceOperationActiveController | ConvertTo-Json | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceOperationActiveController.json") $appServiceOperationActiveController | Format-List | Out-String | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceOperationActiveController.txt") "[{0}] - Completed ActiveController operation information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Starting WFF operation information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $appServiceOperationWFF = Get-AppServiceOperation -OperatorName WFF $appServiceOperationWFF | ConvertTo-Json | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceOperationWFF.json") $appServiceOperationWFF | Format-List | Out-String | Out-File (Join-Path -Path $logDirectory -ChildPath "AppServiceOperationWFF.txt") "[{0}] - Completed WFF operation information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green $serversStatus = [System.Collections.ArrayList]::new() #Checking reachability "[{0}] - Reachability test started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $currentProgressPreference = $ProgressPreference $ProgressPreference = "SilentlyContinue" foreach ($server in $roleServers) { $serverStatus = New-Object MachineStatus $serverStatus.Role = $server.Role $serverStatus.IPAddress = $server.Name $serverStatus.ServerState = $server.ServerState $serverStatus.workerSizeName = $server.workerSizeName $serverStatus.ComputeMode = $server.ComputeMode "[{0}] - `tTesting reachability for {1} - {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan if (Test-NetConnection $server.name -Port 5985) { if ($server.role -eq "WebWorker") { try { $serverStatus.Name = Invoke-Command -ComputerName $server.Name -ScriptBlock { $env:Computername } -Credential $workerCred $serverStatus.Reachable = $true } catch { $serverStatus.Reachable = $false } } else { try { $serverStatus.Name = Invoke-Command -ComputerName $server.Name -ScriptBlock { $env:Computername } $serverStatus.Reachable = $true } catch { $serverStatus.Reachable = $false } } } else { $serverStatus.Reachable = $false $serverStatus.Name = "Unknown" } $serversStatus.Add($serverStatus) | Out-Null } $ProgressPreference = $currentProgressPreference "[{0}] - Reachability test completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Setup Log collection jobs started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $jobPrefix = "AppService-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss").ToString() $jobsConfig = [System.Collections.ArrayList]::new() foreach ($_serverStatus in $serversStatus) { if ($_serverStatus.Reachable) { "[{0}] - `tSetting up data collection on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_serverStatus.Name, $_serverStatus.IPAddress, $_serverStatus.Role | Write-Host -ForegroundColor Cyan if ($_serverStatus.role -eq "WebWorker") { $jobsConfig.Add([PSCustomObject]@{ ServerName = $_serverStatus.Name ServerIPAddress = $_serverStatus.IPAddress ServerRole = $_serverStatus.Role JobName = ("{0}-{1}" -f $jobPrefix, $_serverStatus.IPAddress) Started = $false JobParams = @{ ComputerName = $_serverStatus.IPAddress ScriptBlock = $collectionScript Credential = $workerCred AsJob = $true JobName = ("{0}-{1}" -f $jobPrefix, $_serverStatus.IPAddress) Verbose = $Verbose ArgumentList = ($_serverStatus.Name, $_serverStatus.IPAddress, $localLogDirectory, $MaxZipSizeInMB, $FromDate, $ToDate, $currentDate) } }) | Out-Null } else { $jobsConfig.Add([PSCustomObject]@{ ServerName = $_serverStatus.Name ServerIPAddress = $_serverStatus.IPAddress ServerRole = $_serverStatus.Role JobName = ("{0}-{1}" -f $jobPrefix, $_serverStatus.IPAddress) Started = $false JobParams = @{ ComputerName = $_serverStatus.IPAddress ScriptBlock = $collectionScript AsJob = $true JobName = ("{0}-{1}" -f $jobPrefix, $_serverStatus.IPAddress) Verbose = $Verbose ArgumentList = ($_serverStatus.Name, $_serverStatus.IPAddress, $localLogDirectory, $MaxZipSizeInMB, $FromDate, $ToDate, $currentDate) } }) | Out-Null } } else { "[{0}] - `tSkipped data collection on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_serverStatus.Name, $_serverStatus.IPAddress, $_serverStatus.Role | Write-Host -ForegroundColor Cyan $serverStatus.CollectStatus = [Status]::NotDone } } "[{0}] - Setup Log collection jobs Completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan #Log collection "[{0}] - Log collection started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $jobPrefix = "AppService-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss").ToString() $jobsCompletedCount = 0 $jobsFailedCount = 0 while ($jobsToRun = $jobsConfig | Where-Object Started -eq $false) { foreach ($_jobToRun in $jobsToRun) { $IPAddressesRunning = Get-Job | Where-Object Name -like "$jobPrefix-*" | ForEach-Object { $_.Name.Split("-")[-1] } $rolesRunning = ($serversStatus | Where-Object IPAddress -in $IPAddressesRunning).Role | Sort-Object -Unique if ($VerbosePreference -eq "Continue") { "RolesRunning" | Write-Host -ForegroundColor Magenta $rolesRunning | Write-Host -ForegroundColor Magenta } if ($_jobToRun.ServerRole -notin $rolesRunning) { $jobParams = $_jobToRun.JobParams try { "[{0}] - `tStarting data collection on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $_jobToRun.ServerName, $_jobToRun.ServerIPAddress, $_jobToRun.ServerRole | Write-Host -ForegroundColor Cyan Invoke-Command @JobParams | Out-Null } catch {} foreach ($_jobConfig in $jobsConfig) { if ($_jobConfig.JobName -eq $_jobToRun.JobName) { $_jobConfig.Started = $true break } } $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution ` -JobPrefix $jobPrefix ` -roleServers $roleServers ` -serversStatus $serversStatus ` -jobsCompletedCount $jobsCompletedCount ` -jobsFailedCount $jobsFailedCount ` -MaxParallelJobs $MaxParallelJobs ` -startTime $startTime ` -timer $timer ` -Step "CollectStatus" if ($timer -le 0) { break } } } $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution ` -JobPrefix $jobPrefix ` -roleServers $roleServers ` -serversStatus $serversStatus ` -jobsCompletedCount $jobsCompletedCount ` -jobsFailedCount $jobsFailedCount ` -MaxParallelJobs $MaxParallelJobs ` -startTime $startTime ` -timer $timer ` -Step "CollectStatus" ` -Wait if ($timer -le 0) { break } } $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution ` -JobPrefix $jobPrefix ` -roleServers $roleServers ` -serversStatus $serversStatus ` -jobsCompletedCount $jobsCompletedCount ` -jobsFailedCount $jobsFailedCount ` -MaxParallelJobs $MaxParallelJobs ` -startTime $startTime ` -timer $timer ` -Step "CollectStatus" ` -FilterByRole $FilterByRole ` -Cleanup if ($remaniningJobs = Get-Job -Name ("{0}-*" -f $jobPrefix)) { "[{0}] - Cleaning up background jobs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $remaniningJobs | Remove-Job -Force } "[{0}] - Log collection completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green #Log copying "[{0}] - Log copying started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $jobPrefix = "AppService-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss").ToString() $jobsCompletedCount = 0 $jobsFailedCount = 0 $timer = $originalTimer foreach ($serverStatus in $serversStatus) { if ($serverStatus.CollectStatus -eq [Status]::OK) { if ($serverStatus.Role -eq "WebWorker") { "[{0}] - `tStarting data copying on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan Start-Job { $localLogDirectory = $using:localLogDirectory if ($workerSession = New-PSSession -Credential $using:workerCred -ComputerName $($using:serverStatus.IPAddress)) { Invoke-Command -Session $workerSession -ScriptBlock { Get-ChildItem -Path $using:localLogDirectory } | Format-Table LastWriteTime, Length, FullName -AutoSize | Out-String | Write-Host -ForegroundColor Cyan Copy-Item -FromSession $workerSession -Path $localLogDirectory\* -Destination $using:logDirectory Remove-PSSession $workerSession } else { "Unable to create PSSession to {0}" -f $using:serverStatus.IPAddress | Write-Error } } -Name ("{0}-{1}" -f $jobPrefix, $serverStatus.IPAddress) | Out-Null } else { "[{0}] - `tStarting data copying on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan Start-Job { Get-ChildItem -Path \\$($using:serverStatus.IPAddress)\$($using:localLogDirectory -replace "C:","C$") | Format-Table LastWriteTime, Length, FullName -AutoSize | Out-String | Write-Host -ForegroundColor Cyan Copy-Item \\$($using:serverStatus.IPAddress)\$($using:localLogDirectory -replace "C:","C$")\* -Destination $using:logDirectory } -Name ("{0}-{1}" -f $jobPrefix, $serverStatus.IPAddress) | Out-Null } } else { "[{0}] - `tSkipped data moving on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan $serverStatus.CopyStatus = [Status]::NotDone } $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution ` -JobPrefix $jobPrefix ` -roleServers $roleServers ` -serversStatus $serversStatus ` -jobsCompletedCount $jobsCompletedCount ` -jobsFailedCount $jobsFailedCount ` -MaxParallelJobs $MaxParallelJobs ` -startTime $startTime ` -timer $timer ` -Step "CopyStatus" ` -FilterByRole $FilterByRole } $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution ` -JobPrefix $jobPrefix ` -roleServers $roleServers ` -serversStatus $serversStatus ` -jobsCompletedCount $jobsCompletedCount ` -jobsFailedCount $jobsFailedCount ` -MaxParallelJobs $MaxParallelJobs ` -startTime $startTime ` -timer $timer ` -Step "CopyStatus" ` -FilterByRole $FilterByRole ` -Cleanup ` if ($remaniningJobs = Get-Job -Name ("{0}-*" -f $jobPrefix)) { "[{0}] - Cleaning up background jobs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $remaniningJobs | Remove-Job -Force } "[{0}] - Log copying completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green #Log cleanup "[{0}] - Log cleanup started" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $jobPrefix = "AppService-{0}" -f (Get-Date -Format "yyyyMMdd-HHmmss").ToString() $jobsCompletedCount = 0 $jobsFailedCount = 0 $timer = $originalTimer foreach ($serverStatus in $serversStatus) { if ($serverStatus.CopyStatus -eq [Status]::OK) { if ($serverStatus.role -eq "WebWorker") { try { "[{0}] - `tStarting data cleanup on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan Invoke-Command -ComputerName $serverStatus.IPAddress ` -ScriptBlock $cleanupScript ` -Credential $workerCred ` -AsJob ` -JobName ("{0}-{1}" -f $jobPrefix, $serverStatus.IPAddress) ` -Verbose:$Verbose ` -ArgumentList $serverStatus.Name, $serverStatus.IPAddress, $localLogDirectory ` | Out-Null } catch { $serverStatus.CleanupStatus = [Status]::Failed } } else { try { "[{0}] - `tStarting data cleanup on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan Invoke-Command -ComputerName $serverStatus.IPAddress ` -ScriptBlock $cleanupScript ` -AsJob ` -JobName ("{0}-{1}" -f $jobPrefix, $serverStatus.IPAddress) ` -Verbose:$Verbose ` -ArgumentList $serverStatus.Name, $serverStatus.IPAddress, $localLogDirectory ` | Out-Null } catch { $serverStatus.CleanupStatus = [Status]::Failed } } } else { "[{0}] - `tSkipped data cleanup on {1} - {2} - {3}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString(), $serverStatus.Name, $serverStatus.IPAddress, $serverStatus.Role | Write-Host -ForegroundColor Cyan $serverStatus.CleanupStatus = [Status]::NotDone } $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution ` -JobPrefix $jobPrefix ` -roleServers $roleServers ` -serversStatus $serversStatus ` -jobsCompletedCount $jobsCompletedCount ` -jobsFailedCount $jobsFailedCount ` -MaxParallelJobs $MaxParallelJobs ` -startTime $startTime ` -timer $timer ` -Step "CleanupStatus" ` -FilterByRole $FilterByRole } $timer, $serversStatus, $jobsCompletedCount, $jobsFailedCount = Get-JobsExecution ` -JobPrefix $jobPrefix ` -roleServers $roleServers ` -serversStatus $serversStatus ` -jobsCompletedCount $jobsCompletedCount ` -jobsFailedCount $jobsFailedCount ` -MaxParallelJobs $MaxParallelJobs ` -startTime $startTime ` -timer $timer ` -Step "CleanupStatus" ` -FilterByRole $FilterByRole ` -Cleanup if ($remaniningJobs = Get-Job -Name ("{0}-*" -f $jobPrefix)) { "[{0}] - Cleaning up background jobs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $remaniningJobs | Remove-Job -Force } "[{0}] - Log cleanup completed" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green $endTime = Get-Date $actionTree = [PSCustomObject]@{ ScriptVersion = $scriptVersion LogDirectory = $logDirectory LogFileName = $transcriptFileName StartTime = $startTime EndTime = $endTime DurationObj = $endTime - $startTime Duration = ($endTime - $startTime).ToString() FilterByRole = $FilterByRole FilterByNode = $FilterByNode TimeOutInMinutes = $TimeOutInMinutes MaxParallelJobs = $MaxParallelJobs MaxZipSizeInMB = $MaxZipSizeInMB FromDate = $FromDate ToDate = $ToDate TimeFrameObj = $ToDate - $FromDate TimeFrame = ($ToDate - $FromDate).ToString() ServersStatus = $serversStatus | Select-Object ` Role, Name, IPAddress, ServerState, workerSizeName, ComputeMode, Reachable, ` @{N = "CollectStatus"; E = { [Enum]::GetName([Status], $_.CollectStatus) } }, ` @{N = "CopyStatus"; E = { [Enum]::GetName([Status], $_.CopyStatus) } }, ` @{N = "CleanupStatus"; E = { [Enum]::GetName([Status], $_.CleanupStatus) } } } $actionTree | ConvertTo-Json | Out-File -FilePath (Join-Path -Path $logDirectory -ChildPath "ActionTree.json") $actionTree | Select-Object * -ExcludeProperty ServersStatus, TimeFrameObj, DurationObj | Format-List # We stop the transcript before printing the colored status table because the output gets all scrambled Stop-Transcript | Write-Host -ForegroundColor Yellow if ($serversStatus) { $header = ($serversStatus | Format-Table | Out-String -Stream)[1] if ($thingsFound = [Regex]::Matches($header, "\w+ *")) { ($serversStatus | Format-Table | Out-String -Stream)[0..2] | Write-Host $serversStatus | ForEach-Object { $item = $_ $thingsFound | ForEach-Object { $match = $_ $property = $_.Value.TrimEnd() switch ($property) { { $_ -like "*Status" } { $colorParam = @{} if (($value = [Enum]::GetName([Status], $item.$property)) -and ($StatusColors[$value])) { $colorParam = @{ ForegroundColor = $StatusColors[$value] } } else { $value = "Undetermined" $colorParam = @{ ForegroundColor = $StatusColors[[Enum]::GetName([Status], 0)] } } $value = ($value).ToString().PadLeft($match.Length - 1, " ") } "Reachable" { $colorParam = @{} if ($item.$property -eq $true) { $colorParam = @{ ForegroundColor = [ConsoleColor]::Green } } else { $colorParam = @{ ForegroundColor = [ConsoleColor]::Red } } $value = ($item.$property).ToString().PadRight($match.Length - 1, " ") } Default { $colorParam = @{} if ($item.$property) { $value = ($item.$property).ToString().PadRight($match.Length - 1, " ") } else { $value = "".PadRight($match.Length - 1, " ") } } } "{0} " -f $value | Write-Host @colorParam -NoNewline } Write-Host -ForegroundColor Yellow } } } else { "Nothing collected" | Write-Host -ForegroundColor Yellow -BackgroundColor Red } Write-Host Write-Host "If logs are to be copied manually copy the contents of '{0}' from this computer using your prefered method" -f $logDirectory | Write-Host -ForegroundColor Yellow Write-Host "If logs are to be copied to an external share execute the following command (Requires outbound SMB/CIFS connectivity):" | Write-Host -ForegroundColor Yellow "'{0}' -LogBundlePath '{1}' -OutputSharePath '\\xx.xx.xx.xx\share' -OutputSharePathCreds (Get-Credential) -OutputShareDriveLetter X" -f $MyInvocation.MyCommand.Path, $logDirectory | Write-Host -ForegroundColor Yellow Write-Host "If logs are to be copied directly to a sasuri provided by the support enginner execute the following command (Requires outbound internet connectivity):" | Write-Host -ForegroundColor Yellow "'{0}' -LogBundlePath '{1}' OutputSasUri 'SASURI'" -f $MyInvocation.MyCommand.Path, $logDirectory | Write-Host -ForegroundColor Yellow Write-Host } "InstallAzCopy" { if ($azcopy = Get-ChildItem -Path "$dir\AzCopy.exe" -Recurse | Select-Object -ExpandProperty FullName) { return } do { Write-Host "" Write-Host "AzCopy is required. It was not detected in the default location. Would you like to download and install it? [Y/N]: " -f Yellow $choice = Read-Host Write-Host "" $ok = @("Y", "N") -contains $choice if ( -not $ok) { Write-Host "Invalid selection" } } until ( $ok ) switch ( $choice ) { "Y" { Write-Host "You selected Yes. Downloading AzCopy from http://aka.ms/downloadazcopy-v10-windows and installing to $dir" Start-BitsTransfer -Source http://aka.ms/downloadazcopy-v10-windows -Destination $dir\$inst | Wait-Process Import-Module Microsoft.PowerShell.Archive Expand-Archive -Path $dir\$inst -DestinationPath $dir if ($azcopy = Get-ChildItem -Path "$dir\AzCopy.exe" -Recurse | Select-Object -ExpandProperty FullName) { "AzCopy is now available" | Write-Host -ForegroundColor Green } else { "AzCopy is NOT available. Install it manually" | Write-Host -ForegroundColor Red } } Default { "AzCopy is NOT available. Install it manually" | Write-Host -ForegroundColor Red } } } "SendLogsNetworkShare" { $TotalSteps = 4 $itemPath = Get-Item -Path $LogBundlePath try { $Step = 1 $StepText = "Calculating size to copy" $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"' $StatusBlock = [ScriptBlock]::Create($StatusText) Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText $colItems = (Get-ChildItem $LogBundlePath -Recurse | Measure-Object -Property length -Sum) $size = ("{0:N2}" -f ($colItems.sum / 1MB) + " MB") Write-Host "" Write-Host "Upload times will vary across environments. The total size of the log bundle is "-ForegroundColor Cyan -NoNewline Write-Host "$size" -ForegroundColor Yellow $count = $colItems.Count Write-Host "The number of files to copy is "-ForegroundColor Cyan -NoNewline Write-Host "$count" -ForegroundColor Yellow Write-Host "" $Step = 2 $StepText = "Map network share" $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"' $StatusBlock = [ScriptBlock]::Create($StatusText) Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText New-AzsSupportNetworkShare -DriveLetter:$OutputShareDriveLetter -RemoteSharePath $OutputSharePath.FullName -RemoteShareCredentials $OutputSharePathCreds $Step = 2 $StepText = "Copying the following items to {0}" -f $OutputSharePath.FullName $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"' $StatusBlock = [ScriptBlock]::Create($StatusText) Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText "Copying the following items to {0}`r`n`n{1}" -f $OutputSharePath.FullName, (` $itemPath ` | Select-Object @{n = "Name"; e = { "`t$($_.FullName)" } } ` | Select-Object -ExpandProperty Name ` | Out-String ` ) | Write-Host -ForegroundColor Cyan Copy-Item -Path $itemPath.FullName -Destination $OutputSharePath.FullName -Recurse -Force -Verbose } catch { throw $_ } } "SendLogsSasUri" { $TotalSteps = 3 $Step = 1 $StepText = "Checking for AzCopy" $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"' $StatusBlock = [ScriptBlock]::Create($StatusText) Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText if (!($azcopy = Get-ChildItem -Path "$dir\AzCopy.exe" -Recurse | Select-Object -ExpandProperty FullName)) { "AzCopy is not installed. Run {0} -InstallAzCopy" -f $PSCmdlet.MyInvocation.MyCommand return } else { $Step = 2 $StepText = "Calculating size upload" $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"' $StatusBlock = [ScriptBlock]::Create($StatusText) Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText $colItems = (Get-ChildItem $LogBundlePath -Recurse | Measure-Object -Property length -Sum) $size = ("{0:N2}" -f ($colItems.sum / 1MB) + " MB") Write-Host "" Write-Host "Upload times will vary across environments. The total size of the log bundle is "-ForegroundColor Cyan -NoNewline Write-Host "$size" -ForegroundColor Yellow $count = $colItems.Count Write-Host "The number of files to upload is "-ForegroundColor Cyan -NoNewline Write-Host "$count" -ForegroundColor Yellow Write-Host "" Write-Host "Starting to upload with Azcopy. Please check log in " -ForegroundColor Cyan -NoNewline Write-Host "$($env:HOMEDIR)\.azcopy" -ForegroundColor Yellow -NoNewline Write-Host " to see if there are issues uploading" -ForegroundColor Cyan Write-Host "" $Step = 3 $StepText = "Uploading" $StatusText = '"Step $($Step.ToString().PadLeft("$TotalSteps.Count.ToString()".Length)) of $TotalSteps | $StepText"' $StatusBlock = [ScriptBlock]::Create($StatusText) Write-Progress -Id $Id -Activity $Activity -Status (& $StatusBlock) -PercentComplete ($Step / $TotalSteps * 100) -CurrentOperation $StepText Invoke-Command ([ScriptBLock]::Create("$azcopy copy ""$LogBundlePath"" ""$OutputSasUri"" --% --recursive --put-md5")) Write-Host "Azcopy run is finished." -ForegroundColor Yellow } } } } #region Global Vars ### Load Global vars to be used by functions "[{0}] - Collecting App Service RP Information" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan # Load Microsoft.Web.Hosting assembly # 'Add-Type' is primarily used for loading .NET types (classes) and compiling C# code on-the-fly. It expects a full assembly name, including the version, culture, and public key token. # It may also require that the assembly be available in the Global Assembly Cache (GAC) or in a specific directory. [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Web.Hosting") | Out-Null ### Global Vars $script:appServiceConfigGlobal = Get-AppServiceConfig -Type Global #Load global:SiteManager $script:SiteManager = New-Object Microsoft.Web.Hosting.SiteManager "[{0}] - Completed App Service RP information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green $startTime = Get-Date [String]$currentDate = (Get-Date -Date $startTime -Format "yyyyMMdd-HHmmss").ToString() $script:logDirectory = "c:\temp\Test-AzureStackAppServiceRP" ## Get ContentShare from global:SiteManager $script:fileshare = $script:SiteManager.HostingConfiguration.ContentShare ## create fileshare var $script:fileshareip = $script:fileshare -replace "^\\\\|\\.*$", "" ## Get Conn Strings $script:meteringStr = Get-AppServiceConnectionString -Type:Metering $script:hostingStr = Get-AppServiceConnectionString -Type:Hosting ## create sql ip var - fixed tcp: $script:sqlserverip = ($script:hostingStr -replace "Data Source=(.*?);.*", '$1') -replace "^tcp:", "" #endregion function Test-AzsSupportAppService { <# .SYNOPSIS This script is used to collect App Service RP Health information and export it to a HTML report. .DESCRIPTION The script collects App Service RP information, including configuration, events, and certificates. It can export the collected data to a file. .PARAMETER ExportEventsFile If specified, the script will export last 24h of the App Service events to a file. .PARAMETER ExportGlobalConfigFile If specified, the script will export the global configuration to a file. .PARAMETER WebAppName The name of the web app to check. If Web App not found, it will prompt for selection. .EXAMPLE Test-AzsSupportAppService This example runs the script to test the health of the App Service RP (Default usage). .EXAMPLE Test-AzsSupportAppService -WebAppName "MyWebApp" (When a Web App is impacted) This example runs the script to test the health of the App Service RP for the specified web app without exporting events and global configuration. It will add the Web App Workers to all the checks performed. .EXAMPLE Test-AzsSupportAppService -ExportEventsFile -ExportGlobalConfigFile -WebAppName "MyWebApp" (specific scenarios) This example runs the script to test the health of the App Service RP, exporting both events and global configuration for the specified web app. #> ### Parameters [CmdletBinding()] param ( [Parameter()] [Switch]$ExportEventsFile = $false, [Parameter()] [Switch]$ExportGlobalConfigFile = $false, [Parameter()] [string]$WebAppName = "" ) ###### Formating Functions ############# function ConvertTo-HtmlTableWithHeaderAndTitle { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [array]$InputObjects, [string]$TestGroup ) process { $htmlOutput = "" foreach ($InputObject in $InputObjects) { $title = $InputObject.Title $data = $InputObject.Data # Convert and format to HTML Table $htmlTable = $data | ConvertTo-Html -Fragment switch ($TestGroup) { "AppServiceRPOverview" { $htmlTable = ProcessHTMLAppServiceRPOverview } "AppServiceEvents" { $htmlTable = ProcessHTMLAppServiceEvents } "Certificates" { $htmlTable = ProcessHTMLCertificates } "SQLService" { $htmlTable = ProcessHTMLSQLService } "VMGuestOS" { $htmlTable = ProcessHTMLVMGuestOS } } # Concatenate header with the HTML Table $htmlTableWithHeader = "<h2>$title</h2>$htmlTable" # ConvertTo-Html converts the output into HTML fragments. We need to remove some unnecessary tags and style it. $htmlTableWithHeader = $htmlTableWithHeader -replace '<col.*?>' -replace '</col.*?>' -replace '<!DOCTYPE html><html><head></head><body>' -replace '</body></html>' -replace '<tbody>' -replace '</tbody>' $htmlOutput += $htmlTableWithHeader } Write-Output $htmlOutput } } function ProduceFinalHtml { $PageTitle = "<h3 align='center'>Test App Service executed at {0} from {1}</h3>" -f ((Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString()), $env:COMPUTERNAME $TestFileName = "{0}-Test-AzureStackAppServiceRP_{1}.html" -f $env:COMPUTERNAME, $currentDate $CSSstyle = @" <style> body { font-family: Arial, sans-serif; /* Set default font to Arial */} table { border-collapse: collapse; border: 2px solid black; } th, td { border-left: 1px solid black; border-bottom: 1px solid black; padding-left: 5px; padding-right: 5px; padding-top: 1px; padding-bottom:1px;} th { background-color: lightgray; border: 2px solid black; min-width: 50px;} h2 { font-size: 15px; /* Same as h2 */ font-weight: bold; /* Same as h2 */} .Alert { background-color: red !important; } /* Change background color for rows where Status is Stopped */ .Warning { background-color: yellow !important; } .OK { background-color: green !important; } /* Change background color for rows where Status is Running */ .Relevant { background-color: gray !important; } // used to mark rows with relevant info /* Styling the summary element to resemble the H1 tag */ summary { font-size: 17px; /* Size similar to H1 */ font-weight: bold; cursor: pointer; padding: 10px; border: 1px solid #ccc; margin: 10px 0; background-color: #f1f1f1; list-style: none; /* Remove default arrow */ } /* Custom arrow for the summary element */ summary::marker { content: '\25B6 '; /* Arrow for closed state */ } /* Change the arrow icon when the collapsible is open */ details[open] summary::marker { content: '\25BC '; /* Arrow for open state */ } /* Styling the collapsible content */ details { margin-bottom: 10px; border: 1px solid #ccc; padding: 10px; font-size: 17px; } /* Styling for the tag when content is "Alert" */ .regionstatus[data-status="Alert"] { color: red; /* Red color for "Alert" */ display: inline; background-color: #b6b6b6; padding: 11px; vertical-align: bottom; } /* Styling for the tag when content is "OK" */ .regionstatus[data-status="OK"] { color: green; /* Green color for "OK" */ display: inline; background-color: #b6b6b6; padding: 11px; vertical-align: bottom; } /* Styling for the tag when content is "Warning" */ .regionstatus[data-status="Warning"] { color: yellow; /* Green color for "Warning" */ display: inline; background-color: #b6b6b6; padding: 11px; vertical-align: bottom; } </style> "@ $Head = ConvertTo-Html -Title "Test title" -Head $CSSstyle # We need to remove some unnecessary tags added by the convertto-html. $Head = $Head -replace '<col.*?>' -replace '</col.*?>' -replace '<!DOCTYPE html><html><head></head><body>' -replace '</body></html>' -replace '<tbody>' -replace '</tbody>' #Get All segments toghether and output the file $Head, $PageTitle, $region_AppServiceConfig, $region_AppServiceOverview, $region_AppServiceCriticalEvents, $region_Certificates, $region_TestSQL, $region_TestFileshare, $region_CheckGuestVMs | Out-File -FilePath (Join-Path -Path $script:logDirectory -ChildPath $TestFileName) -Encoding UTF8 -Force } #### Funtions to Process HTML specific output for regions ###### function ProcessHTMLAppServiceRPOverview { # Identify rows where Status is Stopped and apply CSS class $rows = $htmlTable -split '<tr>' # Identify the header row (5 row) and extract the column headers $headers = $rows[3] -split '</th>' # Find the index of the desired columns dynamically $statusColumnIndex = -1 $AvailableWorkersColumnIndex = -1 $RunningModeColumnIndex = -1 $serverStateColumnIndex = -1 $PendingOpColumnIndex = -1 for ($i = 0; $i -lt $headers.Length; $i++) { #match column Status if ($headers[$i] -match 'Status') { $statusColumnIndex = $i } #match column ServerState if ($headers[$i] -match 'ServerState') { $serverStateColumnIndex = $i } #match column CPU if ($headers[$i] -match 'AvailableWorkerCount') { $AvailableWorkersColumnIndex = $i } #match column CPU if ($headers[$i] -match 'RunningMode') { $RunningModeColumnIndex = $i } if ($headers[$i] -match 'OperationName') { $PendingOpColumnIndex = $i } } #### Analyse and apply CSS classes according to rules # Iterate over each row (skipping the first element which contains table headers) for ($i = 4; $i -lt $rows.Length; $i++) { $row = $rows[$i] # Split the row into columns $columns = $row -split '</td>' # Check if the "Status" column contains "Stopped" if (($columns[$statusColumnIndex] -match 'Stopped') -or ($columns[$statusColumnIndex] -match 'Not ready') -and ($statusColumnIndex -ne -1)) { ##cell change $columns[$statusColumnIndex] = $columns[$statusColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } # Check if the "Status" column contains "Ready" if (($columns[$statusColumnIndex] -match 'Ready') -and ($statusColumnIndex -ne -1)) { ##cell change $columns[$statusColumnIndex] = $columns[$statusColumnIndex] -replace "<td>", "<td class='OK'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row # $rows[$i] = "<tr class='OK'>$($rows[$i])</tr>" } # Check if the "ServerState" column contains "Offline" #if (($columns[$serverStateColumnIndex] -match 'Offline') -or ($columns[$serverStateColumnIndex] -match 'Installing') -and ($serverStateColumnIndex -ne -1)) { if (($columns[$serverStateColumnIndex] -match 'Offline') -or ($columns[$serverStateColumnIndex] -match 'NotReady') -or ($columns[$serverStateColumnIndex] -match 'Installing') -and ($serverStateColumnIndex -ne -1)) { ##cell change $columns[$serverStateColumnIndex] = $columns[$serverStateColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } # Check if the "AvailableWorkerCount" column $AvailableWorkersvalue = $columns[$AvailableWorkersColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td> if (($AvailableWorkersvalue -eq 0) -and ($AvailableWorkersColumnIndex -ne -1)) { #change cells $columns[$AvailableWorkersColumnIndex] = $columns[$AvailableWorkersColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } # Check if the "Stopped" column if (($columns[$RunningModeColumnIndex] -match "Stopped") -and ($RunningModeColumnIndex -ne -1)) { ##cell change $columns[$RunningModeColumnIndex] = $columns[$RunningModeColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } # Check if the "RunningMode" column if (($columns[$RunningModeColumnIndex] -match "Running") -and ($RunningModeColumnIndex -ne -1)) { ##cell change $columns[$RunningModeColumnIndex] = $columns[$RunningModeColumnIndex] -replace "<td>", "<td class='OK'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='RunningModeStopped'>$($rows[$i])</tr>" } # Check Pending operations if (($columns[$PendingOpColumnIndex] -match "Repair") -and ($PendingOpColumnIndex -ne -1)) { ##cell change $columns[$PendingOpColumnIndex] = $columns[$PendingOpColumnIndex] -replace "<td>", "<td class='Warning'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } # Join the rows back into a single HTML table string $htmlTable = ($rows -join '</tr>') + "<br>" return $htmlTable } function ProcessHTMLAppServiceEvents { # Identify rows where Status is Stopped and apply CSS class $rows = "" $rows = $htmlTable -split '<tr>' # Identify the header row (row 3) and extract the column headers $headers = "" $headers = $rows[3] -split '</th>' # Find the index of the desired columns dynamically $LevelColumnIndex = -1 for ($i = 0; $i -lt $headers.Length; $i++) { #match column Status if ($headers[$i] -match 'TraceLevel') { $LevelColumnIndex = $i } } #### Analyse and apply CSS classes according to rules # Iterate over each row (skipping the first element which contains table headers) for ($i = 4; $i -lt $rows.Length; $i++) { $row = $rows[$i] # Split the row into columns $columns = $row -split '</td>' ### used to use the value inside the cell without <td> $LevelStringValue = $columns[$LevelColumnIndex] -replace '<td>', '' if ($columns[$LevelColumnIndex]) { # Check if the "Level" column if ([int]$LevelStringValue -lt 3 -and [int]$LevelStringValue -gt 1 ) { ##cell change $columns[$LevelColumnIndex] = $columns[$LevelColumnIndex] -replace "<td>", "<td class='Warning'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } if ([int]$LevelStringValue -lt 2) { ##cell change $columns[$LevelColumnIndex] = $columns[$LevelColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } } # Join the rows back into a single HTML table string $htmlTable = ($rows -join '</tr>') + "<br>" return $htmlTable } function ProcessHTMLVMGuestOS { # Identify rows where Status is Stopped and apply CSS class $rows = "" $rows = $htmlTable -split '<tr>' # Identify the header row (row 3) and extract the column headers $headers = "" $headers = $rows[3] -split '</th>' # Find the index of the desired columns dynamically $FreeSpaceColumnIndex = -1 $CPUColumnIndex = -1 $CommitUseColumnIndex = -1 $ServicesColumnIndex = -1 $UpdatesColumnIndex = -1 $FreeSpacevalue = 0 $CPUvalue = 0 $CommitUsevalue = 0 $ServicesStringValue = 0 $UpdatesStringValue = 0 for ($i = 0; $i -lt $headers.Length; $i++) { #match column Status if ($headers[$i] -match 'Free Space') { $FreeSpaceColumnIndex = $i } if ($headers[$i] -match 'CPU') { $CPUColumnIndex = $i } if ($headers[$i] -match 'CommitUse') { $CommitUseColumnIndex = $i } if ($headers[$i] -match 'Status') { $ServicesColumnIndex = $i } if ($headers[$i] -match 'Result') { $UpdatesColumnIndex = $i } } #debug Write-Host $FreeSpaceColumnIndex ' index free space' #### Analyse and apply CSS classes according to rules # Iterate over each row (skipping the first element which contains table headers) for ($i = 4; $i -lt $rows.Length; $i++) { $row = $rows[$i] # Split the row into columns $columns = $row -split '</td>' # Check if the "Free Space (GB)" column contains value below 10GB $FreeSpaceStringValue = $columns[$FreeSpaceColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td> if ([double]::TryParse($FreeSpaceStringValue, [ref]$FreeSpacevalue)) { if ([int]($FreeSpacevalue) -cle 10 -and ($FreeSpaceColumnIndex -ne -1)) { ##cell change $columns[$FreeSpaceColumnIndex] = $columns[$FreeSpaceColumnIndex] -replace "<td>", "<td class='Warning'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } # Check if CPU % column contains value above 90 $CPUStringValue = $columns[$CPUColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td> if ([double]::TryParse($CPUStringValue, [ref]$CPUvalue)) { #debug Write-Host $FreeSpacevalue ' value free space Successfully parsed' if ([int]($CPUvalue) -gt 90 -and ($CPUColumnIndex -ne -1)) { #debug Write-Host $FreeSpacevalue ' value free space int' ##cell change $columns[$CPUColumnIndex] = $columns[$CPUColumnIndex] -replace "<td>", "<td class='Warning'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } # Check if commit in use is above 70% $CommitUseStringValue = $columns[$CommitUseColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td> if ([double]::TryParse($CommitUseStringValue, [ref]$CommitUsevalue)) { if ([int]($CommitUsevalue) -gt 70 -and ($CommitUseColumnIndex -ne -1)) { ##cell change $columns[$CommitUseColumnIndex] = $columns[$CommitUseColumnIndex] -replace "<td>", "<td class='Warning'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row $rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } # Check Services are Running and style $ServicesStringValue = $columns[$ServicesColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td> if ($ServicesStringValue) { if ($ServicesStringValue -eq "Running" -and $ServicesColumnIndex -ne -1) { ##cell change $columns[$ServicesColumnIndex] = $columns[$ServicesColumnIndex] -replace "<td>", "<td class='OK'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } if ($ServicesStringValue -ne "Running" -and $ServicesColumnIndex -ne -1) { ##cell change $columns[$ServicesColumnIndex] = $columns[$ServicesColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } # Check WUpdates failed $UpdatesStringValue = $columns[$UpdatesColumnIndex] -replace '<td>', '' ### used to use the value inside the cell without <td> if ($UpdatesStringValue) { if ($UpdatesStringValue -eq "Succeeded" -and $UpdatesColumnIndex -ne -1) { ##cell change $columns[$UpdatesColumnIndex] = $columns[$UpdatesColumnIndex] -replace "<td>", "<td class='OK'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } if ($UpdatesStringValue -eq "Failed" -and $UpdatesColumnIndex -ne -1) { ##cell change $columns[$UpdatesColumnIndex] = $columns[$UpdatesColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } } # Join the rows back into a single HTML table string $htmlTable = ($rows -join '</tr>') + "<br>" return $htmlTable } function ProcessHTMLCertificates { # Identify rows where Status is Stopped and apply CSS class $rows = "" $rows = $htmlTable -split '<tr>' # Identify the header row (row 3) and extract the column headers $headers = "" $headers = $rows[3] -split '</th>' # Find the index of the desired columns dynamically $notAfterColumnIndex = -1 $notAfterStringValue = 0 $StatusColumnIndex = -1 for ($i = 0; $i -lt $headers.Length; $i++) { #match column Status if ($headers[$i] -imatch 'notAfter') { $notAfterColumnIndex = $i } if ($headers[$i] -imatch 'Status') { $StatusColumnIndex = $i } } #### Analyse and apply CSS classes according to rules # Iterate over each row (skipping the first 4 elements which contains table headers) for ($i = 4; $i -lt $rows.Length; $i++) { $row = $rows[$i] # Split the row into columns $columns = $row -split '</td>' ### used to use the value inside the cell without <td> $notAfterStringValue = $columns[$notAfterColumnIndex] -replace '<td>', '' # Check if $notAfterStringValue is a valid DateTime $nullDate = [datetime]::MinValue $validDate = [datetime]::TryParse($notAfterStringValue, [ref]$nullDate) if ($validDate) { # Parse the date string into a DateTime object in UTC $targetDate = [datetime]::Parse($notAfterStringValue).ToUniversalTime() # Get the current date and time in UTC $currentDate = [datetime]::UtcNow # Calculate the difference using New-TimeSpan $timeDifference = New-TimeSpan -Start $currentDate -End $targetDate # Get the number of days left $daysLeft = $timeDifference.Days # Check if the date is close to the end of the month (e.g., within the last 30 days) if ($daysLeft -le 60) { $columns[$notAfterColumnIndex] = $columns[$notAfterColumnIndex] -replace "<td>", "<td class='Warning'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } if ($daysLeft -le 30) { $columns[$notAfterColumnIndex] = $columns[$notAfterColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } else { $columns[$notAfterColumnIndex] = $columns[$notAfterColumnIndex] -replace "<td>", "<td class='OK'>" $columns[$StatusColumnIndex] = $columns[$StatusColumnIndex] -replace "<td>", "<td class='OK'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } } # Join the rows back into a single HTML table string $htmlTable = ($rows -join '</tr>') + "<br>" return $htmlTable } function ProcessHTMLSQLService { # Identify rows where Status is Stopped and apply CSS class $rows = "" $rows = $htmlTable -split '<tr>' # Identify the header row (row 3) and extract the column headers $headers = "" $headers = $rows[3] -split '</th>' ## Find the index of the desired columns dynamically $StateColumnIndex = -1 $GrowthColumnIndex = -1 $StateStringValue = 0 $GrowthStringValue = 0 $PercentFromMaxSizeColumnIndex = -1 for ($i = 0; $i -lt $headers.Length; $i++) { #match column Status if ($headers[$i] -imatch 'State') { $StateColumnIndex = $i } if ($headers[$i] -imatch 'is_percent_growth') { $GrowthColumnIndex = $i } if ($headers[$i] -imatch 'PercentFromMaxSize') { $PercentFromMaxSizeColumnIndex = $i } } #### Analyse and apply CSS classes according to rules # Iterate over each row (skipping the first 4 elements which contains table headers) for ($i = 4; $i -lt $rows.Length; $i++) { $row = $rows[$i] # Split the row into columns $columns = $row -split '</td>' ### used to use the value inside the cell without <td> $StateStringValue = $columns[$StateColumnIndex] -replace '<td>', '' if ($columns[$StateColumnIndex]) { if ($StateStringValue -eq "ONLINE") { $columns[$StateColumnIndex] = $columns[$StateColumnIndex] -replace "<td>", "<td class='OK'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } elseif ($StateStringValue -ne "ONLINE") { $columns[$StateColumnIndex] = $columns[$StateColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } ### used to use the value inside the cell without <td> if ($columns[$GrowthColumnIndex]) { $GrowthStringValue = $columns[$GrowthColumnIndex] -replace '<td>', '' if ($GrowthStringValue -eq "False") { $columns[$GrowthColumnIndex] = $columns[$GrowthColumnIndex] -replace "<td>", "<td class='OK'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } else { $columns[$GrowthColumnIndex] = $columns[$GrowthColumnIndex] -replace "<td>", "<td class='Warning'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } ### used to use the value inside the cell without <td> if ($columns[$PercentFromMaxSizeColumnIndex]) { $PercentFromMaxSizeStringValue = $columns[$PercentFromMaxSizeColumnIndex] -replace '<td>', '' if ($PercentFromMaxSizeStringValue -lt 80) { $columns[$PercentFromMaxSizeColumnIndex] = $columns[$PercentFromMaxSizeColumnIndex] -replace "<td>", "<td class='OK'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } elseif ($PercentFromMaxSizeStringValue -gt 80 -and $PercentFromMaxSizeStringValue -lt 90) { $columns[$PercentFromMaxSizeColumnIndex] = $columns[$PercentFromMaxSizeColumnIndex] -replace "<td>", "<td class='Warning'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } elseif ($PercentFromMaxSizeStringValue -gt 90 ) { $columns[$PercentFromMaxSizeColumnIndex] = $columns[$PercentFromMaxSizeColumnIndex] -replace "<td>", "<td class='Alert'>" $rows[$i] = $columns -join '</td>' # Add the CSS class to the row #$rows[$i] = "<tr class='Relevant'>$($rows[$i])</tr>" } } } # Join the rows back into a single HTML table string $htmlTable = ($rows -join '</tr>') + "<br>" return $htmlTable } ########### # Main Logic ######## Start transcript Output to TXT ####### Clear-host ##extend the UI size $host.UI.RawUI.BufferSize = new-object System.Management.Automation.Host.Size(600, 0) New-Item -Path $script:logDirectory -ItemType Directory -Force -ErrorAction Ignore | Out-Null $transcriptFileName = "{0}-Test-AzureStackAppServiceRP_{1}.txt" -f $env:COMPUTERNAME, $currentDate Start-Transcript -Path (Join-Path -Path $script:logDirectory -ChildPath $transcriptFileName) | Write-Host -ForegroundColor Yellow #### MAIN ##### "[{0}] - Collecting Config Global Information" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $region_AppServiceConfig = Get-AzsSupportAppServiceConfigOutput -ExportHTML "[{0}] - Completed Config Global information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green if (($PSBoundParameters.ContainsKey("ExportGlobalConfigFile")) -or ($ExportGlobalConfigFile -eq $true)) { Get-AzsSupportAppServiceConfigRedactandExport } else { Write-Host "Not Exporting Global Config to file" } "[{0}] - Checking App Service RP" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $region_AppServiceOverview = Get-AzsSupportAppServiceRPOverview -ExportHTML "[{0}] - Completed Checking App Service RP" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Checking App Service Events" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $region_AppServiceCriticalEvents = Get-AzsSupportAppServiceCriticalEvent -ExportHTML "[{0}] - Completed Checking App Service Events" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green if (($PSBoundParameters.ContainsKey("ExportEventsFile")) -or ($ExportEventsFile -eq $true)) { $AppServiceEvents | ConvertTo-Json | Out-File (Join-Path -Path $script:logDirectory -ChildPath "AppServiceEvent.json") } if (($PSBoundParameters.ContainsKey("WebAppName")) -and ($WebAppName -ne "")) { "[{0}] - Checking Web App $WebAppName Workers" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan Get-AzsSupportWebAppDetails($WebAppName) "[{0}] - Checking Web App Workers" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green } "[{0}] - Testing SQL Service" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $region_TestSQL = Test-AzsSupportAppServiceSQL -ExportHTML "[{0}] - Completed Testing SQL Service" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Testing FileShare" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $region_TestFileshare = Test-AzsSupportAppServiceFileShare -ExportHTML "[{0}] - Completed Testing FileShare" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Testing Certificates" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $region_Certificates = Test-AzsSupportAppServiceCertificates -ExportHTML "[{0}] - Completed Testing Certificates" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Checking Guest VMs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $region_CheckGuestVMs = Test-AzsSupportVMguestOS -ExportHTML # making sure that all sessions were closed $null = get-pssession | Where-Object Name -like "WinRM*" | remove-pssession "[{0}] - Completed Checking Guest VMs" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green "[{0}] - Processing HTML export" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan ProduceFinalHtml "[{0}] - Completed Processing HTML export" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green $filePath = Join-Path -Path $script:logDirectory -ChildPath $TestFileName Write-Host "" Write-Host "Location: $filePath" # Remove the global variables if ($script:possibleWorkersNames) { Remove-Variable -Name possibleWorkersNames -Scope Global } #### Stop output to TXT Stop-Transcript | Write-Host -ForegroundColor Green } #region Exported Functions ##### Get App Service Config And export to TXT and Json ##### function Get-AzsSupportAppServiceConfigOutput { <# .SYNOPSIS This function retrieves the App Service configuration and exports it to a formatted table. .DESCRIPTION This function retrieves the App Service configuration and exports it to a formatted table. .EXAMPLE Get-AzsSupportAppServiceConfigOutput This example retrieves the most relevant App Service configuration. #> [CmdletBinding()] param ( [switch]$ExportHTML ) ## other method to retrieve the the config - it need to be worked the output from db before using it if ($script:appServiceConfigGlobal -eq "") { Write-host -ForegroundColor Red "Collecting Config Global Failed" Write-host -ForegroundColor Yellow "Trying via SQL DB" $script:hostingStr = Get-AppServiceConnectionString -Type Hosting $sqlQuery = @' SELECT TOP (10) [ConfigurationKey], [ConfigurationValue], [RowVersion] FROM [appservice_hosting].[runtime].[HostingConfigurations] '@ $sqlresult = Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQuery $script:appServiceConfigGlobal = $sqlresult } $AppServiceConfigList = @{ CloudId = $script:appServiceConfigGlobal.CloudId AdminExtensionUri = $script:appServiceConfigGlobal.AdminExtensionUri ApplicationClientId = $script:appServiceConfigGlobal.ApplicationClientId AppServicePortalUri = $script:appServiceConfigGlobal.AppServicePortalUri CurrentControllerVersion = $script:appServiceConfigGlobal.CurrentControllerVersion ControllersSubnetPrefix = $script:appServiceConfigGlobal.ControllersSubnetPrefix DWASFilesFolderQuotaInGb = $script:appServiceConfigGlobal.DWASFilesFolderQuotaInGb InfrastructureResourceGroup = $script:appServiceConfigGlobal.InfrastructureResourceGroup LogScavengerParameters = $script:appServiceConfigGlobal.LogScavengerParameters MaxAllowedContentLength = $script:appServiceConfigGlobal.MaxAllowedContentLength MaxAllowedFiles = $script:appServiceConfigGlobal.MaxAllowedFiles MaxAllowedFolders = $script:appServiceConfigGlobal.MaxAllowedFolders MicrosoftUpdateEnabled = $script:appServiceConfigGlobal.MicrosoftUpdateEnabled PlatformVersion = $script:appServiceConfigGlobal.PlatformVersion WorkerSubnetName = $script:appServiceConfigGlobal.WorkerSubnetName RunWindowsUpdate = $script:appServiceConfigGlobal.RunWindowsUpdate DIsableScaleSetSync = $script:appServiceConfigGlobal.DIsableScaleSetSync FileShare = $script:fileshare SQLIP = $script:sqlserverip } # Create a custom object $configObject = New-Object PSObject -Property $AppServiceConfigList # Add the object to an array $configArray = @($configObject) $verticalTable = @() foreach ($config in $configArray) { foreach ($property in $config.psobject.properties) { $verticalTable += [PSCustomObject]@{ Config = $property.Name Value = $property.Value } } } $AppServiceConfigListTable = @{ Title = "" Data = $verticalTable } Write-Host "App Service Configuration:" -ForegroundColor Green Write-Host " " Write-Host "CloudId: "$script:appServiceConfigGlobal.CloudId Write-Host "AdminExtensionUri: "$script:appServiceConfigGlobal.AdminExtensionUri Write-Host "ApplicationClientId: "$script:appServiceConfigGlobal.ApplicationClientId Write-host "AppServicePortalUri: "$script:appServiceConfigGlobal.AppServicePortalUri Write-host "CurrentControllerVersion: "$script:appServiceConfigGlobal.CurrentControllerVersion Write-host "ControllersSubnetPrefix: "$script:appServiceConfigGlobal.ControllersSubnetPrefix Write-host "DWASFilesFolderQuotaInGb: "$script:appServiceConfigGlobal.DWASFilesFolderQuotaInGb Write-host "InfrastructureResourceGroup:"$script:appServiceConfigGlobal.InfrastructureResourceGroup Write-host "LogScavengerParameters: "$script:appServiceConfigGlobal.LogScavengerParameters Write-host "MaxAllowedContentLength: "$script:appServiceConfigGlobal.MaxAllowedContentLength Write-host "MaxAllowedFiles: "$script:appServiceConfigGlobal.MaxAllowedFiles Write-host "MaxAllowedFolders: "$script:appServiceConfigGlobal.MaxAllowedFolders Write-host "MicrosoftUpdateEnabled: "$script:appServiceConfigGlobal.MicrosoftUpdateEnabled Write-host "PlatformVersion: "$script:appServiceConfigGlobal.PlatformVersion Write-host "RunWindowsUpdate: "$script:appServiceConfigGlobal.RunWindowsUpdate Write-host "DIsableScaleSetSync: "$script:appServiceConfigGlobal.DIsableScaleSetSync Write-host "FileShare: "$script:fileshare Write-host "SQL IP: "$script:sqlserverip Write-host "WorkerSubnetName: "$script:appServiceConfigGlobal.WorkerSubnetName Write-Host " " ### convert to html all tables if ($ExportHTML) { $region_AppServiceConfig = "<summary>App Service RP Config Info</summary>" + ($AppServiceConfigListTable | ConvertTo-HtmlTableWithHeaderAndTitle) $region_AppServiceConfig } } function Get-AzsSupportAppServiceConfigRedactandExport { <# .SYNOPSIS This function redacts sensitive information from the App Service configuration and exports it to JSON and TXT files. .DESCRIPTION This function redacts sensitive information from the App Service configuration and exports it to JSON and TXT files. .EXAMPLE Get-AzsSupportAppServiceConfigRedactandExport This example redacts sensitive information from the App Service configuration and exports it to JSON and TXT files. #> $AppServiceConfigGlobalRedacted = $script:appServiceConfigGlobal "[{0}] - Redacting secrets on Config Global information" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Cyan $AppServiceConfigGlobalRedacted.BitbucketClientId = "[REDACTED]" $AppServiceConfigGlobalRedacted.BitbucketClientSecret = "[REDACTED]" $AppServiceConfigGlobalRedacted.BitbucketNextClientId = "[REDACTED]" $AppServiceConfigGlobalRedacted.BitbucketNextClientSecret = "[REDACTED]" $AppServiceConfigGlobalRedacted.BitbucketProdClientId = "[REDACTED]" $AppServiceConfigGlobalRedacted.BitbucketProdClientSecret = "[REDACTED]" $AppServiceConfigGlobalRedacted.BitbucketStageClientId = "[REDACTED]" $AppServiceConfigGlobalRedacted.BitbucketStageClientSecret = "[REDACTED]" $AppServiceConfigGlobalRedacted.CertificatePassword = "[REDACTED]" $AppServiceConfigGlobalRedacted.GitHubClientId = "[REDACTED]" $AppServiceConfigGlobalRedacted.GitHubClientSecret = "[REDACTED]" $AppServiceConfigGlobalRedacted.InfrastructureClientCertificatePassword = "[REDACTED]" $AppServiceConfigGlobalRedacted.InfrastructureClientId = "[REDACTED]" $AppServiceConfigGlobalRedacted.ManagementServerCertificatePassword = "[REDACTED]" $AppServiceConfigGlobalRedacted.PublisherServerCertificatePassword = "[REDACTED]" $AppServiceConfigGlobalRedacted.TokenRequestCertificatePassword = "[REDACTED]" $AppServiceConfigGlobalRedacted.UsageStorageAccountConnString = "[REDACTED]" $AppServiceConfigGlobalRedacted | ConvertTo-Json | Out-File (Join-Path -Path $script:logDirectory -ChildPath "AppServiceConfigGlobal.json") Write-host (Join-Path -Path $script:logDirectory -ChildPath "AppServiceConfigGlobal.json") -ForegroundColor Yellow $AppServiceConfigGlobalRedacted | Format-List | Out-String | Out-File (Join-Path -Path $script:logDirectory -ChildPath "AppServiceConfigGlobal.txt") Write-host (Join-Path -Path $script:logDirectory -ChildPath "AppServiceConfigGlobal.txt") -ForegroundColor Yellow "[{0}] - Completed Redacting Config Global information collection" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss").ToString() | Write-Host -ForegroundColor Green } ####### Check App Service Events ############### function Get-AzsSupportAppServiceCriticalEvent { <# .SYNOPSIS This function retrieves App Service events from the last day and checks for critical errors. .DESCRIPTION This function retrieves App Service events from the last day and checks for critical errors. .EXAMPLE Get-AzsSupportAppServiceCriticalEvent This example retrieves App Service events from the last day and checks for critical errors. #> [CmdletBinding()] param ( [switch]$ExportHTML ) ## collect app service evnts last day $AppServiceEvents = Get-AppServiceEvent -StartTime (get-date).AddDays(-1) -EndTime (get-date) $AppServiceEventsList = @() ### output object if lelvel 2 errors found if (($AppServiceEvents | Where-Object { ($_.TraceLevel -cle 2) }).count -eq 0) { write-host "No Errors found from the last day" -ForegroundColor Green $regionstatus = "OK" $AppServiceEventsListTable = @{ title = "No Errors found from the last day" } } else { Write-Warning 'Found App Service Error Events from last day' $AppServiceEventsList = $AppServiceEvents | Select-Object TimeStamp, ServerName, TraceLevel, Message | Where-Object { ($_.TraceLevel -cle 2) } | Select-Object -First 100 if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } # Check if there are any events with TraceLevel equal to 1 if ($AppServiceEventsList | Where-Object { $_.TraceLevel -eq 1 }) { # If found, update regionstatus to ALERT $regionstatus = "Alert" } $AppServiceEventsList | Format-Table | Out-String | Write-Host -ForegroundColor Yellow $AppServiceEventsListTable = @{ title = "App Service Error Events from last day" data = $AppServiceEventsList } } ### convert to html all Tables if ($ExportHTML) { $region_AppServiceCriticalEvents = "<br><details><summary>App Service Events - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($AppServiceEventsListTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "AppServiceEvents") + "</details>" $region_AppServiceCriticalEvents } } ######### Check App Service RP ################# function Get-AzsSupportAppServiceRPOverview { <# .SYNOPSIS This function retrieves an overview of the App Service RP, including web apps, app service plans, SKU worker tiers, worker tiers, and instances. .DESCRIPTION This function retrieves an overview of the App Service RP, including web apps, app service plans, SKU worker tiers, worker tiers, and instances. .EXAMPLE Get-AzsSupportAppServiceRPOverview This example retrieves an overview of the App Service RP. #> [CmdletBinding()] param ( [switch]$ExportHTML ) Write-Host "Check Web Apps" -ForegroundColor Green $WebAppList = $script:SiteManager.Sites | Select-Object SiteID, SiteName, ComputeMode, SKU, SubscriptionName, RunningMode, AlwaysOn, Kind, @{Name = "RunningWorkers"; Expression = { $_.RunningWorkers.WorkerName } }, @{Name = "ServerFarm"; Expression = { $_.ServerFarm.ServerFarmName } }, @{Name = "VirtualFarm"; Expression = { $_.VirtualFarm.VirtualFarmName } }, @{Name = "HostNames"; Expression = { $_.HostNames -join ', ' } } | Where-Object { $_.HostNames -notlike 'mawscanary*' } | Sort-Object SubscriptionName Write-Verbose ($WebAppList | Format-Table * | Out-String) $WebAppListStopped = @($script:SiteManager.Sites | Select-Object SiteID, SiteName, ComputeMode, SKU, SubscriptionName, RunningMode, AlwaysOn, Kind, @{Name = "RunningWorkers"; Expression = { $_.RunningWorkers.WorkerName } }, @{Name = "ServerFarm"; Expression = { $_.ServerFarm.ServerFarmName } }, @{Name = "VirtualFarm"; Expression = { $_.VirtualFarm.VirtualFarmName } }, @{Name = "HostNames"; Expression = { $_.HostNames -join ', ' } } | Where-Object RunningMode -match "Stopped" | Where-Object { $_.HostNames -notlike 'mawscanary*' } | Sort-Object SubscriptionName ) if ($WebAppListStopped.count -gt 0) { $message = "Found " + $WebAppListStopped.count + " Web App(s) in Stopped State" Write-Warning $message if ($regionstatus -ne "Alert") { if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } } $WebAppListStopped | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow } $WebAppListTable = @{ Title = "Web Apps" Data = $WebAppList } Write-Host "Check AppService Plans" -ForegroundColor Green $AppServicePlanList = Get-AppServiceServerFarm -force | Select-Object ServerFarmId, Name, Status, WebSpace, SubscriptionId, SKU, NumberOfWorkers, CurrentNumberOfWorkers, WorkerSize, ComputeMode | Sort-Object SubscriptionId Write-Verbose ($AppServicePlanList | Format-Table * | Out-String) $AppServicePlanListTable = @{ Title = "AppService Plans" Data = $AppServicePlanList } Write-Host "Check SKU Worker Tiers" -ForegroundColor Green $SKUList = $script:SiteManager.SkuWorkerTiers | Select-Object SkuId, SkuName, Family, WorkerTierId, WorkerTierName, StockSize, @{Name = "ComputeVMSize"; Expression = { $_.WorkerTier.ComputeVmSize } }, @{Name = "ComputeScaleSetName"; Expression = { $_.WorkerTier.ComputeScaleSetName } }, @{Name = "ComputeMode"; Expression = { $_.WorkerTier.ComputeMode } } Write-Verbose ($SKUList | Format-Table * | Out-String) $SKUListTable = @{ Title = "SKU Worker Tiers" Data = $SKUList } Write-Host "Check Worker Tiers" -ForegroundColor Green $WorkerTierList = $script:SiteManager.WorkerTiers | Where-Object ComputeType -eq "Pico" | Select-Object Name, ComputeScaleSetName, ComputeMode, ComputeInstances, UsedWorkerCount, RegisteredWorkerCount, ReadyWorkerCount, AvailableWorkerCount, Enabled, NumberOfCores, MemorySize, ComputeVmSize, ComputeScaleSetProvisioningState, StampCapacityAlias, Id, WorkerSize, ComputeImageReference Write-Verbose ($WorkerTierList | Format-Table * | Out-String) $WorkerTierListEmpty = @($script:SiteManager.WorkerTiers | Where-Object ComputeType -eq "Pico" | Where-Object AvailableWorkerCount -eq 0 | Select-Object Name, ComputeScaleSetName, ComputeMode, ComputeInstances, UsedWorkerCount, RegisteredWorkerCount, ReadyWorkerCount, AvailableWorkerCount, Enabled, NumberOfCores, MemorySize, ComputeVmSize, ComputeScaleSetProvisioningState, StampCapacityAlias, Id, WorkerSize, ComputeImageReference ) if ($WorkerTierListEmpty.count -gt 0) { $message = "Found " + $WorkerTierListEmpty.count + " Worker Tiers with no available workers" Write-Warning $message $regionstatus = "Alert" $WorkerTierListEmpty | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow } #Get-AppServiceWorkerTier | select WorkerSize, Name, Description, ComputeScaleSetName, ComputeInstances, ComputeMode, UsedWorkerCount, ReadyWorkerCount, AvailableWorkerCount, Id | FT -Wrap -AutoSize $WorkerTierListTable = @{ Title = "Worker Tiers List" Data = $WorkerTierList } Write-Host "Check All Instances " -ForegroundColor Green $Instances = Get-AppServiceServer #$Instances | FT * $InstanceDetails = @() foreach ($server in $Instances) { $InstanceInfo = [PSCustomObject]@{ Name = $server.Name Status = $server.Status Role = $server.Role ServerState = $server.ServerState StatusMessage = $server.StatusMessage FeedUrl = $server.FeedUrl ReadyForLoadBalancing = $server.ReadyForLoadBalancing IsDraining = $server.IsDraining LastError = $server.LastError SKU = "" AvailableSinceTimeStamp = "" WebApps = "" SiteCount = "" IsDedicatedWorker = "" ASP = "" MachineName = "" LastHeartBeat = "" IsCurrentOwner = "" CurrentOperations = $server.CurrentOperations } $InstanceDetails += $InstanceInfo } $workers = @() $workers = $script:SiteManager.Workers | Select-Object InstanceName, MachineName, IsDedicatedWorker, SiteCount, @{Name = "ASP"; Expression = { $_.VirtualFarm.VirtualFarmName } }, @{Name = "ASP-NumberOfWorkers"; Expression = { $_.VirtualFarm.TargetNumberOfWorkers } }, @{Name = "WebApps"; Expression = { $_.RunningSites.SiteName } }, @{Name = "SKU"; Expression = { $_.RunningSites.SKU | Select-Object -Unique } }, AvailableSinceTimeStamp foreach ($worker in $workers) { foreach ($server in $InstanceDetails) { if ($server.Name -eq $worker.InstanceName) { $server.MachineName = $worker.MachineName $server.ASP = $worker.ASP $server.IsDedicatedWorker = $worker.IsDedicatedWorker $server.SiteCount = $worker.SiteCount $server.WebApps = $worker.WebApps $server.SKU = $worker.SKU $server.AvailableSinceTimeStamp = $worker.AvailableSinceTimeStamp } } } $Controllers = @() $Controllers = $script:SiteManager.Controllers | Select-Object MachineName, IsCurrentOwner, LastHeartBeat foreach ($Controller in $Controllers) { foreach ($server in $InstanceDetails) { if ($server.Name -eq $Controller.MachineName) { $server.IsCurrentOwner = $Controller.IsCurrentOwner $server.LastHeartBeat = $Controller.LastHeartBeat } } } ## full Table output $InstanceDetailsList = $InstanceDetails | Select-Object Name, MachineName, Status, Role, ServerState, IsCurrentOwner, IsDedicatedWorker, SiteCount, ReadyForLoadBalancing, IsDraining, LastHeartBeat, AvailableSinceTimeStamp, CurrentOperations, SKU, WebApps, ASP, @{Name = "LastMessages"; Expression = { $_.StatusMessage } }, LastError, FeedUrl | Sort-Object Role Write-Verbose ($InstanceDetailsList | Format-Table * | Out-String) $InstanceDetailsListTable = @{ Title = "All Instances List" Data = $InstanceDetailsList } ## Not Ready Table output $InstanceDetailsNotReady = @($InstanceDetails | Where-Object ServerState -NE "Ready" | Select-Object Name, MachineName, Status, Role, ServerState, IsCurrentOwner, IsDraining, CurrentOperations, SKU, WebApps, ASP | Sort-Object Role) if ($InstanceDetailsNotReady.count -gt 0) { Write-Host "Found " $InstanceDetailsNotReady.count " Instances(s) in Not Ready State" -ForegroundColor red $regionstatus = "Alert" $InstanceDetailsNotReady | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow } ## Pending Table output #Write-Host "Pending Operations on Instances" -ForegroundColor Yellow $InstanceDetailsPendingOperations = @($InstanceDetails | Where-Object { -not [string]::IsNullOrEmpty($_.CurrentOperations) } | Select-Object Name, MachineName, Status, Role, ServerState, IsCurrentOwner, IsDraining, CurrentOperations, SKU, WebApps, ASP | Sort-Object Role) if ($InstanceDetailsPendingOperations.count -gt 0) { Write-Host "Found " $InstanceDetailsPendingOperations.count " Instances(s) with Operations Pending" -ForegroundColor red if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } $InstanceDetailsPendingOperations | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow } ### Operations WFF write-host "Checking App Service WebFarm pending operations" -ForegroundColor green $AppServiceOperationsWFF = Get-AppServiceOperation -OperatorName WFF if ($AppServiceOperationsWFF) { Write-Host "Active RP Operations on Instances" -ForegroundColor Yellow #$AppServiceOperationsWFFlist = $AppServiceOperationsWFF | Select-Object OperationId, OperationName, OperatorName, Parameters, @{Name="ServerName"; Expression={$_.Parameters.ServerName}}, @{Name="WebFarmName"; Expression={$_.Parameters.WebFarmName}}, ConsumedTime $AppServiceOperationsWFFlist = $AppServiceOperationsWFF | Select-Object OperationId, OperationName, OperatorName, @{Name = "Parameters"; Expression = { $_.Parameters } }, ConsumedTime if ($regionstatus -ne "Alert") { if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } } $AppServiceOperationsWFFlist | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow $AppServiceOperationsWFFTable = @{ Title = "Pending WebFarm Operations" Data = $AppServiceOperationsWFFlist } } else { Write-Host "No App Service WebFarm Pending Operations found" -ForegroundColor Green $AppServiceOperationsWFFTable = @{ Title = "No Pending WebFarm Operations" Data = $AppServiceOperationsWFFlist } } ### Operations ActiveController write-host "Checking App Service Controller pending operations" -ForegroundColor green $AppServiceOperationsActiveController = Get-AppServiceOperation -OperatorName ActiveController if ($AppServiceOperationsActiveController) { Write-Host "Active RP Operations on Instances" -ForegroundColor Yellow #$AppServiceOperationsActiveControllerlist = $AppServiceOperationsActiveController | Select-Object OperationId, OperationName, OperatorName, Parameters, @{Name="ServerName"; Expression={$_.Parameters.ServerName}}, @{Name="WebFarmName"; Expression={$_.Parameters.WebFarmName}}, ConsumedTime $AppServiceOperationsActiveControllerlist = $AppServiceOperationsActiveController | Select-Object OperationId, OperationName, OperatorName, @{Name = "Parameters"; Expression = { $_.Parameters } }, ConsumedTime if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } $AppServiceOperationsActiveControllerlist | Format-Table * | Out-String | Write-Host -ForegroundColor Yellow $AppServiceOperationsActiveControllerTable = @{ Title = "Pending Controller Operations" Data = $AppServiceOperationsActiveControllerlist } } else { Write-Host "No App Service Controller Pending Operations found" -ForegroundColor Green $AppServiceOperationsActiveControllerTable = @{ Title = "No Pending Controller Operations" Data = $AppServiceOperationsActiveControllerlist } } ### convert to html all Table' if ($ExportHTML) { $region_AppServiceOverview = "<br><details><summary>App Service Overview - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($WebAppListTable, $AppServicePlanListTable, $SKUListTable, $WorkerTierListTable, $InstanceDetailsListTable, $AppServiceOperationsWFFTable, $AppServiceOperationsActiveControllerTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "AppServiceRPOverview") + "</details>" $region_AppServiceOverview } } ######### Web App View ################# function Select-WebApp { <# .SYNOPSIS This is a helper function that allows the user to select a web app from a paginated list. .DESCRIPTION This function allows the user to select a web app from a paginated list. It displays the web apps in pages of 100 and allows navigation through the pages. .EXAMPLE Select-WebApp -WebAppNameList $WebAppNameList This example displays the web apps in pages of 100 and allows the user to select a web app or navigate through the pages. .PARAMETER WebAppNameList An array of web app names to display in the paginated list. #> param ( [Parameter(Mandatory = $true)] [Array[]]$WebAppNameList ) # Calculate total number of pages $totalPages = [Math]::Ceiling($WebAppNameList.Count / 100) $currentPage = 1 # Pagination loop while ($true) { # Display web apps for the current page $startIndex = ($currentPage - 1) * 100 $endIndex = [Math]::Min($startIndex + 99, $WebAppNameList.Count - 1) Write-Host "Page $currentPage / $totalPages" Write-Host "Select a Web App:" for ($i = $startIndex; $i -le $endIndex; $i++) { $index = $i + 1 Write-Host "$index. $($WebAppNameList[$i])" } # Prompt user to navigate or select a web app $selection = Read-Host "Enter the number of the Web App to select, or type 'n' for next page, 'p' for previous page, or 'q' to quit" if ($selection -eq 'q') { Write-Host "Exiting..." break } elseif ($selection -eq 'n' -and $currentPage -lt $totalPages) { $currentPage++ Write-Host "`nPress any key to continue..." $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Write-Host "`n" } elseif ($selection -eq 'p' -and $currentPage -gt 1) { $currentPage-- Write-Host "`nPress any key to continue..." $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") Write-Host "`n" } elseif (-not [int]::TryParse($selection, [ref]$null)) { Write-Host "`nnot a valid key pressed..." } elseif ([int]$selection -ge 1 -and [int]$selection -le $WebAppNameList.Count) { $selectedWebAppName = $WebAppNameList[$selection - 1] Write-Host "You have selected: $selectedWebAppName" return $selectedWebAppName } else { Write-Host "Invalid selection. Please enter a valid number or command." } } } function Get-AzsSupportWebAppDetails { <# .SYNOPSIS This function retrieves details of a specific web app, including its workers and command line for log collection. .DESCRIPTION This function retrieves details of a specific web app, including its workers and command line for log collection. .EXAMPLE Get-AzsSupportWebAppDetails -WebAppName "MyWebApp" This example retrieves details of the specified web app, including its workers and command line for log collection. #> [CmdletBinding()] param ( [string]$WebAppName = "" ) $WebAppList = Get-AppServiceSite -RawView | Sort-Object SiteName $WebAppNameList = $WebAppList.SiteName if ([string]::IsNullOrWhiteSpace($WebAppName)) { Write-Warning -Message "Couldn't find any sites for the given web app." $selectedWebAppName = Select-WebApp -WebAppNameList $WebAppNameList Get-AzsSupportWebAppDetails -WebAppName $selectedWebAppName return } $sites = $script:SiteManager.Sites | Where-Object { $_.SiteName -like "$WebAppName" } if ($null -eq $sites) { Write-Warning -Message "Couldn't find any sites for the given web app '$WebAppName'" $selectedWebAppName = Select-WebApp -WebAppNameList $WebAppNameList Get-AzsSupportWebAppDetails -WebAppName $selectedWebAppName return } $sites | Select-Object SiteID, SiteName, HostNames, ComputeMode, SKU, RunningMode, AlwaysOn, RootDirectory | Format-Table -AutoSize $siteVirtualFarmId = $sites | Select-Object -ExpandProperty VirtualFarmId -Unique $siteVirtualFarm = $sites | Select-Object -ExpandProperty VirtualFarm -Unique $TargetWorkerSize = $siteVirtualFarm.TargetWorkerSize Write-Host "Possible Workers for WebApp $WebAppName" -ForegroundColor Green $possibleWorkers = $script:SiteManager.Workers | Where-Object { ($_.WorkerSize -like $TargetWorkerSize) -and ($_.VirtualFarmId -eq $siteVirtualFarmId) } $possibleWorkers | Select-Object Name, MachineName, SiteCount | Format-Table -AutoSize $script:possibleWorkersNames = $possibleWorkers | Select-Object -ExpandProperty Name $AppServiceLogFilterRoleCommand = "C:\temp\Get-AppServiceLogs.ps1 -FilterByRole WebWorker -workerCred (get-credential) -FilterByNode " + ($possibleWorkersNames -join ',') Write-Host "`nCommandline for log collection from Workers (copied to clipboard) --> `"$AppServiceLogFilterRoleCommand`"" -ForegroundColor DarkYellow $AppServiceLogFilterRoleCommand | Set-Clipboard } ######### Helper Function: Evaluate Certificate Status ######### function Get-CertificateStatus { [CmdletBinding()] param ( [datetime]$NotAfter, [datetime]$CurrentDate, [int]$NearExpirationThreshold, [int]$WarningThreshold ) $notAfterDate = $NotAfter.ToUniversalTime() $daysLeft = (New-TimeSpan -Start $CurrentDate -End $notAfterDate).Days if ($notAfterDate -lt $CurrentDate) { return "Expired" } elseif ($daysLeft -le $NearExpirationThreshold) { return "Alert: Expires in $daysLeft days. Immediate action required!" } elseif ($daysLeft -le $WarningThreshold) { return "Warning: Expires in $daysLeft days. Please renew soon." } else { return "OK" } } ### Aux Functions ### function Get-CertificateDetails { param ( [byte[]]$blob, [SecureString]$password ) try { $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) $unsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($ptr) $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $blob, $unsecurePassword $cert } catch { Write-Error "Error retrieving certificate details: $_" } finally { if ($unsecurePassword) { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR( [Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) ) } } } function Get-CertificateDetailsFromConfig { $script:appServiceConfigGlobal = Get-AppServiceConfig -Type Global try { # Array of certificate types to query $certificateTypes = @{ CertificateBlob = "Web Traffic Default SSL Cert"; InfrastructureClientCertificateBlob = "SSO"; ManagementServerCertificateBlob = "API"; PublisherServerCertificateBlob = "FTP"; TokenRequestCertificateBlob = "SSO" } $certificates = @() # Loop through each certificate type foreach ($certificateType in $certificateTypes.keys) { #Write-Host "Querying $certificateType" $blob = $script:appServiceConfigGlobal.$certificateType $password = $script:appServiceConfigGlobal."$($certificateType.TrimEnd('Blob'))Password" $securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force $cert = Get-CertificateDetails -blob $blob -password $securePassword $cert | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "CertType" -Value $certificateType $cert | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "CertAlias" -Value $certificateTypes.$certificateType $certificates += $cert } $certificates } catch { Write-Error "Error retrieving certificate details from config: $_" } } function Get-ArmClientCertficate { try { $script:appServiceConfigGlobal = Get-AppServiceConfig -Type Global $adminDiscoverUrl = $script:appServiceConfigGlobal.AdminThumbprintsDiscoveryUri -replace '{ArmEndpoint}', ($script:appServiceConfigGlobal.ArmEndpoint -replace '^https://').TrimEnd('/') $tenantDiscoverUrl = $script:appServiceConfigGlobal.TenantThumbprintsDiscoveryUri -replace '{TenantArmEndpoint}', ($script:appServiceConfigGlobal.TenantArmEndpoint -replace '^https://').TrimEnd('/') try { $adminCertResponse = Invoke-WebRequest $adminDiscoverUrl $adminCert = (ConvertFrom-Json $adminCertResponse.Content).clientCertificates $adminCert | Add-Member -MemberType NoteProperty -Name "DiscoverUrl" -Value $adminDiscoverUrl } catch { Write-Error $_ } try { $tenantCertResponse = Invoke-WebRequest $tenantDiscoverUrl $tenantCert = (ConvertFrom-Json $tenantCertResponse.Content).clientCertificates $tenantCert | Add-Member -MemberType NoteProperty -Name "DiscoverUrl" -Value $tenantDiscoverUrl } catch { Write-Error $_ } $adminCert $tenantCert } catch { Write-Error "Error retrieving ARM certificate : $_" } } ######### Check Certificates ######### function Test-AzsSupportAppServiceCertificates { <# .SYNOPSIS This function checks the status of ARM client certificates and App Service certificates, reporting any that are expired or near expiration. .DESCRIPTION This function checks the status of ARM client certificates and App Service certificates, reporting any that are expired or near expiration. .EXAMPLE Test-AzsSupportAppServiceCertificates -verbose This example checks the status of ARM client certificates and App Service certificates, providing certificates output. #> [CmdletBinding()] param ( [switch]$ExportHTML ) # Thresholds $nearExpirationThreshold = 30 $warningThreshold = 60 $currentDate = [datetime]::UtcNow $regionstatus = "OK" ######### ARM Client Certificates ######### Write-Host "Checking ARM Client Certificates" -ForegroundColor Green $ArmClientCertificatesResult = Get-ArmClientCertficate $ArmClientCertificatesResult | ForEach-Object { $_ | Add-Member -MemberType NoteProperty -Name "Status" -Value ( Get-CertificateStatus -NotAfter $_.NotAfter -CurrentDate $currentDate -NearExpirationThreshold $nearExpirationThreshold -WarningThreshold $warningThreshold ) } ## Output the results even if no alerts Write-Verbose ($ArmClientCertificatesResult | Format-Table Thumbprint, NotBefore, NotAfter, DiscoverUrl, Status | Out-String) $ArmCertAlerts = $ArmClientCertificatesResult | Where-Object { $_.Status -ne "OK" } if ($ArmCertAlerts) { Write-Host "The following ARM certificates are expired or near expiration:" -ForegroundColor Yellow $ArmCertAlerts | Format-Table Thumbprint, NotBefore, NotAfter, DiscoverUrl, Status | Out-String | Write-Host -ForegroundColor Yellow if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } } else { Write-Host "No expired or near-expiring ARM certificates found." -ForegroundColor Green } $ArmClientCertificatesResultTable = @{ title = "ARM Client Certificates" data = $ArmClientCertificatesResult | Select-Object Thumbprint, NotBefore, NotAfter, DiscoverUrl, Status } ######### App Service Certificates ######### Write-Host "Checking App Service Certificates" -ForegroundColor Green $AppServiceCertificatesResult = Get-CertificateDetailsFromConfig $AppServiceCertificatesResult | ForEach-Object { $_ | Add-Member -MemberType NoteProperty -Name "Status" -Value ( Get-CertificateStatus -NotAfter $_.NotAfter -CurrentDate $currentDate -NearExpirationThreshold $nearExpirationThreshold -WarningThreshold $warningThreshold ) } ## Output the results even if no alerts Write-Verbose ($AppServiceCertificatesResult | Format-Table CertType, CertAlias, Thumbprint, NotBefore, NotAfter, Subject, Status | Out-String) $AppServiceCertAlerts = $AppServiceCertificatesResult | Where-Object { $_.Status -ne "OK" } if ($AppServiceCertAlerts) { Write-Host "The following App Service certificates are expired or near expiration:" -ForegroundColor Yellow $AppServiceCertAlerts | Format-Table Thumbprint, NotBefore, NotAfter, Subject, Status | Out-String | Write-Host -ForegroundColor Yellow if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } } else { Write-Host "No expired or near-expiring App Service certificates found." -ForegroundColor Green } $AppServiceCertificatesResultTable = @{ title = "App Service Certificates" data = $AppServiceCertificatesResult | Select-Object CertType, CertAlias, Thumbprint, NotBefore, NotAfter, Subject, Status } ######### convert to html all Tables ######### if ($ExportHTML) { $region_CheckCertificates = "<br><details><summary>Certificates - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($ArmClientCertificatesResultTable, $AppServiceCertificatesResultTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "Certificates") + "</details>" return $region_CheckCertificates } } ######### Check VM guest OS ################# function Test-AzsSupportVMguestOS { <# .SYNOPSIS This function checks the guest OS status of VMs in the App Service. .DESCRIPTION It verifies the OS version, updates, and other relevant information. .EXAMPLE Test-AzsSupportVMguestOS This example checks the guest OS status of VMs in the App Service. #> [CmdletBinding()] param ( [switch]$ExportHTML ) $regionstatus = "OK" ################################## #Checking Roles Disk Space status. #Unhealthy: Free space is less then 10GB #disk space, see Get-AzsSupportAppServiceRoleFreeSpace Write-Host "Check Free Space" -ForegroundColor Green $AppServiceRoleFreeSpaceResult = Get-AzsSupportAppServiceRoleFreeSpace -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' if ($script:possibleWorkersNames) { $AppServiceRoleFreeSpaceResult += Get-AzsSupportAppServiceRoleFreeSpace -Name $script:possibleWorkersNames } $FreeSpaceAlert = $AppServiceRoleFreeSpaceResult | Select-Object ComputerName, IPv4, Drive, "Free Space (GB)", "Total Capacity (GB)" | Where-Object { $_."Free Space (GB)" -lt 10 } if ($FreeSpaceAlert) { if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } Write-Warning "Detected Low disk space on some of the Instances" $AppServiceRoleFreeSpaceResult | Format-Table | Out-String | Write-Host -ForegroundColor Yellow } $AppServiceRoleFreeSpaceResultTable = @{ title = "Free Space" data = $AppServiceRoleFreeSpaceResult | Select-Object ComputerName, IPv4, Drive, "Free Space (GB)", "Total Capacity (GB)" | Sort-Object ComputerName } ################################ #Checking Roles Hotfixes status. #Unhealthy: Installation result is Failed. #Hotfixes, see Test-AzsSupportAppServiceRoleUpdateStatus Write-Host "Check Windows Updates" -ForegroundColor Green $AppServiceRoleUpdateStatusResult = Test-AzsSupportAppServiceRoleUpdateStatus -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' if ($script:possibleWorkersNames) { $AppServiceRoleUpdateStatusResult += Test-AzsSupportAppServiceRoleUpdateStatus -Name $script:possibleWorkersNames } # list failed $FailedUpdateResults = $AppServiceRoleUpdateStatusResult | Select-Object ComputerName, IPv4, KB, Date, Result, Title | Where-Object { $_.Result -eq "Failed" } if ($FailedUpdateResults) { # Display the filtered results with failed updates if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } Write-Warning "Detected Windows Updates Failed on the last 60 days on some of the Instances" $FailedUpdateResults | Format-Table | Out-String | Write-Host -ForegroundColor Yellow } $AppServiceRoleUpdateStatusResultTable = @{ title = "Windows Update in Past 60 Days(Except Security Intelligence Update)" data = $AppServiceRoleUpdateStatusResult | Sort-Object Date -Descending #data = $AppServiceRoleUpdateStatusResult | Sort-Object PSComputerName } ################################### #Checking Roles Performance status. #Unhealthy: Cpu higher than 90% #Unhealthy: Available(GB) < RAM(GB)*0.03 #Unhealthy: Commit(GB) > RAM(GB)*1.5 #performance counters, see Test-AzsSupportAppServiceRolePerformance Write-Host "Check Role Performance" -ForegroundColor Green $AppServiceRolePerformanceResult = Test-AzsSupportAppServiceRolePerformance -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' if ($script:possibleWorkersNames) { $AppServiceRolePerformanceResult += Test-AzsSupportAppServiceRolePerformance -Name $script:possibleWorkersNames } #check unhealthy status $rolesPerformanceAlert = $AppServiceRolePerformanceResult | Where-Object { $_."CPU(%)" -gt 90 -or $_."Available(GB)" / $_."RAM(GB)" -lt 0.03 -or $_."Commit(GB)" / $_."RAM(GB)" -gt 1.5 } if ($rolesPerformanceAlert) { # Display the filtered results if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } Write-Warning "Detected CPU usage higher than 90% OR Available(GB) < RAM(GB)*0.03 OR ommit(GB) > RAM(GB)*1.5 on some of the Instances" $rolesPerformanceAlert | Format-Table | Out-String | Write-Host -ForegroundColor Yellow } $AppServiceRolePerformanceResultTable = @{ title = "Performance" data = $AppServiceRolePerformanceResult | Sort-Object ComputerName } ################################ #Checking Roles services status. #Unhealthy: Requested services are not running #services status. See Test-AzsSupportAppServiceRoleServices Write-Host "Check Role Services" -ForegroundColor Green $AppServiceRoleServicesResult = Test-AzsSupportAppServiceRoleServices -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' if ($script:possibleWorkersNames) { $AppServiceRoleServicesResult += Test-AzsSupportAppServiceRoleServices -Name $script:possibleWorkersNames } $rolesServicesAlert = $AppServiceRoleServicesResult | Where-Object { $_.Status -ne 'Running' } if ($rolesServicesAlert) { # Display the filtered results which services are not running. if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } Write-Warning "Detected requested services are not running on some of the Instances" $rolesServicesAlert | Format-Table | Out-String | Write-Host -ForegroundColor Yellow } $AppServiceRoleServicesResult = @{ title = "Services" data = $AppServiceRoleServicesResult | Sort-Object ComputerName } ################################ #Checking Roles processes status. #Unhealthy: Requested processes are not running #Processes status. See Test-AzsSupportAppServiceRoleProcesses Write-Host "Check Role Processes" -ForegroundColor Green $AppServiceRoleProcessesResult = Test-AzsSupportAppServiceRoleProcesses -Role 'LoadBalancer' #if ($script:possibleWorkersNames) {$AppServiceRoleServicesResult += Test-AzsSupportAppServiceRoleServices -Name $script:possibleWorkersNames} $rolesProcessesAlert = $AppServiceRoleProcessesResult | Where-Object { $_.Status -ne 'Running' } if ($rolesProcessesAlert) { # Display the filtered results which processes are not running. if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } Write-Warning "Detected requested processes are not running on some of the Instances" $rolesServicesAlert | Format-Table | Out-String | Write-Host -ForegroundColor Yellow } $AppServiceRoleProcessesResult = @{ title = "Processes" data = $AppServiceRoleProcessesResult | Sort-Object ComputerName } ### convert to html all Tables if ($ExportHTML) { $region_CheckGuestVMs = "<br><details><summary>VM Guest OS - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($AppServiceRoleFreeSpaceResultTable, $AppServiceRolePerformanceResultTable, $AppServiceRoleServicesResult, $AppServiceRoleProcessesResult, $AppServiceRoleUpdateStatusResultTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "VMGuestOS") + "</details>" $region_CheckGuestVMs } } Function New-AzsSupportAppServicePSsession { <# .SYNOPSIS Creates PowerShell sessions to app service roles computers. .DESCRIPTION This function creates PowerShell sessions to one or more remote computers specified by their name IP addresses. .PARAMETER ComputerName Specifies the name or IP address of the remote computers. This parameter supports aliases (-Name). Example: "LNV-W10--000001", "10.0.3.5" .EXAMPLE PS> New-AzsSupportAppServicePSsession -ComputerName "10.0.3.5","10.0.4.4","10.0.6.6","LNV-W10--000001" .EXAMPLE PS> New-AzsSupportAppServicePSsession -Role "ManagementServer", "Publisher", "LoadBalancer" #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")] [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')] [String[]]$Role, [Parameter(Mandatory = $true, ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")] [Alias("Name")] [string[]]$ComputerName ) try { $sessions = @() $appServiceServer = Get-AppServiceServer $AllCredentials = Get-AppServiceConfig -Type Credential # Define a function to create a PSCredential object function Get-PSCredential { param ( [String]$RoleName ) $CredentialRAW = $AllCredentials | Where-Object { $_.CredentialName -eq $RoleName } $PSCredential = New-Object System.Management.Automation.PSCredential($CredentialRAW.UserName, (ConvertTo-SecureString -String $CredentialRAW.Password -AsPlainText -Force)) return $PSCredential } # Build the hash table of Role to PSCredential $RoleToPSCredential = @{ LoadBalancer = Get-PSCredential -RoleName "FrontEndCredential" Publisher = Get-PSCredential -RoleName "PublisherCredential" WebWorker = Get-PSCredential -RoleName "WorkerCredential" ManagementServer = Get-PSCredential -RoleName "ManagementServerCredential" Controller = Get-PSCredential -RoleName "ManagementServerCredential" } if ($PSCmdlet.ParameterSetName -eq "Role") { $ComputerName = $appServiceServer | Where-Object Role -In $Role | Select-Object -ExpandProperty Name } Foreach ($Machine in $ComputerName) { #Filter the IP or resolve the ComputerName to IP. if ([bool]($Machine -as [ipaddress] -and ($Machine.ToCharArray() | Where-Object { $_ -eq "." }).count -eq 3)) { $Server = $appServiceServer | Where-Object Name -EQ $Machine } else { $Server = $appServiceServer | Where-Object Name -EQ (Resolve-DnsName -Name $Machine | Select-Object -ExpandProperty IPAddress) } if ($Server) { $ServerName = $Server.Name $Credential = $RoleToPSCredential[$Server.Role.ToString()] $Session = New-PSSession -ComputerName $ServerName -Credential $Credential "PowerShell remoting session established to $ServerName." | Out-Null #place holder, trace logs in the future. $sessions += $Session } else { "$Machine is an invalid app service server." | write-host } } return $Sessions } catch { Write-Error "Failed to establish PowerShell remoting session to $ComputerName. Error: $_" } } function Get-AzsSupportAppServiceRoleFreeSpace { <# .SYNOPSIS This function retrieves the free disk space on app service roles. .DESCRIPTION It checks the free disk space on specified app service roles or computers and returns the results. .EXAMPLE Get-AzsSupportAppServiceRoleFreeSpace -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' Get-AzsSupportAppServiceRoleFreeSpace -ComputerName "10.0.3.5","10.0.4.4","10.0.6.6","LNV-W10--000001" #> [CmdletBinding(DefaultParameterSetName = 'Role')] param ( [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")] [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')] [String[]]$Role = @('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer'), [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")] [Alias("Name")] [string[]]$ComputerName ) try { $getFreeSpace = { $vol = Get-Volume -DriveLetter ($env:SYSTEMDRIVE).trimend(":") if ($vol) { $total = [math]::Round($vol.Size / 1024 / 1024 / 1024, 2) $remaining = [math]::Round($vol.SizeRemaining / 1024 / 1024 / 1024, 2) $freeSpace = New-Object -TypeName psobject $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "ComputerName" -Value $env:computername $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "IPv4" -Value $IPv4 $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Drive" -Value $env:SYSTEMDRIVE $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Free Space (GB)" -Value $remaining $freeSpace | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Total Capacity (GB)" -Value $total $freeSpace } else { throw "Unable to get Volume of system drive." } } if ($PSCmdlet.ParameterSetName -eq "Role") { $session = New-AzsSupportAppServicePSsession -Role $Role } elseif ($PSCmdlet.ParameterSetName -eq "Name") { $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName } #$role = 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' #$session = New-AzsSupportAppServicePSsession -role $Role $Result = Invoke-Command -Session $session -ScriptBlock $getFreeSpace | Select-Object -Property *, PSComputerName, RunspaceId -ExcludeProperty PSComputerName, RunspaceId $Result | Sort-Object ComputerName } catch { Write-Error "Failure on $ComputerName. Error: $_" } finally { if ($session) { Remove-PSSession -Session $session -Confirm:$false } } } function Test-AzsSupportAppServiceRolePerformance { <# .SYNOPSIS This function tests the performance of app service roles by collecting various performance counters. .DESCRIPTION It retrieves performance counters such as CPU usage, memory availability, disk I/O, and network statistics for specified app service roles or computers. .PARAMETER Role Specifies the roles for which to collect performance data. Valid roles include 'Controller', 'ManagementServer', 'WebWorker', 'Publisher', and 'LoadBalancer'. If not specified, it defaults to all roles. .EXAMPLE Test-AzsSupportAppServiceRolePerformance -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' Test-AzsSupportAppServiceRolePerformance -ComputerName "10.0.3.5","10.0.4.4","10.0.6.6","LNV-W10--000001" #> [CmdletBinding(DefaultParameterSetName = 'Role')] param ( [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")] [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')] [String[]]$Role = @('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer'), [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")] [Alias("Name")] [string[]]$ComputerName ) try { $SampleInterval = 3 $MaxSamples = 20 $countersHash = @{"VMCPU(%)" = "\Processor(_total)\% processor time"; "Available(GB)" = "\Memory\Available Bytes"; "Commit(GB)" = "\Memory\committed bytes"; "CommitUse(%)" = "\Memory\% committed bytes in use"; "NIC(MB/s)" = "\Network Interface(*)\bytes total/sec"; "DiskR(GB/s)" = "\physicaldisk(_total)\disk read bytes/sec"; "DiskR(IOPS)" = "\physicaldisk(_total)\disk reads/sec"; "RLatency(ms)" = "\physicaldisk(_total)\avg. disk sec/read"; "RQD" = "\physicaldisk(_total)\avg. disk read queue length"; "DiskW(GB/s)" = "\physicaldisk(_total)\disk write bytes/sec"; "DiskW(IOPS)" = "\physicaldisk(_total)\disk writes/sec"; "WLatency(ms)" = "\physicaldisk(_total)\avg. disk sec/write"; "WQD" = "\physicaldisk(_total)\avg. disk write queue length"; "HostCPU(%)" = "\Hyper-V Hypervisor Logical Processor(_total)\% Total Run Time" } $countersList = $countersHash.Values -as [System.Object[]] #$session = New-AzsSupportAppServicePSsession -ComputerName $infVM #$session = New-AzsSupportAppServicePSsession -Role $Role if ($PSCmdlet.ParameterSetName -eq "Role") { $session = New-AzsSupportAppServicePSsession -Role $Role } elseif ($PSCmdlet.ParameterSetName -eq "Name") { $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName } $perf = Invoke-Command -Session $session { (Get-Counter -Counter $using:countersList -SampleInterval $using:SampleInterval -MaxSamples $using:MaxSamples -ErrorAction SilentlyContinue).CounterSamples } function Get-CounterValue { param ( $CounterValues, [string]$CounterName ) $ctrName = "*$CounterName*" ($CounterValues | Where-Object { $_.Path -like $ctrName }).CookedValue } function Select-CounterValueByMachine { param ( $CounterValues, [string]$ComputerName ) $CounterValues | Where-Object { $_.Path -match $ComputerName } } $vmSettings = Invoke-Command -Session $session { $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress $vmSettings = @() $r = New-Object psobject $r | Add-Member -MemberType NoteProperty -name Name -Value $env:computername $r | Add-Member -MemberType NoteProperty -name IPv4 -Value $IPv4 $r | Add-Member -MemberType NoteProperty -name ProcessorCount -Value (Get-WmiObject -Class Win32_processor).NumberOfCores $r | Add-Member -MemberType NoteProperty -name MemoryStartup -Value (Get-WmiObject -Class Win32_ComputerSystem).TotalPhysicalMemory $vmSettings += $r $vmSettings } $serverNameList = $vmSettings.Name $counterValues = $perf $counterTable = @() foreach ($machine in $serverNameList) { #Log-Info -Message "Checking counters on $machine" $MachineCounterValues = Select-CounterValueByMachine -CounterValues $counterValues -ComputerName $machine $counterDetails = New-Object -TypeName psobject $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "ComputerName" -Value $machine $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "IPv4" -Value (($vmSettings | Where-Object Name -like $machine).IPv4) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Cores" -Value (($vmSettings | Where-Object Name -like $machine).ProcessorCount) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "CPU(%)" -Value ([math]::Round((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("VMCPU(%)") -ComputerName $machine | Microsoft.PowerShell.Utility\Measure-Object -Average).Average, 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "RAM(GB)" -Value ([math]::Round(($vmSettings | Where-Object Name -like $machine).MemoryStartup / 1GB)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Available(GB)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("Available(GB)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average) / 1GB, 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "Commit(GB)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("Commit(GB)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average) / 1GB, 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "CommitUse(%)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("CommitUse(%)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "NIC(MB/s)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("NIC(MB/s)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average / 1MB), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "DiskR(GB/s)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("DiskR(GB/s)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average / 1GB), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "DiskR(IOPS)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("DiskR(IOPS)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "RLatency(ms)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("RLatency(ms)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average * 1000), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "RQD" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("RQD") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "DiskW(GB/s)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("DiskW(GB/s)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average / 1GB), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "DiskW(IOPS)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("DiskW(IOPS)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "WLatency(ms)" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("WLatency(ms)") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average * 1000), 2)) $counterDetails | Add-Member -Type NoteProperty -TypeName System.Management.Automation.PSCustomObject -Name "WQD" -Value ([math]::Round(((Get-CounterValue -CounterValues $MachineCounterValues -CounterName $countersHash.Get_Item("WQD") | Microsoft.PowerShell.Utility\Measure-Object -Average).Average), 2)) $counterTable += $counterDetails } $counterTable | Sort-Object ComputerName } catch { Write-Error "Failure on $ComputerName. Error: $_" } finally { if ($session) { Remove-PSSession -Session $session -Confirm:$false } } } function Test-AzsSupportAppServiceRoleUpdateStatus { <# .SYNOPSIS This function checks the update status of app service roles. .DESCRIPTION It retrieves the update history for specified app service roles or computers and returns the results. .PARAMETER Role Specifies the roles for which to check update status. Valid roles include 'Controller', 'ManagementServer', 'WebWorker', 'Publisher', and 'LoadBalancer'. If not specified, it defaults to all roles. .PARAMETER ComputerName Specifies the name or IP address of the remote computers to check update status. This parameter supports aliases (-Name). Example: "LNV-W10--000001", "10.0.3.5" .EXAMPLE Test-AzsSupportAppServiceRoleUpdateStatus -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' Test-AzsSupportAppServiceRoleUpdateStatus -ComputerName "10.0.3.5", "10.0.4.4", "10.0.6.6", "LNV-W10--000001" #> [CmdletBinding(DefaultParameterSetName = 'Role')] param ( [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")] [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')] [String[]]$Role = @('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer'), [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")] [Alias("Name")] [string[]]$ComputerName ) try { $hotfixScript = { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false | Out-Null Install-Module -Name PSWindowsUpdate -Force -Confirm:$false | Out-Null $WUHistory = Get-WUHistory -MaxDate ((Get-Date).AddDays(-60)) -Confirm:$false | Where-Object { $_.Title -notmatch "Security Intelligence Update for Microsoft Defender Antivirus" } $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress $WUHistory | Add-Member -MemberType NoteProperty -name IPv4 -Value $IPv4 Remove-Module -Name PSWindowsUpdate -Force | Out-Null $WUHistory } if ($PSCmdlet.ParameterSetName -eq "Role") { $session = New-AzsSupportAppServicePSsession -Role $Role } elseif ($PSCmdlet.ParameterSetName -eq "Name") { $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName } $data = Invoke-Command -Session $session -ScriptBlock $hotfixScript | Select-Object ComputerName, IPv4, KB, Date, Result, Title $data | Sort-Object ComputerName } catch { Write-Error "Failure on $ComputerName. Error: $_" } finally { if ($session) { Remove-PSSession -Session $session -Confirm:$false } } } function Test-AzsSupportAppServiceRoleServices { <# .SYNOPSIS This function checks the status of services on app service roles. .DESCRIPTION It retrieves the status of specified services for app service roles or computers and returns the results. .PARAMETER Role Specifies the roles for which to check service status. Valid roles include 'Controller', 'ManagementServer', 'WebWorker', 'Publisher', and 'LoadBalancer'. If not specified, it defaults to all roles. .PARAMETER ComputerName Specifies the name or IP address of the remote computers to check service status. This parameter supports aliases (-Name). Example: "LNV-W10--000001", "10.0.3.5" .EXAMPLE Test-AzsSupportAppServiceRoleServices -Role 'Controller', 'ManagementServer', 'Publisher', 'LoadBalancer' Test-AzsSupportAppServiceRoleServices -ComputerName "10.0.3.5", "10.0.4.4", "10.0.6.6", "LNV-W10--000001" #> [CmdletBinding(DefaultParameterSetName = 'Role')] param ( [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")] [ValidateSet('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer')] [String[]]$Role = @('Controller', 'ManagementServer', 'WebWorker', 'Publisher', 'LoadBalancer'), [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")] [Alias("Name")] [string[]]$ComputerName ) try { $serviceScript = { param( $appServiceServer, $PlatformVersion ) $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress $roleName = $appServiceServer | Where-Object { $_.Name -EQ $IPv4 } | Select-Object -ExpandProperty Role switch ($roleName) { "Controller" { $serviceList = @( "WebFarmService", "W3SVC", "ResourceMetering" ) } "ManagementServer" { $serviceList = @( "WebFarmAgentService", "W3SVC", "ImportExportService", "ResourceMetering", "UsageService" ) } "WebWorker" { $serviceList = @( "WebFarmAgentService", "DWASSvc", "ResourceMetering" ) } "LoadBalancer" { if ($PlatformVersion -gt '102.0.0.0') { $serviceList = @( "WebFarmAgentService", "esc", "ResourceMetering" ) } else { $serviceList = @( "WebFarmAgentService", "W3SVC", "esc", "ResourceMetering" ) } } "Publisher" { $serviceList = @( "WebFarmAgentService", "ResourceMetering", "DWASSvc", "ftpsvc" ) } } $services = Get-Service -Name $serviceList -ErrorAction SilentlyContinue $services = $services | Sort-Object -Property Name $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress $services | Add-Member -MemberType NoteProperty -name ComputerName -Value $env:computername $services | Add-Member -MemberType NoteProperty -name IPv4 -Value $IPv4 $services | Add-Member -MemberType NoteProperty -name Role -Value $roleName $services } if ($PSCmdlet.ParameterSetName -eq "Role") { $session = New-AzsSupportAppServicePSsession -Role $Role } elseif ($PSCmdlet.ParameterSetName -eq "Name") { $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName } $appServiceServer = Get-AppServiceServer $data = Invoke-Command -Session $session -ScriptBlock $serviceScript -ArgumentList $appServiceServer, $script:appServiceConfigGlobal.PlatformVersion | Select-Object ComputerName, IPv4, Role, Name, Status, StartType $data | Sort-Object ComputerName } catch { Write-Error "Failure on $ComputerName. Error: $_" } finally { if ($session) { Remove-PSSession -Session $session -Confirm:$false } } } function Test-AzsSupportAppServiceRoleProcesses { <# .SYNOPSIS This function checks the status of processes on app service LoadBalancer role. .DESCRIPTION It retrieves the status of specified processes for app service LoadBalancer role or computers and returns the results. .PARAMETER Role Specifies the roles for which to check process status. Valid role is 'LoadBalancer'. If not specified, it defaults to 'LoadBalancer'. .PARAMETER ComputerName Specifies the name or IP address of the remote computers to check process status. This parameter supports aliases (-Name). Example: "LNV-FE---00001", "10.0.3.5" .EXAMPLE Test-AzsSupportAppServiceRoleProcesses -Role 'LoadBalancer' Test-AzsSupportAppServiceRoleProcesses -ComputerName "10.0.6.7", "LNV-FE---000000" #> [CmdletBinding(DefaultParameterSetName = 'Role')] param ( [Parameter(ParameterSetName = 'Role', HelpMessage = "Enter a valid role.")] [ValidateSet('LoadBalancer')] [String[]]$Role = @('LoadBalancer'), [Parameter(ParameterSetName = 'Name', HelpMessage = "Enter the computer name. Or IP address")] [Alias("Name")] [string[]]$ComputerName ) try { $processScript = { param( $appServiceServer, $PlatformVersion ) $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress $roleName = $appServiceServer | Where-Object { $_.Name -EQ $IPv4 } | Select-Object -ExpandProperty Role switch ($roleName) { "LoadBalancer" { if ($PlatformVersion -gt '102.0.0.0') { $processList = @( "krpbfe", "WebFarmAgentService" ) } else { $processList = @( "WebFarmAgentService" ) } } } $processesOutput = @() $processes = Get-Process -Name $processList -ErrorAction SilentlyContinue foreach ($process in $processList) { if ($processes.Name -contains $process) { $processStatus = "Running" $FileVersion = ($processes | Where-Object { $_.Name -eq $process }).FileVersion } else { $processStatus = "Stopped" $FileVersion = "" } $customObject = New-Object PSObject -Property @{ 'Name' = $process 'Status' = $processStatus 'FileVersion' = $FileVersion } $processesOutput += $customObject } $IPv4 = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.AddressState -eq "Preferred" }).IPAddress $processesOutput | Add-Member -MemberType NoteProperty -name ComputerName -Value $env:computername $processesOutput | Add-Member -MemberType NoteProperty -name IPv4 -Value $IPv4 $processesOutput | Add-Member -MemberType NoteProperty -name Role -Value $roleName $processesOutput } if ($PSCmdlet.ParameterSetName -eq "Role") { $session = New-AzsSupportAppServicePSsession -Role $Role } elseif ($PSCmdlet.ParameterSetName -eq "Name") { $session = New-AzsSupportAppServicePSsession -ComputerName $ComputerName } $appServiceServer = Get-AppServiceServer $data = Invoke-Command -Session $session -ScriptBlock $processScript -ArgumentList $appServiceServer, $script:appServiceConfigGlobal.PlatformVersion | Select-Object ComputerName, IPv4, Role, Name, Status, FileVersion $data | Sort-Object ComputerName } catch { Write-Error "Failure on $ComputerName. Error: $_" } finally { if ($session) { Remove-PSSession -Session $session -Confirm:$false } } } ######### App Service File share / SQL Tests ################# ## Define Invoke-SQL funtion to run SQL queries function Invoke-SQL { <# .SYNOPSIS This function executes a SQL query against a specified SQL Server using the provided connection string. .DESCRIPTION It connects to the SQL Server using the provided connection string, executes the specified query, and returns the results as a DataTable. .PARAMETER ConnectionString The connection string to connect to the SQL Server. .PARAMETER Query The SQL query to be executed against the SQL Server. .EXAMPLE $connectionString = "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password='';" $query = "SELECT * FROM myTable" $result = Invoke-SQL -ConnectionString $connectionString -Query $query This example connects to the SQL Server using the specified connection string and executes the query, returning the results as a DataTable. .OUTPUTS Returns a DataTable containing the results of the executed SQL query. .NOTES This function requires the System.Data.SqlClient assembly to be available in the PowerShell environment. #> [CmdletBinding()] param( [string]$ConnectionString, [string]$Query ) $connection = New-Object System.Data.SqlClient.SqlConnection($ConnectionString) $command = New-Object System.Data.SqlClient.SqlCommand($Query, $connection) $connection.Open() $adapter = New-Object System.Data.SqlClient.SqlDataAdapter $command $dataset = New-Object System.Data.DataSet $adapter.Fill($dataSet) | Out-Null $connection.Close() return $dataSet.Tables } ## check SQL DB function Test-AzsSupportAppServiceSQL { <# .SYNOPSIS This function checks the SQL Server connection, version, database sizes, and trace messages in the App Service environment. .DESCRIPTION This function checks the SQL Server connection, version, database sizes, and trace messages in the App Service environment. .EXAMPLE Test-AzsSupportAppServiceSQL #> [CmdletBinding()] param ( [switch]$ExportHTML ) ## check connectivity $tncSQL = Test-NetConnection -Port 1433 $script:sqlserverip If ($tncSQL.TcpTestSucceeded -eq "True") { Write-Host "TCP connection Succeded" -ForegroundColor Green $regionstatus = "OK" } else { Write-Host "TCP connection Failed" -ForegroundColor Red $regionstatus = "Alert" } ## General SQL query $sqlQueryVersion = @' SELECT @@VERSION '@ $sqlQueryHostingCheckDBSize = @' SELECT [type] ,[type_desc] ,[name] ,[physical_name] ,[state] ,[state_desc] ,CAST(size AS bigint) * 8 / 1024 AS Size_MB ,CAST(max_size AS bigint) * 8 / 1024 AS MaxSize_MB ,CAST(growth AS bigint) * 8 / 1024 AS growth_MB ,[is_sparse] ,[is_percent_growth] FROM [appservice_hosting].[sys].[database_files] '@ $sqlQueryMeteringCheckDBSize = @' SELECT [type] ,[type_desc] ,[name] ,[physical_name] ,[state] ,[state_desc] ,CAST(size AS bigint) * 8 / 1024 AS Size_MB ,CAST(max_size AS bigint) * 8 / 1024 AS MaxSize_MB ,CAST(growth AS bigint) * 8 / 1024 AS growth_MB ,[is_sparse] ,[is_percent_growth] FROM [appservice_metering].[sys].[database_files] '@ $sqlQueryTraceMessagesCount = @' SELECT COUNT (*) FROM [appservice_hosting].[runtime].[TraceMessages]; '@ $sqlQueryTraceMessagesbyYear = @' SELECT YEAR([Timestamp]) AS [Year], COUNT(*) AS [MessageCount] FROM [appservice_hosting].[runtime].[TraceMessages] GROUP BY YEAR([Timestamp]) ORDER BY [Year]; '@ try { ## check SQL version $SQLVersion = Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQueryVersion $SQLVersionList = @{ Version = $SQLVersion[0].Column1 } # Create a custom object $SQLVersionconfigObject = New-Object PSObject -Property $SQLVersionList $SQLVersionTable = @{ title = 'SQL Version and OS' data = $SQLVersionconfigObject } Write-Host 'SQL Version and OS' -ForegroundColor Green Write-Host $SQLVersion[0].Column1 ## Execute SQL tests Write-Host -ForegroundColor Green "Checking DB details" $HostingDBConnTime = Measure-Command { $sqlHostingDBSize = Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQueryHostingCheckDBSize } $MeteringDBConnTime = Measure-Command { $sqlMeteringDBSize = Invoke-SQL -ConnectionString $script:meteringStr -Query $sqlQueryMeteringCheckDBSize } $sqlDBSize = @() $sqlDBSize += $sqlHostingDBSize $sqlDBSize += $sqlMeteringDBSize # Initialize an empty array to store custom objects $sqlDBDetailsObjects = @() # Loop through each element in the array foreach ($item in $sqlDBSize) { # Calculate the percentage of current size against the max size if ($item.MaxSize_MB -gt 0) { $percentFromMaxSize = ($item.Size_MB / $item.MaxSize_MB) * 100 } else { $percentFromMaxSize = 0 } if ($percentFromMaxSize -gt 80 -and $percentFromMaxSize -lt 90) { $message = "Warning: Database $($item.name) on $($script:sqlserverip) is using $([math]::Round($percentFromMaxSize, 2))% of its maximum allowed size." if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } Write-Warning $message } if ($percentFromMaxSize -gt 90 ) { $message = "Alert: Database $($item.name) on $($script:sqlserverip) is using $([math]::Round($percentFromMaxSize, 2))% of its maximum allowed size." $regionstatus = "Alert" Write-Warning $message } # Create a custom object for each element $customObject = New-Object PSObject -Property @{ 'SQL Server' = $script:sqlserverip 'Name' = $item.name 'DB Size MB' = $item.Size_MB 'State' = $item.state_desc 'physical_name' = $item.physical_name 'max_size MB' = $item.MaxSize_MB 'growth MB' = $item.growth_MB 'is_sparse' = $item.is_sparse 'is_percent_growth' = $item.is_percent_growth 'PercentFromMaxSize' = [math]::Round($percentFromMaxSize, 2) # Rounded to 2 decimal places } # Add the custom object to the array $sqlDBDetailsObjects += $customObject } $sqlDBDetailsObjects = $sqlDBDetailsObjects | Select-Object "SQL Server", Name, "DB Size MB", State, physical_name, "max_size MB", "growth MB", is_sparse, is_percent_growth, 'PercentFromMaxSize' $sqlDBDetailsObjects | Format-Table | Out-String | Write-Host -ForegroundColor White $sqlDBDetailsTable = @{ title = "Database details" data = $sqlDBDetailsObjects } ## Alert if conn time is more than 2 seconds, expected to be below 1s if (($HostingDBConnTime.Seconds -gt 2) -or ($MeteringDBConnTime.Seconds -gt 2)) { Write-Warning 'Hosting DB connection Time: '$HostingDBConnTime.Seconds 'seconds' Write-Warning 'Metering DB connection Time: '$MeteringDBConnTime.Seconds 'seconds' } Write-Host -ForegroundColor Green "Checking Tables" $TraceMessagesbyYear = Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQueryTraceMessagesbyYear $TraceMessagesCount = [int](Invoke-SQL -ConnectionString $script:hostingStr -Query $sqlQueryTraceMessagesCount)[0].Column1[0] $TraceMessagesTable = @{ title = "TraceMessages Table is below 500k rows" } if ([int]$TraceMessagesCount -gt 500000) { Write-Warning "Found Number of Rows on Table TraceMessages > 500k " Write-Host $TraceMessagesCount " Messages Found" -ForegroundColor Yellow $TraceMessagesbyYear | Format-Table | Out-String | Write-Host -ForegroundColor Yellow if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } $TraceMessagesTable = @{ title = "TraceMessages Table" data = $TraceMessagesbyYear | Select-Object Year, MessageCount } } } catch { Write-Error "Error processing SQL server with message: $($_.Exception.Message)" if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } } ### convert to html all Tables if ($ExportHTML) { $region_TestSQL = "<br><details><summary>SQL Service - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($SQLVersionTable, $sqlDBDetailsTable, $TraceMessagesTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "SQLService") + "</details>" $region_TestSQL } } ### Check FileServer performance ### function Test-AzsSupportAppServiceFileShare { <# .SYNOPSIS This function tests the file share performance by writing and reading a file to/from the file share. .DESCRIPTION This function tests the file share performance by writing and reading a file to/from the file share. .EXAMPLE Test-AzsSupportAppServiceFileShare .EXAMPLE Test-AzsSupportAppServiceFileShare -FileSizeMB 50MB #> [CmdletBinding()] param( [switch]$ExportHTML, [int32]$FileSizeMB = 50MB ) $LocalFilePath = "C:\temp\File1.txt" $RemoteFilePath = "$script:fileshare\File1.txt" ### The values from transfer rates may not be relevant ### The intention is to test the User and Owner creds writing and reading from/to fileshare try { ## check connectivity $tncSQL = Test-NetConnection -Port 445 $script:fileshareip If ($tncSQL.TcpTestSucceeded -eq "True") { Write-Host "TCP connection Succeded" -ForegroundColor Green $regionstatus = "OK" } else { Write-Host "TCP connection Failed" -ForegroundColor Red if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } } ## get FileshareOwner password from global:SiteManager $fileshareOwner = $script:SiteManager.HostingConfiguration.FileShareOwnerCredential.UserName $fileshareOwnerpass = $script:SiteManager.HostingConfiguration.FileShareOwnerCredential.Password ## get FileshareUser password from global:SiteManager $fileshareuser = $script:SiteManager.HostingConfiguration.FileShareUserCredential.UserName $fileshareuserpass = $script:SiteManager.HostingConfiguration.FileShareUserCredential.Password #create local file $f = new-object System.IO.FileStream "$LocalFilePath", Create, ReadWrite $f.SetLength(($FileSizeMB / 1GB) * 1GB) ### trick to overcome the int64 convertion $f.Close() ### create SMB connection with Owner $timeconnectwrite = Measure-Command -Expression { New-SmbMapping -RemotePath $script:fileshare -Username $fileshareOwner -Password $fileshareOwnerpass } ## output info for screen Write-Host "Test Fileshare" $fileshare -ForegroundColor green write-host "Statistics for file" $filename "with size" $FileSizeMB -ForegroundColor green Write-Host "Time spent on first connect to the share" $timeconnectwrite -ForegroundColor green ## mesure write Write-Host "Write Test using user "$fileshareOwner -ForegroundColor Green $timewrite = Measure-Command -Expression { Copy-Item -literalpath "$LocalFilePath" "$RemoteFilePath" -Force } ## Disconnect from share using owner Remove-SmbMapping -RemotePath $script:fileshare -force ### create SMB connection Read $null = New-SmbMapping -RemotePath $script:fileshare -Username $fileshareuser -Password $fileshareuserpass # mesure read Write-Host "Read Test using user "$fileshareuser -ForegroundColor Green $timeread = Measure-Command -Expression { Get-Item "$RemoteFilePath" } ## Disconnect from share using User Remove-SmbMapping -RemotePath "$script:fileshare" -force $item = Get-Item "$LocalFilePath" $WriteRate = ($item.length / 1024 / 1024) / $timewrite.TotalSeconds $ReadRate = ($item.length / 1024 / 1024) / $timeread.TotalSeconds $resultwrite = New-Object -TypeName psobject -Property @{ Operation = "Write" Source = $item.fullname TimeTaken = [math]::round($timewrite.TotalSeconds, 2) TransferRateMBs = [math]::round($WriteRate, 2) User = $fileshareOwner } $resultread = New-Object -TypeName psobject -Property @{ Operation = "Read" Source = $item.fullname TimeTaken = [math]::round($timeread.TotalSeconds, 2) TransferRateMBs = [math]::round($ReadRate, 2) User = $fileshareuser } ###Clean UP files $null = New-SmbMapping -RemotePath $script:fileshare -Username $fileshareOwner -Password $fileshareOwnerpass Write-Host "Performing cleanup..." -ForegroundColor green Remove-Item "$LocalFilePath" Remove-Item "$RemoteFilePath" ## Disconnect from share using Owner Remove-SmbMapping -RemotePath $fileshare -force } catch { Write-Error "Error processing FileShare with message: $($_.Exception.Message)" if ($regionstatus -ne "Alert") { $regionstatus = "Warning" } ## Disconnect from share using User in case previous failure $null = Remove-SmbMapping -RemotePath $script:fileshare -force } $FileshareTestResult = @() $FileshareTestResult += $resultwrite $FileshareTestResult += $resultread $FileshareTestResultTable = @{ title = "FileShare Read Write Tests" data = $FileshareTestResult } ### convert to html all Tables if ($ExportHTML) { $region_TestFileshare = "<br><details><summary>Fileshare Service - <div class='regionstatus' data-status='$regionstatus'>" + $regionstatus + " </div></summary>" + ($FileshareTestResultTable | ConvertTo-HtmlTableWithHeaderAndTitle -TestGroup "Fileshare") + "</details>" $region_TestFileshare } } #endregion # SIG # Begin signature block # MIIoOQYJKoZIhvcNAQcCoIIoKjCCKCYCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAcZItVe1xDFOmc # 81VMAsdq5crgWJGOe4aj8e1QuRhIXqCCDYUwggYDMIID66ADAgECAhMzAAAEA73V # lV0POxitAAAAAAQDMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTEzWhcNMjUwOTExMjAxMTEzWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQCfdGddwIOnbRYUyg03O3iz19XXZPmuhEmW/5uyEN+8mgxl+HJGeLGBR8YButGV # LVK38RxcVcPYyFGQXcKcxgih4w4y4zJi3GvawLYHlsNExQwz+v0jgY/aejBS2EJY # oUhLVE+UzRihV8ooxoftsmKLb2xb7BoFS6UAo3Zz4afnOdqI7FGoi7g4vx/0MIdi # kwTn5N56TdIv3mwfkZCFmrsKpN0zR8HD8WYsvH3xKkG7u/xdqmhPPqMmnI2jOFw/ # /n2aL8W7i1Pasja8PnRXH/QaVH0M1nanL+LI9TsMb/enWfXOW65Gne5cqMN9Uofv # ENtdwwEmJ3bZrcI9u4LZAkujAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU6m4qAkpz4641iK2irF8eWsSBcBkw # VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh # dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwMjkyNjAfBgNVHSMEGDAW # gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw # MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx # XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB # AFFo/6E4LX51IqFuoKvUsi80QytGI5ASQ9zsPpBa0z78hutiJd6w154JkcIx/f7r # EBK4NhD4DIFNfRiVdI7EacEs7OAS6QHF7Nt+eFRNOTtgHb9PExRy4EI/jnMwzQJV # NokTxu2WgHr/fBsWs6G9AcIgvHjWNN3qRSrhsgEdqHc0bRDUf8UILAdEZOMBvKLC # rmf+kJPEvPldgK7hFO/L9kmcVe67BnKejDKO73Sa56AJOhM7CkeATrJFxO9GLXos # oKvrwBvynxAg18W+pagTAkJefzneuWSmniTurPCUE2JnvW7DalvONDOtG01sIVAB # +ahO2wcUPa2Zm9AiDVBWTMz9XUoKMcvngi2oqbsDLhbK+pYrRUgRpNt0y1sxZsXO # raGRF8lM2cWvtEkV5UL+TQM1ppv5unDHkW8JS+QnfPbB8dZVRyRmMQ4aY/tx5x5+ # sX6semJ//FbiclSMxSI+zINu1jYerdUwuCi+P6p7SmQmClhDM+6Q+btE2FtpsU0W # +r6RdYFf/P+nK6j2otl9Nvr3tWLu+WXmz8MGM+18ynJ+lYbSmFWcAj7SYziAfT0s # IwlQRFkyC71tsIZUhBHtxPliGUu362lIO0Lpe0DOrg8lspnEWOkHnCT5JEnWCbzu # iVt8RX1IV07uIveNZuOBWLVCzWJjEGa+HhaEtavjy6i7MIIHejCCBWKgAwIBAgIK # YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm # aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw # OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD # VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la # UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc # 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D # dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+ # lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk # kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6 # A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd # X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL # 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd # sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3 # T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS # 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI # bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL # BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD # uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv # c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF # BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h # cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA # YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn # 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7 # v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b # pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/ # KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy # CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp # mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi # hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb # BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS # oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL # gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX # cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p # Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAQDvdWVXQ87GK0AAAAA # BAMwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIKsK # FR7FXNBRbrkX2KoKbbSOyyguhZ41FyYb/+DY4hcuMEIGCisGAQQBgjcCAQwxNDAy # oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20wDQYJKoZIhvcNAQEBBQAEggEAnd4+Oydr4UUWZi4lZ2pVYEnuwgJf4VFhFGMl # yRQBbavw2U2Z6za6UWFP2VZwRvHYcu8PRpjGAc6+amStGJxJNZlwMCRNmXSZBWBw # l7pHCGf1sS99BXQ3bAYdCvJCyhYNqjIKyXwHnnjgtk7+lTjCNOkj9Mpf5wuZ2fr6 # /zKts3iI3uDKCV3JC8U1wvAChWnzA09sehAehrK4ViJ7E/EORLnOAey2OZwBTvsy # lYkv9QQfJ9DQna5arKvAw9c5bRRFDOV4Mgsaof8Hx097Lb/xkJHt68WCeXlsgX8m # UdkrLxt4bSmcX+N4ac/Wok38IQi7vjG7xEnkCu10SGENZJJ2paGCF5QwgheQBgor # BgEEAYI3AwMBMYIXgDCCF3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZI # AWUDBAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGE # WQoDATAxMA0GCWCGSAFlAwQCAQUABCAF5lP5VHbUDHD5e+Yyw7qerNZgGACjO9T+ # eWalFuorBAIGaEtWkPcGGBMyMDI1MDYyNzE2NTgxNi44NTJaMASAAgH0oIHRpIHO # MIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL # ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxk # IFRTUyBFU046MzMwMy0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1l # LVN0YW1wIFNlcnZpY2WgghHqMIIHIDCCBQigAwIBAgITMwAAAg9XmkcUQOZG5gAB # AAACDzANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx # MDAeFw0yNTAxMzAxOTQzMDRaFw0yNjA0MjIxOTQzMDRaMIHLMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l # cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046MzMwMy0w # NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Uw # ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCl6DTurxf66o73G0A2yKo1 # /nYvITBQsd50F52SQzo2cSrt+EDEFCDlSxZzWJD7ujQ1Z1dMbMT6YhK7JUvwxQ+L # kQXv2k/3v3xw8xJ2mhXuwbT+s1WOL0+9g9AOEAAM6WGjCzI/LZq3/tzHr56in/Z+ # +o/2soGhyGhKMDwWl4J4L1Fn8ndtoM1SBibPdqmwmPXpB9QtaP+TCOC1vAaGQOds # qXQ8AdlK6Vuk9yW9ty7S0kRP1nXkFseM33NzBu//ubaoJHb1ceYPZ4U4EOXBHi/2 # g09WRL9QWItHjPGJYjuJ0ckyrOG1ksfAZWP+Bu8PXAq4s1Ba/h/nXhXAwuxThpva # Fb4T0bOjYO/h2LPRbdDMcMfS9Zbhq10hXP6ZFHR0RRJ+rr5A8ID9l0UgoUu/gNvC # qHCMowz97udo7eWODA7LaVv81FHHYw3X5DSTUqJ6pwP+/0lxatxajbSGsm267zqV # NsuzUoF2FzPM+YUIwiOpgQvvjYIBkB+KUwZf2vRIPWmhAEzWZAGTox/0vj4eHgxw # ER9fpThcsbZGSxx0nL54Hz+L36KJyEVio+oJVvUxm75YEESaTh1RnL0Dls91sBw6 # mvKrO2O+NCbUtfx+cQXYS0JcWZef810BW9Bn/eIvow3Kcx0dVuqDfIWfW7imeTLA # K9QAEk+oZCJzUUTvhh2hYQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFJnUMQ2OtyAh # LR/MD2qtJ9lKRP9ZMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8G # A1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv # Y3JsL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBs # BggrBgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy # MDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUH # AwgwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBTowbo1bUE7fXT # y+uW9m58qGEXRBGVMEQiFEfSui1fhN7jS+kSiN0SR5Kl3AuV49xOxgHo9+GIne5M # pg5n4NS5PW8nWIWGj/8jkE3pdJZSvAZarXD4l43iMNxDhdBZqVCkAYcdFVZnxdy+ # 25MRY6RfaGwkinjnYNFA6DYL/1cxw6Ya4sXyV7FgPdMmxVpffnPEDFv4mcVx3jvP # Zod7gqiDcUHbyV1gaND3PejyJ1MGfBYbAQxsynLX1FUsWLwKsNPRJjynwlzBT/OQ # bxnzkjLibi4h4dOwcN+H4myDtUSnYq9Xf4YvFlZ+mJs5Ytx4U9JVCyW/WERtIEie # TvTRgvAYj/4Mh1F2Elf8cdILgzi9ezqYefxdsBD8Vix35yMC5LTnDUoyVVulUeeD # AJY8+6YBbtXIty4phIkihiIHsyWVxW2YGG6A6UWenuwY6z9oBONvMHlqtD37ZyLn # 0h1kCkkp5kcIIhMtpzEcPkfqlkbDVogMoWy80xulxt64P4+1YIzkRht3zTO+jLON # u1pmBt+8EUh7DVct/33tuW5NOSx56jXQ1TdOdFBpgcW8HvJii8smQ1TQP42HNIKI # JY5aiMkK9M2HoxYrQy2MoHNOPySsOzr3le/4SDdX67uobGkUNerlJKzKpTR5ZU0S # eNAu5oCyDb6gdtTiaN50lCC6m44sXjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKb # SZkAAAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQI # EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv # ZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmlj # YXRlIEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIy # NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT # B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE # AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXI # yjVX9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjo # YH1qUoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1y # aa8dq6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v # 3byNpOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pG # ve2krnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viS # kR4dPf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYr # bqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlM # jgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSL # W6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AF # emzFER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIu # rQIDAQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIE # FgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWn # G1M1GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEW # M2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5 # Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBi # AEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV # 9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3Js # Lm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAx # MC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2 # LTIzLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv # 6lwUtj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZn # OlNN3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1 # bSNU5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4 # rPf5KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU # 6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDF # NLB62FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/ # HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdU # CbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKi # excdFYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTm # dHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZq # ELQdVTNYs6FwZvKhggNNMIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJp # Y2EgT3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjMzMDMtMDVF # MC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMK # AQEwBwYFKw4DAhoDFQBetIzj2C/MkdiI03EyNsCtSOMdWqCBgzCBgKR+MHwxCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv # c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7AjxfjAi # GA8yMDI1MDYyNzEwMzEyNloYDzIwMjUwNjI4MTAzMTI2WjB0MDoGCisGAQQBhFkK # BAExLDAqMAoCBQDsCPF+AgEAMAcCAQACAim2MAcCAQACAhPGMAoCBQDsCkL+AgEA # MDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAI # AgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBAOFK/2jIPrzX1jwrzRBCs+k4O+BL # GGM9w7cuytRDVBZM8LSN/zMGLS+yz+8IoifD9SMTxhAiGY070LPmbE9f6x3uLhH5 # zLpliW2WzuwdpemZ3KDJp4zGoicbPP6iqnaZvdrwQ8+vbODZSsMxmxe4fkDJs7pS # sdXA+5r9SPUavzBW+WvHGhfl99htUj6JRG2wfn2yOYSzqKDg7nW+s2vSjj6pEfpH # leWhKe4WESVEVh6UwwqjH6kDqInbs9Oon3yf/uxJXsEQ3AWoUDpxduj/mh75VvMD # yVps6tw2sGXl2WYhxphJMS2Abj/ZF5E7H81PuR0hGqjAotdL4+Fcl57EIaUxggQN # MIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ # MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAg9X # mkcUQOZG5gABAAACDzANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0G # CyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCBd8LbN5NROBHR3diFLIb8O99Pu # 1v5h8FIaQ8+P7XLU1zCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIN1Hd5Um # Knm7FW7xP3niGsfHJt4xR8Xu+MxgXXc0iqn4MIGYMIGApH4wfDELMAkGA1UEBhMC # VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV # BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp # bWUtU3RhbXAgUENBIDIwMTACEzMAAAIPV5pHFEDmRuYAAQAAAg8wIgQgv4CYMLP4 # ZXsKD/8L0tMxoMEinmsX3Zd/mZ6wZo5moHYwDQYJKoZIhvcNAQELBQAEggIAgi90 # JVW3Ll4ADUCRQR5pe9MxQ0Blolz+Y4wk8MW5WIWHXn7ka0Y396Ia/kS/zoxfxfXp # +kZRfl525be4vbRBejrvMboyvEMeevLd2lwK65PBpI7fyMCu6BshgzrfC4DP1jGv # jU5eHYwj+K1TV7Xy7QuKPsuVM0mUtiRYQc8oGvl318sUdh2taOkX6X59CB2DccO3 # YtizdULzjSjAJIcOJXPdKhDBP2LO4ce+BbZm3SttutEMl4Prl8caElXONZrrDbF/ # VAdO51+9A0Q4hfh6kbWD443WNqfeUhbqLkN/wBJ7/OaDZYg/QU+vqgw0ruxlE1gb # KRo1d6kpQTYHodhMNGN/AVrf+aiJO9XIJrp8WrEnaLaSMrIeOOz6Ct1qScEq/ai9 # 1l2bQEUyN1jP/gET4hAJwFQx7SCzWhfSNusdEmblpddPlKuFUR/N1L3eicHPwG17 # ISWrwDnmUw53dNFY1DI2uStzzfkcyl+nDkoyM/WS9WABaDFqNT1ZI9PFzHTD04Js # nSAaprfaN9FgvQYFl4qrI01oqUbJvJM24y9q+qgAuZT+/TOcXksOkTgv24TfIKL2 # kYyyeVYjSm2mpt8gOsvo+LxnY9yJDQPbuiWFlmqoZVdJo/lfT9JD91gyZOUuHOz3 # nIEh0XNKswO386jp/Gt57OFCKPjn4h1gPtX9VgY= # SIG # End signature block |