Minor update that cleans up the code a little and also adds the server name to the shadow timestamp so that you can see which backuppc host has created the shadows. This is useful in case you have multiple backuppc servers backing up a single client (I do this so that I have totally redundant backups in case one backuppc server fails)
------------------------------------------------------------------------- ################################################################################ ################################################################################ ##### Create/delete Windows Volume Snapshot Service (VSS) snapshots and adjust ##### $Conf{ShareName2Path} accordingly ##### Version 0.6.1 (Feb 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 copy reparse-points my $shadowdir = "${cygdrive}/c/shadow"; #LOCATION for shadow copies $shadowdir =~ s|/*$|/|; #Ensure that ends in slash #Format of host timestamp (allows for mulitple simultaneous backups of same host from different BackupPC servers) chomp(my $hostname = `hostname -s`); chomp(my $hosttimestamp = `date +"%Y%m%d.%H%M%S-$hostname"`); #Number of days after which to expire old/orphaned shadows my $DAYS=2; #Note a shadow copy will be created (and pointed to) for every 'letter' drive referenced in the values of the ClientShareName2Path hash #In particular, if you don't set up ClientShareName2Path then no shadows will be created #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: 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-$hosttimestamp" 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 = " DAYS=$DAYS\n"; $bash_deleteoldshadows .= <<'EOF'; 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\}\)-$hostname$/\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 'hosttimestamp' based on entries in $shadowdir my $bash_unwindshadow = <<'EOF'; for II in $(\ls -d ${shadowdir}*-${hosttimestamp} 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'; #Trap on error: unwind shadows and exit 1. function errortrap { 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'; #Note: perl code returns the exit code times 256 in $? (in this case: 256 * 1) exit 1 } trap errortrap ERR EOF sub escape_bash { #Escape the bash scripts $_ = shift; s/^\s*#.*//mg; #Delete comments that start at beginning of line s/^\n//msg; #Delete blank lines s/([][;&()<>{}|^\n\r\t *\$\\'"`?])/\\$1/g; #Escape key characters s/\\\$(\\(\{))?([a-z][a-z0-9_]*)(\\(\}))?/\$$2$3$5/g; #We want to evaluate and pass all-lowercase variables, so unescape them; return $_; }; $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 \$hosttimestamp = "$hosttimestamp"; my \$hostname = "$hostname"; my \$shadowdir = "$shadowdir"; my \$shadows = "$shadows"; my \$bashscript = "\n$bash_prescript"; $sshrunbashscript; my \$sharenameref=\$bpc->{Conf}{ClientShareName2Path}; foreach my \$key (keys %{\$sharenameref}) { #Rewrite ClientShareName2Path \$sharenameref->{\$key} = "\$shadowdir\$2-\$hosttimestamp\$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 \$hosttimestamp = "$hosttimestamp"; #Initialize variables my \$shadowdir = "$shadowdir"; my \$shadows = "$shadows"; my \$bashscript = "\n$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/