Hyper-V backups with PowerShell

backup_iconНередко встречаются случаи, когда в организации используется один или несколько хостов под управлением Hyper-V, на которых расположены виртуальные машины с “инфраструктурой” и “бизнес-приложениями” (в кавычках, потому что граница между ними достаточно размыта).
В таком случае разумно выполнять резервное копирование виртуальных машин (расписание для “инфраструктурных” и “бизнесовых” обычно разное), резервное копирование конфигурации хостов и резервное копирования баз SQL сервера.
Резервное копирование в таких случаях чаще всего выполняется на USB диск или сетевую шару. Неплохо держать на UBS или локальном диске резервную копию “на подхвате”, а на сетевой шаре хранить бэкап на случай если что-то случится с основной площадкой.
Для таких задач использование “тяжелых” систем резервного копирования вроде SC DPM, Symantec, Acronis очевидно излишне.
Поэтому я решил написать несколько скриптов, которые будут создавать резервные копии, удалять старые и отправлять отчеты на электронную почту с шифрованием.

Резервное копирование виртуальных машин Hyper-V

Обычно для отправки отчетов используется отдельный почтовый ящик, пароль от которого не представляет особой ценности и поэтому хранится как plain text в теле скриптов.
Но во-первых я буду использовать свой основной почтовый ящик для отправки отчетов, а во-вторых хранить пароли в plain text плохо :)
Поэтому для начала сделаем “дамп” пароля (после выполнения командлета пароль нужно будет ввести):

Screen Shot 2015-06-11 at 18.46.35
Этот файл нужно будет создавать заново на каждом хосте, где вы будуте использовать этот скрипт.
Отправлять результаты будем не только на email, но и в Event Log – в инфраструктурах среднего и крупного размера это сильно удобнее. Для этого создадим в логе Application новый Source – “Backup scripts”:

.. что и как в него будет записываться:
Screen Shot 2015-06-18 at 17.55.02
Резервное копирование виртуальных машин можно делать с помощью их экспорта, например так:

# Hyper-V VMs Export script
# by Dmitriy Kagarlickij 
# dmitriy@kagarlickij.com

# Get hostname
$Hostname = hostname

# Get current date
$Date = Get-Date -Format dd-MMMM-yyyy_HH-mm

# Set backup path
$BackupPath = New-Item "G:\vms_export_$Date" -ItemType directory -Force

# Select running VMs
$VMs = Get-VM -ComputerName $Hostname | Where-Object {$_.State -eq "Running"}

# Export selected VMs
Export-VM $VMs -Path $BackupPath

$ExportedVMs = $VMs | Select-Object Name | Out-String

# Get result
if(-not $?) {
    $Result = "Failure"
    Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Warning -EventId 2 -Message "Hyper-V VMs Export $Result. Exported VMs: $ExportedVMs"
} else {
    $Result = "Success"
    Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Information -EventId 1 -Message "Hyper-V VMs Export $Result. Exported VMs: $ExportedVMs"
} 

# Send result to email
$From = "dmitriy@kagarlickij.com"
$To = "dmitriy@kagarlickij.com"
$Subject = "Hyper-V VMs Export on $Hostname $Date $Result"

$Body = "Hyper-V VMs Export on $Hostname $Date <br>"
$Body += "Result: <b>$Result</b> <br>"
$Body += "<br>"
$Body += "Exported VMs: $ExportedVMs <br>"

$SMTPServer = "smtp.office365.com"
$SMTPPort = "587"

