AACollectLogs.ps1

<#PSScriptInfo
.VERSION 1.0.2
.GUID 0fc474c8-1392-49fe-883d-a7837e8635f7
.AUTHOR bmcder@microsoft.com
.COMPANYNAME
.COPYRIGHT
.TAGS AzureAutomationNotSupported
.LICENSEURI
.PROJECTURI
.ICONURI
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
.DESCRIPTION
This is a script to collect useful logs and diagnostic info for Microsoft Support for cases involving the following services/products
 Azure Arc enabled servers
 Azure Automation Hybrid Runbook Workers
 Automation Desired State Configuration(DSC)
 Automation Update Management
 Automation Change Tracking and Inventory
#>
 

$ErrorActionPreference = "SilentlyContinue"
$global:useLogFile = $true
$global:logFile = ""
$global:IsAzure = $false

[string]$global:defaultOutputDir = ""

Function Write-Log
{
    [cmdletbinding()]
    Param
    (
        [Parameter(Mandatory=$False)]
        [ValidateSet("INFO","WARN","ERROR","FATAL","DEBUG")]
        [String]
        $Level = "INFO",

        [Parameter(Mandatory=$True)]
        [String]
        $FunctionName,

        [Parameter(Mandatory=$True)]
        [AllowEmptyString()]
        [String]
        $Message
    )

    $timestamp = Get-Timestamp
    $log = "$timestamp || $Level || $FunctionName || $Message"
    if($global:uselogfile)
    {
        Add-Content $global:logfile -Value $log
        Write-Output $log
    }
    else 
    {
        Write-Output $log
    }
}

function CheckRunningAsAdmin
{
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Checking for elevated permissions..."
    if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) 
        {   
            Write-Log  -FunctionName $MyInvocation.MyCommand -Message "Exception: Insufficient permissions to run this script. Open the PowerShell console as an administrator and run this script again."
            exit
        }
}

function Get-Timestamp
{
    $timestamp = (Get-Date).ToString("hh_mm_MM_dd_yyyy")
    return $timestamp
}

function Get-TemporaryDirectoryLocation
{

    $dirName = $env:computername
    $dirName += "_"
    $dirName += Get-Timestamp
    $path = ""

    try 
    {
        $path = [System.IO.Path]::GetTempPath() + $dirName    
    }
    catch 
    {
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Exception: $_.Exception.Message"
    }

    return $path    
}

function CheckSoftwareInstalled
{
   param(
   [String]$software 
   )
  $InstalledSoftware = (get-childitem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall | Get-ItemProperty).DisplayName
  return ($InstalledSoftware -icontains $software)
}

function IsAzureVM 
{
   Write-Log -FunctionName $MyInvocation.MyCommand -Message "Checking if VM is on Azure..."
   $AzureMetadataDestination = "$global:defaultOutputDir\AzureMetadata.log"
   $response = Invoke-WebRequest -Uri "http://169.254.169.254/metadata/instance/compute?api-version=2019-06-01" -Headers @{Metadata = "true"} -TimeoutSec 1 -ErrorAction SilentlyContinue
       
   if ($null -ne $response -and $response.StatusCode -eq 200)
     {
       $global:IsAzure = $true
       $response.Content | out-file $AzureMetadataDestination
       Write-Log -FunctionName $MyInvocation.MyCommand -Message "VM is on Azure. Metadata collected"
       Write-Log -FunctionName $MyInvocation.MyCommand -Message "Collecting Guest Agent logs..."
       $CollectLogsExe = (get-childitem -Path c:\windowsazure -Filter CollectGuestLogs.exe -Recurse | sort-object LastAccessTime -desc | select-object -first 1).FullName
       & "$CollectLogsExe"  -FileName:$global:defaultOutputDir\AzureGuestAgentLogs.zip
     }
   else
     {
       Write-Log -FunctionName $MyInvocation.MyCommand -Message "VM is on NOT Azure."
       $global:IsAzure = $false
     }
}

function IsMMAInstalled
{
  CheckSoftwareInstalled "Microsoft Monitoring Agent"
}

