# encoding: ascii
# api: powershell
# title: Ashwin
# description: Some of commands get outdated and needs to get updated with latest powershell upgrade
# version: 0.1
# type: script
# author: Ashwin Rayaprolu 
# license: CC0
# function: Backup-VMs
# x-poshcode-id: 6496
# x-archived: 2016-09-03T23:43:32
# x-published: 2016-08-31T01:48:00
#
# Added condition not to export VM which is already in progress of getting exported
# Remove Shutdown/Stop VM call as that is unnecessary in most cases
#
# 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
# This is modified version updated to work in win 2012 Datacenter environment 
# ----------------------------------------------------------------------------
# Note that this script requires administrator privileges for proper execution
# ----------------------------------------------------------------------------
# Author: Ashwin Rayaprolu (Ashwin.rayaprolu@gmail.com)
# Note that this script requires the following:
# Original version is at http://poshcode.org/3927 and this is plagiarized version
# 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
{
        [CmdletBinding(SupportsShouldProcess=$True)]
        Param(
                [parameter(Mandatory = $true)]
        [string]$BackupDriveLetter,                     # $BackupDriveLetter:\VMBackups\$backupDate
               
                [ValidateNotNullOrEmpty()]
                [string]$VMHost,                                        # the host that holds the vms we wish to back up, otherwise the one running the script
                [string[]]$VMNames,                                     # if not specified, back up all of them
                [switch]$ShutHostDownWhenFinished       # when set, shuts down the target host, including any vms on it
        )
        process
        {
                # first, run a bunch of checks
                #region checks
                # check if the PowerShellPack modules are loaded. Not required
               	Write-Host -ForegroundColor Red "Processsing........"

               
                # 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 "$($BackupDriveLetter):") -eq $false)
                {
                        Write-Host -ForegroundColor Red "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
                }
                # check to make sure the specified host is a vmhost
                if (!(Get-VMHost) -icontains $VMHost)
                {
                        Write-Host -ForegroundColor Red "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 -ComputerName $VMHost))
                {
                        Write-Host -ForegroundColor Red "Host $($VMHost) does not appear to have any VMs running on it according to 'Get-VM -Server $($VMHost)'."
                        Write-Host -ForegroundColor Yellow "This can be occur if PowerShell is not running with elevated privileges."
                        Write-Host -ForegroundColor Yellow "Please make sure that you are running PowerShell with Administrator privileges and try again."
                        Write-Host -ForegroundColor Red "Terminating backup script."
                        Break
                }
                #endregion
               
                #region directory business
                # make our parent directory if needed
                if ((Test-Path "$($BackupDriveLetter):\VMBackup") -eq $false)
                {
                        $parentDir = New-Item -Path "$($BackupDriveLetter):\VMBackup" -ItemType "directory"
                        if ((Test-Path $parentDir) -eq $false)
                        {
                                Write-Host -ForegroundColor Red "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 "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 "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
                T -text "Starting Hyper-V virtual machine backup for host $VMHost at:"
                $dateTimeStart = date
                T -text "$($dateTimeStart)"
                T -text ""
               
                # export the vms to the destination
                ExportMyVms -VMHost $VMHost -Destination $destDir -VMNames $VMNames
               
                T -text ""
                T -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
                        $backupDrive = Get-WmiObject win32_logicaldisk -ComputerName $hostname | Where-Object { $_.DeviceID -eq "$($BackupDriveLetter):" }
                        $backupDriveFreeSpace = ($backupDrive.FreeSpace / 1GB)
                       
                        # tell the user what we've found
                        $formattedBackupDriveFreeSpace = "{0:N2}" -f $backupDriveFreeSpace
                        $formattedSourceDirSize = "{0:N2}" -f $sourceDirSize
                        T -text "Checking free space for compression:"
                        T -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)
                                {
                                        T -text "Removing the oldest backup [$($backups[0])] to clear up some more room"
                                        Remove-Item "$($BackupDriveLetter):\VMBackup\$($backups[0])" -Recurse -Force
                                        # now check again
                                        $backupDrive = Get-WmiObject win32_logicaldisk -ComputerName $hostname | Where-Object { $_.DeviceID -eq "$($BackupDriveLetter):" }
                                        $backupDriveFreeSpace = ($backupDrive.FreeSpace / 1GB)
                                        $formattedBackupDriveFreeSpace = "{0:N2}" -f $backupDriveFreeSpace
                                        T -text "Now we have $formattedBackupDriveFreeSpace GB of room"
                                }
                                else
                                {
                                        # we're down to just one backup left, don't delete it!
                                        $downToOne = $true
                                }
                        }
                        T -text "Compressing the backup..directory $destDir VMHost $VMHost."
                        # zip up everything we just did
                        ZipFolder -directory $destDir -VMHost $VMHost
                       
                        $zipFileName = $destDir -replace ".$"
                        $zipFileName = $zipFileName + ".zip"
                       
                        T -text "Backup [$($zipFileName)] created successfully"
                        $destZipFileSize = (Get-ChildItem $zipFileName).Length / 1GB
                        $formattedDestSize = "{0:N2}" -f $destZipFileSize
                        T -text "Uncompressed size:`t$formattedSourceDirSize GB"
                        T -text "Compressed size:  `t$formattedDestSize GB"
                }
                #endregion
                                       
                # delete the non-compressed directory, leaving just the compressed one
                Remove-Item $destDir -Recurse -Force
               
                T -text ""
                T -text "Finished backup of $VMHost at:"
                $dateTimeEnd = date
                T -text "$($dateTimeEnd)"
                $length = ($dateTimeEnd - $dateTimeStart).TotalMinutes
                $length = "{0:N2}" -f $length
                T -text "The operation took $length minutes"
               
                if ($ShutHostDownWhenFinished -eq $true)
                {		
                		#No need to shutdown any VM or host
                        T -text "Attempting to shut down host machine $VMHost"
                        #ShutdownTheHost -HostToShutDown $VMHost
                }
        }
}
 
