Skip to content

Commit c332a54

Browse files
authored
Add files via upload
1 parent f00cc39 commit c332a54

1 file changed

Lines changed: 296 additions & 0 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
<#
2+
.SYNOPSIS
3+
Monitors an Azure Automation runbook job and displays new stream records until the job finishes.
4+
5+
.DESCRIPTION
6+
Monitors an existing Azure Automation runbook job, periodically reports its current state,
7+
and displays newly available Output, Verbose, Warning, Error, or Progress stream records.
8+
9+
The script can monitor a supplied job ID or resolve the most recent job for a specified
10+
runbook. It reuses the current Azure PowerShell context by default and does not start, stop,
11+
suspend, or modify Azure Automation jobs.
12+
13+
.PARAMETER ResourceGroupName
14+
Name of the resource group containing the Azure Automation account.
15+
16+
.PARAMETER AutomationAccountName
17+
Name of the Azure Automation account.
18+
19+
.PARAMETER RunbookName
20+
Name of the runbook. When JobId is omitted, the script monitors the most recent job for this runbook.
21+
22+
.PARAMETER JobId
23+
Optional Azure Automation job ID. When omitted, the most recent job for RunbookName is selected.
24+
25+
.PARAMETER Streams
26+
Stream to display. Specify Any to display Output, Verbose, Warning, Error, and Progress records.
27+
28+
.PARAMETER PollSeconds
29+
Number of seconds between status checks. The default is 5 seconds.
30+
31+
.PARAMETER MaxWaitMinutes
32+
Maximum monitoring duration in minutes. A value of 0, the default, waits without a time limit.
33+
34+
.PARAMETER AllowInteractiveLogin
35+
Allows the script to call Connect-AzAccount when no current Azure context exists.
36+
Without this switch, the script stops and asks the operator to authenticate separately.
37+
38+
.EXAMPLE
39+
.\Watch-AzAutomationRunbookJob.ps1 `
40+
-ResourceGroupName "rg-automation" `
41+
-AutomationAccountName "aa-security-operations" `
42+
-RunbookName "Invoke-SecurityValidation"
43+
44+
Monitors the latest job for the specified runbook and displays its Output stream.
45+
46+
.EXAMPLE
47+
.\Watch-AzAutomationRunbookJob.ps1 `
48+
-ResourceGroupName "rg-automation" `
49+
-AutomationAccountName "aa-security-operations" `
50+
-RunbookName "Invoke-SecurityValidation" `
51+
-JobId "00000000-0000-0000-0000-000000000000" `
52+
-Streams Any `
53+
-PollSeconds 10
54+
55+
Monitors a specific job and displays all supported job streams every 10 seconds.
56+
57+
.EXAMPLE
58+
.\Watch-AzAutomationRunbookJob.ps1 `
59+
-ResourceGroupName "rg-automation" `
60+
-AutomationAccountName "aa-security-operations" `
61+
-RunbookName "Invoke-SecurityValidation" `
62+
-AllowInteractiveLogin
63+
64+
Permits an interactive Azure sign-in only if no reusable Az context is available.
65+
#>
66+
67+
[CmdletBinding()]
68+
param(
69+
[Parameter(Mandatory)]
70+
[ValidateNotNullOrEmpty()]
71+
[string] $ResourceGroupName,
72+
73+
[Parameter(Mandatory)]
74+
[ValidateNotNullOrEmpty()]
75+
[string] $AutomationAccountName,
76+
77+
[Parameter(Mandatory)]
78+
[ValidateNotNullOrEmpty()]
79+
[string] $RunbookName,
80+
81+
[Parameter()]
82+
[ValidateScript({
83+
if ([string]::IsNullOrWhiteSpace($_)) { return $true }
84+
$parsedGuid = [guid]::Empty
85+
if (-not [guid]::TryParse($_, [ref] $parsedGuid)) {
86+
throw 'JobId must be a valid GUID.'
87+
}
88+
return $true
89+
})]
90+
[string] $JobId,
91+
92+
[Parameter()]
93+
[ValidateSet('Output', 'Verbose', 'Warning', 'Error', 'Progress', 'Any')]
94+
[string] $Streams = 'Output',
95+
96+
[Parameter()]
97+
[ValidateRange(1, 3600)]
98+
[int] $PollSeconds = 5,
99+
100+
[Parameter()]
101+
[ValidateRange(0, 525600)]
102+
[int] $MaxWaitMinutes = 0,
103+
104+
[Parameter()]
105+
[switch] $AllowInteractiveLogin
106+
)
107+
108+
Set-StrictMode -Version Latest
109+
$ErrorActionPreference = 'Stop'
110+
111+
$requiredCommands = @(
112+
'Get-AzContext',
113+
'Connect-AzAccount',
114+
'Get-AzAutomationJob',
115+
'Get-AzAutomationJobOutput',
116+
'Get-AzAutomationJobOutputRecord'
117+
)
118+
119+
$missingCommands = @(
120+
foreach ($command in $requiredCommands) {
121+
if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) {
122+
$command
123+
}
124+
}
125+
)
126+
127+
if ($missingCommands.Count -gt 0) {
128+
throw "Required Azure PowerShell commands are unavailable: $($missingCommands -join ', '). Install or import the Az.Accounts and Az.Automation modules."
129+
}
130+
131+
$azContext = Get-AzContext -ErrorAction SilentlyContinue
132+
if (-not $azContext) {
133+
if (-not $AllowInteractiveLogin) {
134+
throw 'No active Azure context was found. Run Connect-AzAccount first, or rerun with -AllowInteractiveLogin.'
135+
}
136+
137+
Write-Verbose 'No active Azure context was found. Starting interactive authentication.'
138+
$null = Connect-AzAccount
139+
$azContext = Get-AzContext -ErrorAction Stop
140+
}
141+
142+
Write-Verbose ("Using Azure context for account '{0}' in subscription '{1}' ({2})." -f `
143+
$azContext.Account.Id,
144+
$azContext.Subscription.Name,
145+
$azContext.Subscription.Id)
146+
147+
$jobQueryParameters = @{
148+
ResourceGroupName = $ResourceGroupName
149+
AutomationAccountName = $AutomationAccountName
150+
ErrorAction = 'Stop'
151+
}
152+
153+
if ([string]::IsNullOrWhiteSpace($JobId)) {
154+
Write-Verbose "Resolving the most recent job for runbook '$RunbookName'."
155+
156+
$job = Get-AzAutomationJob @jobQueryParameters -RunbookName $RunbookName |
157+
Sort-Object -Property @{ Expression = {
158+
if ($null -ne $_.StartTime) { $_.StartTime }
159+
elseif ($null -ne $_.CreationTime) { $_.CreationTime }
160+
else { [datetime]::MinValue }
161+
}; Descending = $true } |
162+
Select-Object -First 1
163+
164+
if (-not $job) {
165+
throw "No Azure Automation jobs were found for runbook '$RunbookName'."
166+
}
167+
168+
$JobId = [string] $job.JobId
169+
}
170+
else {
171+
$job = Get-AzAutomationJob @jobQueryParameters -Id $JobId
172+
if (-not $job) {
173+
throw "Azure Automation job '$JobId' was not found."
174+
}
175+
176+
if ($job.RunbookName -and $job.RunbookName -ne $RunbookName) {
177+
throw "Job '$JobId' belongs to runbook '$($job.RunbookName)', not '$RunbookName'."
178+
}
179+
}
180+
181+
$selectedStreams = if ($Streams -eq 'Any') {
182+
@('Output', 'Verbose', 'Warning', 'Error', 'Progress')
183+
}
184+
else {
185+
@($Streams)
186+
}
187+
188+
$seenRecordIds = @{}
189+
$seenSummaryKeys = @{}
190+
foreach ($streamName in $selectedStreams) {
191+
$seenRecordIds[$streamName] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
192+
$seenSummaryKeys[$streamName] = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
193+
}
194+
195+
function Write-JobStreamRecord {
196+
[CmdletBinding()]
197+
param(
198+
[Parameter(Mandatory)]
199+
[ValidateSet('Output', 'Verbose', 'Warning', 'Error', 'Progress')]
200+
[string] $StreamName
201+
)
202+
203+
$records = @(
204+
Get-AzAutomationJobOutput `
205+
-ResourceGroupName $ResourceGroupName `
206+
-AutomationAccountName $AutomationAccountName `
207+
-Id $JobId `
208+
-Stream $StreamName `
209+
-ErrorAction Stop
210+
)
211+
212+
foreach ($record in $records) {
213+
$recordId = [string] $record.Id
214+
215+
if (-not [string]::IsNullOrWhiteSpace($recordId)) {
216+
if (-not $seenRecordIds[$StreamName].Add($recordId)) {
217+
continue
218+
}
219+
220+
$detail = Get-AzAutomationJobOutputRecord `
221+
-ResourceGroupName $ResourceGroupName `
222+
-AutomationAccountName $AutomationAccountName `
223+
-Id $recordId `
224+
-ErrorAction Stop
225+
226+
$value = $detail.Value
227+
if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string] $value)) {
228+
$value = $record.Summary
229+
}
230+
231+
Write-Host ("[{0:u}] [{1}] {2}" -f (Get-Date), $StreamName, ([string] $value))
232+
continue
233+
}
234+
235+
$summary = [string] $record.Summary
236+
$summaryKey = '{0}|{1}' -f $StreamName, $summary
237+
if ($seenSummaryKeys[$StreamName].Add($summaryKey)) {
238+
Write-Host ("[{0:u}] [{1}] {2}" -f (Get-Date), $StreamName, $summary)
239+
}
240+
}
241+
}
242+
243+
$terminalStates = @('Completed', 'Failed', 'Stopped', 'Suspended')
244+
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
245+
$lastStatusLine = $null
246+
247+
Write-Host ("Monitoring job '{0}' for runbook '{1}' in Automation account '{2}' (resource group '{3}')." -f `
248+
$JobId,
249+
$RunbookName,
250+
$AutomationAccountName,
251+
$ResourceGroupName) -ForegroundColor Cyan
252+
253+
try {
254+
while ($true) {
255+
$job = Get-AzAutomationJob @jobQueryParameters -Id $JobId
256+
if (-not $job) {
257+
throw "Azure Automation job '$JobId' is no longer available."
258+
}
259+
260+
$statusLine = "Status: $($job.Status) | Started: $($job.StartTime) | Last changed: $($job.LastModifiedTime)"
261+
if ($statusLine -ne $lastStatusLine) {
262+
Write-Host ("[{0:u}] {1}" -f (Get-Date), $statusLine)
263+
$lastStatusLine = $statusLine
264+
}
265+
266+
foreach ($streamName in $selectedStreams) {
267+
Write-JobStreamRecord -StreamName $streamName
268+
}
269+
270+
if ([string] $job.Status -in $terminalStates) {
271+
Write-Host "Job reached terminal state: $($job.Status)" -ForegroundColor Cyan
272+
break
273+
}
274+
275+
if ($MaxWaitMinutes -gt 0 -and $stopwatch.Elapsed.TotalMinutes -ge $MaxWaitMinutes) {
276+
throw "Monitoring exceeded the configured limit of $MaxWaitMinutes minute(s). The Azure Automation job was not modified."
277+
}
278+
279+
Start-Sleep -Seconds $PollSeconds
280+
}
281+
}
282+
finally {
283+
$stopwatch.Stop()
284+
}
285+
286+
[pscustomobject]@{
287+
JobId = [string] $job.JobId
288+
RunbookName = [string] $job.RunbookName
289+
Status = [string] $job.Status
290+
StartTime = $job.StartTime
291+
LastModifiedTime = $job.LastModifiedTime
292+
MonitoredFor = $stopwatch.Elapsed
293+
Streams = $selectedStreams -join ','
294+
ResourceGroupName = $ResourceGroupName
295+
AutomationAccountName = $AutomationAccountName
296+
}

0 commit comments

Comments
 (0)