InstallAzModule.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
<#PSScriptInfo
 
.VERSION 1.6
 
.GUID 70e6f41b-5941-4ec7-b797-60b96a301319
 
.AUTHOR Ted Sdoukos
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS AzureAutomation,Runbook
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALModuleDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
v1.6 Changelog
    *Corrected bug from v1.5
        *Missing $All and $Wait Parameter definitions in Install-AzAutomationModule
 
.PRIVATEDATA
 
#>


<#
.SYNOPSIS
Installs Az Modules to automation account.
 
.DESCRIPTION
This Azure Automation runbook installs the Az Modules selected into an
Azure Automation account with the Module versions published to the PowerShell Gallery.
Prerequisite: an Azure Automation account with an Azure Run As account credential.
 
.PARAMETER ResourceGroupName
The Azure resource group name.
 
.PARAMETER AutomationAccountName
The Azure Automation account name.
 
.PARAMETER All
This will install the Az Module and all dependancies.
 
.PARAMETER AzModule
This will install selected Module and dependancies.
 
.PARAMETER Wait
This will wait for the install of each Module.
 
.NOTES
Credit to: https://stackoverflow.com/questions/60847861/how-to-import-Modules-into-azure-automation-account-using-powershell
Credit to: https://github.com/microsoft/AzureAutomation-Account-Modules-Update
#>


[CmdletBinding()]
Param(
    [Parameter(Mandatory)]$ResourceGroupName,
    [Parameter(Mandatory)]$AutomationAccountName,
    [string]$AzModule,
    [string]$ModuleVersion,
    [bool]$All,
    [bool]$Wait
)
#region Functions
Function Get-AzModuleInfo {
    [CmdletBinding()]
    Param(
        $ModuleName,
        $PsGalleryApiUrl
        )
    Write-Verbose -Message "Finding Module information for $ModuleName within Get-AzModuleInfo"
    $ModuleUrlFormat = "$PsGalleryApiUrl/Search()?`$filter={1}&searchTerm=%27{0}%27&targetFramework=%27%27&includePrerelease=false&`$skip=0&`$top=40"
    $CurrentModuleURL = $ModuleUrlFormat -f $ModuleName, 'IsLatestVersion'
    Write-Verbose -Message $CurrentModuleURL
    $SearchID = Invoke-RestMethod -Method Get -Uri $CurrentModuleURL -UseBasicParsing | 
    Where-Object -FilterScript { $_.Title.InnerText -eq $ModuleName }
    Invoke-RestMethod -Method Get -UseBasicParsing -Uri $SearchID.id
}

Function Get-AzModuleDependency {
    [CmdletBinding()]
    Param($ModuleName)
    Write-Verbose -Message "Finding Dependent Modules for $ModuleName from within Get-AzModuleDependency"
    $Output = (Get-AzModuleInfo -ModuleName $ModuleName -PsGalleryApiUrl $PsGalleryApiUrl).entry.properties.Dependencies
    if ($Output) {
        ($Output -split '\|' | ForEach-Object { $_ -replace ':.*:' }).Trim()
    }
}

