Backing up your Hyper-V VM environment using PowerShell has never been easier! PowerShell allows you to backup your VMs, log the results and even email you a report with a few easy commands.
In this case, I have two physical hosts, VH-SERVER01 and VH-SERVER02. Each of them has quite a bit of HD space so for the purposes of recovery from a simple hardware failure, back up the VMs on one host to the other is a great, simple way to go.
I created shares on each server for the other to use as a repository. VH-SERVER0xexports. I map the Z: drive to this share for simplicity’s sake.
One caveat that I found is that a Domain Controller VM will not export to a SMB share, even when it is mapped with a drive letter. I researched this and found all sorts of suggestions about permissions related to compluter accounts and despite my efforts, I was unable to resolve the direct export. So, for the DCs, I export them locally and then move the files to the same share where the other exports are sent.
After all the exports are done, I call the script Get-DirStats.ps1 to calculate the sizes of the exported files. I output the results to a file. The script code for that is located below the main script. I then use the great command, Send-MailMessage to email me the outputted results! Note that you need to have an SMTP server that will allow you to route email. Since this is a corporate client, I have an Exchange Receive Connector that will allow me to send SMTP traffic without authentication.
Some helpful links:
technet.microsoft.com/en-us/library/hh848491.aspx
technet.microsoft.com/en-us/library/hh849925.aspx
stackoverflow.com/questions/25917637/create-folder-with-current-date-as-name-in-powershell
blogs.technet.microsoft.com/heyscriptingguy/2012/05/25/getting-directory-sizes-in-powershell/
The script code!
#deletes and adds Z: as a persistent drive since this script needs to run whether someone is logged in or not
Get-PSDrive Z | Remove-PSDrive
New-PSDrive -Name “Z” -PSProvider “FileSystem” -Root “\VH-SERVER01exports” -Persist
Remove-Item D:ExpADSERVER02 -Recurse
New-Item -ItemType directory -Path “D:ExpADSERVER02”
#define export paths
$ExportPath_D = “D:ExpADSERVER02”
$ExportPath_Z = “Z:”
$date = Get-Date
$date = $date.ToString(“yyyy-MM-dd”)
#Deletes files-folders that are older than 20 days
$limit = (Get-Date).AddDays(-20)
$path = $ExportPath_Z
# Delete files older than the $limit.
Get-ChildItem -Path $path -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $limit } | Remove-Item -Force
# Delete any empty directories left behind after deleting the old files.
Get-ChildItem -Path $path -Recurse -Force | Where-Object { $_.PSIsContainer -and (Get-ChildItem -Path $_.FullName -Recurse -Force | Where-Object { !$_.PSIsContainer }) -eq $null } | Remove-Item -Force -Recurse
New-Item -ItemType directory -Path “$ExportPath_Z$date”
#Exports are here
Export-VM -Name “ADSERVER02” -Path $ExportPath_D
Move-item $($ExportPath_D) Z:$($date)
Export-VM -Name “SERVER03″,”SERVER04″,”SERVER05″,”SERVER06” -Path $ExportPath_Z$date
#Report creation and email
c:scriptsGet-DirStats.ps1 -Path Z:$($date) -Every >C:Backup-LogsVH-SERVER02-$date.csv
Send-MailMessage -From “Backups <backups@YOURDOMAIN.com>” -To “Alerts <alerts@YOURDOMAIN.com>” -Subject “VH-SERVER02 Backup Status for $date” -Body “This is the VM backup report for VH-SERVER02.” -Attachments “c:backup-logsVH-SERVER02-$date.csv” -Priority High -dno onSuccess, onFailure -SmtpServer “MAIL.YOURDOMAIN.COM”
exit
.SYNOPSIS
Outputs file system directory statistics..DESCRIPTION
Outputs file system directory statistics (number of files and the sum of all file sizes) for one or more directories.
.PARAMETER Path
Specifies a path to one or more file system directories. Wildcards are not permitted. The default path is the current directory (.).
.PARAMETER LiteralPath
Specifies a path to one or more file system directories. Unlike Path, the value of LiteralPath is used exactly as it is typed.
.PARAMETER Only
Outputs statistics for a directory but not any of its subdirectories.
.PARAMETER Every
Outputs statistics for every directory in the specified path instead of only the first level of directories.
.PARAMETER FormatNumbers
Formats numbers in the output object to include thousands separators.
.PARAMETER Total
Outputs a summary object after all other output that sums all statistics.
#>
[CmdletBinding(DefaultParameterSetName=”Path”)]
param(
[parameter(Position=0,Mandatory=$false,ParameterSetName=”Path”,ValueFromPipeline=$true)]
$Path=(get-location).Path,
[parameter(Position=0,Mandatory=$true,ParameterSetName=”LiteralPath”)]
[String[]] $LiteralPath,
[Switch] $Only,
[Switch] $Every,
[Switch] $FormatNumbers,
[Switch] $Total
)
begin {
$ParamSetName = $PSCmdlet.ParameterSetName
if ( $ParamSetName -eq “Path” ) {
$PipelineInput = ( -not $PSBoundParameters.ContainsKey(“Path”) ) -and ( -not $Path )
}
elseif ( $ParamSetName -eq “LiteralPath” ) {
$PipelineInput = $false
}
# Script-level variables used with -Total.
[UInt64] $script:totalcount = 0
[UInt64] $script:totalbytes = 0
# Returns a [System.IO.DirectoryInfo] object if it exists.
function Get-Directory {
param( $item )
if ( $ParamSetName -eq “Path” ) {
if ( Test-Path -Path $item -PathType Container ) {
$item = Get-Item -Path $item -Force
}
}
elseif ( $ParamSetName -eq “LiteralPath” ) {
if ( Test-Path -LiteralPath $item -PathType Container ) {
$item = Get-Item -LiteralPath $item -Force
}
}
if ( $item -and ($item -is [System.IO.DirectoryInfo]) ) {
return $item
}
}
# Filter that outputs the custom object with formatted numbers.
function Format-Output {
process {
$_ | Select-Object Path,
@{Name=”Files”; Expression={“{0:N0}” -f $_.Files}},
@{Name=”Size”; Expression={“{0:N0}” -f $_.Size}}
}
}
# Outputs directory statistics for the specified directory. With -recurse,
# the function includes files in all subdirectories of the specified
# directory. With -format, numbers in the output objects are formatted with
# the Format-Output filter.
function Get-DirectoryStats {
param( $directory, $recurse, $format )
Write-Progress -Activity “Get-DirStats.ps1” -Status “Reading ‘$($directory.FullName)'”
$files = $directory | Get-ChildItem -Force -Recurse:$recurse | Where-Object { -not $_.PSIsContainer }
if ( $files ) {
Write-Progress -Activity “Get-DirStats.ps1” -Status “Calculating ‘$($directory.FullName)'”
$output = $files | Measure-Object -Sum -Property Length | Select-Object `
@{Name=”Path”; Expression={$directory.FullName}},
@{Name=”Files”; Expression={$_.Count; $script:totalcount += $_.Count}},
@{Name=”Size”; Expression={$_.Sum; $script:totalbytes += $_.Sum}}
}
else {
$output = “” | Select-Object `
@{Name=”Path”; Expression={$directory.FullName}},
@{Name=”Files”; Expression={0}},
@{Name=”Size”; Expression={0}}
}
if ( -not $format ) { $output } else { $output | Format-Output }
}
}
process {
# Get the item to process, no matter whether the input comes from the
# pipeline or not.
if ( $PipelineInput ) {
$item = $_
}
else {
if ( $ParamSetName -eq “Path” ) {
$item = $Path
}
elseif ( $ParamSetName -eq “LiteralPath” ) {
$item = $LiteralPath
}
}
# Write an error if the item is not a directory in the file system.
$directory = Get-Directory -item $item
if ( -not $directory ) {
Write-Error -Message “Path ‘$item’ is not a directory in the file system.” -Category InvalidType
return
}
# Get the statistics for the first-level directory.
Get-DirectoryStats -directory $directory -recurse:$false -format:$FormatNumbers
# -Only means no further processing past the first-level directory.
if ( $Only ) { return }
# Get the subdirectories of the first-level directory and get the statistics
# for each of them.
$directory | Get-ChildItem -Force -Recurse:$Every |
Where-Object { $_.PSIsContainer } | ForEach-Object {
Get-DirectoryStats -directory $_ -recurse:(-not $Every) -format:$FormatNumbers
}
}
end {
# If -Total specified, output summary object.
if ( $Total ) {
$output = “” | Select-Object `
@{Name=”Path”; Expression={“<Total>”}},
@{Name=”Files”; Expression={$script:totalcount}},
@{Name=”Size”; Expression={$script:totalbytes}}
if ( -not $FormatNumbers ) { $output } else { $output | Format-Output }
}
}