Hyper-V Virtual Machine Backup Utility v4.3

In a previous post I wrote about the first version of my Hyper-V backup PowerShell script. This post will serve as a change log and documentation page, as the previous post was about how the script was written, as a reference for those wanting to learn PowerShell.

My Hyper-V Backup Utility PowerShell script can also be downloaded from the Microsoft TechNet Gallery the PowerShell Gallery and GitHub.

-Mike

Twitter – @Digressive

 

Features and Requirements

  • The script is designed to be run on a Hyper-V host.
  • The device must also have Hyper-V management tools and PowerShell modules installed.
  • The script can be used to backup VMs to a device which the Hyper-V host does not have the access to run a regular export.
  • The script can be used to backup VMs in a Hyper-V cluster.
  • The script requires at least PowerShell 5.0

The script has been tested on Windows 10, Windows Server 2016 (Datacenter and Core installations) and Windows Server 2012 R2 (Datacenter and Core Installations) with PowerShell 5.0.

 

Should You Use The -NoPerms Switch?

The -NoPerms switch is intended as a workaround when used in an environment where the Hyper-V host can not be given the required permissions to run a regular export operation. If you are unsure, you should do a test run of the script without the -NoPerms switch first and see if you run into problems.

Below are the operations the script performs with and without the -NoPerms switch.

When the -NoPerms switch is enabled:

  1. Gracefully shutdown the first alphabetically named VM.
  2. Copy all configuration, VHD, and snapshot/checkpoint files to the specified backup location.
  3. Start the Virtual Machine, and move on to the next VM if applicable.
  4. Optionally cleanup old backups or keep a configurable number of days worth of backups.
  5. Optionally create a zip file of the export and remove the original backup folder.
  6. Optionally create a log file and email it to an address of your choice.

When the -NoPerms switch is not enabled:

  1. Run an export operation of each VM alphabetically, exporting the VMs to the specified backup location. The VMs are kept online.
  2. Optionally cleanup old backups or keep a configurable number of days worth of backups.
  3. Optionally create a zip file of the export and remove the original backup folder.
  4. Optionally create a log file and email it to an address of your choice.

 

Why Is The -NoPerms Switch Needed?

Hyper-V’s export operation requires that the computer account in Active Directory have access to the location where the exports are being saved. I recommend creating an Active Directory group for the Hyper-V hosts and then giving the group the required ‘Full Control’ file and share permissions. When a NAS such as a QNAP device is intended to be used as an export location, Hyper-V will not be able to complete the operation as the computer account will not have access to the share on the NAS. Unfortunately to copy all the files necessary for a complete backup, the VM must be in an offline state for the operation to be completed, so the VM will be shutdown for the duration of the copy process when the -NoPerms switch is used.

 

Using This Script With A Hyper-V Cluster

I’ve tested the script backing up VMs running on a Hyper-V cluster and it works just as with standalone Hyper-V hosts. I recommend setting up a staggered Scheduled Task to run the script on each of the Hyper-V hosts in the cluster. The script will detect if there are any Virtual Machines with the status of ‘Running’ and perform a backup, as configured. The script can also be configured to accept a list of VMs via a TXT file, if this option is used the script will only look for the listed VMs and not any with the ‘Running’ status.

 

Generating A Password File

The password used for SMTP server authentication must be in an encrypted text file. To generate the password file, run the following command in PowerShell, on the computer that is going to run the script and logged in with the user that will be running the script. When you run the command you will be prompted for a username and password. Enter the username and password you want to use to authenticate to your SMTP server.

Please note: This is only required if you need to authenticate to the SMTP server when send the log via e-mail.

$creds = Get-Credential
$creds.Password | ConvertFrom-SecureString | Set-Content c:\scripts\ps-script-pwd.txt

After running the commands, you will have a text file containing the encrypted password. When configuring the -Pwd switch enter the path and file name of this file.

 

Configuration

The table below shows all the command line options available with descriptions and example configurations.

Command Line Switch Mandatory Description Example
-List No Specify a TXT file containing a list of the VMs to be backed up. The VMs should be listed by the name of the VM in Hyper-V and one per line.

If the option isn’t specified, the running VMs will be backed up.

