JobLogsForICM.ps1


<#PSScriptInfo
 
.VERSION 1.2
 
.GUID f7037068-1135-4a5d-bd75-82acdbc1f0d5
 
.AUTHOR nali2@microsoft.com
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
 
.DESCRIPTION
 Job logs collector tool
 
#>
 

param (
    [Parameter(Mandatory = $true)]
    [string]$subscriptionId,

    [Parameter(Mandatory = $true)]
    [string]$resourceGroupName,

    [Parameter(Mandatory = $true)]
    [string]$automationAccountName,

    [Parameter(Mandatory = $true)]
    [string[]]$jobIds
)


<#
.SYNOPSIS
    This PowerShell script collects required information of Azure automation and jobs to Microsoft CSS team for further investigation.
 
.DESCRIPTION
 
    This PowerShell script is designed to collect all logs in an Azure Automation account.
 
.PARAMETER subscriptionId
    Required. Subscription of the Azure Automation account.
  
.PARAMETER resourceGroupName
    Required. The name of the resource group of the Azure Automation account.
     
.PARAMETER automationAccountName
    Required. The name of the Azure Automation account.
 
.PARAMETER jobIds
    Required. The list of job ids which has issues.
 
.NOTES
    AUTHOR: Nina Li
    LASTEDIT: Mar 20, 2024
 
    example: .\JobLogsForICM.ps1 -subscriptionId <subscription id> -resourceGroupName <resource group name> -automationAccountName <automation account> -jobIds <job id 1>,<job id 2>
#>



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

[string]$global:defaultOutputDir = ""

Set-Variable REGEXE -Value ([string] "$($env:systemroot)\system32\reg.exe")
Set-Variable REGEDIT -Value ([string] "$($env:systemroot)\regedit.exe")

function enableScriptExecution
{
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Setting script execution policy to unrestricted."

    try
    {
        $execp = Get-ExecutionPolicy

        if(-not($execp -eq "Unrestricted") -and -not($execp -contains 'Bypass'))
        {
            if(-not($force) -and -not($PSCmdlet.ShouldContinue(("Your current policy " + $execp +" is not executable policy, so the script cannot be loaded, see details https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-7.2."), "Would you still like to set execution policy with unrestricted?")))
            {
                Write-Host "User has chosen to reject this request, skipping log collection, please set execution policy to bypass or unrestricted before continuing"
                exit
            }
        }

        Set-ExecutionPolicy unrestricted

    }
    catch 
    {
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Failed to set script's execution policy."
    }
}

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

function Get-TemporaryDirectoryLocation
{
    [OutputType([String])]
    Param ($tempPath)

    $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 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 = "$Level || $FunctionName || $Message"
    if($global:uselogfile)
    {
        Add-Content $global:logfile -Value $log
        Write-Output $log
    }
    else 
    {
        Write-Output $log
    }
}

function init
{
    
    enableScriptExecution

    $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 + "\..\Jobs.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 data collection."
}

# Function to log in to Azure
function Login-AzAccount {
    try
    {
        # This script login to azure by user credential from local powershell window.
        "Logging in to Azure..."
        Connect-AzAccount -SubscriptionId $subscriptionId
        
    }
    catch {
        Write-Error -Message $_.Exception
        throw $_.Exception
    }
}

function Show-Menu
{
    Clear-Host
    Write-Host "================ Please select the platform of job execution, select option #2 if you have both cloud jobs and HRW jobs ================"
    Write-Host 
    Write-Host "1: Run on Azure"
    Write-Host "2: Run on HRW (Hybrid runbook worker)"
    Write-Host "Q: Press 'Q' to quit."
    Write-Host "============================================================================================"
}

