Hyper-V-Backup.ps1

<#PSScriptInfo
 
.VERSION 21.08.10
 
.GUID c7fb05cc-1e20-4277-9986-523020060668
 
.AUTHOR Mike Galvin Contact: mike@gal.vin / twitter.com/mikegalvin_ / discord.gg/5ZsnJ5k
 
.COMPANYNAME Mike Galvin
 
.COPYRIGHT (C) Mike Galvin. All rights reserved.
 
.TAGS Hyper-V Virtual Machines Full Backup Export Permissions Zip History 7-Zip
 
.LICENSEURI
 
.PROJECTURI https://gal.vin/posts/vm-backup-for-hyper-v
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
#>


<#
    .SYNOPSIS
    Hyper-V Backup Utility - Flexible backup of Hyper-V Virtual Machines.
 
    .DESCRIPTION
    This script will create a full backup of virtual machines, complete with configuration, snapshots/checkpoints, and VHD files.
    This script should be run on a Hyper-V host and the Hyper-V PowerShell management modules should be installed.
 
    To send a log file via e-mail using ssl and an SMTP password you must generate an encrypted password file.
    The password file is unique to both the user and machine.
    To create the password file run this command as the user and on the machine that will use the file:
 
    $creds = Get-Credential
    $creds.Password | ConvertFrom-SecureString | Set-Content C:\scripts\ps-script-pwd.txt
 
    .PARAMETER BackupTo
    The path the virtual machines should be backed up to.
    Each VM will have its own folder inside this location.
    Do not add a trailing backslash.
 
    .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 Wd
    The path to the working directory to use for the backup before copying it to the final backup directory.
    Use a directory on local fast media to improve performance.
 
    .PARAMETER NoPerms
    Configures the utility to shut down the running VM(s) to do the file-copy based backup instead of using the Hyper-V export function.
    If no list is specified and multiple VMs are running, the process will run through the VMs alphabetically.
 
    .PARAMETER Keep
    Instructs the utility to keep a specified number of days’ worth of backups.
    VM backups older than the number of days specified will be deleted.
    Every effort has been taken to only remove backup files or folders generated by this utility.
 
    .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 Sz
    Configure the utility to use 7-Zip to compress the VM backups.
    7-Zip must be installed in the default location ($env:ProgramFiles) if it is not found, Windows compression will be used as a fallback.
     
    .PARAMETER SzOptions
    Use this option to configure options for 7-Zip. The switches must be comma separated and surrounded by single quotes.
    For example, archive type, split files, password protection would be '-t7z,-v2G,-ppassword'
 
    .PARAMETER ShortDate
    Configure the script to use only the Year, Month and Day in backup filenames.
 
    .PARAMETER NoBanner
    Use this option to hide the ASCII art title in the console.
 
    .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.
    Do not add a trailing \ backslash.
 
    .PARAMETER Subject
    The subject line for the e-mail log. Encapsulate with single or double quotes.
    If no subject is specified, the default of "Hyper-V Backup Utility Log" will be used.
 
    .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 Port
    The Port that should be used for the SMTP server.
 
    .PARAMETER User
    The user account to authenticate to the SMTP server.
 
    .PARAMETER Pwd
    The txt file containing the encrypted password for SMTP authentication.
 
    .PARAMETER UseSsl
    Configures the utility to connect to the SMTP server using SSL.
 
    .EXAMPLE
    Hyper-V-Backup.ps1 -BackupTo \\nas\vms -List C:\scripts\vms.txt -Wd E:\temp -NoPerms -Keep 30 -Compress -Sz
    -SzOptions '-t7z,-v2g,-ppassword' -L C:\scripts\logs -Subject 'Server: Hyper-V Backup' -SendTo me@contoso.com
    -From hyperv@contoso.com -Smtp smtp.outlook.com -User user -Pwd C:\scripts\ps-script-pwd.txt -UseSsl
 
    This will shutdown, one at a time, all the VMs listed in the file located in C:\scripts\vms.txt and back up
    their files to \\nas\vms, using E:\temp as a working directory. A .7z file for each VM folder will be created using
    7-zip. 7-zip will use 8 threads, medium compression and split the files in 2GB volumes. Any backups older than 30 days
    will also be deleted. The log file will be output to C:\scripts\logs and sent via e-mail with a custom subject line.
#>


