Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save MauricioZa/2743c669d889a59f4b839acba5b2f692 to your computer and use it in GitHub Desktop.

Select an option

Save MauricioZa/2743c669d889a59f4b839acba5b2f692 to your computer and use it in GitHub Desktop.
# Azure VM Restore Runbook - Full VM restore from Recovery Services Vault
# For use in Azure Automation Account
# Uses Managed Identity for authentication
param(
[Parameter(Mandatory=$true)]
[string]$SubscriptionId,
[Parameter(Mandatory=$true)]
[string]$VaultName,
[Parameter(Mandatory=$true)]
[string]$VMName,
[Parameter(Mandatory=$true)]
[string]$restoredVmName,
[Parameter(Mandatory=$true)]
[string]$TargetResourceGroupName,
[Parameter(Mandatory=$true)]
[string]$StagingStorageAccountResourceGroup,
[Parameter(Mandatory=$true)]
[string]$StagingStorageAccountName,
[Parameter(Mandatory=$false)]
[int]$RecoveryPointIndex = 0,
[Parameter(Mandatory=$false)]
[int]$MaxDaysBack = 60,
[Parameter(Mandatory=$false)]
[int]$RestoreTimeoutMinutes = 720
)
# Connect to Azure using Managed Identity
try {
Write-Output "Connecting to Azure using Managed Identity..."
Connect-AzAccount -Identity -ErrorAction Stop
Write-Output "Successfully connected to Azure"
} catch {
Write-Error "Failed to connect to Azure using Managed Identity: $($_.Exception.Message)"
exit 1
}
# Set the subscription context
try {
Write-Output "Setting subscription context to: $SubscriptionId"
Set-AzContext -SubscriptionId $SubscriptionId -ErrorAction Stop
$context = Get-AzContext
Write-Output "Successfully set context to subscription: $($context.Subscription.Name)"
} catch {
Write-Error "Failed to set subscription context: $($_.Exception.Message)"
exit 1
}
# region Get Recovery Services Vault and VM details
# Get the Recovery Services vault
try {
Write-Output "Retrieving Recovery Services vault: $VaultName"
$vault = Get-AzRecoveryServicesVault -Name $VaultName -ErrorAction Stop
Write-Output "Found Recovery Services vault: $($vault.Name) (Resource Group: $($vault.ResourceGroupName))"
Set-AzRecoveryServicesVaultContext -Vault $vault
Write-Output "Set vault context successfully"
} catch {
Write-Error "Recovery Services vault '$VaultName' not found: $($_.Exception.Message)"
exit 1
}
# Get the backup item for the specified VM
try {
Write-Output "Searching for backup item for VM: $VMName"
$namedContainer = Get-AzRecoveryServicesBackupContainer -ContainerType "AzureVM" -FriendlyName $VMName -VaultId $vault.ID -ErrorAction Stop
$backupitem = Get-AzRecoveryServicesBackupItem -Container $namedContainer -WorkloadType "AzureVM" -VaultId $vault.ID -ErrorAction Stop
if (!$backupItem) {
throw "No backup data found for VM '$VMName'"
}
Write-Output "Backup item found for VM: $($backupItem.Name)"
} catch {
Write-Error "Failed to find backup item for VM '$VMName': $($_.Exception.Message)"
exit 1
}
# Get recovery points
try {
Write-Output "Retrieving recovery points for the last $MaxDaysBack days..."
$startDate = (Get-Date).AddDays(-$MaxDaysBack).ToUniversalTime()
$endDate = (Get-Date).ToUniversalTime()
$rpList = Get-AzRecoveryServicesBackupRecoveryPoint -Item $backupItem -StartDate $startDate -EndDate $endDate -VaultId $vault.ID
if (!$rpList -or $rpList.Count -eq 0) {
throw "No recovery points found for VM '$VMName' in the selected date range"
}
Write-Output "Found $($rpList.Count) recovery points for VM '$VMName'"
for ($i = 0; $i -lt [Math]::Min($rpList.Count, 10); $i++) {
$rp = $rpList[$i]
$timeUTC = [DateTime]$rp.RecoveryPointTime
$timeStr = $timeUTC.ToString("yyyy-MM-dd HH:mm:ss 'UTC'")
Write-Output "[$i] $timeStr ($($rp.RecoveryPointType))"
}
# Validate recovery point index
if ($RecoveryPointIndex -ge $rpList.Count -or $RecoveryPointIndex -lt 0) {
Write-Error "Invalid RecoveryPointIndex: $RecoveryPointIndex. Valid range: 0-$($rpList.Count - 1)"
exit 1
}
$chosenRP = $rpList[$RecoveryPointIndex]
Write-Output "Selected recovery point: Time: $($chosenRP.RecoveryPointTime), Type: $($chosenRP.RecoveryPointType)"
} catch {
Write-Error "Failed to retrieve recovery points: $($_.Exception.Message)"
exit 1
}
# Validate target resource group
try {
Write-Output "Validating target resource group: $TargetResourceGroupName"
$targetRG = Get-AzResourceGroup -Name $TargetResourceGroupName -ErrorAction Stop
Write-Output "Target resource group validated: $($targetRG.ResourceGroupName)"
} catch {
Write-Error "Target resource group '$TargetResourceGroupName' not found: $($_.Exception.Message)"
exit 1
}
# Validate staging storage account
try {
Write-Output "Validating staging storage account: $StagingStorageAccountName in RG: $StagingStorageAccountResourceGroup"
$storageAcct = Get-AzStorageAccount -ResourceGroupName $StagingStorageAccountResourceGroup -Name $StagingStorageAccountName -ErrorAction Stop
Write-Output "Staging storage account validated: $($storageAcct.StorageAccountName)"
} catch {
Write-Error "Storage account '$StagingStorageAccountName' not found in resource group '$StagingStorageAccountResourceGroup': $($_.Exception.Message)"
exit 1
}
# Handle tag policy exemptions
try {
Write-Output "Checking for 'Require a tag on resources' policy..."
$policyScope = $targetRG.ResourceId
$tagPolicyAssignments = Get-AzPolicyAssignment -Scope $policyScope | Where-Object { $_.properties.DisplayName -like "*Require a tag on resources*"}
if ($tagPolicyAssignments) {
foreach ($assignment in $tagPolicyAssignments) {
$exemptionName = "TempTagExemption-$($assignment.Name)-$(Get-Date -Format 'yyyyMMddHHmmss')"
$expiresOn = (Get-Date).AddHours(4)
try {
$assignmentScope = $assignment.properties.Scope
New-AzPolicyExemption -Name $exemptionName -PolicyAssignment $assignment -Scope $assignmentScope -ExemptionCategory "Waiver" -ExpiresOn $expiresOn -ErrorAction SilentlyContinue | Out-Null
Write-Output "Created 4-hour exemption for policy: $($assignment.Properties.DisplayName)"
} catch {
Write-Warning "Failed to create policy exemption: $($_.Exception.Message)"
}
}
} else {
Write-Output "No 'Require a tag on resources' policy assignments found"
}
} catch {
Write-Warning "Error checking policy assignments: $($_.Exception.Message)"
}
#endregion
# region Restore Process
# Initiate the restore job
try {
Write-Output "Initiating restore job (this may take several minutes)..."
$restoreJob = Restore-AzRecoveryServicesBackupItem -RecoveryPoint $chosenRP -StorageAccountName $StagingStorageAccountName -StorageAccountResourceGroupName $StagingStorageAccountResourceGroup -TargetResourceGroupName $targetRG.ResourceGroupName -VaultId $vault.ID -UseSystemAssignedIdentity -ErrorAction Stop
if ($restoreJob) {
Write-Output "Restore job initiated successfully!"
Write-Output "Job ID: $($restoreJob.JobId)"
Write-Output "Status: $($restoreJob.Status)"
} else {
throw "Failed to initiate restore job - no job object returned"
}
} catch {
Write-Error "Failed to initiate restore job: $($_.Exception.Message)"
exit 1
}
# Wait for the restore job to complete
try {
Write-Output "Waiting for restore job to complete (timeout: $RestoreTimeoutMinutes minutes)..."
Wait-AzRecoveryServicesBackupJob -Job $restoreJob -Timeout ($RestoreTimeoutMinutes * 60)
$restorejob = Get-AzRecoveryServicesBackupJob -Job $restorejob -VaultId $vault.ID
$details = Get-AzRecoveryServicesBackupJobDetail -Job $restorejob -VaultId $vault.ID
Write-Output "-------------------------"
Write-Output "RESTORE JOB DETAILS"
Write-Output "-------------------------"
Write-Output "Job ID: $($restorejob.JobId)"
Write-Output "Activity ID: $($restorejob.ActivityId)"
Write-Output "Job Type: $($restorejob.JobType)"
Write-Output "Operation: $($restorejob.Operation)"
Write-Output "Status: $($restorejob.Status)"
Write-Output "WorkloadType: $($restorejob.WorkloadType)"
Write-Output "Duration: $($restorejob.Duration)"
Write-Output "Start Time: $($restorejob.StartTime)"
Write-Output "End Time: $($restorejob.EndTime)"
if ($restorejob.ErrorDetails) {
Write-Output "Error Details:"
Write-Output "Error Code: $($restorejob.ErrorDetails.ErrorCode)"
Write-Output "Error Message: $($restorejob.ErrorDetails.ErrorMessage)"
Write-Output "Recommendations: $($restorejob.ErrorDetails.Recommendations)"
}
if ($details -and $details.SubTasks) {
Write-Output "Sub Tasks:"
$details.SubTasks | ForEach-Object {
Write-Output " - Task: $($_.Name)"
Write-Output " Status: $($_.Status)"
Write-Output " Start Time: $($_.StartTime)"
Write-Output " End Time: $($_.EndTime)"
}
}
# Check final status
if ($restorejob.Status -eq "Failed") {
Write-Error "Restore job failed: $($restorejob.ErrorDetails.ErrorMessage)"
exit 1
} elseif ($restorejob.Status -eq "Completed") {
Write-Output "Restore job completed successfully"
} else {
Write-Warning "Restore job status: $($restorejob.Status)"
}
} catch {
Write-Error "Error during restore job execution: $($_.Exception.Message)"
exit 1
}
#endregion
# region VM Deployment
# Create VM from restored disks
try {
Write-Output "Starting VM deployment from restored disks..."
$properties = $details.properties
$storageAccountName = $properties["Target Storage Account Name"]
$containerName = $properties["Config Blob Container Name"]
$templateBlobURI = $properties["Create VM Template Blob Uri"]
# Extract template name from the URI
$templateName = $templateBlobURI.Split('/')[-1]
Write-Output "Template name: $templateName"
# Set storage account context and create SAS token
Set-AzCurrentStorageAccount -Name $storageAccountName -ResourceGroupName $StagingStorageAccountResourceGroup
$templateBlobFullURI = New-AzStorageBlobSASToken -Container $containerName -Blob $templateName -Permission r -FullUri
# Create unique deployment name
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$deploymentName = "VM-Restore-$VMName-$timestamp"
Write-Output "Deployment name: $deploymentName"
# Deploy the VM
Write-Output "Starting VM deployment..."
$deployment = New-AzResourceGroupDeployment -Name $deploymentName -ResourceGroupName $TargetResourceGroupName -VirtualMachineName $restoredVmName -TemplateUri $templateBlobFullURI -ErrorAction Stop
} catch {
Write-Error "Failed to initiate VM deployment: $($_.Exception.Message)"
exit 1
}
# Monitor deployment results
try {
Write-Output "================================="
Write-Output "VM DEPLOYMENT RESULTS"
Write-Output "================================="
if ($deployment) {
$deploymentDetails = Get-AzResourceGroupDeployment -ResourceGroupName $TargetResourceGroupName -Name $deploymentName
Write-Output "Deployment Name: $($deploymentDetails.DeploymentName)"
Write-Output "Resource Group: $($deploymentDetails.ResourceGroupName)"
Write-Output "Timestamp: $($deploymentDetails.Timestamp)"
Write-Output "Duration: $($deploymentDetails.Duration)"
Write-Output "Deployment Status: $($deploymentDetails.ProvisioningState)"
if ($deploymentDetails.ProvisioningState -eq "Succeeded") {
Write-Output "VM deployment completed successfully!"
} else {
Write-Error "VM deployment failed with status: $($deploymentDetails.ProvisioningState)"
# Show error details
$operations = Get-AzResourceGroupDeploymentOperation -ResourceGroupName $TargetResourceGroupName -DeploymentName $deploymentName
$operations | Where-Object { $_.ProvisioningState -eq "Failed" } | ForEach-Object {
Write-Output "Failed Resource: $($_.Properties.TargetResource.ResourceName)"
Write-Output "Error Code: $($_.Properties.StatusMessage.Error.Code)"
Write-Output "Error Message: $($_.Properties.StatusMessage.Error.Message)"
}
exit 1
}
# Show deployment outputs
if ($deploymentDetails.Outputs) {
Write-Output "Deployment Outputs:"
$deploymentDetails.Outputs.PSObject.Properties | ForEach-Object {
Write-Output "$($_.Name): $($_.Value.Value)"
}
}
} else {
Write-Error "Failed to initiate VM deployment - no deployment object returned"
exit 1
}
} catch {
Write-Error "Error during VM deployment monitoring: $($_.Exception.Message)"
exit 1
}
#endregion
Write-Output "================================="
Write-Output "RUNBOOK EXECUTION COMPLETED"
Write-Output "================================="
Write-Output "VM '$VMName' has been successfully restored to resource group '$TargetResourceGroupName'"
Write-Output "Restore job ID: $($restorejob.JobId)"
Write-Output "Deployment name: $deploymentName"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment