Just posting an updated version of my perl hacks to the /etc/backuppc/pc/windowshost.pl file that will allow you to easily and reliably create (and later remove) VSS shadow copies on any Windows host so that all files (including locked ones) can be backed up.
This approach in my mind is infinitely better than others proposed on this list for the following reasons: 1. No separate client-side configuration is necessary 2. No separate programs (or even scripts) are needed on either the host or the client for sideband communication to setup/takedown shadows on the remote host. Specifically, there is no need for Winexe advocated in the past on this board 3. In particular, on the Windows host all you need is a basic cygwin-like installation (commands needed include bash shell functionality, grep, awk, sed, tail, cygpath, date) plus basic VSS and junction commands that are typically part of Windows (e.g., wmic, vssadmin, fsutil, mklink) -- really just a vanilla Windows 7+ installation plus a core cygwin installation. 3. All the code is self-included in the backuppc host config file so there is no need to maintain separate scripts or programs. 4. In general, shadow copies are automagically set up and taken down with little if any custom configuration in the backuppc config file. 5. It's even possible to do multiple backups simultaneously using distinct, time-stamped VSS shadows.I even just added the ability to delete "old" stale shadow copies that can persist if the connection between server and remote host breaks before the post dump command can run. While the code below is not of commercial quality and may require some tweaking for your specific situation, it is heavily documented and should be easily adaptable. Basically, my approach is based on the fact that the entire backuppc config file is executed as a perl script. In particular DumpPreUserCmd and DumpPostUserCmd can take as input perl code and not just a single bash commands. Then I can use perl to launch ssh processes on the remote Windows host to set up, manage, and takedown VSS shadows. Just add the below to the end of your windows host config file. It's all a hack... but a very useful and powerful one :) ################################################################################ ################################################################################ ##### Create/delete Windows Volume Snapshot Service (VSS) snapshots and adjust ##### $Conf{ShareName2Path} accordingly ##### Version 0.6 (Jan 2021) ##### Jeff Kosowsky # COPYRIGHT # Copyright (C) 2020-2021 Jeff Kosowsky # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. ################################################################################ #### VARIABLES #Login name for windows host (use RsyncdUserName or if empty, defaults to BackupPCUser) $User = $Conf{RsyncdUserName} or $Conf{BackupPCUser}; #Use RsyncUserName as an alternative login user for Windows if set #Option to specify alternative ssh identity file for access to windows host $SshID = `echo -n ~$Conf{BackupPCUser}` . "/.ssh/id_rsa"; $SshID = '-i ' . $SshID if defined $SshID;; #Cygwin base: my $cygdrive = "/cygdrive"; $cygdrive =~ s|/*$||; #Ensure that doesn't end in slash #Location for storing shadow copies reparse-points my $shadowdir = "${cygdrive}/c/shadow"; #LOCATION for shadow copies $shadowdir =~ s|/*$|/|; #Ensure that ends in slash #Format of timestamp (allows for mulitple simultaneous backups of same host from different BackupPC servers) chomp(my $timestamp = `date +"%Y%m%d-%H%M%S"`); #Note a shadow copy will be created (and pointed to) for every 'letter' drive referenced in the values of the ClientShareName2Path hash #If you wish to skip some drive letters, add them to the following shadow exclude array; my @shadowexcludes = (); my %excludeshash = map { $_ => 1 } @shadowexcludes; #Pack shadow drive letters in string separated by spaces since can only pass in scalar context to $Conf{Dump...} my @pathvalues = values %{$Conf{ClientShareName2Path}}; #Get share paths s#^($cygdrive)?/([a-zA-Z])(/.*)?$|.*#$2# for @pathvalues; #Extract drive letters (note only match single drive letter [a-zA-Z]) #Remove empties, excluded shadows, duplicates & sort (this is nice perl foo) @pathvalues = sort(grep($_ ne '' && ! $excludeshash{$_} && !$hash{$_}++, @pathvalues )); my $shadows = join(' ', @pathvalues); #Concatenate into space separated list for passing & use by bash scripts #### Bash scripts to run on remote Windows client for Dump Pre/Post User Commands: #Load bash scripts into corresponding variables #NOTE: Put semicolon at the end of each line *EXCEPT* for final line #NOTE: Upper case variables are for bash evaluation on remote machine, # Lowercase are for perl evaluation locally on the BackupPC server # Distinction is used later to determine what variables to escape (i.e., don't escape lowercase variables) #BASH_CREATESHADOW: Create single shadow for drive letter: $I my $bash_createshadow = <<'EOF'; #Create shadow copy and capture shadow id { SHADOWID="$(wmic shadowcopy call create Volume=${I}:\\ | sed -ne 's|[ \t]*ShadowID = "\([^"]*\).*|\1|p')" ; } 2> >(tail +2); #Note: redirection removes extra new line from stderr #Get shadow GLOBALROOT path from shadow id SHADOWPATH="$(wmic shadowcopy | awk -v id=$SHADOWID 'id == $8 {print $3}')"; #Create reparse-point link in shadowdir (since GLOBALROOT paths not readable by cygwin or rsync) SHADOWLINK="$(cygpath -w ${shadowdir})$I-$timestamp"; cmd /c "mklink /j $SHADOWLINK $SHADOWPATH\\"; #Unset in case error before fully set again unset SHADOWID SHADOWPATH SHADOWLINK; EOF #BASH_LOOPCREATESHADOW group of shadows based on space-separated list of drive letters ($shadows) my $bash_loopcreateshadow = <<'EOF'; [ -n "$shadows" ] && mkdir -p $shadowdir; for I in $shadows; do if ! [ -d "$(cygpath -u ${I}:)" ] || ! grep -qE "^${I^^}: \S+ ntfs " <(mount -m); then echo "No such NTFS drive '${I}:' skipping corresponding shadow setup..."; continue; fi; EOF $bash_loopcreateshadow .= $bash_createshadow; $bash_loopcreateshadow .= <<'EOF'; done EOF #BASH_DELETESHADOW: Delete single shadow: II my $bash_deleteshadow = <<'EOF'; SHADOWLINK="$(cygpath -w $II)"; #Extract the drive letter DRIVE=${SHADOWLINK##*\\}; DRIVE=${DRIVE%%-*}; DRIVE="${DRIVE^^}:"; #Fsutil used to get the target reparse point which is the GLOBALROOT path of the shadow copy #NOTE: '\r' is used to remove trailing '^M' in output of fsutil SHADOWPATH=$(fsutil reparsepoint query $SHADOWLINK | sed -ne "s|^Print Name:[[:space:]]*\(.*\)\\\\\r|\1|p"); #Get the shadow id based on the shadowpath SHADOWID="$(wmic shadowcopy | awk -v path=${SHADOWPATH//\\/\\\\} 'path == $3 {print $8}')"; echo " Deleting shadow for '$DRIVE' PATH=$SHADOWPATH; ID=$SHADOWID; LINK=$SHADOWLINK"; #Delete the shadow copy (vssadmin delete shadows /shadow=$SHADOWID /quiet || echo " ERROR: Couldn't delete shadow copy for '$DRIVE': $SHADOWLINK") | tail +4; #Delete the reparse point (note cygwin rmdir won't work) cmd /c rmdir $SHADOWLINK || echo " ERROR: Couldn't delete link for '$DRIVE': $SHADOWLINK"; EOF #BASH_DELETEOLDSHADOWS: delete shadows older than 2 days (these are almost definitely stale) my $bash_deleteoldshadows = <<'EOF'; DAYS=2 AGE=$(( $(date +%s) - 86400 * $DAYS )) for II in $(\ls -d ${shadowdir}* 2>/dev/null); do DATE=$(echo $II | sed -ne "s/^[^-]\+-\([0-9]\{8\}\)-\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)$/\1 \2:\3:\4/p"); if [ -n "$DATE" -a $(date -d "$DATE" +%s) -lt $AGE ] ; then EOF $bash_deleteoldshadows .= $bash_deleteshadow; $bash_deleteoldshadows .= <<'EOF'; fi done EOF #BASH_UNWINDSHADOW: unwind/delete successful shadows with 'timestamp' based on entries in $shadowdir my $bash_unwindshadow = <<'EOF'; for II in $(\ls -d ${shadowdir}*-${timestamp} 2>/dev/null); do EOF $bash_unwindshadow .= $bash_deleteshadow; $bash_unwindshadow .= <<'EOF'; done EOF #BASH_TRAP: trap script to run to unwind shadows & return error code if error creating shadows my $bash_trap = <<'EOF'; function errortrap { #NOTE: Trap on error: unwind shadows and exit 1. echo "ERROR setting up shadows..."; #First delete any partially created shadows from current loop if [ -n "$SHADOWID" ]; then unset ERROR; (vssadmin delete shadows /shadow=$SHADOWID /quiet || ERROR="ERROR ") | tail +4; echo " ${ERROR}Deleting shadow copy for '${I^^}:' $SHADOWID"; fi if [ -n "$SHADOWLINK" ]; then unset ERROR; cmd /c rmdir $SHADOWLINK || ERROR="ERROR "; echo " ${ERROR}Deleting shadow link for '${I^^}:' $SHADOWLINK"; fi #Then loop through any previously created shadows/shadow-links to to delete them EOF $bash_trap .= $bash_unwindshadow; #Pull in unwind script to delete any previously created shadows $bash_trap .= <<'EOF'; exit 1; #Note: perl code returns the exit code times 256 in $? (in this case: 256) }; trap errortrap ERR; EOF sub escape_bash { #Escape the bash scripts my $string = shift; $string =~ s/([][;&()<>{}|^\n\r\t *\$\\'"`?])/\\$1/g; $string =~ s/\\\$(\\(\{))?([a-z][a-z0-9_]*)(\\(\}))?/\$$2$3$5/g; #Note we want to evaluate and pass lower case variables - so unescape them; return $string; }; $bash_prescript = &escape_bash($bash_deleteoldshadows . $bash_trap . $bash_loopcreateshadow); $bash_postscript = &escape_bash($bash_unwindshadow); my $sshrunbashscript = qq( #Run script \$bashscript on remote host via ssh open(my \$out_fh, "|-", "$Conf{SshPath} -q -x $SshID -l $User \$args[0]->{hostIP} bash -s") or warn "Can't start ssh: \$!"; print \$out_fh \"\$bashscript\"; close \$out_fh; # or warn "Error flushing/closing pipe to ssh: \$!"; ); #### DumpPreUserCmd $Conf{DumpPreUserCmd} = qq(&{sub { #Load variables my \$timestamp = "$timestamp"; my \$shadowdir = "$shadowdir"; my \$shadows = "$shadows"; my \$bashscript = "$bash_prescript"; $sshrunbashscript; my \$sharenameref=\$bpc->{Conf}{ClientShareName2Path}; foreach my \$key (keys %{\$sharenameref}) { #Rewrite ClientShareName2Path \$sharenameref->{\$key} = "\$shadowdir\$2-\$timestamp\$3" if \$sharenameref->{\$key} =~ m#^($cygdrive)?/([a-zA-Z])(/.*)?\$#; #Add shadow if letter drive } print map { " '\$_' => \$sharenameref->{\$_}\n" } sort(keys %{\$sharenameref}) unless \$?; }}); ##### DumpPostUserCmd $Conf{DumpPostUserCmd} = qq(&{sub { #Load variables my \$timestamp = "$timestamp"; #Initialize variables my \$shadowdir = "$shadowdir"; my \$shadows = "$shadows"; my \$bashscript = "$bash_postscript"; $sshrunbashscript; }}); ############################################################################### ############################################################################### _______________________________________________ BackupPC-users mailing list BackupPC-users@lists.sourceforge.net List: https://lists.sourceforge.net/lists/listinfo/backuppc-users Wiki: https://github.com/backuppc/backuppc/wiki Project: https://backuppc.github.io/backuppc/