## Set up command line switches.
[CmdletBinding()]
Param(
    [parameter(Mandatory=$True)]
    [alias("BackupTo")]
    $Backup,
    [alias("Keep")]
    $History,
    [alias("List")]
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    $VmList,
    [alias("Wd")]
    $WorkDir,
    [alias("SzOptions")]
    $SzSwitches,
    [alias("L")]
    [ValidateScript({Test-Path $_ -PathType 'Container'})]
    $LogPath,
    [alias("Subject")]
    $MailSubject,
    [alias("SendTo")]
    $MailTo,
    [alias("From")]
    $MailFrom,
    [alias("Smtp")]
    $SmtpServer,
    [alias("Port")]
    $SmtpPort,
    [alias("User")]
    $SmtpUser,
    [alias("Pwd")]
    [ValidateScript({Test-Path -Path $_ -PathType Leaf})]
    $SmtpPwd,
    [switch]$UseSsl,
    [switch]$NoPerms,
    [switch]$Compress,
    [switch]$Sz,
    [switch]$ShortDate,
    [switch]$NoBanner)

If ($NoBanner -eq $False)
{
    Write-Host ""
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " _ _ __ __ ____ _ _ _ _ _ _ _ _ "
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " | | | | \ \ / / | _ \ | | | | | | | (_) (_) | "
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " | |__| |_ _ _ __ ___ _ _\ \ / / | |_) | __ _ ___| | ___ _ _ __ | | | | |_ _| |_| |_ _ _ "
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " | __ | | | | '_ \ / _ \ '__\ \/ / | _ < / _ |/ __| |/ / | | | '_ \ | | | | __| | | | __| | | | "
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " | | | | |_| | |_) | __/ | \ / | |_) | (_| | (__| <| |_| | |_) | | |__| | |_| | | | |_| |_| | "
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " |_| |_|\__, | .__/ \___|_| \/ |____/ \__,_|\___|_|\_\\__,_| .__/ \____/ \__|_|_|_|\__|\__, | "
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " __/ | | | | __/ | "
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " |___/|_| Mike Galvin https://gal.vin |_| Version 21.08.10 |___/ "
    Write-Host -ForegroundColor Yellow -BackgroundColor Black " "
    Write-Host ""
}

## If logging is configured, start logging.
## If the log file already exists, clear it.
If ($LogPath)
{
    $LogFile = ("Hyper-V-Backup_{0:yyyy-MM-dd_HH-mm-ss}.log" -f (Get-Date))
    $Log = "$LogPath\$LogFile"

    $LogT = Test-Path -Path $Log

    If ($LogT)
    {
        Clear-Content -Path $Log
    }

    Add-Content -Path $Log -Encoding ASCII -Value "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") [INFO] Log started"
}

## Function to get date in specific format.
Function Get-DateFormat
{
    Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}

Function Get-DateShort
{
    Get-Date -Format "yyyy-MM-dd"
}

Function Get-DateLong
{
    Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
}

## Function for logging.
Function Write-Log($Type, $Evt)
{
    If ($Type -eq "Info")
    {
        If ($Null -ne $LogPath)
        {
            Add-Content -Path $Log -Encoding ASCII -Value "$(Get-DateFormat) [INFO] $Evt"
        }

        Write-Host "$(Get-DateFormat) [INFO] $Evt"
    }

    If ($Type -eq "Succ")
    {
        If ($Null -ne $LogPath)
        {
            Add-Content -Path $Log -Encoding ASCII -Value "$(Get-DateFormat) [SUCCESS] $Evt"
        }

        Write-Host -ForegroundColor Green "$(Get-DateFormat) [SUCCESS] $Evt"
    }

    If ($Type -eq "Err")
    {
        If ($Null -ne $LogPath)
        {
            Add-Content -Path $Log -Encoding ASCII -Value "$(Get-DateFormat) [ERROR] $Evt"
        }

        Write-Host -ForegroundColor Red -BackgroundColor Black "$(Get-DateFormat) [ERROR] $Evt"
    }

    If ($Type -eq "Conf")
    {
        If ($Null -ne $LogPath)
        {
            Add-Content -Path $Log -Encoding ASCII -Value "$Evt"
        }

        Write-Host -ForegroundColor Cyan -Object "$Evt"
    }
}


##
## Start of backup Options function
##

