@@ -0,0 +1,593 @@
<#
. SYNOPSIS
Migrates a Azure Virtual Machine to another subscription or data centre
. DESCRIPTION
Shutsdown the source VM
Exports the VM config to a temporary file
Loops through all Azure disks attached to the source VM
Schedules an async copy of the underlying VHD to the destination storage account
- optionally overwrites existing VHD in destination if it exists
Waits for all copy jobs to complete
Adds Azure disks in the destination subscription for every disk copied
- optionally removes the existing Azure Disk if it exists
Restores VM in the destination cloud service
Starts the migrated VM
. NOTES
File Name : Migrate-AzureVM.ps1
Requires : Windows Azure Cmdlets Snapin v8.7.1 or greater
Author : Scott Scovell
Version : 1.0 - 07/11/2014 - Scott Scovell - Created script
Assumptions:
- Azure Service Management certificates are installed on the machine running the script
- Azure Subscription profiles have been created on the machine running the script (use Get-AzureSubscription to check)
- Destination storage accounts, cloud services, vnets, etc already exist
Constraints:
- No support for internal load balanced endpoints. Will fail if ILB instance not found
. EXAMPLE
.\Migrate-AzureVM.ps1 -SourceSubscription "MySub" -SourceServiceName "MyCloudService" -VMName "MyVM" `
-DestSubscription "AnotherSub" -DestStorageAccountName "mydeststorage" -DestServiceName "MyDestCloudService" -DestVNETName "MyRegionalVNet" `
-IsReadOnlySecondary $false -Overwrite $false -RemoveDestAzureDisk $false
#>
param
(
# Name of the source Azure subscription
[string ] $SourceSubscription = " Sco's MSDN" ,
# Name of the source cloud service
[string ] $SourceServiceName = " dev-cloud" ,
# Name of the VM to migrate
[string ] $VMName = " dev-cloud" ,
# Name of the destination Azure subscription
[string ] $DestSubscription = " Sco's MSDN" ,
# Name of the destination storage account
[string ] $DestStorageAccountName = " scovhdsau" ,
# Name of the destination cloud service
[string ] $DestServiceName = " dev-cloud-aueast" ,
# Name of the destination VNET - blank if none used
[string ] $DestVNETName = " " ,
# Indicates if we are copying from the source storage accounts read-only secondary location
[switch ] $IsReadOnlySecondary = $false ,
# Indicates if we are overwriting if the VHD already exists
[switch ] $Overwrite = $false ,
# Indicates if we remove an Azure Disk if it already exists in the destination repository
[switch ] $RemoveDestAzureDisk = $true
)
# region === Runtime Configuration ===========================================
# Script Path/Directories
$ScriptPath = (Split-Path ((Get-Variable MyInvocation).Value).MyCommand.Path)
# Date Format
$DateFormat = Get-Date - Format " yyyyMMdd_HHmmss"
# Zero out errors
$Error.Clear ()
# Define collections to keep track of async operations
$copyTasks = @ ()
# Define storage account context collection so we can reuse these
$storageContexts = @ {}
# Show verbose output
$VerbosePreference = " Continue"
# Define Stop/Start Instance Status values
$StoppedStatus = " StoppedDeallocated"
$StartedStatus = " ReadyRole"
# endregion configuration
# region === Functions =======================================================
<#
. SYNOPSIS
Write to output including a timestamp and computer name
. EXAMPLE
Write-Log "message"
. OUTPUTS
None
#>
function Write-Log
{
param
(
[string ] $Message ,
[switch ] $IsError = $false
)
if ($IsError )
{
Write-Error " $ ( [DateTime ]::Now.ToLongTimeString()) - $Message "
}
else
{
Write-Verbose " $ ( [DateTime ]::Now.ToLongTimeString()) - $Message "
}
}
<#
. SYNOPSIS
Stops the specified VM and waits until the instance enters the stopped state
. DESCRIPTION
Checks the state of the VM instance
Stops the VM
Loops until the state of the VM equals the stopped state
. EXAMPLE
Stop-AzureVMAndWait -ServiceName "MyCloudService" -VMName "MyVMName"
. OUTPUTS
None
#>
function Stop-AzureVMAndWait
{
param
(
# Name of the service hosting the VM
[Parameter (Mandatory = $true )]
[String ]
$ServiceName ,
# Name of the VM
[Parameter (Mandatory = $true )]
[String ]
$VMName
)
Write-Log " Checking current VM state..."
# Gather current status of the VM
$vmStatus = Get-AzureVM - ServiceName $ServiceName - Name $VMName
# Check if VM exists
if ($vmStatus -eq $null ) {
Write-Log " $VMName is not been provisioned"
return
}
# Attempt to stop VM
if ($vmStatus.InstanceStatus -ne $StoppedStatus ) {
Write-Log " Stopping $VMName ..."
# Stop VM
foreach ($retry in (1 .. 3 )) {
Stop-AzureVM - ServiceName $ServiceName - Name $VMName - Force - Verbose - ErrorVariable lastError - ErrorAction SilentlyContinue | Out-Null
if ($? ) { break } # Success
else {
# Failure
if ($retry -eq 3 ) {
Write-Log " $VMName failed to stop after $retry retires"
return
}
# Wait
Sleep - Seconds 3
}
}
# Wait for VM to shutdown
$vmStatus = Get-AzureVM - ServiceName $ServiceName - Name $VMName
While ($vmStatus.InstanceStatus -ne $StoppedStatus )
{
# Take a break
Write-Log " Waiting for VM to enter $StoppedStatus state... Current Status: $ ( $vmStatus.InstanceStatus ) "
Start-Sleep - Seconds 15
# Gather current status
$vmStatus = Get-AzureVM - ServiceName $ServiceName - Name $VMName
}
}
Write-Log " $VMName is in the $StoppedStatus state"
}
<#
. SYNOPSIS
Starts the specified VM and waits until the instance enters the ready state
. DESCRIPTION
Checks the state of the VM instance
Starts the VM
Loops until the state of the VM equals the ready state
. EXAMPLE
Stop-AzureVMAndWait -ServiceName "MyCloudService" -VMName "MyVMName"
. OUTPUTS
None
#>
function Start-AzureVMAndWait
{
param
(
# Name of the service hosting the VM
[Parameter (Mandatory = $true )]
[String ]
$ServiceName ,
# Name of the VM
[Parameter (Mandatory = $true )]
[String ]
$VMName
)
Write-Log " Checking current VM state..."
# Gather current status of the VM
$vmStatus = Get-AzureVM - ServiceName $ServiceName - Name $VMName
# Check if VM exists
if ($vmStatus -eq $null ) {
Write-Log " $VMName is not been provisioned"
return
}
# Attempt to stop VM
if ($vmStatus.InstanceStatus -ne $StoppedStatus ) {
Write-Log " Starting $VMName ..."
# Stop VM
foreach ($retry in (1 .. 3 )) {
Start-AzureVM - ServiceName $ServiceName - Name $VMName - Verbose - ErrorVariable lastError - ErrorAction SilentlyContinue | Out-Null
if ($? ) { break } # Success
else {
# Failure
if ($retry -eq 3 ) {
Write-Log " $VMName failed to start after $retry retires"
return
}
# Wait
Sleep - Seconds 3
}
}
# Wait for VM to shutdown
$vmStatus = Get-AzureVM - ServiceName $ServiceName - Name $VMName
While ($vmStatus.InstanceStatus -ne $StartedStatus )
{
# Take a break
Write-Log " Waiting for VM to enter $StartedStatus state... Current Status: $ ( $vmStatus.InstanceStatus ) "
Start-Sleep - Seconds 15
# Gather current status
$vmStatus = Get-AzureVM - ServiceName $ServiceName - Name $VMName
}
}
Write-Log " $VMName is in the $StartedStatus state"
}
<#
. SYNOPSIS
Returns a reference to the storage account context
. EXAMPLE
Get-AzureStorageContext -SubscriptionName "MySub" -StorageAccountName "mystorage"
. OUTPUTS
[AzureStorageContext]
#>
function Get-AzureStorageContext
{
param
(
[string ] $SubscriptionName ,
[string ] $StorageAccountName
)
# Set subscription context
Select-AzureSubscription - SubscriptionName $SubscriptionName - Current
# Check our collection if we have already generated a storage context for this account
$context = $storageContexts [$StorageAccountName ]
if ($context -eq $null )
{
# Generate context for this storage account
$storageAccountKey = (Get-AzureStorageKey - StorageAccountName $StorageAccountName ).Primary
$context = New-AzureStorageContext - StorageAccountName $StorageAccountName - StorageAccountKey $storageAccountKey
# Add context to collection for next time
$storageContexts [$StorageAccountName ] = $context
}
return $context
}
<#
. SYNOPSIS
Start the async copy of the underlying VHD to the corresponding destination storage account
. EXAMPLE
Copy-AzureDiskAsync -SourceDisk $disk
. OUTPUTS
None
#>
function Copy-AzureDiskAsync
{
param
(
# Reference to source Azure disk to copy
[Parameter (Mandatory = $true )]
$SourceDisk
)
# Gather container and blob details from the source disk
$container = ($SourceDisk.MediaLink.Segments [1 ]).Replace(" /" , " " )
$blobName = $SourceDisk.MediaLink.Segments | Where-Object { $_ -like " *.vhd" }
$sourceUri = $SourceDisk.MediaLink.AbsoluteUri
# Gather storage account details.
$srcStorageAccount = $SourceDisk.MediaLink.Host.Replace (" .blob.core.windows.net" , " " )
$destStorageAccount = $DestStorageAccountName.ToLower ()
Write-Log " Preparing to copy source disk $ ( $SourceDisk.DiskName ) to $destStorageAccount ..."
# Get storage contexts for source and destination
$srcContext = Get-AzureStorageContext - SubscriptionName $SourceSubscription - StorageAccountName $srcStorageAccount
$destContext = Get-AzureStorageContext - SubscriptionName $DestSubscription - StorageAccountName $destStorageAccount
if ($srcContext -eq $null -or $destContext -eq $null )
{
if ($srcContext -eq $null ) { Write-Log " Could not access source storage account $srcStorageAccount " - IsError }
if ($destContext -eq $null ) { Write-Log " Could not access destination storage account $destStorageAccount " - IsError }
throw " Failed to create storage contexts for storage accounts"
}
# Create destination container if it doesnt already exist
if ((Get-AzureStorageContainer - Name $container - Context $destContext - ErrorAction SilentlyContinue) -eq $null )
{
Write-Log " Creating container $container in destination storage account..."
New-AzureStorageContainer - Name $container - Context $destContext
}
# Check if the VHD already exists in the destination container
$blob = Get-AzureStorageBlob - Container $container - Blob $blobName - Context $destContext - ErrorAction SilentlyContinue
if ($blob -ne $null -and $Overwrite -eq $false )
{
throw " A blob with the name $blobName already exists in the destination storage account"
}
Write-Log " Scheduling the async copy of source disk $ ( $SourceDisk.DiskName ) to $destStorageAccount ..."
# [SS: Check if we are copying from a RA-GRS secondary storage account]
if ($IsReadOnlySecondary -eq $true )
{
# Append "-secondary" to the media location URI to reference the RA-GRS copy
$sourceUri = $sourceUri.Replace ($srcStorageAccount , " $srcStorageAccount -secondary" )
}
# [SS: Need to be in the source subscription context for the copy operation to work correctly]
# Set context to source subscription
Select-AzureSubscription - SubscriptionName $SourceSubscription - Current
# Schedule a blob copy operation of the source disk to the destination storage account
if ($Overwrite -eq $true )
{
# Use the Force flag to overwrite destination blob if it exists
$copyTask = Start-AzureStorageBlobCopy - Context $srcContext - SrcUri $sourceUri `
- DestContext $destContext - DestContainer $container - DestBlob $blobName `
- Force `
- ErrorAction SilentlyContinue - ErrorVariable LastError
}
else
{
# Without the force flag
$copyTask = Start-AzureStorageBlobCopy - Context $srcContext - SrcUri $sourceUri `
- DestContext $destContext - DestContainer $container - DestBlob $blobName `
- ErrorAction SilentlyContinue - ErrorVariable LastError
}
# Check if the copy task was created successfully
if ($copyTask -eq $null )
{
throw " Failed to schedule async copy task of blob: $blob to storage account: $destStorageAccount . Details: $LastError "
}
Write-Log " Copy of source disk $ ( $SourceDisk.DiskName ) to $destStorageAccount has been scheduled successfully"
return $copyTask
}
<#
. SYNOPSIS
Monitor async copy tasks and wait for all to complete
. EXAMPLE
WaitAll-AsyncCopyJobs
. OUTPUTS
None
#>
function WaitAll-AsyncCopyJobs
{
# Monitor async tasks and wait for all to complete
$delaySeconds = 10
Write-Log " Checking storage copy job status every $delaySeconds seconds."
do
{
$continue = $false
$progressId = 1
foreach ($copyTask in $copyTasks )
{
# [SS: For some reason we get some non blob copy tasks in the collection so we need to filter these out]
# Check the copy state for the blob
if ($copyTask.ICloudBlob -ne $null )
{
$copyState = $copyTask | Get-AzureStorageBlobCopyState
$copyStatus = $copyState.Status
# Display progress
Write-Progress - Id $progressId - Activity " Copying..." - PercentComplete (($copyState.BytesCopied / $copyState.TotalBytes )* 100 ) - CurrentOperation $copyTask.Name - Status $copyState.Status
}
else { $copyStatus = [Microsoft.WindowsAzure.Storage.Blob.CopyStatus ]::Invalid }
# Continue checking status as long as at least one operations is still pending
if ($copyStatus -eq [Microsoft.WindowsAzure.Storage.Blob.CopyStatus ]::Pending ) { $continue = $true }
$progressId += 1
}
# Pause if we are checking again
if ($continue ) { Start-Sleep - Seconds $delaySeconds }
} while ($continue )
Write-Log " All async tasks have completed. Check output for failures."
# Display final state
$copyTasks | Get-AzureStorageBlobCopyState | Format-Table - AutoSize - Property Status, BytesCopied, TotalBytes, Source
}
# endregion Functions
# region === Script Execution ================================================
try
{
#
# region - Shutdown and Export VM configuration
#
# Set source subscription context
Select-AzureSubscription - SubscriptionName $SourceSubscription - Current
# Stop VM
Stop-AzureVMAndWait - ServiceName $SourceServiceName - VMName $VMName
# Export VM config to temporary file
$exportPath = " {0}\{1}-{2}-State.xml" -f $ScriptPath , $SourceServiceName , $VMName
Export-AzureVM - ServiceName $SourceServiceName - Name $VMName - Path $exportPath
if (-not (Test-Path $exportPath ))
{
throw " Failed to export VM state. Aborting..."
}
# endregion
#
#
# region - Copy all attached VHDs to destination storage using async copy jobs
#
# Get list of azure disks that are currently attached to the VM
$disks = Get-AzureDisk | ? { $_.AttachedTo.RoleName -eq $VMName }
# Loop through each disk
foreach ($disk in $disks )
{
try
{
# Start the async copy of the underlying VHD to the corresponding destination storage account
$copyTasks += Copy-AzureDiskAsync - SourceDisk $disk
}
catch {} # Support for existing VHD in destination storage account
}
# Monitor async copy tasks and wait for all to complete
WaitAll- AsyncCopyJobs
# endregion
#
#
# region - Re-construct OS and Data disks
#
# Set destination subscription context
Select-AzureSubscription - SubscriptionName $DestSubscription - Current
# Load VM config
$vmConfig = Import-AzureVM - Path $exportPath
# Loop through each disk again
$diskNum = 0
foreach ($disk in $disks )
{
# Construct new Azure disk name as [DestServiceName]-[VMName]-[Index]
$destDiskName = " {0}-{1}-{2}" -f $DestServiceName , $VMName , $diskNum
Write-Log " Checking if $destDiskName exists..."
# Check if an Azure Disk already exists in the destination subscription
$azureDisk = Get-AzureDisk - DiskName $destDiskName - ErrorAction SilentlyContinue - ErrorVariable LastError
if ($azureDisk -ne $null )
{
Write-Log " $destDiskName already exists"
if ($RemoveDisk -eq $true )
{
# Remove the disk from the repository
Remove-AzureDisk - DiskName $destDiskName
Write-Log " Removed AzureDisk $destDiskName "
$azureDisk = $null
}
# else keep the disk and continue
}
# Determine media location
$container = ($disk.MediaLink.Segments [1 ]).Replace(" /" , " " )
$blobName = $disk.MediaLink.Segments | Where-Object { $_ -like " *.vhd" }
$destMediaLocation = " http://{0}.blob.core.windows.net/{1}/{2}" -f $DestStorageAccountName , $container , $blobName
# Attempt to add the azure OS or data disk
if ($disk.OS -ne $null -and $disk.OS.Length -ne 0 )
{
# OS disk
if ($azureDisk -eq $null )
{
$azureDisk = Add-AzureDisk - DiskName $destDiskName - MediaLocation $destMediaLocation - Label $destDiskName - OS $disk.OS - ErrorAction SilentlyContinue - ErrorVariable LastError
}
# Update VM config
$vmConfig.OSVirtualHardDisk.DiskName = $azureDisk.DiskName
}
else
{
# Data disk
if ($azureDisk -eq $null )
{
$azureDisk = Add-AzureDisk - DiskName $destDiskName - MediaLocation $destMediaLocation - Label $destDiskName - ErrorAction SilentlyContinue - ErrorVariable LastError
}
# Update VM config
# Match on source disk name and update with dest disk name
$vmConfig.DataVirtualHardDisks.DataVirtualHardDisk | ? { $_.DiskName -eq $disk.DiskName } | ForEach-Object {
$_.DiskName = $azureDisk.DiskName
}
}
# Next disk number
$diskNum = $diskNum + 1
}
# endregion
#
#
# region - Restore VM in destination cloud service
#
Write-Log " Restoring $VMName to $DestServiceName ..."
# Restore VM
$existingVMs = Get-AzureService - ServiceName $DestServiceName | Get-AzureVM
if ($existingVMs -eq $null -and $DestVNETName.Length -gt 0 )
{
# Restore first VM to the cloud service specifying VNet
$vmConfig | New-AzureVM - ServiceName $DestServiceName - VNetName $DestVNETName - WaitForBoot
}
else
{
# Restore VM to the cloud service
$vmConfig | New-AzureVM - ServiceName $DestServiceName - WaitForBoot
}
# Startup VM
Start-AzureVMAndWait - ServiceName $DestServiceName - VMName $VMName
# endregion
#
}
catch
{
Write-Log " Exception caught while migrating $VMName . Details: $Error "
}
# endregion Script Execution