As a follow-up and complement to the scripts I have previously posted to create/delete VSS Shadow backups for Windows, here is a similar script that can be used for BTRFS snapshots on Linux.
The script can be extended without much difficulty to other *nix filesystems with snapshot capability. - Once again, all is done in the host config file (e.g., /etc/backuppc/pc/<host>.pl) using the somewhat obscure (but officially supported) ability to substitute perl code for a single shell command in the variables $Conf{DumpPreUsrCmd} and $Conf{DumpPostUsrCmd}. - Again, no external functions or scripts are required besides basic installation of 'ssh', 'sudo' and btrs utilities - As discussed on a recent thread, I use '/etc/sudoers' to allow restricted sudo access to create/delete btrfs subvolumes. Specifically, if backing up the backuppc server itself, I just run 'sudo' (without 'ssh') and create the required privileges for the 'backuppc' user itself. When backing up a remote client, I create a special user on the remote machine (RemoteUser) who has those same sudo privileges and who allows ssh from the backuppc user on the backuppc server. - The variable '$islocal' intelligently tries to determine if the backup is local or not. - To hashes are used to make sure the right snapshots are created: %snapshots - which maps share names to the root mount of their corresponding btrfs subvolumes %snappaths - which allows for optional relative paths relative to the btrfs subvolume root for each corresponding share - A parameter 'Days' can also be set to cleanup old btrfs snapshots that haven't been deleted (this mostly occurs if the connection to the remote machine is lost during backup preventing the running of $Conf{DumpPostUsrCmd}. Here is the latest code: -------------------------------------------------------------------- ################################################################################ ##### Create/delete BTRFS subvolume snapshots and adjust ##### $Conf{ShareName2Path} accordingly ##### Version 0.6 (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/>. if(%snapshots) { #Create/delete btrfs snapshots and adjust $Conf{ShareName2Path} accordingly #Note: '%snapshots' is a hash of btrfs volumes to snapshot of form: # my %snapshots = ( # 'sharename_1' => '<path-to-corresponding-btrfs-volume-root>', # 'sharename_2' => '<path-to-corresponding-btrfs-volume-root>', # ... # ); #Note: '%snappaths' is a hash of paths relative to each btrfs snapshot root # my %snappaths = ( # 'sharename_1' => '<relative-path-to-share-base-from-volume-root>', # 'sharename_2' => '<relative-path-to-share-base-from-volume-root>', # ... # ); #Note: Set 'islocal' if you are backing up the local host and don't need to ssh to a remote machine my $islocal = (($Conf{ClientNameAlias} ne '' && `hostname -s` =~ /^$Conf{ClientNameAlias}$/) || `hostname -s` =~ /^$_[1]$/) ? 1 : 0; #Login name for remote host (needs to have permissions to create/delete btrfs subvolumes $RemoteUser = 'backuppc-client'; #Create a restricted user with limited sudo permissions #Note: permission to create/delete btrfs snapshots is added to /etc/sudoers: #backuppc,<RemoteUser> ALL=NOPASSWD: /bin/btrfs subvolume snapshot -r *, /bin/btrfs subvolume delete * #E.g., backuppc,backuppc-client ALL=NOPASSWD: /bin/btrfs subvolume snapshot -r /mnt/btrfs-*/@* /mnt/btrfs-*/@*_snapshot-backuppc-*, /bin/btrfs subvolume delete /mnt/btrfs-*/@*_snapshot-backuppc-* my $DAYS = 2; #Number of days to wait before expiring stale snapshots chomp(my $hostname = `hostname -s`); chomp(my $timestamp = `date +"%Y%m%d.%H%M%S"`); my $snapid = "_snapshot-backuppc"; my $suffix = "$snapid-$timestamp-$hostname"; my $snapshots = join(',', %snapshots); #Pack as string since can only pass in scalar context to $Conf{Dump...} my $snappaths = join(',', %snappaths); #Note: Need to escape scalar and array variables but not hashes $Conf{DumpPreUserCmd} = qq(&{sub { my \$RemoteUser = "$RemoteUser"; my \$hostname = "$hostname"; my \$snapid = "$snapid"; my \$suffix = "$suffix"; my \$AGE = `date +%s` - $DAYS * 86400; my \@sshcmd = $islocal == 1 ? () : ('ssh', '-q', '-x', '-l', \$RemoteUser, \$args[0]->{host}); #If <host> is not local, prepend 'ssh -l -q -x [-i <id_rsa>] <RemoteUser> <host>' so that command is executed on remote host via ssh my %snapshots = split(/,/, "$snapshots"); #Convert back to hash (note need to quote '\$snapshots' since gets expanded in scalar context) my %snappaths = split(/,/, "$snappaths"); my \@snapsdone; system('sync'); while(my (\$name, \$mount) = each %snapshots) { #Iterate through hash #Set the path of 'sharename' to point to the corresponding snapshot of the share plus optional 'snappath' ending path \$bpc->{Conf}{ClientShareName2Path}->{\$name} = \$mount . \$suffix . (\$snappaths{\$name} =~ m|^[^/]| ? '/' : '') . \$snappaths{\$name}; #Prepend '/' if not already present next if grep(/^\${mount}\$/, \@snapsdone); #Skip the following if already created (i.e. if share has same btrfs-volume) #Delete old expired snapshots (older than $DAYS) my \@oldsnaps = split ' ', `\@sshcmd ls -d \${mount}\${snapid}-*-\${hostname} 2>/dev/null`; #Array of old snapshots for $mount foreach(\@oldsnaps) { /\${mount}\${snapid}-([0-9]*)\.([0-9]{2})([0-9]{2})([0-9]{2})-\${hostname}/; #Extract date system(\@sshcmd, 'sudo', '/bin/btrfs', 'subvolume', 'delete', \$_) if `date -d "\$1 \$2:\$3:\$4" +%s` < \$AGE; } #Create read-only snapshot of 'sharemount' with added '$suffix' system(\@sshcmd, 'sudo', '/bin/btrfs', 'subvolume', 'snapshot', '-r', \$mount, \$mount . \$suffix); if(\$? >> 8 != 0) { #Error creating snapshots foreach(\@snapsdone) { #Unwind existing snapshots system(\@sshcmd, 'sudo', '/bin/btrfs', 'subvolume', 'delete', \$_ . \$suffix); #Delete snapshot } \$? = 257; #Set error code return; } push(\@snapsdone, \$mount); #Add to list of successful snapshots } }}); $Conf{DumpPostUserCmd} = qq(&{sub { my \$RemoteUser = "$RemoteUser"; my \$suffix = "$suffix"; my \@sshcmd = $islocal == 1 ? () : ('ssh', '-q', '-x', '-l', \$RemoteUser, \$args[0]->{host}); #If <host> is not local, prepend 'ssh -l -q -x [-i <id_rsa>] <RemoteUser> <host>' so that command is executed on remote host via ssh my %snapshots = split(/,/, "$snapshots"); #Convert back to hash (note need to quote $snapshots since gets expanded in scalar context) foreach (keys %{{ map{\$_ => 1} values(%snapshots)}}) { #Kluge to create list of unique 'mounts' (values) from %snapshot hash system(\@sshcmd, 'sudo', '/bin/btrfs', 'subvolume', 'delete', \$_ . \$suffix); #Delete snapshot } }}); } else { $Conf{DumpPreUserCmd} = undef; $Conf{DumpPostUserCmd} = undef; } _______________________________________________ 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/