function enableScriptExecution
{
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Setting script execution policy to unrestricted."

    try
    {
        $execp = Get-ExecutionPolicy

        if(-not($execp -eq "Unrestricted") -and -not($execp -contains 'Bypass'))
        {
            if(-not($force) -and -not($PSCmdlet.ShouldContinue(("Your current policy " + $execp +" is not executable policy, so the script cannot be loaded, see details https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-7.2."), "Would you still like to set execution policy with unrestricted?")))
            {
                Write-Host "User has chosen to reject this request, skipping log collection, please set execution policy to bypass or unrestricted before continuing"
                exit
            }
        }

        Set-ExecutionPolicy unrestricted

    }
    catch 
    {
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Failed to set script's execution policy."
    }
}

function getAccountInfo
{
    $AADirectory = "$global:defaultOutputDir\AccountInfo"
    if ($False -eq (Test-Path $AADirectory) )
    {
        New-Item -Path $AADirectory -ItemType "directory" -Force | Out-Null
    }

    $AAinfo = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName -Name $automationAccountName
    $region = $AAinfo.Location
    $network = $AAinfo.PublicNetworkAccess

    Write-Log -FunctionName $MyInvocation.MyCommand -Message "=== AA info ==="
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Region: $region"
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Is public network enabled: $network"

    $AA = Invoke-AzRestMethod -Method GET -Uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName\?api-version=2023-11-01"
    $content = $AA.Content
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Save detailed automation account info ..."
   
    $content | Out-File $AADirectory\$automationAccountName.log
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "..............................."
    Write-Log -FunctionName $MyInvocation.MyCommand -Message "..............................."
}

function getCloudJobInfo
{
    init

    getAccountInfo

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

    foreach ($jobId in $jobIds){
        $job = Get-AzAutomationJob -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -Id $jobId
        $id = $job.JobId
        $state = $job.Status
        $param = $job.JobParameters | ConvertTo-Json
        $startTime = $job.StartTime
        $endTime = $job.EndTime
        $runbookName = $job.RunbookName
        $except = $job.Exception
        $logs = Get-AzAutomationJobOutput -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -Id $jobId -Stream "Any" | ft Time, Type, Summary | Out-String -Width 4096
        $runbook = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -Name $runbookName
        $runbookModified = $runbook.LastModifiedTime

        $script = Invoke-AzRestMethod -Method GET -Uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/content?api-version=2023-11-01"
        $content = $script.Content

        Write-Log -FunctionName $MyInvocation.MyCommand -Message "=== job info ==="
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Job id: $id"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Runbook name: $runbookName"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Last modified time of this runbook: $runbookModified"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Job status: $state"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Start Time: $startTime"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "End Time: $endTime"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Input parameters: $param"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "=== job logs ==="
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Saving runbook content ..."
        $content | Out-File $jobsDirectory\$runbookName_$id.ps1.txt
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Runbook content saved!"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Save job exception if any"
        $except | Out-File $jobsDirectory\exception_$id.log
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Saving job logs ..."
        $logs | Out-File $jobsDirectory\$id.log
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Job logs saved!"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "..............................."
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "..............................."
    }
}

