-
Notifications
You must be signed in to change notification settings - Fork 38
/
SnapImage.ps1
385 lines (354 loc) · 15.3 KB
/
SnapImage.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
<#
.SYNOPSIS
This script automates the process of creating an image of an Azure VM without destroying the source, or reference VM.
.DESCRIPTION
This script automates the process of creating an image from an Azure VM without destroying it during the capture process.
At a high-level, the following steps are taken:
Snapshot of source "reference" VM > create a temp "capture" Resource Group > Create an OS disk from snapshot >
create a VNet and VM in the capture RG > sysprep the VM with a Custom Script Extension > capture the VM >
If using Azure Compute Gallery, add image to the gallery
If not using Azure Compute Gallery, add image to reference VM Resource Group
> remove capture Resource Group > remove snapshot
*Requires the powershell AZ module
*log into the target Azure subscription before running the script
.PARAMETER refVmName
The name of the reference, or source VM used to build the image.
.PARAMETER refVmRg
The name of the reference VM resource Group, also used for the location
.PARAMETER cseURI
Optional, the URI for the Sysprep Custom Script Extension. Default value is located on a public GitHub repo.
No guaranty on availability. Recommend copying the file to your own location. The file must be
publicly available. Looking for something with more PowerShell Options? Check out Image Builder.
https://youtube.com/playlist?list=PLnWpsLZNgHzWeiHA_wG0xuaZMlk1Nag7E
.PARAMETER galDeploy
Optional, indicates if the image will go to an Azure Compute Gallery.
.PARAMETER galName
Required if -galDeploy is used. The name of the Azure Compute Gallery.
.PARAMETER galDefName
Required if -galDeploy is used. The Image Definition name in the Azure Compute Gallery
Be sure the hardware version (Gen1 or Gen2) match.
.PARAMETER delSnap
Optional, indicates if the source snapshot of the reference computer will be
deleted as part of the cleanup process.
.NOTES
## Script is offered as-is with no warranty, expressed or implied. ##
## Test it before you trust it! ##
## Please see the list below before running the script: ##
1. This script assumes the VM's, resource groups and Azure Compute Gallery, if used, are in the same region.
2. If the script fails, you will need to manually clean up artifacts created (remove snapshot and capture Resource Group).
3. Update the reference VM or disable updates. Sysprep won't run with updates pending.
4. The script will create a new, temporary "Capture" resource group and delete it once finished.
5. The public IP and NSG is not required and can be commented out (update the NIC config also). It's helpful for troubleshooting.
Author : Travis Roberts, Ciraltos llc
Website : www.ciraltos.com
Version : 1.0.0.0 Initial Build 3/12/2022
.EXAMPLE
Create an image and add it to the source computers resource group:
.\SnapImage.ps1 -refVmName "<ComputerName>" -refVmRg '<RGName>'
Create an image and add it to an Azure Compute Gallery:
.\SnapImage.ps1 -refVmName "<ComputerName>" -refVmRg '<RGName>' -galDeploy -galName '<Azure Compute Gallery>' -galDefName '<Image Definition>'
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)][string]$refVmName,
[Parameter(Mandatory = $true)][string]$refVmRg,
[Parameter(Mandatory = $false)][string]$cseURI = 'https://raw.githubusercontent.com/tsrob50/WVD-Public/master/SysprepCSE.ps1',
[Parameter(Mandatory = $false)][switch]$galDeploy = $false,
[Parameter(Mandatory = $false)][string]$galName,
[parameter(Mandatory = $false)][string]$galDefName,
[parameter(Mandatory = $false)][string]$delSnap = $true
)
#Validate the Azure Compute Gallery settings were added correctly if used
Try {
if ($galDeploy -eq $true) {
$gallery = Get-AzGallery -ErrorAction Stop -Name $galName
$galleryDef = Get-AzGalleryImageDefinition -ErrorAction Stop -ResourceGroupName $gallery.ResourceGroupName -GalleryName $galName -GalleryImageDefinitionName $galDefName
}
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error with Azure Compute Gallery Settings ' + $ErrorMessage)
Break
}
#Set the date, used as unique ID for artifacts and image version
$date = (get-date -Format yyyyMMddHHmm)
#Set the image name, modify as needed
#Default based off reference computer name and date
$imageName = ($refVmName + 'Image' + $date)
#Set the image version (Name)
#Used if adding the image to an Azure Compute Gallery
#Format is 0.yyyyMM.ddHHmm date format for the version to keep unique and increment each new image version
$imageVersion = '0.' + $date.Substring(0, 6) + '.' + $date.Substring(6, 6)
#Disable breaking change warning message
Set-Item -Path Env:\SuppressAzurePowerShellBreakingChangeWarnings -Value $true
#Set the location, based on the reference computer resource group location
$location = (Get-AzResourceGroup -Name $refVmRg).Location
##### Start Script #####
#region Create Snapshot of reference VM
try {
Write-Host "Creating a snapshot of $refVmName"
$vm = Get-AzVM -ErrorAction Stop -ResourceGroupName $refVmRg -Name $refVmName
$snapshotConfig = New-AzSnapshotConfig -ErrorAction Stop -SourceUri $vm.StorageProfile.OsDisk.ManagedDisk.Id -Location $vm.Location -CreateOption copy -SkuName Standard_LRS
$snapshot = New-AzSnapshot -ErrorAction Stop -Snapshot $snapshotConfig -SnapshotName "$refVmName$date" -ResourceGroupName $refVmRg
}
catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating snapshot from reference computer ' + $ErrorMessage)
Break
}
#For testing
#Get the snapshot $snapshot = Get-AzSnapshot -ResourceGroupName $refVmRg
#endregion
#region Create a resource group for the reference VM
Try {
Write-Host "Creating the capture Resource Group"
$capVmRg = New-AzResourceGroup -Name ($refVmName + $date + "_ImageRG") -Location $location
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating the resource group ' + $ErrorMessage)
Break
}
#endregion
#region Create a VM from the snapshot
#Create the managed disk from the Snapshot
Try {
$osDiskConfig = @{
ErrorAction = 'Stop'
Location = $location
CreateOption = 'copy'
SourceResourceID = $snapshot.Id
}
write-host "creating the OS disk form the snapshot"
$osDisk = New-AzDisk -ErrorAction Stop -DiskName 'TempOSDisk' -ResourceGroupName $capVmRg.ResourceGroupName -disk (New-AzDiskConfig @osDiskConfig)
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating the managed disk ' + $ErrorMessage)
Break
}
#Create a new VNet
Try {
Write-Host "Creating the VNet and Subnet"
$singleSubnet = New-AzVirtualNetworkSubnetConfig -ErrorAction Stop -Name ('tempSubnet' + $date) -AddressPrefix '10.0.0.0/24'
$vnetConfig = @{
ErrorAction = 'Stop'
Name = ('tempSubnet' + $date)
ResourceGroupName = $capVmRg.ResourceGroupName
Location = $location
AddressPrefix = "10.0.0.0/16"
Subnet = $singleSubnet
}
$vnet = New-AzVirtualNetwork @vnetConfig
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating the temp VNet ' + $ErrorMessage)
Break
}
#Create the NSG
Try {
$nsgRuleConfig = @{
Name = 'myRdpRule'
ErrorAction = 'Stop'
Description = 'Allow RDP'
Access = 'allow'
Protocol = 'Tcp'
Direction = 'Inbound'
Priority = '110'
SourceAddressPrefix = 'Internet'
SourcePortRange = '*'
DestinationAddressPrefix = '*'
DestinationPortRange = '3389'
}
write-host "Creating the NSG"
$rdpRule = New-AzNetworkSecurityRuleConfig @nsgRuleConfig
$nsg = New-AzNetworkSecurityGroup -ErrorAction Stop -ResourceGroupName $capVmRg.ResourceGroupName -Location $location -Name 'tempNSG' -SecurityRules $rdpRule
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating the NSG ' + $ErrorMessage)
Break
}
#Create the public IP address
Try {
Write-Host "Creating the public IP address"
$pip = New-AzPublicIpAddress -ErrorAction Stop -Name 'tempPip' -ResourceGroupName $capVmRg.ResourceGroupName -Location $location -AllocationMethod Dynamic
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating the public IP address ' + $ErrorMessage)
Break
}
#Create the NIC
Try {
$nicConfig = @{
ErrorAction = 'Stop'
Name = 'tempNic'
ResourceGroupName = $capVmRg.ResourceGroupName
Location = $location
SubnetId = $vnet.Subnets[0].Id
PublicIpAddressId = $pip.Id
NetworkSecurityGroupId = $nsg.Id
}
Write-Host "Creating the NIC"
$nic = New-AzNetworkInterface @nicConfig
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating the NIC ' + $ErrorMessage)
Break
}
#Create and start the VM the VM
Try {
Write-Host "Creating the temporary capture VM, this will take a couple minutes"
$capVmName = ('tempVM' + $date)
$CapVmConfig = New-AzVMConfig -ErrorAction Stop -VMName $CapVmName -VMSize $vm.HardwareProfile.VmSize
$capVm = Add-AzVMNetworkInterface -ErrorAction Stop -vm $CapVmConfig -id $nic.Id
$capVm = Set-AzVMOSDisk -vm $CapVm -ManagedDiskId $osDisk.id -StorageAccountType Standard_LRS -DiskSizeInGB 128 -CreateOption Attach -Windows
$capVM = Set-AzVMBootDiagnostic -vm $CapVm -disable
$capVm = new-azVM -ResourceGroupName $capVmRg.ResourceGroupName -Location $location -vm $capVm -DisableBginfoExtension
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating the VM ' + $ErrorMessage)
Break
}
#endregion
#region Sysprep the new capture VM (capVm)
#Wait for VM to be ready, display status = VM running
$displayStatus = ""
$count = 0
while ($displayStatus -notlike "VM running") {
Write-Host "Waiting for the VM display status to change to VM running"
$displayStatus = (get-azvm -Name $capVmName -ResourceGroupName $capVmRg.ResourceGroupName -Status).Statuses[1].DisplayStatus
write-output "starting 30 second sleep"
start-sleep -Seconds 30
$count += 1
if ($count -gt 7) {
Write-Error "five minute wait for VM to start ended, canceling script"
Exit
}
}
#Run Sysprep from a Custom Script Extension
try {
$cseSettings = @{
ErrorAction = 'Stop'
FileUri = $cseURI
ResourceGroupName = $capVmRg.ResourceGroupName
VMName = $CapVmName
Name = "Sysprep"
location = $location
Run = './SysprepCSE.ps1'
}
Write-Host "Running the Sysprep custom script extension"
Set-AzVMCustomScriptExtension @cseSettings | Out-Null
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error running the Sysprep Custom Script Extension ' + $ErrorMessage)
Break
}
<# For testing
$status = Get-AzVMDiagnosticsExtension -ResourceGroupName $capVmRg.ResourceGroupName -VMName $capVmName -name "Sysprep" -status
$status.SubStatuses.message
#>
#endregion
#region Capture VM image
#Deallocate the VM
#Wait for Sysprep to finish, shuts down the VM once finished
$displayStatus = ""
$count = 0
Try {
while ($displayStatus -notlike "VM stopped") {
Write-Host "Waiting for the VM display status to change to VM stopped"
$displayStatus = (get-azvm -ErrorAction Stop -Name $capVmName -ResourceGroupName $capVmRg.ResourceGroupName -Status).Statuses[1].DisplayStatus
write-output "starting 15 second sleep"
start-sleep -Seconds 15
$count += 1
if ($count -gt 11) {
Write-Error "Three minute wait for VM to stop ended, canceling script. Verify no updates are required on the source"
Exit
}
}
Write-Host "Deallocating the VM and setting to Generalized"
Stop-AzVM -ErrorAction Stop -ResourceGroupName $capVmRg.ResourceGroupName -Name $capVmName -Force | Out-Null
Set-AzVM -ErrorAction Stop -ResourceGroupName $capVmRg.ResourceGroupName -Name $capVmName -Generalized | Out-Null
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error deallocating the VM ' + $ErrorMessage)
Break
}
#Create the image from the VM
#Place the image in the reference computer Resource Group if $galDeploy is set to $false
#Place in temporary capture Resource Group if $galDeploy is set to $true
Try {
Write-Host "Capturing the VM image"
$capVM = Get-AzVM -ErrorAction Stop -Name $capVmName -ResourceGroupName $capVmRg.ResourceGroupName
$vmGen = (Get-AzVM -ErrorAction Stop -Name $capVmName -ResourceGroupName $capVmRg.ResourceGroupName -Status).HyperVGeneration
$image = New-AzImageConfig -ErrorAction Stop -Location $location -SourceVirtualMachineId $capVm.Id -HyperVGeneration $vmGen
if ($galDeploy -eq $true) {
Write-Host "Azure Compute Gallery used, saving image to the capture VM Resource Group"
$image = New-AzImage -Image $image -ImageName $imageName -ResourceGroupName $capVmRg.ResourceGroupName
}
elseif ($galDeploy -eq $false) {
Write-Host "Azure Compute Gallery not used, saving image to the reference VM Resource Group"
New-AzImage -Image $image -ImageName $imageName -ResourceGroupName $refVmRg | Out-Null
}
else {
Write-Error 'Please set galDeploy to $true or $false'
}
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error creating the image ' + $ErrorMessage)
Break
}
#Add image to the Azure Compute Gallery if that option was selected
Try {
if ($galDeploy -eq $true) {
Write-Host 'Adding image to the Azure Compute Gallery, this can take a few minutes'
$imageSettings = @{
ErrorAction = 'Stop'
ResourceGroupName = $gallery.ResourceGroupName
GalleryName = $gallery.Name
GalleryImageDefinitionName = $galDefName
Name = $imageVersion
Location = $gallery.Location
SourceImageId = $image.Id
}
$GalImageVer = New-AzGalleryImageVersion @imageSettings
Write-Host "Image version $($GalImageVer.Name) added to the image definition"
}
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error adding the image to the Azure Compute Gallery ' + $ErrorMessage)
Break
}
#endregion
#region Remove the capture computer RG
Try {
Write-Host "Removing the capture Resource Group $($capVmRg.ResourceGroupName)"
Remove-AzResourceGroup -ErrorAction Stop -Name $capVmRg.ResourceGroupName -Force | Out-Null
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error removing resource group ' + $ErrorMessage)
Break
}
#Remove the snapshot (Optional)
#Removes reference computer snapshot if $delSnap is set to $true
if ($delSnap -eq $true) {
Try {
Write-Host "Removing the snapshot $($snapshot.Name)"
Remove-AzSnapshot -ErrorAction Stop -ResourceGroupName $refVmRg -SnapshotName $snapshot.Name -Force | Out-Null
}
Catch {
$ErrorMessage = $_.Exception.message
Write-Error ('Error removing the snapshot ' + $ErrorMessage)
Break
}
}
#endregion