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/

Reply via email to