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/

Reply via email to