C:\foo\vms.txt
-BackupTo Yes Location of where to store the backups. Each VM will be stored in it’s own folder under the specified backup location. Can be local or UNC. Do not add a trailing \ on the end of the path. \\nas\Backups OR E:\Backups
-NoPerms No Set if the backup location is a location where the Hyper-V host will not have the permissions to perform a export operation, such as a NAS appliance

When set the VMs will be shutdown to perform the backup, when not set a regular Hyper-V export, with the VMs kept online, will be done.

N/A
-Keep No Use this switch to instruct the script to keep a specified number of days worth of backups. The script used the file creation date to determine how old a backup is. If -Compress is also used it will only delete zip file backups, and not folder backups. Every effort has been taken to only remove backup files or folders generated by this utility. 30
-Compress No Use this switch to generate a compressed zip file of the backup and remove the original backup folder afterwards. N/A
-L No Location to store the optional log file. The name of the log file is generated automatically. Do not add a trailing \ to the end of the path. C:\foo
-SendTo No The email address to send the log file to. me@contoso.com
-From No* The email address that the log file should be sent from.

*This switch isn’t mandatory but is required if you wish to email the log file.

HyperV@contoso.com
-Smtp No* SMTP server address to use for the email functionality.

*This switch isn’t mandatory but is required if you wish to email the log file.

mail01.contoso.com

OR

smtp.live.com

OR

smtp.office365.com

-User No* The username of the account to use for SMTP authentication.

*This switch isn’t mandatory but may be required depending on the configuration of the SMTP server.

example@contoso.com
-Pwd No* The location of the file containing the encrypted password of the account to use for SMTP authentication.

*This switch isn’t mandatory but may be required depending on your SMTP server.

c:\foo\ps-script-pwd.txt
-UseSsl No* Add this option if you wish to use SSL with the configured SMTP server.

Tip: If you wish to send email to outlook.com or office365.com you will need this.

*This switch isn’t mandatory but may be required depending on the configuration of the SMTP server.

N/A

 

Change Log

2018-06-21 Version 4.3

  • Added the ability to specify the VMs to be backed up using a txt file.

2018-03-04 Version 4.2

  • Improved logging slightly to be more clear about which VM’s previous backups are being deleted.

2018-03-03 Version 4.1

  • Added option to compress the VM backups to a zip file. This option will remove the original VM backup
  • Added option to keep a configurable number of days worth of backups, so you can keep a history/archive of previous backups. Every effort has been taken to only remove backup files or folders generated by this utility.
  • Changed the script so that when backup is complete, the VM backup folders/zip files will be have the time and date append to them.

2018-01-15 Version 4.0

  • The backup script no longer creates a folder named after the Host server. The VM backups are placed in the root of the specified backup location.
  • Fixed a small issue with logging where the script completes the backup process, then states incorrectly “there are no VMs to backup”.

2018-01-12 Version 3.9

  • Fixed a small bug that occurred when there were no VMs to backup, the script incorrectly logged an error in exporting the VMs. It now states that that are no VMs to backup.

2018-01-12 Version 3.8

  • The script has been tested performing backups of Virtual Machines running on a Hyper-V cluster.
  • Minor update to documentation.

2017-10-16 Version 3.7

  • Changed SMTP authentication to require an encrypted password file.
  • Added instructions on how to generate an encrypted password file.

2017-10-07 Version 3.6

  • Added necessary information to add the script to the PowerShell Gallery.

2017-09-18 Version 3.5

  • Improved the log output to be easier to read.

2017-07-22 Version 3.4

  • Improved commenting on the code for documentation purposes.
  • Added authentication and SSL options for e-mail notification.

2017-05-20 Version 3.3

  • Added configuration via command line switches.
  • Added option to perform regular online export if destination allows it.

2017-04-24 Minor Update

  • Cleaned up the formatting and commented sections of the script.

2017-04-21 Minor Update

  • Added the ability to email the log file when the script completes.

 

PowerShell Code


