Hyper-V-Backup.ps1
<#PSScriptInfo
.VERSION 4.2 .GUID c7fb05cc-1e20-4277-9986-523020060668 .AUTHOR Mike Galvin twitter.com/digressive .COMPANYNAME .COPYRIGHT (C) Mike Galvin. All rights reserved. .TAGS Hyper-V Virtual Machines Cluster CSV Full Backup Export Permissions Zip History .LICENSEURI .PROJECTURI https://gal.vin/2017/09/18/vm-backup-for-hyper-v .ICONURI .EXTERNALMODULEDEPENDENCIES Windows 10/Windows Server 2016/Windows 2012 R2 Hyper-V PowerShell Management Modules .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES Hyper-V PowerShell Management Tools .RELEASENOTES #> <# .SYNOPSIS Hyper-V Backup PowerShell Utility - Creates a full backup of running Hyper-V Virtual Machines. .DESCRIPTION This script creates a full backup of running Hyper-V Virtual Machines. This script will: Create a full backup of Virtual Machine(s), complete with configuration, snapshots/checkpoints, and VHD files. If the -NoPerms switch is used, the script will shutdown the VM and copy all the files to the backup location, then start the VM. You should use the -NoPerms switch if Hyper-V does not have the appropriate permissions to the specified backup location to do an export. If the -NoPerms switch is NOT used, the script will use the built-in export function, and the VMs will continue to run. The -Keep switch should be used to keep the specified number of days worth of backups. For example, to keep one months worth of backups use -Keep 30. The -Compress switch should be used to generate a zip file of each VM that is backed up. The original backup folder will be deleted afterwards. Important note: This script should be run on a Hyper-V host. The Hyper-V PowerShell management modules should be installed. Please note: to send a log file using ssl and an SMTP password you must generate an encrypted password file. The password file is unique to both the user and machine. The command is as follows: $creds = Get-Credential $creds.Password | ConvertFrom-SecureString | Set-Content c:\foo\ps-script-pwd.txt .PARAMETER BackupTo The path the Virtual Machines should be backed up to. A folder will be created in the specified path and each VM will have it's own folder inside. .PARAMETER L The path to output the log file to. The file name will be Hyper-V-Backup-YYYY-MM-dd-HH-mm-ss.log .PARAMETER NoPerms Instructs the script to shutdown the running VM(s) to do the file-copy based backup, instead of using the Hyper-V export function. When multiple VMs are running, the first VM (alphabetically) will be shutdown, backed up, and then started, then the next and so on. .PARAMETER Keep Instructs the script to keep a specified number of days worth of backups. The script will delete VM backups older than the number of days specified. .PARAMETER Compress This option will create a .zip file of each Hyper-V VM backup. Available disk space should be considered when using this option. .PARAMETER SendTo The e-mail address the log should be sent to. .PARAMETER From The from address the log should be sent from. .PARAMETER Smtp The DNS or IP address of the SMTP server. .PARAMETER User The user account to connect to the SMTP server. .PARAMETER Pwd The txt file containing the encrypted password for the user account. .PARAMETER UseSsl Connect to the SMTP server using SSL. .EXAMPLE Hyper-V-Backup.ps1 -BackupTo \\nas\vms -NoPerms -Keep 30 -Compress -L E:\scripts -SendTo me@contoso.com -From hyperv@contoso.com -Smtp smtp.outlook.com -User user -Pwd C:\foo\pwd.txt -UseSsl This will shutdown all running VMs and back up their files to \\nas\vms. Each VM will have their own folder. A zip file for each VM folder will be created, and the folder will be deleted. Any backups older than 30 days will also be deleted. The log file will be output to E:\scripts and sent via email. #> [CmdletBinding()] Param( [parameter(Mandatory=$True)] [alias("BackupTo")] $Backup, [alias("keep")] $History, [alias("L")] $LogPath, [alias("SendTo")] $MailTo, [alias("From")] $MailFrom, [alias("Smtp")] $SmtpServer, [alias("User")] $SmtpUser, [alias("Pwd")] $SmtpPwd, [switch]$Compress, [switch]$UseSsl, [switch]$NoPerms) ## If logging is configured, start log If ($LogPath) { $LogFile = ("Hyper-V-Backup-{0:yyyy-MM-dd-HH-mm-ss}.log" -f (Get-Date)) $Log = "$LogPath\$LogFile" ## If the log file already exists, clear it $LogT = Test-Path -Path $Log If ($LogT) { Clear-Content -Path $Log } Add-Content -Path $Log -Value "****************************************" Add-Content -Path $Log -Value "$(Get-Date -Format G) Log started" Add-Content -Path $Log -Value "" } ## Set variables for computer name and get all running VMs $Vs = $Env:ComputerName $Vms = Get-VM | Where-Object {$_.State -eq 'Running'} ## Check to see if there are any running VMs If ($Vms.count -ne 0) { ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) This virtual host is: $Vs" Add-Content -Path $Log -Value "$(Get-Date -Format G) The following VMs will be backed up:" ForEach ($Vm in $Vms) { Add-Content -Path $Log -Value "$($Vm.name)" } } ## If the NoPerms switch is set do the following commands If ($NoPerms) { ## For each VM do the following ForEach ($Vm in $Vms) { ## Test for the existence of a previous VM export. If it exists, delete it $VmExportBackupTest = Test-Path "$Backup\$($Vm.name)" If ($VmExportBackupTest -eq $True) { Remove-Item "$Backup\$($Vm.name)" -Recurse -Force } ## Create directories New-Item "$Backup\$($Vm.name)" -ItemType Directory -Force New-Item "$Backup\$($Vm.name)\Virtual Machines" -ItemType Directory -Force New-Item "$Backup\$($Vm.name)\VHD" -ItemType Directory -Force New-Item "$Backup\$($Vm.name)\Snapshots" -ItemType Directory -Force ## For logging, test for creation of backup folders, report if they havn't been created If ($LogPath) { $VmFolderTest = Test-Path "$Backup\$($Vm.name)\Virtual Machines" If ($VmFolderTest -eq $True) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created backup folder $Backup\$($Vm.name)\Virtual Machines" } Else { Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem creating folder $Backup\$($Vm.name)\Virtual Machines" } $VmVHDTest = Test-Path "$Backup\$($Vm.name)\VHD" If ($VmVHDTest -eq $True) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created backup folder $Backup\$($Vm.name)\VHD" } Else { Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem creating folder $Backup\$($Vm.name)\VHD" } $VmSnapTest = Test-Path "$Backup\$($Vm.name)\Snapshots" If ($VmSnapTest -eq $True) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created backup folder $Backup\$($Vm.name)\Snapshots" } Else { Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem creating folder $Backup\$($Vm.name)\Snapshots" } } ## Stop the VM Stop-VM $Vm ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Stopping VM: $($Vm.name)" } ## Pause the script for 5 seconds Start-Sleep -S 5 ## Copy the config files and folders Copy-Item "$($Vm.ConfigurationLocation)\Virtual Machines\$($Vm.id)" "$Backup\$($Vm.name)\Virtual Machines\" -Recurse -Force Copy-Item "$($Vm.ConfigurationLocation)\Virtual Machines\$($Vm.id).*" "$Backup\$($Vm.name)\Virtual Machines\" -Recurse -Force ## For logging If ($LogPath) { $VmConfigTest = Test-Path "$Backup\$($Vm.name)\Virtual Machines\*" If ($VmConfigTest -eq $True) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully copied $($Vm.name) configuration to $Backup\$($Vm.name)\Virtual Machines" } Else { Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem copying the configuration for $($Vm.name)" } } ## Copy the VHD Copy-Item $Vm.HardDrives.Path -Destination "$Backup\$($Vm.name)\VHD\" -Recurse -Force ## For logging If ($LogPath) { $VmVHDCopyTest = Test-Path "$Backup\$($Vm.name)\VHD\*" If ($VmVHDCopyTest -eq $True) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully copied $($Vm.name) VHDs to $Backup\$($Vm.name)\VHD" } Else { Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem copying the VHDs for $($Vm.name)" } } ## Get the VMs snapshots/checkpoints, if any $Snaps = Get-VMSnapshot $Vm ## For each snapshot do the following ForEach ($Snap in $Snaps) { ## Copy the snapshot config files and folders Copy-Item "$($Vm.ConfigurationLocation)\Snapshots\$($Snap.id)" "$Backup\$($Vm.name)\Snapshots\" -Recurse -Force Copy-Item "$($Vm.ConfigurationLocation)\Snapshots\$($Snap.id).*" "$Backup\$($Vm.name)\Snapshots\" -Recurse -Force ## For logging If ($LogPath) { $VmSnapCopyTest = Test-Path "$Backup\$($Vm.name)\Snapshots\*" If ($VmSnapCopyTest -eq $True) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully copied checkpoint configuration for $Backup\$($Vm.name)\Snapshots" } Else { Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem copying the checkpoint configuration for $($Vm.name)" } } ## Copy the snapshot root VHD Copy-Item $Snap.HardDrives.Path -Destination "$Backup\$($Vm.name)\VHD\" -Recurse -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully copied checkpoint VHDs for $($Vm.name) to $Backup\$($Vm.name)\VHD" } } ## Start the VM Start-VM $Vm ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Starting VM: $($Vm.name)" } ## Pause the script for 30 seconds before proceeding Start-Sleep -S 30 ## If the keep option is not configured If ($History -eq $Null) { ## If the compress option is not configured If ($Compress -eq $False) { ## Remove all previous backup folders Get-ChildItem -Path $Backup -Filter "$($Vm.name)-*-*-*-*-*-*" -Directory | Remove-Item -Recurse -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing previous backup folders for $($Vm.name)" } } } ## If the keep option is configured Else { ## If the compress option is not configured If ($Compress -eq $False) { ## Remove all previous backup folder that are older than the configured number of days Get-ChildItem -Path $Backup -Filter "$($Vm.name)-*-*-*-*-*-*" -Directory | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Recurse -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing backup folders for $($Vm.name) older than: $History days" } } } ## If the compress option is configured If ($Compress) { ## If the keep option is not configured If ($History -eq $Null) { ## Remove all previous compressed backups Remove-Item "$Backup\$($Vm.name)-*-*-*-*-*-*.zip" -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing previous compressed backups for $($Vm.name)" } } ## If the keep option is configured Else { ## Remove previous compressed backups that are older than the configured number of days Get-ChildItem -Path "$Backup\$($Vm.name)-*-*-*-*-*-*.zip" | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing compressed backups for $($Vm.name) older than: $History days" } } ## Compress the VM backup into a zip, and delete the VM export folder Add-Type -AssemblyName "system.io.compression.filesystem" [io.compression.zipfile]::CreateFromDirectory("$Backup\$($Vm.name)", "$Backup\$($Vm.name)-{0:yyyy-MM-dd-HH-mm-ss}.zip" -f (Get-Date)) Get-ChildItem -Path $Backup -Filter "$($Vm.name)" -Directory | Remove-Item -Recurse -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created compressed backup of $($Vm.name)" } } ## If the compress option is not configured Else { ## Rename the export of each VM to include the date Get-ChildItem -Path $Backup -Filter $($Vm.name) -Directory | Rename-Item -NewName ("$Backup\$($Vm.name)-{0:yyyy-MM-dd-HH-mm-ss}" -f (Get-Date)) } ## Pause the script for 30 seconds before proceeding Start-Sleep -S 30 } } ## If the NoPerms option is not set Else { ForEach ($Vm in $Vms) { ## Test for the existence of a previous VM export. If it exists, delete it otherwise the export will fail $VmExportBackupTest = Test-Path "$Backup\$($Vm.name)" If ($VmExportBackupTest -eq $True) { Remove-Item "$Backup\$($Vm.name)" -Recurse -Force } } ## Do a regular export of the VMs $Vms | Export-VM -Path "$Backup" ## For logging If ($LogPath) { $VmExportTest = Test-Path "$Backup\*" If ($VmExportTest -eq $True) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully exported specified VMs to $Backup" } Else { Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem exporting the specified VMs to $Backup" } } ForEach ($Vm in $Vms) { ## If the keep option is not configured If ($History -eq $Null) { ## If the compress option is not configured If ($Compress -eq $False) { ## Remove all previous backup folders Get-ChildItem -Path $Backup -Filter "$($Vm.name)-*-*-*-*-*-*" -Directory | Remove-Item -Recurse -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing previous backup folders for $($Vm.name)" } } } ## If the keep option is configured Else { ## If the compress option is not configured If ($Compress -eq $False) { ## Remove previous backup folders older than the configured number of days Get-ChildItem -Path $Backup -Filter "$($Vm.name)-*-*-*-*-*-*" -Directory | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Recurse -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing backup folders for $($Vm.name) older than: $History days" } } } ## If the compress option is enabled If ($Compress) { ## If the keep option is not configured If ($History -eq $Null) { ## Remove all previous compressed backups Remove-Item "$Backup\$($Vm.name)-*-*-*-*-*-*.zip" -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing previous compressed backups for $($Vm.name)" } } ## If the keep option is configured Else { ## Remove previous compressed backups older than the configured number of days Get-ChildItem -Path "$Backup\$($Vm.name)-*-*-*-*-*-*.zip" | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing compressed backups for $($Vm.name) older than: $History days" } } ## Compress the VM backup into a zip, and delete the VM export folder Add-Type -AssemblyName "system.io.compression.filesystem" [io.compression.zipfile]::CreateFromDirectory("$Backup\$($Vm.name)", "$Backup\$($Vm.name)-{0:yyyy-MM-dd-HH-mm-ss}.zip" -f (Get-Date)) Get-ChildItem -Path $Backup -Filter "$($Vm.name)" -Directory | Remove-Item -Recurse -Force ## For logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created compressed backup of $($Vm.name)" } } ## If the compress option is not enabled Else { ## Rename the export of each VM to include the date Get-ChildItem -Path $Backup -Filter $($Vm.name) -Directory | Rename-Item -NewName ("$Backup\$($Vm.name)-{0:yyyy-MM-dd-HH-mm-ss}" -f (Get-Date)) } } } } ## If there are no VMs, then do nothing Else { ## For Logging If ($LogPath) { Add-Content -Path $Log -Value "$(Get-Date -Format G) There are no VMs running to backup" } } ## If log was configured stop the log If ($LogPath) { Add-Content -Path $Log -Value "" Add-Content -Path $Log -Value "$(Get-Date -Format G) Log finished" Add-Content -Path $Log -Value "****************************************" ## If email was configured, set the variables for the email subject and body If ($SmtpServer) { $MailSubject = "Hyper-V Backup Log" $MailBody = Get-Content -Path $Log | Out-String ## If an email password was configured, create a variable with the username and password If ($SmtpPwd) { $SmtpPwdEncrypt = Get-Content $SmtpPwd | ConvertTo-SecureString $SmtpCreds = New-Object System.Management.Automation.PSCredential -ArgumentList ($SmtpUser, $SmtpPwdEncrypt) ## If ssl was configured, send the email with ssl If ($UseSsl) { Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer -UseSsl -Credential $SmtpCreds } ## If ssl wasn't configured, send the email without ssl Else { Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer -Credential $SmtpCreds } } ## If an email username and password were not configured, send the email without authentication Else { Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer } } } ## End |