function getHRWJobInfo
{
    init

    getAccountInfo

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

    foreach ($jobId in $jobIds){
        $job = Get-AzAutomationJob -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -Id $jobId

        $id = $job.JobId
        $state = $job.Status
        $param = $job.JobParameters | ConvertTo-Json
        $startTime = $job.StartTime
        $endTime = $job.EndTime
        $runbookName = $job.RunbookName
        $except = $job.Exception
        $logs = Get-AzAutomationJobOutput -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -Id $jobId -Stream "Any" | ft Time, Type, Summary | Out-String -Width 4096
        $runbook = Get-AzAutomationRunbook -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -Name $runbookName
        $runbookModified = $runbook.LastModifiedTime

        $script = Invoke-AzRestMethod -Method GET -Uri "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/runbooks/$runbookName/content?api-version=2023-11-01"
        $content = $script.Content

        Write-Log -FunctionName $MyInvocation.MyCommand -Message "=== job info ==="
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Job id: $id"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Runbook name: $runbookName"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Last modified time of this runbook: $runbookModified"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Job status: $state"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Start Time: $startTime"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "End Time: $endTime"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Input parameters: $param"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "=== job logs ==="
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Saving runbook content ..."
        $content | Out-File $HRWDirectory\$runbookName_$id.ps1.txt
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Runbook content saved!"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Save job exception if any"
        $except | Out-File $HRWDirectory\exception_$id.log
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Saving job logs ..."
        $logs | Out-File $HRWDirectory\$id.log
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Job logs saved!"


        $HRWG = $job.HybridWorker

        if (!$HRWG)
        {
            Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Failed to find any HRW for this job, please double check if this job is running on HRW"
            Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Skipping HRW info collection..."
        }
        else{
            $HRW = Get-AzAutomationHybridRunbookWorker -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName -HybridRunbookWorkerGroupName $HRWG
            $currentTime = [DateTime]::UtcNow
            
            foreach ($hw in $HRW){
                $worker = $hw.WorkerName
                $id = $hw.Name
                $regtime = $hw.RegisteredDateTime
                $lasttime = $hw.LastSeenDateTime
                Write-Log -FunctionName $MyInvocation.MyCommand -Message "=== HRW info ==="
                Write-Log -FunctionName $MyInvocation.MyCommand -Message "HRW Name:$worker"
                Write-Log -FunctionName $MyInvocation.MyCommand -Message "HRW id: $id"
                Write-Log -FunctionName $MyInvocation.MyCommand -Message "Current datetime in UTC: $currentTime"
                Write-Log -FunctionName $MyInvocation.MyCommand -Message "HRW registration time: $regtime"
                Write-Log -FunctionName $MyInvocation.MyCommand -Message "Last seen time: $lasttime"
            }
        }
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "..............................."
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "..............................."
    }
}

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

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

    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Compressing logs collected in folder: $Source"
    
    try 
    {
        Add-Type -Path "C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.IO.Compression.FileSystem\v4.0_4.0.0.0__b77a5c561934e089\System.IO.Compression.FileSystem.dll"
        [System.IO.Compression.ZipFile]::CreateFromDirectory($Source, $Destination, [System.IO.Compression.CompressionLevel]::Optimal, $false)
    }
    catch 
    {
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Exception: $_.Exception.Message"
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Failed to create zip with path: $Destination"
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Collected logs are stored at: $Source. Please manually zip the folder."
        exit
    }    
}

function CheckLogsForErrors
{
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $True)]
        [String]
        $OutputFile
    )

    try
    {
        Get-ChildItem -Path $global:defaultOutputDir\* -recurse -exclude *.dmp,*.exe,*.etl, *.dll | Select-String -List error | Format-List * | Out-File -FilePath $OutputFile
    }
    catch
    {
        Write-Log -Level "Error" -FunctionName $MyInvocation.MyCommand -Message "Failed to CheckLogsForErrors."
        Write-Log -FunctionName $MyInvocation.MyCommand -Message "Exception: $_.Exception.Message"
    }
}

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

    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Analyzing collected logs for errors."
    $analysisResultsFile = "$global:defaultOutputDir\errors.txt"
    CheckLogsForErrors -OutputFile $analysisResultsFile

    Write-Log -FunctionName $MyInvocation.MyCommand -Message "Analysis completed and written to file: $analysisResultsFile"

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

    Create-Zip -Source "$global:defaultOutputDir" -Destination "$global:defaultOutputDir.zip"

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

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

    Invoke-Expression "explorer '/select,$global:defaultOutputDir.zip'"
}

function main
{
    Login-AzAccount

    $AAinfo = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName -Name $automationAccountName
    if (!$AAinfo){
        Write-Host "In your subscription $subscriptionId, Automation account $automationAccountName under resource group $resourceGroupName does not exist, please double check name and restart the script!"
        return
    }
    
    Show-Menu    

    $selection = Read-Host "Please select an option"
    switch ($selection)
    {
        '1'
        {
            'You selected option #1'
            getCloudJobInfo
        }
        '2'
        {
            'You selected option #2'
            getHRWJobInfo
        }
        'q' 
        {
            return
        }
        'Q'
        {
            return
        }
    }
    archiveLogs
}

main