In this series of three articles, I'll show you how to clone an Azure VM with PowerShell. This article demonstrates how to clone the VM. In a follow-up article, I will explain how to clone data drives. Finally, I will show you how to produce the XML answer file and run sysprep on the newly created VM.
Alex Chaika

One of the most common tasks I encounter every day when I work in Azure is cloning a VM. Most on-premises virtual solutions offer this common option. Thus, it was an unpleasant surprise that this option just doesn't exist in Azure.

Of course, you can create an image from the source VM and then create a clone from it. But this is not convenient because I don't want to run sysprep on the source VM. Moreover, I don't really need an image, since I don't intend to create a bunch of VMs. What I need is just one copy of a source VM, so I can run sysprep on it and give it to the owner.

Clone Azure VM

Clone Azure VM

I did some research and then wrote a PowerShell script that helps me to achieve this goal without too much manual intervention. The PowerShell script below copies the original VM OS drive and then creates a new VM from the copy:

[CmdletBinding()]
Param(
  [Parameter(Mandatory=$True)]
   [string]$SvcName,
  [Parameter(Mandatory=$True)]
   [string]$SourceVMName,
  [Parameter(Mandatory=$True)]
   [string]$NewVMName,
  [Parameter(Mandatory=$True)]
   [string]$Subscription,
  [Parameter(Mandatory=$False)]
   [string]$NewSvcName,
  [Parameter(Mandatory=$False)]
   [string]$InstanceSize,
  [Parameter(Mandatory=$False)]
   [string]$newSubnet,
  [Parameter(Mandatory=$True)]
   [string]$SrcHostName           
)

$SourceVMName = $SourceVMName
$dt = Get-Date -UFormat %c
Write-Host "Cloning VM $SourceVMName in ServiceName $Svcname" -ForegroundColor Yellow
Write-Output "Cloning VM $SourceVMName in ServiceName $Svcname timestamt: $dt" >> $file
$vm = Get-AzureVM -ServiceName $SvcName -Name $SourceVMName
if(!($newSvcName)){
   $newSvcName = $vm.ServiceName
  }
if(!($InstanceSize)){
   $InstanceSize = $vm.InstanceSize
  }
if(!($newSubnet)){
   $newSubnet = $vm | Get-AzureSubnet
  }

$OSDisk = $vm | Get-AzureOSDisk
$StorageAccountName = $OSDisk.MediaLink.Host.Split('.')[0]
$srcUri = $OSDisk.MediaLink.AbsoluteUri
$DstBlob = ($OSDisk.MediaLink.LocalPath | split-path -Leaf).TrimEnd(".vhd") + "_clone_{0:MMddyyyy_HHmm}" -f (Get-Date) + ".vhd"
$containerName = "vhds"
$dt = Get-Date -UFormat %c
Write-Host "Stopping and Deallocating the VM $SourceVMName" -ForegroundColor Yellow
Write-Output "Stopping and Deallocating VM $SourceVMName, timestamp: $dt" >> $file
$vm | Stop-Azurevm -Force -Verbose
do{
      sleep 5
     $status = (get-azurevm -ServiceName $SvcName -Name $SourceVMName).InstanceStatus
     }
   until($status -eq "StoppedDeallocated")

$dt = Get-Date -UFormat %c
Write-Host "Copying VMs OS drive" -ForegroundColor Yellow
Write-Output "Copying VMs OS drive, timestamp: $dt" >> $file
$StorageAccountKey = Get-AzureStorageKey -StorageAccountName $StorageAccountName
$Ctx = New-AzureStorageContext $StorageAccountName -StorageAccountKey $StorageAccountKey.Primary
$blob1 = Start-AzureStorageBlobCopy -srcUri $srcUri -SrcContext $Ctx -DestContainer $containerName -DestBlob $DstBlob -DestContext $Ctx
$status = $blob1 | Get-AzureStorageBlobCopyState 
While($status.Status -eq "Pending"){
  $status = $blob1 | Get-AzureStorageBlobCopyState 
  Start-Sleep 10
  
  $status
}
$NewDiskUri = $blob1.Context.BlobEndPoint + "\vhds\" + $blob1.Name
$NewDiskName = $NewVMName + "_{0:MMddyyyy_HHmm}" -f (Get-Date)
$NewDiskName = $NewDiskName
Add-AzureDisk -DiskName $NewDiskName -MediaLocation $NewDiskUri -Label $NewDiskName -OS "Windows"
$StorageAccount = (Get-AzureDisk -DiskName $NewDiskName).MediaLink.Host.Split('.')[0]
Set-AzureSubscription –SubscriptionName $Subscription –CurrentStorageAccountName $StorageAccount 