## this function will shut down any vms running on the host executing this script and then shut down said host
Function ShutdownTheHost
{
        [CmdletBinding(SupportsShouldProcess=$True)]
        Param(
        [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")
                                {
                                        T -text ""
                                        T -text "HeartBeat Service for $VMName is responding $summhb, saving the machine state"
                                       
                                        Save-VM -VM $VMName -Server $VMHost -Force -Wait
                                }
                                ## Checks to see if the VM is already stopped
                                elseif (($summes -eq "Stopped") -or ($summes -eq "Suspended"))
                                {
                                        T -text ""
                                        T -text "$VMName is $summes"
                                }
                               
                                ## If the HeartBeat service is not OK, aborting this VM
                                elseif ($summhb -ne "OK" -and $summes -ne "Stopped")
                                {
                                        T -text
                                        T -text "HeartBeat Service for $VMName is responding $summhb. Aborting save state."
                                }
                        }
                        T -text "All VMs on $HostToShutDown shut down or suspended."
                }
                T -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 IsFileLocked(
    [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 WaitForZipOperationToFinish(
    [__ComObject] $zipFile,
    [int] $expectedNumberOfItemsInZipFile)
{
    T -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 = IsFileLocked($zipFile.Self.Path)
    while($isFileLocked)
    {
        Write-Host -NoNewLine "."
        Start-Sleep -Seconds 5
       
        $isFileLocked = IsFileLocked($zipFile.Self.Path)
    }
   
    T -text ""    
}
 
function ZipFolder(
    [IO.DirectoryInfo] $directory)
{    
        $backupFullName = $directory.FullName
       
    T -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 ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
       
    $shellApp = New-Object -ComObject Shell.Application
    $zipFile = $shellApp.NameSpace($zipFileName)
 
    If ($zipFile -eq $null)
    {
        T -text "Failed to get zip file object."
    }
   
    [int] $expectedCount = (Get-ChildItem $directory -Force -Recurse).Count
    $expectedCount += 1 # account for the top-level folder
   
        T -text "Copying $expectedCount items into file $zipFileName..."
       
    $zipFile.CopyHere($directory.FullName)
 
    # wait for CopyHere operation to complete
    WaitForZipOperationToFinish $zipFile $expectedCount
   
    T -text "Successfully created zip file for folder ($backupFullName)."
}




$lStr_GetSummaryInfoError = "Get Summary info error : {0}"


Function Get-VMSummary
{# .ExternalHelp  MAML-VM.XML
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
           [parameter(ValueFromPipeline = $true)]
           $VM="%" ,
           
           [ValidateNotNullOrEmpty()]
           $Server="."   #May need to look for VM(s) on Multiple servers
         )         
    Process {
        if ($VM -is [String]) {$VM=(Get-VM -Name $VM -ComputerName $Server) }
        if ($VM.count -gt 1 ) {[Void]$PSBoundParameters.Remove("VM") ;  $VM | ForEach-object {Get-VMSummary -VM $_  @PSBoundParameters}}
        if ($vm.__CLASS -eq 'Msvm_ComputerSystem') { 
            $VSMgtSvc=Get-WmiObject -computerName $Vm.__Server -NameSpace $HyperVNamespace  -Class "MsVM_virtualSystemManagementService" 
            $result=$VSMgtSvc.GetSummaryInformation( @( (Get-VMSettingData $vm ).__Path ) ,  @(0,1,2,3,4,100,101,102,103,104,105,106,107,108))
            if ($Result.ReturnValue -eq 0) {$result.SummaryInformation | foreach-object {
                 New-Object PSObject -Property @{      
                   "Host"             =  $VM.__server                   ;  "VMElementName"    =  $_.elementname
                   "Name"             =  $_.name                        ;  "CreationTime"     =  $_.CreationTime
                   "EnabledState"     =  ([vmstate]($_.EnabledState))   ;  "Notes"            =  $_.Notes
                   "CPUCount"         =  $_.NumberOfProcessors          ;  "CPULoad"          =  $_.ProcessorLoad
                   "CPULoadHistory"   =  $_.ProcessorLoadHistory        ;  "MemoryUsage"      =  $_.MemoryUsage
                   "GuestOS"          =  $_.GuestOperatingSystem        ;  "Snapshots"        =  $_.Snapshots.count
                   "Jobs"             =  $_.AsynchronousTasks           ;  "Uptime"           =  $_.UpTime
                   "UptimeFormatted"  =  $(if ($_.uptime -gt 0) {([datetime]0).addmilliseconds($_.UpTime).tostring("hh:mm:ss")} else {0} )
                   "Heartbeat"        =  $(if ($_.heartBeat) { [HeartBeatICStatus] $_.Heartbeat} else {$null} )
                   "FQDN"             =  ((get-vmkvp -vm $vm).FullyQualifiedDomainName)
        	       "IpAddress"        =  ((Ping-VM $vm).NetworkAddress)}
            }}
            else  {write-Warning ($lStr_GetSummaryInfo  -f [ReturnCode]$result.returnValue )}
        }
    }
}



 
## 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 ExportMyVms
{
        [CmdletBinding(SupportsShouldProcess=$True)]
        Param(
        [string]$Destination,
                [string[]]$VMNames,
                [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
                #T -text "VM Names passed :$VMNames"
                if ($VMNames)
                {
                        if (($VMNames.Length) -gt 1)
                        {
                                # pass in a multiple-element string array directly
                                $VMs = Get-VM -Name $VMNames -ComputerName $VMHost
                        }
                        else
                        {
                                # turn a single-element string array back into a string
                                $VMNames = [string]$VMNames
                                #T -text "VM Names Resolving :$VMNames"
                                $VMs = Get-VM -Name "$VMNames" -ComputerName $VMHost
                        }
                }
                else
                {
                        $VMs = Get-VM -ComputerName $VMHost
                }
               
                ## only run through the list if there's anything in it
                if ($VMs)
                {
                        foreach ($VM in @($VMs))
                        {
                                $listOfVmNames += $VM.Name + ", "
                        }
                        $listOfVmNames = $listOfVmNames -replace "..$"
                        T -text "Attempting to backup the following VMs:"
                        T -text "$listOfVmNames"
                        T -text ""
                        Write-Host "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.Name
                                $summofvm = get-vm $VMName 
                                $restartWhenDone = $false
                               
                                $doexport = "yes"
                               
                               
                                $i = 1
                                if ($doexport -eq "yes")
                                {
                                        $VMState = get-vm $VMName
                                        #$VMEnabledState = $VMState.State
                                        $VMExportingStatus = $VMState.Status
                                       
                                        T -text "Exporting $VMName with state :$VMExportingStatus"

                                        # Don't trigger export on already exporting VM's
                                        if($VMExportingStatus -ne "Exporting virtual machine")
                                        {
                                                ## If a folder already exists for the current VM, delete it.
                                                if ([IO.Directory]::Exists("$($Destination)\$($VMName)"))
                                                {
                                                        [IO.Directory]::Delete("$($Destination)\$($VMName)", $True)
                                                }
                                                T -text "Exporting $VMName"
                                               
                                                ## Begin export of the VM
                                                export-vm $VMName -ComputerName $VMHost -path $Destination 
                                               
                                                ## 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)
                                                {
    	                                           	 T -text "***** Automated export failed for $VMName *****"
	                                                 T -text "***** Manual export advised *****"
                                                }
                                               
                                        }
                                        else
                                        {
                                                T -text "Could not properly save state on VM $VMName, skipping this one and moving on."
                                        }
                                }
                        }
                }
                else
                {
                        T -text "No VMs found to back up."
                }
        }
}
 
## This is just a hand-made wrapper function that mimics the Tee-Object cmdlet with less fuss
## Plus, it makes our log file prettier
Function T
{
        [CmdletBinding(SupportsShouldProcess=$True)]
        Param(
        [string]$text
        )
        process
        {
                Write-Host "$text"
                $now = date
                $text = "$now`t: $text"
                Add-content -LiteralPath $Logfile -value $text
        }
}


#Get-VMSummary -VM DEVELOPERIIS localhost
Backup-VMs -BackupDriveLetter Y -VMHost localhost  -VMNames DEVELOPERIIS