function IsArcVM 
{
  CheckSoftwareInstalled "Azure Connected Machine Agent"
}

function Get-RegistryKeys
{
#"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\SystemCertificates\Microsoft Monitoring Agent\Certificates"
# HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL
# HKEY_USERS\S-1-5-18\Software\Microsoft\Windows\CurrentVersion\Internet Settings
# HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\HealthService\Parameters\Http Connection Proxy Url
"Hello"
}

function init
{
    $global:defaultOutputDir = (Get-TemporaryDirectoryLocation).ToString()
    if($true -eq [string]::IsNullOrEmpty($global:defaultOutputDir))
    {
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Default output dir empty: $global:defaultOutputDir"
        throw 'init failed'
    }
    New-Item -Path $global:defaultOutputDir -ItemType "directory" -Force | Out-Null
    $global:logFile = $global:defaultOutputDir + "\tool.log"

    if (Test-Path $global:logFile)
    {
        Write-Output "Removing tool's old log file."
        Remove-Item $global:logFile -Force
    }
    
    if($True -eq $global:useLogFile)
    {
        Write-Output "Tool's log data is being redirected to $global:logFile"
    }    

    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Starting AA Log collection on $env:computername at $(Get-Date) local time = $((Get-Date).ToUniversalTime()) UTC"
}

function Get-SystemData
{   
   Write-Log -FunctionName $MyInvocation.MyCommand -Message "Collecting system information..."

   $SysInfoDestination = "$global:defaultOutputDir\sysinfo.log"
   try
   {
        systeminfo | Out-File $SysInfoDestination
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "System information collected."
   }
   catch 
   {
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Exception: $_.Exception.Message"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Failed to collect system information."
   }
}

function Get-EventViewerLogs
{
    [cmdletbinding()]
    Param
    (
        [Parameter(Mandatory=$True)]
        [String]
        $Source,

        [Parameter(Mandatory=$True)]
        [String]
        $OutputFile
    )

    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Collecting event logs from: $Source"

    try 
    {
        $log = Get-CIMInstance Win32_NTEventlogFile -Filter "LogFileName = ""$Source"""
        $log.BackupEventLog($OutputFile) | Out-Null
        if($True -eq $?)
        {
            Write-Log -FunctionName $MyInvocation.MyCommand -Message "Logs from source: $Source, stored at: $OutputFile"
        }
    }
    catch 
    {
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Exception: $_.Exception.Message"
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Failed to collect event logs from: $Source"
    }
}

function CollectEventLogs
{
    
    if ($global:IsAzure -eq $true)
    {
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "We have already ran the Azure Guest Agent collection utility, so all event logs can be found in that zip. We will not duplicate the log collection"
        return
    } 

    $eventsDirectory = "$global:defaultOutputDir\Event Logs"
    if ($False -eq (Test-Path $eventsDirectory) )
    {
        New-Item -Path $eventsDirectory -ItemType "directory" -Force | Out-Null
    }    

    $eventsSource = "Application"
    Get-EventViewerLogs -Source $eventsSource -OutputFile $eventsDirectory\$eventsSource.evtx

    $eventsSource = "System"
    Get-EventViewerLogs -Source $eventsSource -OutputFile $eventsDirectory\$eventsSource.evtx

    if (IsMMAInstalled)
    {
        $eventsSource = "Operations Manager"
        Get-EventViewerLogs -Source $eventsSource -OutputFile $eventsDirectory\$eventsSource.evtx
    }

}

function archiveLogs
{
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Data collection completed."

    Copy-Item "$global:defaultOutputDir\..\tool.log" -Destination $global:defaultOutputDir

    Compress-Archive -Path "$global:defaultOutputDir" -DestinationPath "$global:defaultOutputDir.zip"

    Remove-Item -Recurse -Force -Path $global:defaultOutputDir

    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Collected logs available at: $global:defaultOutputDir.zip"

    & "explorer" "/select,$global:defaultOutputDir.zip"
}

function get-ArcInfo
{
    if (IsArcVM) 
    {
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Collecting Arc logs..."
        azcmagent logs -o "$global:defaultOutputDir\azcmagentlogs.zip"
    }
}

