Резервное копирование виртуальных дисков Hyper-V с помощью PowerShell

Делаем резервные копии виртуальных дисков Hyper-V без специального софта средствами PowerShell.

Создаем скрипт powershell с таким содержимым

$Destination="D:\Backup" #место хранения копий
$Versions="2" #количество хранимых копий
$BackupDirs="D:\Hyper-V\Virtual Hard Disks" #место расположения данных для копирования
$Log="Log.txt" #имя лог-файла
$LoggingLevel="1" #уровень логирования - 1=smart, 3=Heavy

#настройки E-mail
$SMTPServer = "smtp.domain.ru"
$SMTPPort = "587"
$Username = "support@domain.ru"
$Password = "emailpassword"
$to = "admin@domain.ru"
$subject = "DOMAIN.ru VM-HDD Backup Log"
$body = "Файл лога резервного копирования во вложении"


#STOP-no changes from here
#Settings - do not change anything from here
$Backupdir=$Destination +"\Backup-"+ (Get-Date -format yyyy-MM-dd)+"-"+(Get-Random -Maximum 100000)+"\"
$Items=0
$Count=0
$ErrorCount=0
$StartDate=Get-Date #-format dd.MM.yyyy-HH:mm:ss

#FUNCTION
#Logging
Function Logging ($State, $Message) {
$Datum=Get-Date -format dd.MM.yyyy-HH:mm:ss

if (!(Test-Path -Path $Log)) {
New-Item -Path $Log -ItemType File | Out-Null
}
$Text="$Datum - $State"+":"+" $Message"

if ($LoggingLevel -eq "1" -and $Message -notmatch "was copied") {Write-Host $Text}
elseif ($LoggingLevel -eq "3" -and $Message -match "was copied") {Write-Host $Text}

add-Content -Path $Log -Value $Text
}
Logging "INFO" "----------------------"
Logging "INFO" "Start the Script"

#Create Backupdir
Function Create-Backupdir {
Logging "INFO" "Create Backupdir $Backupdir"
New-Item -Path $Backupdir -ItemType Directory | Out-Null

Logging "INFO" "Move Log file to $Backupdir"
Move-Item -Path $Log -Destination $Backupdir

Set-Location $Backupdir
Logging "INFO" "Continue with Log File at $Backupdir"
}

#Delete Backupdir
Function Delete-Backupdir {
$Folder=Get-ChildItem $Destination | where {$_.Attributes -eq "Directory"} | Sort-Object -Property $_.LastWriteTime -Descending:$false | Select-Object -First 1

Logging "INFO" "Remove Dir: $Folder"

$Folder.FullName | Remove-Item -Recurse -Force 
}

#Check if Backupdirs and Destination is available
function Check-Dir {
Logging "INFO" "Check if BackupDir and Destination exists"
if (!(Test-Path $BackupDirs)) {
return $false
Logging "Error" "$BackupDirs does not exist"
}
if (!(Test-Path $Destination)) {
return $false
Logging "Error" "$Destination does not exist"
}
}