$MailPass = Get-Content C:\scripts\MailboxSecurePass.txt | ConvertTo-SecureString
$MailCred = New-Object -TypeName System.Management.Automation.PSCredential  `
-argumentlist "dmitriy@kagarlickij.com", $MailPass

Send-MailMessage -From $From -to $To -Subject $Subject `
-Body $Body -BodyAsHtml -SmtpServer $SMTPServer -port $SMTPPort -UseSsl `
-Credential $MailCred

# Variables cleanup
Remove-Variable -Name * -ErrorAction SilentlyContinue

Screen Shot 2015-06-11 at 18.43.54

Screen Shot 2015-06-11 at 18.35.50

Screen Shot 2015-06-11 at 18.37.07

Такой вариант не очень удобен, и хотя его можно улучшить, лучше воспользоваться HVBackup:

# Hyper-V VMs HVBackup script
# by Dmitriy Kagarlickij 
# dmitriy@kagarlickij.com

# Get hostname
$Hostname = hostname

# Get current date
$Date = Get-Date -Format dd-MMMM-yyyy_HH-mm

# Set backup path
$BackupPath = "G:\VMs_HVBackup"

# Set logs location
$LogsPath = "C:\scripts\HVBackup\logs"

# Set maximum age of backups
$DelDate = (Get-Date).AddDays(-2)

# Delete old backup files
Get-ChildItem $BackupPath -Recurse | Where-Object {$_.LastWriteTime -lt $DelDate} | Remove-Item -Recurse

# Delete old log files
Get-ChildItem $LogsPath -Recurse | Where-Object {$_.LastWriteTime -lt $DelDate} | Remove-Item -Recurse

# Set Operations Log
$OperationsLogName = "vms_HVBackup_operations_log_$Date.txt"
$OperationsLog = New-Item -ItemType File -Name $OperationsLogName -Path $LogsPath -Force

# Set Error Log
$ErrorLogName = "vms_HVBackup_error_log_$Date.txt"
$ErrorLog = New-Item -ItemType File -Name $ErrorLogName -Path $LogsPath -Force

# VMs list
$VMsList = "C:\scripts\HVBackup\vms_list.txt"

# VMs list string
$VMsListString = Get-Content "C:\scripts\HVBackup\vms_list.txt" | Out-String

# Run HVBackup using file with VMs list
C:\scripts\HVBackup\.\HVBackup.exe --f $VMsList --compressionlevel 0 --output $BackupPath 1> $OperationsLog 2> $ErrorLog

$ErrorLogContent = Get-Content $ErrorLog | Out-String
$OperationsLogContent = Get-Content $OperationsLog | Out-String

if ($ErrorLog.length -gt 0Kb) {
    $Result = "Failure"
    Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Warning -EventId 2 `
    -Message "Hyper-V VMs HVBackup Failure. Backuped VMs: $VMsListString `n Logspath = $LogsPath"
} else {
    $Result = "Success"    
    Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Information -EventId 1 `
    -Message "Hyper-V VMs HVBackup Success. Backuped VMs: $VMsListString `n  Logspath = $LogsPath"
}

# Send logs via email
$From = "dmitriy@kagarlickij.com"
$To = "dmitriy@kagarlickij.com"
$Subject = "Hyper-V VMs HVBackup on $Hostname $Date $Result"

$Body = "Hyper-V VMs HVBackup on $Hostname $Date <br>"
$Body += "Result: <b>$Result</b> <br>"
$Body += "<br>"
$Body += "Backuped VMs: $VMsListString <br>"

$SMTPServer = "smtp.office365.com"
$SMTPPort = "587"

$MailPass = Get-Content C:\scripts\MailboxSecurePass.txt | ConvertTo-SecureString
$MailCred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist "dmitriy@kagarlickij.com", $MailPass

Send-MailMessage -From $From -to $To -Subject $Subject `
-Body $Body -BodyAsHtml -SmtpServer $SMTPServer -port $SMTPPort -UseSsl `
-Credential $MailCred 

# Variables cleanup
Remove-Variable -Name * -ErrorAction SilentlyContinue

Screen Shot 2015-06-11 at 21.50.28

Screen Shot 2015-06-18 at 17.35.39

Screen Shot 2015-06-18 at 17.29.53

Если вам нужно хранить резервные копии в еще одном месте, их можно копировать по расписанию (например раз в сутки или неделю) с помощью такого скрипта:

# VMs backup copy script
# by Dmitriy Kagarlickij 
# dmitriy@kagarlickij.com
 
# Get Hostname
$Hostname = Hostname
 
# Get current Date
$Date = Get-Date -Format dd-MMMM-yyyy_HH-mm
 
# Free target backup share name
Get-PSDrive -Name X -ErrorAction SilentlyContinue
if(-not $?) {
} else {
    Remove-PSDrive -Name X -Force
}
 