function IsUsingChangeTracking
{
    if ((get-childitem -Path "$($MMARegKey.InstallDirectory)Health Service State\" -Filter CT_*) -or (get-childitem -Path "$($MMARegKey.InstallDirectory)Health Service State\" -Filter FCT_*))
    {
        return $true
    }
}

function get-MMAInfo
{
  if (IsMMAInstalled)
  {
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Collecting MMA agent information..."

    $MMAgentDestination = "$global:defaultOutputDir\MMAgent.log"
    try
    {
      $MMARegKey = get-itemproperty -path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" | Select-Object AgentVersion,InstallDirectory,InstalledOn
      $MMAgent = New-Object -ComObject 'AgentConfigManager.MgmtSvcCfg'
      $MMARegKey  | format-list * | Out-File $MMAgentDestination
      $MMAgent.GetCloudWorkspaces() | Out-File $MMAgentDestination -append
      "MMA Proxy : $($MMAgent.ProxyURl)" >> $MMAgentDestination
      "MMA Proxy Username : $($MMAgent.ProxyUserName)" >> $MMAgentDestination
      Write-Log -FunctionName $MyInvocation.MyCommand -Message "MMA agent information collected."
      test-MMAConnectivity
    }
    catch 
    {
       Write-Log -FunctionName $MyInvocation.MyCommand -Message "Exception: $_.Exception.Message"
       Write-Log -FunctionName $MyInvocation.MyCommand -Message "Failed to collect MMA agent information."
    }  
    if (IsUsingChangeTracking)
    {
        Get-ChangeTrackingLogs
    }
  } 
  else
  {
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "MMA agent not installed."
  }  
}

function test-MMAConnectivity
{
  Write-Log -FunctionName $MyInvocation.MyCommand -Message "Checking MMA connectivity with testcloudconnection..."
  try
   {
        $TCCDestination = "$global:defaultOutputDir\TestCloudConnection.log"
        & "$($MMARegKey.InstallDirectory)TestCloudConnection.exe" | Out-File $TCCDestination
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "MMA connectivity checked."
   }
  catch 
   {
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Exception: $_.Exception.Message"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Failed to collect testcloudconnection output."
   }
}

function Copy-WindowsUpdateETLs
{
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Collecting Windows Update ETLs in case they haven't been formatted correctly..."
    new-item "$global:defaultOutputDir\WindowsUpdateETLs" -ItemType Directory
    copy-item C:\Windows\Logs\WindowsUpdate\*.etl -Destination "$global:defaultOutputDir\WindowsUpdateETLs\"
}

function Start-AUMTroubleshooter
{
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Downloading AUM Troubleshooter..."
    Invoke-WebRequest -Uri "https://raw.githubusercontent.com/Azure/updatemanagement/main/UM_Windows_Troubleshooter_Offline.ps1" `
        -OutFile "$global:defaultOutputDir\UM_Windows_Troubleshooter_Offline.ps1"
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Running AUM Troubleshooter..."
    & "$global:defaultOutputDir\UM_Windows_Troubleshooter_Offline.ps1" | out-file "$global:defaultOutputDir\AUMTroubleshooter.log"
}

function get-WindowsUpdateInfo
{
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Getting Windows Update information..."
    $windowsUpdateFile = $Env:WinDir + "\Windowsupdate.log"
    if (Get-Command "Get-WindowsUpdateLog" -errorAction SilentlyContinue)  
    {
        Convert-WindowsUpdateLog
        Copy-WindowsUpdateETLs 
    }
    else {
        Copy-Item $windowsUpdateFile -Destination "$global:defaultOutputDir\WindowsUpdate.log" -Force
    }
    
    Start-AUMTroubleshooter

    # TODO: Get TRSOP result
    get-childitem HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\ >> "$global:defaultOutputDir\WindowsUpdateRegKey.txt"
}

function convert-WindowsUpdateLog
{
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Converting raw Windows Update etls into readable WindowsUpdate.log file. This may take some time."
    Get-WindowsUpdateLog -LogPath "$global:defaultOutputDir\WindowsUpdate.log"
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Finished converting WindowsUpdate.log"
}

function get-HRWInfo
{
  $IsHRW = $false
  Write-Log -FunctionName $MyInvocation.MyCommand -Message "Checking for installed HRWs..."
  if (test-path HKLM:\SOFTWARE\Microsoft\HybridRunbookWorker)
  {
    $IsHRW = $true
    $HRWv1Destination = "$global:defaultOutputDir\HRWv1.log"
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Agent HRWv1 installed."
    $MMARegKey = get-itemproperty -path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" | Select-Object AgentVersion,InstallDirectory,InstalledOn
    $ReadHRWDetailsPath = (get-childitem -Path "$($MMARegKey.InstallDirectory)AzureAutomation\" -Filter Read-HybridRunbookWorkerDetails.ps1 -Recurse).FullName
    & "$ReadHRWDetailsPath" -LogFileFullPath:"$HRWv1Destination"
  }
  
  if (test-path HKLM:\SOFTWARE\Microsoft\HybridRunbookWorkerV2)
  {
    $IsHRW = $true
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "HRWv2 extension installed."
    $HRWv2Destination = "$global:defaultOutputDir\HRWv2.log"
    get-childitem HKLM:\SOFTWARE\Microsoft\HybridRunbookWorkerv2 -Recurse | out-file $HRWv2Destination

    $HRWv2TShooterDestination = "$global:defaultOutputDir\HRWv2TShooter.log"
    $HRWv2exepath = get-itempropertyValue -path HKLM:\SYSTEM\CurrentControlSet\Services\HybridWorkerService -Name ImagePath
    $HRWv2FolderPath = $HRWv2exepath.Substring(0, ($HRWv2exepath.Length - 61))
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Running HRWv2 extension TroubleshootWindowsExtension script."
    $HRWv2TroubleshooterScriptPath = $HRWv2FolderPath + "bin\troubleshooter\TroubleshootWindowsExtension.ps1"
    & $HRWv2TroubleshooterScriptPath | out-file $HRWv2TShooterDestination 
    $PullLogsScript = (get-childitem -Path "C:\Packages\Plugins\Microsoft.Azure.Automation.HybridWorker.HybridWorkerForWindows\" -Filter PullLogs.ps1 -Recurse).FullName
    & "$PullLogsScript" "$global:defaultOutputDir\HRW2Logs.zip"
  }
    
  if ($IsHRW -eq $false)
  {
    Write-Log -Level WARN -FunctionName $MyInvocation.MyCommand -Message "No HRW installed."
  }  
}

function get-ChangeTrackingLogs
{
   Write-Log -FunctionName $MyInvocation.MyCommand -Message "Copying Change Tracking logs..."
   New-Item "global:defaultOutputDir\ChangeTrackingLogs" -ItemType Directory
   Get-ChildItem -Path "$($MMARegKey.InstallDirectory)Health Service State\CT_*" | Copy-Item -Destination "$global:defaultOutputDir\ChangeTrackingLogs" -recurse -Force -Verbose
   Get-ChildItem -Path "$($MMARegKey.InstallDirectory)Health Service State\FCT_*" | Copy-Item -Destination "$global:defaultOutputDir\ChangeTrackingLogs" -recurse -Force -Verbose
}

function get-DSClogs
{
    if  (Get-DscConfigurationStatus)
    {
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Getting DSC Diagnostic logs..."
        if (-not $(get-command New-XDscDiagnostics))
        {
            Find-Module xDSCDiagnostics | Install-Module
            New-xDscDiagnosticsZip -destinationPath $global:defaultOutputDir\
        }   
    }
    else 
    {
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Skipping DSC Diagnostic logs collection as DSC not in use..."
    }
}

## MAIN BODY OF CODE START HERE

init
CheckRunningAsAdmin 
Get-SystemData
IsAzureVm
get-MMAInfo
get-HRWInfo
get-WindowsUpdateInfo
get-ArcInfo
CollectEventLogs
get-DSCLogs
archiveLogs