#Save all the Files
Function Make-Backup {
Logging "INFO" "Started the Backup"
$Files=@()
$SumMB=0
$SumItems=0
$SumCount=0
$colItems=0
Logging "INFO" "Count all files and create the Top Level Directories"

foreach ($Backup in $BackupDirs) {
$colItems = (Get-ChildItem $Backup -recurse | Where-Object {$_.mode -notmatch "h"} | Measure-Object -property length -sum) 
$Items=0
$FilesCount += Get-ChildItem $Backup -Recurse | Where-Object {$_.mode -notmatch "h"} 
Copy-Item -Path $Backup -Destination $Backupdir -Force -ErrorAction SilentlyContinue
$SumMB+=$colItems.Sum.ToString()
$SumItems+=$colItems.Count
}

$TotalMB="{0:N2}" -f ($SumMB / 1MB) + " MB of Files"
Logging "INFO" "There are $SumItems Files with $TotalMB to copy"

foreach ($Backup in $BackupDirs) {
$Index=$Backup.LastIndexOf("\")
$SplitBackup=$Backup.substring(0,$Index)
$Files = Get-ChildItem $Backup -Recurse | Where-Object {$_.mode -notmatch "h"} 
foreach ($File in $Files) {
$restpath = $file.fullname.replace($SplitBackup,"")
try {
Copy-Item $file.fullname $($Backupdir+$restpath) -Force -ErrorAction SilentlyContinue |Out-Null
Logging "INFO" "$file was copied"
}
catch {
$ErrorCount++
Logging "ERROR" "$file returned an error an was not copied"
}
$Items += (Get-item $file.fullname).Length
$status = "Copy file {0} of {1} and copied {3} MB of {4} MB: {2}" -f $count,$SumItems,$file.Name,("{0:N2}" -f ($Items / 1MB)).ToString(),("{0:N2}" -f ($SumMB / 1MB)).ToString()
$Index=[array]::IndexOf($BackupDirs,$Backup)+1
$Text="Copy data Location {0} of {1}" -f $Index ,$BackupDirs.Count
Write-Progress -Activity $Text $status -PercentComplete ($Items / $SumMB*100) 
if ($File.Attributes -ne "Directory") {$count++}
}
}
$SumCount+=$Count
$SumTotalMB="{0:N2}" -f ($Items / 1MB) + " MB of Files"
Logging "INFO" "----------------------"
Logging "INFO" "Copied $SumCount files with $SumTotalMB"
Logging "INFO" "$ErrorCount Files could not be copied"
}

#Check if Backupdir needs to be cleaned and create Backupdir
$Count=(Get-ChildItem $Destination | where {$_.Attributes -eq "Directory"}).count
Logging "INFO" "Check if there are more than $Versions Directories in the Backupdir"

if ($count -lt $Versions) {

Create-Backupdir

} else {

Delete-Backupdir

Create-Backupdir
}

#Check if all Dir are existing and do the Backup
$CheckDir=Check-Dir

if ($CheckDir -eq $false) {
Logging "ERROR" "One of the Directory are not available, Script has stopped"
} else {
Make-Backup

$Enddate=Get-Date #-format dd.MM.yyyy-HH:mm:ss
$span = $EndDate - $StartDate
$Minutes=$span.Minutes
$Seconds=$Span.Seconds

Logging "INFO" "Backupduration $Minutes Minutes and $Seconds Seconds"
Logging "INFO" "----------------------"
Logging "INFO" "----------------------" 
}

$file = "$Backupdir\Log.txt"
$message = New-Object System.Net.Mail.MailMessage
$message.subject = $subject
$message.body = $body
$message.to.add($to)
$message.from = $username
$message.attachments.add($file)

$smtp = New-Object System.Net.Mail.SmtpClient($SMTPServer, $SMTPPort);
$smtp.EnableSSL = $true
$smtp.Credentials = New-Object System.Net.NetworkCredential($Username, $Password);
$smtp.send($message)

Результат выполнения скрипта:

PS D:\Backup> D:\Backup\VM_HHD_BackupScript.ps1
30.09.2019-09:42:20 - INFO: ----------------------
30.09.2019-09:42:20 - INFO: Start the Script
30.09.2019-09:42:20 - INFO: Check if there are more than 2 Directories in the Backupdir
30.09.2019-09:42:20 - INFO: Create Backupdir D:\Backup\Backup-2019-09-30-18390\
30.09.2019-09:42:20 - INFO: Move Log file to D:\Backup\Backup-2019-09-30-18390\
30.09.2019-09:42:20 - INFO: Continue with Log File at D:\Backup\Backup-2019-09-30-18390\
30.09.2019-09:42:20 - INFO: Check if BackupDir and Destination exists
30.09.2019-09:42:20 - INFO: Started the Backup
30.09.2019-09:42:20 - INFO: Count all files and create the Top Level Directories
30.09.2019-09:42:21 - INFO: There are 2 Files with 22 728,00 MB of Files to copy
30.09.2019-09:54:54 - INFO: ----------------------
30.09.2019-09:54:54 - INFO: Copied 2 files with 22 728,00 MB of Files
30.09.2019-09:54:54 - INFO: 0 Files could not be copied
30.09.2019-09:54:54 - INFO: Backupduration 12 Minutes and 33 Seconds
30.09.2019-09:54:54 - INFO: ----------------------
30.09.2019-09:54:54 - INFO: ----------------------

PS D:\Backup\Backup-2019-09-30-18390>

Эту же информацию получаем на почту, указанную в настройках скрипта admin@domain.ru.

Остаётся добавить задание в планировщик