During one of my projects, I worked on scripting a procedure for backing up virtual machines with the Windows Server 2003 / 2008 operating system. Microsoft provides Azure Backup Services for protecting virtual machines hosted in Azure. However, that service does not officially support legacy operating systems older than Windows Server 2008 R2. The main goal was to write a script that uses PowerShell parallelism and Azure Automation functionality.
The following piece of code contains the automation of copying disks in parallel for many virtual machines and their disks. The script uses asynchronous data transfer within the storage account and deletes backups older than the specified amount in script parameters. Each virtual machine has a tag that defines maintenance group called ‘BackupGroupName’.
Runbooks performs the following tasks:
• Stop virtual machine
• Copy all virtual machine VHD files to defined storage account
• Start virtual machine
• Check and clean-up old backup files – only keeping last backup files defined in runbook configuration
———————————————
workflow Start-BackupLegacyVM
{
# Define output data type
[OutputType([object])]
#Input parameters
param (
# Maintenace group parameters
[Parameter(Mandatory=$true)]
[string]$BackupGroupName = “01”,
[Parameter(Mandatory=$true)]
[string]$BackupStorageAccount = “saeunstlrsbbvmbackup01”,
[Parameter(Mandatory=$true)]
[int]$NumberOfBackups = 3
)
# Define time zone
$timeZone = “GMT Standard Time”
# Specify Action Preferences
$ErrorActionPreference = ‘Stop’
$PSPersistPreference = $true
# Logging to Azure
write-output “————————`nLogging to Azure`n————————”
$connectionName = “AzureRunAsConnection”
$servicePrincipalConnection = Get-AutomationConnection -Name $connectionName
$Output = Add-AzureRmAccount `
-ServicePrincipal `
-TenantId $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
# Get all virtual machines objects tagged with BackupMG
$virtualMachines = Get-AzureRMVM | Where-Object -FilterScript {$_.Tags.Keys -eq “BackupGroupName” -and $_.Tags.Values -eq “$BackupGroupName”}
$VMs = $($virtualMachines | measure-object).Count
# For each virtual machine start in parrallel copy process
#——————————————
If ($VMs -gt 0) {
write-output “————————`nVirtual machines log`n————————”
$VMDetails = $virtualMachines | Select-Object Name, ResourceGroupName
write-output $VMDetails
write-output “`n”
foreach -parallel ($VM in $virtualMachines) {
# Main code
# —————————————————————————-
inlinescript
{
function Get-LocalTime {
param ($timeZone)
$UTCTime = (Get-Date).ToUniversalTime()
$TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($timeZone)
$localTime = [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TZ)
return $localTime
}
try {
# Get variables outside inlinescript
$storageAccount = $Using:BackupStorageAccount
$numberofBackups = $Using:NumberOfBackups
$timeZone = $Using:timeZone
$vm = Get-AzureRMVM -Name ($Using:VM.Name) -ResourceGroupName ($Using:VM.ResourceGroupName)
# Define static variables
$CopyStatus = @{}
$CopyIndex = 0
# Get virtual machine status
$vmStatus = $($VM | Get-AzureRmVM -Status).Statuses[1].Code
# Stop virtual machine if it is not in stopped / deallocated state
# ————————-
If ($vmStatus -ne “PowerState/deallocated”) {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Stop virtual machine”
$output = Stop-AzureRmVM -Name $($vm.Name) -ResourceGroupName $($vm.ResourceGroupName) -Force
}
else {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Virtual machine is in stopped (deallocated) state”
}
# Set destination storage settings before copy process start (storage object, key and context)
$dstStorageAccountObject = Get-AzureRmStorageAccount | Where-Object {$_.StorageAccountName -eq $storageAccount}
$dstStorageKey = $(Get-AzureRmStorageAccountKey -ResourceGroupName $dstStorageAccountObject.ResourceGroupName -StorageAccountName $dstStorageAccountObject.StorageAccountName).Key1
$dstContext = New-AzureStorageContext -StorageAccountName $dstStorageAccountObject.StorageAccountName -StorageAccountKey $dstStorageKey
$dstDiskDate = “_” + $(Get-Date -Format yyyyMMddHHmm)
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Set destination storage settings before copy process start (storage object, key and context)”
# Get information about virtual machine disks
$vmOSDisks = $vm.StorageProfile.osDisk
$vmDataDisks = $vm.StorageProfile.dataDisks
# Start copy process for OS disk
# ————————-
$srcDiskUri = $vmOSDisks.Vhd.Uri
$srcStorageAccount = $srcDiskUri.Substring(8,$srcDiskUri.IndexOf(“.”)-8)
$srcStorageAccountObject = Get-AzureRmStorageAccount | Where-Object -FilterScript {$_.StorageAccountName -eq $srcStorageAccount}
$srcStorageKey = $(Get-AzureRmStorageAccountKey -ResourceGroupName $srcStorageAccountObject.ResourceGroupName -StorageAccountName $srcStorageAccountObject.StorageAccountName).Key1
$srcContext = New-AzureStorageContext -StorageAccountName $srcStorageAccountObject.StorageAccountName -StorageAccountKey $srcStorageKey
$dstDiskName = $vmOSDisks.Name + $dstDiskDate
# If destination container (virtual machine name) doesn’t exist create new one / ErrorAction set to SilentlyContinue in case of existing container
$Output = New-AzureStorageContainer -Name $vm.Name.ToLower() -Permission Container -Context $dstContext -ErrorAction SilentlyContinue
# Start copy process
$blob = Start-AzureStorageBlobCopy -SrcUri $srcDiskUri -SrcContext $srcContext -DestContainer $vm.Name.ToLower() -DestBlob “$dstDiskName.vhd”.ToLower() -DestContext $dstContext -Force
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Start copy process for OS disk – $($vmOSDisks.Name) – file name: $dstDiskName.vhd”
$CopyStatus[$CopyIndex] = $blob
$CopyIndex++
# Start copy process for DATA disks if there is more than zero
# ————————-
If ($vmDataDisks.Count -gt 0) {
foreach ($dataDisk in $vmDataDisks) {
# Set source storage settings and destination disk name
$srcDiskUri = $dataDisk.Vhd.Uri
$srcStorageAccount = $srcDiskUri.Substring(8, $srcDiskUri.IndexOf(“.”) – 8)
$srcStorageAccountObject = Get-AzureRmStorageAccount | Where-Object -FilterScript {$_.StorageAccountName -eq $srcStorageAccount}
$srcStorageKey = $(Get-AzureRmStorageAccountKey -ResourceGroupName $srcStorageAccountObject.ResourceGroupName -StorageAccountName $srcStorageAccountObject.StorageAccountName).Key1
$srcContext = New-AzureStorageContext -StorageAccountName $srcStorageAccountObject.StorageAccountName -StorageAccountKey $srcStorageKey
$dstDiskName = $dataDisk.Name + $dstDiskDate
# Start copy process
$blob = Start-AzureStorageBlobCopy -SrcUri $srcDiskUri -SrcContext $srcContext -DestContainer $vm.Name.ToLower() -DestBlob “$dstDiskName.vhd”.ToLower() -DestContext $dstContext -Force
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Start copy process for DATA disk – $($dataDisk.Name) – file name: $dstDiskName.vhd”
$CopyStatus[$CopyIndex] = $blob
$CopyIndex++
}
}
Else {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Virtual machine doesn’t have any DATA disks”
}
# Check all copies status
# ————————-
for($i=0;$i -lt $CopyIndex; $i++){
$status = $CopyStatus[$i] | Get-AzureStorageBlobCopyState
While($status.Status -eq “Pending”){
Start-Sleep -Seconds 60
$status = $CopyStatus[$i] | Get-AzureStorageBlobCopyState
}
}
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – All copies finished”
# Start virtual machine
# ————————-
If ($vmStatus -ne “PowerState/deallocated”) {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Virtual machine is starting.”
$output = Start-AzureRmVM -Name $vm.Name -ResourceGroupName $vm.ResourceGroupName
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Virtual is running.”
}
# Delete old backups on retention policy
# —————————————
$blobs = Get-AzureStorageBlob -Context $dstContext -Container $vm.Name.ToLower()
$backupGUIDs = @()
# Get timestamp which is uniq ID of backup instance
foreach ($blob in $blobs) {
If ($backupGUIDs -notcontains $($blob.Name.Substring($blob.Name.Length-16,12))) {
$backupGUIDs += $blob.Name.Substring($blob.Name.Length-16,12)
}
}
# Sort all backup instances
$backupGUIDs = $backupGUIDs | Sort-Object
# If there is more backup instances than specified, remove old one
If ($backupGUIDs.Count -gt $numberOfBackups) {
# Count number of backups to remove
$items = $backupGUIDs.Count – $numberOfBackups
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Number of stored backups to delete – $items”
$backupItemsToRemove = @()
# Get all backup GUIDs (timestamp yyyyMMddHHmm) to remove
for ($i = 0; $i -lt $items; $i++) {
$backupItemsToRemove += $backupGUIDs[$i]
}
# Remove old backups
foreach ($blob in $blobs) {
if ($backupItemsToRemove -contains $($blob.Name.Substring($blob.Name.Length-16,12))) {
$blob | Remove-AzureStorageBlob -Force
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Deleted old backup – file: $($blob.Name)”
}
}
}
else {
Write-Output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Number of stored backups is less than specified – files have not been removed.”
}
}
catch
{
write-output “$(Get-LocalTime($timeZone)) – $($vm.Name) – Found error: $($_.Exception.Message)”
}
}
# —————————————————————————-
}
}
ElseIf ($VMs -eq 0) {
write-output “Virtual machines with tag BackupGroupName – $BackupGroupName haven’t been defined”
}
Else {
write-output “Return information are in unknown format.”
}
}