$dt = Get-Date -UFormat %c
Write-Host "Creating new VM" -ForegroundColor Yellow
Write-Output "Creating new VM, timestamp: $dt" >> $file
$vmI1 = New-AzureVMConfig -Name $NewVMName -InstanceSize $InstanceSize -DiskName $NewDiskName  | Set-AzureSubnet $newSubnet
$vmI1 | New-AzureVM -ServiceName $vm.ServiceName

Let's take a quick look at the parameters section:
CmdletBinding()]
Param(
  [Parameter(Mandatory=$True)]
   [string]$SvcName,
  [Parameter(Mandatory=$True)]
   [string]$SourceVMName,
  [Parameter(Mandatory=$True)]
   [string]$NewVMName,
  [Parameter(Mandatory=$True)]
   [string]$Subscription,
  [Parameter(Mandatory=$False)]
   [string]$NewSvcName,
  [Parameter(Mandatory=$False)]
   [string]$InstanceSize,
  [Parameter(Mandatory=$False)]
   [string]$newSubnet,
  [Parameter(Mandatory=$True)]
   [string]$SrcHostName
)

In order to clone a VM, I need to know the source VM name and its cloud service name. The $SourceVMName and $SvcName string parameters serve this purpose. Then I need to store the Azure subscription name, the new VM name and the target VM service name. The $Subscription, $NewVMName and $NewSvcNam strings store these parameters. Finally, $InstanceSize, $newSubnet and $SrcHostName store the instance size, the new VM subnet name and the VM source host name.

Next, I assign the source VM name value to the $SourceVMName variable.

$dt = Get-Date -UFormat %c
Write-Host "Cloning VM $SourceVMName in ServiceName $SvcName" -ForegroundColor Yellow
Write-Output "Cloning VM $SourceVMName in ServiceName $SvcName timestamp: $dt" >> $file

The $dt variable is nothing other than a timestamp I'm using for logging. The two following lines just update the console and log file. I know I didn't really set up any log file in that script, but this is not a mistake. The separate wrapper script configures the log file. This script's only purpose is to run the other scripts one after another. I will explain the wrapper script in a later post.
$vm = Get-AzureVM -ServiceName $SvcName -Name $SourceVMName
if(!($newSvcName)){
   $newSvcName = $vm.ServiceName
  }
if(!($InstanceSize)){
   $InstanceSize = $vm.InstanceSize
  }
if(!($newSubnet)){
   $newSubnet = $vm | Get-AzureSubnet
  }

Now I'm storing source VM parameters in the $vm variable and then checking whether optional parameters $newSvcname, $InstanceSize and $newSubnet were assigned any values. If not, I assume that the new VM will use the corresponding values of the existing VM.
$OSDisk = $vm | Get-AzureOSDisk
$StorageAccountName = $OSDisk.MediaLink.Host.Split('.')[0]
$srcUri = $OSDisk.MediaLink.AbsoluteUri
$DstBlob = ($OSDisk.MediaLink.LocalPath | split-path -Leaf).TrimEnd(".vhd") + "_clone_{0:MMddyyyy_HHmm}" -f (Get-Date) + ".vhd"
$containerName = "vhds"

Since I'm going to copy the OS disk of the source VM, I need to get its information into the $OSDisk variable. Then I'm getting the storage account name from that variable by splitting the .MediaLink.Host part with the '.' delimiter and taking only the first part of it. This represents the Azure storage account name. Next, I'm getting the URI of the source OS disk and saving it to the $srcUri variable.

I also need to copy the disk somewhere, so $DstBlob is the local path for creating a copy from the OS drive. The Azure blob file is in fact the same thing as the Azure disk, but it lacks the disk attributes. I'll add these attributes later. I also need to know the container name where I'm going to put the blob. Since all my VM disk files are in the "vhds" container, I'm going to put new one in the same place.

$dt = Get-Date -UFormat %c
Write-Host "Stopping and deallocating the VM $SourceVMName" -ForegroundColor Yellow
Write-Output "Stopping and deallocating VM $SourceVMName, timestamp: $dt" >> $file
$vm | Stop-Azurevm -Force –Verbose

Now I'm updating the console and log file again and stopping the source VM.
do{
     sleep 5
     $status = (get-azurevm -ServiceName $SvcName -Name $SourceVMName).InstanceStatus
     }
   until($status -eq "StoppedDeallocated")