Function OptionsRun
{
    ## Remove previous backup folders. -Keep switch and -Compress switch are NOT configured.
    If ($Null -eq $History -And $Compress -eq $False)
    {
        Write-Log -Type Info -Evt "Removing previous backups of $Vm"

        ## Remove all previous backup folders
        If ($ShortDate)
        {
            Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*" -Directory | Remove-Item -Recurse -Force
        }

        else {
            Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*_*-*-*" -Directory | Remove-Item -Recurse -Force
        }

        ## If working directory is configured by user, remove all previous backup folders
        If ($WorkDir -ne $Backup)
        {
            ## Make sure the backup directory exists.
            $BackupFolderT = Test-Path $Backup

            If ($BackupFolderT)
            {
                If ($ShortDate)
                {
                    Get-ChildItem -Path $Backup -Filter "$VmFixed-*-*-*" -Directory | Remove-Item -Recurse -Force
                }

                else {
                    Get-ChildItem -Path $Backup -Filter "$VmFixed-*-*-*_*-*-*" -Directory | Remove-Item -Recurse -Force
                }
            }
        }
    }

    ## Remove previous backup folders older than X configured days. -Keep switch is configuired and -Compress switch is NOT.
    else {
        If ($Compress -eq $False)
        {
            Write-Log -Type Info -Evt "Removing backup folders of $Vm older than: $History days"

            ## Remove previous backup folders older than the configured number of days.
            If ($ShortDate)
            {
                Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*" -Directory | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Recurse -Force
            }

            else {
                Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*_*-*-*" -Directory | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Recurse -Force
            }

            ## If working directory is configured by user, remove all previous backup folders older than X configured days.
            If ($WorkDir -ne $Backup)
            {
                ## Make sure the backup directory exists.
                $BackupDirT = Test-Path $Backup

                If ($BackupDirT)
                {
                    If ($ShortDate)
                    {
                        Get-ChildItem -Path $Backup -Filter "$VmFixed-*-*-*" -Directory | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Recurse -Force
                    }

                    else {
                        Get-ChildItem -Path $Backup -Filter "$VmFixed-*-*-*_*-*-*" -Directory | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Recurse -Force
                    }
                }
            }
        }
    }

    ## Remove ALL previous backup files. -Keep switch is NOT configuired and -Compress switch IS.
    If ($Compress)
    {
        If ($Null -eq $History)
        {
            Write-Log -Type Info -Evt "Removing all previous compressed backups"

            ## Remove all previous compressed backups
            If ($ShortDate)
            {
                Remove-Item "$WorkDir\$VmFixed-*-*-*.*" -Force
            }

            else {
                Remove-Item "$WorkDir\$VmFixed-*-*-*_*-*-*.*" -Force
            }

            ## If working directory is configured by user, remove all previous backup files.
            If ($WorkDir -ne $Backup)
            {
                ## Make sure the backup directory exists.
                $BackupFolderT = Test-Path $Backup

                If ($BackupFolderT)
                {
                    If ($ShortDate)
                    {
                        Remove-Item "$Backup\$VmFixed-*-*-*.*" -Force
                    }

                    else {
                        Remove-Item "$Backup\$VmFixed-*-*-*_*-*-*.*" -Force
                        }
                }
            }
        }

        ## Remove previous backup files older than X days. -Keep and -Compress switch are configured.
        else {

            Write-Log -Type Info -Evt "Removing compressed backups of $Vm older than: $History days"

            ## Remove previous compressed backups older than the configured number of days.
            If ($ShortDate)
            {
                Get-ChildItem -Path "$WorkDir\$VmFixed-*-*-*.*" | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Force
            }

            else {
                Get-ChildItem -Path "$WorkDir\$VmFixed-*-*-*_*-*-*.*" | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Force
            }

            ## If working directory is configured by user, remove previous backup files older than X days.
            If ($WorkDir -ne $Backup)
            {
                ## Make sure the backup directory exists.
                $BackupFolderT = Test-Path $Backup

                If ($BackupFolderT)
                {
                    If ($ShortDate)
                    {
                        Get-ChildItem -Path "$Backup\$VmFixed-*-*-*.*" | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Force
                    }

                    else {
                        Get-ChildItem -Path "$Backup\$VmFixed-*-*-*_*-*-*.*" | Where-Object CreationTime –lt (Get-Date).AddDays(-$History) | Remove-Item -Force
                    }
                }
            }
        }

        ## If -Compress and -Sz are configured AND 7-zip is installed - compress the backup folder, if it isn't fallback to Windows compression.
        If ($Sz -eq $True -AND $7zT -eq $True)
        {
            Write-Log -Type Info -Evt "Compressing $Vm backup using 7-Zip compression"

            ## If -Shortdate is configured, test for an old backup file, if true append a number (and increase the number if file still exists) before the file extension.
            If ($ShortDate)
            {
                ## If using 7zip's split file feature with short dates, we need to handle the files a little differently.
                If ($SzSwSplit -like "-v*")
                {
                    $ShortDateT = Test-Path -Path ("$WorkDir\$VmFixed-$(Get-DateShort).*.*")

                    If ($ShortDateT)
                    {
                        Write-Log -Type Info -Evt "File $VmFixed-$(Get-DateShort) already exists, appending number"
                        $i = 1
                        $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)
                        $ShortDateExistT = Test-Path -Path "$WorkDir\$ShortDateNN.*.*"

                        If ($ShortDateExistT)
                        {
                            do {
                                $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)
                                $ShortDateExistT = Test-Path -Path "$WorkDir\$ShortDateNN.*.*"
                            } until ($ShortDateExistT -eq $false)
                        }

                        ## 7-zip compression with shortdate configured and a number appened.
                        try {
                            & "$env:programfiles\7-Zip\7z.exe" $SzSwSplit -bso0 a ("$WorkDir\$ShortDateNN") "$WorkDir\$Vm\*"
                        }
                        catch{
                            $_.Exception.Message | Write-Log -Type Err -Evt $_
                        }
                    }

                    else {
                        ## 7-zip compression with shortdate configured and no need for a number appened.
                        try {
                            & "$env:programfiles\7-Zip\7z.exe" $SzSwSplit -bso0 a ("$WorkDir\$VmFixed-$(Get-DateShort)") "$WorkDir\$Vm\*"
                        }
                        catch{
                            $_.Exception.Message | Write-Log -Type Err -Evt $_
                        }
                    }
                }

                else
                {
                    $ShortDateT = Test-Path -Path ("$WorkDir\$VmFixed-$(Get-DateShort).*")

                    If ($ShortDateT)
                    {
                        Write-Log -Type Info -Evt "File $VmFixed-$(Get-DateShort) already exists, appending number"
                        $i = 1
                        $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)
                        $ShortDateExistT = Test-Path -Path "$WorkDir\$ShortDateNN.*"

                        If ($ShortDateExistT)
                        {
                            do {
                                $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)
                                $ShortDateExistT = Test-Path -Path "$WorkDir\$ShortDateNN.*"
                            } until ($ShortDateExistT -eq $false)
                        }

                        ## 7-zip compression with shortdate configured and a number appened.
                        try {
                            & "$env:programfiles\7-Zip\7z.exe" $SzSwSplit -bso0 a ("$WorkDir\$ShortDateNN") "$WorkDir\$Vm\*"
                        }
                        catch{
                            $_.Exception.Message | Write-Log -Type Err -Evt $_
                        }
                    }

                    ## 7-zip compression with shortdate configured and no need for a number appened.
                    try {
                        & "$env:programfiles\7-Zip\7z.exe" $SzSwSplit -bso0 a ("$WorkDir\$VmFixed-$(Get-DateShort)") "$WorkDir\$Vm\*"
                    }
                    catch{
                        $_.Exception.Message | Write-Log -Type Err -Evt $_
                    }
                }
            }

            else {
                ## 7-zip compression with longdate.
                try {
                    & "$env:programfiles\7-Zip\7z.exe" $SzSwSplit -bso0 a ("$WorkDir\$VmFixed-$(Get-DateLong)") "$WorkDir\$Vm\*"
                }
                catch{
                    $_.Exception.Message | Write-Log -Type Err -Evt $_
                }
            }
        }

        ## Compress the backup folder using Windows compression. -Compress is configured, -Sz switch is not, or it is and 7-zip isn't detected.
        ## This is also the "fallback" windows compression code.
        else {
            Write-Log -Type Info -Evt "Compressing $Vm backup using Windows compression"
            Add-Type -AssemblyName "system.io.compression.filesystem"

            If ($ShortDate)
            {
                $ShortDateT = Test-Path -Path ("$WorkDir\$VmFixed-$(Get-DateShort).zip")

                If ($ShortDateT)
                {
                    Write-Log -Type Info -Evt "File $VmFixed-$(Get-DateShort) already exists, appending number"
                    $i = 1
                    $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}.zip" -f $i++)
                    $ShortDateExistT = Test-Path -Path $WorkDir\$ShortDateNN

                    If ($ShortDateExistT)
                    {
                        do {
                            $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}.zip" -f $i++)
                            $ShortDateExistT = Test-Path -Path $WorkDir\$ShortDateNN
                        } until ($ShortDateExistT -eq $false)
                    }

                    ## Windows compression with shortdate configured and a number appened.
                    try {
                        [io.compression.zipfile]::CreateFromDirectory("$WorkDir\$Vm", ("$WorkDir\$ShortDateNN"))
                    }
                    catch{
                        $_.Exception.Message | Write-Log -Type Err -Evt $_
                    }
                }

                else {
                    try {
                        [io.compression.zipfile]::CreateFromDirectory("$WorkDir\$Vm", ("$WorkDir\$VmFixed-$(Get-DateShort).zip"))
                    }
                    catch{
                        $_.Exception.Message | Write-Log -Type Err -Evt $_
                    }
                }
            }

            else {
                try {
                    [io.compression.zipfile]::CreateFromDirectory("$WorkDir\$Vm", ("$WorkDir\$VmFixed-$(Get-DateLong).zip"))
                }
                catch{
                    $_.Exception.Message | Write-Log -Type Err -Evt $_
                }
            }
        }

        ## Remove the VMs export folder.
        Get-ChildItem -Path $WorkDir -Filter "$Vm" -Directory | Remove-Item -Recurse -Force

        ## If working directory has been configured by the user, move the compressed backup to the backup folder and rename to include the date.
        If ($WorkDir -ne $Backup)
        {
            ## Make sure the backup directory exists.
            $BackupFolderT = Test-Path $Backup

            If ($BackupFolderT -eq $False)
            {
                Write-Log -Type Info -Evt "Backup directory $Backup doesn't exist. Creating it."
                New-Item $Backup -ItemType Directory -Force | Out-Null
            }

            ## Get the exact name of the backup file and append numbers onto the filename, keeping the extention intact.
            If ($ShortDate)
            {
                If ($SzSwSplit -like "-v*")
                {
                    $SzSplitFiles = Get-ChildItem -Path ("$WorkDir\$VmFixed-$(Get-DateShort).*.*") -File
                    
                    ForEach ($SplitFile in $SzSplitFiles) {
                        $ShortDateT = Test-Path -Path "$Backup\$($SplitFile.name)"

                        If ($ShortDateT)
                        {
                            Write-Log -Type Info -Evt "File $($SplitFile.name) already exists, appending number"
                            $FileExist = Get-ChildItem -Path "$Backup\$($SplitFile.name)" -File
                            $i = 1

                            $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + $FileExist.Extension)
                            $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN

                            If ($ShortDateExistT)
                            {
                                do {
                                    $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + $FileExist.Extension)
                                    $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                                } until ($ShortDateExistT -eq $false)
                            }

                            try {
                                Get-ChildItem -Path $SplitFile | Move-Item -Destination $Backup\$ShortDateNN -ErrorAction 'Stop'
                            }
                            catch{
                                $_.Exception.Message | Write-Log -Type Err -Evt $_
                            }
                        }

                        else {
                            try {
                                Get-ChildItem -Path $SplitFile | Move-Item -Destination $Backup\$ShortDateNN -ErrorAction 'Stop'
                            }
                            catch{
                                $_.Exception.Message | Write-Log -Type Err -Evt $_
                            }
                        }
                    }
                }

                else{
                    $BackupFile = Get-ChildItem -Path ("$WorkDir\$VmFixed-$(Get-DateShort).*") -File
                    $BackupFileN = $BackupFile.name
                    $BackupFileNSplit = $BackupFileN.split(".")

                    $ShortDateT = Test-Path -Path $Backup\$BackupFileN

                    If ($ShortDateT)
                    {
                        Write-Log -Type Info -Evt "File $BackupFileN already exists, appending number"
                        $FileExist = Get-ChildItem -Path $BackupFile -File
                        $i = 1
                        
                        If ($Null -eq $BackupFileNSplit[2])
                        {
                            $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + $FileExist.Extension)
                            $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                        }
                        else {
                            $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + "." + $BackupFileNSplit[1] + $FileExist.Extension)
                            $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                        }

                        If ($ShortDateExistT)
                        {
                            If ($Null -eq $BackupFileNSplit[2])
                            {
                                do {
                                    $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + $FileExist.Extension)
                                    $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                                } until ($ShortDateExistT -eq $false)
                            }
                            else {
                                do {
                                    $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++ + "." + $BackupFileNSplit[1] + $FileExist.Extension)
                                    $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                                } until ($ShortDateExistT -eq $false)
                            }
                        }

                        ## Move with shortdate and appened number
                        try {
                            Get-ChildItem -Path $BackupFile | Move-Item -Destination $Backup\$ShortDateNN -ErrorAction 'Stop'
                        }
                        catch{
                            $_.Exception.Message | Write-Log -Type Err -Evt $_
                        }
                    }

                    ## Move with shortdate
                    try {
                        Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*.*" | Move-Item -Destination $Backup -ErrorAction 'Stop'
                    }
                    catch{
                        $_.Exception.Message | Write-Log -Type Err -Evt $_
                    }
                }
            }

            ## Move with long date
            else {
                $BackupFile = Get-ChildItem -Path ("$WorkDir\$VmFixed-$(Get-DateLong).*") -File
                $BackupFileN = $BackupFile.name

                try {
                    Get-ChildItem -Path $WorkDir -Filter $BackupFileN | Move-Item -Destination $Backup -ErrorAction 'Stop'
                }
                catch{
                    $_.Exception.Message | Write-Log -Type Err -Evt $_
                }
            }
        }
    }

    ## -Compress switch is NOT configured and the -Keep switch is configured.
    ## Rename the export of each VM to include the date.
    else {
        If ($ShortDate)
        {
            $ShortDateT = Test-Path -Path ("$WorkDir\$VmFixed-$(Get-DateShort)")

            If ($ShortDateT)
            {
                Write-Log -Type Info -Evt "File $VmFixed-$(Get-DateShort) already exists, appending number"
                $i = 1
                $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)
                $ShortDateExistT = Test-Path -Path $WorkDir\$ShortDateNN

                If ($ShortDateExistT)
                {
                    do {
                        $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)
                        $ShortDateExistT = Test-Path -Path $WorkDir\$ShortDateNN
                    } until ($ShortDateExistT -eq $false)
                }

                try {
                    Get-ChildItem -Path $WorkDir -Filter $Vm -Directory | Rename-Item -NewName ("$WorkDir\$ShortDateNN")
                }
                catch{
                    $_.Exception.Message | Write-Log -Type Err -Evt $_
                }
            }

            try {
                Get-ChildItem -Path $WorkDir -Filter $Vm -Directory | Rename-Item -NewName ("$WorkDir\$VmFixed-$(Get-DateShort)")
            }
            catch{
                $_.Exception.Message | Write-Log -Type Err -Evt $_
            }
        }

        else {
            try {
                Get-ChildItem -Path $WorkDir -Filter $Vm -Directory | Rename-Item -NewName ("$WorkDir\$VmFixed-$(Get-DateLong)")
            }
            catch{
                $_.Exception.Message | Write-Log -Type Err -Evt $_
            }
        }

        ## If working directory has been configured by the user, move the backup to the backup folder and rename to include the date.
        If ($WorkDir -ne $Backup)
        {
            ## Make sure the backup directory exists.
            $BackupFolderT = Test-Path $Backup

            If ($BackupFolderT -eq $False)
            {
                Write-Log -Type Info -Evt "Backup directory $Backup doesn't exist. Creating it."
                New-Item $Backup -ItemType Directory -Force | Out-Null
            }

            If ($ShortDate)
            {
                $ShortDateT = Test-Path -Path ("$Backup\$VmFixed-$(Get-DateShort)")

                If ($ShortDateT)
                {
                    Write-Log -Type Info -Evt "File $VmFixed-$(Get-DateShort) already exists, appending number"
                    $i = 1
                    $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)
                    $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN

                    ## If backup folder already exists with same name, append a number
                    If ($ShortDateExistT)
                    {
                        do {
                            $ShortDateNN = ("$VmFixed-$(Get-DateShort)-{0:D3}" -f $i++)
                            $ShortDateExistT = Test-Path -Path $Backup\$ShortDateNN
                        } until ($ShortDateExistT -eq $false)
                    }

                    ## Moving backup folder with shortdate and append number
                    try {
                        Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*" -Directory | Move-Item -Destination $Backup\$ShortDateNN -ErrorAction 'Stop'
                    }
                    catch{
                        $_.Exception.Message | Write-Log -Type Err -Evt $_
                    }
                }

                ## Moving backup folder with shortdate
                try {
                    Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*" -Directory | Move-Item -Destination ("$Backup\$VmFixed-$(Get-DateShort)") -ErrorAction 'Stop'
                }
                catch{
                    $_.Exception.Message | Write-Log -Type Err -Evt $_
                }
            }

            ## Moving backup folder with longdate
            else {
                try {
                    Get-ChildItem -Path $WorkDir -Filter "$VmFixed-*-*-*_*-*-*" -Directory | Move-Item -Destination ("$Backup\$VmFixed-$(Get-DateLong)") -ErrorAction 'Stop'
                }
                catch{
                    $_.Exception.Message | Write-Log -Type Err -Evt $_
                }
            }
        }
    }
}
##
## End of backup Options function
##