Function Install-AzModuleDependency {
    [CmdletBinding()]
    Param(
        $ModuleName,
        $ModuleVersion,
        $PsGalleryApiUrl
    )
    foreach ($M in $ModuleName) {
        Write-Verbose -Message "Calling Get-AzModuleInfo for module $M"
        $Module = (Get-AzModuleInfo -ModuleName $M -PsGalleryApiUrl $PsGalleryApiUrl).Entry.Properties
        If ($ModuleVersion) {
            $Link = "$PsGalleryApiUrl/package/$($Module.id)/$($Module.Version)"
        }
        else {
            $Link = "$PsGalleryApiUrl/package/$($Module.id)"
        }
        # Find the actual blob storage location of the Module
        do {
            $Link = (Invoke-WebRequest -Uri $Link -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location 
        } until ($Link.Contains('.nupkg'))
        $Status = Get-AzureRmAutomationModule -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Name $Module.id -ErrorAction SilentlyContinue

        If ( (-Not($Status)) -or ($Status.Version -ne $Module.Version)) {
            Write-Verbose -Message "Currently installing the $($Module.id) - Version-$($Module.version) dependency"
            $null = New-AzureRmAutomationModule -AutomationAccountName $AutomationAccountName -Name $Module.id -ContentLink $Link -ResourceGroupName $ResourceGroupName
            Do {
                $State = Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $Module.id
                Start-Sleep -Seconds 1
                Write-Verbose -Message "Waiting on install of $($Module.id)"
                Write-Progress -Activity "Installing $($Module.id)" -Status "Current Status is: $($state.ProvisioningState)"
            }
            While ($state.ProvisioningState -eq 'Creating')
        }
        If ($state.ProvisioningState -eq 'Failed') { Throw "Unable to install $($Module.id)" }
        While ($state.ProvisioningState -ne 'Succeeded') {
            $State = Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $Module.id
            Start-Sleep -Seconds 1
        }
        If ($state.ProvisioningState -eq 'Succeeded') { 
            Write-Progress -Activity "Installing $($Module.id)" -Status "Current Status is: $($state.ProvisioningState)"
            Write-Verbose -Message "Installation of $($Module.id) successful" 
        }
    }
}

Function Install-AzAutomationModule {
    <#
    .Synopsis
        Installs Az Modules in your azure automation account
    .DESCRIPTION
        Intalls the Az Modules selected into your desired automation account. This will search for and install any Dependent Modules as well.
    .EXAMPLE
        Install-AzAutomationModule -ResourceGroupName 'ContosoResourceGroup' -AutomationAccountName 'ContosoAutomationAccount' -All
 
        This example will install the Az Module along with latest versions of Dependent Modules.
    .EXAMPLE
        Install-AzAutomationModule -ResourceGroupName 'ContosoResourceGroup' -AutomationAccountName 'ContosoAuto1' -AzModule 'Az.Blueprint'
 
        This example will install the Az.Blueprint Module along with latest versions of any Dependent Modules.
    .NOTES
        Author: Ted Sdoukos
        Credit to: https://stackoverflow.com/questions/60847861/how-to-import-Modules-into-azure-automation-account-using-powershell
        Credit to: https://github.com/microsoft/AzureAutomation-Account-Modules-Update
        REQUIEMENTS: AzureRM Automation Module or Az.Automation Module with aliases enabled.
    #>

    [CmdletBinding()]
    Param(
        $AzModule,
        $ResourceGroupName,
        $AutomationAccountName,
        $All,
        $Wait
    )

    If ($All) {
        $AzModule = 'Az'
    }
    $DepList = New-Object -TypeName System.Collections.ArrayList
    $List = New-Object -TypeName System.Collections.ArrayList
    Write-Verbose -Message "Finding Dependent modules for $AzModule"
    Get-AzModuleDependency -ModuleName $AzModule | ForEach-Object {
        $null = $List.Add($_)
    }
    Write-Verbose -Message "Current value of List is: $List | AzModule: $AzModule"
    foreach ($Item in $List) {
        Get-AzModuleDependency -ModuleName $Item | ForEach-Object {
            If ($DepList -notcontains $_) {
                $null = $DepList.Add($_)
            }
        }
    }
    Write-Verbose -Message "List = $List`r`nDepList = $DepList"
    If ($List -and -not $DepList) {
        $List | ForEach-Object { $null = $DepList.Add($_) }
    }
    $null = $List.Add($AzModule)
    If ($List -contains 'Az') {
        $null = $List.Remove('Az')
    }
    $AzModule = $List
    Write-Verbose -Message "FINAL Dependent List:`n$DepList"
   
    If ($DepList) { Install-AzModuleDependency -ModuleName $DepList -PsGalleryApiUrl $PsGalleryApiUrl }
    $AzModule | ForEach-Object {
        if (($_) -notin $DepList) {
            $InstallState = Get-AzureRmAutomationModule -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Name $_ -ErrorAction SilentlyContinue
            If (($InstallState.ProvisioningState -ne 'Succeeded') -or (-not($InstallState))) {
                $Module = (Get-AzModuleInfo -ModuleName $_ -PsGalleryApiUrl $PsGalleryApiUrl).Entry.Properties
                If ($ModuleVersion) {
                    $Link = "$PsGalleryApiUrl/package/$($_)/$($Module.Version)"
                }
                else {
                    $Link = "$PsGalleryApiUrl/package/$($_)"
                }
                do {
                    $TryCount = 0
                    $Link = (Invoke-WebRequest -Uri $Link -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location 
                    $TryCount++
                } until ($Link.Contains('.nupkg') -or $TryCount -gt 10)
                
                $ModName = $Module.Id
                Write-Verbose -Message "Currently installing $ModName"
                Write-Progress -Activity "Installing $ModName"
                $null = New-AzureRmAutomationModule -AutomationAccountName $AutomationAccountName -Name $ModName -ContentLink $Link -ResourceGroupName $ResourceGroupName
                If ($Wait) {
                    Do {
                        $Status = Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $_ 
                        Start-Sleep -Seconds 1
                    }
                    While ($Status.ProvisioningState -eq 'Creating')
                    Write-Verbose -Message "Provisioning of $_ is complete. Current Status is $($Status.ProvisioningState)"
                }
                #Added a sleep in here to alleviate errors
                # Most common error: Index was out of range. Must be non-negative and less than the size of the collection.
                Start-Sleep -Seconds 2
            }
        }
    }
    $AzModule | ForEach-Object {
        Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $_ |
        Select-Object -Property Name, ProvisioningState
    }
}

function Connect-AzureAutomation {
    try {
        $RunAsConnection = Get-AutomationConnection -Name 'AzureRunAsConnection'
        $RunAsConnection | Select-Object -Property *
        Write-Verbose -Message "Logging in to Azure ($AzureEnvironment)..."
        
        if (!$RunAsConnection.ApplicationId) {
            $ErrorMessage = "Connection 'AzureRunAsConnection' is incompatible type."
            throw $ErrorMessage            
        }
        Add-AzureRmAccount -ServicePrincipal -TenantId $RunAsConnection.TenantId -ApplicationId $RunAsConnection.ApplicationId `
            -CertificateThumbprint $RunAsConnection.CertificateThumbprint 

        Select-AzureRmSubscription -SubscriptionId $RunAsConnection.SubscriptionID | Write-Verbose
    }
    catch {
        if (!$RunAsConnection) {
            $_.Exception
            $ErrorMessage = "Connection 'AzureRunAsConnection' not found."
            throw $ErrorMessage
        }

        throw $_.Exception
    }
}
#EndRegion Functions

#region main script
$PsGalleryApiUrl = 'https://www.powershellgallery.com/api/v2'
If (Get-Module -Name Az.Automation -ListAvailable) {
    Try { Enable-AzureRmAlias }catch {}
}
$null = Connect-AzureAutomation 
Write-Verbose -Message "Bound Params: rgName - $ResourceGroupName`n`rauName: $AutomationAccountName`n`rModule: $AzModule"
Install-AzAutomationModule @PSBoundParameters
$failCheck = Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName | 
Where-Object -FilterScript { $_.ProvisioningState -eq 'Failed' }
If ($failCheck) {
    Write-Warning -Message "The following Modules failed to install: $($failCheck.Name | ForEach-Object {"`n$_"})"
    Write-Warning -Message `
        "Type the following to retry: `nInstall-AzAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -AzModule $($failCheck.name -join ', ') -Wait"
}
#endRegion