$TargetBackupPass = Get-Content C:\scripts\fs1SecurePass.txt | ConvertTo-SecureString
$TargetBackupCred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist "bckp_usr", $TargetBackupPass
 
# Connect target backup share
New-PSDrive -Name X -PSProvider FileSystem -Root \\fs1.bckp.lab\bckp_share\vms -Credential $TargetBackupCred

# Check target backup path 
$BackupPath = Test-Path -Path X:\

if($BackupPath -ne 'True') {
    $Result = "Failure"
    Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Warning -EventId 2 -Message "Target backup path connection $Result"
} else {
    # Set target backup path
    Set-Location X:\
    $TargetBackupPath = New-Item -ItemType directory -Name $Date -Force

    # Copy all backups
    Get-ChildItem G:\VMs_HVBackup | Where-Object {$_.Name -like "*zip"} | `
    Copy-Item -destination $TargetBackupPath -Recurse -Container -ErrorAction SilentlyContinue

    # Get copied items
    $CopiedItems = Get-ChildItem $TargetBackupPath

    #Compare hashes
    $SourceHashes = Get-ChildItem G:\VMs_HVBackup | Get-FileHash -Algorithm MD5 | Select-Object Hash 
    $TargetHashes = Get-ChildItem $TargetBackupPath | Get-FileHash -Algorithm MD5 | Select-Object Hash

    $HashesCompare = Compare-Object -ReferenceObject $SourceHashes -DifferenceObject $TargetHashes -IncludeEqual | Select-Object SideIndicator

    if (($HashesCompare -like '*<*') -or ($HashesCompare -like '*>*')) {
        $Result = "Failure"
        Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Warning -EventId 2 -Message "VMs backup copy $Result. Copied Items: $CopiedItems"
    } else {
        $Result = "Success"
        Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Information -EventId 1 -Message "VMs backup copy $Result. Copied Items: $CopiedItems"
    }

# Back to source location
Set-Location G:\
 
# Disconnect target backup share
Remove-PSDrive -Name X
}

# Send logs via email
$From = "dmitriy@kagarlickij.com"
$To = "dmitriy@kagarlickij.com"
$Subject = "VMs backup copy on $Hostname $Date $Result"

$Body = "VMs backup copy on $Hostname $Date <br>"
$Body += "Result: <b>$Result</b> <br>"
$Body += "<br>"
$Body += "Copied Items: $CopiedItems <br>"

$SMTPServer = "smtp.office365.com"
$SMTPPort = "587"

$MailPass = Get-Content C:\scripts\MailboxSecurePass.txt | ConvertTo-SecureString
$MailCred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist "dmitriy@kagarlickij.com", $MailPass

Send-MailMessage -From $From -to $To -Subject $Subject `
-Body $Body -BodyAsHtml -SmtpServer $SMTPServer -port $SMTPPort -UseSsl `
-Credential $MailCred

# Variables cleanup
Remove-Variable -Name * -ErrorAction SilentlyContinue

Screen Shot 2015-06-18 at 17.46.23

Screen Shot 2015-06-18 at 17.49.08

Что касается восстановления, его я выполняю не с помощью HVBackup, а вручную.

Для этого распакуем архив и сделаем из него импорт виртуальной машины. Т.к. восстановление задача относительно редкая, и может проходить каждый раз с новыми условиями, есть смысл использовать не скрипт, а GUI:

Screen Shot 2015-05-24 at 21.25.17

Screen Shot 2015-05-24 at 21.30.01

Условно виртуальные машины можно поделить на infrastructure, production и lab, для каждого из типов скорректировать скрипт и настроить расписание.

Как показывает практика, несмотря на то что предложенное решение простое и бесплатное, оно в состоянии удовлетворить потребности большого количества небольших предприятий.

Резервное копирование хоста Hyper-V

Для резервного копирования хоста Hyper-V нам нужно будет Bare metal recovery, создадим соответствующее расписание с помощью Windows Server Backup:

Screen Shot 2015-05-31 at 20.05.15

Напомню, на компьютере, резервное копирование которого будет выполнятся должен присутствовать такой же пользователь как и на сетевой шаре, с таким же паролем – на время тестов я включил его в группу Backup Operators.

На сетевой шаре будет доступна только последняя резервная копия:

Screen Shot 2015-06-18 at 21.10.01

Screenshot at Jun 18 23-15-47

В моем случае я бэкаплю с помощью WBS только System state, так что мне этого будет вполне достаточно.

Если вам нужно несколько версий, или не только System State, есть смысл делать бэкапы на отдельный диск, тип бэкапа дифференциальный, так что с местом все будет ок:

Screenshot at Jun 18 21-37-27

Можно, конечно, извратится и таки сохранять на сетевой шаре несколько копий WSB, но т.к. виртуалки мы бэкапим отдельно то смысла в этом не так чтобы много.

Если все-таки решитесь на это, обратите внимание что старые резервные копии с сетеовой шары нужно будет удалять вручную или скриптом (только папки с именем Backup %date%):

# WSB old copies delete
# by Dmitriy Kagarlickij 
# dmitriy@kagarlickij.com

# Get hostname
$Hostname = hostname

# Get current date
$Date = Get-Date

# Set backup path
$BackupPath = "G:\WindowsImageBackup\host"
 
# Set maximum age of backups
$DelDate = $Date.AddHours(-1)
 
# Delete old backups
Get-ChildItem $BackupPath -Attributes Directory -Recurse | Where-Object { $_.LastWriteTime -lt $DelDate -and $_.Name -like 'Backup*'} | Remove-Item -Recurse -Force

# Check old backups
$OldBackups = Get-ChildItem $BackupPath -Attributes Directory -Recurse | Where-Object { $_.LastWriteTime -lt $DelDate -and $_.Name -like 'Backup*'}

if ($OldBackups -ne $null) {
    $Result = "Failure"
    Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Warning -EventId 2 -Message "WSB old copies delete $Result."    
} else {
    $Result = "Success"
    Write-EventLog -LogName Application -Source "Backup scripts" -EntryType Information -EventId 1 -Message "WSB old copies delete $Result `n Remaining copies - $OldBackups"
}