## Setting an easier to use variable for computer name of the Hyper-V server.
$Vs = $Env:ComputerName

## If a VM list file is configured, get the content of the file, otherwise just get the running VMs.
If ($VmList)
{
    $Vms = Get-Content $VmList
}

else {
    $Vms = Get-VM | Where-Object {$_.State -eq 'Running'} | Select-Object -ExpandProperty Name
}

## Check to see if there are any VMs to process.
## If there are no VMs, then do nothing.
If ($Vms.count -ne 0)
{
    ## If the user has not configured the working directory, set it as the backup directory.
    If ($Null -eq $WorkDir)
    {
        $WorkDir = "$Backup"
    }

    If ($Null -eq $ShortDate)
    {
        $ShortDate = "$LongDate"
    }

    If ($Null -ne $SzSwitches)
    {
        $SzSwSplit = $SzSwitches.split(",")
    }

    If ($Sz -eq $True)
    {
        $7zT = Test-Path "$env:programfiles\7-Zip\7z.exe"
    }

    ##
    ## Display the current config and log if configured.
    ##

    Write-Log -Type Conf -Evt "************ Running with the following config *************."
    Write-Log -Type Conf -Evt "This virtual host:.......$Vs."
    Write-Log -Type Conf -Evt "VMs to backup:..........."

    ForEach ($Vm in $Vms)
    {
        Write-Log -Type Conf -Evt ".........................$Vm"
    }

    Write-Log -Type Conf -Evt "Backup directory:........$Backup."
    Write-Log -Type Conf -Evt "Working directory:.......$WorkDir."
    
    If ($Null -ne $History)
    {
        Write-Log -Type Conf -Evt "Backups to keep:.........$History days"
    }

    else {
        Write-Log -Type Conf -Evt "Backups to keep:.........No Config"
    }

    If ($Null -ne $LogPath)
    {
        Write-Log -Type Conf -Evt "Logs directory:..........$LogPath."
    }
    
    else {
        Write-Log -Type Conf -Evt "Logs directory:..........No Config"
    }
    
    If ($MailTo)
    {
        Write-Log -Type Conf -Evt "E-mail log to:...........$MailTo."
    }
    
    else {
        Write-Log -Type Conf -Evt "E-mail log to:...........No Config"
    }
    
    If ($MailFrom)
    {
        Write-Log -Type Conf -Evt "E-mail log from:.........$MailFrom."
    }
    
    else {
        Write-Log -Type Conf -Evt "E-mail log from:.........No Config"
    }
    
    If ($MailSubject)
    {
        Write-Log -Type Conf -Evt "E-mail subject:..........$MailSubject."
    }

    else {
        Write-Log -Type Conf -Evt "E-mail subject:..........Default"
    }

    If ($SmtpServer)
    {
        Write-Log -Type Conf -Evt "SMTP server:.............$SmtpServer."
    }

    else {
        Write-Log -Type Conf -Evt "SMTP server:.............No Config"
    }

    If ($SmtpPort)
    {
        Write-Log -Type Conf -Evt "SMTP Port:...............$SmtpPort."
    }

    else {
        Write-Log -Type Conf -Evt "SMTP Port:...............Default"
    }

    If ($SmtpUser)
    {
        Write-Log -Type Conf -Evt "SMTP user:...............$SmtpUser."
    }

    else {
        Write-Log -Type Conf -Evt "SMTP user:...............No Config"
    }

    If ($SmtpPwd)
    {
        Write-Log -Type Conf -Evt "SMTP pwd file:...........$SmtpPwd."
    }

    else {
        Write-Log -Type Conf -Evt "SMTP pwd file:...........No Config"
    }

    Write-Log -Type Conf -Evt "-UseSSL switch:..........$UseSsl."
    Write-Log -Type Conf -Evt "-NoPerms switch:.........$NoPerms."
    Write-Log -Type Conf -Evt "-ShortDate switch:.......$ShortDate."
    Write-Log -Type Conf -Evt "-Compress switch:........$Compress."
    Write-Log -Type Conf -Evt "-Sz switch:..............$Sz."

    If ($Sz)
    {
        Write-Log -Type Conf -Evt "7-zip installed:.........$7zT."
    }

    If ($SzSwitches)
    {
        Write-Log -Type Conf -Evt "7-zip Options:...........$SzSwitches."
    }
    
    else {
        Write-Log -Type Conf -Evt "7-zip Options:...........Default."
    }

    Write-Log -Type Conf -Evt "************************************************************"
    Write-Log -Type Info -Evt "Process started."

    ##
    ## Display current config ends here.
    ##

    ##
    ## -NoPerms process starts here.
    ##

    ## If the -NoPerms switch is set, start a custom process to copy all the VM data.
    If ($NoPerms)
    {
        ForEach ($Vm in $Vms)
        {
            ## For 7zip, replace . dots with - hyphens in the vm name
            $VmFixed = $Vm.replace(".","-")
            $VmInfo = Get-VM -name $Vm

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

            ## Create directories for the VM export.
            try {
                New-Item "$WorkDir\$Vm" -ItemType Directory -Force | Out-Null
                New-Item "$WorkDir\$Vm\Virtual Machines" -ItemType Directory -Force | Out-Null
                New-Item "$WorkDir\$Vm\VHD" -ItemType Directory -Force | Out-Null
                New-Item "$WorkDir\$Vm\Snapshots" -ItemType Directory -Force | Out-Null
            }
            catch{
                $_.Exception.Message | Write-Log -Type Err -Evt $_
            }

            Write-Log -Type Info -Evt "Stopping VM: $Vm"
            Stop-VM $Vm

            ##
            ## Copy the VM config files and test for success or failure.
            ##

            try {
                Copy-Item "$($VmInfo.ConfigurationLocation)\Virtual Machines\$($VmInfo.id)" "$WorkDir\$Vm\Virtual Machines\" -Recurse -Force
                Copy-Item "$($VmInfo.ConfigurationLocation)\Virtual Machines\$($VmInfo.id).*" "$WorkDir\$Vm\Virtual Machines\" -Recurse -Force
            }
            catch{
                $_.Exception.Message | Write-Log -Type Err -Evt $_
            }

            ##
            ## End of VM config files.
            ##

            ##
            ## Copy the VHDs and test for success or failure.
            ##

            try {
                Copy-Item $VmInfo.HardDrives.Path -Destination "$WorkDir\$Vm\VHD\" -Recurse -Force
            }
            catch{
                $_.Exception.Message | Write-Log -Type Err -Evt $_
            }

            ##
            ## End of VHDs.
            ##

            ## Get the VM snapshots/checkpoints.
            $Snaps = Get-VMSnapshot $Vm

            ForEach ($Snap in $Snaps)
            {
                ##
                ## Copy the snapshot config files and test for success or failure.
                ##

                try {
                    Copy-Item "$($VmInfo.ConfigurationLocation)\Snapshots\$($Snap.id)" "$WorkDir\$Vm\Snapshots\" -Recurse -Force
                    Copy-Item "$($VmInfo.ConfigurationLocation)\Snapshots\$($Snap.id).*" "$WorkDir\$Vm\Snapshots\" -Recurse -Force
                }
                catch{
                    $_.Exception.Message | Write-Log -Type Err -Evt $_
                }

                ##
                ## End of snapshot config.
                ##

                ## Copy the snapshot root VHD.
                try {
                    Copy-Item $Snap.HardDrives.Path -Destination "$WorkDir\$Vm\VHD\" -Recurse -Force -ErrorAction 'Stop'
                }
                catch{
                    $_.Exception.Message | Write-Log -Type Err -Evt $_
                }
            }

            Start-VM $Vm
            Write-Log -Type Info -Evt "Starting VM: $Vm"
            Start-Sleep -S 60
            OptionsRun
        }
    }

    ##
    ## -NoPerms process ends here.
    ##
    ##
    ## Standard export process starts here.
    ##

    ## If the -NoPerms switch is NOT set, for each VM check for the existence of a previous export.
    ## If it exists then delete it, otherwise the export will fail.
    else {
        ForEach ($Vm in $Vms)
        {
            ## For 7zip, replace . dots with - hyphens in the vm name
            $VmFixed = $Vm.replace(".","-")

            $VmExportBackupT = Test-Path "$WorkDir\$VmFixed"
            If ($VmExportBackupT -eq $True)
            {
                Remove-Item "$WorkDir\$VmFixed" -Recurse -Force
            }

            If ($WorkDir -ne $Backup)
            {
                $VmExportWDT = Test-Path "$Backup\$VmFixed"
                If ($VmExportWDT -eq $True)
                {
                    Remove-Item "$Backup\$VmFixed" -Recurse -Force
                }
            }
        }

        ## Do a regular export of the VMs.
        ForEach ($Vm in $Vms)
        {
            ## For 7zip, replace . dots with - hyphens in the vm name
            $VmFixed = $Vm.replace(".","-")

            Write-Log -Type Info -Evt "Attempting to export $Vm"
            try {
                $Vm | Export-VM -Path "$WorkDir" -ErrorAction 'Stop'
            }
            catch{
                $_.Exception.Message | Write-Log -Type Err -Evt $_
            }
        }

        ## Run the configuration options on the above backup files and folders.
        ForEach ($Vm in $Vms)
        {
            ## For 7zip filename, replace . dots with - hyphens in the vm name
            $VmFixed = $Vm.replace(".","-")
            OptionsRun
        }
    }

    ##
    ## End of standard export block
    ##
}

