This article will show how to automatically copy VHDs from a source storage account to a new one, without hardcoding values. Secondly how to create a new VM with the disks in the new Storage Account, reusing the same value of the original VM. The first thing is to create a PowerShell module file where keeps all functions that will be invoked by the main script.
Ideally, this module could be reused for other purposes and new functions should be added according to your needs.
Open your preferred PowerShell editor and creates a new file called "Module-Azure.ps1"
Note: all function will be declared as global in order to be available to others script
The first function to be added is called Connect-Azure and it will simplify Azure connection activities.
[powershell] function global:Connect-Azure { Login-AzureRmAccount $global:subName = (Get-AzureRmSubscription | select SubscriptionName | Out-GridView -Title "Select a subscription" -OutputMode Single).SubscriptionName Select-AzureRmSubscription -SubscriptionName $subName } [/powershell]
Above function, using Out-GridView cmdlets, will show all Azure subscriptions associated with your account and allow you to select the one against which execute script
The second function to be added is called CopyVHDs. It will take care of copy all VHDs from the selected source Storage Account to the selected destination Storage Account
[powershell]
function global:CopyVHDs { param ( $sourceSAItem, $destinationSAItem
)
$sourceSA = Get-AzureRmStorageAccount -ResourceGroupName $sourceSAItem.ResourceGroupName -Name $sourceSAItem.StorageAccountName
$sourceSAContainerName = "vhds"
$sourceSAKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $sourceSAItem.ResourceGroupName -Name $sourceSAItem.StorageAccountName)[0].Value
$sourceSAContext = New-AzureStorageContext -StorageAccountName $sourceSAItem.StorageAccountName -StorageAccountKey $sourceSAKey
$blobItems = Get-AzureStorageBlob -Context $sourceSAContext -Container $sourceSAContainerName
$destinationSAKey = (Get-AzureRmStorageAccountKey -ResourceGroupName $destinationSAItem.ResourceGroupName -Name $destinationSAItem.StorageAccountName)[0].Value
$destinationContainerName = "vhds"
$destinationSAContext = New-AzureStorageContext -StorageAccountName $destinationSAItem.StorageAccountName -StorageAccountKey $destinationSAKey
foreach ( $blobItem in $blobItems) {
# Copy the blob Write-Host "Copying " $blobItem.Name " from " $sourceSAItem.StorageAccountName " to " $destinationSAItem.StorageAccountName
$blobCopy = Start-AzureStorageBlobCopy -DestContainer $destinationContainerName -DestContext $destinationSAContext -SrcBlob $blobItem.Name -Context $sourceSAContext -SrcContainer $sourceSAContainerName
$blobCopyStatus = Get-AzureStorageBlob -Blob $blobItem.Name -Container $destinationContainerName -Context $destinationSAContext | Get-AzureStorageBlobCopyState
[int] $i = 0;
while ( $blobCopyStatus.Status -ne "Success") { Start-Sleep -Seconds 180
$i = $i + 1
$blobCopyStatus = Get-AzureStorageBlob -Blob $blobItem.Name -Container $destinationContainerName -Context $destinationSAContext | Get-AzureStorageBlobCopyState
Write-Host "Blob copy status is " $blobCopyStatus.Status Write-Host "Bytes Copied: " $blobCopyStatus.BytesCopied Write-Host "Total Bytes: " $blobCopyStatus.TotalBytes
Write-Host "Cycle Number $i" }
Write-Host "Blob " $blobItem.Name " copied"
}
return $true }
[/powershell]
This function is basically executing the same commands that were showed in the first article. Of course the difference is the it takes as input two objects which contains required information to copy VHDs between the two Storage Account. A couple of notes:
- Because it is unknown how many VHDs should be copied, there is foreach that will iterate over all VHDs that will copied
- In order to minimize any side effects, aforementioned for each contains a while that will ensure that copy activity is really completed before return control
The third function to be added is called Create-AzureVMFromVHDs. It will take care of create a new VM using existing VHDs. In order to provide a PoC about what could be achieved, following assumptions have been made:
- New VM will be deployed in an existing vnet / subnet
- New VM will have the same size of the original VM
- New VM will be deployed in a new Resource Group
- New VM will be deployed in the same location of the (destination) Azure Storage Account where VHDs have been copied
- New VM will have the same credentials of the source one
- New VM will have assigned a new dynamic public IP
- All VHDs copied from source Storage Account (which were attached to the source VM) will be attached to the new VM
[powershell] function global:Create-AzureVMFromVHDs { param ( $destinationVNETItem, $destinationSubnetItem, $destinationSAItem, $sourceVMItem )
$destinationSA = Get-AzureRmStorageAccount -Name $destinationSAItem.StorageAccountName -ResourceGroupName $destinationSAItem.ResourceGroupName
$Location = $destinationSA.PrimaryLocation
$destinationVMItem = '' | select name,ResourceGroupName
$destinationVMItem.name = ($sourceVMItem.Name + "02").ToLower()
$destinationVMItem.ResourceGroupName = ($sourceVMItem.ResourceGroupName + "02").ToLower()
$InterfaceName = $destinationVMItem.name + "-nic"
$destinationResourceGroup = New-AzureRmResourceGroup -location $Location -Name $destinationVMItem.ResourceGroupName
$sourceVM = get-azurermvm -Name $sourceVMItem.Name -ResourceGroupName $sourceVMItem.ResourceGroupName
$VMSize = $sourceVM.HardwareProfile.VmSize
$sourceVHDs = $sourceVM.StorageProfile.DataDisks
$OSDiskName = $sourceVM.StorageProfile.OsDisk.Name
$publicIPName = $destinationVMItem.name + "-pip"
$sourceVMOSDiskUri = $sourceVM.StorageProfile.OsDisk.Vhd.Uri
$OSDiskUri = $sourceVMOSDiskUri.Replace($sourceSAItem.StorageAccountName,$destinationSAItem.StorageAccountName)
# Network Script $VNet = Get-AzureRMVirtualNetwork -Name $destinationVNETItem.Name -ResourceGroupName $destinationVNETItem.ResourceGroupName $Subnet = Get-AzureRMVirtualNetworkSubnetConfig -Name $destinationSubnetItem.Name -VirtualNetwork $VNet
#Public IP script $publicIP = New-AzureRmPublicIpAddress -Name $publicIPName -ResourceGroupName $destinationVMItem.ResourceGroupName -Location $location -AllocationMethod Dynamic
# Create the Interface $Interface = New-AzureRMNetworkInterface -Name $InterfaceName -ResourceGroupName $destinationVMItem.ResourceGroupName -Location $Location -SubnetId $Subnet.Id -PublicIpAddressId $publicIP.Id
#Compute script $VirtualMachine = New-AzureRMVMConfig -VMName $destinationVMItem.name -VMSize $VMSize
$VirtualMachine = Add-AzureRMVMNetworkInterface -VM $VirtualMachine -Id $Interface.Id $VirtualMachine = Set-AzureRMVMOSDisk -VM $VirtualMachine -Name $OSDiskName -VhdUri $OSDiskUri -CreateOption Attach -Windows
$VirtualMachine = Set-AzureRmVMBootDiagnostics -VM $VirtualMachine -Disable
#Adding Data disk
if ( $sourceVHDs.Length -gt 0) { Write-Host "Found Data disks"
foreach ($sourceVHD in $sourceVHDs) { $destinationDataDiskUri = ($sourceVHD.Vhd.Uri).Replace($sourceSAItem.StorageAccountName,$destinationSAItem.StorageAccountName)
$VirtualMachine = Add-AzureRmVMDataDisk -VM $VirtualMachine -Name $sourceVHD.Name -VhdUri $destinationDataDiskUri -Lun $sourceVHD.Lun -Caching $sourceVHD.Caching -CreateOption Attach
}
} else { Write-Host "No Data disk found" }
# Create the VM in Azure New-AzureRMVM -ResourceGroupName $destinationVMItem.ResourceGroupName -Location $Location -VM $VirtualMachine
Write-Host "VM created. Well Done !!"
}
[/powershell]
A couple of note:
- The URI of the VHDs copied in the destination Storage Account has been calculated replacing the source Storage Account name with destination Storage Account name in URI
- destination VHDs will be attached in the same order (LUN) of source VHDs
Module-Azure.ps1 should have a structure like this:
Now it's time to create another file called Move-VM.ps1 which should be stored in the same folder of Module-Azure
Note: if you want to store in a different folder, then update line 7
Paste following code:
[powershell] $ScriptDir = $PSScriptRoot
Write-Host "Current script directory is $ScriptDir"
Set-Location -Path $ScriptDir
.\Module-Azure.ps1
Connect-Azure
$vmItem = Get-AzureRmVM | select ResourceGroupName,Name | Out-GridView -Title "Select VM" -OutputMode Single
$sourceSAItem = Get-AzureRmStorageAccount | select StorageAccountName,ResourceGroupName | Out-GridView -Title "Select Source Storage Account" -OutputMode Single
$destinationSAItem = Get-AzureRmStorageAccount | select StorageAccountName,ResourceGroupName | Out-GridView -Title "Select Destination Storage Account" -OutputMode Single
# Stop VM
Write-Host "Stopping VM " $vmItem.Name
get-azurermvm -name $vmItem.Name -ResourceGroupName $vmItem.ResourceGroupName | stop-azurermvm
Write-Host "Stopped VM " $vmItem.Name
CopyVHDs -sourceSAItem $sourceSAItem -destinationSAItem $destinationSAItem
$destinationVNETItem = Get-AzureRmVirtualNetwork | select Name,ResourceGroupName | Out-GridView -Title "Select Destination VNET" -OutputMode Single
$destinationVNET = Get-AzureRmVirtualNetwork -Name $destinationVNETItem.Name -ResourceGroupName $destinationVNETItem.ResourceGroupName
$destinationSubnetItem = Get-AzureRmVirtualNetworkSubnetConfig -VirtualNetwork $destinationVNET | select Name,AddressPrefix | Out-GridView -Title "Select Destination Subnet" -OutputMode Single
Create-AzureVMFromVHDs -destinationVNETItem $destinationVNETItem -destinationSubnetItem $destinationSubnetItem -destinationSAItem $destinationSAItem -sourceVMItem $vmItem
[/powershell]
Comments:
- Line 7: Module-Azure function is invoked
- Line 9: Connect-Azure function (declared in Module-Azure) is invoked. This is possible because it has been declared as global
- From Line 11 to Line 15: a subset of source VM, source Storage Account and destination Storage Account info are retrieved. They will be used later
- Line 19-23: source VM is stopped
- Line 25: Copy-VHDs function (declared in Module-Azure) is invoked. This is possible because it has been declared as global. Note that we're just passing three previously retrieved parameters
- From Line 27 to Line 31: VNET and subnet where new VM will be attached are retrieved
- Line 33: Create-AzureVMFromVHDs function (declared in Module-Azure) is invoked. This is possible because it has been declared as global. Note that we're just passing already retrieved parameters
Following screenshots shows an execution of Move-VM script:
Select Azure subscription
Select source VM
Select source Storage Account
Select Destination Storage Account
Confirm to stop VM
Select destination VNET
Select destination Subnet
Output sample #1
Output sample #2
Source VM Resource Group
Destination VM RG
Destination Storage Account RG
Source VHDs
Destination VHDs
Thanks for your patience. Any feedback is appreciated
Note: Above script has been tested with Azure PS 3.7.0 (March 2017).
Starting from Azure PS 4.x, this cmdlets returns an array of objects with the following properties: Name, Id, TenantId and State.
The function Connect-Azure is using the value SubscriptionName that is no more available. This is the reason why some people saw an empty Window.
Connect-Azure function should be modified as follows to work with Azure PS 4.x:
[powershell]
function global:Connect-Azure { Login-AzureRmAccount
$global:subName = (Get-AzureRmSubscription | select Name | Out-GridView -Title "Select a subscription" -OutputMode Single).Name
Select-AzureRmSubscription -SubscriptionName $subName }
[/powershell]