[CmdletBinding()] Param ( [Parameter()] [String]$ManageAlchemyPath = "G:\Virtual Machines\Recovery Store\Manage-Alchemy.ps1", [Parameter()] [String]$VshadowPath = "G:\Virtual Machines\Recovery Store\vshadow.exe", [Parameter()] [String]$SourceDrive = "G:", [Parameter()] [String]$ShadowDrive = "S:", [Parameter()] [String]$ShadowPath = "$ShadowDrive\Virtual Machines\" ) ############################################################################### # # BackupVMs.ps1 - alchemy.nodomain.net VM backup script # # Copyright 2017-2020 - Josh Moyer , all rights reserved. # ############################################################################### # To do: # Support automated restartability - i.e. without having to manually destroy the shadow and offline the drive # Known Issues: # - If VM config references files in a different directory than the config then the files might get skipped # - No verbose logging # - C style error handling is not as modern as PowerShell can be # - If the backup drive is full the script will never complete, as robocopy keeps retrying the failed copy. # Test cases: # - Destination path does/does not exist # - Disk failure/removal # - Disk full # - Disk offline # - Disk online # - Disk read only # - Disk subsystem configuration changes # - Multiple backup disks connected # - Multiple script instances started Begin { Function Send-Alert($Subject, $Body) { $Message = [System.Web.Mail.MailMessage]::new() $Message.To = "JMoyer@NODOMAIN.NET" $Message.To = "4258213371@tmomail.net" $Message.From = "$($Env:COMPUTERNAME).NODOMAIN.NET" $Message.Subject = $Subject $Message.Body = $Body [System.Web.Mail.SmtpMail]::SmtpServer = '172.23.71.1' [System.Web.Mail.SmtpMail]::Send($Message) } Function ConcatStringCollection ($Object) { For ($I = 0 ; $I -LT $Object.Length ; $I++) { $Output = -Join ($Output, $Object[$I], "`n") } Return $Output } # -5: failed to copy at least one file Function CopyVMs ($BackupPath) { If (!(Test-Path $BackupPath)) {Return -5} $BackupItems = Get-ChildItem $BackupPath ForEach ($BackupItem In $BackupItems) { If (Test-Path $ShadowPath$BackupItem) { #If vm.length -LT (freespace + vm.backup.length) #Requires -RunAsAdministrator RoboCopy $ShadowPath$BackupItem $BackupPath$BackupItem /COPYALL /B /J /MIR /SL /DCOPY:DAT /NP #| Out-Null #If ($Notify -EQ $True) { Notify vm.specification } } } } # -6: failed to create or expose shadow Function CreateShadow { #Requires -RunAsAdministrator $Output = & $VshadowPath "-p" "-nw" "$($SourceDrive)" $Output = ConcatStringCollection($Output) #If ($Output -EQ $Null) #{ # Write-Output "VC Redist installed?" # Return -61 #} Write-Debug $Output $ShadowCopySet = ($Output | Select-String "Shadow copy Set: ({[a-f0-9-]*})").Matches.Groups[1].Value $SnapshotId = ($Output | Select-String "SNAPSHOT ID = ({[a-f0-9-]*})").Matches.Groups[1].Value If (($ShadowCopySet -EQ $Null -OR $SnapshotId -EQ $Null)) { Return -6 } If (Test-Path $ShadowDrive) { Return -6 } #Requires -RunAsAdministrator $Output = & $VshadowPath "-el=`"$($SnapshotId),$($ShadowDrive)`"" $Output = ConcatStringCollection($Output) Write-Debug $Output If (!(Test-Path $ShadowDrive)) { Return -6 } Return $ShadowCopySet } # -7: unable to destroy shadow Function DestroyShadow($ShadowCopySet) { #Requires -RunAsAdministrator $Output = & $VshadowPath "-dx=`"$ShadowCopySet`"" If (Test-Path $ShadowDrive) { Return -7 } } # -4: offline disks was not equal to 1 Function OnlineDrive { $Drives = "list disk" | DISKPART | Where {$_ -Match "Disk ([0-9]+)\s+Offline"} If ($Matches.Count -NE 2) {Return -4} $Drive = $Matches[1] "select disk $Drive`nattributes disk clear readonly`nonline disk" | DISKPART | Out-Null $Drive } # -3: failed to offline backup drive Function OfflineDrive ($Drive) { "select disk $Drive`noffline disk" | DISKPART | Out-Null } # -2: backup drive signature not found Function ReturnDrive { $IDFile = "vmbackupdrive.id" $Drives = Get-WmiObject -Query "SELECT * from win32_logicaldisk WHERE DriveType = 3” $Drives | ForEach-Object -Process { $Drive = $_.DeviceID $Candidate = Get-ChildItem "$Drive\$IDFile" -ErrorAction Ignore If ($Candidate) {Return $Drive} } If ($Drive -LT 0) {Return -2} } # -1: failure in main Function Main { $DebugPreference = "Continue" $ErrorActionPreference = "Stop" Date Echo "Suspending VMs." & $ManageAlchemyPath -Command Suspend-VMs Date Echo "Creating shadow copy." $ShadowSet = CreateShadow If ($ShadowSet.Length -NE 38) {Return $ShadowSet} Date Echo "Resuming VMs." & $ManageAlchemyPath -Command Start-VMs Date Echo "Bringing backup drive online." $Drive = OnlineDrive If ($Drive -LT 0) {Return $Drive} Date Echo "Identifying backup drive." $BackupDrive = ReturnDrive If ($BackupDrive -LT 0) {Return $BackupDrive} Date Echo "Copying items." CopyVMs $BackupDrive Date Echo "Destroying shadow copy." DestroyShadow $ShadowSet Date Echo "Taking backup drive offline." OfflineDrive $Drive Date } } Process { Main } End {}