<#PSScriptInfo .VERSION 4.3 .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. The -List switch should be used to specify a txt file with a list of VM names to backup. If this option is not configured, all running VMs will be backed up. 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 List Enter the path to a txt file with a list of Hyper-V VM names to backup. If this option is not configured, all running VMs will be backed up. .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 e-mail address the log should be sent from. .PARAMETER Smtp The DNS name 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 Configures the script to connect to the SMTP server using SSL. .EXAMPLE Hyper-V-Backup.ps1 -BackupTo \\nas\vms -List E:\scripts\vms.txt -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 the VMs listed in the file located in E:\scripts\vms.txt, 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. #>

## Set up command line switches and what variables they map to.
[CmdletBinding()]
Param(
    [parameter(Mandatory=$True)]
    [alias("BackupTo")]
    $Backup,
    [alias("Keep")]
    $History,
    [alias("List")]
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    $VmList,
    [alias("L")]
    [ValidateScript({Test-Path $_ -PathType 'Container'})]
    $LogPath,
    [alias("SendTo")]
    $MailTo,
    [alias("From")]
    $MailFrom,
    [alias("Smtp")]
    $SmtpServer,
    [alias("User")]
    $SmtpUser,
    [alias("Pwd")]
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    $SmtpPwd,
    [switch]$Compress,
    [switch]$UseSsl,
    [switch]$NoPerms)

## If logging is configured, start logging.
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

## If a VM list file is configured, backup the servers specified in the file.
If ($VmList)
{
    $Vms = Get-Content $VmList
}

## If a VM list file is not configured, back up the running VMs.
Else
{
    $Vms = Get-VM | Where-Object {$_.State -eq 'Running'} | Select-Object -ExpandProperty Name
}

