-
Notifications
You must be signed in to change notification settings - Fork 155
/
Update-AutomationAzureModulesForAccount.ps1
501 lines (394 loc) · 19.8 KB
/
Update-AutomationAzureModulesForAccount.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
<#
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.
#>
<#
.SYNOPSIS
Update Azure PowerShell modules in an Azure Automation account.
.DESCRIPTION
This Azure Automation runbook updates Azure PowerShell modules imported into an
Azure Automation account with the module versions published to the PowerShell Gallery.
Prerequisite: an Azure Automation account with a System assigned Managed Identity Enabled.
.PARAMETER ResourceGroupName
The Azure resource group name.
.PARAMETER AutomationAccountName
The Azure Automation account name.
.PARAMETER SimultaneousModuleImportJobCount
(Optional) The maximum number of module import jobs allowed to run concurrently.
.PARAMETER AzureModuleClass
(Optional) The class of module that will be updated (AzureRM or Az)
If set to Az, this script will rely on only Az modules to update other modules.
Set this to Az if your runbooks use only Az modules to avoid conflicts.
.PARAMETER AzureEnvironment
(Optional) Azure environment name.
.PARAMETER Login
(Optional) If $false, do not login to Azure.
.PARAMETER ModuleVersionOverrides
(Optional) Module versions to use instead of the latest on the PowerShell Gallery.
If $null, the currently published latest versions will be used.
If not $null, must contain a JSON-serialized dictionary, for example:
'{ "AzureRM.Compute": "5.8.0", "AzureRM.Network": "6.10.0" }'
or
@{ 'AzureRM.Compute'='5.8.0'; 'AzureRM.Network'='6.10.0' } | ConvertTo-Json
.PARAMETER PsGalleryApiUrl
(Optional) PowerShell Gallery API URL.
.LINK
https://docs.microsoft.com/en-us/azure/automation/automation-update-azure-modules
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")]
param(
[Parameter(Mandatory = $true)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $true)]
[string] $AutomationAccountName,
[Parameter(Mandatory = $true)]
[string] $SubscriptionId,
[int] $SimultaneousModuleImportJobCount = 10,
[string] $AzureModuleClass = 'AzureRM',
[string] $AzureEnvironment = 'AzureCloud',
[bool] $Login = $true,
[string] $ModuleVersionOverrides = $null,
[string] $PsGalleryApiUrl = 'https://www.powershellgallery.com/api/v2'
)
$ErrorActionPreference = "Continue"
#region Constants
$script:AzureRMProfileModuleName = "AzureRM.Profile"
$script:AzureRMAutomationModuleName = "AzureRM.Automation"
$script:GetAzureRmAutomationModule = "Get-AzureRmAutomationModule"
$script:NewAzureRmAutomationModule = "New-AzureRmAutomationModule"
$script:AzAccountsModuleName = "Az.Accounts"
$script:AzAutomationModuleName = "Az.Automation"
$script:GetAzAutomationModule = "Get-AzAutomationModule"
$script:NewAzAutomationModule = "New-AzAutomationModule"
$script:AzureSdkOwnerName = "azure-sdk"
#endregion
#region Functions
function ConvertJsonDictTo-HashTable($JsonString) {
try{
$JsonObj = ConvertFrom-Json $JsonString -ErrorAction Stop
} catch [System.ArgumentException] {
throw "Unable to deserialize the JSON string for parameter ModuleVersionOverrides: ", $_
}
$Result = @{}
foreach ($Property in $JsonObj.PSObject.Properties) {
$Result[$Property.Name] = $Property.Value
}
$Result
}
# Use the Run As connection to login to Azure
function Login-AzureAutomation([bool] $AzModuleOnly) {
try {
Write-Output "Logging in to Azure ($AzureEnvironment)..."
if ($AzModuleOnly) {
# Ensures you do not inherit an AzContext in your runbook
Disable-AzContextAutosave -Scope Process
# Connect to Azure with system-assigned managed identity.
# Please enable appropriate RBAC permissions to the system identity of this automation account. Otherwise, the runbook may fail...
$context = (Connect-AzAccount -Identity -Environment $AzureEnvironment).Context
# set and store context
$AzureContext = Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop
Select-AzSubscription -SubscriptionId $SubscriptionId | Write-Verbose
} else {
# Connect to Azure with system-assigned managed identity.
# Please enable appropriate RBAC permissions to the system identity of this automation account. Otherwise, the runbook may fail...
$context = (Connect-AzureRmAccount -Identity -Environment $AzureEnvironment).Context
$AzureContext = Set-AzureRmContext -SubscriptionId $SubscriptionId -ErrorAction Stop
Select-AzureRmSubscription -SubscriptionId $SubscriptionId | Write-Verbose
}
} catch {
throw $_.Exception
}
}
# Checks the PowerShell Gallery for the latest available version for the module
function Get-ModuleDependencyAndLatestVersion([string] $ModuleName) {
$ModuleUrlFormat = "$PsGalleryApiUrl/Search()?`$filter={1}&searchTerm=%27{0}%27&targetFramework=%27%27&includePrerelease=false&`$skip=0&`$top=40"
$ForcedModuleVersion = $ModuleVersionOverridesHashTable[$ModuleName]
$CurrentModuleUrl =
if ($ForcedModuleVersion) {
$ModuleUrlFormat -f $ModuleName, "Version%20eq%20'$ForcedModuleVersion'"
} else {
$ModuleUrlFormat -f $ModuleName, 'IsLatestVersion'
}
$SearchResult = Invoke-RestMethod -Method Get -Uri $CurrentModuleUrl -UseBasicParsing
if (!$SearchResult) {
Write-Verbose "Could not find module $ModuleName on PowerShell Gallery. This may be a module you imported from a different location. Ignoring this module"
} else {
if ($SearchResult.Length -and $SearchResult.Length -gt 1) {
$SearchResult = $SearchResult | Where-Object { $_.title.InnerText -eq $ModuleName }
}
if (!$SearchResult) {
Write-Verbose "Could not find module $ModuleName on PowerShell Gallery. This may be a module you imported from a different location. Ignoring this module"
} else {
$PackageDetails = Invoke-RestMethod -Method Get -UseBasicParsing -Uri $SearchResult.id
# Ignore the modules that are not published as part of the Azure SDK
if ($PackageDetails.entry.properties.Owners -ne $script:AzureSdkOwnerName) {
Write-Warning "Module : $ModuleName is not part of azure sdk. Ignoring this."
} else {
$ModuleVersion = $PackageDetails.entry.properties.version
$Dependencies = $PackageDetails.entry.properties.dependencies
@($ModuleVersion, $Dependencies)
}
}
}
}
function Get-ModuleContentUrl($ModuleName) {
$ModuleContentUrlFormat = "$PsGalleryApiUrl/package/{0}"
$VersionedModuleContentUrlFormat = "$ModuleContentUrlFormat/{1}"
$ForcedModuleVersion = $ModuleVersionOverridesHashTable[$ModuleName]
if ($ForcedModuleVersion) {
$VersionedModuleContentUrlFormat -f $ModuleName, $ForcedModuleVersion
} else {
$ModuleContentUrlFormat -f $ModuleName
}
}
# Imports the module with given version into Azure Automation
function Import-AutomationModule([string] $ModuleName, [bool] $UseAzModule = $false) {
$NewAutomationModule = $null
$GetAutomationModule = $null
if ($UseAzModule) {
$GetAutomationModule = $script:GetAzAutomationModule
$NewAutomationModule = $script:NewAzAutomationModule
} else {
$GetAutomationModule = $script:GetAzureRmAutomationModule
$NewAutomationModule = $script:NewAzureRmAutomationModule
}
$LatestModuleVersionOnGallery = (Get-ModuleDependencyAndLatestVersion $ModuleName)[0]
$ModuleContentUrl = Get-ModuleContentUrl $ModuleName
# Find the actual blob storage location of the module
do {
$ModuleContentUrl = (Invoke-WebRequest -Uri $ModuleContentUrl -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location
} while (!$ModuleContentUrl.Contains(".nupkg"))
$CurrentModule = & $GetAutomationModule `
-Name $ModuleName `
-ResourceGroupName $ResourceGroupName `
-AutomationAccountName $AutomationAccountName
if ($CurrentModule.Version -eq $LatestModuleVersionOnGallery) {
Write-Output "Module : $ModuleName is already present with version $LatestModuleVersionOnGallery. Skipping Import"
} else {
Write-Output "Importing $ModuleName module of version $LatestModuleVersionOnGallery to Automation"
& $NewAutomationModule `
-ResourceGroupName $ResourceGroupName `
-AutomationAccountName $AutomationAccountName `
-Name $ModuleName `
-ContentLink $ModuleContentUrl > $null
}
}
# Parses the dependency got from PowerShell Gallery and returns name and version
function GetModuleNameAndVersionFromPowershellGalleryDependencyFormat([string] $Dependency) {
if ($null -eq $Dependency) {
throw "Improper dependency format"
}
$Tokens = $Dependency -split":"
if ($Tokens.Count -ne 3) {
throw "Improper dependency format"
}
$ModuleName = $Tokens[0]
$ModuleVersion = $Tokens[1].Trim("[","]")
@($ModuleName, $ModuleVersion)
}
# Validates if the given list of modules has already been added to the module import map
function AreAllModulesAdded([string[]] $ModuleListToAdd) {
$Result = $true
foreach ($ModuleToAdd in $ModuleListToAdd) {
$ModuleAccounted = $false
# $ModuleToAdd is specified in the following format:
# ModuleName:ModuleVersionSpecification:
# where ModuleVersionSpecification follows the specifiation
# at https://docs.microsoft.com/en-us/nuget/reference/package-versioning#version-ranges-and-wildcards
# For example:
# AzureRm.profile:[4.0.0]:
# or
# AzureRm.profile:3.0.0:
# In any case, the dependency version specification is always separated from the module name with
# the ':' character. The explicit intent of this runbook is to always install the latest module versions,
# so we want to completely ignore version specifications here.
$ModuleNameToAdd = $ModuleToAdd -replace '\:.*', ''
foreach($AlreadyIncludedModules in $ModuleImportMapOrder) {
if ($AlreadyIncludedModules -contains $ModuleNameToAdd) {
$ModuleAccounted = $true
break
}
}
if (!$ModuleAccounted) {
$Result = $false
break
}
}
$Result
}
# Creates a module import map. This is a 2D array of strings so that the first
# element in the array consist of modules with no dependencies.
# The second element only depends on the modules in the first element, the
# third element only dependes on modules in the first and second and so on.
function Create-ModuleImportMapOrder([bool] $AzModuleOnly) {
$ModuleImportMapOrder = $null
$ProfileOrAccountsModuleName = $null
$GetAutomationModule = $null
# Use the relevant module class to avoid conflicts
if ($AzModuleOnly) {
$ProfileOrAccountsModuleName = $script:AzAccountsModuleName
$GetAutomationModule = $script:GetAzAutomationModule
} else {
$ProfileOrAccountsModuleName = $script:AzureRmProfileModuleName
$GetAutomationModule = $script:GetAzureRmAutomationModule
}
# Get all the non-conflicting modules in the current automation account
$CurrentAutomationModuleList = & $GetAutomationModule `
-ResourceGroupName $ResourceGroupName `
-AutomationAccountName $AutomationAccountName |
?{
($AzModuleOnly -and ($_.Name -eq 'Az' -or $_.Name -like 'Az.*')) -or
(!$AzModuleOnly -and ($_.Name -eq 'AzureRM' -or $_.Name -like 'AzureRM.*' -or
$_.Name -eq 'Azure' -or $_.Name -like 'Azure.*'))
}
# Get the latest version of the AzureRM.Profile OR Az.Accounts module
$VersionAndDependencies = Get-ModuleDependencyAndLatestVersion $ProfileOrAccountsModuleName
$ModuleEntry = $ProfileOrAccountsModuleName
$ModuleEntryArray = ,$ModuleEntry
$ModuleImportMapOrder += ,$ModuleEntryArray
do {
$NextAutomationModuleList = $null
$CurrentChainVersion = $null
# Add it to the list if the modules are not available in the same list
foreach ($Module in $CurrentAutomationModuleList) {
$Name = $Module.Name
Write-Verbose "Checking dependencies for $Name"
$VersionAndDependencies = Get-ModuleDependencyAndLatestVersion $Module.Name
if ($null -eq $VersionAndDependencies) {
continue
}
$Dependencies = $VersionAndDependencies[1].Split("|")
$AzureModuleEntry = $Module.Name
# If the previous list contains all the dependencies then add it to current list
if ((-not $Dependencies) -or (AreAllModulesAdded $Dependencies)) {
Write-Verbose "Adding module $Name to dependency chain"
$CurrentChainVersion += ,$AzureModuleEntry
} else {
# else add it back to the main loop variable list if not already added
if (!(AreAllModulesAdded $AzureModuleEntry)) {
Write-Verbose "Module $Name does not have all dependencies added as yet. Moving module for later import"
$NextAutomationModuleList += ,$Module
}
}
}
$ModuleImportMapOrder += ,$CurrentChainVersion
$CurrentAutomationModuleList = $NextAutomationModuleList
} while ($null -ne $CurrentAutomationModuleList)
$ModuleImportMapOrder
}
# Wait and confirm that all the modules in the list have been imported successfully in Azure Automation
function Wait-AllModulesImported(
[Collections.Generic.List[string]] $ModuleList,
[int] $Count,
[bool] $UseAzModule = $false) {
$GetAutomationModule = if ($UseAzModule) {
$script:GetAzAutomationModule
} else {
$script:GetAzureRmAutomationModule
}
$i = $Count - $SimultaneousModuleImportJobCount
if ($i -lt 0) { $i = 0 }
for ( ; $i -lt $Count; $i++) {
$Module = $ModuleList[$i]
Write-Output ("Checking import Status for module : {0}" -f $Module)
while ($true) {
$AutomationModule = & $GetAutomationModule `
-Name $Module `
-ResourceGroupName $ResourceGroupName `
-AutomationAccountName $AutomationAccountName
$IsTerminalProvisioningState = ($AutomationModule.ProvisioningState -eq "Succeeded") -or
($AutomationModule.ProvisioningState -eq "Failed")
if ($IsTerminalProvisioningState) {
break
}
Write-Verbose ("Module {0} is getting imported" -f $Module)
Start-Sleep -Seconds 30
}
if ($AutomationModule.ProvisioningState -ne "Succeeded") {
Write-Error ("Failed to import module : {0}. Status : {1}" -f $Module, $AutomationModule.ProvisioningState)
} else {
Write-Output ("Successfully imported module : {0}" -f $Module)
}
}
}
# Uses the module import map created to import modules.
# It will only import modules from an element in the array if all the modules
# from the previous element have been added.
function Import-ModulesInAutomationAccordingToDependency([string[][]] $ModuleImportMapOrder, [bool] $UseAzModule) {
foreach($ModuleList in $ModuleImportMapOrder) {
$i = 0
Write-Output "Importing Array of modules : $ModuleList"
foreach ($Module in $ModuleList) {
Write-Verbose ("Importing module : {0}" -f $Module)
Import-AutomationModule -ModuleName $Module -UseAzModule $UseAzModule
$i++
if ($i % $SimultaneousModuleImportJobCount -eq 0) {
# It takes some time for the modules to start getting imported.
# Sleep for sometime before making a query to see the status
Start-Sleep -Seconds 20
Wait-AllModulesImported -ModuleList $ModuleList -Count $i -UseAzModule $UseAzModule
}
}
if ($i -lt $SimultaneousModuleImportJobCount) {
Start-Sleep -Seconds 20
Wait-AllModulesImported -ModuleList $ModuleList -Count $i -UseAzModule $UseAzModule
}
}
}
function Update-ProfileAndAutomationVersionToLatest([string] $AutomationModuleName) {
# Get the latest azure automation module version
$VersionAndDependencies = Get-ModuleDependencyAndLatestVersion $AutomationModuleName
# Automation only has dependency on profile
$ModuleDependencies = GetModuleNameAndVersionFromPowershellGalleryDependencyFormat $VersionAndDependencies[1]
$ProfileModuleName = $ModuleDependencies[0]
# Create web client object for downloading data
$WebClient = New-Object System.Net.WebClient
# Download AzureRM.Profile to temp location
$ModuleContentUrl = Get-ModuleContentUrl $ProfileModuleName
$ProfileURL = (Invoke-WebRequest -Uri $ModuleContentUrl -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location
$ProfilePath = Join-Path $env:TEMP ($ProfileModuleName + ".zip")
$WebClient.DownloadFile($ProfileURL, $ProfilePath)
# Download AzureRM.Automation to temp location
$ModuleContentUrl = Get-ModuleContentUrl $AutomationModuleName
$AutomationURL = (Invoke-WebRequest -Uri $ModuleContentUrl -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location
$AutomationPath = Join-Path $env:TEMP ($AutomationModuleName + ".zip")
$WebClient.DownloadFile($AutomationURL, $AutomationPath)
# Create folder for unzipping the Module files
$PathFolderName = New-Guid
$PathFolder = Join-Path $env:TEMP $PathFolderName
# Unzip files
$ProfileUnzipPath = Join-Path $PathFolder $ProfileModuleName
Expand-Archive -Path $ProfilePath -DestinationPath $ProfileUnzipPath -Force
$AutomationUnzipPath = Join-Path $PathFolder $AutomationModuleName
Expand-Archive -Path $AutomationPath -DestinationPath $AutomationUnzipPath -Force
# Import modules
Import-Module (Join-Path $ProfileUnzipPath ($ProfileModuleName + ".psd1")) -Force -Verbose
Import-Module (Join-Path $AutomationUnzipPath ($AutomationModuleName + ".psd1")) -Force -Verbose
}
#endregion
#region Main body
if ($ModuleVersionOverrides) {
$ModuleVersionOverridesHashTable = ConvertJsonDictTo-HashTable $ModuleVersionOverrides
} else {
$ModuleVersionOverridesHashTable = @{}
}
$UseAzModule = $null
$AutomationModuleName = $null
# We want to support updating Az modules. This means this runbook should support upgrading using only Az modules
if ($AzureModuleClass -eq "Az") {
$UseAzModule = $true
$AutomationModuleName = $script:AzAutomationModuleName
} elseif ( $AzureModuleClass -eq "AzureRM") {
$UseAzModule = $false
$AutomationModuleName = $script:AzureRMAutomationModuleName
} else {
Write-Error "Invalid AzureModuleClass: '$AzureModuleClass'. Must be either Az or AzureRM" -ErrorAction Stop
}
# Import the latest version of the Az automation and accounts version to the local sandbox
Update-ProfileAndAutomationVersionToLatest $AutomationModuleName
if ($Login) {
Login-AzureAutomation $UseAzModule
}
$ModuleImportMapOrder = Create-ModuleImportMapOrder $UseAzModule
Import-ModulesInAutomationAccordingToDependency $ModuleImportMapOrder $UseAzModule
#endregion