This loop just helps ensure stopping the VM before the script goes ahead with copying the OS drive.
$dt = Get-Date -UFormat %c
Write-Host "Copying VM OS drive" -ForegroundColor Yellow
Write-Output "Copying VM OS drive, timestamp: $dt" >> $file

The script updates the console and log file again.
$StorageAccountKey = Get-AzureStorageKey -StorageAccountName $StorageAccountName
$Ctx = New-AzureStorageContext $StorageAccountName -StorageAccountKey $StorageAccountKey.Primary
$blob1 = Start-AzureStorageBlobCopy -srcUri $srcUri -SrcContext $Ctx -DestContainer $containerName -DestBlob $DstBlob -DestContext $Ctx
$status = $blob1 | Get-AzureStorageBlobCopyState

Now I'm getting to the interesting part. In order to start copying, I need to get some data first. In particular, I need to supply the Start-AzureStorageBlobCopy cmdlet with the following: source blob URI, source storage account context, container name, destination blob URI and destination storage account context.

It seems that I already have everything except the source and destination account contexts. In my case, since I'm copying the VM to the same storage account, those two would be the same. But I still need to find out this information.

To do this, I use Get-AzureStorageKey and then, when I have stored the information in the $StorageAccountKey variable, I can use it to create a storage account context using the New‑AzureStorageContext cmdlet.

When I have that stored in the $Ctx variable, I can start the blob copy. I'm saving the results of the copy operation to the $blob1 variable to get the copy status later using the Get‑AzureStorageBlobCopyState cmdlet.

While($status.Status -eq "Pending"){
  $status = $blob1 | Get-AzureStorageBlobCopyState 
  Start-Sleep 10
  
  $status
}

This while loop helps me get a status update every ten seconds and proceeds only when the copy finishes.
$NewDiskUri = $blob1.Context.BlobEndPoint + "\vhds\" + $blob1.Name
$NewDiskName = $NewVMName + "_{0:MMddyyyy_HHmm}" -f (Get-Date)
Add-AzureDisk -DiskName $NewDiskName -MediaLocation $NewDiskUri -Label $NewDiskName -OS "Windows"

This creates the new blob, and I can then create the Azure disk from it. But first, I need to know its URI, name and label. So I create those using the information I saved in the $blob1 variable ($NewDiskUri), the $NewVMName parameter and the current date and time. The latter make the name unique. I then store this information in the $NewDiskName variable. When all of this data is ready, I add the disk using the Add-AzureDisk cmdlet.

$StorageAccount = (Get-AzureDisk -DiskName $NewDiskName).MediaLink.Host.Split('.')[0]
Set-AzureSubscription –SubscriptionName $Subscription –CurrentStorageAccountName $StorageAccount

To be able to create the new VM, I should configure the default storage account for my Azure subscription. So this is exactly what I'm doing here: getting the storage account of the newly created Azure disk and setting it up as the default for my current Azure subscription.

$dt = Get-Date -UFormat %c
Write-Host "Creating new VM" -ForegroundColor Yellow
Write-Output "Creating new VM, timestamp: $dt" >> $file

These three lines just update the log file and the console again.
$vmI1 = New-AzureVMConfig -Name $NewVMName -InstanceSize $InstanceSize -DiskName $NewDiskName | Set-AzureSubnet $newSubnet
$vmI1 | New-AzureVM -ServiceName $newSvcName

Believe it or not, I'm now getting to the final part of this post: creating the new VM. Firstly, I create a new Azure VM configuration using the New-AzureVMConfig cmdlet and supply it with the $NewVMName and $InstanceSize parameters as well as the $NewDiskName.

Then I'm piping it to the Set-AzureSubnet cmdlet to configure the Azure subnet for the new VM. After that, I'm saving all this data to the $vmI1 variable. Finally, I'm using this variable to pipe it to the New-AzureVM cmdlet and combining it with the $NewSvcName parameter to create the new VM.

Subscribe to 4sysops newsletter!

Cloning OS drives in Azure with PowerShell

Cloning OS drives in Azure with PowerShell

The next article is going to be about copying the Azure data drives and attaching them to the new VM. Then in the last part, I will explain how to create the XML answer file automatically and run sysprep on the newly created VM. I'll also explain the wrapper script at the end of the third article.

2 Comments

Leave a reply

Please enclose code in pre tags: <pre></pre>

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2024

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending
WindowsUpdatePreventer

Log in with your credentials

or    

Forgot your details?

Create Account