├── AzureSnapFunctions.ps1 ├── AzureStorageFunctions.ps1 └── README.md /AzureSnapFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | A set of functions to create, delete or revert to a snapshot of an Azure RM VM 4 | .DESCRIPTION 5 | A detailed description of the function or script. This keyword can be 6 | used only once in each topic. 7 | .NOTES 8 | File Name : AzureSnapFunctions.ps1 9 | Author : Dave Hall 10 | Prerequisite : PowerShell V5 (Tested with V5, may work in earlier) 11 | AzureRM Powershell Module (Install-Module AzureRM) 12 | Copyright 2016 - Dave Hall 13 | .LINK 14 | http://superautomation.blogspot.com 15 | .EXAMPLE 16 | Example 1 17 | .EXAMPLE 18 | Example 2 19 | #> 20 | 21 | Import-Module AzureRM 22 | #Login-AzureRMAccount 23 | . "$($PSScriptRoot)\AzureStorageFunctions.PS1" 24 | 25 | Function New-AzureRmVmSnap { 26 | Param( 27 | [Parameter(Mandatory=$true)]$VMName, 28 | [Parameter(Mandatory=$true)]$SnapshotName, 29 | $SnapshotDescription="", 30 | [switch]$Force=$False 31 | ) 32 | 33 | Write-Host "Create Snapshot for VM: " -ForegroundColor Yellow -NoNewline 34 | Write-Host $VMName -ForegroundColor Cyan 35 | 36 | $VM = Get-AzureRmVm | Where-Object {$_.Name -eq $VMName} 37 | if ($VM) { 38 | 39 | #Warn if VM OS disk is on Premium storage and break 40 | if (Test-Premium $VM.StorageProfile.OsDisk.Vhd.Uri) { 41 | Write-Warning "Premium storage is not currently supported by this function" 42 | Break 43 | } 44 | 45 | #Warn if VM is running and break 46 | $VMState = $VM | Get-AzureRmVm | Get-AzureRmVm -Status | 47 | select Name, @{n="Status"; e={$_.Statuses[1].DisplayStatus}} 48 | 49 | if ($VMState.Status -eq "VM Running") { 50 | #Stop the VM if force enabled 51 | if ($Force) { 52 | Write-Host "Stopping VM..." 53 | $Stopped = $VM | Stop-AzureRmVm -Force 54 | } else { 55 | #Show warning 56 | Write-Warning "VM is currently running, use -force to stop the VM" 57 | Break 58 | } 59 | } 60 | 61 | $SnapshotGUID = New-GUID 62 | 63 | $DiskUriList=@() 64 | $DiskUriList += $VM.StorageProfile.OsDisk.Vhd.Uri 65 | Foreach ($Disk in $VM.StorageProfile.DataDisks) { 66 | $DiskUriList += $Disk.Vhd.Uri 67 | } 68 | 69 | $BaseStorageContext = Get-StorageContextForUri -DiskUri $VM.StorageProfile.OsDisk.Vhd.Uri 70 | 71 | Foreach ($DiskUri in $DiskUriList) { 72 | Write-Host "Snapshot disk: " -ForegroundColor Yellow -NoNewline 73 | Write-Host $DiskUri -ForegroundColor Cyan 74 | 75 | $DiskInfo = Get-DiskInfo $DiskUri 76 | 77 | $StorageContext = Get-StorageContextForUri -DiskUri $DiskUri 78 | 79 | $DiskBlob = Get-AzureStorageBlob -Container $DiskInfo.ContainerName ` 80 | -Context $StorageContext -Blob $DiskInfo.VHDName 81 | 82 | $Snapshot = $DiskBlob.ICloudBlob.CreateSnapshot() 83 | 84 | Write-SnapInfo -VMName $VMName -SnapGUID $SnapshotGUID ` 85 | -PrimaryUri $DiskInfo.Uri.ToString() ` 86 | -SnapshotUri $Snapshot.SnapshotQualifiedStorageUri.PrimaryUri.AbsoluteUri.ToString() ` 87 | -StorageContext $BaseStorageContext -DiskNum $DiskUriList.IndexOf($DiskUri) ` 88 | -SnapshotName $SnapshotName -SnapshotDescription $SnapshotDescription 89 | 90 | } 91 | 92 | if ($VMState.Status -eq "VM Running") { 93 | Write-Host "Restarting VM..." 94 | $Started = $VM | Start-AzureRmVm 95 | } 96 | 97 | Retrieve-SnapInfo -VMName $VMName -SnapGUID $SnapshotGUID -StorageContext $StorageContext 98 | } else { 99 | Write-Host "Unable to get VM" 100 | } 101 | } 102 | 103 | Function Get-AzureRmVmSnap { 104 | Param( 105 | [Parameter(Mandatory=$true)]$VMName, 106 | $SnapshotGUID, 107 | [switch]$GetBlobs=$False 108 | ) 109 | $VM = Get-AzureRmVm | Where-Object {$_.Name -eq $VMName} 110 | if ($VM) { 111 | $OSDiskUri += $VM.StorageProfile.OsDisk.Vhd.Uri 112 | $StorageContext = Get-StorageContextForUri -DiskUri $OSDiskUri 113 | 114 | if ($SnapshotGUID) { 115 | $SnapInfo = Retrieve-SnapInfo -VMName $VMName -SnapGUID $SnapshotGUID -StorageContext $StorageContext 116 | } else { 117 | $SnapInfo = Retrieve-SnapInfo -VMName $VMName -StorageContext $StorageContext 118 | } 119 | 120 | if ($GetBlobs) { 121 | $SnapshotBlobs = Get-AzureRmVmSnapBlobs -VMName $VMName 122 | Foreach ($SnapInfo in $SnapInfo) { 123 | $SnapshotBlobs | ? {$_.ICloudBlob.SnapshotQualifiedStorageUri.PrimaryUri.AbsoluteUri.ToString() -eq $SnapInfo.SnapshotUri.ToString()} 124 | } 125 | } else { 126 | $SnapInfo 127 | } 128 | } 129 | } 130 | 131 | Function Delete-AzureRmVmSnap { 132 | Param ( 133 | [Parameter(Mandatory=$true)]$VMName, 134 | [switch]$Force=$False, 135 | $SnapshotGuid 136 | ) 137 | $VM = Get-AzureRmVm | Where-Object {$_.Name -eq $VMName} 138 | if ($VM) { 139 | $OSDiskUri += $VM.StorageProfile.OsDisk.Vhd.Uri 140 | $StorageContext = Get-StorageContextForUri $OSDiskUri 141 | $SnapInfo = Retrieve-SnapInfo -VMName $VMName -StorageContext $StorageContext 142 | $UniqueGuids = $SnapInfo | ForEach-Object {$_.SnapGuid} | Sort-Object -Unique 143 | if ($SnapshotGuid) {$UniqueGuids = $UniqueGuids | Where-Object {$_ -eq $SnapshotGuid}} 144 | if ($UniqueGuids -ne $null -or $UniqueGuids.Count -gt 0) { 145 | Foreach ($Guid in $UniqueGuids) { 146 | $SnapInfo | Where-Object {$_.SnapGUID -eq $Guid} 147 | $SnapBlobs = Get-AzureRmVmSnap -VMName $VMName -SnapshotGUID $Guid -GetBlobs 148 | if ($SnapBlobs) { 149 | if (!($Force)) { 150 | $Delete = Read-Host "$($SnapBlobs.Count) Disk(s) in this snapshot set - Delete this snap? [y/N]: " 151 | } 152 | if ($Delete -eq "y" -or $Force) { 153 | Try { 154 | $SnapBlobs | ForEach-Object {$_.ICloudBlob.Delete()} 155 | } catch { 156 | Write-Error "Unable to delete the snapshot $Guid" 157 | } finally { 158 | Clear-SnapInfo -SnapGUID $Guid -StorageContext $StorageContext 159 | } 160 | } 161 | } else { 162 | Write-Error "No snapshot blobs exist for snapshot GUID: $($Guid), Snapshot table is out of sync with actual snapshot blobs" 163 | } 164 | } 165 | } else { 166 | Write-Warning "Snapshot not found" 167 | } 168 | } else { 169 | Write-Host "Unable to find VM" 170 | } 171 | } 172 | 173 | Function Revert-AzureRmVmSnap { 174 | Param ( 175 | [Parameter(Mandatory=$true)]$VMName, 176 | [switch]$Force=$False, 177 | $SnapshotGuid 178 | ) 179 | $VM = Get-AzureRmVm | Where-Object {$_.Name -eq $VMName} 180 | if ($VM) { 181 | #Get user selected snapshot 182 | $OSDiskUri += $VM.StorageProfile.OsDisk.Vhd.Uri 183 | $StorageContext = Get-StorageContextForUri $OSDiskUri 184 | $SnapInfo = Retrieve-SnapInfo -VMName $VMName -StorageContext $StorageContext 185 | $UniqueGuids = $SnapInfo | Foreach-Object {$_.SnapGuid} | Sort-Object -Unique 186 | if ($SnapshotGuid) {$UniqueGuids = $UniqueGuids | Where-Object {$_ -eq $SnapshotGuid}} 187 | Foreach ($Guid in $UniqueGuids) { 188 | $SnapBlobs = Get-AzureRmVmSnap -VMName $VMName -SnapshotGUID $Guid -GetBlobs 189 | if ($Force -and $UniqueGuids.Count -eq 1) { 190 | $Revert = "y" 191 | } else { 192 | if ($Force) { 193 | Write-Warning ` 194 | "-Force only works with a given GUID or if there is only 1 snapshot set for this VM" 195 | Break 196 | } 197 | $SnapInfo | Where-Object {$_.SnapGUID -eq $Guid} 198 | $Revert = Read-Host "$($SnapBlobs.Count) Disks in this snapshot set - Revert to this snap? [y/N]: " 199 | } 200 | if ($Revert -eq "y") { 201 | Write-Host "Reverting to snapshot with GUID: " -ForegroundColor Yellow -NoNewLine 202 | Write-Host $Guid -ForegroundColor Cyan 203 | 204 | #Shut down the VM 205 | Try { 206 | Write-Host "Stopping the VM..." 207 | $VM | Stop-AzureRmVm -Force 208 | } catch { 209 | Write-Error "Failed to stop the VM" 210 | Break 211 | } 212 | 213 | #Back up the VM config in case something goes wrong - recovery function not working yet 214 | #TODO: Recovery Function 215 | if (!(Test-Path ".\VM-Backups")) { 216 | $Null = New-Item -Path ".\VM-Backups" -ItemType "Directory" 217 | } 218 | Export-Clixml -Path ".\VM-Backups\$($VM.Name).Original.xml" -InputObject $VM 219 | 220 | #Remove the VM config 221 | Try { 222 | Write-Host "Removing the VM configuration..." 223 | $VM | Remove-AzureRmVm -Force 224 | } catch { 225 | Write-Error "Failed to remove the VM config" 226 | if (Get-AzureRmVm | Where-Object {$_.Name -eq $VMName}) { 227 | Write-Host "VM Still exists, exiting..." 228 | Break 229 | } else { 230 | Write-Host "VM was actually removed, continuting..." 231 | } 232 | } 233 | 234 | Try { 235 | Write-Host "Reverting the snapshot..." 236 | Foreach ($SnapBlob in $SnapBlobs) { 237 | $ThisSnap = $SnapInfo | ? {$_.SnapGUID -eq $guid -and $_.SnapshotUri -eq ` 238 | $SnapBlob.ICloudBlob.SnapshotQualifiedStorageUri.PrimaryUri.AbsoluteUri.ToString()} 239 | Write-Host "Reverting disk $($ThisSnap.DiskNum)..." 240 | $DiskInfo = Get-DiskInfo -DiskUri $SnapBlob.ICloudBlob.Uri.OriginalString 241 | $OriginalDiskBlob = Get-AzureStorageBlob -Container $DiskInfo.ContainerName ` 242 | -Context $StorageContext | 243 | Where-Object {$_.Name -eq $DiskInfo.VHDName ` 244 | -and -not $_.ICloudBlob.IsSnapshot ` 245 | -and $_.SnapshotTime -eq $null 246 | } 247 | $Copy = $OriginalDiskBlob.ICloudBlob.StartCopy($SnapBlob.ICloudBlob) 248 | Write-Host "Reversion Compete for disk $($ThisSnap.DiskNum) - Copy ID: $($Copy)" 249 | } 250 | 251 | } catch { 252 | Write-Error "Failed to revert the snapshot, recreating VM with current disk" 253 | } 254 | 255 | #Remove disallowed settings 256 | $osType = $VM.StorageProfile.OsDisk.OsType 257 | $VM.StorageProfile.OsDisk.OsType = $null 258 | $VM.StorageProfile.ImageReference = $Null 259 | $VM.OSProfile = $null 260 | 261 | #Old VM Information 262 | $rgName=$VM.ResourceGroupName 263 | $locName=$VM.Location 264 | $osDiskUri=$VM.StorageProfile.OsDisk.Vhd.Uri 265 | $diskName=$VM.StorageProfile.OsDisk.Name 266 | $osDiskCaching = $VM.StorageProfile.OsDisk.Caching 267 | 268 | #Set the OS disk to attach 269 | #TODO: Replace -windows with correct OS 270 | $vm=Set-AzureRmVmOSDisk -VM $vm -VhdUri $osDiskUri -name $DiskName ` 271 |     -CreateOption attach -Windows -Caching $osDiskCaching 272 | 273 | #Attach data Disks 274 | if ($VM.StorageProfile.DataDisks.count -gt 0) { 275 | Write-Host "Configure additional disks" 276 | $DataDisks = $VM.StorageProfile.DataDisks 277 | $VM.StorageProfile.DataDisks = $Null 278 | foreach ($DataDisk in $DataDisks) { 279 | $VM = Add-AzureRmVmDataDisk -VM $VM -VhdUri $DataDisk.Vhd.Uri ` 280 | -Name $DataDisk.Name -CreateOption "Attach" ` 281 | -Caching $DataDisk.Caching -DiskSizeInGB $DataDisk.DiskSizeInGB ` 282 | -Lun $DataDisk.Lun 283 | } 284 | } 285 | 286 | #If this isn't set the VM will default to Windows and get stuck in the "Updating" state 287 | #Probably because -windows is set when adding the OS disk! 288 | $VM.StorageProfile.OsDisk.OsType = $osType 289 | 290 | #Recreate the VM 291 | Write-Host "`nRecreate the VM..." 292 | New-AzureRmVm -ResourceGroupName $rgName -Location $locName -VM $VM -WarningAction Ignore 293 | 294 | #Break out of the UniqueGuids loop 295 | Break 296 | } 297 | } 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /AzureStorageFunctions.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Helper functions for AzureSnapFunctions.ps1 4 | .NOTES 5 | File Name : AzureStorageFunctions.ps1 6 | Author : Dave Hall 7 | Prerequisite : PowerShell V5 (Tested, may work in earlier) 8 | AzureRM Powershell Module 9 | Copyright 2016 - Dave Hall 10 | .LINK 11 | http://superautomation.blogspot.com 12 | .EXAMPLE 13 | Example 1 14 | .EXAMPLE 15 | Example 2 16 | #> 17 | 18 | Function Get-StorageTable { 19 | Param( 20 | [Parameter(Mandatory=$true)]$TableName, 21 | [Parameter(Mandatory=$true)]$StorageContext 22 | ) 23 | $SnapTable = Get-AzureStorageTable -Context $StorageContext | 24 | ? {$_.CloudTable.Name -eq $TableName} 25 | if (!($SnapTable)) { 26 | $SnapTable = New-AzureStorageTable -Name $TableName -Context $StorageContext 27 | } 28 | return $SnapTable 29 | } 30 | 31 | Function Retrieve-SnapInfo { 32 | Param( 33 | $VMName, 34 | $SnapGUID, 35 | [Parameter(Mandatory=$true)]$StorageContext 36 | ) 37 | 38 | $TableName = "AzureVMSnapshots" 39 | $SnapTable = Get-StorageTable -TableName $TableName -StorageContext $StorageContext 40 | 41 | $query = New-Object "Microsoft.WindowsAzure.Storage.Table.TableQuery" 42 | if ($SnapGUID) { 43 | $Query.FilterString = "PartitionKey eq '$($SnapGUID)'" 44 | } 45 | 46 | $SnapInfo = $SnapTable.CloudTable.ExecuteQuery($query) 47 | foreach ($snap in $SnapInfo) { 48 | $OutputItem = "" | Select VMName, SnapshotName, DiskNum, SnapGUID, ` 49 | PrimaryUri, SnapshotUri, SnapshotDescription, SnapshotTime 50 | $OutputItem.SnapGUID = $Snap.PartitionKey 51 | $OutPutItem.DiskNum = $Snap.RowKey 52 | $OutputItem.VMName = $Snap.Properties.VMName.StringValue 53 | $OutputItem.PrimaryUri = $Snap.Properties.BaseURI.StringValue 54 | $OutputItem.SnapshotUri = $Snap.Properties.SnapshotURI.StringValue 55 | $OutputItem.SnapshotName = $Snap.Properties.SnapshotName.StringValue 56 | $OutputItem.SnapshotDescription = $Snap.Properties.SnapshotDescription.StringValue 57 | $OutputItem.SnapshotTime = $Snap.Properties.SnapshotTime.StringValue 58 | if ($VMName) { 59 | $OutputItem | ? {$_.VMName -eq $VMName} 60 | } else { 61 | $OutputItem 62 | } 63 | } 64 | } 65 | 66 | Function Write-SnapInfo { 67 | Param( 68 | [Parameter(Mandatory=$true)]$VMName, 69 | [Parameter(Mandatory=$true)]$DiskNum, 70 | [Parameter(Mandatory=$true)]$SnapGUID, 71 | [Parameter(Mandatory=$true)]$PrimaryUri, 72 | [Parameter(Mandatory=$true)]$SnapshotUri, 73 | [Parameter(Mandatory=$true)]$SnapshotName, 74 | $SnapshotDescription="", 75 | [Parameter(Mandatory=$true)]$StorageContext 76 | ) 77 | $TableName = "AzureVMSnapshots" 78 | $SnapTable = Get-StorageTable -TableName $TableName -StorageContext $StorageContext 79 | 80 | $entity = New-Object "Microsoft.WindowsAzure.Storage.Table.DynamicTableEntity" ` 81 | $SnapGUID, $DiskNum 82 | 83 | $entity.Properties.Add("VMName", $VMName) 84 | $entity.Properties.Add("BaseURI", $PrimaryUri) 85 | $entity.Properties.Add("SnapshotURI", $SnapshotUri) 86 | $entity.Properties.Add("SnapshotName", $SnapshotName) 87 | $entity.Properties.Add("SnapshotDescription", $SnapshotDescription) 88 | $entity.Properties.Add("SnapshotTime", ((Get-Date).ToString())) 89 | $result = $SnapTable.CloudTable.Execute( 90 | [Microsoft.WindowsAzure.Storage.Table.TableOperation]::Insert($entity)) 91 | } 92 | 93 | Function Clear-SnapInfo { 94 | Param( 95 | [Parameter(Mandatory=$true)]$SnapGUID, 96 | [Parameter(Mandatory=$true)]$StorageContext 97 | ) 98 | $TableName = "AzureVMSnapshots" 99 | $SnapTable = Get-StorageTable -TableName $TableName -StorageContext $StorageContext 100 | 101 | $query = New-Object "Microsoft.WindowsAzure.Storage.Table.TableQuery" 102 | $Query.FilterString = "PartitionKey eq '$($SnapGUID)'" 103 | 104 | $SnapInfo = $SnapTable.CloudTable.ExecuteQuery($query) 105 | foreach ($Snap in $SnapInfo) { 106 | $result = $SnapTable.CloudTable.Execute( 107 | [Microsoft.WindowsAzure.Storage.Table.TableOperation]::Delete($Snap)) 108 | } 109 | } 110 | 111 | Function Get-DiskInfo { 112 | Param([Parameter(Mandatory=$true)]$DiskUri) 113 | 114 | $DiskInfo = "" | Select Uri, StorageAccountName, VHDName, ContainerName 115 | $DiskInfo.Uri = $DiskUri 116 | $DiskInfo.StorageAccountName = ($DiskUri -split "https://")[1].Split(".")[0] 117 | $DiskInfo.VHDName = $DiskUri.Split("/")[-1] 118 | $DiskInfo.ContainerName = $DiskUri.Split("/")[3] 119 | Return $DiskInfo 120 | } 121 | 122 | Function Get-StorageContextForUri { 123 | Param([Parameter(Mandatory=$true)]$DiskUri) 124 | 125 | $DiskInfo = Get-DiskInfo -DiskUri $DiskUri 126 | 127 | $StorageAccountResource = find-azurermresource ` 128 | -ResourceNameContains $DiskInfo.StorageAccountName ` 129 | -WarningAction Ignore | 130 | Where-Object {$_.Name -eq $DiskInfo.StorageAccountName} 131 | 132 | $StorageKey = Get-AzureRmStorageAccountKey ` 133 | -Name $StorageAccountResource.Name ` 134 | -ResourceGroupName $StorageAccountResource.ResourceGroupName 135 | 136 | $StorageContext = New-AzureStorageContext ` 137 | -StorageAccountName $StorageAccountResource.Name ` 138 | -StorageAccountKey $StorageKey[0].Value 139 | 140 | return $StorageContext 141 | } 142 | 143 | Function Get-AzureRMVMSnapBlobs { 144 | Param( 145 | [Parameter(Mandatory=$true)]$VMName 146 | ) 147 | 148 | $VM = Get-AzureRmVM | ? {$_.Name -eq $VMName} 149 | if ($VM) { 150 | $DiskUriList=@() 151 | $DiskUriList += $VM.StorageProfile.OsDisk.Vhd.Uri 152 | Foreach ($Disk in $VM.StorageProfile.DataDisks) { 153 | $DiskUriList += $Disk.Vhd.Uri 154 | } 155 | Foreach ($DiskUri in $DiskUriList) { 156 | $DiskInfo = Get-DiskInfo -DiskUri $DiskUri 157 | 158 | $StorageContext = Get-StorageContextForUri -DiskUri $DiskUri 159 | 160 | Get-AzureStorageBlob -Container $DiskInfo.ContainerName ` 161 | -Context $StorageContext | 162 | ? {$_.Name -eq $DiskInfo.VHDName ` 163 | -and $_.ICloudBlob.IsSnapshot ` 164 | -and $_.SnapshotTime -ne $null } 165 | } 166 | 167 | } else { 168 | Write-Host "Unable to get VM" 169 | } 170 | } 171 | 172 | Function Test-Premium { 173 | Param ( 174 | [Parameter(Mandatory=$true)]$DiskUri 175 | ) 176 | $DiskInfo = Get-DiskInfo -DiskUri $DiskUri 177 | 178 | $StorageAccount = Get-AzureRmStorageAccount | 179 | ? {$_.StorageAccountName -eq $DiskInfo.StorageAccountName} 180 | 181 | Return $StorageAccount.Sku.Tier -eq "Premium" 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure-VM-Snapshots 2 | Powershell Functions for Creating and Reverting to Azure RM VM Snapshots. 3 | 4 | The functions handle multiple disk VMs by saving meta-data to an Azure Table on the OS disk's storage account. 5 | 6 | **DO NOT use this on a production VM! The revert function will delete your VM configuration and recreate it using the same settings. It's possible that the script may cause unintended results.** 7 | 8 | There are quite a few edge cases that this script will miss. Premium disks are not supported as they do not support Azure Table storage which is required for the script to save it's metadata. 9 | 10 | It also won't play nice if disks are removed from the VM after a snap is taken. 11 | 12 | Tested with PowerShell 5.1 and AzureRm Module 4.0.2 13 | 14 | Examples: 15 | 16 | Load the functions, you will also need to login to your Azure account 17 | 18 | C:\> . .\AzureSnapFunctions.PS1 19 | C:\> Login-AzureRMAccount 20 | 21 | Create a New snapshot for all VHDs in a VM 22 | 23 | C:\> New-AzureRMVMSnap -VMName MyVM -SnapshotName "Foo" 24 | 25 | 26 | View the snapshots for all VHDs on a VM 27 | 28 | C:\> Get-AzureRMVMSnap -VMName MyVM 29 | 30 | 31 | Delete all snapshots for all VHDs on a VM 32 | 33 | C:\> Delete-AzureRMVMSnap -VMName MyVM -Force 34 | 35 | 36 | Revert to a snapshots for all VHDs on a VM - This will remove the VM and recreate it using the reverted disk. 37 | 38 | C:\> Revert-AzureRMVMSnap -VMName MyVM 39 | 40 | --------------------------------------------------------------------------------