## 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"
        }
    }

    ## If the NoPerms switch is set do the following commands.
    If ($NoPerms) 
    {
        ## For each VM do the following.
        ForEach ($Vm in $Vms)
        {
            $VmInfo = Get-VM -name $Vm

            ## Test for the existence of a previous VM export. If it exists, delete it.
            $VmExportBackupTest = Test-Path "$Backup\$Vm"
            If ($VmExportBackupTest -eq $True)
            {
                Remove-Item "$Backup\$Vm" -Recurse -Force
            }

            ## Create directories.
            New-Item "$Backup\$Vm" -ItemType Directory -Force
            New-Item "$Backup\$Vm\Virtual Machines" -ItemType Directory -Force
            New-Item "$Backup\$Vm\VHD" -ItemType Directory -Force
            New-Item "$Backup\$Vm\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\Virtual Machines"
                If ($VmFolderTest -eq $True)
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created backup folder $Backup\$Vm\Virtual Machines"
                }

                Else
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem creating folder $Backup\$Vm\Virtual Machines"
                }

                $VmVHDTest = Test-Path "$Backup\$Vm\VHD"
                If ($VmVHDTest -eq $True)
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created backup folder $Backup\$Vm\VHD"
                }

                Else
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem creating folder $Backup\$Vm\VHD"
                }
            
                $VmSnapTest = Test-Path "$Backup\$Vm\Snapshots"
                If ($VmSnapTest -eq $True)
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created backup folder $Backup\$Vm\Snapshots"
                }

                Else
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem creating folder $Backup\$Vm\Snapshots"
                }
            }

            ## Stop the VM.
            Stop-VM $Vm

            ## For logging.
            If ($LogPath)
            {
                Add-Content -Path $Log -Value "$(Get-Date -Format G) Stopping VM: $Vm"
            }

            ## Pause the script for 5 seconds.
            Start-Sleep -S 5

            ## Copy the config files and folders.
            Copy-Item "$($VmInfo.ConfigurationLocation)\Virtual Machines\$($VmInfo.id)" "$Backup\$Vm\Virtual Machines\" -Recurse -Force
            Copy-Item "$($VmInfo.ConfigurationLocation)\Virtual Machines\$($VmInfo.id).*" "$Backup\$Vm\Virtual Machines\" -Recurse -Force

            ## For logging.
            If ($LogPath)
            {
                $VmConfigTest = Test-Path "$Backup\$Vm\Virtual Machines\*"
                If ($VmConfigTest -eq $True)
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully copied $Vm configuration to $Backup\$Vm\Virtual Machines"
                }

                Else
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem copying the configuration for $Vm"
                }
            }

            ## Copy the VHD.
            Copy-Item $VmInfo.HardDrives.Path -Destination "$Backup\$Vm\VHD\" -Recurse -Force

            ## For logging.
            If ($LogPath)
            {
                $VmVHDCopyTest = Test-Path "$Backup\$Vm\VHD\*"
                If ($VmVHDCopyTest -eq $True)
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully copied $Vm VHDs to $Backup\$Vm\VHD"
                }

                Else
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem copying the VHDs for $Vm"
                }
            }

            ## Get the VM 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 "$($VmInfo.ConfigurationLocation)\Snapshots\$($Snap.id)" "$Backup\$Vm\Snapshots\" -Recurse -Force
                Copy-Item "$($VmInfo.ConfigurationLocation)\Snapshots\$($Snap.id).*" "$Backup\$Vm\Snapshots\" -Recurse -Force

                ## For logging.
                If ($LogPath)
                {
                    $VmSnapCopyTest = Test-Path "$Backup\$Vm\Snapshots\*"
                    If ($VmSnapCopyTest -eq $True)
                    {
                        Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully copied checkpoint configuration for $Backup\$Vm\Snapshots"
                    }

                    Else
                    {
                        Add-Content -Path $Log -Value "$(Get-Date -Format G) ERROR: There was a problem copying the checkpoint configuration for $Vm"
                    }
                }

                ## Copy the snapshot root VHD.
                Copy-Item $Snap.HardDrives.Path -Destination "$Backup\$Vm\VHD\" -Recurse -Force

                ## For logging.
                If ($LogPath)
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully copied checkpoint VHDs for $Vm to $Backup\$Vm\VHD"
                }
            }

            ## Start the VM.
            Start-VM $Vm

            ## For logging.
            If ($LogPath)
            {
                Add-Content -Path $Log -Value "$(Get-Date -Format G) Starting VM: $Vm"
            }

            ## 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-*-*-*-*-*-*" -Directory | Remove-Item -Recurse -Force

                    ## For logging.
                    If ($LogPath)
                    {
                        Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing previous backup folders."
                    }
                }
            }

            ## 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-*-*-*-*-*-*" -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 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-*-*-*-*-*-*.zip" -Force

                    ## For logging.
                    If ($LogPath)
                    {
                        Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing previous compressed backups."
                    }
                }

                ## 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-*-*-*-*-*-*.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 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", "$Backup\$Vm-{0:yyyy-MM-dd-HH-mm-ss}.zip" -f (Get-Date))
                Get-ChildItem -Path $Backup -Filter "$Vm" -Directory | Remove-Item -Recurse -Force

                ## For logging.
                If ($LogPath)
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created compressed backup of $Vm"
                }
            }
        
            ## If the compress option is not configured.
            Else
            {
                ## Rename the export of each VM to include the date.
                Get-ChildItem -Path $Backup -Filter $Vm -Directory | Rename-Item -NewName ("$Backup\$Vm-{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"
            If ($VmExportBackupTest -eq $True)
            {
                Remove-Item "$Backup\$Vm" -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"
            }
        }

        ## Loop through the VMs do perform operations for the keep and compress options, if configured.
        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-*-*-*-*-*-*" -Directory | Remove-Item -Recurse -Force

                    ## For logging.
                    If ($LogPath)
                    {
                        Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing previous backup folders"
                    }
                }
            }

            ## 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-*-*-*-*-*-*" -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 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-*-*-*-*-*-*.zip" -Force

                    ## For logging.
                    If ($LogPath)
                    {
                        Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing previous compressed backups"
                    }
                }

                ## If the keep option is configured.
                Else
                {
                    ## Remove previous compressed backups older than the configured number of days.
                    Get-ChildItem -Path "$Backup\$Vm-*-*-*-*-*-*.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 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", "$Backup\$Vm-{0:yyyy-MM-dd-HH-mm-ss}.zip" -f (Get-Date))
                Get-ChildItem -Path $Backup -Filter "$Vm" -Directory | Remove-Item -Recurse -Force

                ## For logging.
                If ($LogPath)
                {
                    Add-Content -Path $Log -Value "$(Get-Date -Format G) Successfully created compressed backup of $Vm"
                }
            }
        
            ## If the compress option is not enabled.
            Else
            {
                ## Rename the export of each VM to include the date.
                Get-ChildItem -Path $Backup -Filter $Vm -Directory | Rename-Item -NewName ("$Backup\$Vm-{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

62 Comments Add yours

  1. Joe says:

    Hi. I’m trying to use your script, but every VM that it tried to backup it says
    Export failed for virtual machine with error ‘The specified server cannot perform the
    requested operation.’ (0x8007003A).

    Like

    1. Mike Galvin says:

      Hi there,

      I’ve not come across this error before. What version of Hyper-V are you running?

      Like

      1. Joe says:

        I’m using Hyper-V on windows 10. It works if i do -noperms

        Like

      2. Mike Galvin says:

        Ok, thanks. If it works with the -noperms option then it’s most likly because of the permissions that the computer running Hyper-V has to the destination that you have configured.

        For me, I added the -noperms switch because my Hyper-V server is a member of a domain and I wanted to export the VMs to a NAS device which was not running a Windows OS, and so could not grant the permissions needed for the proper export function to work.

        Like

      3. Joe says:

        That makes sense, I’m in the same boat trying to backup to a NAS.

        But the script is awesome, thank you for sharing!

        Like

      4. Mike Galvin says:

        No problem. Thanks for checking it out, I’m glad it’s helped you out. 🙂

        Like

  2. Papee John says:

    Great script, amazing job.
    Easy to undestarnd code

    Thank you very much

    Like

    1. Mike Galvin says:

      Thanks for the kind words, glad it helped!

      Like

  3. R. E. Martin says:

    I want to select specific VM’s so I assume I need to change the;
    $vs = $env:computername
    $vms = Get-VM | Where-Object {$_.State -eq ‘Running’}
    to somehow select the specific names instead of just running?
    I am not a programmer or understand scripting well, but it seems that it should be fairly easy since most of the VM’s names are similar to XXX16Vxx. Or just explicitly name them when running the script.

    Like

    1. Mike Galvin says:

      That’s right. You can change:
      $vms = Get-VM | Where-Object {$_.State -eq ‘Running’}

      to

      $vms = Get-VM -name *16V*

      for all VMs with 16V in the name. Lots more info on the Get-VM cmdlet here: https://technet.microsoft.com/en-us/itpro/powershell/windows/hyper-v/get-vm?f=255&MSPPError=-2147217396

      Like

  4. R. E. Martin says:

    Can I assume from the script the previously exported VM is deleted before the next export for that particular VM name?
    Also, I am going to try to modify the script in order to export certain VM’s to one host and other VM’s to another for DR purposes. I am not very good at scripting, but it seems that it should be fairly easy to modify one script or just have 2 scripts run (one script exports VM’s 1 & 2 to HOST1 and one script VM 3 to HOST2).

    Like

    1. Mike Galvin says:

      That’s correct, it will remove the previous backup of the VM, if it’s there. I like the idea of exporting to another VM host. 🙂

      Like

    2. rafael alejandro angulo says:

      Is it a typo, and the variable name is “$Vms” or do I have an outdated version of the script?

      Like

      1. Mike Galvin says:

        Hi there,

        You are correct, the variable is “$Vms”

        What’s the issue you’re having?

        -Mike

        Like

  5. Federico Barreiro says:

    Hello, i have an issue, when i execute the scritp and put the backup directory g:\ the shell said couldn’t creat a directory because didn’t founde the file in line 225 character 12
    + $Vms | Export-VM -Path “$Backup\$vs”
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : ObjectNotFound: (Microsoft.HyperV.PowerShell.VMTask:VM
    Task) [Export-VM], VirtualizationOperationFailedException
    + FullyQualifiedErrorId : ObjectNotFound,Microsoft.HyperV.PowerShell.Commands.Ex
    portVMCommand

    Like

    1. Federico Barreiro says:

      solved, bad typing lol

      Liked by 1 person

  6. Lennon says:

    Awesome script, great job. One feature that would be great is date and time stamped directories e.g. ///. Right now I’m scripting them at the top level via another script that calls the backup script but it’s a bit clunky. Feels like the time stamp should be lower in the tree per the example.

    Like

    1. Lennon says:

      Ouch, parsed the brackets out of my last example. Should have been: /host/VM/time&date/resources

      Like

      1. Mike Galvin says:

        That’s a good idea! I’ll maybe add it in a future release. Cheers!
        -Mike

        Like

  7. Quinten Questel says:

    Hi If i wanted to keep the last three backups how would i accomplish this?

    Like

    1. Quinten Questel says:

      Ok I don’t need this feature anymore,

      Like

  8. emerson says:

    hi, how to modify the script as weekly run?

    Like

    1. Mike Galvin says:

      You shouldn’t have to modify the script at all, just run the script as a Scheduled Task, once a week. The command would look something like this: PowerShell -ExecutionPolicy bypass F:\scripts\Hyper-V-Backup.ps1 -backupto \\nas\Archive\VM_Backups -l F:\scripts -noperms -sendto me@contoso.com -from vs10@nct.ac.uk -smtp smtp.contoso.com

      -Mike

      Like

      1. emerson says:

        Thanks Mike.
        One more question, if I want to copy the backup to NAS, is it necessary to shutdown the VM to backup?

        Like

      2. Mike Galvin says:

        Hi there,
        It depends on the NAS. I used the example of a NAS because for me, my NAS is a QNAP and not running Windows, so Hyper-V can’t authenticate with it to do a live export – so the VM must be shutdown to copy the files. You may have a NAS which Hyper-V can authenticate to. I realise it’s not ideal to have to shutdown, but I’d rather shutdown and get a known good backup.

        -Mike

        Like

  9. Alfred says:

    Hi Mike. Works great. I’m a newbe at this… can you point me on how to restore the VM backups if the original Windows 10 machine was totally lost… so now the VMs need to be restored to a different Windows 10 machine. Thanks

    Like

    1. Mike Galvin says:

      Hi Alfred, Sure thing – you copy the folders of the VMs from your backup location, and do a Virtual Machine import in Hyper-V. You can put the VHDs separate from the VM configuration files if you want to, or need to. When importing, it’ll ask you where the locations of the files are.

      -Mike

      Like

  10. Alfred says:

    Thanks Mike. Had done that but had gotten an error. Did it again and here are the details:

    On Windows 10 Host A:
    – Created a ubuntu vm called “test” (no checkpoint was created)
    – Successfully launched your script: ” .\Hyper-V-Backup.ps1 –backup to D:\Bridge\VMBackup\VM -l D:\Bridge\VMBackup\logs -noperms”
    – The new backup dir has: An empty “Snapshots” dir —– a “VHD” dir with just one .avhdx file —– and a “Virtual Machines” dir with 3 files in it: .vmcx, .vmgs and .VMRS and an empty dir called “6d68d914-6ec0-4bb2-9854-ace2930df601”

    Copied the backup dir to a new Windows 10 (Host B) and did the following:
    – Launched “Import Virtual Machine”
    – For “Specify the folder containing the virtual machine to import”: Pointed to the parent dir containing the 3 dir your backup script created
    – For “Select the virtual machine to import” I selected “test”
    – For choose the type of import to perform I selected “Registered the virtual machine in-place” (fyi also tried “copy the virtual machine” but get the same error below)
    – HERE IS THE PROBLEM.. For “Where are the virtual disks for this virtual machine stored”: Pointed to the VHD dir… seems to accept it…
    – … but when I click “Finish” I get an import error… “fail to open virtual disk” … seems like it’s looking for a .avhdx file

    What am I doing wrong?

    -Cheers, Alfred

    Like

    1. Alfred says:

      solved… typo 😦

      Liked by 1 person

  11. CJ says:

    Very nice script, running it right now and it looks good.
    But two things 🙂

    I was not sure if I can put “:” in the script because my mailhost running a diffrent port for secure SMTP (even non secure it running another port then 25)

    I also notice it does not like if I have pass-through disks that is attached.
    I maybe can’t run a export if a virtual machine have that.

    But going to see tomorrow if it have run a export of the rest of my virtual machines.

    Like

    1. CJ says:

      A little update, it did do a nice update of the viritual machine (but not the machine that have pass-through disks but that is ok)
      But I can’t use : so maybe is a good idea next time add it make it easy to change port 🙂

      Liked by 1 person

  12. Love your script saves me a lot of time, i tweak it a bit for my usage to load a vm names from a file so i can change the vms i wanted to backup, and when i set up the scheduler i thought why not putting all the params in a config file and just give back the config path. it can be like another optional parameter if it is passed it overrides all the other options.

    Like

  13. Jorge says:

    how can i set a path location to save my backup, because every time that i run the script the command line say this:
    PS C:\Users\user1\Desktop\Hyper-V-Backup_v3-5> C:\Users\user1\Desktop\Hyper-V-Backup_v3-5\Hyper-V-Backup_v3-5.ps1
    cmdlet Hyper-V-Backup_v3-5.ps1 at command pipeline position 1
    Supply values for the following parameters:
    Backup:

    Like

    1. Mike Galvin says:

      Hi Jorge,

      You must add the command line switch “-BackupTo E:\Backups” for example. No quotes. I have actually just updated to add a few more features and update documentation.

      -Mike

      Like

  14. Michael says:

    Hi Mike

    Thanks for a great script. I have tried this script on a Windows 12016 host and it works perfectly. I then copied it to a Windows 10 host and ran it but it isnt doing anything. When I check the log files it says there are no VMs running to backup however I have about 9 VMs running. Any advice on this would be appreciated

    Like

    1. Mike Galvin says:

      Hi Michael,

      This might seem like a silly questions but: The 9 VMs that are running, are they on the Windows 10 machine? The script is designed to run on the Hyper-V host that the VMs are running on, not a remote host.

      -Mike

      Like

      1. Michael says:

        So after double checking everything it turns out the PC isnt running windows 10 but rather windows 8.1
        Im going to assume this script doesnt play well with Windows 8.1
        Back to the drawing board…

        Like

      2. Mike Galvin says:

        Ah I see. I haven’t tested it with Windows 8.1, but seeing as Windows Server 2012 R2 is based off of Windows 8.1 the script should work fine. It also could be the version of PowerShell running on your Windows 8.1 PC.

        -Mike

        Like

  15. tadeusznikitin says:

    thanks for that script. great job!

    Liked by 1 person

  16. Timothy Dorsey says:

    Mike,

    When i run the script, it ask for backup parameter and i enter it. Then it continues without asking for any other parameter values. ie Keep, compress, email etc.

    Am i suppose to assign those manually within the script code?

    Thanks

    Tim

    Like

    1. Mike Galvin says:

      Hi Timothy,

      The script is intended to be run with command line options, however only the -backup parameter is mandatory, so the other options are, optional which is why you are not being prompted to specify them. If you want to use the optional options, specify them using the switches in the command line. The options are explained in the post.

      Hope this helps,

      -Mike

      Like

  17. Marek says:

    Hello, Great script!
    Is there any chance to get modification of this scirpt which creates backup VMs provided in csv file?

    Like

    1. Mike Galvin says:

      Hi Marek,

      Thanks! I’ve added this feature to the latest update.

      -Mike

      Like

  18. Joseph says:

    Thanks for the script. Any chance there is a way to set a higher compression level and the ability to encrypt the compressed file?

    Thanks!

    Like

    1. Mike Galvin says:

      Hi Joesph,

      I’ll see if I can build it into a future release.

      -Mike

      Like

  19. Domosed says:

    Great script, works like a charm however due to lack of knowledge I’m stuck on how to create file with the encrypted email password, HELP please.

    Like

    1. Mike Galvin says:

      Hi Domosed,

      The password file is a little tricky, it’s still trip me up from time to time too. Firstly, make you you actually need to use a password file – it’s only for emailing log files using email systems that require authentication. If you do need it, no problem.

      What you need to do is log on to the computer that will be running the script as the user that the script will be running as – this is because the encryption is generated using the user and computer information. Once logged on, run the command in PowerShell:

      $creds = Get-Credential
      $creds.Password | ConvertFrom-SecureString | Set-Content c:\scripts\ps-script-pwd.txt

      You will be prompted for a username and password. It doesn’t matter what you enter for username, but the password must be the password required for the e-mail account you are sending the email as.

      -Mike

      Like

  20. Victor says:

    Hello. I get an error: “Cannot take checkpoint for ‘DB2’ because one or more shareable VHDX are attached and this is not part of a checkpoint collection. (Virtual machine ID 9EA529B1-D1A4-4035-8FB2-D582CDDE1A1C)” what to do in such a case?

    Like

    1. Mike Galvin says:

      Hi Victor,

      I could do with some more information. Are the VMs running in a cluster? Is DB2 a VHDX attached to a VM or the name of a checkpoint?

      I have tested the script with VMs with multiple VHD/VHDX’s and many checkpoints, but there may be issues I haven’t seen and been able to account for.

      -Mike

      Like

      1. Victor says:

        Hy Mike.
        Yes, machines work in a cluster.
        img :
        https://ibb.co/eGR5Xe
        https://ibb.co/c6215z

        Like

      2. Victor says:

        Yes. machines work in a cluster. db2 is running on another virtual machine, not on db1.

        Like

      3. Mike Galvin says:

        Hi Victor,

        I’ve seen the screenshots you sent. I’ve just noticed that the other VHDs are shared drives – I’ve not tested the script with this configuration so I cannot say why it’s not working exactly. It’s been tested on Hyper-V in both standalone and clustered configurations of the Hyper-V hosts but not with VMs with these shared drives – the script hasn’t been made with these in mind. Sorry about that.

        When using the script without the -NoPerms switch (which is what I’m guessing you are using) it just uses the regular export function, you might have more luck trying that. The -NoPerms switch is a bit of a work-around or hack for backing up VMs to a location where you can’t grant the needed permissions for Hyper-V to do it’s normal export function.

        -Mike

        Like

  21. grac says:

    Hi. Thank you for the script.

    I really appreciate the compress switch. I’d just like to confirm if the compress and delete action happens only after all VMs are exported or does it happen after each VM?

    I really have space constraints and the second scenario (for each VM export, compress, delete export) would be much better for me.

    Thanks!

    Like

    1. Mike Galvin says:

      Hi grac,

      The compress action happens after the backup. With a earlier unreleased version I did try to be as space efficient as possible but in the end I had to get the backup/export operations done first and then perform the other options after. I can’t remember the exact reason but some issues came up in testing which caused me to change it to the current way it operates. I’m sorry that this isn’t the best news for you and your situation. What I can suggest is that I will try to look at it in a future version or if you’d like, the code is freely available for you to play with if your comfortable.

      -Mike

      Like

  22. JS says:

    Hi Mike

    A fantastic script right there! Coming from a UNIX background, it was fantastic how easily I could implement the script on a Windows environment!

    However, I seem to have having an issue sending out emails via Office 365 – would you be able to advise on where in the CLI I might be going wrong?
    .\Hyper-V-Backup.ps1 -List C:\BackUpScripts\Hyper-V-Backup\vms.txt -BackupTo G:\ -L C:\BackUpScripts\Hyper-V-Backup -SendTo test@example.com -From test2@example.com -Smtp smtp.office365.com -UseSsl

    Send-MailMessage : The SMTP server requires a secure connection or the client was not authenticated. The server response was: 5.7.57 SMTP; Client was not authenticated to send anonymous mail during MAIL FROM
    [DB6PR07CA0191.eurprd07.prod.outlook.com]
    At C:\BackUpScripts\Hyper-V-Backup\Hyper-V-Backup.ps1:592 char:13
    + Send-MailMessage -To $MailTo -From $MailFrom -Subject $Ma …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.Mail.SmtpClient:SmtpClient) [Send-MailMessage], SmtpException
    + FullyQualifiedErrorId : SmtpException,Microsoft.PowerShell.Commands.SendMailMessage

    Once again, a fantastic script!!

    Thanks
    JS

    Like

    1. Mike Galvin says:

      Hi JS,

      Thanks for the kind words about the script! I can certainly help you with the error you’re having. To send e-mails using Office 365 you’ll need to specify the user name and password of the account. To do this use the following switches: -User and -Pwd.

      The -User switch should be the email address of an Office 365 account you wish to use, for the password you’ll need to generate a TXT file containing the password of the Office 365 account. I’ve explained how to do this in the post under the “Generating A Password File” section.

      Hope this helps.

      -Mike

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.