# Send logs via email
$From = "dmitriy@kagarlickij.com"
$To = "dmitriy@kagarlickij.com"
$Subject = "WSB old copies delete on $Hostname $Date $Result"

$Body = "WSB old copies delete on $Hostname $Date <br>"
$Body += "Result: <b>$Result</b> <br>"
$Body += "<br>"

$SMTPServer = "smtp.office365.com"
$SMTPPort = "587"

$MailPass = Get-Content C:\scripts\MailboxSecurePass.txt | ConvertTo-SecureString
$MailCred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist "dmitriy@kagarlickij.com", $MailPass

Send-MailMessage -From $From -to $To -Subject $Subject `
-Body $Body -BodyAsHtml -SmtpServer $SMTPServer -port $SMTPPort -UseSsl `
-Credential $MailCred

# Variables cleanup
Remove-Variable -Name * -ErrorAction SilentlyContinue

Screen Shot 2015-06-19 at 17.33.30

Screen Shot 2015-06-19 at 17.33.54

Результаты работы WSB можно отправлять на email из Task Scheduler, но я не хочу отправлять эту почту без шифрования, поэтому из Task Scheduler в случае успеха (ID 4):

Screen Shot 2015-05-31 at 20.17.36

… я буду запускать такой скрипт:

# WSB succeed alert
# by Dmitriy Kagarlickij 
# dmitriy@kagarlickij.com

# Get hostname
$Hostname = hostname

# Get current date
$Date = Get-Date -Format dd-MMMM-yyyy_HH-mm

# Send logs via email
$From = "dmitriy@kagarlickij.com"
$To = "dmitriy@kagarlickij.com"
$Subject = "WSB on $Hostname $Date succeed"

$Body = "WSB on $Hostname $Date <b>succeed</b><br>"

$SMTPServer = "smtp.office365.com"
$SMTPPort = "587"

$MailPass = Get-Content C:\scripts\MailboxSecurePass.txt | ConvertTo-SecureString
$MailCred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist "dmitriy@kagarlickij.com", $MailPass

