# encoding: ascii
# api: powershell
# title: Backup Hyper-V VMs
# description: This complete module greatly facilitates backing up your Hyper-V virtual machines.
# version: 0.1
# type: script
# author: jhjbhjb h
# license: CC0
# function: Backup-VMs
# x-poshcode-id: 6430
# x-archived: 2016-07-06T18:57:15
# x-published: 2016-07-01T04:34:00
#
# It works with VMs hosted on any networked machine.
# It pauses (if needed) and exports all specified VMs (or just everything if you don’t specify).
# The exported VMs are then placed into a compressed archive to save space.
# If specified, the host (and any VMs on it) can then be shut down.
# All segments that I plagiarized are noted in the module.
# Requires:
# Administrator privileges
# PowerShell Management Library for Hyper-V (http://pshyperv.codeplex.com/downloads/get/219013)
# Windows PowerShell Pack (http://archive.msdn.microsoft.com/PowerShellPack/Release/ProjectReleases.aspx?ReleaseId=3341)
#
#requires -Version 2
<#
Directions for use:
Import this script using the Import-Module cmdlet
All output is logged to the backup directory in the $($BackupDriveLetter):\VMBackup\Backup-VMs.log file
Use the Backup-VMs cmdlet to begin the process
Parameter BackupDriveLetter indicates the drive to put this backup onto. It must be mounted to the host running the script.
Parameter VMHost defines the host that contains the VMs you want to back up. If it's blank, then it just targets the host the script is running on
Parameter VMNames defines the specific VMs you wish to backup, otherwise it'll back up all of them on the target host
Switch parameter ShutHostDownWhenFinished will cause the specified host (and any VMs running on it) to shut down upon completion of the backup
Example:
PS> Import-Module D:\Backup-VMs.ps1
PS> Backup-VMs -BackupDriveLetter F -VMHost HyperVHost -VMNames mydevmachine,broker77
----------------------------------------------------------------------------
Note that this script requires administrator privileges for proper execution
----------------------------------------------------------------------------
Note that this script requires the following:
PowerShell Management Library for Hyper-V (for the Get-VM and Export-VM cmdlets)
This installs itself wherever you downloaded it - make sure the HyperV folder finds its way to somewhere in $env:PSModulePath
http://pshyperv.codeplex.com/downloads/get/219013
Windows PowerShell Pack (for the Copy-ToZip cmdlet)
This installs to $home\Documents\WindowsPowerShell\Modules, make sure that this path is in $env:PSModulePath
http://archive.msdn.microsoft.com/PowerShellPack/Release/ProjectReleases.aspx?ReleaseId=3341
#>
# our one global variable is for logging
$Logfile = ''
function Backup-VMs {
<#
.SYNOPSIS
The main function
.DESCRIPTION
The main function
.PARAMETER BackupDriveLetter
A description of the BackupDriveLetter parameter.
.PARAMETER VMHost
$BackupDriveLetter:\VMBackups\$backupDate
.PARAMETER VMNames
the host that holds the vms we wish to back up, otherwise the one running the script
.PARAMETER ShutHostDownWhenFinished
if not specified, back up all of them
.NOTES
My Version of Backup Hyper-V VMs by jhjbhjb h
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param
(
[Parameter(Mandatory = $true)]
[System.String]$BackupDriveLetter,
[ValidateNotNullOrEmpty()]
[System.String]$VMHost,
[System.String[]]$VMNames,
[switch]$ShutHostDownWhenFinished
)
process {
# first, run a bunch of checks
#region checks
# check if the PowerShellPack modules are loaded
$isPowerShellPackLoaded = Get-Module -Name PowerShellPack
if (!$isPowerShellPackLoaded) {
Write-Host -Object 'Attempting to load PowerShellPack modules...'
Import-Module -Name PowerShellPack
$isPowerShellPackLoaded = Get-Module -Name PowerShellPack
if (!$isPowerShellPackLoaded) {
Write-Host -ForegroundColor Red -Object 'Cannot load PowerShellPack module - terminating backup script.'
Break
}
}
# check if the HyperV module is loaded
$isHyperVModuleLoaded = Get-Module -Name HyperV
if (!$isHyperVModuleLoaded) {
Write-Host -Object 'Attempting to load HyperV module...'
Import-Module -Name HyperV
$isHyperVModuleLoaded = Get-Module -Name HyperV
if (!$isHyperVModuleLoaded) {
Write-Host -ForegroundColor Red -Object 'Cannot load HyperV module - terminating backup script.'
Break
}
}
# sanitize user input (F: will become F)
if ($BackupDriveLetter -like '*:') {
$BackupDriveLetter = $BackupDriveLetter -replace ".$"
}
# check to make sure the user specified a valid backup location
if ((Test-Path -Path "$($BackupDriveLetter):") -eq $false) {
Write-Host -ForegroundColor Red -Object "Drive $($BackupDriveLetter): does not exist - terminating backup script."
Break
}
# if host was not speicified, use the host running the script
if ($VMHost -eq '') {
$VMHost = HOSTNAME.EXE
}
# check to make sure the specified host is a vmhost
if (!(Get-VMHost) -icontains $VMHost) {
Write-Host -ForegroundColor Red -Object "Host $($VMHost) is not listed in Get-VMHost - terminating backup script."
Break
}
# check to make sure the specified host has any vms to back up
if (!(Get-VM -Server $VMHost)) {
Write-Host -ForegroundColor Red -Object "Host $($VMHost) does not appear to have any VMs running on it according to 'Get-VM -Server $($VMHost)'."
Write-Host -ForegroundColor Yellow -Object 'This can be occur if PowerShell is not running with elevated privileges.'
Write-Host -ForegroundColor Yellow -Object 'Please make sure that you are running PowerShell with Administrator privileges and try again.'
Write-Host -ForegroundColor Red -Object 'Terminating backup script.'
Break
}
#endregion
#region directory business
# make our parent directory if needed
if ((Test-Path -Path "$($BackupDriveLetter):\VMBackup") -eq $false) {
$parentDir = New-Item -Path "$($BackupDriveLetter):\VMBackup" -ItemType 'directory'
if ((Test-Path $parentDir) -eq $false) {
Write-Host -ForegroundColor Red -Object "Problem creating $parentDir - terminating backup script."
Break
}
}
# initialize our logfile
$Logfile = "$($BackupDriveLetter):\VMBackup\Backup-VMs.log"
if ((Test-Path $Logfile) -eq $false) {
$newFile = New-Item -Path $Logfile -ItemType 'file'
if ((Test-Path $Logfile) -eq $false) {
Write-Host -ForegroundColor Red -Object "Problem creating $Logfile - terminating backup script."
Break
}
}
$backupDate = Get-Date -Format 'yyyy-MM-dd'
$destDir = "$($BackupDriveLetter):\VMBackup\$backupDate-$VMHost-backup\"
# make our backup directory if needed
if ((Test-Path $destDir) -eq $false) {
$childDir = New-Item -Path $destDir -ItemType 'directory'
if ((Test-Path $childDir) -eq $false) {
Write-Host -ForegroundColor Red -Object "Problem creating $childDir - terminating backup script."
Break
}
}
#endregion
Add-Content -LiteralPath $Logfile -Value '==================================================================================================='
Add-Content -LiteralPath $Logfile -Value '==================================================================================================='
# now that our checks are done, start backing up
Write-InfoDump -text "Starting Hyper-V virtual machine backup for host $VMHost at:"
$dateTimeStart = date
Write-InfoDump -text "$($dateTimeStart)"
Write-InfoDump -text ''
# export the vms to the destination
Export-MyVms -VMHost $VMHost -Destination $destDir -VMNames $VMNames
Write-InfoDump -text ''
Write-InfoDump -text 'Exporting finished'
#region compression
# get what we just backed up
$sourceDirectory = Get-ChildItem $destDir
if ($sourceDirectory) {
# get the total size of all of the files we just backed up
$sourceDirSize = Get-ChildItem $destDir -Recurse | Measure-Object -Property length -Sum -ErrorAction SilentlyContinue
$sourceDirSize = ($sourceDirSize.sum / 1GB)
# get how much free space is left on our backup drive
$hostname = HOSTNAME.EXE
$backupDrive = Get-WmiObject -Class win32_logicaldisk -ComputerName $hostname | Where-Object -FilterScript { $_.DeviceID -eq "$($BackupDriveLetter):" }
$backupDriveFreeSpace = ($backupDrive.FreeSpace / 1GB)
# tell the user what we've found
$formattedBackupDriveFreeSpace = '{0:N2}' -f $backupDriveFreeSpace
$formattedSourceDirSize = '{0:N2}' -f $sourceDirSize
Write-InfoDump -text 'Checking free space for compression:'
Write-InfoDump -text "Drive $($BackupDriveLetter): has $formattedBackupDriveFreeSpace GB free on it, this backup took $formattedSourceDirSize GB"
# check if we need to make any room for the next backup
$downToOne = $false
while (!$downToOne -and $sourceDirSize > $backupDriveFreeSpace) {
# clear out the oldest backup if this is the case
$backups = Get-ChildItem -Path "$($BackupDriveLetter):\VMBackup\" -Include '*-backup.zip' -Recurse -Name
$backups = [array]$backups | Sort-Object
# make sure we aren't deleting the only directory!
if ($backups.length -gt 1) {
Write-InfoDump -text "Removing the oldest backup [$($backups[0])] to clear up some more room"
Remove-Item -Path "$($BackupDriveLetter):\VMBackup\$($backups[0])" -Recurse -Force
# now check again
$backupDrive = Get-WmiObject -Class win32_logicaldisk -ComputerName $hostname | Where-Object -FilterScript { $_.DeviceID -eq "$($BackupDriveLetter):" }
$backupDriveFreeSpace = ($backupDrive.FreeSpace / 1GB)
$formattedBackupDriveFreeSpace = '{0:N2}' -f $backupDriveFreeSpace
Write-InfoDump -text "Now we have $formattedBackupDriveFreeSpace GB of room"
} else {
# we're down to just one backup left, don't delete it!
$downToOne = $true
}
}
Write-InfoDump -text 'Compressing the backup...'
# zip up everything we just did
Invoke-ZipFolder -directory $destDir -VMHost $VMHost
$zipFileName = $destDir -replace ".$"
$zipFileName = $zipFileName + '.zip'
Write-InfoDump -text "Backup [$($zipFileName)] created successfully"
$destZipFileSize = (Get-ChildItem $zipFileName).Length / 1GB
$formattedDestSize = '{0:N2}' -f $destZipFileSize
Write-InfoDump -text "Uncompressed size:`t$formattedSourceDirSize GB"
Write-InfoDump -text "Compressed size: `t$formattedDestSize GB"
}
#endregion
# delete the non-compressed directory, leaving just the compressed one
Remove-Item $destDir -Recurse -Force
Write-InfoDump -text ''
Write-InfoDump -text "Finished backup of $VMHost at:"
$dateTimeEnd = date
Write-InfoDump -text "$($dateTimeEnd)"
$length = ($dateTimeEnd - $dateTimeStart).TotalMinutes
$length = '{0:N2}' -f $length
Write-InfoDump -text "The operation took $length minutes"
if ($ShutHostDownWhenFinished -eq $true) {
Write-InfoDump -text "Attempting to shut down host machine $VMHost"
Invoke-ShutdownTheHost -HostToShutDown $VMHost
}
}
}
# this function will shut down any vms running on the host executing this script and then shut down said host
function Invoke-ShutdownTheHost {
<#
.SYNOPSIS
this function will shut down any vms running on the host executing this script and then shut down said host
.DESCRIPTION
this function will shut down any vms running on the host executing this script and then shut down said host
.PARAMETER HostToShutDown
A description of the HostToShutDown parameter.
.NOTES
My Version of Backup Hyper-V VMs by jhjbhjb h
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param
(
[System.String]$HostToShutDown
)
process {
# Get a list of all VMs on $HostToShutDown
$VMs = Get-VM -Server $HostToShutDown
# only run through the list if there's anything in it
if ($VMs) {
# For each VM on Node, Save (if necessary), Export and Restore (if necessary)
foreach ($VM in @($VMs)) {
$VMName = $VM.ElementName
$summofvm = get-vmsummary $VMName
$summhb = $summofvm.heartbeat
$summes = $summofvm.enabledstate
# Shutdown the VM if HeartBeat Service responds
if ($summhb -eq 'OK') {
Write-InfoDump -text ''
Write-InfoDump -text "HeartBeat Service for $VMName is responding $summhb, saving the machine state"
Save-VM -VM $VMName -Server $VMHost -Force -Wait
} elseif (($summes -eq 'Stopped') -or ($summes -eq 'Suspended')) {
# Checks to see if the VM is already stopped
Write-InfoDump -text ''
Write-InfoDump -text "$VMName is $summes"
} elseif ($summhb -ne 'OK' -and $summes -ne 'Stopped') {
# If the HeartBeat service is not OK, aborting this VM
Write-InfoDump -text
Write-InfoDump -text "HeartBeat Service for $VMName is responding $summhb. Aborting save state."
}
}
Write-InfoDump -text "All VMs on $HostToShutDown shut down or suspended."
}
Write-InfoDump -text "Shutting down machine $HostToShutDown..."
Stop-Computer -ComputerName $HostToShutDown
}
}
# the following three functions relating to zipping up a folder come from Jeremy Jameson
# http://www.technologytoolbox.com/blog/jjameson/archive/2012/02/28/zip-a-folder-using-powershell.aspx
# I have modified his approach to suit the multi-gigabyte files we'll be dealing with
function Test-IsFileLocked {
<#
.SYNOPSIS
syn
.DESCRIPTION
desc
.PARAMETER path
A description of the path parameter.
.NOTES
My Version of Backup Hyper-V VMs by jhjbhjb h
#>
[CmdletBinding()]
param
(
[System.String]$path
)
[bool]$fileExists = Test-Path $path
If ($fileExists -eq $false) {
Throw 'File does not exist (' + $path + ')'
}
[bool]$isFileLocked = $true
$file = $null
Try {
$file = [IO.File]::Open(
$path,
[IO.FileMode]::Open,
[IO.FileAccess]::Read,
[IO.FileShare]::None)
$isFileLocked = $false
} Catch [IO.IOException] {
If ($_.Exception.Message.EndsWith('it is being used by another process.') -eq $false) {
Throw $_.Exception
}
} Finally {
If ($file -ne $null) {
$file.Close()
}
}
return $isFileLocked
}
function Wait-ForZipOperationToFinish {
<#
.SYNOPSIS
syn
.DESCRIPTION
desc
.PARAMETER zipFile
A description of the zipFile parameter.
.PARAMETER expectedNumberOfItemsInZipFile
A description of the expectedNumberOfItemsInZipFile parameter.
.NOTES
My Version of Backup Hyper-V VMs by jhjbhjb h
#>
[CmdletBinding()]
param
(
[__ComObject]$zipFile,
[System.Int64]$expectedNumberOfItemsInZipFile
)
Write-InfoDump -text "Waiting for zip operation to finish on $($zipFile.Self.Path)..."
Start-Sleep -Seconds 5 # ensure zip operation had time to start
# wait for the operation to finish
# the folder is locked while we're zipping stuff up
[bool]$isFileLocked = Test-IsFileLocked($zipFile.Self.Path)
while ($isFileLocked) {
Write-Host -NoNewline -Object '.'
Start-Sleep -Seconds 5
$isFileLocked = Test-IsFileLocked($zipFile.Self.Path)
}
Write-InfoDump -text ''
}
function Invoke-ZipFolder {
<#
.SYNOPSIS
syn
.DESCRIPTION
desc
.PARAMETER directory
A description of the directory parameter.
.NOTES
My Version of Backup Hyper-V VMs by jhjbhjb h
#>
[CmdletBinding()]
param
(
[IO.DirectoryInfo]$directory
)
$backupFullName = $directory.FullName
Write-InfoDump -text ("Creating zip file for folder ($backupFullName)...")
[IO.DirectoryInfo]$parentDir = $directory.Parent
[string]$zipFileName
If ($parentDir.FullName.EndsWith('\') -eq $true) {
# e.g. $parentDir = "C:\"
$zipFileName = $parentDir.FullName + $directory.Name + '.zip'
} Else {
$zipFileName = $parentDir.FullName + '\' + $directory.Name + '.zip'
}
Set-Content $zipFileName -Value ('PK' + [char]5 + [char]6 + ("$([char]0)" * 18))
$shellApp = New-Object -ComObject Shell.Application
$zipFile = $shellApp.NameSpace($zipFileName)
If ($zipFile -eq $null) {
Write-InfoDump -text 'Failed to get zip file object.'
}
[int]$expectedCount = (Get-ChildItem $directory -Force -Recurse).Count
$expectedCount += 1 # account for the top-level folder
Write-InfoDump -text "Copying $expectedCount items into file $zipFileName..."
$zipFile.CopyHere($directory.FullName)
# wait for CopyHere operation to complete
Wait-ForZipOperationToFinish $zipFile $expectedCount
Write-InfoDump -text "Successfully created zip file for folder ($backupFullName)."
}
<#
Powershell Script to Shutdown and Export Hyper-V 2008 R2 VMs, one at a time.
Written by Stan Czerno
http://www.czerno.com/default.asp?inc=/html/windows/hyperv/cluster/HyperV_Export_VMs.asp
I have modified his approach to suit our purposes
#>
function Export-MyVms {
<#
.SYNOPSIS
The export Job itself
.DESCRIPTION
The export Job itself
.PARAMETER Destination
A description of the Destination parameter.
.PARAMETER VMNames
A description of the VMNames parameter.
.PARAMETER VMHost
A description of the VMHost parameter.
.NOTES
My Version of Backup Hyper-V VMs by jhjbhjb h
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param
(
[System.String]$Destination,
[System.String[]]$VMNames,
[System.String]$VMHost
)
process {
<#
The script requires the PowerShell Management Library for Hyper-V for it to work.
The PowerShell Management Library for Hyper-V can be downloaded at http://pshyperv.codeplex.com/
Be sure to read the documentation before using:
http://pshyperv.codeplex.com/releases/view/62842
http://pshyperv.codeplex.com/releases/view/38769
This is how I backup the VMs on my Two-Node Hyper-V Cluster. I can afford for my servers to be down while this is done and
some of my other resources are clustered so there is minimum down time.
I also do System State Backups, Exchange Backups and SQL Backups in addition.
This script can be used on a Stand-Alone Hyper-V Server as well.
Let me know if you have a better way of doing this as I am not a very good developer and new to Powershell.
#>
# Get a list of all VMs on Node
if ($VMNames) {
if (($VMNames.Length) -gt 1) {
# pass in a multiple-element string array directly
$VMs = Get-VM -Name $VMNames -Server $VMHost
} else {
# turn a single-element string array back into a string
$VMNames = [string]$VMNames
$VMs = Get-VM -Name "$VMNames" -Server $VMHost
}
} else {
$VMs = Get-VM -Server $VMHost
}
# only run through the list if there's anything in it
if ($VMs) {
foreach ($VM in @($VMs)) {
$listOfVmNames += $VM.ElementName + ', '
}
$listOfVmNames = $listOfVmNames -replace "..$"
Write-InfoDump -text 'Attempting to backup the following VMs:'
Write-InfoDump -text "$listOfVmNames"
Write-InfoDump -text ''
Write-Host -Object 'Do not cancel the export process as it may cause unpredictable VM behavior' -ForegroundColor Yellow
# For each VM on Node, Save (if necessary), Export and Restore (if necessary)
foreach ($VM in @($VMs)) {
$VMName = $VM.ElementName
$summofvm = get-vmsummary $VMName
$summhb = $summofvm.heartbeat
$summes = $summofvm.enabledstate
$restartWhenDone = $false
$doexport = 'no'
# Shutdown the VM if HeartBeat Service responds
if ($summhb -eq 'OK') {
$doexport = 'yes'
Write-InfoDump -text ''
Write-InfoDump -text "HeartBeat Service for $VMName is responding $summhb, saving the machine state"
$restartWhenDone = $true
Save-VM -VM $VMName -Server $VMHost -Force -Wait
} elseif (($summes -eq 'Stopped') -or ($summes -eq 'Suspended')) {
# Checks to see if the VM is already stopped
$doexport = 'yes'
Write-InfoDump -text ''
Write-InfoDump -text "$VMName is $summes, starting export"
} elseif ($summhb -ne 'OK' -and $summes -ne 'Stopped') {
# If the HeartBeat service is not OK, aborting this VM
$doexport = 'no'
Write-InfoDump -text
Write-InfoDump -text "HeartBeat Service for $VMName is responding $summhb. Save state and export aborted for $VMName"
}
$i = 1
if ($doexport -eq 'yes') {
$VMState = get-vmsummary $VMName
$VMEnabledState = $VMState.enabledstate
if ($VMEnabledState -eq 'Suspended' -or $VMEnabledState -eq 'Stopped') {
# If a folder already exists for the current VM, delete it.
if ([IO.Directory]::Exists("$($Destination)\$($VMName)")) {
[IO.Directory]::Delete("$($Destination)\$($VMName)", $true)
}
Write-InfoDump -text "Exporting $VMName"
# Begin export of the VM
export-vm -VM $VMName -Server $VMHost -path $Destination -CopyState -Wait -Force -ErrorAction SilentlyContinue
# check to ensure the export succeeded
$exportedCount = (Get-ChildItem $Destination -Force -Recurse).Count
# there should be way more than 5 elements in the destination - this is to account for empty folders
if ($exportedCount -lt 5) {
Write-InfoDump -text "***** Automated export failed for $VMName *****"
Write-InfoDump -text '***** Manual export advised *****'
}
if ($restartWhenDone) {
Write-InfoDump -text "Restarting $VMName..."
# Start the VM and wait for a Heartbeat with a 5 minute time-out
Start-VM $VMName -HeartBeatTimeOut 300 -Wait
}
} else {
Write-InfoDump -text "Could not properly save state on VM $VMName, skipping this one and moving on."
}
}
}
} else {
Write-InfoDump -text 'No VMs found to back up.'
}
}
}
function Write-InfoDump {
<#
.SYNOPSIS
This is just a hand-made wrapper function that mimics the Tee-Object cmdlet with less fuss
.DESCRIPTION
This is just a hand-made wrapper function that mimics the Tee-Object cmdlet with less fuss
Plus, it makes our log file prettier
.PARAMETER text
Text to dump
.NOTES
Additional information about the function.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param
(
[System.Single]$text
)
process {
Write-Host -Object "$text"
$now = date
$text = "$now`t: $text"
Add-Content -LiteralPath $Logfile -Value $text
}
}