forked from helderpinto/AzureUpdateManagerTools
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Create-StagedMaintenanceConfiguration.ps1
298 lines (268 loc) · 13.8 KB
/
Create-StagedMaintenanceConfiguration.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
<#
.SYNOPSIS
Runbook that automatically creates one or more maintenance configurations in Azure Update Manager, based on update packages already installed on an initial environment (Pre/Dev/Test/QA).
.DESCRIPTION
This Runbook uses Azure Update Manager installation results to query the latest update packages installed on a set of machines, and based on a maintenance configuration already deployed,
and creates one or more maintenance configurations based on the next stages definitions set as a parameter.
These parameters are needed:
.PARAMETER MaintenanceConfigurationId
ARM Id of the Maintenance Configuration to be used as a reference to create maintenance configurations for further stages
.PARAMETER NextStagePropertiesJson
JSON-formatted parameter that will define the scope of the new maintenance configurations. See https://github.com/disbenovi/AzureUpdateManagerTools for more details.
.NOTES
OG AUTHOR: Helder Pinto and Wiszanyel Cruz
FORKED BY: Navi Singh.
Lines 28-40 manipulate `NextStagePropertiesJson` to address `jsonencode` function's formatting in Terraform,
which adds extra newlines and escape sequences. Steps include:
1. **Remove Newline Characters**: `-replace '\\', ''` removes backslashes added by `jsonencode`.
2. **Format "aum-stage" Value**: `-replace '("aum-stage":)"([^"]+)"', '$1["$2"]'` ensures "aum-stage" is formatted as an array,
correcting `jsonencode`'s potential misformatting.
These steps ensure the JSON string from Terraform's `jsonencode` is PowerShell-parsable, preventing errors or unexpected behavior.
#>
#>
param(
[parameter(Mandatory = $true)]
[string]$MaintenanceConfigurationId,
[parameter(Mandatory = $true)]
[string]$NextStagePropertiesJson
)
# Remove newline characters from the JSON string
$NextStagePropertiesJson = $NextStagePropertiesJson -replace '\\', ''
# Now, convert the cleaned JSON string to an object
try {
$NextStageProperties = $NextStagePropertiesJson | ConvertFrom-Json
# Proceed with using $NextStageProperties...
}
catch {
Write-Host "Failed to parse JSON: $_"
}
#$ErrorActionPreference = "Stop"
$NextStageProperties = $NextStagePropertiesJson | ConvertFrom-Json
Connect-AzAccount -Identity
$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" } | ForEach-Object { "$($_.Id)"}
Write-Output "Getting the latest maintenance configuration execution run..."
$argQuery = @"
patchinstallationresources
| where type endswith '/patchinstallationresults'
| extend maintenanceRunId=tolower(split(properties.maintenanceRunId,'/providers/microsoft.maintenance/applyupdates')[0])
| where maintenanceRunId =~ '$MaintenanceConfigurationId'
| top 1 by todatetime(properties.lastModifiedDateTime)
| project lastRunDateTime = datetime_add('Hour',-12,todatetime(properties.lastModifiedDateTime))
"@
$lastRunDateTime = Search-AzGraph -Query $argQuery -Subscription $subscriptions
if ($lastRunDateTime.Data -and $lastRunDateTime.GetType().Name -like "PSResourceGraphResponse*")
{
$lastRunDateTime = $lastRunDateTime.Data
}
if ($lastRunDateTime[0])
{
Write-Output "Latest run for maintenance configuration $MaintenanceConfigurationId ended at $($lastRunDateTime[0].lastRunDateTime.AddHours(12).ToString("u"))"
}
else
{
throw "No maintenance configuration runs found for $MaintenanceConfigurationId"
}
$ARGPageSize = 1000
$installedPackages = @()
$resultsSoFar = 0
Write-Output "Querying for packages to install..."
$argQuery = @"
patchinstallationresources
| where type endswith '/patchinstallationresults'
| extend maintenanceRunId=tolower(split(properties.maintenanceRunId,'/providers/microsoft.maintenance/applyupdates')[0])
| where maintenanceRunId =~ '$MaintenanceConfigurationId'
| where todatetime(properties.lastModifiedDateTime) > todatetime('$($lastRunDateTime[0].lastRunDateTime.ToString("u"))')
| extend vmId = tostring(split(tolower(id), '/patchinstallationresults/')[0])
| extend osType = tostring(properties.osType)
| extend lastDeploymentStart = tostring(properties.startDateTime)
| extend deploymentStatus = tostring(properties.status)
| join kind=inner (
patchinstallationresources
| where type endswith '/patchinstallationresults/softwarepatches'
| where todatetime(properties.lastModifiedDateTime) > todatetime('$($lastRunDateTime[0].lastRunDateTime.ToString("u"))')
| extend vmId = tostring(split(tolower(id), '/patchinstallationresults/')[0])
| extend patchName = tostring(properties.patchName)
| extend patchVersion = tostring(properties.version)
| extend kbId = tostring(properties.kbId)
| extend installationState = tostring(properties.installationState)
| project vmId, installationState, patchName, patchVersion, kbId
) on vmId
| join kind=inner (
resources
| where type == 'microsoft.maintenance/maintenanceconfigurations'
| extend maintenanceDuration = tostring(properties.maintenanceWindow.duration)
| extend rebootSetting = tostring(properties.installPatches.rebootSetting)
| project maintenanceRunId=tolower(id), maintenanceDuration, rebootSetting, location, mcTags=tostring(tags)
) on maintenanceRunId
| where installationState == 'Installed'
| distinct osType, lastDeploymentStart, maintenanceDuration, patchName, patchVersion, kbId, rebootSetting, location, mcTags
"@
do
{
if ($resultsSoFar -eq 0)
{
$packages = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions
}
else
{
$packages = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions
}
if ($packages -and $packages.GetType().Name -eq "PSResourceGraphResponse")
{
$packages = $packages.Data
}
$resultsCount = $packages.Count
$resultsSoFar += $resultsCount
$installedPackages += $packages
} while ($resultsCount -eq $ARGPageSize)
Write-Output "$($installedPackages.Count) packages were installed in the latest run for maintenance configuration $MaintenanceConfigurationId."
if ($installedPackages.Count -gt 0)
{
$lastDeploymentDate = ($installedPackages | Select-Object -Property lastDeploymentStart -Unique -First 1).lastDeploymentStart
$maintenanceConfLocation = ($installedPackages | Select-Object -Property location -Unique -First 1).location
$maintenanceDuration = ($installedPackages | Select-Object -Property maintenanceDuration -Unique -First 1).maintenanceDuration
$rebootSetting = ($installedPackages | Select-Object -Property rebootSetting -Unique -First 1).rebootSetting
$tags = ($installedPackages | Select-Object -Property mcTags -Unique -First 1).mcTags
$windowsPackages = ($installedPackages | Where-Object { $_.osType -eq "Windows" -and -not([string]::isNullOrEmpty($_.kbId))} | Select-Object -Property kbId -Unique).kbId
$windowsPackageNames = ($installedPackages | Where-Object { $_.osType -eq "Windows" } | Select-Object -Property patchName -Unique).patchName
$kbNumbersToInclude = "[ ]"
if ($windowsPackages)
{
if ($windowsPackages.Count -eq 1)
{
$kbNumbersToInclude = '[ "' + $windowsPackages + '" ]'
}
else
{
$kbNumbersToInclude = $windowsPackages | ConvertTo-Json
}
}
$linuxPatches = ($installedPackages | Where-Object { $_.osType -eq "Linux" } | Select-Object -Property patchName -Unique).patchName
$packageNameMasksToInclude = "[ ]"
$linuxPackages = @()
foreach ($linuxPatch in $linuxPatches)
{
$linuxPatchVersion = ($installedPackages | Where-Object { $_.osType -eq "Linux" -and $_.patchName -eq $linuxPatch } | Select-Object -Property patchVersion -Unique | Sort-Object -Property patchVersion -Descending | Select-Object -First 1).patchVersion
$linuxPackage = "$linuxPatch=$linuxPatchVersion"
$linuxPackages += $linuxPackage
}
if ($linuxPackages.Count -eq 1)
{
$packageNameMasksToInclude = '[ "' + $linuxPackages + '" ]'
}
else
{
if ($linuxPackages.Count -gt 1)
{
$packageNameMasksToInclude = $linuxPackages | ConvertTo-Json
}
}
Write-Output "Creating $($NextStageProperties.Count) maintenance stages using $($lastDeploymentDate.ToString('u')) as the reference date..."
foreach ($stageProperties in $NextStageProperties)
{
if (-not([string]::IsNullOrEmpty($stageProperties.offsetTimeSpan)))
{
$offsetTS = [Xml.XmlConvert]::ToTimeSpan($stageProperties.offsetTimeSpan)
$stageDayOfWeek = $lastDeploymentDate.Add($offsetTS).DayOfWeek
$stageStartTime = $lastDeploymentDate.Add($offsetTS).ToString("u").Substring(0,16)
$stageEndTime = $lastDeploymentDate.Add($offsetTS).AddHours(5).ToString("u").Substring(0,16)
}
elseif ($stageProperties.offsetDays -gt 0)
{
$stageDayOfWeek = $lastDeploymentDate.AddDays($stageProperties.offsetDays).DayOfWeek
$stageStartTime = $lastDeploymentDate.AddDays($stageProperties.offsetDays).ToString("u").Substring(0,16)
$stageEndTime = $lastDeploymentDate.AddDays($stageProperties.offsetDays).AddHours(5).ToString("u").Substring(0,16)
}
else
{
throw "No offset time span or days defined for stage $($stageProperties.stageName)"
}
$offsetTS = $stageProperties.offsetTimeSpan
$maintenanceConfName = $stageProperties.stageName
$maintenanceConfSubId = $MaintenanceConfigurationId.Split("/")[2]
$maintenanceConfRG = $MaintenanceConfigurationId.Split("/")[4]
$maintenanceConfDeploymentTemplateJson = @"
{
`"`$schema`": `"http://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#`",
`"contentVersion`": `"1.0.0.0`",
`"resources`": [
{
`"type`": `"Microsoft.Maintenance/maintenanceConfigurations`",
`"apiVersion`": `"2023-04-01`",
`"name`": `"$($maintenanceConfName)`",
`"location`": `"$maintenanceConfLocation`",
`"tags`": $tags,
`"properties`": {
`"maintenanceScope`": `"InGuestPatch`",
`"installPatches`": {
`"linuxParameters`": {
`"classificationsToInclude`": null,
`"packageNameMasksToExclude`": null,
`"packageNameMasksToInclude`": $packageNameMasksToInclude
},
`"windowsParameters`": {
`"classificationsToInclude`": null,
`"kbNumbersToExclude`": null,
`"kbNumbersToInclude`": $kbNumbersToInclude
},
`"rebootSetting`": `"$rebootSetting`"
},
`"extensionProperties`": {
`"InGuestPatchMode`": `"User`"
},
`"maintenanceWindow`": {
`"startDateTime`": `"$stageStartTime`",
`"duration`": `"$maintenanceDuration`",
`"timeZone`": `"UTC`",
`"expirationDateTime`": `"$stageEndTime`",
`"recurEvery`": `"1Week $stageDayOfWeek`"
}
}
}
]
}
"@
Write-Output "Creating/updating $maintenanceConfName maintenance configuration to be run on $stageStartTime for the following packages:"
Write-Output $linuxPatches
Write-Output $windowsPackageNames
$deploymentNameTemplate = "{0}-" + (Get-Date).ToString("yyMMddHHmmss")
$templateFile = "./$deploymentNameTemplate.json"
Set-Content -Path $templateFile -Value $maintenanceConfDeploymentTemplateJson
if ((Get-AzContext).Subscription.Id -ne $maintenanceConfSubId)
{
Select-AzSubscription -SubscriptionId $maintenanceConfSubId | Out-Null
}
New-AzResourceGroupDeployment -TemplateFile $templateFile -ResourceGroupName $maintenanceConfRG -Name ($deploymentNameTemplate -f $maintenanceConfName) | Out-Null
Write-Output "Maintenance configuration deployed."
foreach ($scope in $stageProperties.scope)
{
$assignmentName = "$($maintenanceConfName)dynamicassignment1"
$maintenanceConfAssignApiPath = "$scope/providers/Microsoft.Maintenance/configurationAssignments/$($assignmentName)?api-version=2023-04-01"
$maintenanceConfAssignApiBody = @"
{
"properties": {
"maintenanceConfigurationId": "/subscriptions/$maintenanceConfSubId/resourceGroups/$maintenanceConfRG/providers/Microsoft.Maintenance/maintenanceConfigurations/$maintenanceConfName",
"resourceId": "$scope",
"filter": $($stageProperties.filter | ConvertTo-Json -Depth 3)
}
}
"@
Write-Output "Creating/updating $assignmentName maintenance configuration assignment for scope $scope..."
$response = Invoke-AzRestMethod -Path $maintenanceConfAssignApiPath -Method PUT -Payload $maintenanceConfAssignApiBody
if ($response.StatusCode -eq 200)
{
Write-Output "Maintenance configuration assignment created/updated."
}
else
{
Write-Output "Maintenance configuration assignment creation/update failed (HTTP $($response.StatusCode))."
Write-Output $response.Content
throw "Maintenance configuration assignment creation/update failed (HTTP $($response.StatusCode))."
}
}
}
}
else
{
Write-Output "No need to create further maintenance stages"
}