## If there are no VMs running, then do nothing.
else {
    Write-Log -Type Info -Evt "There are no VMs running to backup"
}

Write-Log -Type Info -Evt "Process finished."

## If logging is configured then finish the log file.
If ($LogPath)
{
    Add-Content -Path $Log -Encoding ASCII -Value "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") [INFO] Log finished"

    ## This whole block is for e-mail, if it is configured.
    If ($SmtpServer)
    {
        ## Default e-mail subject if none is configured.
        If ($Null -eq $MailSubject)
        {
            $MailSubject = "Hyper-V Backup Utility Log"
        }

        ## Default Smtp Port if none is configured.
        If ($Null -eq $SmtpPort)
        {
            $SmtpPort = "25"
        }

        ## Setting the contents of the log to be the e-mail body.
        $MailBody = Get-Content -Path $Log | Out-String

        ## If an smtp password is configured, get the username and password together for authentication.
        ## If an smtp password is not provided then send the e-mail without authentication and obviously no SSL.
        If ($SmtpPwd)
        {
            $SmtpPwdEncrypt = Get-Content $SmtpPwd | ConvertTo-SecureString
            $SmtpCreds = New-Object System.Management.Automation.PSCredential -ArgumentList ($SmtpUser, $SmtpPwdEncrypt)

            ## If -ssl switch is used, send the email with SSL.
            ## If it isn't then don't use SSL, but still authenticate with the credentials.
            If ($UseSsl)
            {
                Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer -Port $SmtpPort -UseSsl -Credential $SmtpCreds
            }

            else {
                Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer -Port $SmtpPort -Credential $SmtpCreds
            }
        }

        else {
            Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer -Port $SmtpPort
        }
    }
    ## End of Email block
}

## End