Send-MailMessage -From $From -to $To -Subject $Subject `
-Body $Body -BodyAsHtml -SmtpServer $SMTPServer -port $SMTPPort -UseSsl `
-Credential $MailCred

# Variables cleanup
Remove-Variable -Name * -ErrorAction SilentlyContinue

.. и получать такое уведомление:

Screen Shot 2015-06-19 at 17.36.23

А в случае ошибок (ID 5,8,9,17,22,49,50,52,100,517,518,521,527,528,544,545,546,561,564,612):

Screen Shot 2015-05-31 at 20.27.36

.. запускать немного другой скрипт:

# WSB failure alert
# by Dmitriy Kagarlickij 
# dmitriy@kagarlickij.com

# Get hostname
$Hostname = hostname

# Get current date
$Date = Get-Date -Format dd-MMMM-yyyy_HH-mm

# Send logs via email
$From = "dmitriy@kagarlickij.com"
$To = "dmitriy@kagarlickij.com"
$Subject = "WSB on $Hostname $Date failure"

$Body = "WSB on $Hostname $Date <b>failure</b><br>"

$SMTPServer = "smtp.office365.com"
$SMTPPort = "587"

$MailPass = Get-Content C:\scripts\MailboxSecurePass.txt | ConvertTo-SecureString
$MailCred = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist "dmitriy@kagarlickij.com", $MailPass

Send-MailMessage -From $From -to $To -Subject $Subject `
-Body $Body -BodyAsHtml -SmtpServer $SMTPServer -port $SMTPPort -UseSsl `
-Credential $MailCred

# Variables cleanup
Remove-Variable -Name * -ErrorAction SilentlyContinue

.. и получать соответствующее уведомление:

Screen Shot 2015-06-19 at 18.01.35

Если вы выполняете резервное копирование на диск, есть смысл сохранять вторую копию на сетевой шаре, сделать это можно с помощью скрипта, аналогичного тому, который я приводил для виртуальных машин.

SQL

Для резервного копирования SQL будем использовать встроенные средства, благо они достаточно хороши.

Для начала включим Autostart для SQL Server Agent:

Screen Shot 2015-06-18 at 18.20.43

Затем создадим оператора:

Screen Shot 2015-06-18 at 18.24.11

Запустим Database Mail Configuration Wizard:

Screenshot at Jun 18 18-55-23

Screen Shot 2015-06-18 at 18.56.03

Screen Shot 2015-06-18 at 19.13.23

Screen Shot 2015-06-18 at 18.59.59

Screen Shot 2015-06-18 at 19.00.29

Теперь включим все это дело в SQL Agent:

Screen Shot 2015-06-18 at 19.01.57

Используя мастер создадим Maintenance Plan:

Screen Shot 2015-06-19 at 17.56.17

Screen Shot 2015-06-19 at 17.57.55

.. и проверим его результат:

Screenshot at Jun 19 18-04-08

Как и с Windows Server Backup, для SQL резервное копирование выполнять удобнее на локальный диск, а затем копировать содержимое на сетевую шару.

Но в ряде случаев необходимо создавать резервные копии сразу на сетевую шару, для этого будем использовать процедуру с net use:

Проверить очень просто:

Screen Shot 2015-06-19 at 18.38.40

Screen Shot 2015-06-19 at 18.39.17

Screen Shot 2015-06-19 at 18.57.43

Теперь можно скорректировать Maintenance Plan:

Screen Shot 2015-06-19 at 18.59.52

Screen Shot 2015-06-19 at 19.00.33

Если вы выполняете резервное копирование на диск, есть смысл сохранять вторую копию на сетевой шаре, сделать это можно с помощью скрипта, аналогичного тому, который я приводил для виртуальных машин.

Заключение

Чтобы выполнять скрипты регулярно, запускать их можно из Task Scheduler:

Screen Shot 2015-05-24 at 20.25.36

В качестве альтернативы можно предложить использование Azure Backup – но о нем в следующий раз =)

Надеюсь озвученная информация будет полезной, а если нужна будет помощь — используйте форму на главной странице моего сайта.