mirror of
https://github.com/rsnapshot/rsnapshot.git
synced 2025-11-21 02:39:30 +01:00
7861 lines
216 KiB
Perl
Executable file
7861 lines
216 KiB
Perl
Executable file
#!@PERL@
|
||
|
||
########################################################################
|
||
# #
|
||
# rsnapshot #
|
||
# by Nathan Rosenquist #
|
||
# #
|
||
# The official rsnapshot website is located at #
|
||
# http://www.rsnapshot.org/ #
|
||
# #
|
||
# Copyright (C) 2003-2005 Nathan Rosenquist #
|
||
# #
|
||
# Portions Copyright (C) 2002-2006 Mike Rubel, Carl Wilhelm Soderstrom,#
|
||
# Ted Zlatanov, Carl Boe, Shane Liebling, Bharat Mediratta, #
|
||
# Peter Palfrader, Nicolas Kaiser, David Cantrell, Chris Petersen, #
|
||
# Robert Jackson, Justin Grote, David Keegel, Alan Batie #
|
||
# #
|
||
# rsnapshot comes with ABSOLUTELY NO WARRANTY. This is free software, #
|
||
# and you may copy, distribute and/or modify it under the terms of #
|
||
# the GNU GPL (version 2 or at your option any later version). #
|
||
# See the GNU General Public License (in file: COPYING) for details. #
|
||
# #
|
||
# Based on code originally by Mike Rubel #
|
||
# http://www.mikerubel.org/computers/rsync_snapshots/ #
|
||
# #
|
||
########################################################################
|
||
|
||
# tabstops are set to 4 spaces
|
||
# in vi, do: set ts=4 sw=4
|
||
|
||
########################################
|
||
### STANDARD MODULES ###
|
||
########################################
|
||
|
||
require 5.012000; # first version where POSIX::lchown is documented
|
||
use strict;
|
||
use warnings;
|
||
|
||
use DirHandle; # DirHandle()
|
||
use Cwd; # cwd()
|
||
use Getopt::Std; # getopts()
|
||
use File::Path; # mkpath(), rmtree()
|
||
use File::stat; # stat(), lstat()
|
||
use POSIX qw(locale_h lchown); # setlocale(), lchown()
|
||
use Fcntl; # sysopen()
|
||
use IO::File; # recursive open in parse_config_file
|
||
use IPC::Open3 qw(open3); # open rsync with error output
|
||
use IO::Handle; # handle autoflush for rsync-output
|
||
|
||
########################################
|
||
### DECLARE GLOBAL VARIABLES ###
|
||
########################################
|
||
|
||
# turn off buffering
|
||
$| = 1;
|
||
|
||
# version of rsnapshot
|
||
my $VERSION = '@VERSION@';
|
||
|
||
# command or interval to execute (first cmd line arg)
|
||
my $cmd;
|
||
|
||
# default configuration file
|
||
my $config_file;
|
||
|
||
# hash to hold variables from the configuration file
|
||
my %config_vars;
|
||
|
||
# array of hash_refs containing the destination backup point
|
||
# and either a source dir or a script to run
|
||
my @backup_points;
|
||
|
||
# array of backup points to rollback, in the event of failure
|
||
my @rollback_points;
|
||
|
||
# "intervals" are user defined time periods (e.g., alpha, beta)
|
||
# this array holds hash_refs containing the name of the interval,
|
||
# and the number of snapshots to keep of it
|
||
#
|
||
# NB, intervals and now called backup levels, and the config parameter
|
||
# is 'retain'
|
||
|
||
my @intervals;
|
||
|
||
# store interval data (mostly info about which one we're on, what was before, etc.)
|
||
# this is a convenient reference to some of the data from and metadata about @intervals
|
||
my $interval_data_ref;
|
||
|
||
# intervals can't have these values, because they're either taken by other commands
|
||
# or reserved for future use
|
||
my @reserved_words = qw(
|
||
archive
|
||
check-config-version
|
||
configtest
|
||
diff
|
||
delete
|
||
du
|
||
get-latest-snapshot
|
||
help
|
||
history
|
||
list
|
||
print-config
|
||
restore
|
||
rollback
|
||
sync
|
||
upgrade-config-file
|
||
version
|
||
version-only
|
||
);
|
||
|
||
# global flags that change the outcome of the program,
|
||
# and are configurable by both cmd line and config flags
|
||
#
|
||
my $test = 0; # turn verbose on, but don't execute
|
||
# any filesystem commands
|
||
my $do_configtest = 0; # parse config file and exit
|
||
my $one_fs = 0; # one file system (don't cross
|
||
# partitions within a backup point)
|
||
my $link_dest = 0; # use the --link-dest option to rsync
|
||
my $stop_on_stale_lockfile = 0; # stop if there is a stale lockfile
|
||
|
||
# how much noise should we make? the default is 2
|
||
#
|
||
# 0 Absolutely quiet (reserved, but not implemented)
|
||
# 1 Don't display warnings about FIFOs and special files
|
||
# 2 Default (errors only)
|
||
# 3 Verbose (show shell commands and equivalents)
|
||
# 4 Extra verbose messages (individual actions inside some subroutines, output from rsync)
|
||
# 5 Debug
|
||
#
|
||
# define verbose and loglevel
|
||
my $verbose = undef;
|
||
my $loglevel = undef;
|
||
|
||
# set defaults for verbose and loglevel
|
||
my $default_verbose = 2;
|
||
my $default_loglevel = 3;
|
||
|
||
# assume the config file is valid until we find an error
|
||
my $config_perfect = 1;
|
||
|
||
# exit code for rsnapshot
|
||
my $exit_code = 0;
|
||
|
||
# global defaults for external programs
|
||
my $default_rsync_short_args = '-a';
|
||
my $default_rsync_long_args = '--delete --numeric-ids --relative --delete-excluded';
|
||
my $default_ssh_args = undef;
|
||
my $default_du_args = '-csh';
|
||
|
||
# set default for use_lazy_deletes
|
||
my $use_lazy_deletes = 0; # do not delete the oldest archive until after backup
|
||
|
||
# set default for number of tries
|
||
my $rsync_numtries = 1; # by default, try once
|
||
|
||
# set default wait time between tries
|
||
my $rsync_wait_between_tries = 0; # by default, don't wait
|
||
|
||
# exactly how the program was called, with all arguments
|
||
# this is set before getopts() modifies @ARGV
|
||
my $run_string = "$0 " . join(' ', @ARGV);
|
||
|
||
# if we have any errors, we print the run string once, at the top of the list
|
||
my $have_printed_run_string = 0;
|
||
|
||
# pre-buffer the include/exclude parameter flags
|
||
# local to parse_config_file and validate_config_file
|
||
my $rsync_include_args = undef;
|
||
my $rsync_include_file_args = undef;
|
||
|
||
# hash used to register traps and execute in bail
|
||
my %traps;
|
||
$traps{"linux_lvm_snapshot"} = 0;
|
||
$traps{"linux_lvm_mountpoint"} = 0;
|
||
|
||
########################################
|
||
### SIGNAL HANDLERS ###
|
||
########################################
|
||
|
||
# shut down gracefully if necessary
|
||
$SIG{'HUP'} = 'IGNORE';
|
||
$SIG{'INT'} = sub { bail('rsnapshot was sent INT signal... cleaning up'); };
|
||
$SIG{'QUIT'} = sub { bail('rsnapshot was sent QUIT signal... cleaning up'); };
|
||
$SIG{'ABRT'} = sub { bail('rsnapshot was sent ABRT signal... cleaning up'); };
|
||
$SIG{'TERM'} = sub { bail('rsnapshot was sent TERM signal... cleaning up'); };
|
||
|
||
# For a PIPE error, we dont want any more output so set $verbose less than 1.
|
||
$SIG{'PIPE'} = sub {
|
||
$verbose = 0;
|
||
bail(
|
||
'rsnapshot was sent PIPE signal... Hint: if rsnapshot is running from cron, check that mail is installed on this system, or redirect stdout and stderr in cron job'
|
||
);
|
||
};
|
||
|
||
########################################
|
||
### CORE PROGRAM STRUCTURE ###
|
||
########################################
|
||
|
||
# what follows is a linear sequence of events.
|
||
# all of these subroutines will either succeed or terminate the program safely.
|
||
|
||
# figure out the path to the default config file (with autoconf we have to check)
|
||
# this sets $config_file to the full config file path
|
||
find_config_file();
|
||
|
||
# parse command line options
|
||
# (this can override $config_file, if the -c flag is used on the command line)
|
||
parse_cmd_line_opts();
|
||
|
||
# if we need to run a command that doesn't require fully parsing the config file, do it now (and exit)
|
||
if (!defined($cmd) or ((!$cmd) && ('0' ne $cmd))) {
|
||
show_usage();
|
||
}
|
||
elsif ($cmd eq 'help') {
|
||
show_help();
|
||
}
|
||
elsif ($cmd eq 'version') {
|
||
show_version();
|
||
}
|
||
elsif ($cmd eq 'version-only') {
|
||
show_version_only();
|
||
}
|
||
elsif ($cmd eq 'check-config-version') {
|
||
check_config_version();
|
||
}
|
||
elsif ($cmd eq 'upgrade-config-file') {
|
||
upgrade_config_file();
|
||
}
|
||
|
||
# if we're just doing a configtest, set that flag
|
||
if ($cmd eq 'configtest') {
|
||
$do_configtest = 1;
|
||
}
|
||
|
||
# parse config file (if it exists), note: we can't parse a directory
|
||
if (defined($config_file) && (-r $config_file) && (!-d $config_file)) {
|
||
|
||
# if there is a problem, this subroutine will exit the program and notify the user of the error
|
||
parse_config_file();
|
||
validate_config_file();
|
||
|
||
}
|
||
|
||
# no config file found
|
||
else {
|
||
|
||
# warn user and exit the program
|
||
exit_no_config_file();
|
||
}
|
||
|
||
# if we're just doing a configtest, exit here with the results
|
||
if (1 == $do_configtest) {
|
||
exit_configtest();
|
||
}
|
||
|
||
# if we're just using "du" or "rsnapshot-diff" to check the disk space, do it now (and exit)
|
||
# these commands are down here because they needs to know the contents of the config file
|
||
if ($cmd eq 'du') {
|
||
show_disk_usage();
|
||
}
|
||
elsif ($cmd eq 'diff') {
|
||
show_rsnapshot_diff();
|
||
}
|
||
elsif ($cmd eq 'get-latest-snapshot') {
|
||
show_latest_snapshot();
|
||
}
|
||
|
||
#
|
||
# IF WE GOT THIS FAR, PREPARE TO RUN A BACKUP
|
||
#
|
||
|
||
# log the beginning of this run
|
||
log_startup();
|
||
|
||
# this is reported to fix some semi-obscure problems with rmtree()
|
||
set_posix_locale();
|
||
|
||
# if we're using a lockfile, try to add it
|
||
# (the program will bail if one exists and it's not stale)
|
||
add_lockfile();
|
||
|
||
# create snapshot_root if it doesn't exist (and no_create_root != 1)
|
||
create_snapshot_root();
|
||
|
||
# now chdir to the snapshot_root.
|
||
# note that this is needed because in the rare case that you do this ...
|
||
# sudo -u peon rsnapshot ... and are in a directory that 'peon' can't
|
||
# read, then some versions of GNU rm will later fail, as they try to
|
||
# lstat the cwd. It's safe to chdir because all directories etc that
|
||
# we ever mention are absolute.
|
||
chdir($config_vars{'snapshot_root'});
|
||
|
||
# actually run the backup job
|
||
# $cmd should store the name of the interval we'll run against
|
||
handle_interval($cmd);
|
||
|
||
# if we have a lockfile, remove it
|
||
# however, this will have already been done if use_lazy_deletes is turned
|
||
# on, and there may be a lockfile from another process now in place,
|
||
# so in that case don't just blindly delete!
|
||
remove_lockfile() unless ($use_lazy_deletes);
|
||
|
||
# if we got this far, the program is done running
|
||
# write to the log and syslog with the status of the outcome
|
||
#
|
||
exit_with_status();
|
||
|
||
########################################
|
||
### SUBROUTINES ###
|
||
########################################
|
||
|
||
# concise usage information
|
||
# runs when rsnapshot is called with no arguments
|
||
# exits with an error condition
|
||
sub show_usage {
|
||
print <<HERE;
|
||
rsnapshot $VERSION
|
||
Usage: rsnapshot [-vtxqVD] [-c cfgfile] [command] [args]
|
||
Type \"rsnapshot help\" or \"man rsnapshot\" for more information.
|
||
HERE
|
||
|
||
exit(1);
|
||
}
|
||
|
||
# extended usage information
|
||
# runs when rsnapshot is called with "help" as an argument
|
||
# exits 0
|
||
sub show_help {
|
||
print <<HERE;
|
||
rsnapshot $VERSION
|
||
Usage: rsnapshot [-vtxqVD] [-c cfgfile] [command] [args]
|
||
Type "man rsnapshot" for more information.
|
||
|
||
rsnapshot is a filesystem snapshot utility. It can take incremental
|
||
snapshots of local and remote filesystems for any number of machines.
|
||
|
||
rsnapshot comes with ABSOLUTELY NO WARRANTY. This is free software,
|
||
and you are welcome to redistribute it under certain conditions.
|
||
See the GNU General Public License for details.
|
||
|
||
Options:
|
||
-v verbose - Show equivalent shell commands being executed.
|
||
-t test - Show verbose output, but don't touch anything.
|
||
This will be similar, but not always exactly the same
|
||
as the real output from a live run.
|
||
-c [file] - Specify alternate config file (-c /path/to/file)
|
||
-q quiet - Suppress non-fatal warnings.
|
||
-V extra verbose - The same as -v, but with more detail.
|
||
-D debug - A firehose of diagnostic information.
|
||
-x one_fs - Don't cross filesystems (same as -x option to rsync).
|
||
|
||
Commands:
|
||
[backuplevel] - A backup level as defined in rsnapshot.conf.
|
||
configtest - Syntax check the config file.
|
||
sync [dest] - Sync files, without rotating. "sync_first" must be
|
||
enabled for this to work. If a full backup point
|
||
destination is given as an optional argument, only
|
||
those files will be synced.
|
||
diff - Front-end interface to the rsnapshot-diff program.
|
||
Accepts two optional arguments which can be either
|
||
filesystem paths or backup directories within the
|
||
snapshot_root (e.g., /etc/ beta.0/etc/). The default
|
||
is to compare the two most recent snapshots.
|
||
du - Show disk usage in the snapshot_root.
|
||
Accepts an optional destination path for comparison
|
||
across snapshots (e.g., localhost/home/user/foo).
|
||
version - Show the version number for rsnapshot.
|
||
help - Show this help message.
|
||
HERE
|
||
|
||
exit(0);
|
||
}
|
||
|
||
# prints out the name and version
|
||
# exits 0
|
||
sub show_version {
|
||
print "rsnapshot $VERSION\n";
|
||
exit(0);
|
||
}
|
||
|
||
# prints only the version number
|
||
# this is "undocumented", just for use with some of the makefile targets
|
||
# exits 0
|
||
sub show_version_only {
|
||
print "$VERSION\n";
|
||
exit(0);
|
||
}
|
||
|
||
# For Getopt::Std
|
||
sub VERSION_MESSAGE {
|
||
show_version;
|
||
}
|
||
|
||
# For Getopt::Std
|
||
sub HELP_MESSAGE {
|
||
show_help;
|
||
}
|
||
|
||
# accepts no arguments
|
||
# sets the $config_file global variable
|
||
#
|
||
# this program works both "as-is" in the source tree, and when it has been parsed by autoconf for installation
|
||
# the variables with "@" symbols on both sides get replaced during ./configure
|
||
# this subroutine returns the correct path to the default config file
|
||
#
|
||
sub find_config_file {
|
||
|
||
# autoconf variables (may have too many slashes)
|
||
my $autoconf_sysconfdir = '@sysconfdir@';
|
||
my $autoconf_prefix = '@prefix@';
|
||
my $default_config_file = '/etc/rsnapshot.conf';
|
||
|
||
# consolidate multiple slashes
|
||
$autoconf_sysconfdir =~ s/\/+/\//g;
|
||
$autoconf_prefix =~ s/\/+/\//g;
|
||
|
||
# remove trailing slashes
|
||
$autoconf_sysconfdir =~ s/\/$//g;
|
||
$autoconf_prefix =~ s/\/$//g;
|
||
|
||
# if --sysconfdir was not set explicitly during ./configure, but we did use autoconf
|
||
if ($autoconf_sysconfdir eq '${prefix}/etc') {
|
||
$default_config_file = "$autoconf_prefix/etc/rsnapshot.conf";
|
||
|
||
# if --sysconfdir was set explicitly at ./configure, overriding the --prefix setting
|
||
}
|
||
elsif ($autoconf_sysconfdir ne ('@' . 'sysconfdir' . '@')) {
|
||
$default_config_file = "$autoconf_sysconfdir/rsnapshot.conf";
|
||
}
|
||
|
||
# set global variable
|
||
$config_file = $default_config_file;
|
||
}
|
||
|
||
# accepts no args
|
||
# returns no args
|
||
# sets some global flag variables
|
||
# exits the program with an error if we were passed invalid options
|
||
sub parse_cmd_line_opts {
|
||
my %opts;
|
||
my $result;
|
||
|
||
# get command line options
|
||
$result = getopts('vtxqVDc:', \%opts);
|
||
|
||
#
|
||
# validate command line args
|
||
#
|
||
|
||
# make sure config file is a file
|
||
if (defined($opts{'c'})) {
|
||
if (!-r "$opts{'c'}") {
|
||
print STDERR "File not found: $opts{'c'}\n";
|
||
$result = undef;
|
||
}
|
||
}
|
||
|
||
# die if we don't understand all the flags
|
||
if (!defined($result) or (1 != $result)) {
|
||
|
||
# At this point, getopts() or our @ARGV check will have printed out "Unknown option: -X"
|
||
print STDERR "Type \"rsnapshot help\" or \"man rsnapshot\" for more information.\n";
|
||
exit(1);
|
||
}
|
||
|
||
#
|
||
# with that out of the way, we can go about the business of setting global variables
|
||
#
|
||
|
||
# set command
|
||
$cmd = $ARGV[0];
|
||
|
||
# check for extra bogus arguments that getopts() didn't catch
|
||
if (defined($cmd) && ('du' ne $cmd) && ('diff' ne $cmd) && ('sync' ne $cmd)) {
|
||
if (scalar(@ARGV) > 1) {
|
||
for (my $i = 1; $i < scalar(@ARGV); $i++) {
|
||
print STDERR "Unknown option: $ARGV[$i]\n";
|
||
print STDERR "Please make sure all switches come before commands\n";
|
||
print STDERR "(e.g., 'rsnapshot -v alpha', not 'rsnapshot alpha -v')\n";
|
||
exit(1);
|
||
}
|
||
|
||
$result = undef;
|
||
}
|
||
}
|
||
|
||
# alternate config file?
|
||
if (defined($opts{'c'})) {
|
||
$config_file = $opts{'c'};
|
||
}
|
||
|
||
# test? (just show what WOULD be done)
|
||
if (defined($opts{'t'})) {
|
||
$test = 1;
|
||
$verbose = 3;
|
||
}
|
||
|
||
# quiet?
|
||
if (defined($opts{'q'})) { $verbose = 1; }
|
||
|
||
# verbose (or extra verbose)?
|
||
if (defined($opts{'v'})) { $verbose = 3; }
|
||
if (defined($opts{'V'})) { $verbose = 4; }
|
||
|
||
# debug
|
||
if (defined($opts{'D'})) { $verbose = 5; }
|
||
|
||
# one file system? (don't span partitions with rsync)
|
||
if (defined($opts{'x'})) { $one_fs = 1; }
|
||
}
|
||
|
||
# accepts an optional argument - no arg means to parse the default file,
|
||
# if an arg is present parse that file instead
|
||
# returns no value
|
||
# this subroutine parses the config file (rsnapshot.conf)
|
||
#
|
||
sub parse_config_file {
|
||
|
||
# count the lines in the config file, so the user can pinpoint errors more precisely
|
||
my $file_line_num = 0;
|
||
my @configs = ();
|
||
|
||
# open the config file
|
||
my $current_config_file = shift() || $config_file;
|
||
my $CONFIG;
|
||
if ($current_config_file =~ /^`(.*)`$/) {
|
||
open($CONFIG, "$1 |")
|
||
or bail("Couldn't execute \"$1\" to get config information\n");
|
||
}
|
||
else {
|
||
$CONFIG = IO::File->new($current_config_file)
|
||
or bail("Could not open config file \"$current_config_file\"\nAre you sure you have permission?");
|
||
}
|
||
|
||
# read it line by line
|
||
@configs = <$CONFIG>;
|
||
while (my $line = $configs[$file_line_num]) {
|
||
chomp($line);
|
||
|
||
# count line numbers
|
||
$file_line_num++;
|
||
|
||
# Ensure the correct filename is reported in error messages. Setting it on
|
||
# every iteration ensures it will be reset after recursive calls to this
|
||
# function.
|
||
$config_file = $current_config_file;
|
||
|
||
# assume the line is formatted incorrectly
|
||
my $line_syntax_ok = 0;
|
||
|
||
# ignore comments
|
||
if (is_comment($line)) { next; }
|
||
|
||
# ignore blank lines
|
||
if (is_blank($line)) { next; }
|
||
|
||
# if the next line begins with space or tab and also has a non-space character, then it belongs to this line as a continuation.
|
||
while (defined($configs[$file_line_num]) && $configs[$file_line_num] =~ /^[\t ]+\S/) {
|
||
(my $newline = $configs[$file_line_num]) =~ s/^\s+|\s+$//g;
|
||
$line = $line . "\t" . $newline;
|
||
$file_line_num++;
|
||
}
|
||
|
||
# parse line
|
||
my ($var, $value, $value2, $value3) = split(/\t+/, $line, 4);
|
||
|
||
# warn about entries we don't understand, and immediately prevent the
|
||
# program from running or parsing anything else
|
||
if (!defined($var)) {
|
||
config_err($file_line_num, "$line - could not find a first word on this line");
|
||
next;
|
||
}
|
||
if (!defined($value) && $var eq $line) {
|
||
|
||
# No tabs found in $line.
|
||
if ($line =~ /\s/) {
|
||
|
||
# User put spaces in config line instead of tabs.
|
||
config_err($file_line_num, "$line - missing tabs to separate words - change spaces to tabs.");
|
||
next;
|
||
}
|
||
else {
|
||
|
||
# User put only one word
|
||
config_err($file_line_num, "$line - could not find second word on this line");
|
||
next;
|
||
}
|
||
}
|
||
foreach (grep { defined($_) && index($_, ' ') == 0 } ($value, $value2, $value3)) {
|
||
print_warn("$line - extra space found between tab and $_");
|
||
}
|
||
|
||
# INCLUDEs
|
||
if ($var eq 'include_conf') {
|
||
$value =~ /^`(.*)`$/;
|
||
if ( (defined($value) && -f $value && -r $value)
|
||
|| (defined($1) && is_valid_script($1))) {
|
||
$line_syntax_ok = 1;
|
||
parse_config_file($value);
|
||
}
|
||
else {
|
||
if (defined($1)) {
|
||
config_err($file_line_num, "$line - not a valid script: '$1'");
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - can't find or read file '$value'");
|
||
}
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CONFIG_VERSION
|
||
if ($var eq 'config_version') {
|
||
if (defined($value)) {
|
||
|
||
# right now 1.2 is the only possible value
|
||
if ('1.2' eq $value) {
|
||
$config_vars{'config_version'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - config_version not recognized!");
|
||
next;
|
||
}
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - config_version not defined!");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# SNAPSHOT_ROOT
|
||
if ($var eq 'snapshot_root') {
|
||
|
||
# make sure this is a full path
|
||
if (0 == is_valid_local_abs_path($value)) {
|
||
if (is_ssh_path($value) || is_anon_rsync_path($value) || is_cwrsync_path($value)) {
|
||
config_err($file_line_num,
|
||
"$line - snapshot_root must be a local path - you cannot have a remote snapshot_root");
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - snapshot_root must be a full path");
|
||
}
|
||
next;
|
||
|
||
# if the snapshot root already exists:
|
||
}
|
||
elsif (-e "$value") {
|
||
|
||
# if path exists already, make sure it's a directory
|
||
if ((-e "$value") && (!-d "$value")) {
|
||
config_err($file_line_num, "$line - snapshot_root must be a directory");
|
||
next;
|
||
}
|
||
|
||
# make sure it's readable
|
||
if (!-r "$value") {
|
||
config_err($file_line_num, "$line - snapshot_root exists but is not readable");
|
||
next;
|
||
}
|
||
|
||
# make sure it's writable
|
||
if ($cmd ne 'du' && !-w "$value") {
|
||
config_err($file_line_num, "$line - snapshot_root exists but is not writable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# remove the trailing slash(es) if present
|
||
$value = remove_trailing_slash($value);
|
||
|
||
$config_vars{'snapshot_root'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# SYNC_FIRST
|
||
# if this is enabled, rsnapshot syncs data to a staging directory with the "rsnapshot sync" command,
|
||
# and all "interval" runs will simply rotate files. this changes the behaviour of the lowest interval.
|
||
# when a sync occurs, no directories are rotated. the sync directory is kind of like a staging area for data transfers.
|
||
# the files in the sync directory will be hard linked with the others in the other snapshot directories.
|
||
# the sync directory lives at: /<snapshot_root>/.sync/
|
||
#
|
||
if ($var eq 'sync_first') {
|
||
if (defined($value)) {
|
||
if ('1' eq $value) {
|
||
$config_vars{'sync_first'} = 1;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
elsif ('0' eq $value) {
|
||
$config_vars{'sync_first'} = 0;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - sync_first must be set to either 1 or 0");
|
||
next;
|
||
}
|
||
}
|
||
}
|
||
|
||
# NO_CREATE_ROOT
|
||
if ($var eq 'no_create_root') {
|
||
if (defined($value)) {
|
||
if ('1' eq $value) {
|
||
$config_vars{'no_create_root'} = 1;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
elsif ('0' eq $value) {
|
||
$config_vars{'no_create_root'} = 0;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - no_create_root must be set to either 1 or 0");
|
||
next;
|
||
}
|
||
}
|
||
}
|
||
|
||
# CHECK FOR RSYNC (required)
|
||
if ($var eq 'cmd_rsync') {
|
||
$value =~ s/\s+$//;
|
||
if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) {
|
||
$config_vars{'cmd_rsync'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR SSH (optional)
|
||
if ($var eq 'cmd_ssh') {
|
||
$value =~ s/\s+$//;
|
||
if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) {
|
||
$config_vars{'cmd_ssh'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR GNU cp (optional)
|
||
if ($var eq 'cmd_cp') {
|
||
$value =~ s/\s+$//;
|
||
if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) {
|
||
$config_vars{'cmd_cp'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR rm (optional)
|
||
if ($var eq 'cmd_rm') {
|
||
$value =~ s/\s+$//;
|
||
if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) {
|
||
$config_vars{'cmd_rm'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR LOGGER (syslog program) (optional)
|
||
if ($var eq 'cmd_logger') {
|
||
$value =~ s/\s+$//;
|
||
if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) {
|
||
$config_vars{'cmd_logger'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR du (optional)
|
||
if ($var eq 'cmd_du') {
|
||
$value =~ s/\s+$//;
|
||
if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) {
|
||
$config_vars{'cmd_du'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR lvcreate (optional)
|
||
if ($var eq 'linux_lvm_cmd_lvcreate') {
|
||
if (is_valid_script($value)) {
|
||
$config_vars{'linux_lvm_cmd_lvcreate'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not a valid executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR lvremove (optional)
|
||
if ($var eq 'linux_lvm_cmd_lvremove') {
|
||
if (is_valid_script($value)) {
|
||
$config_vars{'linux_lvm_cmd_lvremove'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not a valid executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR mount (optional)
|
||
if ($var eq 'linux_lvm_cmd_mount') {
|
||
if (is_valid_script($value)) {
|
||
$config_vars{'linux_lvm_cmd_mount'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not a valid executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR umount (optional)
|
||
if ($var eq 'linux_lvm_cmd_umount') {
|
||
if (is_valid_script($value)) {
|
||
$config_vars{'linux_lvm_cmd_umount'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not a valid executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# CHECK FOR cmd_preexec (optional)
|
||
if ($var eq 'cmd_preexec') {
|
||
my $script; # script file (no args)
|
||
|
||
# make sure script exists and is executable
|
||
if (!is_valid_script($value, \$script)) {
|
||
config_err($file_line_num,
|
||
"$line - \"$script\" is not executable or can't be found."
|
||
. ($script !~ m{^/} ? " Please use an absolute path." : ""));
|
||
next;
|
||
}
|
||
|
||
$config_vars{$var} = $value;
|
||
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# CHECK FOR cmd_postexec (optional)
|
||
if ($var eq 'cmd_postexec') {
|
||
my $script; # script file (no args)
|
||
|
||
# make sure script exists and is executable
|
||
if (!is_valid_script($value, \$script)) {
|
||
config_err($file_line_num,
|
||
"$line - \"$script\" is not executable or can't be found."
|
||
. ($script !~ m{^/} ? " Please use an absolute path." : ""));
|
||
next;
|
||
}
|
||
|
||
$config_vars{$var} = $value;
|
||
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# CHECK FOR rsnapshot-diff (optional)
|
||
if ($var eq 'cmd_rsnapshot_diff') {
|
||
$value =~ s/\s+$//;
|
||
if ((-f "$value") && (-x "$value") && (1 == is_real_local_abs_path($value))) {
|
||
$config_vars{'cmd_rsnapshot_diff'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - $value is not executable");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# INTERVALS
|
||
# 'retain' is the new name for this parameter, although for
|
||
# Laziness reasons (plus the fact that I'm making this change
|
||
# at 10 minutes to midnight and so am wary of making changes
|
||
# throughout the code and getting it wrong) the code will
|
||
# still call it 'interval'. Documentation and messages should
|
||
# refer to 'retain'. The old 'interval' will be kept as an
|
||
# alias.
|
||
if ($var eq 'interval' || $var eq 'retain') {
|
||
my $retain = $var; # either 'interval' or 'retain'
|
||
# check if interval is blank
|
||
if (!defined($value)) { config_err($file_line_num, "$line - $retain can not be blank"); }
|
||
|
||
foreach my $word (@reserved_words) {
|
||
if ($value eq $word) {
|
||
config_err($file_line_num,
|
||
"$line - \"$value\" is not a valid interval name, reserved word conflict");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# make sure interval is alphanumeric
|
||
if ($value !~ m/^[\w\d]+$/) {
|
||
config_err($file_line_num,
|
||
"$line - \"$value\" is not a valid $retain name, must be alphanumeric characters only");
|
||
next;
|
||
}
|
||
|
||
# check if number is blank
|
||
if (!defined($value2)) {
|
||
config_err($file_line_num, "$line - \"$value\" number can not be blank");
|
||
next;
|
||
}
|
||
|
||
# check if number is valid
|
||
if ($value2 !~ m/^\d+$/) {
|
||
config_err($file_line_num, "$line - \"$value2\" is not a legal value for a retention count");
|
||
next;
|
||
|
||
}
|
||
|
||
# ok, it's a number. is it positive?
|
||
else {
|
||
|
||
# make sure number is positive
|
||
if ($value2 <= 0) {
|
||
config_err($file_line_num, "$line - \"$value\" must be at least 1 or higher");
|
||
next;
|
||
}
|
||
}
|
||
|
||
my %hash;
|
||
$hash{'interval'} = $value;
|
||
$hash{'number'} = $value2;
|
||
push(@intervals, \%hash);
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# BACKUP POINTS
|
||
if ($var eq 'backup') {
|
||
my $src = $value; # source directory
|
||
my $dest = $value2; # dest directory
|
||
my $opt_str = $value3; # option string from this backup point
|
||
my $opts_ref = undef; # array_ref to hold parsed opts
|
||
|
||
if (!defined($config_vars{'snapshot_root'})) {
|
||
config_err($file_line_num, "$line - snapshot_root needs to be defined before backup points");
|
||
next;
|
||
}
|
||
|
||
if (!defined($src)) {
|
||
config_err($file_line_num, "$line - no source path specified for backup point");
|
||
next;
|
||
}
|
||
|
||
if (!defined($dest) || $dest eq "") {
|
||
config_err($file_line_num, "$line - no destination path specified for backup point");
|
||
next;
|
||
}
|
||
|
||
# make sure we aren't traversing directories
|
||
if (is_directory_traversal($src)) {
|
||
config_err($file_line_num, "$line - Directory traversal attempted in $src");
|
||
next;
|
||
}
|
||
if (is_directory_traversal($dest)) {
|
||
config_err($file_line_num, "$line - Directory traversal attempted in $dest");
|
||
next;
|
||
}
|
||
|
||
# validate source path
|
||
#
|
||
# local absolute?
|
||
if (is_real_local_abs_path($src)) {
|
||
$line_syntax_ok = 1;
|
||
|
||
# syntactically valid remote ssh?
|
||
}
|
||
elsif (is_ssh_path($src)) {
|
||
|
||
# if it's an ssh path, make sure we have ssh
|
||
if (!defined($config_vars{'cmd_ssh'})) {
|
||
config_err($file_line_num, "$line - Cannot handle $src, cmd_ssh not defined in $config_file");
|
||
next;
|
||
}
|
||
$line_syntax_ok = 1;
|
||
|
||
# if it's anonymous rsync, we're ok
|
||
}
|
||
elsif (is_anon_rsync_path($src)) {
|
||
$line_syntax_ok = 1;
|
||
|
||
# check for cwrsync
|
||
}
|
||
elsif (is_cwrsync_path($src)) {
|
||
$line_syntax_ok = 1;
|
||
|
||
# check for lvm
|
||
}
|
||
elsif (is_linux_lvm_path($src)) {
|
||
|
||
# if it's an lvm path, make sure we have lvm commands and arguments
|
||
if (!defined($config_vars{'linux_lvm_cmd_lvcreate'})) {
|
||
config_err($file_line_num,
|
||
"$line - Cannot handle $src, linux_lvm_cmd_lvcreate not defined in $config_file");
|
||
next;
|
||
}
|
||
if (!defined($config_vars{'linux_lvm_cmd_lvremove'})) {
|
||
config_err($file_line_num,
|
||
"$line - Cannot handle $src, linux_lvm_cmd_lvremove not defined in $config_file");
|
||
next;
|
||
}
|
||
if (!defined($config_vars{'linux_lvm_cmd_mount'})) {
|
||
config_err($file_line_num,
|
||
"$line - Cannot handle $src, linux_lvm_cmd_mount not defined in $config_file");
|
||
next;
|
||
}
|
||
if (!defined($config_vars{'linux_lvm_cmd_umount'})) {
|
||
config_err($file_line_num,
|
||
"$line - Cannot handle $src, linux_lvm_cmd_umount not defined in $config_file");
|
||
next;
|
||
}
|
||
if (!defined($config_vars{'linux_lvm_snapshotsize'})) {
|
||
config_err($file_line_num,
|
||
"$line - Cannot handle $src, linux_lvm_snapshotsize not defined in $config_file");
|
||
next;
|
||
}
|
||
if (!defined($config_vars{'linux_lvm_snapshotname'})) {
|
||
config_err($file_line_num,
|
||
"$line - Cannot handle $src, linux_lvm_snapshotname not defined in $config_file");
|
||
next;
|
||
}
|
||
if (!defined($config_vars{'linux_lvm_vgpath'})) {
|
||
config_err($file_line_num,
|
||
"$line - Cannot handle $src, linux_lvm_vgpath not defined in $config_file");
|
||
next;
|
||
}
|
||
if (!defined($config_vars{'linux_lvm_mountpath'})) {
|
||
config_err($file_line_num,
|
||
"$line - Cannot handle $src, linux_lvm_mountpath not defined in $config_file");
|
||
next;
|
||
}
|
||
$line_syntax_ok = 1;
|
||
|
||
}
|
||
|
||
# fear the unknown
|
||
else {
|
||
config_err($file_line_num, "$line - Source directory \"$src\" doesn't exist");
|
||
next;
|
||
}
|
||
|
||
# validate destination path
|
||
#
|
||
# make sure we have a local NON absolute path for dest
|
||
if (!is_valid_local_non_abs_path($dest)) {
|
||
config_err($file_line_num, "$line - Backup destination $dest must be a local, relative path");
|
||
next;
|
||
}
|
||
|
||
# if we have special options specified for this backup point, remember them
|
||
if (defined($opt_str) && $opt_str) {
|
||
$opts_ref = parse_backup_opts($opt_str);
|
||
if (!defined($opts_ref)) {
|
||
config_err($file_line_num, "$line - Syntax error on line $file_line_num in extra opts: $opt_str");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# remember src/dest
|
||
my %hash;
|
||
$hash{'src'} = $src;
|
||
$hash{'dest'} = normalize_dest_file_path_part($dest);
|
||
if (defined($opts_ref)) {
|
||
$hash{'opts'} = $opts_ref;
|
||
}
|
||
|
||
# If this backup point contains the snapshot root, add an exclude to avoid
|
||
# backing up the snapshot root recursively. The exclude is anchored (by virtue
|
||
# of the leading slash of $config_vars{'snapshot_root'}) and applies to absolute
|
||
# paths (the "/" modifier), so it should match the snapshot root and nothing else
|
||
# regardless of --relative.
|
||
#
|
||
# This should work in any version of rsync since 2.6.4 except for 2.6.7, due to a bug:
|
||
# http://lists.samba.org/archive/rsync/2006-March/014953.html
|
||
if ((is_real_local_abs_path("$src")) && ($config_vars{'snapshot_root'} =~ m/^$src/)) {
|
||
$hash{'opts'}{'extra_rsync_long_args'} .= sprintf(' --filter=-/_%s', $config_vars{'snapshot_root'});
|
||
}
|
||
|
||
push(@backup_points, \%hash);
|
||
next;
|
||
}
|
||
|
||
# BACKUP SCRIPTS
|
||
if ($var eq 'backup_script') {
|
||
my $full_script = $value; # backup script to run (including args)
|
||
my $dest = $value2; # dest directory
|
||
my %hash; # tmp hash to stick in the backup points array
|
||
my $script; # script file (no args)
|
||
my @script_argv; # tmp array to help us separate the script from the args
|
||
|
||
if (!defined($config_vars{'snapshot_root'})) {
|
||
config_err($file_line_num, "$line - snapshot_root needs to be defined before backup scripts");
|
||
next;
|
||
}
|
||
|
||
if (!defined($dest)) {
|
||
config_err($file_line_num, "$line - no destination path specified");
|
||
next;
|
||
}
|
||
|
||
# get the base name of the script, not counting any arguments to it
|
||
@script_argv = split(/\s+/, $full_script);
|
||
$script = $script_argv[0];
|
||
|
||
# make sure the destination is a relative path
|
||
if (0 == is_valid_local_non_abs_path($dest)) {
|
||
config_err($file_line_num, "$line - Backup destination $dest must be a local, relative path");
|
||
next;
|
||
}
|
||
|
||
# make sure we aren't traversing directories (exactly 2 dots can't be next to each other)
|
||
if (1 == is_directory_traversal($dest)) {
|
||
config_err($file_line_num, "$line - Directory traversal attempted in $dest");
|
||
next;
|
||
}
|
||
|
||
# make sure script exists and is executable
|
||
if (((!-f "$script") or (!-x "$script")) or !is_real_local_abs_path($script)) {
|
||
config_err($file_line_num,
|
||
"$line - \"$script\" is not executable or can't be found."
|
||
. ($script !~ m{^/} ? " Please use an absolute path." : ""));
|
||
next;
|
||
}
|
||
|
||
$hash{'script'} = $full_script;
|
||
$hash{'dest'} = normalize_dest_file_path_part($dest);
|
||
|
||
$line_syntax_ok = 1;
|
||
|
||
push(@backup_points, \%hash);
|
||
|
||
next;
|
||
}
|
||
|
||
# BACKUP EXEC - just run a unix command
|
||
if ($var eq 'backup_exec') {
|
||
my %hash;
|
||
my $cmd = $value;
|
||
my $importance = $value2;
|
||
if (!defined($cmd)) {
|
||
config_err($file_line_num, "$line - a command to be executed must be provided");
|
||
next;
|
||
}
|
||
|
||
# Valid importance level options: 'optional', 'required'.
|
||
# Default value if not specified: 'optional'
|
||
if (!defined($importance)) {
|
||
$importance = 'optional';
|
||
}
|
||
elsif ($importance ne 'optional' && $importance ne 'required') {
|
||
config_err($file_line_num, "$line - requirement level \"$importance\" is invalid");
|
||
next;
|
||
}
|
||
$hash{'cmd'} = $cmd;
|
||
$hash{'importance'} = $importance;
|
||
$line_syntax_ok = 1;
|
||
push(@backup_points, \%hash);
|
||
next;
|
||
}
|
||
|
||
# GLOBAL OPTIONS from the config file
|
||
# ALL ARE OPTIONAL
|
||
#
|
||
# LINK_DEST
|
||
if ($var eq 'link_dest') {
|
||
if (!defined($value)) {
|
||
config_err($file_line_num, "$line - link_dest can not be blank");
|
||
next;
|
||
}
|
||
if (!is_boolean($value)) {
|
||
config_err($file_line_num,
|
||
"$line - \"$value\" is not a legal value for link_dest, must be 0 or 1 only");
|
||
next;
|
||
}
|
||
|
||
$link_dest = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# ONE_FS
|
||
if ($var eq 'one_fs') {
|
||
if (!defined($value)) {
|
||
config_err($file_line_num, "$line - one_fs can not be blank");
|
||
next;
|
||
}
|
||
if (!is_boolean($value)) {
|
||
config_err($file_line_num,
|
||
"$line - \"$value\" is not a legal value for one_fs, must be 0 or 1 only");
|
||
next;
|
||
}
|
||
|
||
$one_fs = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# LOCKFILE
|
||
if ($var eq 'lockfile') {
|
||
if (!defined($value)) { config_err($file_line_num, "$line - lockfile can not be blank"); }
|
||
if (0 == is_valid_local_abs_path("$value")) {
|
||
config_err($file_line_num, "$line - lockfile must be a full path");
|
||
next;
|
||
}
|
||
$config_vars{'lockfile'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
#STOP_ON_STALE_LOCKFILE
|
||
if ($var eq 'stop_on_stale_lockfile') {
|
||
if (!defined($value)) {
|
||
config_err($file_line_num, "$line - stop_on_stale_lockfile can not be blank");
|
||
next;
|
||
}
|
||
if (!is_boolean($value)) {
|
||
config_err($file_line_num,
|
||
"$line - \"$value\" is not a legal value for stop_on_stale_lockfile, must be 0 or 1 only");
|
||
next;
|
||
}
|
||
|
||
$stop_on_stale_lockfile = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# INCLUDE
|
||
if ($var eq 'include') {
|
||
if (!defined($rsync_include_args)) {
|
||
$rsync_include_args = "--include=$value";
|
||
}
|
||
else {
|
||
$rsync_include_args .= " --include=$value";
|
||
}
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# EXCLUDE
|
||
if ($var eq 'exclude') {
|
||
if (!defined($rsync_include_args)) {
|
||
$rsync_include_args = "--exclude=$value";
|
||
}
|
||
else {
|
||
$rsync_include_args .= " --exclude=$value";
|
||
}
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# INCLUDE FILE
|
||
if ($var eq 'include_file') {
|
||
if (0 == is_real_local_abs_path($value)) {
|
||
config_err($file_line_num, "$line - include_file $value must be a valid absolute path");
|
||
next;
|
||
}
|
||
elsif (1 == is_directory_traversal($value)) {
|
||
config_err($file_line_num, "$line - Directory traversal attempted in $value");
|
||
next;
|
||
}
|
||
elsif ((-e "$value") && (!-f "$value")) {
|
||
config_err($file_line_num, "$line - include_file $value exists, but is not a file");
|
||
next;
|
||
}
|
||
elsif (!-r "$value") {
|
||
config_err($file_line_num, "$line - include_file $value exists, but is not readable");
|
||
next;
|
||
}
|
||
else {
|
||
if (!defined($rsync_include_file_args)) {
|
||
$rsync_include_file_args = "--include-from=$value";
|
||
}
|
||
else {
|
||
$rsync_include_file_args .= " --include-from=$value";
|
||
}
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
}
|
||
|
||
# EXCLUDE FILE
|
||
if ($var eq 'exclude_file') {
|
||
if (0 == is_real_local_abs_path($value)) {
|
||
config_err($file_line_num, "$line - exclude_file $value must be a valid absolute path");
|
||
next;
|
||
}
|
||
elsif (1 == is_directory_traversal($value)) {
|
||
config_err($file_line_num, "$line - Directory traversal attempted in $value");
|
||
next;
|
||
}
|
||
elsif ((-e "$value") && (!-f "$value")) {
|
||
config_err($file_line_num, "$line - exclude_file $value exists, but is not a file");
|
||
next;
|
||
}
|
||
elsif (!-r "$value") {
|
||
config_err($file_line_num, "$line - exclude_file $value exists, but is not readable");
|
||
next;
|
||
}
|
||
else {
|
||
if (!defined($rsync_include_file_args)) {
|
||
$rsync_include_file_args = "--exclude-from=$value";
|
||
}
|
||
else {
|
||
$rsync_include_file_args .= " --exclude-from=$value";
|
||
}
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
}
|
||
|
||
# RSYNC SHORT ARGS
|
||
if ($var eq 'rsync_short_args') {
|
||
|
||
# must be in the format '-abcde'
|
||
if (0 == is_valid_rsync_short_args($value)) {
|
||
config_err($file_line_num, "$line - rsync_short_args \"$value\" not in correct format");
|
||
next;
|
||
}
|
||
else {
|
||
$config_vars{'rsync_short_args'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
}
|
||
|
||
# RSYNC LONG ARGS
|
||
if ($var eq 'rsync_long_args') {
|
||
$config_vars{'rsync_long_args'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# SSH ARGS
|
||
if ($var eq 'ssh_args') {
|
||
if (!defined($default_ssh_args) && defined($config_vars{'ssh_args'})) {
|
||
config_err($file_line_num,
|
||
"$line - global ssh_args can only be set once, but is already set. Perhaps you wanted to use a per-backup-point ssh_args instead."
|
||
);
|
||
next;
|
||
}
|
||
else {
|
||
$config_vars{'ssh_args'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
}
|
||
|
||
# DU ARGS
|
||
if ($var eq 'du_args') {
|
||
$config_vars{'du_args'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# LVM CMDS
|
||
if ($var =~ m/^linux_lvm_cmd_(lvcreate|mount)$/) {
|
||
$config_vars{$var} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# LVM ARGS
|
||
if ($var =~ m/^linux_lvm_(vgpath|snapshotname|snapshotsize|mountpath)$/) {
|
||
$config_vars{$var} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# LOGFILE
|
||
if ($var eq 'logfile') {
|
||
if (0 == is_valid_local_abs_path($value)) {
|
||
config_err($file_line_num, "$line - logfile must be a valid absolute path");
|
||
next;
|
||
}
|
||
elsif (1 == is_directory_traversal($value)) {
|
||
config_err($file_line_num, "$line - Directory traversal attempted in $value");
|
||
next;
|
||
}
|
||
elsif ((-e "$value") && (!-f "$value") && (!-p "$value")) {
|
||
config_err($file_line_num, "$line - logfile $value exists, but is not a file");
|
||
next;
|
||
}
|
||
else {
|
||
$config_vars{'logfile'} = $value;
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
}
|
||
|
||
# VERBOSE
|
||
if ($var eq 'verbose') {
|
||
if (1 == is_valid_loglevel($value)) {
|
||
if (!defined($verbose)) {
|
||
$verbose = $value;
|
||
} elsif($verbose < $value ) {
|
||
print_warn("The verbosity-level is \"$verbose\" despite subsequent declaration at line $file_line_num.");
|
||
}
|
||
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - verbose must be a value between 1 and 5");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# LOGLEVEL
|
||
if ($var eq 'loglevel') {
|
||
if (1 == is_valid_loglevel($value)) {
|
||
if (!defined($loglevel)) {
|
||
$loglevel = $value;
|
||
}
|
||
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
else {
|
||
config_err($file_line_num, "$line - loglevel must be a value between 1 and 5");
|
||
next;
|
||
}
|
||
}
|
||
|
||
# USE LAZY DELETES
|
||
if ($var eq 'use_lazy_deletes') {
|
||
if (!defined($value)) {
|
||
config_err($file_line_num, "$line - use_lazy_deletes can not be blank");
|
||
next;
|
||
}
|
||
if (!is_boolean($value)) {
|
||
config_err($file_line_num,
|
||
"$line - \"$value\" is not a legal value for use_lazy_deletes, must be 0 or 1 only");
|
||
next;
|
||
}
|
||
|
||
if (1 == $value) { $use_lazy_deletes = 1; }
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# RSYNC NUMBER OF TRIES
|
||
if ($var eq 'rsync_numtries') {
|
||
if (!defined($value)) {
|
||
config_err($file_line_num, "$line - rsync_numtries can not be blank");
|
||
next;
|
||
}
|
||
if (!is_valid_rsync_numtries($value)) {
|
||
config_err($file_line_num,
|
||
"$line - \"$value\" is not a legal value for rsync_numtries, must be greater than or equal to 1");
|
||
next;
|
||
}
|
||
|
||
$rsync_numtries = int($value);
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# RSYNC WAIT BETWEEN TRIES
|
||
if ($var eq 'rsync_wait_between_tries') {
|
||
if (!defined($value)) {
|
||
config_err($file_line_num, "$line - rsync_wait_between_tries can not be blank");
|
||
next;
|
||
}
|
||
|
||
if (!is_integer($value)) {
|
||
config_err($file_line_num, "$line - rsync_wait_between_tries must be an integer");
|
||
next;
|
||
}
|
||
|
||
$rsync_wait_between_tries = int($value);
|
||
if ($rsync_wait_between_tries < 0) {
|
||
config_err($file_line_num,
|
||
"$line - \"$value\" is not a legal value for rsync_wait_between_tries, must be greater than or equal to 0");
|
||
next;
|
||
}
|
||
|
||
$line_syntax_ok = 1;
|
||
next;
|
||
}
|
||
|
||
# make sure we understood this line
|
||
# if not, warn the user, and prevent the program from executing
|
||
# however, don't bother if the user has already been notified
|
||
if (1 == $config_perfect) {
|
||
if (0 == $line_syntax_ok) {
|
||
config_err($file_line_num, $line);
|
||
next;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
sub validate_config_file {
|
||
####################################################################
|
||
# SET SOME SENSIBLE DEFAULTS FOR VALUES THAT MAY NOT HAVE BEEN SET #
|
||
####################################################################
|
||
|
||
# if we didn't manage to get a verbose level yet, either through the config file
|
||
# or the command line, use the default
|
||
if (!defined($verbose)) {
|
||
$verbose = $default_verbose;
|
||
}
|
||
|
||
# same for loglevel
|
||
if (!defined($loglevel)) {
|
||
$loglevel = $default_loglevel;
|
||
}
|
||
|
||
# assemble rsync include/exclude args
|
||
if (defined($rsync_include_args)) {
|
||
if (!defined($config_vars{'rsync_long_args'})) {
|
||
$config_vars{'rsync_long_args'} = $default_rsync_long_args;
|
||
}
|
||
$config_vars{'rsync_long_args'} .= " $rsync_include_args";
|
||
}
|
||
|
||
# assemble rsync include/exclude file args
|
||
if (defined($rsync_include_file_args)) {
|
||
if (!defined($config_vars{'rsync_long_args'})) {
|
||
$config_vars{'rsync_long_args'} = $default_rsync_long_args;
|
||
}
|
||
$config_vars{'rsync_long_args'} .= " $rsync_include_file_args";
|
||
}
|
||
|
||
###############################################
|
||
# NOW THAT THE CONFIG FILE HAS BEEN READ IN, #
|
||
# DO A SANITY CHECK ON THE DATA WE PULLED OUT #
|
||
###############################################
|
||
|
||
# SINS OF COMMISSION
|
||
# (incorrect entries in config file)
|
||
if (0 == $config_perfect) {
|
||
print_err("---------------------------------------------------------------------", 1);
|
||
print_err("Errors were found in $config_file,", 1);
|
||
print_err("rsnapshot can not continue. If you think an entry looks right, make", 1);
|
||
print_err("sure you don't have spaces where only tabs should be.", 1);
|
||
|
||
# if this wasn't a test, report the error to syslog
|
||
if (0 == $do_configtest) {
|
||
syslog_err("Errors were found in $config_file, rsnapshot can not continue.");
|
||
}
|
||
|
||
# exit showing an error
|
||
exit(1);
|
||
}
|
||
|
||
# SINS OF OMISSION
|
||
# (things that should be in the config file that aren't)
|
||
#
|
||
# make sure config_version was set
|
||
if (!defined($config_vars{'config_version'})) {
|
||
print_err("config_version was not defined. rsnapshot can not continue.", 1);
|
||
syslog_err("config_version was not defined. rsnapshot can not continue.");
|
||
exit(1);
|
||
}
|
||
|
||
# make sure rsync was defined
|
||
if (!defined($config_vars{'cmd_rsync'})) {
|
||
print_err("cmd_rsync was not defined.", 1);
|
||
syslog_err("cmd_rsync was not defined.", 1);
|
||
exit(1);
|
||
}
|
||
|
||
# make sure we got a snapshot_root
|
||
if (!defined($config_vars{'snapshot_root'})) {
|
||
print_err("snapshot_root was not defined. rsnapshot can not continue.", 1);
|
||
syslog_err("snapshot_root was not defined. rsnapshot can not continue.");
|
||
exit(1);
|
||
}
|
||
|
||
# make sure we have at least one interval
|
||
if (0 == scalar(@intervals)) {
|
||
print_err("At least one backup level must be set. rsnapshot can not continue.", 1);
|
||
syslog_err("At least one backup level must be set. rsnapshot can not continue.");
|
||
exit(1);
|
||
}
|
||
|
||
# make sure we have at least one backup point
|
||
if (0 == scalar(@backup_points)) {
|
||
print_err("At least one backup point must be set. rsnapshot can not continue.", 1);
|
||
syslog_err("At least one backup point must be set. rsnapshot can not continue.");
|
||
exit(1);
|
||
}
|
||
|
||
# SINS OF CONFUSION
|
||
# (various, specific, undesirable interactions)
|
||
#
|
||
# make sure that we don't have only one copy of the first interval,
|
||
# yet expect rotations on the second interval
|
||
if (scalar(@intervals) > 1) {
|
||
if (defined($intervals[0]->{'number'})) {
|
||
if (1 == $intervals[0]->{'number'}) {
|
||
print_err(
|
||
"Can not have first backup level's retention count set to 1, and have a second backup level", 1);
|
||
syslog_err(
|
||
"Can not have first backup level's retention count set to 1, and have a second backup level");
|
||
exit(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
# make sure that the snapshot_root exists if no_create_root is set to 1
|
||
if (defined($config_vars{'no_create_root'})) {
|
||
if (1 == $config_vars{'no_create_root'}) {
|
||
if (!-d "$config_vars{'snapshot_root'}") {
|
||
if (-e "$config_vars{'snapshot_root'}") {
|
||
print_err("$config_vars{'snapshot_root'} is not a directory.", 1);
|
||
}
|
||
else {
|
||
my $snapshot_root = $config_vars{'snapshot_root'};
|
||
|
||
# Check parent directories until we find one that exists
|
||
while (!-e $snapshot_root) {
|
||
print_err("$snapshot_root does not exist.", 1);
|
||
$snapshot_root =~ m%(.*)/[^/]*%;
|
||
if (defined($1) && $1 ne $snapshot_root) {
|
||
$snapshot_root = $1;
|
||
}
|
||
else {
|
||
last;
|
||
}
|
||
}
|
||
if (-e $snapshot_root && !-d $snapshot_root) {
|
||
print_err("$snapshot_root is not a directory.", 1);
|
||
syslog_err("$snapshot_root is not a directory.");
|
||
}
|
||
}
|
||
print_err("rsnapshot refuses to create snapshot_root when no_create_root is enabled", 1);
|
||
syslog_err("rsnapshot refuses to create snapshot_root when no_create_root is enabled");
|
||
exit(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
# make sure that the user didn't call "sync" if sync_first isn't enabled
|
||
if (($cmd eq 'sync') && (!$config_vars{'sync_first'})) {
|
||
print_err("\"sync_first\" must be enabled for \"sync\" to work", 1);
|
||
syslog_err("\"sync_first\" must be enabled for \"sync\" to work");
|
||
exit(1);
|
||
}
|
||
|
||
# make sure that the backup_script destination paths don't nuke data copied over for backup points
|
||
{
|
||
my @backup_dest = ();
|
||
my @backup_script_dest = ();
|
||
|
||
# remember where the destination paths are...
|
||
foreach my $bp_ref (@backup_points) {
|
||
|
||
# skip for backup_exec since it uses no destination
|
||
next if (defined($$bp_ref{'cmd'}));
|
||
|
||
# backup
|
||
if (defined($$bp_ref{'src'})) {
|
||
push(@backup_dest, $$bp_ref{'dest'});
|
||
|
||
# backup_script
|
||
}
|
||
elsif (defined($$bp_ref{'script'})) {
|
||
push(@backup_script_dest, $$bp_ref{'dest'});
|
||
|
||
}
|
||
|
||
# something else is wrong
|
||
else {
|
||
print_err("logic error in parse_config_file(): a backup point has no src and no script", 1);
|
||
syslog_err("logic error in parse_config_file(): a backup point has no src and no script");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
# loop through and check for conflicts between backup and backup_script destination paths
|
||
foreach my $b_dest (@backup_dest) {
|
||
foreach my $bs_dest (@backup_script_dest) {
|
||
if (defined($b_dest) && defined($bs_dest)) {
|
||
my $tmp_b = $b_dest;
|
||
my $tmp_bs = $bs_dest;
|
||
|
||
# add trailing slashes back in so similarly named directories don't match
|
||
# e.g., localhost/abc/ and localhost/ab/
|
||
$tmp_b .= '/';
|
||
$tmp_bs .= '/';
|
||
|
||
if ("$tmp_b" =~ m/^$tmp_bs/) {
|
||
|
||
# duplicate entries, stop here
|
||
print_err(
|
||
"destination conflict between \"$tmp_b\" and \"$tmp_bs\" in backup / backup_script entries", 1);
|
||
syslog_err(
|
||
"destination conflict between \"$tmp_b\" and \"$tmp_bs\" in backup / backup_script entries");
|
||
exit(1);
|
||
}
|
||
}
|
||
else {
|
||
print_err("logic error in parse_config_file(): unique destination check failed unexpectedly", 1);
|
||
syslog_err("logic error in parse_config_file(): unique destination check failed unexpectedly");
|
||
exit(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
# loop through and check for conflicts between different backup_scripts
|
||
for (my $i = 0; $i < scalar(@backup_script_dest); $i++) {
|
||
for (my $j = 0; $j < scalar(@backup_script_dest); $j++) {
|
||
if ($i != $j) {
|
||
my $path1 = $backup_script_dest[$i];
|
||
my $path2 = $backup_script_dest[$j];
|
||
|
||
# add trailing slashes back in so similarly named directories don't match
|
||
# e.g., localhost/abc/ and localhost/ab/
|
||
$path1 .= '/';
|
||
$path2 .= '/';
|
||
|
||
if (("$path1" =~ m/^$path2/) or ("$path2" =~ m/^$path1/)) {
|
||
print_err(
|
||
"destination conflict between \"$path1\" and \"$path2\" in multiple backup_script entries", 1);
|
||
syslog_err(
|
||
"destination conflict between \"$path1\" and \"$path2\" in multiple backup_script entries");
|
||
exit(1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts a string of options
|
||
# returns an array_ref of parsed options
|
||
# returns undef if there is an invalid option
|
||
#
|
||
# this is for individual backup points only
|
||
sub parse_backup_opts {
|
||
my $opts_str = shift(@_);
|
||
my @pairs;
|
||
my %parsed_opts;
|
||
|
||
# pre-buffer extra rsync arguments
|
||
my $rsync_include_args = undef;
|
||
my $rsync_include_file_args = undef;
|
||
|
||
# make sure we got something (it's quite likely that we didn't)
|
||
if (!defined($opts_str)) { return (undef); }
|
||
if (!$opts_str) { return (undef); }
|
||
|
||
# split on commas first
|
||
@pairs = split(/,/, $opts_str);
|
||
|
||
# then loop through and split on equals
|
||
foreach my $pair (@pairs) {
|
||
my $additive;
|
||
if ($pair =~ /^\+/) {
|
||
$additive = 1;
|
||
$pair =~ s/^.//;
|
||
}
|
||
else {
|
||
$additive = 0;
|
||
}
|
||
|
||
my ($name, $value) = split(/=/, $pair, 2);
|
||
if (!defined($name) or !defined($value)) {
|
||
return (undef);
|
||
}
|
||
|
||
# parameters can't have spaces in them
|
||
$name =~ s/\s+//go;
|
||
|
||
# strip whitespace from both ends
|
||
$value =~ s/^\s{0,}//o;
|
||
$value =~ s/\s{0,}$//o;
|
||
|
||
# ok, it's a name/value pair and it's ready for more validation
|
||
if ($additive) {
|
||
$parsed_opts{'extra_' . $name} = $value;
|
||
}
|
||
else {
|
||
$parsed_opts{$name} = $value;
|
||
}
|
||
|
||
# VALIDATE ARGS
|
||
# one_fs
|
||
if ($name eq 'one_fs') {
|
||
if (!is_boolean($parsed_opts{'one_fs'})) {
|
||
return (undef);
|
||
}
|
||
|
||
# rsync_short_args
|
||
}
|
||
elsif ($name eq 'rsync_short_args') {
|
||
|
||
# must be in the format '-abcde'
|
||
if (0 == is_valid_rsync_short_args($value)) {
|
||
print_err("rsync_short_args \"$value\" not in correct format", 2);
|
||
return (undef);
|
||
}
|
||
|
||
# rsync_long_args
|
||
}
|
||
elsif ($name eq 'rsync_long_args') {
|
||
|
||
# pass unchecked
|
||
|
||
# ssh_args
|
||
}
|
||
elsif ($name eq 'ssh_args') {
|
||
|
||
# pass unchecked
|
||
|
||
# lvm args
|
||
}
|
||
elsif ($name =~ m/^linux_lvm_(vgpath|snapshotname|snapshotsize|mountpath)$/) {
|
||
|
||
# pass unchecked
|
||
|
||
# include
|
||
}
|
||
elsif ($name eq 'include') {
|
||
|
||
# don't validate contents
|
||
# coerce into rsync_include_args
|
||
# then remove the "include" key/value pair
|
||
if (!defined($rsync_include_args)) {
|
||
$rsync_include_args = "--include=$parsed_opts{'include'}";
|
||
}
|
||
else {
|
||
$rsync_include_args .= " --include=$parsed_opts{'include'}";
|
||
}
|
||
|
||
delete($parsed_opts{'include'});
|
||
|
||
# exclude
|
||
}
|
||
elsif ($name eq 'exclude') {
|
||
|
||
# don't validate contents
|
||
# coerce into rsync_include_args
|
||
# then remove the "include" key/value pair
|
||
if (!defined($rsync_include_args)) {
|
||
$rsync_include_args = "--exclude=$parsed_opts{'exclude'}";
|
||
}
|
||
else {
|
||
$rsync_include_args .= " --exclude=$parsed_opts{'exclude'}";
|
||
}
|
||
|
||
delete($parsed_opts{'exclude'});
|
||
|
||
# include_file
|
||
}
|
||
elsif ($name eq 'include_file') {
|
||
|
||
# verify that this file exists and is readable
|
||
if (0 == is_real_local_abs_path($value)) {
|
||
print_err("include_file $value must be a valid absolute path", 2);
|
||
return (undef);
|
||
}
|
||
elsif (1 == is_directory_traversal($value)) {
|
||
print_err("Directory traversal attempted in $value", 2);
|
||
return (undef);
|
||
}
|
||
elsif ((-e "$value") && (!-f "$value")) {
|
||
print_err("include_file $value exists, but is not a file", 2);
|
||
return (undef);
|
||
}
|
||
elsif (!-r "$value") {
|
||
print_err("include_file $value exists, but is not readable", 2);
|
||
return (undef);
|
||
}
|
||
|
||
# coerce into rsync_include_file_args
|
||
# then remove the "include_file" key/value pair
|
||
if (!defined($rsync_include_file_args)) {
|
||
$rsync_include_file_args = "--include-from=$parsed_opts{'include_file'}";
|
||
}
|
||
else {
|
||
$rsync_include_file_args .= " --include-from=$parsed_opts{'include_file'}";
|
||
}
|
||
|
||
delete($parsed_opts{'include_file'});
|
||
|
||
# exclude_file
|
||
}
|
||
elsif ($name eq 'exclude_file') {
|
||
|
||
# verify that this file exists and is readable
|
||
if (0 == is_real_local_abs_path($value)) {
|
||
print_err("exclude_file $value must be a valid absolute path", 2);
|
||
return (undef);
|
||
}
|
||
elsif (1 == is_directory_traversal($value)) {
|
||
print_err("Directory traversal attempted in $value", 2);
|
||
return (undef);
|
||
}
|
||
elsif ((-e "$value") && (!-f "$value")) {
|
||
print_err("exclude_file $value exists, but is not a file", 2);
|
||
return (undef);
|
||
}
|
||
elsif (!-r "$value") {
|
||
print_err("exclude_file $value exists, but is not readable", 2);
|
||
return (undef);
|
||
}
|
||
|
||
# coerce into rsync_include_file_args
|
||
# then remove the "exclude_file" key/value pair
|
||
if (!defined($rsync_include_file_args)) {
|
||
$rsync_include_file_args = "--exclude-from=$parsed_opts{'exclude_file'}";
|
||
}
|
||
else {
|
||
$rsync_include_file_args .= " --exclude-from=$parsed_opts{'exclude_file'}";
|
||
}
|
||
|
||
delete($parsed_opts{'exclude_file'});
|
||
|
||
# Not (yet?) implemented as per-backup-point options
|
||
}
|
||
elsif ($name eq 'cmd_preexec'
|
||
|| $name eq 'cmd_postexec'
|
||
|| $name eq 'cmd_ssh'
|
||
|| $name eq 'cmd_rsync'
|
||
|| $name eq 'verbose'
|
||
|| $name eq 'loglevel') {
|
||
print_err("$name is not implemented as a per-backup-point option in this version of rsnapshot", 2);
|
||
return (undef);
|
||
|
||
}
|
||
|
||
# if we don't know about it, it doesn't exist
|
||
else {
|
||
return (undef);
|
||
}
|
||
}
|
||
|
||
# merge rsync_include_args and rsync_file_include_args in with either $default_rsync_long_args
|
||
# or $parsed_opts{'rsync_long_args'}
|
||
if (defined($rsync_include_args) or defined($rsync_include_file_args)) {
|
||
|
||
# if we never defined rsync_long_args, populate it with the global default
|
||
if (!defined($parsed_opts{'rsync_long_args'})) {
|
||
if (defined($config_vars{'rsync_long_args'})) {
|
||
$parsed_opts{'rsync_long_args'} = $config_vars{'rsync_long_args'};
|
||
}
|
||
else {
|
||
$parsed_opts{'rsync_long_args'} = $default_rsync_long_args;
|
||
}
|
||
}
|
||
|
||
# now we have something in our local rsync_long_args
|
||
# let's concatenate the include/exclude/file stuff to it
|
||
if (defined($rsync_include_args)) {
|
||
$parsed_opts{'rsync_long_args'} .= " $rsync_include_args";
|
||
}
|
||
if (defined($rsync_include_file_args)) {
|
||
$parsed_opts{'rsync_long_args'} .= " $rsync_include_file_args";
|
||
}
|
||
}
|
||
|
||
# if we got anything, return it as an array_ref
|
||
if (%parsed_opts) {
|
||
return (\%parsed_opts);
|
||
}
|
||
|
||
return (undef);
|
||
}
|
||
|
||
# accepts line number, errstr
|
||
# prints a config file error
|
||
# also sets global $config_perfect var off
|
||
sub config_err {
|
||
my $line_num = shift(@_);
|
||
my $errstr = shift(@_);
|
||
|
||
if (!defined($line_num)) { $line_num = -1; }
|
||
if (!defined($errstr)) { $errstr = 'config_err() called without an error string!'; }
|
||
|
||
# show the user the file and line number
|
||
print_err("$config_file on line $line_num:", 1);
|
||
|
||
# print out the offending line
|
||
# don't print past 69 columns (because they all start with 'ERROR: ')
|
||
# similarly, indent subsequent lines 9 spaces to get past the 'ERROR: ' message
|
||
print_err(wrap_cmd($errstr, 69, 9), 1);
|
||
|
||
# invalidate entire config file
|
||
$config_perfect = 0;
|
||
}
|
||
|
||
# accepts an error string
|
||
# prints to STDERR and maybe syslog. removes the lockfile if it exists.
|
||
# exits the program safely and consistently
|
||
sub bail {
|
||
my $str = shift(@_);
|
||
|
||
# print out error
|
||
if ($str) {
|
||
print_err($str, 1);
|
||
}
|
||
|
||
# write to syslog if we're running for real (and we have a message)
|
||
if ((0 == $do_configtest) && (0 == $test) && defined($str) && ('' ne $str)) {
|
||
syslog_err($str);
|
||
}
|
||
|
||
# umount LVM Snapshot if it is mounted
|
||
if (1 == $traps{"linux_lvm_mountpoint"}) {
|
||
$traps{"linux_lvm_mountpoint"} = 0;
|
||
linux_lvm_unmount();
|
||
}
|
||
|
||
# destroy snapshot created by rsnapshot
|
||
if (0 ne $traps{"linux_lvm_snapshot"}) {
|
||
my $tmp = $traps{"linux_lvm_snapshot"};
|
||
$traps{"linux_lvm_snapshot"} = 0;
|
||
linux_lvm_snapshot_del(linux_lvm_parseurl($tmp));
|
||
}
|
||
|
||
# get rid of the lockfile, if it exists
|
||
if (0 == $stop_on_stale_lockfile) {
|
||
remove_lockfile();
|
||
}
|
||
|
||
# exit showing an error
|
||
exit(1);
|
||
}
|
||
|
||
# accepts a string (or an array)
|
||
# prints the string, but separates it across multiple lines with backslashes if necessary
|
||
# also logs the command, but on a single line
|
||
sub print_cmd {
|
||
|
||
# take all arguments and make them into one string
|
||
my $str = join(' ', @_);
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
|
||
# remove newline and consolidate spaces
|
||
chomp($str);
|
||
$str =~ s/\s+/ /g;
|
||
|
||
# write to log (level 3 is where we start showing commands)
|
||
log_msg($str, 3);
|
||
|
||
if (!defined($verbose) or ($verbose >= 3)) {
|
||
print wrap_cmd($str), "\n";
|
||
}
|
||
}
|
||
|
||
# accepts a string
|
||
# wraps the text to fit in 80 columns with backslashes at the end of each wrapping line
|
||
# returns the wrapped string
|
||
sub wrap_cmd {
|
||
my $str = shift(@_);
|
||
my $colmax = shift(@_);
|
||
my $indent = shift(@_);
|
||
|
||
my @tokens;
|
||
my $chars = 0; # character tally
|
||
my $outstr = ''; # string to return
|
||
|
||
# max chars before wrap (default to 80 column terminal)
|
||
if (!defined($colmax)) {
|
||
$colmax = 76;
|
||
}
|
||
|
||
# number of spaces to indent subsequent lines
|
||
if (!defined($indent)) {
|
||
$indent = 4;
|
||
}
|
||
|
||
# break up string into individual pieces
|
||
@tokens = split(/\s+/, $str);
|
||
|
||
# stop here if we don't have anything
|
||
if (0 == scalar(@tokens)) { return (''); }
|
||
|
||
# print the first token as a special exception, since we should never start out by line wrapping
|
||
if (defined($tokens[0])) {
|
||
$chars = (length($tokens[0]) + 1);
|
||
$outstr .= $tokens[0];
|
||
|
||
# don't forget to put the space back in
|
||
if (scalar(@tokens) > 1) {
|
||
$outstr .= ' ';
|
||
}
|
||
}
|
||
|
||
# loop through the rest of the tokens and print them out, wrapping when necessary
|
||
for (my $i = 1; $i < scalar(@tokens); $i++) {
|
||
|
||
# keep track of where we are (plus a space)
|
||
$chars += (length($tokens[$i]) + 1);
|
||
|
||
# wrap if we're at the edge
|
||
if ($chars > $colmax) {
|
||
$outstr .= "\\\n";
|
||
$outstr .= (' ' x $indent);
|
||
|
||
# 4 spaces + string length
|
||
$chars = $indent + length($tokens[$i]);
|
||
}
|
||
|
||
# print out this token
|
||
$outstr .= $tokens[$i];
|
||
|
||
# print out a space unless this is the last one
|
||
if ($i < scalar(@tokens)) {
|
||
$outstr .= ' ';
|
||
}
|
||
}
|
||
|
||
return ($outstr);
|
||
}
|
||
|
||
# accepts string, and level
|
||
# prints string if level is as high as verbose
|
||
# logs string if level is as high as loglevel
|
||
sub print_msg {
|
||
my $str = shift(@_);
|
||
my $level = shift(@_);
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
if (!defined($level)) { $level = 0; }
|
||
|
||
chomp($str);
|
||
|
||
# print to STDOUT
|
||
if ((!defined($verbose)) or ($verbose >= $level)) {
|
||
print $str, "\n";
|
||
}
|
||
|
||
# write to log
|
||
log_msg($str, $level);
|
||
}
|
||
|
||
# accepts string, and level
|
||
# prints string if level is as high as verbose
|
||
# logs string if level is as high as loglevel
|
||
# also raises a warning for the exit code
|
||
sub print_warn {
|
||
my $str = shift(@_);
|
||
my $level = shift(@_);
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
if (!defined($level)) { $level = 0; }
|
||
|
||
# we can no longer say the execution of the program has been error free
|
||
raise_warning();
|
||
|
||
chomp($str);
|
||
|
||
# print to STDERR
|
||
if ((!defined($verbose)) or ($level <= $verbose)) {
|
||
print STDERR 'WARNING: ', $str, "\n";
|
||
}
|
||
|
||
# write to log
|
||
log_warn($str, $level);
|
||
}
|
||
|
||
# accepts string, and level
|
||
# prints string if level is as high as verbose
|
||
# logs string if level is as high as loglevel
|
||
# also raises an error for the exit code
|
||
sub print_err {
|
||
my $str = shift(@_);
|
||
my $level = shift(@_);
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
if (!defined($level)) { $level = 0; }
|
||
|
||
# we can no longer say the execution of the program has been error free
|
||
raise_error();
|
||
|
||
chomp($str);
|
||
|
||
# print the run string once
|
||
# this way we know where the message came from if it's in an e-mail
|
||
# but we can still read messages at the console
|
||
if (0 == $have_printed_run_string) {
|
||
if ((!defined($verbose)) or ($level <= $verbose)) {
|
||
print STDERR "----------------------------------------------------------------------------\n";
|
||
print STDERR "rsnapshot encountered an error! The program was invoked with these options:\n";
|
||
print STDERR wrap_cmd($run_string), "\n";
|
||
print STDERR "----------------------------------------------------------------------------\n";
|
||
}
|
||
|
||
$have_printed_run_string = 1;
|
||
}
|
||
|
||
# print to STDERR
|
||
if ((!defined($verbose)) or ($level <= $verbose)) {
|
||
|
||
#print STDERR $run_string, ": ERROR: ", $str, "\n";
|
||
print STDERR "ERROR: ", $str, "\n";
|
||
}
|
||
|
||
# write to log
|
||
log_err($str, $level);
|
||
}
|
||
|
||
# accepts string, and level
|
||
# logs string if level is as high as loglevel
|
||
sub log_msg {
|
||
my $str = shift(@_);
|
||
my $level = shift(@_);
|
||
my $result = undef;
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
if (!defined($level)) { return (undef); }
|
||
|
||
chomp($str);
|
||
|
||
# if this is just noise, don't log it
|
||
if (defined($loglevel) && ($level > $loglevel)) {
|
||
return (undef);
|
||
}
|
||
|
||
# open logfile, write to it, close it back up
|
||
# if we fail, don't use the usual print_* functions, since they just call this again
|
||
if ((0 == $test) && (0 == $do_configtest)) {
|
||
if (defined($config_vars{'logfile'})) {
|
||
$result = open(LOG, ">> $config_vars{'logfile'}");
|
||
if (!defined($result)) {
|
||
print STDERR "Could not open logfile $config_vars{'logfile'} for writing\n";
|
||
print STDERR "Do you have write permission for this file?\n";
|
||
exit(1);
|
||
}
|
||
|
||
print LOG '[', get_current_date(), '] ', $str, "\n";
|
||
|
||
$result = close(LOG);
|
||
if (!defined($result)) {
|
||
print STDERR "Could not close logfile $config_vars{'logfile'}\n";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts string, and level
|
||
# logs string if level is as high as loglevel
|
||
# also raises a warning for the exit code
|
||
sub log_warn {
|
||
my $str = shift(@_);
|
||
my $level = shift(@_);
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
if (!defined($level)) { return (undef); }
|
||
|
||
# this run is no longer perfect since we have an error
|
||
raise_warning();
|
||
|
||
chomp($str);
|
||
|
||
$str = 'WARNING: ' . $str;
|
||
log_msg($str, $level);
|
||
}
|
||
|
||
# accepts string, and level
|
||
# logs string if level is as high as loglevel
|
||
# also raises an error for the exit code
|
||
sub log_err {
|
||
my $str = shift(@_);
|
||
my $level = shift(@_);
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
if (!defined($level)) { return (undef); }
|
||
|
||
# this run is no longer perfect since we have an error
|
||
raise_error();
|
||
|
||
chomp($str);
|
||
|
||
$str = "$run_string: ERROR: " . $str;
|
||
log_msg($str, $level);
|
||
}
|
||
|
||
# log messages to syslog
|
||
# accepts message, facility, level
|
||
# only message is required
|
||
# return 1 on success, undef on failure
|
||
sub syslog_msg {
|
||
my $msg = shift(@_);
|
||
my $facility = shift(@_);
|
||
my $level = shift(@_);
|
||
my $result = undef;
|
||
|
||
if (!defined($msg)) { return (undef); }
|
||
if (!defined($facility)) { $facility = 'user'; }
|
||
if (!defined($level)) { $level = 'info'; }
|
||
|
||
if (defined($config_vars{'cmd_logger'})) {
|
||
|
||
# print out our call to syslog
|
||
if (defined($verbose) && ($verbose >= 4)) {
|
||
print_cmd("$config_vars{'cmd_logger'} -p $facility.$level -t rsnapshot[$$] $msg");
|
||
}
|
||
|
||
# log to syslog
|
||
if (0 == $test) {
|
||
$result = system($config_vars{'cmd_logger'}, '-p', "$facility.$level", '-t', "rsnapshot[$$]", $msg);
|
||
if (0 != $result) {
|
||
print_warn("Could not log to syslog:", 2);
|
||
print_warn("$config_vars{'cmd_logger'} -p $facility.$level -t rsnapshot[$$] $msg", 2);
|
||
}
|
||
}
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# log warnings to syslog
|
||
# accepts warning message
|
||
# returns 1 on success, undef on failure
|
||
# also raises a warning for the exit code
|
||
sub syslog_warn {
|
||
my $msg = shift(@_);
|
||
|
||
# this run is no longer perfect since we have an error
|
||
raise_warning();
|
||
|
||
return syslog_msg("WARNING: $msg", 'user', 'err');
|
||
}
|
||
|
||
# log errors to syslog
|
||
# accepts error message
|
||
# returns 1 on success, undef on failure
|
||
# also raises an error for the exit code
|
||
sub syslog_err {
|
||
my $msg = shift(@_);
|
||
|
||
# this run is no longer perfect since we have an error
|
||
raise_error();
|
||
|
||
return syslog_msg("$run_string: ERROR: $msg", 'user', 'err');
|
||
}
|
||
|
||
# sets exit code for at least a warning
|
||
sub raise_warning {
|
||
if ($exit_code != 1) {
|
||
$exit_code = 2;
|
||
}
|
||
}
|
||
|
||
# sets exit code for error
|
||
sub raise_error {
|
||
$exit_code = 1;
|
||
}
|
||
|
||
# accepts no arguments
|
||
# returns the current date (for the logfile)
|
||
#
|
||
# there's probably a wonderful module that can do this all for me,
|
||
# but unless it comes standard with perl 5.12.0 and later, i'd rather
|
||
# do it this way :)
|
||
#
|
||
sub get_current_date {
|
||
|
||
# localtime() gives us an array with these elements:
|
||
# 0 = seconds
|
||
# 1 = minutes
|
||
# 2 = hours
|
||
# 3 = day of month
|
||
# 4 = month + 1
|
||
# 5 = year + 1900
|
||
|
||
# example date format (ISO 8601)
|
||
# 2012-04-24T22:30:13 (used to be 28/Feb/2004:23:45:59, like Apache)
|
||
my @fields = localtime(time());
|
||
|
||
return sprintf(
|
||
"%04i-%02i-%02iT%02i:%02i:%02i",
|
||
$fields[5] + 1900,
|
||
$fields[4] + 1,
|
||
$fields[3], $fields[2], $fields[1], $fields[0]
|
||
);
|
||
}
|
||
|
||
# accepts no arguments
|
||
# returns nothing
|
||
# simply prints out a startup message to the logs and STDOUT
|
||
sub log_startup {
|
||
log_msg("$run_string: started", 2);
|
||
}
|
||
|
||
# accepts no arguments
|
||
# returns undef if lockfile isn't defined in the config file, and 1 upon success
|
||
# also, it can make the program exit with 1 as the return value if it can't create the lockfile
|
||
#
|
||
# we don't use bail() to exit on error, because that would remove the
|
||
# lockfile that may exist from another invocation
|
||
#
|
||
# if a lockfile exists, we try to read it (and stop if we can't) to get a PID,
|
||
# then see if that PID exists. If it does, we stop, otherwise we assume it's
|
||
# a stale lock and remove it first.
|
||
sub add_lockfile {
|
||
|
||
# if we don't have a lockfile defined, just return undef
|
||
if (!defined($config_vars{'lockfile'})) {
|
||
return (undef);
|
||
}
|
||
|
||
my $lockfile = $config_vars{'lockfile'};
|
||
|
||
# valid?
|
||
if (0 == is_valid_local_abs_path($lockfile)) {
|
||
print_err("Lockfile $lockfile is not a valid file name", 1);
|
||
syslog_err("Lockfile $lockfile is not a valid file name");
|
||
exit(1);
|
||
}
|
||
|
||
# does a lockfile already exist?
|
||
if (1 == is_real_local_abs_path($lockfile)) {
|
||
if (!open(LOCKFILE, $lockfile)) {
|
||
print_err("Lockfile $lockfile exists and can't be read, can not continue!", 1);
|
||
syslog_err("Lockfile $lockfile exists and can't be read, can not continue");
|
||
exit(1);
|
||
}
|
||
my $pid = <LOCKFILE> || "";
|
||
chomp($pid);
|
||
close(LOCKFILE);
|
||
if ($pid =~ m/^[0-9]+$/ && kill(0, $pid)) {
|
||
print_err("Lockfile $lockfile exists and so does its process, can not continue");
|
||
syslog_err("Lockfile $lockfile exists and so does its process, can not continue");
|
||
exit(1);
|
||
}
|
||
else {
|
||
if (1 == $stop_on_stale_lockfile) {
|
||
print_err("Stale lockfile $lockfile detected. You need to remove it manually to continue", 1);
|
||
syslog_err("Stale lockfile $lockfile detected. Exiting.");
|
||
exit(1);
|
||
}
|
||
else {
|
||
print_warn("Removing stale lockfile $lockfile", 1);
|
||
syslog_warn("Removing stale lockfile $lockfile");
|
||
remove_lockfile();
|
||
}
|
||
}
|
||
}
|
||
|
||
# create the lockfile
|
||
print_cmd("echo $$ > $lockfile");
|
||
|
||
if (0 == $test) {
|
||
|
||
# sysopen() can do exclusive opens, whereas perl open() can not
|
||
my $result = sysopen(LOCKFILE, $lockfile, O_WRONLY | O_EXCL | O_CREAT, 0644);
|
||
if (!defined($result) || 0 == $result) {
|
||
print_err("Could not write lockfile $lockfile: $!", 1);
|
||
syslog_err("Could not write lockfile $lockfile");
|
||
exit(1);
|
||
}
|
||
|
||
# print PID to lockfile
|
||
print LOCKFILE $$;
|
||
|
||
$result = close(LOCKFILE);
|
||
if (!defined($result) || 0 == $result) {
|
||
print_warn("Could not close lockfile $lockfile: $!", 2);
|
||
}
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts no arguments
|
||
#
|
||
# returns undef if lockfile isn't defined in the config file
|
||
# return 1 upon success or if there's no lockfile to remove
|
||
# warn if the PID in the lockfile is not the same as the PID of this process
|
||
# exit with a value of 1 if it can't read the lockfile
|
||
# exit with a value of 1 if it can't remove the lockfile
|
||
#
|
||
# we don't use bail() to exit on error, because that would call
|
||
# this subroutine twice in the event of a failure
|
||
sub remove_lockfile {
|
||
|
||
# if we don't have a lockfile defined, return undef
|
||
if (!defined($config_vars{'lockfile'})) {
|
||
return (undef);
|
||
}
|
||
|
||
my $lockfile = $config_vars{'lockfile'};
|
||
my $result = undef;
|
||
|
||
if (-e "$lockfile") {
|
||
if (open(LOCKFILE, $lockfile)) {
|
||
my $locked_pid = <LOCKFILE> || "";
|
||
chomp($locked_pid);
|
||
close(LOCKFILE);
|
||
if ($locked_pid && $locked_pid != $$) {
|
||
print_warn(
|
||
"About to remove lockfile $lockfile which belongs to a different process: $locked_pid (this is OK if it's a stale lock)"
|
||
);
|
||
}
|
||
}
|
||
else {
|
||
print_err("Could not read lockfile $lockfile: $!", 0);
|
||
syslog_err("Error! Could not read lockfile $lockfile: $!");
|
||
exit(1);
|
||
}
|
||
print_cmd("rm -f $lockfile");
|
||
if (0 == $test) {
|
||
$result = unlink($lockfile);
|
||
if (0 == $result) {
|
||
print_err("Could not remove lockfile $lockfile", 1);
|
||
syslog_err("Error! Could not remove lockfile $lockfile");
|
||
exit(1);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
print_msg("No need to remove non-existent lock $lockfile", 5);
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts no arguments
|
||
# returns nothing
|
||
# sets the locale to POSIX (C) to mitigate some problems with the rmtree() command
|
||
#
|
||
sub set_posix_locale {
|
||
|
||
# set POSIX locale
|
||
# this may fix some potential problems with rmtree()
|
||
# another solution is to enable "cmd_rm" in rsnapshot.conf
|
||
print_msg("Setting locale to POSIX \"C\"", 4);
|
||
setlocale(POSIX::LC_ALL, 'C');
|
||
}
|
||
|
||
# accepts no arguments
|
||
# returns nothing
|
||
# creates the snapshot_root directory (chmod 0700), if it doesn't exist and no_create_root == 0
|
||
sub create_snapshot_root {
|
||
|
||
# attempt to create the directory if it doesn't exist
|
||
if (!-d "$config_vars{'snapshot_root'}") {
|
||
|
||
# make sure no_create_root == 0
|
||
if (defined($config_vars{'no_create_root'})) {
|
||
if (1 == $config_vars{'no_create_root'}) {
|
||
print_err("rsnapshot refuses to create snapshot_root when no_create_root is enabled", 1);
|
||
syslog_err("rsnapshot refuses to create snapshot_root when no_create_root is enabled");
|
||
bail();
|
||
}
|
||
}
|
||
|
||
# actually create the directory
|
||
print_cmd("mkdir -m 0700 -p $config_vars{'snapshot_root'}/");
|
||
if (0 == $test) {
|
||
eval {
|
||
# don't pass a trailing slash to mkpath
|
||
mkpath("$config_vars{'snapshot_root'}", 0, 0700);
|
||
};
|
||
if ($@) {
|
||
bail(
|
||
"Unable to create $config_vars{'snapshot_root'}/,\nPlease make sure you have the right permissions."
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts current interval
|
||
# returns a hash_ref containing information about the intervals
|
||
# exits the program if we don't have good data to work with
|
||
sub get_interval_data {
|
||
my $cur_interval = shift(@_);
|
||
|
||
# make sure we were passed an interval
|
||
if (!defined($cur_interval)) { bail("cur_interval not specified in get_interval_data()\n"); }
|
||
|
||
# the hash to return
|
||
my %hash;
|
||
|
||
# which of the intervals are we operating on?
|
||
# if we defined alpha, beta, gamma ... alpha = 0, beta = 1, gamma = 2
|
||
my $interval_num;
|
||
|
||
# the highest possible number for the current interval context
|
||
# if we are working on alpha, and alpha is set to 6, this would be
|
||
# equal to 5 (since we start at 0)
|
||
my $interval_max;
|
||
|
||
# this is the name of the previous interval, in relation to the one we're
|
||
# working on. e.g., if we're operating on gamma, this should be "beta"
|
||
my $prev_interval;
|
||
|
||
# same as $interval_max, except for the previous interval.
|
||
# this is used to determine which of the previous snapshots to pull from
|
||
# e.g., cp -al alpha.$prev_interval_max/ beta.0/
|
||
my $prev_interval_max;
|
||
|
||
# FIGURE OUT WHICH INTERVAL WE'RE RUNNING, AND HOW IT RELATES TO THE OTHERS
|
||
# THEN RUN THE ACTION FOR THE CHOSEN INTERVAL
|
||
# remember, in each hashref in this loop:
|
||
# "interval" is something like "beta", "gamma", etc.
|
||
# "number" is the number of these intervals to keep on the filesystem
|
||
|
||
my $i = 0;
|
||
foreach my $i_ref (@intervals) {
|
||
|
||
# this is the interval we're set to run
|
||
if ($$i_ref{'interval'} eq $cur_interval) {
|
||
$interval_num = $i;
|
||
|
||
# how many of these intervals should we keep?
|
||
# we start counting from 0, so subtract one
|
||
# e.g., 6 intervals == interval.0 .. interval.5
|
||
$interval_max = $$i_ref{'number'} - 1;
|
||
|
||
# we found our interval, exit the foreach loop
|
||
last;
|
||
}
|
||
|
||
# since the "last" command above breaks from this entire block,
|
||
# and since we loop through the intervals in order, if we got this
|
||
# far in the first place it means that we're looking at an interval
|
||
# which isn't selected to run, and that there will be more intervals in the loop.
|
||
# therefore, this WILL be the previous interval's information, the next time through.
|
||
#
|
||
$prev_interval = $$i_ref{'interval'};
|
||
|
||
# which of the previous interval's numbered directories should we pull from
|
||
# for the interval we're currently set to run?
|
||
# e.g., beta.0/ might get pulled from alpha.6/
|
||
#
|
||
$prev_interval_max = $$i_ref{'number'} - 1;
|
||
|
||
$i++;
|
||
}
|
||
|
||
# make sure we got something that makes sense
|
||
if ($cur_interval ne 'sync') {
|
||
if (!defined($interval_num)) { bail("Interval \"$cur_interval\" unknown, check $config_file"); }
|
||
}
|
||
|
||
# populate our hash
|
||
$hash{'interval'} = $cur_interval;
|
||
$hash{'interval_num'} = $interval_num;
|
||
$hash{'interval_max'} = $interval_max;
|
||
$hash{'prev_interval'} = $prev_interval;
|
||
$hash{'prev_interval_max'} = $prev_interval_max;
|
||
|
||
# and return the values
|
||
return (\%hash);
|
||
}
|
||
|
||
# accepts no arguments
|
||
# prints the most recent snapshot directory and exits
|
||
# this is for use with the get-latest-snapshot command line argument
|
||
sub show_latest_snapshot {
|
||
|
||
# this should only be called after parse_config_file(), but just in case...
|
||
if (!@intervals) { bail("Error! intervals not defined in show_latest_snapshot()"); }
|
||
if (!%config_vars) { bail("Error! config_vars not defined in show_latest_snapshot()"); }
|
||
|
||
# regardless of .sync, this is the latest "real" snapshot
|
||
print $config_vars{'snapshot_root'} . '/' . $intervals[0]->{'interval'} . '.0/' . "\n";
|
||
|
||
exit(0);
|
||
}
|
||
|
||
# accepts no args
|
||
# prints out status to the logs, then exits the program with the current exit code
|
||
sub exit_with_status {
|
||
if (0 == $exit_code) {
|
||
syslog_msg("$run_string: completed successfully");
|
||
log_msg("$run_string: completed successfully", 2);
|
||
exit($exit_code);
|
||
|
||
}
|
||
elsif (1 == $exit_code) {
|
||
syslog_err("$run_string: completed, but with some errors");
|
||
log_err("$run_string: completed, but with some errors", 2);
|
||
exit($exit_code);
|
||
|
||
}
|
||
elsif (2 == $exit_code) {
|
||
syslog_warn("$run_string: completed, but with some warnings");
|
||
log_warn("$run_string: completed, but with some warnings", 2);
|
||
exit($exit_code);
|
||
|
||
}
|
||
|
||
# this should never happen
|
||
else {
|
||
syslog_err("$run_string: completed, but with no definite status");
|
||
log_err("$run_string: completed, but with no definite status", 2);
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
# accepts no arguments
|
||
# returns nothing
|
||
#
|
||
# exits the program with the status of the config file (e.g., Syntax OK).
|
||
# the exit code is 0 for success, 1 for failure (although failure should never happen)
|
||
sub exit_configtest {
|
||
|
||
# if we're just doing a configtest, exit here with the results
|
||
if (1 == $do_configtest) {
|
||
if (1 == $config_perfect) {
|
||
print "Syntax OK\n";
|
||
exit(0);
|
||
|
||
}
|
||
|
||
# this should never happen, because any errors should have killed the program before now
|
||
else {
|
||
print "Syntax Error\n";
|
||
exit(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts no arguments
|
||
# prints out error messages since we can't find the config file
|
||
# exits with a return code of 1
|
||
sub exit_no_config_file {
|
||
|
||
if (-d $config_file) {
|
||
print STDERR "Can't read the config file: \"$config_file\" is a directory.\n";
|
||
if (0 == $do_configtest) {
|
||
syslog_err("Can't read the config file: \"$config_file\" is a directory.\n");
|
||
}
|
||
}
|
||
else {
|
||
# warn that the config file could not be found
|
||
print STDERR "Config file \"$config_file\" does not exist or is not readable.\n";
|
||
if (0 == $do_configtest) {
|
||
syslog_err("Config file \"$config_file\" does not exist or is not readable.");
|
||
}
|
||
}
|
||
|
||
# if we have the default config from the install, remind the user to create the real config
|
||
if ((-e "$config_file.default") && (!-e "$config_file")) {
|
||
print STDERR "Did you copy $config_file.default to $config_file yet?\n";
|
||
}
|
||
|
||
# exit showing an error
|
||
exit(1);
|
||
}
|
||
|
||
# accepts a loglevel
|
||
# returns 1 if it's valid, 0 otherwise
|
||
sub is_valid_loglevel {
|
||
my $value = shift(@_);
|
||
|
||
if (!defined($value)) { return (0); }
|
||
|
||
if ($value =~ m/^\d$/) {
|
||
if (($value >= 1) && ($value <= 5)) {
|
||
return (1);
|
||
}
|
||
}
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts a positive number formatted as string
|
||
# returns 1 if it's valid, 0 otherwise
|
||
sub is_valid_rsync_numtries {
|
||
my $value = shift(@_);
|
||
if (!defined($value)) { return (0); }
|
||
|
||
if ($value =~ m/^\d+$/) {
|
||
if (($value >= 1)) {
|
||
return (1);
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts one argument
|
||
# checks if argument is a integer
|
||
# returns 1 on success, 0 on failure
|
||
sub is_integer {
|
||
my $var = shift(@_);
|
||
|
||
if (!defined($var)) { return (0); }
|
||
if ($var !~ m/^\d+$/) { return (0); }
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts one argument
|
||
# checks to see if that argument is set to 1 or 0
|
||
# returns 1 on success, 0 on failure
|
||
sub is_boolean {
|
||
my $var = shift(@_);
|
||
|
||
if (!defined($var)) { return (0); }
|
||
if ($var !~ m/^\d+$/) { return (0); }
|
||
|
||
if (1 == $var) { return (1); }
|
||
if (0 == $var) { return (1); }
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts string
|
||
# returns 1 if it is a comment line (beginning with #)
|
||
# returns 0 otherwise
|
||
sub is_comment {
|
||
my $str = shift(@_);
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
if ($str =~ m/^#/) { return (1); }
|
||
return (0);
|
||
}
|
||
|
||
# accepts string
|
||
# returns 1 if it is blank, or just pure white space
|
||
# returns 0 otherwise
|
||
sub is_blank {
|
||
my $str = shift(@_);
|
||
|
||
if (!defined($str)) { return (undef); }
|
||
if ($str !~ m/\S/) { return (1); }
|
||
return (0);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a valid ssh path
|
||
# returns 0 otherwise
|
||
sub is_ssh_path {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (undef); }
|
||
|
||
# make sure we don't have leading/trailing spaces
|
||
if ($path =~ m/^\s/) { return (undef); }
|
||
if ($path =~ m/\s$/) { return (undef); }
|
||
|
||
# don't match paths that look like URIs (rsync://, etc.)
|
||
if ($path =~ m,://,) { return (undef); }
|
||
|
||
# must have [user@]host:[~.]/path syntax for ssh
|
||
if ($path =~ m/^(.*?\@)?.*?:[~.]?\/.*$/) { return (1); }
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a valid cwrsync server path (user@host::sharename)
|
||
# return 0 otherwise
|
||
sub is_cwrsync_path {
|
||
my $path = shift(@_);
|
||
if (!defined($path)) { return (undef); }
|
||
if ($path =~ m/^[^\/]+::/) { return (1); }
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a syntactically valid anonymous rsync path
|
||
# returns 0 otherwise
|
||
sub is_anon_rsync_path {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (undef); }
|
||
if ($path =~ m/^rsync:\/\/.*$/) { return (1); }
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a syntactically valid LVM path
|
||
# returns 0 otherwise
|
||
sub is_linux_lvm_path {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (undef); }
|
||
if ($path =~ m|^lvm://.*$|) { return (1); }
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts proposed list for rsync_short_args
|
||
# makes sure that rsync_short_args is in the format '-abcde'
|
||
# (not '-a -b' or '-ab c', etc)
|
||
# returns 1 if it's OK, or 0 otherwise
|
||
sub is_valid_rsync_short_args {
|
||
my $rsync_short_args = shift(@_);
|
||
|
||
if (!defined($rsync_short_args)) { return (0); }
|
||
|
||
# no blank space allowed
|
||
if ($rsync_short_args =~ m/\s/) { return (0); }
|
||
|
||
# first character must be a dash, followed by alphanumeric characters
|
||
if ($rsync_short_args !~ m/^\-{1,1}\w+$/) { return (0); }
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a real absolute path that currently exists
|
||
# returns 0 otherwise
|
||
sub is_real_local_abs_path {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (undef); }
|
||
if (1 == is_valid_local_abs_path($path)) {
|
||
|
||
# check for symlinks first, since they might not link to a real file
|
||
if ((-l "$path") or (-e "$path")) {
|
||
return (1);
|
||
}
|
||
}
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a syntactically valid absolute path
|
||
# returns 0 otherwise
|
||
sub is_valid_local_abs_path {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (undef); }
|
||
if ($path =~ m/^\//) {
|
||
if (0 == is_directory_traversal($path)) {
|
||
return (1);
|
||
}
|
||
}
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a syntactically valid non-absolute (relative) path
|
||
# returns 0 otherwise
|
||
# does not check for directory traversal, since we want to use
|
||
# a different error message if there is ".." in the path
|
||
sub is_valid_local_non_abs_path {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (0); }
|
||
if ($path =~ m/^\//) {
|
||
return (0); # Absolute path => bad
|
||
}
|
||
|
||
if ($path =~ m/^\S/) {
|
||
return (1); # Starts with a non-whitespace => good
|
||
}
|
||
else {
|
||
return (0); # Empty or starts with whitespace => bad
|
||
}
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a directory traversal attempt
|
||
# returns 0 if it's safe
|
||
sub is_directory_traversal {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (undef); }
|
||
|
||
# /..
|
||
if ($path =~ m/\/\.\./) { return (1); }
|
||
|
||
# ../
|
||
if ($path =~ m/\.\.\//) { return (1); }
|
||
return (0);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a file (doesn't have a trailing slash)
|
||
# returns 0 otherwise
|
||
sub is_file {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (undef); }
|
||
|
||
if ($path !~ m/\/$/o) {
|
||
return (1);
|
||
}
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts path
|
||
# returns 1 if it's a directory (has a trailing slash)
|
||
# returns 0 otherwise
|
||
sub is_directory {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { return (undef); }
|
||
|
||
if ($path =~ m/\/$/o) {
|
||
return (1);
|
||
}
|
||
|
||
return (0);
|
||
}
|
||
|
||
# accepts a string with a script file and optional arguments
|
||
# returns 1 if it the script file exists, is executable and has absolute path.
|
||
# returns 0 otherwise
|
||
sub is_valid_script {
|
||
my $full_script = shift(@_); # script to run (including args)
|
||
my $script_ref = shift(@_); # reference to script file name
|
||
my $script; # script file (no args)
|
||
my @script_argv; # all script arguments
|
||
|
||
# get the base name of the script, not counting any arguments to it
|
||
@script_argv = split(/\s+/, $full_script);
|
||
$script = $script_argv[0];
|
||
$$script_ref = $script; # Output $script in case caller wants it
|
||
|
||
# make sure script exists and is executable
|
||
if (-f "$script" && -x "$script" && is_real_local_abs_path($script)) {
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
# accepts string
|
||
# removes trailing slash, returns the string
|
||
sub remove_trailing_slash {
|
||
my $str = shift(@_);
|
||
|
||
# it's not a trailing slash if it's the root filesystem
|
||
if ($str eq '/') { return ($str); }
|
||
|
||
# it's not a trailing slash if it's a remote root filesystem
|
||
if ($str =~ m%:/$%) { return ($str); }
|
||
|
||
$str =~ s/\/+$//;
|
||
|
||
return ($str);
|
||
}
|
||
|
||
# accepts string
|
||
# partially normalizes file path intended to be prefixed with some other path (such as backup dest)
|
||
# does not handle symlinks or '..'
|
||
sub normalize_dest_file_path_part {
|
||
my $str = shift(@_);
|
||
# it's not a trailing slash if it's the root filesystem
|
||
if ($str eq '/') { return ($str); }
|
||
$str =~ s/^\.\/|\/\.\/|\.$/\//g;
|
||
$str =~ s/\/+/\//g;
|
||
return ($str);
|
||
}
|
||
|
||
# accepts the interval (cmd) to run against
|
||
# returns nothing
|
||
# calls the appropriate subroutine, depending on whether this is the lowest interval or a higher one
|
||
# also calls preexec/postexec scripts if we're working on the lowest interval
|
||
#
|
||
sub handle_interval {
|
||
my $cmd = shift(@_);
|
||
|
||
if (!defined($cmd)) { bail('cmd not defined in handle_interval()'); }
|
||
|
||
my $id_ref = get_interval_data($cmd);
|
||
|
||
my $result = 0;
|
||
|
||
# here we used to check for interval.delete directories. This was
|
||
# removed when we switched to using _delete.$$ directories. This
|
||
# was done so that you can run another (eg) rsnapshot alpha, while
|
||
# the .delete directory from the previous alpha backup was still
|
||
# going. Potentially you may have several parallel deletes going on
|
||
# with the new scheme, but I'm pretty sure that you'll catch up
|
||
# eventually and not hopelessly wedge the machine -- DRC
|
||
|
||
# handle toggling between sync_first being enabled and disabled
|
||
|
||
# link_dest is enabled
|
||
if (1 == $link_dest) {
|
||
|
||
# sync_first is enabled
|
||
if ($config_vars{'sync_first'}) {
|
||
|
||
# create the sync root if it doesn't exist (and we need it right now)
|
||
if ($cmd eq 'sync') {
|
||
|
||
# don't create the .sync directory, it gets created later on
|
||
}
|
||
|
||
}
|
||
|
||
# sync_first is disabled
|
||
else {
|
||
|
||
# if the sync directory is still here after sync_first is disabled, delete it
|
||
if (-d "$config_vars{'snapshot_root'}/.sync") {
|
||
|
||
display_rm_rf("$config_vars{'snapshot_root'}/.sync/");
|
||
if (0 == $test) {
|
||
$result = rm_rf("$config_vars{'snapshot_root'}/.sync/");
|
||
if (0 == $result) {
|
||
bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/.sync/\")");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# link_dest is disabled
|
||
else {
|
||
|
||
# sync_first is enabled
|
||
if ($config_vars{'sync_first'}) {
|
||
|
||
# create the sync root if it doesn't exist
|
||
if (!-d "$config_vars{'snapshot_root'}/.sync") {
|
||
|
||
# If .sync does not exist but lowest.0 does, then copy that.
|
||
|
||
# call generic cp_al() subroutine
|
||
my $interval_0 = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0";
|
||
my $sync_dir = "$config_vars{'snapshot_root'}/.sync";
|
||
|
||
if (-d $interval_0) {
|
||
display_cp_al("$interval_0", "$sync_dir");
|
||
if (0 == $test) {
|
||
$result = cp_al("$interval_0", "$sync_dir");
|
||
if (!$result) {
|
||
bail("Error! cp_al(\"$interval_0\", \"$sync_dir\")");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# sync_first is disabled
|
||
else {
|
||
|
||
# if the sync directory still exists, delete it
|
||
if (-d "$config_vars{'snapshot_root'}/.sync") {
|
||
display_rm_rf("$config_vars{'snapshot_root'}/.sync/");
|
||
if (0 == $test) {
|
||
$result = rm_rf("$config_vars{'snapshot_root'}/.sync/");
|
||
if (0 == $result) {
|
||
bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/.sync/\")");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#
|
||
# now that the preliminaries are out of the way, the main backups happen here
|
||
#
|
||
|
||
# backup the lowest interval (or sync content to staging area)
|
||
# we're not sure yet going in whether we'll be doing an actual backup, or just rotating snapshots for the lowest interval
|
||
if ((defined($$id_ref{'interval_num'}) && (0 == $$id_ref{'interval_num'})) or ($cmd eq 'sync')) {
|
||
|
||
# if we're doing a sync, run the pre/post exec scripts, and do the backup
|
||
if ($cmd eq 'sync') {
|
||
exec_cmd_preexec();
|
||
backup_lowest_interval($id_ref);
|
||
exec_cmd_postexec();
|
||
|
||
}
|
||
|
||
# if we're working on the lowest interval, either run the backup and rotate the snapshots, or just rotate them
|
||
# (depending on whether sync_first is enabled
|
||
else {
|
||
if ($config_vars{'sync_first'}) {
|
||
rotate_lowest_snapshots($$id_ref{'interval'});
|
||
}
|
||
else {
|
||
exec_cmd_preexec();
|
||
rotate_lowest_snapshots($$id_ref{'interval'});
|
||
backup_lowest_interval($id_ref);
|
||
exec_cmd_postexec();
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# just rotate the higher intervals
|
||
else {
|
||
|
||
# this is not the most frequent unit, just rotate
|
||
rotate_higher_interval($id_ref);
|
||
}
|
||
|
||
# if use_lazy_delete is on, delete the _delete.$$ directory
|
||
if ($use_lazy_deletes) {
|
||
|
||
# Besides the _delete.$$ directory, the lockfile has to be removed as well.
|
||
# The reason is that the last task to do in this subroutine is to delete the _delete.$$ directory, and it can take quite a while.
|
||
# we remove the lockfile here since this delete shouldn't block other rsnapshot jobs from running
|
||
remove_lockfile();
|
||
|
||
# Check for the directory. It might not exist, e.g. in case of the 'sync' command.
|
||
if (-d "$config_vars{'snapshot_root'}/_delete.$$") {
|
||
|
||
# start the delete
|
||
display_rm_rf("$config_vars{'snapshot_root'}/_delete.$$");
|
||
if (0 == $test) {
|
||
my $result = rm_rf("$config_vars{'snapshot_root'}/_delete.$$");
|
||
if (0 == $result) {
|
||
bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/_delete.$$\")\n");
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
# only spit this out if lazy deletes are turned on.
|
||
# Still need to suppress this if they're turned on but we've
|
||
# not done enough backups to yet need to delete anything
|
||
print_msg("No directory to delete: $config_vars{'snapshot_root'}/_delete.$$", 5);
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts an interval_data_ref
|
||
# acts on the interval defined as $$id_ref{'interval'} (e.g., alpha)
|
||
# this should be the smallest interval (e.g., alpha, not beta)
|
||
#
|
||
# rotates older dirs within this interval, hard links .0 to .1,
|
||
# and rsync data over to .0
|
||
#
|
||
# does not return a value, it bails instantly if there's a problem
|
||
sub backup_lowest_interval {
|
||
my $id_ref = shift(@_);
|
||
|
||
# this should never happen
|
||
if (!defined($id_ref)) { bail('backup_lowest_interval() expects an argument'); }
|
||
if (!defined($$id_ref{'interval'})) { bail('backup_lowest_interval() expects an interval'); }
|
||
|
||
# this also should never happen
|
||
if ($$id_ref{'interval'} ne 'sync') {
|
||
if (!defined($$id_ref{'interval_num'}) or (0 != $$id_ref{'interval_num'})) {
|
||
bail('backup_lowest_interval() can only operate on the lowest interval');
|
||
}
|
||
}
|
||
|
||
my $sync_dest_matches = 0;
|
||
my $sync_dest_dir = undef;
|
||
|
||
# if we're trying to sync only certain directories, remember the path to match
|
||
if ($ARGV[1]) {
|
||
$sync_dest_dir = $ARGV[1];
|
||
}
|
||
|
||
# sync live filesystem data and backup script output to $interval.0
|
||
# loop through each backup point, backup exec, and backup script
|
||
foreach my $bp_ref (@backup_points) {
|
||
|
||
# rsync the given backup point into the snapshot root
|
||
if (defined($$bp_ref{'dest'}) && (defined($$bp_ref{'src'}) or defined($$bp_ref{'script'}))) {
|
||
|
||
# if we're doing a sync and we specified an parameter on the command line (for the destination path),
|
||
# only sync directories matching the destination path
|
||
if (($$id_ref{'interval'} eq 'sync') && (defined($sync_dest_dir))) {
|
||
my $avail_path = remove_trailing_slash($$bp_ref{'dest'});
|
||
my $req_path = remove_trailing_slash($sync_dest_dir);
|
||
|
||
# if we have a match, sync this entry
|
||
if ($avail_path eq $req_path) {
|
||
|
||
# rsync
|
||
if ($$bp_ref{'src'}) {
|
||
rsync_backup_point($$id_ref{'interval'}, $bp_ref);
|
||
|
||
# backup_script
|
||
}
|
||
elsif ($$bp_ref{'script'}) {
|
||
exec_backup_script($$id_ref{'interval'}, $bp_ref);
|
||
}
|
||
|
||
# ok, we got at least one dest match
|
||
$sync_dest_matches++;
|
||
}
|
||
|
||
}
|
||
|
||
# this is a normal operation, either a sync or a lowest interval sync/rotate
|
||
else {
|
||
|
||
# rsync
|
||
if ($$bp_ref{'src'}) {
|
||
rsync_backup_point($$id_ref{'interval'}, $bp_ref);
|
||
|
||
# backup_script
|
||
}
|
||
elsif ($$bp_ref{'script'}) {
|
||
exec_backup_script($$id_ref{'interval'}, $bp_ref);
|
||
}
|
||
}
|
||
|
||
# run a simple command
|
||
}
|
||
elsif (defined($$bp_ref{'cmd'})) {
|
||
my $rc = exec_cmd($$bp_ref{'cmd'});
|
||
if ($rc != 0) {
|
||
if ($$bp_ref{'importance'} eq 'required') {
|
||
bail("\"$$bp_ref{'cmd'}\" returned \"$rc\". Exiting.");
|
||
}
|
||
else {
|
||
print_warn("\"$$bp_ref{'cmd'}\" returned \"$rc\"", 2);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# this should never happen
|
||
else {
|
||
bail('invalid backup point data in backup_lowest_interval()');
|
||
}
|
||
}
|
||
|
||
if ($$id_ref{'interval'} eq 'sync') {
|
||
if (defined($sync_dest_dir) && (0 == $sync_dest_matches)) {
|
||
bail("No matches found for \"$sync_dest_dir\"");
|
||
}
|
||
}
|
||
|
||
# rollback failed backups
|
||
rollback_failed_backups($$id_ref{'interval'});
|
||
|
||
# update mtime on $interval.0/ to show when the snapshot completed
|
||
touch_interval_dir($$id_ref{'interval'});
|
||
}
|
||
|
||
# accepts $interval
|
||
# returns nothing
|
||
#
|
||
# operates on directories in the given interval (it should be the lowest one)
|
||
# deletes the highest numbered directory in the interval, and rotates the ones below it
|
||
# if link_dest is enabled, .0 gets moved to .1
|
||
# otherwise, we do cp -al .0 .1
|
||
#
|
||
# if we encounter an error, this script will terminate the program with an error condition
|
||
#
|
||
sub rotate_lowest_snapshots {
|
||
my $interval = shift(@_);
|
||
|
||
if (!defined($interval)) { bail('interval not defined in rotate_lowest_snapshots()'); }
|
||
|
||
my $id_ref = get_interval_data($interval);
|
||
my $interval_num = $$id_ref{'interval_num'};
|
||
my $interval_max = $$id_ref{'interval_max'};
|
||
my $prev_interval = $$id_ref{'prev_interval'};
|
||
my $prev_interval_max = $$id_ref{'prev_interval_max'};
|
||
|
||
my $result;
|
||
|
||
# cancel if sync_first is enabled but .sync directory not existent
|
||
if (($config_vars{'sync_first'}) && (!-d "$config_vars{'snapshot_root'}/.sync/")) {
|
||
print_err("sync_first is enabled but there is no .sync directory. Refusing to rotate this level ($interval). Run rsnapshot with the ´sync´ command first.", 1);
|
||
syslog_err("sync_first is enabled but there is no .sync directory. Refusing to rotate this level ($interval). Run rsnapshot with the ´sync´ command first.");
|
||
bail();
|
||
}
|
||
|
||
# remove oldest directory
|
||
if ((-d "$config_vars{'snapshot_root'}/$interval.$interval_max") && ($interval_max > 0)) {
|
||
|
||
# if use_lazy_deletes is set move the oldest directory to _delete.$$
|
||
if (1 == $use_lazy_deletes) {
|
||
print_cmd(
|
||
"mv",
|
||
"$config_vars{'snapshot_root'}/$interval.$interval_max/",
|
||
"$config_vars{'snapshot_root'}/_delete.$$/"
|
||
);
|
||
|
||
if (0 == $test) {
|
||
my $result = safe_rename("$config_vars{'snapshot_root'}/$interval.$interval_max",
|
||
"$config_vars{'snapshot_root'}/_delete.$$");
|
||
if (0 == $result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.$interval_max/\", \"";
|
||
$errstr .= "$config_vars{'snapshot_root'}/_delete.$$/\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# otherwise the default is to delete the oldest directory for this interval
|
||
else {
|
||
display_rm_rf("$config_vars{'snapshot_root'}/$interval.$interval_max/");
|
||
|
||
if (0 == $test) {
|
||
my $result = rm_rf("$config_vars{'snapshot_root'}/$interval.$interval_max/");
|
||
if (0 == $result) {
|
||
bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/$interval.$interval_max/\")\n");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# rotate the middle ones
|
||
if ($interval_max > 0) {
|
||
|
||
# Have we rotated a directory for this interval?
|
||
my $dir_rotated = 0;
|
||
for (my $i = ($interval_max - 1); $i > 0; $i--) {
|
||
if (-d "$config_vars{'snapshot_root'}/$interval.$i") {
|
||
print_cmd(
|
||
"mv",
|
||
"$config_vars{'snapshot_root'}/$interval.$i/ ",
|
||
"$config_vars{'snapshot_root'}/$interval." . ($i + 1) . "/"
|
||
);
|
||
|
||
if (0 == $test) {
|
||
my $result = safe_rename(
|
||
"$config_vars{'snapshot_root'}/$interval.$i",
|
||
("$config_vars{'snapshot_root'}/$interval." . ($i + 1))
|
||
);
|
||
if (0 == $result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.$i/\", \"";
|
||
$errstr .= "$config_vars{'snapshot_root'}/$interval." . ($i + 1) . '/' . "\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
$dir_rotated = 1;
|
||
}
|
||
elsif ($dir_rotated) {
|
||
|
||
# We have rotated a directory for this interval, but $i
|
||
# does not exist - that probably means a hole.
|
||
print_msg("Note: $config_vars{'snapshot_root'}/$interval.$i missing, cannot rotate it", 4);
|
||
}
|
||
}
|
||
}
|
||
|
||
# .0 and .1 require more attention, especially now with link_dest and sync_first
|
||
|
||
# sync_first enabled
|
||
if ($config_vars{'sync_first'}) {
|
||
|
||
# we move .0 to .1 no matter what (assuming it exists)
|
||
|
||
if (-d "$config_vars{'snapshot_root'}/$interval.0/") {
|
||
print_cmd(
|
||
"mv",
|
||
"$config_vars{'snapshot_root'}/$interval.0/",
|
||
"$config_vars{'snapshot_root'}/$interval.1/"
|
||
);
|
||
|
||
if (0 == $test) {
|
||
my $result = safe_rename("$config_vars{'snapshot_root'}/$interval.0",
|
||
"$config_vars{'snapshot_root'}/$interval.1");
|
||
if (0 == $result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.0/\", \"";
|
||
$errstr .= "$config_vars{'snapshot_root'}/$interval.1/\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
}
|
||
|
||
# if we're using rsync --link-dest, we need to mv sync to .0 now
|
||
if (1 == $link_dest) {
|
||
|
||
# mv sync .0
|
||
|
||
if (-d "$config_vars{'snapshot_root'}/.sync") {
|
||
print_cmd(
|
||
"mv",
|
||
"$config_vars{'snapshot_root'}/.sync/",
|
||
"$config_vars{'snapshot_root'}/$interval.0/"
|
||
);
|
||
|
||
if (0 == $test) {
|
||
my $result =
|
||
safe_rename("$config_vars{'snapshot_root'}/.sync", "$config_vars{'snapshot_root'}/$interval.0");
|
||
if (0 == $result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/.sync/\", \"";
|
||
$errstr .= "$config_vars{'snapshot_root'}/$interval.0/\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# otherwise, we hard link (except for directories, symlinks, and special files) sync to .0
|
||
else {
|
||
|
||
# cp -al .sync .0
|
||
|
||
if (-d "$config_vars{'snapshot_root'}/.sync/") {
|
||
display_cp_al("$config_vars{'snapshot_root'}/.sync/", "$config_vars{'snapshot_root'}/$interval.0/");
|
||
if (0 == $test) {
|
||
$result = cp_al("$config_vars{'snapshot_root'}/.sync", "$config_vars{'snapshot_root'}/$interval.0");
|
||
if (!$result) {
|
||
bail(
|
||
"Error! cp_al(\"$config_vars{'snapshot_root'}/.sync\", \"$config_vars{'snapshot_root'}/$interval.0\")"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# sync_first disabled (make sure we have a .0 directory and someplace to put it)
|
||
}
|
||
elsif ((-d "$config_vars{'snapshot_root'}/$interval.0") && ($interval_max > 0)) {
|
||
|
||
# if we're using rsync --link-dest, we need to mv .0 to .1 now
|
||
if (1 == $link_dest) {
|
||
|
||
# move .0 to .1
|
||
|
||
if (-d "$config_vars{'snapshot_root'}/$interval.0/") {
|
||
print_cmd(
|
||
"mv $config_vars{'snapshot_root'}/$interval.0/ $config_vars{'snapshot_root'}/$interval.1/");
|
||
|
||
if (0 == $test) {
|
||
my $result = safe_rename("$config_vars{'snapshot_root'}/$interval.0",
|
||
"$config_vars{'snapshot_root'}/$interval.1");
|
||
if (0 == $result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.0/\", ";
|
||
$errstr .= "\"$config_vars{'snapshot_root'}/$interval.1/\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# otherwise, we hard link (except for directories, symlinks, and special files) .0 over to .1
|
||
else {
|
||
|
||
# call generic cp_al() subroutine
|
||
if (-d "$config_vars{'snapshot_root'}/$interval.0/") {
|
||
display_cp_al("$config_vars{'snapshot_root'}/$interval.0",
|
||
"$config_vars{'snapshot_root'}/$interval.1");
|
||
if (0 == $test) {
|
||
$result =
|
||
cp_al("$config_vars{'snapshot_root'}/$interval.0/", "$config_vars{'snapshot_root'}/$interval.1/");
|
||
if (!$result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! cp_al(\"$config_vars{'snapshot_root'}/$interval.0/\", ";
|
||
$errstr .= "\"$config_vars{'snapshot_root'}/$interval.1/\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts interval, backup_point_ref, ssh_rsync_args_ref
|
||
# returns no args
|
||
# runs rsync on the given backup point
|
||
# this is only run on the lowest points, not for rotations
|
||
sub rsync_backup_point {
|
||
my $interval = shift(@_);
|
||
my $bp_ref = shift(@_);
|
||
|
||
# validate subroutine args
|
||
if (!defined($interval)) { bail('interval not defined in rsync_backup_point()'); }
|
||
if (!defined($bp_ref)) { bail('bp_ref not defined in rsync_backup_point()'); }
|
||
if (!defined($$bp_ref{'src'})) { bail('src not defined in rsync_backup_point()'); }
|
||
if (!defined($$bp_ref{'dest'})) { bail('dest not defined in rsync_backup_point()'); }
|
||
|
||
# set up default args for rsync and ssh
|
||
my $ssh_args = $default_ssh_args;
|
||
my $rsync_short_args = $default_rsync_short_args;
|
||
my $rsync_long_args = $default_rsync_long_args;
|
||
|
||
# other misc variables
|
||
my @cmd_stack = undef;
|
||
my $src = $$bp_ref{'src'};
|
||
my $result = undef;
|
||
|
||
my $linux_lvm_oldpwd = undef;
|
||
my $lvm_src = undef;
|
||
|
||
# if we're using link-dest later, that target depends on whether we're doing a 'sync' or a regular interval
|
||
# if we're doing a "sync", then look at [lowest-interval].0 instead of [cur-interval].1
|
||
my $interval_link_dest;
|
||
my $interval_num_link_dest;
|
||
|
||
# start looking for link_dest targets at interval.$start_num
|
||
my $start_num = 1;
|
||
|
||
# if we're doing a sync, we'll start looking at [lowest-interval].0 for a link_dest target
|
||
if ($interval eq 'sync') {
|
||
$start_num = 0;
|
||
}
|
||
|
||
# look for the most recent link_dest target directory
|
||
# loop through all snapshots until we find the first match
|
||
foreach my $i_ref (@intervals) {
|
||
if (defined($$i_ref{'number'})) {
|
||
for (my $i = $start_num; $i < $$i_ref{'number'}; $i++) {
|
||
my $i_check;
|
||
if ($test && $interval ne 'sync') {
|
||
|
||
# A real run would already have rotated the snapshots up, but this test run hasn't.
|
||
# Hence, to know whether $i would exist at this point of a real run, we must check for $i - 1.
|
||
$i_check = $i - 1;
|
||
}
|
||
else {
|
||
$i_check = $i;
|
||
}
|
||
|
||
# once we find a valid link_dest target, the search is over
|
||
if (-e "$config_vars{'snapshot_root'}/$$i_ref{'interval'}.$i_check/$$bp_ref{'dest'}") {
|
||
if (!defined($interval_link_dest) && !defined($interval_num_link_dest)) {
|
||
$interval_link_dest = $$i_ref{'interval'};
|
||
$interval_num_link_dest = $i;
|
||
}
|
||
|
||
# we'll still loop through the outer loop a few more times, but the defined() check above
|
||
# will make sure the first match wins
|
||
last;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# check to see if this destination path has already failed
|
||
# if it's set to be rolled back, skip out now
|
||
foreach my $rollback_point (@rollback_points) {
|
||
if (defined($rollback_point)) {
|
||
my $tmp_dest = $$bp_ref{'dest'};
|
||
my $tmp_rollback_point = $rollback_point;
|
||
|
||
# don't compare the slashes at the end
|
||
$tmp_dest = remove_trailing_slash($tmp_dest);
|
||
$tmp_rollback_point = remove_trailing_slash($tmp_rollback_point);
|
||
|
||
if ("$tmp_dest" eq "$tmp_rollback_point") {
|
||
print_warn("$src skipped due to rollback plan", 2);
|
||
syslog_warn("$src skipped due to rollback plan");
|
||
return (undef);
|
||
}
|
||
}
|
||
}
|
||
|
||
# if the config file specified rsync or ssh args, use those instead of the hard-coded defaults in the program
|
||
if (defined($config_vars{'rsync_short_args'})) {
|
||
$rsync_short_args = $config_vars{'rsync_short_args'};
|
||
}
|
||
if (defined($config_vars{'rsync_long_args'})) {
|
||
$rsync_long_args = $config_vars{'rsync_long_args'};
|
||
}
|
||
if (defined($config_vars{'ssh_args'})) {
|
||
$ssh_args = $config_vars{'ssh_args'};
|
||
}
|
||
|
||
# extra verbose?
|
||
if ($verbose > 3) { $rsync_short_args .= 'v'; }
|
||
|
||
# split up rsync long args into an array, paying attention to
|
||
# quoting - ideally we'd use Text::Balanced or similar, but that's
|
||
# only relatively recently gone into core
|
||
my @rsync_long_args_stack = split_long_args_with_quotes('rsync_long_args', $rsync_long_args);
|
||
|
||
# create $interval.0/$$bp_ref{'dest'} or .sync/$$bp_ref{'dest'} directory if it doesn't exist
|
||
# (this may create the .sync dir, which is why we had to check for it above)
|
||
#
|
||
create_backup_point_dir($interval, $bp_ref);
|
||
|
||
# check opts, first unique to this backup point, and then global
|
||
#
|
||
# with all these checks, we try the local option first, and if
|
||
# that isn't specified, we attempt to use the global setting as
|
||
# a fallback plan
|
||
#
|
||
# we do the rsync args first since they overwrite the rsync_* variables,
|
||
# whereas the subsequent options append to them
|
||
#
|
||
# RSYNC SHORT ARGS
|
||
if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'rsync_short_args'})) {
|
||
$rsync_short_args = $$bp_ref{'opts'}->{'rsync_short_args'};
|
||
}
|
||
if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'extra_rsync_short_args'})) {
|
||
$rsync_short_args .= '-' if (!$rsync_short_args);
|
||
$rsync_short_args .= substr $$bp_ref{'opts'}->{'extra_rsync_short_args'}, 1;
|
||
}
|
||
|
||
# RSYNC LONG ARGS
|
||
if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'rsync_long_args'})) {
|
||
@rsync_long_args_stack = split_long_args_with_quotes('rsync_long_args (for a backup point)',
|
||
$$bp_ref{'opts'}->{'rsync_long_args'});
|
||
}
|
||
if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'extra_rsync_long_args'})) {
|
||
push(
|
||
@rsync_long_args_stack,
|
||
split_long_args_with_quotes(
|
||
'extra_rsync_long_args (for a backup point)',
|
||
$$bp_ref{'opts'}->{'extra_rsync_long_args'}
|
||
)
|
||
);
|
||
}
|
||
|
||
# SSH ARGS
|
||
if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'ssh_args'})) {
|
||
$ssh_args = $$bp_ref{'opts'}->{'ssh_args'};
|
||
}
|
||
if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'extra_ssh_args'})) {
|
||
$ssh_args .= ' ' . $$bp_ref{'opts'}->{'extra_ssh_args'};
|
||
}
|
||
|
||
# ONE_FS
|
||
if (defined($$bp_ref{'opts'}) && defined($$bp_ref{'opts'}->{'one_fs'})) {
|
||
if (1 == $$bp_ref{'opts'}->{'one_fs'}) {
|
||
$rsync_short_args .= 'x';
|
||
}
|
||
}
|
||
elsif ($one_fs) {
|
||
$rsync_short_args .= 'x';
|
||
}
|
||
|
||
# SEE WHAT KIND OF SOURCE WE'RE DEALING WITH
|
||
#
|
||
# local filesystem
|
||
if (is_real_local_abs_path($src)) {
|
||
|
||
# no change
|
||
|
||
# if this is a user@host:/path (or ...:./path, or ...:~/...), use ssh
|
||
}
|
||
elsif (is_ssh_path($src)) {
|
||
|
||
# if we have any args for SSH, add them
|
||
if (defined($ssh_args)) {
|
||
push(@rsync_long_args_stack, "--rsh=$config_vars{'cmd_ssh'} $ssh_args");
|
||
|
||
}
|
||
|
||
# no arguments is the default
|
||
else {
|
||
push(@rsync_long_args_stack, "--rsh=$config_vars{'cmd_ssh'}");
|
||
}
|
||
|
||
# anonymous rsync
|
||
}
|
||
elsif (is_anon_rsync_path($src)) {
|
||
|
||
# make rsync quiet if we're running in quiet mode
|
||
if ($verbose < 2) { $rsync_short_args .= 'q'; }
|
||
|
||
# cwrsync path
|
||
}
|
||
elsif (is_cwrsync_path($src)) {
|
||
|
||
# make rsync quiet if we're running in quiet mode
|
||
if ($verbose < 2) { $rsync_short_args .= 'q'; }
|
||
|
||
# LVM path
|
||
}
|
||
elsif (is_linux_lvm_path($src)) {
|
||
|
||
# take LVM snapshot and mount, reformat src into local path
|
||
|
||
unless (defined($config_vars{'linux_lvm_snapshotsize'})) {
|
||
bail("Missing required argument for LVM source: linux_lvm_snapshotsize");
|
||
}
|
||
unless (defined($config_vars{'linux_lvm_snapshotname'})) {
|
||
bail("Missing required argument for LVM source: linux_lvm_snapshotname");
|
||
}
|
||
unless (defined($config_vars{'linux_lvm_vgpath'})) {
|
||
bail("Missing required argument for LVM source: linux_lvm_vgpath");
|
||
}
|
||
unless (defined($config_vars{'linux_lvm_mountpath'})) {
|
||
bail("Missing required argument for LVM source: linux_lvm_mountpath");
|
||
}
|
||
|
||
$lvm_src = $src;
|
||
|
||
linux_lvm_snapshot_create(linux_lvm_parseurl($lvm_src));
|
||
$traps{"linux_lvm_snapshot"} = $lvm_src;
|
||
linux_lvm_mount(linux_lvm_parseurl($lvm_src));
|
||
$traps{"linux_lvm_mountpoint"} = 1;
|
||
|
||
# rewrite src to point to mount path
|
||
# - to avoid including the mountpath in the snapshot, change the working directory and use a relative source
|
||
$linux_lvm_oldpwd = cwd();
|
||
print_cmd("chdir($config_vars{'linux_lvm_mountpath'})");
|
||
if (0 == $test) {
|
||
$result = chdir($config_vars{'linux_lvm_mountpath'});
|
||
if (0 == $result) {
|
||
bail("Could not change directory to \"$config_vars{'linux_lvm_mountpath'}\"");
|
||
}
|
||
}
|
||
|
||
$src = './' . (linux_lvm_parseurl($lvm_src))[2];
|
||
|
||
}
|
||
|
||
# this should have already been validated once, but better safe than sorry
|
||
else {
|
||
bail("Could not understand source \"$src\" in backup_lowest_interval()");
|
||
}
|
||
|
||
# if we're using --link-dest, we'll need to specify the link-dest directory target
|
||
# this varies depending on whether we're operating on the lowest interval or doing a 'sync'
|
||
if (1 == $link_dest) {
|
||
|
||
# bp_ref{'dest'} and snapshot_root have already been validated, but these might be blank
|
||
if (defined($interval_link_dest) && defined($interval_num_link_dest)) {
|
||
|
||
# push link_dest arguments onto cmd stack
|
||
push(@rsync_long_args_stack,
|
||
"--link-dest=$config_vars{'snapshot_root'}/$interval_link_dest.$interval_num_link_dest/$$bp_ref{'dest'}"
|
||
);
|
||
}
|
||
}
|
||
|
||
# SPECIAL EXCEPTION:
|
||
# If we're using --link-dest AND the source is a file AND we have a copy from the last time,
|
||
# manually link interval.1/foo to interval.0/foo
|
||
#
|
||
# This is necessary because --link-dest only works on directories
|
||
#
|
||
if ( (1 == $link_dest)
|
||
&& (is_file($src))
|
||
&& defined($interval_link_dest)
|
||
&& defined($interval_num_link_dest)
|
||
&& (-f "$config_vars{'snapshot_root'}/$interval_link_dest.$interval_num_link_dest/$$bp_ref{'dest'}")
|
||
) {
|
||
|
||
# these are both "destination" paths, but we're moving from .1 to .0
|
||
my $srcpath;
|
||
my $destpath;
|
||
|
||
$srcpath =
|
||
"$config_vars{'snapshot_root'}/$interval_link_dest.$interval_num_link_dest/$$bp_ref{'dest'}";
|
||
|
||
if ($interval eq 'sync') {
|
||
$destpath = "$config_vars{'snapshot_root'}/.sync/$$bp_ref{'dest'}";
|
||
}
|
||
else {
|
||
$destpath = "$config_vars{'snapshot_root'}/$interval.0/$$bp_ref{'dest'}";
|
||
}
|
||
|
||
print_cmd("ln $srcpath $destpath");
|
||
|
||
if (0 == $test) {
|
||
$result = link("$srcpath", "$destpath");
|
||
|
||
if (!defined($result) or (0 == $result)) {
|
||
print_err("link(\"$srcpath\", \"$destpath\") failed", 2);
|
||
syslog_err("link(\"$srcpath\", \"$destpath\") failed");
|
||
}
|
||
}
|
||
}
|
||
|
||
# put a trailing slash on the source if we know it's a directory and it doesn't have one
|
||
if ((-d "$src") && ($$bp_ref{'src'} !~ /\/$/)) {
|
||
$src .= '/';
|
||
}
|
||
|
||
# BEGIN RSYNC COMMAND ASSEMBLY
|
||
# take care not to introduce blank elements into the array,
|
||
# since it can confuse rsync, which in turn causes strange errors
|
||
#
|
||
@cmd_stack = ();
|
||
#
|
||
# rsync command
|
||
push(@cmd_stack, $config_vars{'cmd_rsync'});
|
||
#
|
||
# rsync short args
|
||
if (defined($rsync_short_args) && ($rsync_short_args ne '')) {
|
||
push(@cmd_stack, $rsync_short_args);
|
||
}
|
||
#
|
||
# rsync long args
|
||
if (@rsync_long_args_stack && (scalar(@rsync_long_args_stack) > 0)) {
|
||
foreach my $tmp_long_arg (@rsync_long_args_stack) {
|
||
if (defined($tmp_long_arg) && ($tmp_long_arg ne '')) {
|
||
push(@cmd_stack, $tmp_long_arg);
|
||
}
|
||
}
|
||
}
|
||
#
|
||
# src
|
||
push(@cmd_stack, "$src");
|
||
#
|
||
# dest
|
||
if ($interval eq 'sync') {
|
||
push(@cmd_stack, "$config_vars{'snapshot_root'}/.sync/$$bp_ref{'dest'}");
|
||
}
|
||
else {
|
||
push(@cmd_stack, "$config_vars{'snapshot_root'}/$interval.0/$$bp_ref{'dest'}");
|
||
}
|
||
#
|
||
# END RSYNC COMMAND ASSEMBLY
|
||
|
||
# RUN THE RSYNC COMMAND FOR THIS BACKUP POINT BASED ON THE @cmd_stack VARS
|
||
print_cmd(@cmd_stack);
|
||
|
||
my $tryCount = 0;
|
||
$result = 1;
|
||
if (0 == $test) {
|
||
while ($tryCount < $rsync_numtries && $result != 0) {
|
||
|
||
if($tryCount > 0 && $rsync_wait_between_tries > 0) {
|
||
print_msg("retrying rsync in $rsync_wait_between_tries seconds", 5);
|
||
sleep($rsync_wait_between_tries);
|
||
}
|
||
|
||
# open rsync and capture STDOUT and STDERR
|
||
# the 3rd argument is undefined, that STDERR gets mashed into STDOUT and we
|
||
# don't have to care about getting both STREAMS together without mixing up time
|
||
my ($rsync_in, $rsync_out);
|
||
my $pid = open3($rsync_in, $rsync_out, undef, @cmd_stack)
|
||
or die "Couldn't fork rsync: $!\n";
|
||
|
||
# add autoflush to get output by time and not at the end when rsync is finished
|
||
$rsync_out->autoflush();
|
||
|
||
while (<$rsync_out>) {
|
||
print_msg($_, 3);
|
||
}
|
||
|
||
waitpid($pid, 0);
|
||
$result = get_retval($?);
|
||
$tryCount += 1;
|
||
}
|
||
|
||
# now we see if rsync ran successfully, and what to do about it
|
||
if ($result != 0) {
|
||
# print warnings, and set this backup point to rollback if we're using --link-dest
|
||
handle_rsync_error($result, $bp_ref);
|
||
}
|
||
else {
|
||
print_msg("rsync succeeded", 5);
|
||
}
|
||
}
|
||
|
||
if (1 == $traps{"linux_lvm_mountpoint"} || 0 ne $traps{"linux_lvm_snapshot"}) {
|
||
print_cmd("chdir($linux_lvm_oldpwd)");
|
||
if (0 == $test) {
|
||
$result = chdir($linux_lvm_oldpwd);
|
||
if (0 == $result) {
|
||
bail("Could not change directory to \"$linux_lvm_oldpwd\"");
|
||
}
|
||
}
|
||
}
|
||
|
||
# delete the traps manually
|
||
# umount LVM Snapshot if it is mounted
|
||
if (1 == $traps{"linux_lvm_mountpoint"}) {
|
||
$traps{"linux_lvm_mountpoint"} = 0;
|
||
linux_lvm_unmount();
|
||
}
|
||
|
||
# destroy snapshot created by rsnapshot
|
||
if (0 ne $traps{"linux_lvm_snapshot"}) {
|
||
$traps{"linux_lvm_snapshot"} = 0;
|
||
linux_lvm_snapshot_del(linux_lvm_parseurl($lvm_src));
|
||
}
|
||
}
|
||
|
||
#
|
||
# split a LVM backup source into vgname volname and path
|
||
#
|
||
# 1. parameter: full LVM source
|
||
#
|
||
# returns: vgname, volname, path as array
|
||
sub linux_lvm_parseurl() {
|
||
my $src = shift @_;
|
||
|
||
# parse LVM src ('lvm://vgname/volname/path')
|
||
my ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath) =
|
||
($src =~ m|^lvm://([^/]+)/([^/]+)/(.*)$|);
|
||
|
||
# lvmvolname and/or path could be the string "0", so test for 'defined':
|
||
unless (defined($linux_lvmvgname) and defined($linux_lvmvolname) and defined($linux_lvmpath)) {
|
||
bail("Could not understand LVM source \"$src\" in linux_lvm_parseurl()");
|
||
}
|
||
return ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath);
|
||
}
|
||
|
||
#
|
||
# assemble and execute LVM snapshot command
|
||
#
|
||
# parameters: the return of linux_lvm_parseurl()
|
||
#
|
||
# returns: -
|
||
sub linux_lvm_snapshot_create {
|
||
|
||
my $result = undef;
|
||
|
||
my ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath) = @_;
|
||
unless (defined($linux_lvmvgname) and defined($linux_lvmvolname) and defined($linux_lvmpath)) {
|
||
bail("linux_lvm_snapshot_create needs 3 parameters!");
|
||
}
|
||
|
||
my @cmd_stack = ();
|
||
push(@cmd_stack, split(' ', $config_vars{'linux_lvm_cmd_lvcreate'}));
|
||
push(@cmd_stack, '--snapshot');
|
||
|
||
push(@cmd_stack, '--size');
|
||
push(@cmd_stack, $config_vars{'linux_lvm_snapshotsize'});
|
||
push(@cmd_stack, '--name');
|
||
push(@cmd_stack, $config_vars{'linux_lvm_snapshotname'});
|
||
|
||
push(@cmd_stack, join('/', $config_vars{'linux_lvm_vgpath'}, $linux_lvmvgname, $linux_lvmvolname));
|
||
|
||
print_cmd(@cmd_stack);
|
||
if (0 == $test) {
|
||
|
||
# silence gratuitous lvcreate output
|
||
#$result = system(@cmd_stack);
|
||
$result = system(join " ", @cmd_stack, ">/dev/null");
|
||
|
||
if ($result != 0) {
|
||
bail("Create LVM snapshot failed: $result");
|
||
}
|
||
}
|
||
}
|
||
|
||
#
|
||
# delete LVM-snapshot
|
||
#
|
||
# parameters: the return of linux_lvm_parseurl()
|
||
#
|
||
# returns: -
|
||
sub linux_lvm_snapshot_del {
|
||
|
||
my $result = undef;
|
||
|
||
my ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath) = @_;
|
||
unless (defined($linux_lvmvgname) and defined($linux_lvmvolname) and defined($linux_lvmpath)) {
|
||
bail("linux_lvm_snapshot_del needs 3 parameters!");
|
||
}
|
||
|
||
my @cmd_stack = ();
|
||
push(@cmd_stack, $config_vars{'linux_lvm_cmd_lvremove'});
|
||
|
||
push(@cmd_stack, '--force');
|
||
push(
|
||
@cmd_stack,
|
||
join('/',
|
||
$config_vars{'linux_lvm_vgpath'},
|
||
$linux_lvmvgname, $config_vars{'linux_lvm_snapshotname'})
|
||
);
|
||
|
||
print_cmd(@cmd_stack);
|
||
if (0 == $test) {
|
||
|
||
# silence gratuitous lvremove output
|
||
#$result = system(@cmd_stack);
|
||
$result = system(join " ", @cmd_stack, ">/dev/null");
|
||
|
||
if ($result != 0) {
|
||
bail("Removal of LVM snapshot failed: $result");
|
||
}
|
||
}
|
||
}
|
||
|
||
#
|
||
# mount a LVM-snapshot
|
||
#
|
||
# parameters: the return of linux_lvm_parseurl()
|
||
#
|
||
# returns: -
|
||
sub linux_lvm_mount {
|
||
|
||
my $result = undef;
|
||
|
||
my ($linux_lvmvgname, $linux_lvmvolname, $linux_lvmpath) = @_;
|
||
unless (defined($linux_lvmvgname) and defined($linux_lvmvolname) and defined($linux_lvmpath)) {
|
||
bail("linux_lvm_mount needs 3 parameters!");
|
||
}
|
||
|
||
# mount the snapshot
|
||
my @cmd_stack = ();
|
||
push(@cmd_stack, split(' ', $config_vars{'linux_lvm_cmd_mount'}));
|
||
|
||
push(
|
||
@cmd_stack,
|
||
join('/',
|
||
$config_vars{'linux_lvm_vgpath'},
|
||
$linux_lvmvgname, $config_vars{'linux_lvm_snapshotname'})
|
||
);
|
||
push(@cmd_stack, $config_vars{'linux_lvm_mountpath'});
|
||
|
||
print_cmd(@cmd_stack);
|
||
if (0 == $test) {
|
||
$result = system(@cmd_stack);
|
||
|
||
if ($result != 0) {
|
||
bail("Mount LVM snapshot failed: $result");
|
||
}
|
||
}
|
||
}
|
||
|
||
#
|
||
# unmount a LVM-snapshot
|
||
#
|
||
# parameters: -
|
||
#
|
||
# returns: -
|
||
sub linux_lvm_unmount {
|
||
|
||
my $result = undef;
|
||
|
||
my @cmd_stack = ();
|
||
push(@cmd_stack, split(' ', $config_vars{'linux_lvm_cmd_umount'}));
|
||
|
||
push(@cmd_stack, $config_vars{'linux_lvm_mountpath'});
|
||
|
||
print_cmd(@cmd_stack);
|
||
if (0 == $test) {
|
||
$result = system(@cmd_stack);
|
||
|
||
if ($result != 0) {
|
||
bail("Unmount LVM snapshot failed: $result");
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts the name of the argument to split, and its value
|
||
# the name is used for spitting out error messages
|
||
#
|
||
# returns a list
|
||
sub split_long_args_with_quotes {
|
||
my ($argname, $argvalue) = @_;
|
||
my $inquotes = '';
|
||
my @stack = ('');
|
||
for (my $i = 0; $i < length($argvalue); $i++) {
|
||
my $thischar = substr($argvalue, $i, 1);
|
||
|
||
# got whitespace and not in quotes? end this argument, start next
|
||
if ($thischar =~ /\s/ && !$inquotes) {
|
||
$#stack++;
|
||
next;
|
||
|
||
# not in quotes and got a quote? remember that we're in quotes
|
||
# NB the unnecessary \ are to appease emacs
|
||
}
|
||
elsif ($thischar =~ /[\'\"]/ && !$inquotes) {
|
||
$inquotes = $thischar;
|
||
|
||
# in quotes and got a different quote? no nesting allowed
|
||
# more emacs appeasement
|
||
}
|
||
elsif ($thischar =~ /[\'\"]/ && $inquotes ne $thischar) {
|
||
print_err("Nested quotes not allowed in $argname", 1);
|
||
syslog_err("Nested quotes not allowed in $argname");
|
||
exit(1);
|
||
|
||
# in quotes and got a close quote
|
||
}
|
||
elsif ($thischar eq $inquotes) {
|
||
$inquotes = '';
|
||
}
|
||
else {
|
||
$stack[-1] .= $thischar;
|
||
}
|
||
}
|
||
if ($inquotes) {
|
||
print_err("Unbalanced quotes in $argname", 1);
|
||
syslog_err("Unbalanced quotes in $argname");
|
||
exit(1);
|
||
}
|
||
return @stack;
|
||
}
|
||
|
||
# accepts rsync exit code, backup_point_ref
|
||
# prints out an appropriate error message (and logs it)
|
||
# also adds destination path to the rollback queue if link_dest is enabled
|
||
sub handle_rsync_error {
|
||
my $retval = shift(@_);
|
||
my $bp_ref = shift(@_);
|
||
|
||
# shouldn't ever happen
|
||
if (!defined($retval)) { bail('retval undefined in handle_rsync_error()'); }
|
||
if (0 == $retval) { bail('retval == 0 in handle_rsync_error()'); }
|
||
if (!defined($bp_ref)) { bail('bp_ref undefined in handle_rsync_error()'); }
|
||
|
||
# a partial list of rsync exit values (from the rsync 2.6.0 man page)
|
||
#
|
||
# 0 Success
|
||
# 1 Syntax or usage error
|
||
# 23 Partial transfer due to error
|
||
# 24 Partial transfer due to vanished source files
|
||
#
|
||
# if we got error 1 and we were attempting --link-dest, there's
|
||
# a very good chance that this version of rsync is too old.
|
||
#
|
||
if ((1 == $link_dest) && (1 == $retval)) {
|
||
print_err(
|
||
"$config_vars{'cmd_rsync'} syntax or usage error. Does this version of rsync support --link-dest?",
|
||
2
|
||
);
|
||
syslog_err(
|
||
"$config_vars{'cmd_rsync'} syntax or usage error. Does this version of rsync support --link-dest?");
|
||
|
||
# 23 and 24 are treated as warnings because users might be using the filesystem during the backup
|
||
# if you want perfect backups, don't allow the source to be modified while the backups are running :)
|
||
}
|
||
elsif (23 == $retval) {
|
||
print_warn(
|
||
"Some files and/or directories in $$bp_ref{'src'} only transferred partially during rsync operation",
|
||
2
|
||
);
|
||
syslog_warn(
|
||
"Some files and/or directories in $$bp_ref{'src'} only transferred partially during rsync operation"
|
||
);
|
||
|
||
}
|
||
elsif (24 == $retval) {
|
||
print_warn("Some files and/or directories in $$bp_ref{'src'} vanished during rsync operation", 4);
|
||
syslog_warn("Some files and/or directories in $$bp_ref{'src'} vanished during rsync operation");
|
||
|
||
}
|
||
|
||
# other error
|
||
else {
|
||
print_err("$config_vars{'cmd_rsync'} returned $retval while processing $$bp_ref{'src'}", 2);
|
||
syslog_err("$config_vars{'cmd_rsync'} returned $retval while processing $$bp_ref{'src'}");
|
||
|
||
# set this directory to rollback if we're using link_dest
|
||
# (since $interval.0/ will have been moved to $interval.1/ by now)
|
||
if (1 == $link_dest) {
|
||
push(@rollback_points, $$bp_ref{'dest'});
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts interval, backup_point_ref, ssh_rsync_args_ref
|
||
# returns no args
|
||
# runs rsync on the given backup point
|
||
sub exec_backup_script {
|
||
my $interval = shift(@_);
|
||
my $bp_ref = shift(@_);
|
||
|
||
# validate subroutine args
|
||
if (!defined($interval)) { bail('interval not defined in exec_backup_script()'); }
|
||
if (!defined($bp_ref)) { bail('bp_ref not defined in exec_backup_script()'); }
|
||
|
||
# other misc variables
|
||
my $script = undef;
|
||
my $tmpdir = undef;
|
||
my $result = undef;
|
||
|
||
# remember what directory we started in
|
||
my $cwd = cwd();
|
||
|
||
# create $interval.0/$$bp_ref{'dest'} directory if it doesn't exist
|
||
#
|
||
create_backup_point_dir($interval, $bp_ref);
|
||
|
||
# work in a temp dir, and make this the source for the rsync operation later
|
||
# not having a trailing slash is a subtle distinction. it allows us to use
|
||
# the same path if it's NOT a directory when we try to delete it.
|
||
$tmpdir = "$config_vars{'snapshot_root'}/tmp";
|
||
|
||
# remove the tmp directory if it's still there for some reason
|
||
# (this shouldn't happen unless the program was killed prematurely, etc)
|
||
if (-e "$tmpdir") {
|
||
display_rm_rf("$tmpdir/");
|
||
|
||
if (0 == $test) {
|
||
$result = rm_rf("$tmpdir/");
|
||
if (0 == $result) {
|
||
bail("Could not rm_rf(\"$tmpdir/\");");
|
||
}
|
||
}
|
||
}
|
||
|
||
# create the tmp directory
|
||
print_cmd("mkdir -m 0755 -p $tmpdir/");
|
||
|
||
if (0 == $test) {
|
||
eval {
|
||
# don't ever pass a trailing slash to mkpath
|
||
mkpath("$tmpdir", 0, 0755);
|
||
};
|
||
if ($@) {
|
||
bail("Unable to create \"$tmpdir/\",\nPlease make sure you have the right permissions.");
|
||
}
|
||
}
|
||
|
||
# no more calls to mkpath here. the tmp dir needs a trailing slash
|
||
$tmpdir .= '/';
|
||
|
||
# change to the tmp directory
|
||
print_cmd("cd $tmpdir");
|
||
|
||
if (0 == $test) {
|
||
$result = chdir("$tmpdir");
|
||
if (0 == $result) {
|
||
bail("Could not change directory to \"$tmpdir\"");
|
||
}
|
||
}
|
||
|
||
# run the backup script
|
||
#
|
||
# the assumption here is that the backup script is written in such a way
|
||
# that it creates files in its current working directory.
|
||
#
|
||
# the backup script should return 0 on success, anything else is
|
||
# considered a failure.
|
||
#
|
||
print_cmd($$bp_ref{'script'});
|
||
|
||
if (0 == $test) {
|
||
$result = system($$bp_ref{'script'});
|
||
if ($result != 0) {
|
||
|
||
# bitmask return value
|
||
my $retval = get_retval($result);
|
||
|
||
print_err("backup_script $$bp_ref{'script'} returned $retval", 2);
|
||
syslog_err("backup_script $$bp_ref{'script'} returned $retval");
|
||
|
||
# if the backup script failed, roll back to the last good data
|
||
push(@rollback_points, $$bp_ref{'dest'});
|
||
}
|
||
}
|
||
|
||
# change back to the previous directory
|
||
# (/ is a special case)
|
||
if ('/' eq $cwd) {
|
||
print_cmd("cd $cwd");
|
||
}
|
||
else {
|
||
print_cmd("cd $cwd/");
|
||
}
|
||
|
||
if (0 == $test) {
|
||
chdir($cwd);
|
||
}
|
||
|
||
# if we're using link_dest, pull back the previous files (as links) that were moved up if any.
|
||
# this is because in this situation, .0 will always be empty, so we'll pull select things
|
||
# from .1 back to .0 if possible. these will be used as a baseline for diff comparisons by
|
||
# sync_if_different() down below.
|
||
if (1 == $link_dest) {
|
||
my $lastdir;
|
||
my $curdir;
|
||
|
||
if ($interval eq 'sync') {
|
||
$lastdir = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0/$$bp_ref{'dest'}";
|
||
$curdir = "$config_vars{'snapshot_root'}/.sync/$$bp_ref{'dest'}";
|
||
}
|
||
else {
|
||
$lastdir = "$config_vars{'snapshot_root'}/$interval.1/$$bp_ref{'dest'}";
|
||
$curdir = "$config_vars{'snapshot_root'}/$interval.0/$$bp_ref{'dest'}";
|
||
}
|
||
|
||
# make sure we have a slash at the end
|
||
if ($lastdir !~ m/\/$/) {
|
||
$lastdir .= '/';
|
||
}
|
||
if ($curdir !~ m/\/$/) {
|
||
$curdir .= '/';
|
||
}
|
||
|
||
# if we even have files from last time
|
||
if (-e "$lastdir") {
|
||
|
||
# and we're not somehow clobbering an existing directory (shouldn't happen)
|
||
if (!-e "$curdir") {
|
||
|
||
# call generic cp_al() subroutine
|
||
display_cp_al("$lastdir", "$curdir");
|
||
if (0 == $test) {
|
||
$result = cp_al("$lastdir", "$curdir");
|
||
if (!$result) {
|
||
print_err("Warning! cp_al(\"$lastdir\", \"$curdir/\")", 2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# sync the output of the backup script into this snapshot interval
|
||
# this is using a native function since rsync doesn't quite do what we want
|
||
#
|
||
# rsync doesn't work here because it sees that the timestamps are different, and
|
||
# insists on changing things even if the files are bit for bit identical on content.
|
||
#
|
||
# check to see where we're syncing to
|
||
my $target_dir;
|
||
if ($interval eq 'sync') {
|
||
$target_dir = "$config_vars{'snapshot_root'}/.sync/$$bp_ref{'dest'}";
|
||
}
|
||
else {
|
||
$target_dir = "$config_vars{'snapshot_root'}/$interval.0/$$bp_ref{'dest'}";
|
||
}
|
||
|
||
print_cmd("sync_if_different(\"$tmpdir\", \"$target_dir\")");
|
||
|
||
if (0 == $test) {
|
||
$result = sync_if_different("$tmpdir", "$target_dir");
|
||
if (!defined($result)) {
|
||
print_err("Warning! sync_if_different(\"$tmpdir\", \"$$bp_ref{'dest'}\") returned undef", 2);
|
||
}
|
||
}
|
||
|
||
# remove the tmp directory
|
||
if (-e "$tmpdir") {
|
||
display_rm_rf("$tmpdir");
|
||
|
||
if (0 == $test) {
|
||
$result = rm_rf("$tmpdir");
|
||
if (0 == $result) {
|
||
bail("Could not rm_rf(\"$tmpdir\");");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts and runs an arbitrary command string
|
||
# returns the exit value of the command
|
||
sub exec_cmd {
|
||
my $cmd = shift(@_);
|
||
|
||
my $return = 0;
|
||
my $retval = 0;
|
||
|
||
if (!defined($cmd) or ('' eq $cmd)) {
|
||
print_err("Warning! Command \"$cmd\" not found", 2);
|
||
return (undef);
|
||
}
|
||
|
||
print_cmd($cmd);
|
||
if (0 == $test) {
|
||
my $pre_systemcall_cwd = cwd();
|
||
|
||
# run $cmd from $HOME, allows unmounting of the snapshot root by
|
||
# cmd_postexec config option (se Debian Bug #660372)
|
||
chdir();
|
||
$return = system($cmd);
|
||
|
||
# return to the directory we were in before executing $cmd
|
||
chdir($pre_systemcall_cwd);
|
||
|
||
if (!defined($return)) {
|
||
print_err("Warning! exec_cmd(\"$cmd\") returned undef", 2);
|
||
}
|
||
|
||
# bitmask to get the real return value
|
||
$retval = get_retval($return);
|
||
}
|
||
|
||
return ($retval);
|
||
}
|
||
|
||
# accepts no arguments
|
||
# returns the exit code of the defined preexec script, or undef if the command is not found
|
||
sub exec_cmd_preexec {
|
||
my $retval = 0;
|
||
|
||
# exec_cmd will only run if we're not in test mode
|
||
if (defined($config_vars{'cmd_preexec'})) {
|
||
$retval = exec_cmd("$config_vars{'cmd_preexec'}");
|
||
}
|
||
|
||
if (!defined($retval)) {
|
||
print_err("$config_vars{'cmd_preexec'} not found", 2);
|
||
}
|
||
|
||
if (0 != $retval) {
|
||
bail("cmd_preexec \"$config_vars{'cmd_preexec'}\" returned $retval");
|
||
}
|
||
|
||
return ($retval);
|
||
}
|
||
|
||
# accepts no arguments
|
||
# returns the exit code of the defined preexec script, or undef if the command is not found
|
||
sub exec_cmd_postexec {
|
||
my $retval = 0;
|
||
|
||
# exec_cmd will only run if we're not in test mode
|
||
if (defined($config_vars{'cmd_postexec'})) {
|
||
$retval = exec_cmd("$config_vars{'cmd_postexec'}");
|
||
}
|
||
|
||
if (!defined($retval)) {
|
||
print_err("$config_vars{'cmd_postexec'} not found", 2);
|
||
}
|
||
|
||
if (0 != $retval) {
|
||
bail("cmd_postexec \"$config_vars{'cmd_postexec'}\" returned $retval");
|
||
}
|
||
|
||
return ($retval);
|
||
}
|
||
|
||
# accepts interval, backup_point_ref
|
||
# returns nothing
|
||
# exits the program if it encounters a fatal error
|
||
sub create_backup_point_dir {
|
||
my $interval = shift(@_);
|
||
my $bp_ref = shift(@_);
|
||
|
||
# validate subroutine args
|
||
if (!defined($interval)) { bail('interval not defined in create_interval_0()'); }
|
||
if (!defined($bp_ref)) { bail('bp_ref not defined in create_interval_0()'); }
|
||
|
||
# create missing parent directories inside the $interval.x directory
|
||
my @dirs = split(/\//, $$bp_ref{'dest'});
|
||
pop(@dirs);
|
||
|
||
# don't mkdir for dest unless we have to
|
||
my $destpath;
|
||
if ($interval eq 'sync') {
|
||
$destpath = "$config_vars{'snapshot_root'}/.sync/" . join('/', @dirs);
|
||
}
|
||
else {
|
||
$destpath = "$config_vars{'snapshot_root'}/$interval.0/" . join('/', @dirs);
|
||
}
|
||
|
||
# make sure we DON'T have a trailing slash (for mkpath)
|
||
if ($destpath =~ m/\/$/) {
|
||
$destpath = remove_trailing_slash($destpath);
|
||
}
|
||
|
||
# create the directory if it doesn't exist
|
||
if (!-e "$destpath") {
|
||
print_cmd("mkdir -m 0755 -p $destpath/");
|
||
|
||
if (0 == $test) {
|
||
eval { mkpath("$destpath", 0, 0755); };
|
||
if ($@) {
|
||
bail("Could not mkpath(\"$destpath/\", 0, 0755);");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts interval we're operating on
|
||
# returns nothing important
|
||
# rolls back failed backups, as defined in the @rollback_points array
|
||
# this is necessary if we're using link_dest, since it moves the .0 to .1 directory,
|
||
# instead of recursively copying links to the files. it also helps with failed
|
||
# backup scripts.
|
||
#
|
||
sub rollback_failed_backups {
|
||
my $interval = shift(@_);
|
||
|
||
if (!defined($interval)) { bail('interval not defined in rollback_failed_backups()'); }
|
||
|
||
my $result;
|
||
my $rsync_short_args = $default_rsync_short_args;
|
||
|
||
# handle 'sync' case
|
||
my $interval_src;
|
||
my $interval_dest;
|
||
|
||
if ($interval eq 'sync') {
|
||
$interval_src = $intervals[0]->{'interval'} . '.0';
|
||
$interval_dest = '.sync';
|
||
}
|
||
else {
|
||
$interval_src = "$interval.1";
|
||
$interval_dest = "$interval.0";
|
||
}
|
||
|
||
# extra verbose?
|
||
if ($verbose > 3) { $rsync_short_args .= 'v'; }
|
||
|
||
# rollback failed backups (if we're using link_dest)
|
||
foreach my $rollback_point (@rollback_points) {
|
||
|
||
# make sure there's something to rollback from
|
||
if (!-e "$config_vars{'snapshot_root'}/$interval_src/$rollback_point") {
|
||
next;
|
||
}
|
||
|
||
print_warn("Rolling back \"$rollback_point\"", 2);
|
||
syslog_warn("Rolling back \"$rollback_point\"");
|
||
|
||
# using link_dest, this probably won't happen
|
||
# just in case, we may have to delete the old backup point from interval.0 / .sync
|
||
if (-e "$config_vars{'snapshot_root'}/$interval_dest/$rollback_point") {
|
||
display_rm_rf("$config_vars{'snapshot_root'}/$interval_dest/$rollback_point");
|
||
if (0 == $test) {
|
||
$result = rm_rf("$config_vars{'snapshot_root'}/$interval_dest/$rollback_point");
|
||
if (0 == $result) {
|
||
bail("Error! rm_rf(\"$config_vars{'snapshot_root'}/$interval_dest/$rollback_point\")\n");
|
||
}
|
||
}
|
||
}
|
||
|
||
# copy hard links back from .1 to .0
|
||
# this will re-populate the .0 directory without taking up (much) additional space
|
||
#
|
||
# if we're doing a 'sync', then instead of .1 and .0, it's lowest.0 and .sync
|
||
display_cp_al(
|
||
"$config_vars{'snapshot_root'}/$interval_src/$rollback_point",
|
||
"$config_vars{'snapshot_root'}/$interval_dest/$rollback_point"
|
||
);
|
||
if (0 == $test) {
|
||
$result = cp_al(
|
||
"$config_vars{'snapshot_root'}/$interval_src/$rollback_point",
|
||
"$config_vars{'snapshot_root'}/$interval_dest/$rollback_point"
|
||
);
|
||
if (!$result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! cp_al(\"$config_vars{'snapshot_root'}/$interval_src/$rollback_point\", ";
|
||
$errstr .= "\"$config_vars{'snapshot_root'}/$interval_dest/$rollback_point\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts interval
|
||
# returns nothing
|
||
# updates mtime on $interval.0
|
||
sub touch_interval_dir {
|
||
my $interval = shift(@_);
|
||
|
||
if (!defined($interval)) { bail('interval not defined in touch_interval()'); }
|
||
|
||
my $interval_dir;
|
||
|
||
if ($interval eq 'sync') {
|
||
$interval_dir = '.sync';
|
||
}
|
||
else {
|
||
$interval_dir = $interval . '.0';
|
||
}
|
||
|
||
# update mtime of $interval.0 to reflect the time this snapshot was taken
|
||
print_cmd("touch $config_vars{'snapshot_root'}/$interval_dir/");
|
||
|
||
if (0 == $test && -e "$config_vars{'snapshot_root'}/$interval_dir/") {
|
||
my $result = utime(time(), time(), "$config_vars{'snapshot_root'}/$interval_dir/");
|
||
if (0 == $result) {
|
||
bail("Could not utime(time(), time(), \"$config_vars{'snapshot_root'}/$interval_dir/\");");
|
||
}
|
||
}
|
||
}
|
||
|
||
# accepts an interval_data_ref
|
||
# looks at $$id_ref{'interval'} as the interval to act on,
|
||
# and the previous interval $$id_ref{'prev_interval'} to pull up the directory from (e.g., beta, alpha)
|
||
# the interval being acted upon should not be the lowest one.
|
||
#
|
||
# rotates older dirs within this interval, and hard links
|
||
# the previous interval's highest numbered dir to this interval's .0,
|
||
#
|
||
# does not return a value, it bails instantly if there's a problem
|
||
sub rotate_higher_interval {
|
||
my $id_ref = shift(@_);
|
||
|
||
# this should never happen
|
||
if (!defined($id_ref)) { bail('rotate_higher_interval() expects an interval_data_ref'); }
|
||
|
||
# this also should never happen
|
||
if (!defined($$id_ref{'interval_num'}) or (0 == $$id_ref{'interval_num'})) {
|
||
bail('rotate_higher_interval() can only operate on the higher intervals');
|
||
}
|
||
|
||
# set up variables for convenience since we refer to them extensively
|
||
my $interval = $$id_ref{'interval'};
|
||
my $interval_num = $$id_ref{'interval_num'};
|
||
my $interval_max = $$id_ref{'interval_max'};
|
||
my $prev_interval = $$id_ref{'prev_interval'};
|
||
my $prev_interval_max = $$id_ref{'prev_interval_max'};
|
||
|
||
# here we check, if $prev_interval.$prev_interval_max exists and only do the rotation, if from that level can be pulled...
|
||
# otherwise bail. Maybe there should be an option for such new behaviour, too?
|
||
if (!-d "$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max") {
|
||
print_err("Did not find previous interval max ($config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max), refusing to rotate this level ($interval)", 1);
|
||
syslog_err("Did not find previous interval max ($config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max), refusing to rotate this level ($interval)");
|
||
bail();
|
||
}
|
||
|
||
# ROTATE DIRECTORIES
|
||
#
|
||
# delete the oldest one (if we're keeping more than one)
|
||
if (-d "$config_vars{'snapshot_root'}/$interval.$interval_max") {
|
||
|
||
# if use_lazy_deletes is set move the oldest directory to _delete.$$
|
||
# otherwise perform the default behavior
|
||
if (1 == $use_lazy_deletes) {
|
||
print_cmd(
|
||
"mv ",
|
||
"$config_vars{'snapshot_root'}/$interval.$interval_max/ ",
|
||
"$config_vars{'snapshot_root'}/_delete.$$/"
|
||
);
|
||
|
||
if (0 == $test) {
|
||
my $result = safe_rename(
|
||
"$config_vars{'snapshot_root'}/$interval.$interval_max",
|
||
("$config_vars{'snapshot_root'}/_delete.$$")
|
||
);
|
||
if (0 == $result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.$interval_max/\", \"";
|
||
$errstr .= "$config_vars{'snapshot_root'}/_delete.$$/\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
display_rm_rf("$config_vars{'snapshot_root'}/$interval.$interval_max/");
|
||
|
||
if (0 == $test) {
|
||
my $result = rm_rf("$config_vars{'snapshot_root'}/$interval.$interval_max/");
|
||
if (0 == $result) {
|
||
bail("Could not rm_rf(\"$config_vars{'snapshot_root'}/$interval.$interval_max/\");");
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
else {
|
||
print_msg(
|
||
"$config_vars{'snapshot_root'}/$interval.$interval_max not present (yet), nothing to delete", 4);
|
||
}
|
||
|
||
# rotate the middle ones
|
||
for (my $i = ($interval_max - 1); $i >= 0; $i--) {
|
||
if (-d "$config_vars{'snapshot_root'}/$interval.$i") {
|
||
print_cmd(
|
||
"mv $config_vars{'snapshot_root'}/$interval.$i/ ",
|
||
"$config_vars{'snapshot_root'}/$interval." . ($i + 1) . "/"
|
||
);
|
||
|
||
if (0 == $test) {
|
||
my $result = safe_rename(
|
||
"$config_vars{'snapshot_root'}/$interval.$i",
|
||
("$config_vars{'snapshot_root'}/$interval." . ($i + 1))
|
||
);
|
||
if (0 == $result) {
|
||
my $errstr = '';
|
||
$errstr .= "Error! safe_rename(\"$config_vars{'snapshot_root'}/$interval.$i/\", \"";
|
||
$errstr .= "$config_vars{'snapshot_root'}/$interval." . ($i + 1) . '/' . "\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
print_msg("$config_vars{'snapshot_root'}/$interval.$i not present (yet), nothing to rotate", 4);
|
||
}
|
||
}
|
||
|
||
# prev.max and interval.0 require more attention
|
||
if (-d "$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max") {
|
||
my $result;
|
||
|
||
# if the previous interval has at least 2 snapshots,
|
||
# or if the previous interval isn't the smallest one,
|
||
# move the last one up a level
|
||
if (($prev_interval_max >= 1) or ($interval_num >= 2)) {
|
||
|
||
# mv alpha.5 to beta.0 (or whatever intervals we're using)
|
||
print_cmd("mv $config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max/ ",
|
||
"$config_vars{'snapshot_root'}/$interval.0/");
|
||
|
||
if (0 == $test) {
|
||
$result = safe_rename("$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max",
|
||
"$config_vars{'snapshot_root'}/$interval.0");
|
||
if (0 == $result) {
|
||
my $errstr = '';
|
||
$errstr .=
|
||
"Error! safe_rename(\"$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max/\", ";
|
||
$errstr .= "\"$config_vars{'snapshot_root'}/$interval.0/\")";
|
||
bail($errstr);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
print_err("$prev_interval must be above 1 to keep snapshots at the $interval level", 1);
|
||
exit(1);
|
||
}
|
||
}
|
||
else {
|
||
print_msg(
|
||
"$config_vars{'snapshot_root'}/$prev_interval.$prev_interval_max not present (yet), nothing to copy",
|
||
2
|
||
);
|
||
}
|
||
}
|
||
|
||
# accepts src, dest
|
||
# prints out the cp -al command that would be run, based on config file data
|
||
sub display_cp_al {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
|
||
# remove trailing slashes (for newer versions of GNU cp)
|
||
$src = remove_trailing_slash($src);
|
||
$dest = remove_trailing_slash($dest);
|
||
|
||
if (!defined($src)) { bail('src not defined in display_cp_al()'); }
|
||
if (!defined($dest)) { bail('dest not defined in display_cp_al()'); }
|
||
|
||
if (defined($config_vars{'cmd_cp'})) {
|
||
print_cmd("$config_vars{'cmd_cp'} -al $src $dest");
|
||
}
|
||
else {
|
||
print_cmd("native_cp_al(\"$src\", \"$dest\")");
|
||
}
|
||
}
|
||
|
||
# stub subroutine
|
||
# calls either gnu_cp_al() or native_cp_al()
|
||
# returns the value directly from whichever subroutine it calls
|
||
# also prints out what's happening to the screen, if appropriate
|
||
sub cp_al {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
my $result = 0;
|
||
|
||
# use gnu cp if we have it
|
||
if (defined($config_vars{'cmd_cp'})) {
|
||
$result = gnu_cp_al("$src", "$dest");
|
||
|
||
}
|
||
|
||
# fall back to the built-in native perl replacement, followed by an rsync clean-up step
|
||
else {
|
||
|
||
# native cp -al
|
||
$result = native_cp_al("$src", "$dest");
|
||
if (1 != $result) {
|
||
return ($result);
|
||
}
|
||
|
||
# rsync clean-up
|
||
$result = rsync_cleanup_after_native_cp_al("$src", "$dest");
|
||
}
|
||
|
||
return ($result);
|
||
}
|
||
|
||
# This is to test whether cp -al seems to work in a simple case
|
||
# return 0 if cp -al succeeds
|
||
# return 1 if cp -al fails
|
||
# return -1 if something else failed - test inconclusive
|
||
sub test_cp_al {
|
||
my $s = "$config_vars{'snapshot_root'}/cp_al1";
|
||
my $d = "$config_vars{'snapshot_root'}/cp_al2";
|
||
my $result;
|
||
|
||
-d $s || mkdir($s) || return (-1);
|
||
open(TT1, ">>$s/tt1") || return (-1);
|
||
close(TT1) || return (-1);
|
||
$result = system($config_vars{'cmd_cp'}, '-al', "$s", "$d");
|
||
if ($result != 0) {
|
||
return (1);
|
||
}
|
||
unlink("$d/tt1");
|
||
unlink("$s/tt1");
|
||
rmdir($d);
|
||
rmdir($s);
|
||
return (0);
|
||
}
|
||
|
||
# this is a wrapper to call the GNU version of "cp"
|
||
# it might fail in mysterious ways if you have a different version of "cp"
|
||
#
|
||
sub gnu_cp_al {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
my $result = 0;
|
||
my $status;
|
||
|
||
# make sure we were passed two arguments
|
||
if (!defined($src)) { return (0); }
|
||
if (!defined($dest)) { return (0); }
|
||
|
||
# remove trailing slashes (for newer versions of GNU cp)
|
||
$src = remove_trailing_slash($src);
|
||
$dest = remove_trailing_slash($dest);
|
||
|
||
if (!-d "$src") {
|
||
print_err("gnu_cp_al() needs a valid directory as an argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# make the system call to GNU cp
|
||
$result = system($config_vars{'cmd_cp'}, '-al', "$src", "$dest");
|
||
if ($result != 0) {
|
||
$status = $result >> 8;
|
||
print_err("$config_vars{'cmd_cp'} -al $src $dest failed (result $result, exit status $status).", 2);
|
||
if (test_cp_al() > 0) {
|
||
print_err("Perhaps your cp does not support -al options?", 2);
|
||
}
|
||
return (0);
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# This is a purpose built, native perl replacement for GNU "cp -al".
|
||
# However, it is not quite as good. it does not copy "special" files:
|
||
# block, char, fifo, or sockets.
|
||
# Never the less, it does do regular files, directories, and symlinks
|
||
# which should be enough for 95% of the normal cases.
|
||
# If you absolutely have to have snapshots of FIFOs, etc, just get GNU
|
||
# cp on your system, and specify it in the config file.
|
||
#
|
||
# Please note that more recently, this subroutine is followed up by
|
||
# an rsync clean-up step. This combination effectively removes most of
|
||
# the limitations of this technique.
|
||
#
|
||
# In the great perl tradition, this returns 1 on success, 0 on failure.
|
||
#
|
||
sub native_cp_al {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
my $dh = undef;
|
||
my $result = 0;
|
||
|
||
# make sure we were passed two arguments
|
||
if (!defined($src)) { return (0); }
|
||
if (!defined($dest)) { return (0); }
|
||
|
||
# make sure we have a source directory
|
||
if (!-d "$src") {
|
||
print_err("native_cp_al() needs a valid source directory as an argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# strip trailing slashes off the directories,
|
||
# since we'll add them back on later
|
||
$src = remove_trailing_slash($src);
|
||
$dest = remove_trailing_slash($dest);
|
||
|
||
# LSTAT SRC
|
||
my $st = lstat("$src");
|
||
if (!defined($st)) {
|
||
print_err("Warning! Could not lstat source dir (\"$src\") : $!", 2);
|
||
return (0);
|
||
}
|
||
|
||
# MKDIR DEST (AND SET MODE)
|
||
if (!-d "$dest") {
|
||
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "mkdir(\"$dest\", " . get_perms($st->mode) . ")";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
|
||
$result = mkdir("$dest", $st->mode);
|
||
if (!$result) {
|
||
print_err("Warning! Could not mkdir(\"$dest\", $st->mode) : $!", 2);
|
||
return (0);
|
||
}
|
||
}
|
||
|
||
# CHOWN DEST (if root)
|
||
if (0 == $<) {
|
||
|
||
# make sure destination is not a symlink
|
||
if (!-l "$dest") {
|
||
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\")";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
|
||
$result = safe_chown($st->uid, $st->gid, "$dest");
|
||
if (!$result) {
|
||
print_err("Warning! Could not safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\");", 2);
|
||
return (0);
|
||
}
|
||
}
|
||
}
|
||
|
||
# READ DIR CONTENTS
|
||
$dh = new DirHandle("$src");
|
||
if (defined($dh)) {
|
||
my @nodes = $dh->read();
|
||
|
||
# loop through all nodes in this dir
|
||
foreach my $node (@nodes) {
|
||
|
||
# skip '.' and '..'
|
||
next if ($node =~ m/^\.\.?$/o);
|
||
|
||
# make sure the node we just got is valid (this is highly unlikely to fail)
|
||
my $st = lstat("$src/$node");
|
||
if (!defined($st)) {
|
||
print_err("Warning! Could not lstat source node (\"$src/$node\") : $!", 2);
|
||
next;
|
||
}
|
||
|
||
# SYMLINK (must be tested for first, because it will also pass the file and dir tests)
|
||
if (-l "$src/$node") {
|
||
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "copy_symlink(\"$src/$node\", \"$dest/$node\")";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
|
||
$result = copy_symlink("$src/$node", "$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! copy_symlink(\"$src/$node\", \"$dest/$node\")", 2);
|
||
next;
|
||
}
|
||
|
||
# FILE
|
||
}
|
||
elsif (-f "$src/$node") {
|
||
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "link(\"$src/$node\", \"$dest/$node\");";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
|
||
# make a hard link
|
||
$result = link("$src/$node", "$dest/$node");
|
||
if (!$result) {
|
||
print_err("Warning! Could not link(\"$src/$node\", \"$dest/$node\") : $!", 2);
|
||
next;
|
||
}
|
||
|
||
# DIRECTORY
|
||
}
|
||
elsif (-d "$src/$node") {
|
||
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "native_cp_al(\"$src/$node\", \"$dest/$node\")";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
|
||
# call this subroutine recursively, to create the directory
|
||
$result = native_cp_al("$src/$node", "$dest/$node");
|
||
if (!$result) {
|
||
print_err("Warning! Recursion error in native_cp_al(\"$src/$node\", \"$dest/$node\")", 2);
|
||
next;
|
||
}
|
||
}
|
||
|
||
## rsync_cleanup_after_native_cp_al() will take care of the files we can't handle here
|
||
#
|
||
## FIFO
|
||
#} elsif ( -p "$src/$node" ) {
|
||
# # print_err("Warning! Ignoring FIFO $src/$node", 2);
|
||
#
|
||
## SOCKET
|
||
#} elsif ( -S "$src/$node" ) {
|
||
# # print_err("Warning! Ignoring socket: $src/$node", 2);
|
||
#
|
||
## BLOCK DEVICE
|
||
#} elsif ( -b "$src/$node" ) {
|
||
# # print_err("Warning! Ignoring special block file: $src/$node", 2);
|
||
#
|
||
## CHAR DEVICE
|
||
#} elsif ( -c "$src/$node" ) {
|
||
# # print_err("Warning! Ignoring special character file: $src/$node", 2);
|
||
#}
|
||
}
|
||
|
||
}
|
||
else {
|
||
print_err("Could not open \"$src\". Do you have adequate permissions?", 2);
|
||
return (0);
|
||
}
|
||
|
||
# close open dir handle
|
||
if (defined($dh)) { $dh->close(); }
|
||
undef($dh);
|
||
|
||
# UTIME DEST
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "utime(" . $st->atime . ", " . $st->mtime . ", \"$dest\");";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
$result = utime($st->atime, $st->mtime, "$dest");
|
||
if (!$result) {
|
||
print_err("Warning! Could not set utime(" . $st->atime . ", " . $st->mtime . ", \"$dest\") : $!",
|
||
2);
|
||
return (0);
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# If we're using native_cp_al(), it can't transfer special files.
|
||
# So, to make sure no one misses out, this subroutine gets called every time directly
|
||
# after native_cp_al(), with the same source and destinations paths.
|
||
#
|
||
# Essentially it is running between two almost identical hard linked directory trees.
|
||
# However, it will transfer over the few (if any) special files that were otherwise
|
||
# missed.
|
||
#
|
||
# This subroutine specifies its own parameters for rsync's arguments. This is to make
|
||
# sure that nothing goes wrong, since there is not much here that should be left to
|
||
# interpretation.
|
||
#
|
||
sub rsync_cleanup_after_native_cp_al {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
|
||
my $local_rsync_short_args = '-a';
|
||
|
||
# if the user asked for -E, we should use it here too.
|
||
# should we check for OS X? Dunno, but for now that extra
|
||
# check is in here as we know we need it there, and so
|
||
# this is the smallest change for the smallest number of
|
||
# people
|
||
$local_rsync_short_args .= 'E'
|
||
if ( defined($config_vars{'rsync_short_args'})
|
||
&& $config_vars{'rsync_short_args'} =~ /E/
|
||
&& $^O eq 'darwin');
|
||
|
||
my @cmd_stack = ();
|
||
|
||
# make sure we were passed two arguments
|
||
if (!defined($src)) { return (0); }
|
||
if (!defined($dest)) { return (0); }
|
||
|
||
# make sure we have a source directory
|
||
if (!-d "$src") {
|
||
print_err("rsync_cleanup_after_native_cp_al() needs a valid source directory as an argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# make sure we have a destination directory
|
||
if (!-d "$dest") {
|
||
print_err("rsync_cleanup_after_native_cp_al() needs a valid destination directory as an argument",
|
||
2);
|
||
return (0);
|
||
}
|
||
|
||
# make sure src and dest both have a trailing slash for rsync
|
||
$src =~ s/\/?$/\//;
|
||
$dest =~ s/\/?$/\//;
|
||
|
||
# check verbose settings and modify rsync's short args accordingly
|
||
if ($verbose > 3) { $local_rsync_short_args .= 'v'; }
|
||
|
||
# setup rsync command
|
||
#
|
||
# rsync
|
||
push(@cmd_stack, $config_vars{'cmd_rsync'});
|
||
#
|
||
# short args
|
||
push(@cmd_stack, $local_rsync_short_args);
|
||
#
|
||
# long args (not the defaults)
|
||
push(@cmd_stack, '--delete');
|
||
push(@cmd_stack, '--numeric-ids');
|
||
#
|
||
# src
|
||
push(@cmd_stack, "$src");
|
||
#
|
||
# dest
|
||
push(@cmd_stack, "$dest");
|
||
|
||
print_cmd(@cmd_stack);
|
||
|
||
if (0 == $test) {
|
||
my $result = system(@cmd_stack);
|
||
|
||
if ($result != 0) {
|
||
|
||
# bitmask return value
|
||
my $retval = get_retval($result);
|
||
|
||
# a partial list of rsync exit values
|
||
# 0 Success
|
||
# 23 Partial transfer due to error
|
||
# 24 Partial transfer due to vanished source files
|
||
|
||
if (23 == $retval) {
|
||
print_warn(
|
||
"Some files and/or directories in $src only transferred partially during rsync_cleanup_after_native_cp_al operation",
|
||
2
|
||
);
|
||
syslog_warn(
|
||
"Some files and/or directories in $src only transferred partially during rsync_cleanup_after_native_cp_al operation"
|
||
);
|
||
}
|
||
elsif (24 == $retval) {
|
||
print_warn(
|
||
"Some files and/or directories in $src vanished during rsync_cleanup_after_native_cp_al operation",
|
||
2
|
||
);
|
||
syslog_warn(
|
||
"Some files and/or directories in $src vanished during rsync_cleanup_after_native_cp_al operation");
|
||
|
||
}
|
||
else {
|
||
|
||
# other error
|
||
bail("rsync returned error $retval in rsync_cleanup_after_native_cp_al()");
|
||
}
|
||
}
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts a path
|
||
# displays the rm command according to the config file
|
||
sub display_rm_rf {
|
||
my $path = shift(@_);
|
||
|
||
if (!defined($path)) { bail('display_rm_rf() requires an argument'); }
|
||
|
||
if (defined($config_vars{'cmd_rm'})) {
|
||
print_cmd("$config_vars{'cmd_rm'} -rf $path");
|
||
}
|
||
else {
|
||
print_cmd("rm -rf $path");
|
||
}
|
||
}
|
||
|
||
# stub subroutine
|
||
# calls either cmd_rm_rf() or the native perl rmtree()
|
||
# returns 1 on success, 0 on failure
|
||
sub rm_rf {
|
||
my $path = shift(@_);
|
||
my $result = 0;
|
||
|
||
# make sure we were passed an argument
|
||
if (!defined($path)) { return (0); }
|
||
|
||
# extra bonus safety feature!
|
||
# confirm that whatever we're deleting must be inside the snapshot_root
|
||
if (index($path, $config_vars{'snapshot_root'}) != 0) {
|
||
bail("rm_rf() tried to delete something outside of $config_vars{'snapshot_root'}! Quitting now!");
|
||
}
|
||
|
||
# use the rm command if we have it
|
||
if (defined($config_vars{'cmd_rm'})) {
|
||
$result = cmd_rm_rf("$path");
|
||
|
||
}
|
||
|
||
# fall back on rmtree()
|
||
else {
|
||
|
||
# remove trailing slash just in case
|
||
$path =~ s/\/$//;
|
||
$result = rmtree("$path", 0, 0);
|
||
}
|
||
|
||
return ($result);
|
||
}
|
||
|
||
# this is a wrapper to the "rm" program, called with the "-rf" flags.
|
||
sub cmd_rm_rf {
|
||
my $path = shift(@_);
|
||
my $result = 0;
|
||
|
||
# make sure we were passed an argument
|
||
if (!defined($path)) { return (0); }
|
||
|
||
if (!-e "$path") {
|
||
print_err("cmd_rm_rf() needs a valid file path as an argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# make the system call to /bin/rm
|
||
$result = system($config_vars{'cmd_rm'}, '-rf', "$path");
|
||
if ($result != 0) {
|
||
print_err("Warning! $config_vars{'cmd_rm'} failed.", 2);
|
||
return (0);
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts no arguments
|
||
# calls the 'du' command to show rsnapshot's disk usage
|
||
# exits the program with 0 for success, 1 for failure
|
||
#
|
||
# this subroutine isn't like a lot of the "real" ones that write to logfiles, etc.
|
||
# that's why the print_* subroutines aren't used here.
|
||
#
|
||
sub show_disk_usage {
|
||
my @du_dirs = ();
|
||
my $cmd_du = 'du';
|
||
my $du_args = $default_du_args;
|
||
my $dest_path = '';
|
||
my $retval;
|
||
|
||
# first, make sure we have permission to see the snapshot root
|
||
if (!-r "$config_vars{'snapshot_root'}") {
|
||
print STDERR ("ERROR: Permission denied\n");
|
||
exit(1);
|
||
}
|
||
|
||
# check for 'du' program
|
||
if (defined($config_vars{'cmd_du'})) {
|
||
|
||
# it was specified in the config file, use that version
|
||
$cmd_du = $config_vars{'cmd_du'};
|
||
}
|
||
|
||
# check for du args
|
||
if (defined($config_vars{'du_args'})) {
|
||
|
||
# it this was specified in the config file, use that version
|
||
$du_args = $config_vars{'du_args'};
|
||
}
|
||
|
||
# are we looking in subdirectories or at files?
|
||
if (defined($ARGV[1])) {
|
||
$dest_path = $ARGV[1];
|
||
|
||
# consolidate multiple slashes
|
||
$dest_path =~ s/\/+/\//o;
|
||
|
||
if (is_directory_traversal($dest_path)) {
|
||
print STDERR "ERROR: Directory traversal is not allowed\n";
|
||
exit(1);
|
||
}
|
||
if (!is_valid_local_non_abs_path($dest_path)) {
|
||
print STDERR "ERROR: Full paths are not allowed\n";
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
# find the directories to look through, in order
|
||
# only add them to the list if we have read permissions
|
||
if (-r "$config_vars{'snapshot_root'}/") {
|
||
|
||
# if we have a .sync directory, that will have the most recent files, and should be first
|
||
if (-d "$config_vars{'snapshot_root'}/.sync") {
|
||
if (-r "$config_vars{'snapshot_root'}/.sync") {
|
||
push(@du_dirs, "$config_vars{'snapshot_root'}/.sync");
|
||
}
|
||
}
|
||
|
||
# loop through the intervals, most recent to oldest
|
||
foreach my $interval_ref (@intervals) {
|
||
my $interval = $$interval_ref{'interval'};
|
||
my $max_interval_num = $$interval_ref{'number'};
|
||
|
||
for (my $i = 0; $i < $max_interval_num; $i++) {
|
||
if (-r "$config_vars{'snapshot_root'}/$interval.$i/$dest_path") {
|
||
push(@du_dirs, "$config_vars{'snapshot_root'}/$interval.$i/$dest_path");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# if we can see any of the intervals, find out how much space they're taking up
|
||
# most likely we can either see all of them or none at all
|
||
if (scalar(@du_dirs) > 0) {
|
||
my @cmd_stack = ($cmd_du, split_long_args_with_quotes('du_args', $du_args), @du_dirs);
|
||
if (defined($verbose) && ($verbose >= 3)) {
|
||
print wrap_cmd(join(' ', @cmd_stack)), "\n\n";
|
||
}
|
||
|
||
if (0 == $test) {
|
||
$retval = system(@cmd_stack);
|
||
if (0 == $retval) {
|
||
|
||
# exit showing success
|
||
exit(0);
|
||
}
|
||
else {
|
||
|
||
# exit showing error
|
||
print STDERR "Error while calling $cmd_du.\n";
|
||
print STDERR "Please make sure this version of du supports the \"$du_args\" flags.\n";
|
||
print STDERR "GNU du is recommended.\n";
|
||
exit(1);
|
||
}
|
||
}
|
||
else {
|
||
|
||
# test was successful
|
||
exit(0);
|
||
}
|
||
}
|
||
else {
|
||
print STDERR ("No files or directories found\n");
|
||
exit(1);
|
||
}
|
||
|
||
# shouldn't happen
|
||
exit(1);
|
||
}
|
||
|
||
# accept two args from $ARGV[1] and [2], like "beta.0" "beta.1" etc.
|
||
# stick the full snapshot_root path on the beginning, and call rsnapshot-diff with these args
|
||
# NOTE: since this is a read-only operation, we're not concerned with directory traversals and relative paths
|
||
sub show_rsnapshot_diff {
|
||
my $cmd_rsnapshot_diff = 'rsnapshot-diff';
|
||
|
||
my $retval;
|
||
|
||
# this will only hold two entries, no more no less
|
||
# paths_in holds the incoming arguments
|
||
# args will be assigned the arguments that rsnapshot-diff will use
|
||
#
|
||
my @paths_in = ();
|
||
my @cmd_args = ();
|
||
|
||
# first, make sure we have permission to see the snapshot root
|
||
if (!-r "$config_vars{'snapshot_root'}") {
|
||
print STDERR ("ERROR: Permission denied\n");
|
||
exit(1);
|
||
}
|
||
|
||
# check for rsnapshot-diff program (falling back on $PATH)
|
||
if (defined($config_vars{'cmd_rsnapshot_diff'})) {
|
||
$cmd_rsnapshot_diff = $config_vars{'cmd_rsnapshot_diff'};
|
||
}
|
||
|
||
# see if we even got the right number of arguments (none is OK, but 1 isn't. 2 is also OK)
|
||
if (defined($ARGV[1]) && !defined($ARGV[2])) {
|
||
print STDERR "Usage: rsnapshot diff [backup level|dir] [backup level|dir]\n";
|
||
exit(1);
|
||
}
|
||
|
||
# make this automatically pick the two lowest intervals (or .sync dir) for comparison, as the default
|
||
# we actually want to specify the older directory first, since rsnapshot-diff will flip them around
|
||
# anyway based on mod times. doing it this way should make both programs consistent, and cause less
|
||
# surprises.
|
||
if (!defined($ARGV[1]) && !defined($ARGV[2])) {
|
||
|
||
# sync_first is enabled, and .sync exists
|
||
if ($config_vars{'sync_first'} && (-d "$config_vars{'snapshot_root'}/.sync/")) {
|
||
|
||
# interval.0
|
||
if (-d ("$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0")) {
|
||
$cmd_args[0] = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0";
|
||
}
|
||
|
||
# .sync
|
||
$cmd_args[1] = "$config_vars{'snapshot_root'}/.sync";
|
||
|
||
}
|
||
|
||
# sync_first is not enabled, or .sync doesn't exist
|
||
else {
|
||
|
||
# interval.1
|
||
if (-d ("$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".1")) {
|
||
$cmd_args[0] = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".1";
|
||
}
|
||
|
||
# interval.0
|
||
if (-d ("$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0")) {
|
||
$cmd_args[1] = "$config_vars{'snapshot_root'}/" . $intervals[0]->{'interval'} . ".0";
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# if we got some command line arguments, loop through twice and figure out what they mean
|
||
else {
|
||
$paths_in[0] = $ARGV[1]; # the 1st path is the 2nd cmd line argument
|
||
$paths_in[1] = $ARGV[2]; # the 2nd path is the 3rd cmd line argument
|
||
|
||
for (my $i = 0; $i < 2; $i++) {
|
||
|
||
# no interval would start with ../
|
||
if (is_directory_traversal("$paths_in[$i]")) {
|
||
$cmd_args[$i] = $paths_in[$i];
|
||
|
||
# if this directory exists locally, it must be local
|
||
}
|
||
elsif (-e "$paths_in[$i]") {
|
||
$cmd_args[$i] = $paths_in[$i];
|
||
|
||
# absolute path
|
||
}
|
||
elsif (is_valid_local_abs_path("$paths_in[$i]")) {
|
||
$cmd_args[$i] = $paths_in[$i];
|
||
|
||
# we didn't find it locally, but it's in the snapshot root
|
||
}
|
||
elsif (-e "$config_vars{'snapshot_root'}/$paths_in[$i]") {
|
||
$cmd_args[$i] = "$config_vars{'snapshot_root'}/$paths_in[$i]";
|
||
}
|
||
}
|
||
}
|
||
|
||
# double check to make sure the directories exists (and are directories)
|
||
if ( (!defined($cmd_args[0]) or (!defined($cmd_args[1])))
|
||
or ((!-d "$cmd_args[0]") or (!-d "$cmd_args[1]"))) {
|
||
print STDERR "ERROR: Arguments must be valid backup levels or directories\n";
|
||
exit(1);
|
||
}
|
||
|
||
# remove trailing slashes from directories
|
||
$cmd_args[0] = remove_trailing_slash($cmd_args[0]);
|
||
$cmd_args[1] = remove_trailing_slash($cmd_args[1]);
|
||
|
||
# increase verbosity (by possibly sticking a verbose flag in as the first argument)
|
||
#
|
||
# debug
|
||
if ($verbose >= 5) {
|
||
unshift(@cmd_args, '-V');
|
||
|
||
}
|
||
elsif ($verbose >= 4) {
|
||
unshift(@cmd_args, '-v');
|
||
|
||
# verbose
|
||
}
|
||
elsif ($verbose >= 3) {
|
||
unshift(@cmd_args, '-vi');
|
||
}
|
||
|
||
# run rsnapshot-diff
|
||
if (defined($verbose) && ($verbose >= 3)) {
|
||
print wrap_cmd(("$cmd_rsnapshot_diff " . join(' ', @cmd_args))), "\n\n";
|
||
}
|
||
if (0 == $test) {
|
||
$retval = system($cmd_rsnapshot_diff, @cmd_args);
|
||
if (0 == $retval) {
|
||
exit(0);
|
||
}
|
||
else {
|
||
|
||
# exit showing error
|
||
print STDERR "Error while calling $cmd_rsnapshot_diff\n";
|
||
exit(1);
|
||
}
|
||
}
|
||
else {
|
||
|
||
# test was successful
|
||
exit(0);
|
||
}
|
||
|
||
# shouldn't happen
|
||
exit(1);
|
||
}
|
||
|
||
# This subroutine works the way I hoped rsync would under certain conditions.
|
||
# This is no fault of rsync, I just had something slightly different in mind :)
|
||
#
|
||
# This subroutine accepts two arguments, a source path and a destination path.
|
||
# It traverses both recursively.
|
||
# If a file is in the source, but not the destination, it is hard linked into dest
|
||
# If a file is in the destination, but not the source, it is deleted
|
||
# If a file is in both locations and is different, dest is unlinked and src is linked to dest
|
||
# If a file is in both locations and is the same, nothing happens
|
||
#
|
||
# What makes this different than rsync is that it looks only at the file contents to
|
||
# see if the files are different, not at the metadata such as timestamps.
|
||
# I was unable to make rsync work recursively on identical files without unlinking
|
||
# at the destination and using another inode for a new file with the exact same content.
|
||
#
|
||
# If anyone knows of a better way (that doesn't add dependencies) i'd love to hear it!
|
||
#
|
||
sub sync_if_different {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
my $result = 0;
|
||
|
||
# make sure we were passed two arguments
|
||
if (!defined($src)) { return (0); }
|
||
if (!defined($dest)) { return (0); }
|
||
|
||
# make sure we have a source directory
|
||
if (!-d "$src") {
|
||
print_err("sync_if_different() needs a valid source directory as its first argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# strip trailing slashes off the directories,
|
||
# since we'll add them back on later
|
||
$src = remove_trailing_slash($src);
|
||
$dest = remove_trailing_slash($dest);
|
||
|
||
# copy everything from src to dest
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "sync_cp_src_dest(\"$src\", \"$dest\")";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
$result = sync_cp_src_dest("$src", "$dest");
|
||
if (!$result) {
|
||
print_err("Warning! sync_cp_src_dest(\"$src\", \"$dest\")", 2);
|
||
return (0);
|
||
}
|
||
|
||
# delete everything from dest that isn't in src
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "sync_rm_dest(\"$src\", \"$dest\")";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
$result = sync_rm_dest("$src", "$dest");
|
||
if (!$result) {
|
||
print_err("Warning! sync_rm_dest(\"$src\", \"$dest\")", 2);
|
||
return (0);
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts src, dest
|
||
# "copies" everything from src to dest, mainly using hard links
|
||
# called only from sync_if_different()
|
||
# returns 1 on success, 0 if any failures occur
|
||
sub sync_cp_src_dest {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
my $dh = undef;
|
||
my $result = 0;
|
||
my $retval = 1; # return code for this subroutine
|
||
|
||
# make sure we were passed two arguments
|
||
if (!defined($src)) { return (0); }
|
||
if (!defined($dest)) { return (0); }
|
||
|
||
# make sure we have a source directory
|
||
if (!-d "$src") {
|
||
print_err("sync_if_different() needs a valid source directory as its first argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# strip trailing slashes off the directories,
|
||
# since we'll add them back on later
|
||
$src = remove_trailing_slash($src);
|
||
$dest = remove_trailing_slash($dest);
|
||
|
||
# LSTAT SRC
|
||
my $st = lstat("$src");
|
||
if (!defined($st)) {
|
||
print_err("Could not lstat(\"$src\")", 2);
|
||
return (0);
|
||
}
|
||
|
||
# MKDIR DEST (AND SET MODE)
|
||
if (!-d "$dest") {
|
||
|
||
# check to make sure we don't have something here that's not a directory
|
||
if (-e "$dest") {
|
||
$result = unlink("$dest");
|
||
if (0 == $result) {
|
||
print_err("Warning! Could not unlink(\"$dest\")", 2);
|
||
return (0);
|
||
}
|
||
}
|
||
|
||
# create the directory
|
||
$result = mkdir("$dest", $st->mode);
|
||
if (!$result) {
|
||
print_err("Warning! Could not mkdir(\"$dest\", $st->mode);", 2);
|
||
return (0);
|
||
}
|
||
}
|
||
|
||
# CHOWN DEST (if root)
|
||
if (0 == $<) {
|
||
|
||
# make sure destination is not a symlink (should never happen because of unlink() above)
|
||
if (!-l "$dest") {
|
||
$result = safe_chown($st->uid, $st->gid, "$dest");
|
||
if (!$result) {
|
||
print_err("Warning! Could not safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\");", 2);
|
||
return (0);
|
||
}
|
||
}
|
||
}
|
||
|
||
# copy anything different from src into dest
|
||
$dh = new DirHandle("$src");
|
||
if (defined($dh)) {
|
||
my @nodes = $dh->read();
|
||
|
||
# loop through all nodes in this dir
|
||
foreach my $node (@nodes) {
|
||
|
||
# skip '.' and '..'
|
||
next if ($node =~ m/^\.\.?$/o);
|
||
|
||
# if it's a symlink, create the link
|
||
# this check must be done before dir and file because it will
|
||
# pretend to be a file or a directory as well as a symlink
|
||
if (-l "$src/$node") {
|
||
|
||
# nuke whatever is in the destination, since we'd have to recreate the symlink anyway
|
||
# and a real file or directory will be in our way
|
||
# symlinks pretend to be directories, which is why we check it the way that we do
|
||
if (-e "$dest/$node") {
|
||
if ((-l "$dest/$node") or (!-d "$dest/$node")) {
|
||
$result = unlink("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! Could not unlink(\"$dest/$node\")", 2);
|
||
next;
|
||
}
|
||
|
||
}
|
||
|
||
# nuke the destination directory
|
||
else {
|
||
$result = rm_rf("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Could not rm_rf(\"$dest/$node\")", 2);
|
||
next;
|
||
}
|
||
}
|
||
}
|
||
|
||
$result = copy_symlink("$src/$node", "$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! copy_symlink(\"$src/$node\", \"$dest/$node\") failed", 2);
|
||
return (0);
|
||
}
|
||
|
||
}
|
||
|
||
# if it's a directory, recurse!
|
||
elsif (-d "$src/$node") {
|
||
|
||
# if the destination exists but isn't a directory, delete it
|
||
if (-e "$dest/$node") {
|
||
|
||
# a symlink might claim to be a directory, so check for that first
|
||
if ((-l "$dest/$node") or (!-d "$dest/$node")) {
|
||
$result = unlink("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! unlink(\"$dest/$node\") failed", 2);
|
||
next;
|
||
}
|
||
}
|
||
}
|
||
|
||
# ok, dest is a real directory or it isn't there yet, go recurse
|
||
$result = sync_cp_src_dest("$src/$node", "$dest/$node");
|
||
if (!$result) {
|
||
print_err("Warning! Recursion error in sync_cp_src_dest(\"$src/$node\", \"$dest/$node\")", 2);
|
||
}
|
||
|
||
}
|
||
|
||
# if it's a file...
|
||
elsif (-f "$src/$node") {
|
||
|
||
# if dest is a symlink, we need to remove it first
|
||
if (-l "$dest/$node") {
|
||
$result = unlink("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! unlink(\"$dest/$node\") failed", 2);
|
||
next;
|
||
}
|
||
}
|
||
|
||
# if dest is a directory, we need to wipe it out first
|
||
if (-d "$dest/$node") {
|
||
$result = rm_rf("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Could not rm_rf(\"$dest/$node\")", 2);
|
||
return (0);
|
||
}
|
||
}
|
||
|
||
# if dest (still) exists, check for differences
|
||
if (-e "$dest/$node") {
|
||
|
||
# if they are different, unlink dest and link src to dest
|
||
if (1 == file_diff("$src/$node", "$dest/$node")) {
|
||
$result = unlink("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! unlink(\"$dest/$node\") failed", 2);
|
||
next;
|
||
}
|
||
$result = link("$src/$node", "$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! link(\"$src/$node\", \"$dest/$node\") failed", 2);
|
||
next;
|
||
}
|
||
|
||
}
|
||
|
||
# if they are the same, just leave dest alone
|
||
else {
|
||
next;
|
||
}
|
||
|
||
}
|
||
|
||
# ok, dest doesn't exist. just link src to dest
|
||
else {
|
||
$result = link("$src/$node", "$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! link(\"$src/$node\", \"$dest/$node\") failed", 2);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
# FIFO
|
||
elsif (-p "$src/$node") {
|
||
print_err("Warning! Ignoring FIFO $src/$node", 2);
|
||
|
||
}
|
||
|
||
# SOCKET
|
||
elsif (-S "$src/$node") {
|
||
print_err("Warning! Ignoring socket: $src/$node", 2);
|
||
|
||
}
|
||
|
||
# BLOCK DEVICE
|
||
elsif (-b "$src/$node") {
|
||
print_err("Warning! Ignoring special block file: $src/$node", 2);
|
||
|
||
}
|
||
|
||
# CHAR DEVICE
|
||
elsif (-c "$src/$node") {
|
||
print_err("Warning! Ignoring special character file: $src/$node", 2);
|
||
}
|
||
}
|
||
}
|
||
|
||
# close open dir handle
|
||
if (defined($dh)) { $dh->close(); }
|
||
undef($dh);
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts src, dest
|
||
# deletes everything from dest that isn't in src also
|
||
# called only from sync_if_different()
|
||
sub sync_rm_dest {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
my $dh = undef;
|
||
my $result = 0;
|
||
|
||
# make sure we were passed two arguments
|
||
if (!defined($src)) { return (0); }
|
||
if (!defined($dest)) { return (0); }
|
||
|
||
# make sure we have a source directory
|
||
if (!-d "$src") {
|
||
print_err("sync_rm_dest() needs a valid source directory as its first argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# make sure we have a destination directory
|
||
if (!-d "$dest") {
|
||
print_err("sync_rm_dest() needs a valid destination directory as its second argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# strip trailing slashes off the directories,
|
||
# since we'll add them back on later
|
||
$src = remove_trailing_slash($src);
|
||
$dest = remove_trailing_slash($dest);
|
||
|
||
# delete anything from dest that isn't found in src
|
||
$dh = new DirHandle("$dest");
|
||
if (defined($dh)) {
|
||
my @nodes = $dh->read();
|
||
|
||
# loop through all nodes in this dir
|
||
foreach my $node (@nodes) {
|
||
|
||
# skip '.' and '..'
|
||
next if ($node =~ m/^\.\.?$/o);
|
||
|
||
# if this node isn't present in src, delete it
|
||
if (!-e "$src/$node") {
|
||
|
||
# file or symlink
|
||
if ((-l "$dest/$node") or (!-d "$dest/$node")) {
|
||
$result = unlink("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! Could not delete \"$dest/$node\"", 2);
|
||
next;
|
||
}
|
||
|
||
}
|
||
|
||
# directory
|
||
else {
|
||
$result = rm_rf("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! Could not delete \"$dest/$node\"", 2);
|
||
}
|
||
}
|
||
next;
|
||
}
|
||
|
||
# ok, this also exists in src...
|
||
# theoretically, sync_cp_src_dest() should have caught this already, but better safe than sorry
|
||
# also, symlinks can pretend to be directories, so we have to check for those too
|
||
|
||
# if src is a file but dest is a directory, we need to recursively remove the dest dir
|
||
if ((-l "$src/$node") or (!-d "$src/$node")) {
|
||
if (-d "$dest/$node") {
|
||
$result = rm_rf("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! Could not delete \"$dest/$node\"", 2);
|
||
}
|
||
}
|
||
|
||
# otherwise, if src is a directory, but dest is a file, remove the file in dest
|
||
}
|
||
elsif (-d "$src/$node") {
|
||
if ((-l "$dest/$node") or (!-d "$dest/$node")) {
|
||
$result = unlink("$dest/$node");
|
||
if (0 == $result) {
|
||
print_err("Warning! Could not delete \"$dest/$node\"", 2);
|
||
next;
|
||
}
|
||
}
|
||
}
|
||
|
||
# if it's a directory in src, let's recurse into it and compare files there
|
||
if (-d "$src/$node") {
|
||
$result = sync_rm_dest("$src/$node", "$dest/$node");
|
||
if (!$result) {
|
||
print_err("Warning! Recursion error in sync_rm_dest(\"$src/$node\", \"$dest/$node\")", 2);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# close open dir handle
|
||
if (defined($dh)) { $dh->close(); }
|
||
undef($dh);
|
||
|
||
return (1);
|
||
|
||
}
|
||
|
||
# accepts src, dest
|
||
# "copies" a symlink from src by recreating it in dest
|
||
# returns 1 on success, 0 on failure
|
||
sub copy_symlink {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
my $st = undef;
|
||
my $result = undef;
|
||
|
||
my $link_deref_path = undef;
|
||
|
||
# make sure it's actually a symlink
|
||
if (!-l "$src") {
|
||
print_err("Warning! \"$src\" not a symlink in copy_symlink()", 2);
|
||
return (0);
|
||
}
|
||
|
||
# make sure we aren't clobbering the destination
|
||
if (-e "$dest") {
|
||
print_err("Warning! \"$dest\" exists!", 2);
|
||
return (0);
|
||
}
|
||
|
||
# LSTAT
|
||
$st = lstat("$src");
|
||
if (!defined($st)) {
|
||
print_err("Warning! lstat(\"$src\") failed", 2);
|
||
return (0);
|
||
}
|
||
|
||
# CREATE THE SYMLINK
|
||
# This is done in two steps:
|
||
# Reading/dereferencing the link, and creating a new one
|
||
#
|
||
# Why not just hard link the symlink?
|
||
# see http://www.rsnapshot.org/security/2005/001.html
|
||
# and also msgid <5036B23B.3000606@scubaninja.com> on
|
||
# rsnapshot-discuss, on 2012-08-23
|
||
#
|
||
# Step 1: READ THE LINK
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "readlink(\"$src\")\n";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
$link_deref_path = readlink("$src");
|
||
if (!defined($link_deref_path)) {
|
||
print_err("Warning! Could not readlink(\"$src\")", 2);
|
||
return (0);
|
||
}
|
||
#
|
||
# Step 2: RECREATE THE LINK
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "symlink(\"$link_deref_path\", \"$dest\")\n";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
$result = symlink("$link_deref_path", "$dest");
|
||
if (0 == $result) {
|
||
print_err("Warning! Could not symlink(\"$link_deref_path\"), \"$dest\")", 2);
|
||
return (0);
|
||
}
|
||
|
||
# CHOWN DEST (if root)
|
||
if (0 == $<) {
|
||
|
||
# make sure the symlink even exists
|
||
if (-e "$dest") {
|
||
|
||
# print and/or log this if necessary
|
||
if (($verbose > 4) or ($loglevel > 4)) {
|
||
my $cmd_string = "safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\");";
|
||
|
||
if ($verbose > 4) {
|
||
print_cmd($cmd_string);
|
||
}
|
||
elsif ($loglevel > 4) {
|
||
log_msg($cmd_string, 4);
|
||
}
|
||
}
|
||
|
||
$result = safe_chown($st->uid, $st->gid, "$dest");
|
||
|
||
if (0 == $result) {
|
||
print_err("Warning! Could not safe_chown(" . $st->uid . ", " . $st->gid . ", \"$dest\")", 2);
|
||
return (0);
|
||
}
|
||
}
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
# accepts a file permission number from $st->mode (e.g., 33188)
|
||
# returns a "normal" file permission number (e.g., 644)
|
||
# do the appropriate bit shifting to get a "normal" UNIX file permission mode
|
||
sub get_perms {
|
||
my $raw_mode = shift(@_);
|
||
|
||
if (!defined($raw_mode)) { return (undef); }
|
||
|
||
# a lot of voodoo for just one line
|
||
# http://www.perlmonks.org/index.pl?node_id=159906
|
||
my $mode = sprintf("%04o", ($raw_mode & 07777));
|
||
|
||
return ($mode);
|
||
}
|
||
|
||
# accepts return value from the system() command
|
||
# bitmasks it, and returns the same thing "echo $?" would from the shell
|
||
sub get_retval {
|
||
my $retval = shift(@_);
|
||
|
||
if (!defined($retval)) {
|
||
bail('get_retval() was not passed a value');
|
||
}
|
||
if ($retval !~ m/^\d+$/) {
|
||
bail("get_retval() was passed $retval, a number is required");
|
||
}
|
||
|
||
if ($retval & 0x7f) {
|
||
return (128 + ($retval & 0x7f));
|
||
}
|
||
|
||
return ($retval >> 8);
|
||
}
|
||
|
||
# accepts two file paths
|
||
# returns 0 if they're the same, 1 if they're different
|
||
# returns undef if one or both of the files can't be found, opened, or closed
|
||
sub file_diff {
|
||
my $file1 = shift(@_);
|
||
my $file2 = shift(@_);
|
||
my $st1 = undef;
|
||
my $st2 = undef;
|
||
my $buf1 = undef;
|
||
my $buf2 = undef;
|
||
my $result = undef;
|
||
|
||
# number of bytes to read at once
|
||
my $BUFSIZE = 16384;
|
||
|
||
# boolean file comparison flag. assume they're the same.
|
||
my $is_different = 0;
|
||
|
||
if (!-r "$file1") { return (undef); }
|
||
if (!-r "$file2") { return (undef); }
|
||
|
||
# CHECK FILE SIZES FIRST
|
||
$st1 = lstat("$file1");
|
||
$st2 = lstat("$file2");
|
||
|
||
if (!defined($st1)) { return (undef); }
|
||
if (!defined($st2)) { return (undef); }
|
||
|
||
# if the files aren't even the same size, they can't possibly be the same.
|
||
# don't waste time comparing them more intensively
|
||
if ($st1->size != $st2->size) {
|
||
return (1);
|
||
}
|
||
|
||
# ok, we're still here.
|
||
# that means we have to compare files one chunk at a time
|
||
|
||
# open both files
|
||
$result = open(FILE1, "$file1");
|
||
if (!defined($result)) {
|
||
return (undef);
|
||
}
|
||
$result = open(FILE2, "$file2");
|
||
if (!defined($result)) {
|
||
close(FILE1);
|
||
return (undef);
|
||
}
|
||
|
||
# compare files
|
||
while (read(FILE1, $buf1, $BUFSIZE) && read(FILE2, $buf2, $BUFSIZE)) {
|
||
|
||
# exit this loop as soon as possible
|
||
if ($buf1 ne $buf2) {
|
||
$is_different = 1;
|
||
last;
|
||
}
|
||
}
|
||
|
||
# close both files
|
||
$result = close(FILE2);
|
||
if (!defined($result)) {
|
||
close(FILE1);
|
||
return (undef);
|
||
}
|
||
$result = close(FILE1);
|
||
if (!defined($result)) {
|
||
return (undef);
|
||
}
|
||
|
||
# return our findings
|
||
return ($is_different);
|
||
}
|
||
|
||
# accepts src, dest (file paths)
|
||
# calls rename(), forcing the mtime to be correct (to work around a bug in rare versions of the Linux 2.4 kernel)
|
||
# returns 1 on success, 0 on failure, just like the real rename() command
|
||
sub safe_rename {
|
||
my $src = shift(@_);
|
||
my $dest = shift(@_);
|
||
|
||
my $st;
|
||
my $retval;
|
||
my $result;
|
||
|
||
# validate src and dest paths
|
||
if (!defined($src)) {
|
||
print_err("safe_rename() needs a valid source file path as an argument", 2);
|
||
return (0);
|
||
}
|
||
if (!defined($dest)) {
|
||
print_err("safe_rename() needs a valid destination file path as an argument", 2);
|
||
return (0);
|
||
}
|
||
|
||
# stat file before rename
|
||
$st = stat($src);
|
||
if (!defined($st)) {
|
||
print_err("Could not stat() \"$src\"", 2);
|
||
return (0);
|
||
}
|
||
|
||
# rename the file
|
||
$retval = rename("$src", "$dest");
|
||
if (1 != $retval) {
|
||
print_err("Could not rename(\"$src\", \"$dest\")", 2);
|
||
return (0);
|
||
}
|
||
|
||
# give it back the old mtime and atime values
|
||
$result = utime($st->atime, $st->mtime, "$dest");
|
||
if (!defined($result)) {
|
||
print_err("Could not utime( $st->atime, $st->mtime, \"$dest\")", 2);
|
||
return (0);
|
||
}
|
||
|
||
# if we made it this far, it must have worked
|
||
return (1);
|
||
}
|
||
|
||
# accepts no args
|
||
# checks the config file for version number
|
||
# prints the config version to stdout
|
||
# exits the program, 0 on success, 1 on failure
|
||
# this feature is "undocumented", for use with scripts, etc
|
||
sub check_config_version {
|
||
my $version = get_config_version();
|
||
|
||
if (!defined($version)) {
|
||
print "error\n";
|
||
exit(1);
|
||
}
|
||
|
||
print $version, "\n";
|
||
exit(0);
|
||
}
|
||
|
||
# accepts no args
|
||
# scans the config file for the config_version parameter
|
||
# returns the config version, or undef
|
||
sub get_config_version {
|
||
my $result;
|
||
my $version;
|
||
|
||
# make sure the config file exists and we can read it
|
||
if (!defined($config_file)) {
|
||
return (undef);
|
||
}
|
||
if (!-r "$config_file") {
|
||
return (undef);
|
||
}
|
||
|
||
# open the config file
|
||
$result = open(CONFIG, "$config_file");
|
||
if (!defined($result)) {
|
||
return (undef);
|
||
}
|
||
|
||
# scan the config file looking for the config_version parameter
|
||
# if we find it, exit the loop
|
||
while (my $line = <CONFIG>) {
|
||
chomp($line);
|
||
|
||
if ($line =~ m/^config_version/o) {
|
||
if ($line =~ m/^config_version\t+([\d\.\-\w]+)$/o) {
|
||
$version = $1;
|
||
last;
|
||
}
|
||
else {
|
||
$version = 'undefined';
|
||
}
|
||
}
|
||
}
|
||
|
||
$result = close(CONFIG);
|
||
if (!defined($result)) {
|
||
return (undef);
|
||
}
|
||
|
||
if (!defined($version)) {
|
||
$version = 'unknown';
|
||
}
|
||
|
||
return ($version);
|
||
}
|
||
|
||
# accepts no args
|
||
# exits the program, 0 on success, 1 on failure
|
||
# attempts to upgrade the rsnapshot.conf file for compatibility with this version
|
||
sub upgrade_config_file {
|
||
my $result;
|
||
my @lines;
|
||
my $config_version;
|
||
|
||
# check if rsync_long_args is already enabled
|
||
my $rsync_long_args_enabled = 0;
|
||
|
||
# first, see if the file isn't already up to date
|
||
$config_version = get_config_version();
|
||
if (!defined($config_version)) {
|
||
print STDERR "ERROR: Could not read config file during version check.\n";
|
||
exit(1);
|
||
}
|
||
|
||
# right now 1.2 is the only valid version
|
||
if ('1.2' eq $config_version) {
|
||
print "$config_file file is already up to date.\n";
|
||
exit(0);
|
||
|
||
# config_version is set, but not to anything we know about
|
||
}
|
||
elsif ('unknown' eq $config_version) {
|
||
|
||
# this is good, it means the config_version was not already set to anything
|
||
# and is a good candidate for the upgrade
|
||
|
||
}
|
||
else {
|
||
print STDERR "ERROR: config_version is set to unknown version: $config_version.\n";
|
||
exit(1);
|
||
}
|
||
|
||
# make sure config file is present and readable
|
||
if (!defined($config_file)) {
|
||
print STDERR "ERROR: Config file not defined.\n";
|
||
exit(1);
|
||
}
|
||
if (!-r "$config_file") {
|
||
print STDERR "ERROR: $config_file not readable.\n";
|
||
exit(1);
|
||
}
|
||
|
||
# read in original config file
|
||
$result = open(CONFIG, "$config_file");
|
||
if (!defined($result)) {
|
||
print STDERR "ERROR: Could not open $config_file for reading.\n";
|
||
exit(1);
|
||
}
|
||
@lines = <CONFIG>;
|
||
$result = close(CONFIG);
|
||
if (!defined($result)) {
|
||
print STDERR "ERROR: Could not close $config_file after reading.\n";
|
||
exit(1);
|
||
}
|
||
|
||
# see if we can find rsync_long_args, either commented out or uncommented
|
||
foreach my $line (@lines) {
|
||
if ($line =~ m/^rsync_long_args/o) {
|
||
$rsync_long_args_enabled = 1;
|
||
}
|
||
}
|
||
|
||
# back up old config file
|
||
backup_config_file(\@lines);
|
||
|
||
# found rsync_long_args enabled
|
||
if ($rsync_long_args_enabled) {
|
||
print "Found \"rsync_long_args\" uncommented. Attempting upgrade...\n";
|
||
write_upgraded_config_file(\@lines, 0);
|
||
|
||
}
|
||
|
||
# did not find rsync_long_args enabled
|
||
else {
|
||
print "Could not find old \"rsync_long_args\" parameter. Attempting upgrade...\n";
|
||
write_upgraded_config_file(\@lines, 1);
|
||
}
|
||
|
||
print "\"$config_file\" was successfully upgraded.\n";
|
||
|
||
exit(0);
|
||
}
|
||
|
||
# accepts array_ref of config file lines
|
||
# exits 1 on errors
|
||
# attempts to backup rsnapshot.conf to rsnapshot.conf.backup.(#)
|
||
sub backup_config_file {
|
||
my $lines_ref = shift(@_);
|
||
|
||
my $result;
|
||
my $backup_config_file;
|
||
my $backup_exists = 0;
|
||
|
||
if (!defined($lines_ref)) {
|
||
print STDERR "ERROR: backup_config_file() was not passed an argument.\n";
|
||
exit(1);
|
||
}
|
||
|
||
if (!defined($config_file)) {
|
||
print STDERR "ERROR: Could not find config file.\n";
|
||
exit(1);
|
||
}
|
||
|
||
$backup_config_file = "$config_file.backup";
|
||
|
||
print "Backing up \"$config_file\".\n";
|
||
|
||
# pick a unique name for the backup file
|
||
if (-e "$backup_config_file") {
|
||
$backup_exists = 1;
|
||
for (my $i = 0; $i < 100; $i++) {
|
||
if (!-e "$backup_config_file.$i") {
|
||
$backup_config_file = "$backup_config_file.$i";
|
||
$backup_exists = 0;
|
||
last;
|
||
}
|
||
}
|
||
|
||
# if we couldn't write a backup file, exit with an error
|
||
if (1 == $backup_exists) {
|
||
print STDERR "ERROR: Refusing to overwrite $backup_config_file.\n";
|
||
print STDERR "Please move $backup_config_file out of the way and try again.\n";
|
||
print STDERR "$config_file has NOT been upgraded!\n";
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
$result = open(OUTFILE, "> $backup_config_file");
|
||
if (!defined($result) or ($result != 1)) {
|
||
print STDERR "Error opening $backup_config_file for writing.\n";
|
||
print STDERR "$config_file has NOT been upgraded!\n";
|
||
exit(1);
|
||
}
|
||
foreach my $line (@$lines_ref) {
|
||
print OUTFILE $line;
|
||
}
|
||
$result = close(OUTFILE);
|
||
if (!defined($result) or (1 != $result)) {
|
||
print STDERR "could not cleanly close $backup_config_file.\n";
|
||
print STDERR "$config_file has NOT been upgraded!\n";
|
||
exit(1);
|
||
}
|
||
|
||
print "Config file was backed up to \"$backup_config_file\".\n";
|
||
}
|
||
|
||
# accepts no args
|
||
# exits 1 on errors
|
||
# attempts to write an upgraded config file to rsnapshot.conf
|
||
sub write_upgraded_config_file {
|
||
my $lines_ref = shift(@_);
|
||
my $add_rsync_long_args = shift(@_);
|
||
|
||
my $result;
|
||
|
||
my $upgrade_notice = '';
|
||
|
||
$upgrade_notice .=
|
||
"#-----------------------------------------------------------------------------\n";
|
||
$upgrade_notice .= "# UPGRADE NOTICE:\n";
|
||
$upgrade_notice .= "#\n";
|
||
$upgrade_notice .= "# This file was upgraded automatically by rsnapshot.\n";
|
||
$upgrade_notice .= "#\n";
|
||
$upgrade_notice .= "# The \"config_version\" parameter was added, since it is now required.\n";
|
||
$upgrade_notice .= "#\n";
|
||
$upgrade_notice .= "# The default value for \"rsync_long_args\" has changed in this release.\n";
|
||
$upgrade_notice .= "# By explicitly setting it to the old default values, rsnapshot will still\n";
|
||
$upgrade_notice .= "# behave like it did in previous versions.\n";
|
||
$upgrade_notice .= "#\n";
|
||
|
||
if (defined($add_rsync_long_args) && (1 == $add_rsync_long_args)) {
|
||
$upgrade_notice .= "# In this file, \"rsync_long_args\" was not enabled before the upgrade,\n";
|
||
$upgrade_notice .= "# so it has been set to the old default value.\n";
|
||
}
|
||
else {
|
||
$upgrade_notice .= "# In this file, \"rsync_long_args\" was already enabled before the upgrade,\n";
|
||
$upgrade_notice .= "# so it was not changed.\n";
|
||
}
|
||
|
||
$upgrade_notice .= "#\n";
|
||
$upgrade_notice .= "# New features and improvements have been added to rsnapshot that can\n";
|
||
$upgrade_notice .= "# only be fully utilized by making some additional changes to\n";
|
||
$upgrade_notice .=
|
||
"# \"rsync_long_args\" and your \"backup\" points. If you would like to get the\n";
|
||
$upgrade_notice .= "# most out of rsnapshot, please read the INSTALL file that came with this\n";
|
||
$upgrade_notice .= "# program for more information.\n";
|
||
$upgrade_notice .=
|
||
"#-----------------------------------------------------------------------------\n";
|
||
|
||
if (!defined($config_file)) {
|
||
print STDERR "ERROR: Config file not found.\n";
|
||
exit(1);
|
||
}
|
||
if (!-w "$config_file") {
|
||
print STDERR "ERROR: \"$config_file\" is not writable.\n";
|
||
exit(1);
|
||
}
|
||
|
||
$result = open(CONFIG, "> $config_file");
|
||
if (!defined($result)) {
|
||
print "ERROR: Could not open \"$config_file\" for writing.\n";
|
||
exit(1);
|
||
}
|
||
|
||
print CONFIG $upgrade_notice;
|
||
print CONFIG "\n";
|
||
print CONFIG "config_version\t1.2\n";
|
||
print CONFIG "\n";
|
||
|
||
if (defined($add_rsync_long_args) && (1 == $add_rsync_long_args)) {
|
||
print CONFIG "rsync_long_args\t--delete --numeric-ids\n";
|
||
print CONFIG "\n";
|
||
}
|
||
|
||
foreach my $line (@$lines_ref) {
|
||
print CONFIG "$line";
|
||
}
|
||
|
||
$result = close(CONFIG);
|
||
if (!defined($result)) {
|
||
print STDERR "ERROR: Could not close \"$config_file\" after writing\n.";
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
# accepts uid, gid, filepath
|
||
# uses lchown() to change ownership of the file
|
||
# without following symlinks
|
||
# returns 1 upon success (or if lchown() not present)
|
||
# returns 0 on failure
|
||
sub safe_chown {
|
||
my $uid = shift(@_);
|
||
my $gid = shift(@_);
|
||
my $filepath = shift(@_);
|
||
|
||
my $result = undef;
|
||
|
||
if (!defined($uid) or !defined($gid) or !defined($filepath)) {
|
||
print_err("safe_chown() needs uid, gid, and filepath", 2);
|
||
return (0);
|
||
}
|
||
if (!-e "$filepath") {
|
||
print_err("safe_chown() needs a valid filepath (not \"$filepath\")", 2);
|
||
return (0);
|
||
}
|
||
|
||
$result = lchown($uid, $gid, "$filepath");
|
||
if (!defined($result)) {
|
||
return (0);
|
||
}
|
||
|
||
return (1);
|
||
}
|
||
|
||
########################################
|
||
### PERLDOC / POD ###
|
||
########################################
|
||
|
||
=pod
|
||
|
||
=head1 NAME
|
||
|
||
rsnapshot - remote filesystem snapshot utility
|
||
|
||
=head1 SYNOPSIS
|
||
|
||
B<rsnapshot> [B<-vtxqVD>] [B<-c> cfgfile] [command] [args]
|
||
|
||
=head1 DESCRIPTION
|
||
|
||
B<rsnapshot> is a filesystem snapshot utility. It can take incremental
|
||
snapshots of local and remote filesystems for any number of machines.
|
||
|
||
Local filesystem snapshots are handled with L<rsync(1)>. Secure remote
|
||
connections are handled with rsync over L<ssh(1)>, while anonymous
|
||
rsync connections simply use an rsync server. Both remote and local
|
||
transfers depend on rsync.
|
||
|
||
B<rsnapshot> saves much more disk space than you might imagine. The amount
|
||
of space required is roughly the size of one full backup, plus a copy
|
||
of each additional file that is changed. B<rsnapshot> makes extensive
|
||
use of hard links, so if the file doesn't change, the next snapshot is
|
||
simply a hard link to the exact same file.
|
||
|
||
B<rsnapshot> will typically be invoked as root by a cron job, or series
|
||
of cron jobs. It is possible, however, to run as any arbitrary user
|
||
with an alternate configuration file.
|
||
|
||
All important options are specified in a configuration file, which is
|
||
located by default at F</etc/rsnapshot.conf>. An alternate file can be
|
||
specified on the command line. There are also additional options which
|
||
can be passed on the command line.
|
||
|
||
The command line options are as follows:
|
||
|
||
=over 4
|
||
|
||
B<-v> verbose, show shell commands being executed
|
||
|
||
B<-t> test, show shell commands that would be executed
|
||
|
||
B<-c> path to alternate config file
|
||
|
||
B<-x> one filesystem, don't cross partitions within each backup point
|
||
|
||
B<-q> quiet, suppress non-fatal warnings
|
||
|
||
B<-V> same as -v, but with more detail
|
||
|
||
B<-D> a firehose of diagnostic information
|
||
|
||
=back
|
||
|
||
=head1 CONFIGURATION
|
||
|
||
F</etc/rsnapshot.conf> is the default configuration file. All parameters
|
||
in this file must be separated by tabs. F</etc/rsnapshot.conf.default>
|
||
can be used as a reference.
|
||
|
||
It is recommended that you copy F</etc/rsnapshot.conf.default> to
|
||
F</etc/rsnapshot.conf>, and then modify F</etc/rsnapshot.conf> to suit
|
||
your needs.
|
||
|
||
Long lines may be split over several lines. "Continuation" lines
|
||
B<must> begin with a space or a tab character. Continuation lines will
|
||
have all leading and trailing whitespace stripped off, and then be appended
|
||
with an intervening tab character to the previous line when the configuration
|
||
file is parsed.
|
||
|
||
Here is a list of allowed parameters:
|
||
|
||
=over 4
|
||
|
||
B<config_version> Config file version (required). Default is 1.2
|
||
|
||
B<snapshot_root> Local filesystem path to save all snapshots
|
||
|
||
=over
|
||
|
||
This can be a volume mounted over the network on the machine which is running
|
||
rsnapshot, but it I<must> reliably support all POSIX interfaces. SMB/CIFS are
|
||
not supported due to some implementations being unreliable and incomplete.
|
||
|
||
=back
|
||
|
||
B<include_conf> Include another file in the configuration at this point.
|
||
|
||
=over 4
|
||
|
||
This is recursive, but you may need to be careful about paths when specifying
|
||
which file to include. We check to see if the file you have specified is
|
||
readable, and will yell an error if it isn't. We recommend using a full
|
||
path. As a special case, include_conf's value may be enclosed in `backticks`
|
||
in which case it will be executed and whatever it spits to STDOUT will
|
||
be included in the configuration. Note that shell meta-characters may be
|
||
interpreted.
|
||
|
||
=back
|
||
|
||
B<no_create_root> If set to 1, rsnapshot won't create B<snapshot_root> directory
|
||
|
||
B<cmd_rsync> Full path to C<rsync> (required)
|
||
|
||
B<cmd_ssh> Full path to C<ssh> (optional)
|
||
|
||
B<cmd_cp> Full path to C<cp> (optional, but must be GNU version)
|
||
|
||
=over 4
|
||
|
||
If you are using Linux, you should uncomment cmd_cp. If you are using a
|
||
platform which does not have GNU cp, you should leave cmd_cp commented out.
|
||
|
||
With GNU cp, rsnapshot can take care of both normal files and special
|
||
files (such as FIFOs, sockets, and block/character devices) in one pass.
|
||
|
||
If cmd_cp is disabled, rsnapshot will use its own built-in function,
|
||
native_cp_al() to backup up regular files and directories. This will
|
||
then be followed up by a separate call to rsync, to move the special
|
||
files over (assuming there are any).
|
||
|
||
=back
|
||
|
||
B<cmd_rm> Full path to C<rm> (optional)
|
||
|
||
B<cmd_logger> Full path to C<logger> (optional, for syslog support)
|
||
|
||
B<cmd_du> Full path to C<du> (optional, for disk usage reports)
|
||
|
||
B<cmd_rsnapshot_diff> Full path to C<rsnapshot-diff> (optional)
|
||
|
||
B<cmd_preexec>
|
||
|
||
=over 4
|
||
|
||
Full path (plus any arguments) to preexec script (optional).
|
||
This script will run immediately before each backup operation (but not any
|
||
rotations). If the execution fails, rsnapshot will stop immediately.
|
||
|
||
=back
|
||
|
||
B<cmd_postexec>
|
||
|
||
=over 4
|
||
|
||
Full path (plus any arguments) to postexec script (optional).
|
||
This script will run immediately after each backup operation (but not any
|
||
rotations). If the execution fails, rsnapshot will stop immediately.
|
||
|
||
=back
|
||
|
||
B<linux_lvm_cmd_lvcreate>
|
||
|
||
B<linux_lvm_cmd_lvremove>
|
||
|
||
B<linux_lvm_cmd_mount>
|
||
|
||
B<linux_lvm_cmd_umount>
|
||
|
||
=over 4
|
||
|
||
Paths to lvcreate, lvremove, mount and umount commands, for use with Linux
|
||
LVMs. You may include options to the commands also.
|
||
The lvcreate, lvremove, mount and umount commands are required for
|
||
managing snapshots of LVM volumes and are otherwise optional.
|
||
|
||
=back
|
||
|
||
B<retain> [name] [number]
|
||
|
||
=over 4
|
||
|
||
"name" refers to the name of this backup level (e.g., alpha, beta,
|
||
so also called the 'interval'). "number"
|
||
is the number of snapshots for this type of interval that will be retained.
|
||
The value of "name" will be the command passed to B<rsnapshot> to perform
|
||
this type of backup.
|
||
|
||
A deprecated alias for 'retain' is 'interval'.
|
||
|
||
Example: B<retain alpha 6>
|
||
|
||
[root@localhost]# B<rsnapshot alpha>
|
||
|
||
For this example, every time this is run, the following will happen:
|
||
|
||
<snapshot_root>/alpha.5/ will be deleted, if it exists.
|
||
|
||
<snapshot_root>/alpha.{1,2,3,4} will all be rotated +1, if they exist.
|
||
|
||
<snapshot_root>/alpha.0/ will be copied to <snapshot_root>/alpha.1/
|
||
using hard links.
|
||
|
||
Each backup point (explained below) will then be rsynced to the
|
||
corresponding directories in <snapshot_root>/alpha.0/
|
||
|
||
Backup levels must be specified in the config file in order, from most
|
||
frequent to least frequent. The first entry is the one which will be
|
||
synced with the backup points. The subsequent backup levels (e.g., beta,
|
||
gamma, etc) simply rotate, with each higher backup level pulling from the
|
||
one below it for its .0 directory.
|
||
|
||
Example:
|
||
|
||
=over 4
|
||
|
||
B<retain alpha 6>
|
||
|
||
B<retain beta 7>
|
||
|
||
B<retain gamma 4>
|
||
|
||
=back
|
||
|
||
beta.0/ will be moved from alpha.5/, and gamma.0/ will be moved from beta.6/
|
||
|
||
alpha.0/ will be rsynced directly from the filesystem.
|
||
|
||
=back
|
||
|
||
B<link_dest 1>
|
||
|
||
=over 4
|
||
|
||
If your version of rsync supports --link-dest (2.5.7 or newer), you can enable
|
||
this to let rsync handle some things that GNU cp or the built-in subroutines would
|
||
otherwise do. Enabling this makes rsnapshot take a slightly more complicated code
|
||
branch, but it's the best way to support special files on non-Linux systems.
|
||
|
||
=back
|
||
|
||
B<sync_first 1>
|
||
|
||
=over 4
|
||
|
||
sync_first changes the behaviour of rsnapshot. When this is enabled, all calls
|
||
to rsnapshot with various backup levels simply rotate files. All backups are handled
|
||
by calling rsnapshot with the "sync" argument. The synced files are stored in
|
||
a ".sync" directory under the snapshot_root.
|
||
|
||
This allows better recovery in the event that rsnapshot is interrupted in the
|
||
middle of a sync operation, since the sync step and rotation steps are
|
||
separated. This also means that you can easily run "rsnapshot sync" on the
|
||
command line without fear of forcing all the other directories to rotate up.
|
||
This benefit comes at the cost of one more snapshot worth of disk space.
|
||
The default is 0 (off).
|
||
|
||
=back
|
||
|
||
B<verbose 2>
|
||
|
||
=over 4
|
||
|
||
The amount of information to print out when the program is run. Allowed values
|
||
are 1 through 5. The default is 2.
|
||
|
||
1 Quiet Show fatal errors only
|
||
2 Default Show warnings and errors
|
||
3 Verbose Show equivalent shell commands being executed
|
||
4 Extra Verbose Same as verbose, but with more detail
|
||
5 Debug All kinds of information
|
||
|
||
=back
|
||
|
||
B<loglevel 3>
|
||
|
||
=over 4
|
||
|
||
This number means the same thing as B<verbose> above, but it determines how
|
||
much data is written to the logfile, if one is being written.
|
||
|
||
=back
|
||
|
||
B<logfile /var/log/rsnapshot>
|
||
|
||
=over 4
|
||
|
||
Full filesystem path to the rsnapshot log file. If this is defined, a log file
|
||
will be written, with the amount of data being controlled by B<loglevel>. If
|
||
this is commented out, no log file will be written.
|
||
|
||
=back
|
||
|
||
B<include [file-name-pattern]>
|
||
|
||
=over 4
|
||
|
||
This gets passed directly to rsync using the --include directive. This
|
||
parameter can be specified as many times as needed, with one pattern defined
|
||
per line. See the rsync(1) man page for the syntax.
|
||
|
||
=back
|
||
|
||
B<exclude [file-name-pattern]>
|
||
|
||
=over 4
|
||
|
||
This gets passed directly to rsync using the --exclude directive. This
|
||
parameter can be specified as many times as needed, with one pattern defined
|
||
per line. See the rsync(1) man page for the syntax.
|
||
|
||
=back
|
||
|
||
B<include_file /path/to/include/file>
|
||
|
||
=over 4
|
||
|
||
This gets passed directly to rsync using the --include-from directive. See the
|
||
rsync(1) man page for the syntax.
|
||
|
||
=back
|
||
|
||
B<exclude_file /path/to/exclude/file>
|
||
|
||
=over 4
|
||
|
||
This gets passed directly to rsync using the --exclude-from directive. See the
|
||
rsync(1) man page for the syntax.
|
||
|
||
=back
|
||
|
||
B<rsync_short_args -a>
|
||
|
||
=over 4
|
||
|
||
List of short arguments to pass to rsync. If not specified,
|
||
"-a" is the default. Please note that these must be all next to each other.
|
||
For example, "-az" is valid, while "-a -z" is not.
|
||
|
||
"-a" is rsync's "archive mode" which tells it to copy as much of the
|
||
filesystem metadata as it can for each file. This specifically does *not*
|
||
include information about hard links, as that would greatly increase rsync's
|
||
memory usage and slow it down. If you need to preserve hard links in your
|
||
backups, then add "H" to this.
|
||
|
||
=back
|
||
|
||
B<rsync_long_args --delete --numeric-ids --relative --delete-excluded>
|
||
|
||
=over 4
|
||
|
||
List of long arguments to pass to rsync. The default values are
|
||
--delete --numeric-ids --relative --delete-excluded
|
||
This means that the directory structure in each backup point destination
|
||
will match that in the backup point source.
|
||
|
||
Quotes are permitted in B<rsync_long_args>, eg C<--rsync-path='sudo /usr/bin/rsync'>.
|
||
You may use either single (') or double (") quotes, but nested quotes (including
|
||
mixed nested quotes) are not permitted. Similar quoting is also allowed in
|
||
per-backup-point B<rsync_long_args>.
|
||
|
||
=back
|
||
|
||
B<rsync_numtries 0>
|
||
|
||
=over 4
|
||
|
||
Number of additional attempts for running rsync for each given backup source.
|
||
Whenever the rsync operation for a source finishes with a non-zero exitcode,
|
||
rsnapshot will repeat this operation until the configured number of
|
||
"rsync_numtries" is reached or rsync finishes successfully.
|
||
By default no repeated attempts are performed ("rsync_numtries 0").
|
||
|
||
=back
|
||
|
||
B<rsync_wait_between_tries 0>
|
||
|
||
=over 4
|
||
|
||
Wait between tries in seconds.
|
||
Specify the duration in seconds to wait between retries of the rsync operation.
|
||
The number of retries should be defined in rsync_numtries.
|
||
The default wait time is 0 seconds.
|
||
|
||
=back
|
||
|
||
B<ssh_args -p 22>
|
||
|
||
=over 4
|
||
|
||
Arguments to be passed to ssh. If not specified, the default is none.
|
||
|
||
=back
|
||
|
||
B<du_args -csh>
|
||
|
||
=over 4
|
||
|
||
Arguments to be passed to du. If not specified, the default is -csh.
|
||
GNU du supports -csh, BSD du supports -csk, Solaris du doesn't support
|
||
-c at all. The GNU version is recommended, since it offers the most
|
||
features.
|
||
|
||
=back
|
||
|
||
B<lockfile /var/run/rsnapshot.pid>
|
||
|
||
B<stop_on_stale_lockfile 0>
|
||
|
||
=over 4
|
||
|
||
Lockfile to use when rsnapshot is run. This prevents a second invocation
|
||
from clobbering the first one. If not specified, no lock file is used.
|
||
Make sure to use a directory that is not world writeable for security
|
||
reasons. Use of a lock file is strongly recommended.
|
||
|
||
If a lockfile exists when rsnapshot starts, it will try to read the file
|
||
and stop with an error if it can't. If it *can* read the file, it sees if
|
||
a process exists with the PID noted in the file. If it does, rsnapshot
|
||
stops with an error message. If there is no process with that PID, then
|
||
we assume that the lockfile is stale and ignore it *unless*
|
||
stop_on_stale_lockfile is set to 1 in which case we stop.
|
||
|
||
stop_on_stale_lockfile defaults to 0.
|
||
|
||
=back
|
||
|
||
B<one_fs 1>
|
||
|
||
=over 4
|
||
|
||
Prevents rsync from crossing filesystem partitions. Setting this to a value
|
||
of 1 enables this feature. 0 turns it off. This parameter is optional.
|
||
The default is 0 (off).
|
||
|
||
=back
|
||
|
||
B<use_lazy_deletes 1>
|
||
|
||
=over 4
|
||
|
||
Changes default behavior of rsnapshot and does not initially remove the
|
||
oldest snapshot. Instead it moves that directory to _delete.[processid] and
|
||
continues as normal. Once the backup has been completed, the lockfile will
|
||
be removed before rsnapshot starts deleting the directory.
|
||
|
||
Enabling this means that snapshots get taken sooner (since the delete doesn't
|
||
come first), and any other rsnapshot processes are allowed to start while the
|
||
final delete is happening. This benefit comes at the cost of using more
|
||
disk space. The default is 0 (off).
|
||
|
||
The details of how this works have changed in rsnapshot version 1.3.1.
|
||
Originally you could only ever have one .delete directory per backup level.
|
||
Now you can have many, so if your next (eg) alpha backup kicks off while the
|
||
previous one is still doing a lazy delete you may temporarily have extra
|
||
_delete directories hanging around.
|
||
|
||
=back
|
||
|
||
B<linux_lvm_snapshotsize 2G>
|
||
|
||
=over 4
|
||
|
||
LVM snapshot(s) size (lvcreate --size option).
|
||
|
||
=back
|
||
|
||
B<linux_lvm_snapshotname rsnapshot>
|
||
|
||
=over 4
|
||
|
||
Name to be used when creating the LVM logical volume snapshot(s) (lvcreate --name option).
|
||
|
||
=back
|
||
|
||
B<linux_lvm_vgpath /dev>
|
||
|
||
=over 4
|
||
|
||
Path to the LVM Volume Groups.
|
||
|
||
=back
|
||
|
||
B<linux_lvm_mountpath /mnt/lvm-snapshot>
|
||
|
||
=over 4
|
||
|
||
Mount point to use to temporarily mount the snapshot(s).
|
||
|
||
=back
|
||
|
||
B<backup> /etc/ localhost/
|
||
|
||
B<backup> root@example.com:/etc/ example.com/
|
||
|
||
B<backup> rsync://example.com/path2/ example.com/
|
||
|
||
B<backup> /var/ localhost/ one_fs=1
|
||
|
||
B<backup> lvm://vg0/home/path2/ lvm-vg0/
|
||
|
||
B<backup_script> /usr/local/bin/backup_pgsql.sh pgsql_backup/
|
||
|
||
=over 4
|
||
|
||
Examples:
|
||
|
||
B<backup /etc/ localhost/>
|
||
|
||
=over 4
|
||
|
||
Backs up /etc/ to <snapshot_root>/<retain>.0/localhost/etc/ using rsync on
|
||
the local filesystem
|
||
|
||
=back
|
||
|
||
B<backup /usr/local/ localhost/>
|
||
|
||
=over 4
|
||
|
||
Backs up /usr/local/ to <snapshot_root>/<retain>.0/localhost/usr/local/
|
||
using rsync on the local filesystem
|
||
|
||
=back
|
||
|
||
B<backup root@example.com:/etc/ example.com/>
|
||
|
||
=over 4
|
||
|
||
Backs up root@example.com:/etc/ to <snapshot_root>/<retain>.0/example.com/etc/
|
||
using rsync over ssh
|
||
|
||
=back
|
||
|
||
B<backup example.com:/etc/ example.com/>
|
||
|
||
=over 4
|
||
|
||
Same thing but let ssh choose the remote username (as specified in
|
||
~/.ssh/config, otherwise the same as the local username)
|
||
|
||
=back
|
||
|
||
B<backup root@example.com:/usr/local/ example.com/>
|
||
|
||
=over 4
|
||
|
||
Backs up root@example.com:/usr/local/ to
|
||
<snapshot_root>/<retain>.0/example.com/usr/local/ using rsync over ssh
|
||
|
||
=back
|
||
|
||
B<backup rsync://example.com/pub/ example.com/pub/>
|
||
|
||
=over 4
|
||
|
||
Backs up rsync://example.com/pub/ to <snapshot_root>/<retain>.0/example.com/pub/
|
||
using an anonymous rsync server. Please note that unlike backing up local paths
|
||
and using rsync over ssh, rsync servers have "modules", which are top level
|
||
directories that are exported. Therefore, the module should also be specified in
|
||
the destination path, as shown in the example above (the pub/ directory at the
|
||
end).
|
||
|
||
=back
|
||
|
||
B<backup /var/ localhost/ one_fs=1>
|
||
|
||
=over 4
|
||
|
||
This is the same as the other examples, but notice the fourth column.
|
||
This is how you specify per-backup-point options to override global
|
||
settings. This extra parameter can take several options, separated
|
||
by B<commas>.
|
||
|
||
It is most useful when specifying per-backup rsync excludes thus:
|
||
|
||
B<backup root@somehost:/ somehost +rsync_long_args=--exclude=/var/spool/>
|
||
|
||
Note the + sign. That tells rsync_long_args to I<add> to the list of arguments
|
||
to pass to rsync instead of replacing the list. The + sign is only supported for
|
||
rsnapshot's rsync_long_args and rsync_short_args.
|
||
|
||
=back
|
||
|
||
B<backup lvm://vg0/home/path2/ lvm-vg0/>
|
||
|
||
=over 4
|
||
|
||
Backs up the LVM logical volume called home, of volume group vg0, to
|
||
<snapshot_root>/<retain>.0/lvm-vg0/. Will create, mount, backup, unmount and remove an LVM
|
||
snapshot for each lvm:// entry.
|
||
|
||
=back
|
||
|
||
|
||
B<backup_script /usr/local/bin/backup_database.sh db_backup/>
|
||
|
||
=over 4
|
||
|
||
In this example, we specify a script or program to run. This script should simply
|
||
create files and/or directories in its current working directory. rsnapshot will
|
||
then take that output and move it into the directory specified in the third column.
|
||
|
||
Please note that whatever is in the destination directory will be completely
|
||
deleted and recreated. For this reason, rsnapshot prevents you from specifying
|
||
a destination directory for a backup_script that will clobber other backups.
|
||
|
||
So in this example, say the backup_database.sh script simply runs a command like:
|
||
|
||
#!/bin/sh
|
||
|
||
mysqldump -uusername mydatabase > mydatabase.sql
|
||
|
||
chmod u=r,go= mydatabase.sql # r-------- (0400)
|
||
|
||
rsnapshot will take the generated "mydatabase.sql" file and move it into the
|
||
<snapshot_root>/<retain>.0/db_backup/ directory. On subsequent runs,
|
||
rsnapshot checks the differences between the files created against the
|
||
previous files. If the backup script generates the same output on the next
|
||
run, the files will be hard linked against the previous ones, and no
|
||
additional disk space will be taken up.
|
||
|
||
=back
|
||
|
||
B<backup_exec ssh root@1.2.3.4 "du -sh /.offsite_backup" optional>
|
||
|
||
B<backup_exec rsync -az /.snapshots/daily.0 root@1.2.3.4:/.offsite_backup/ required>
|
||
|
||
B<backup_exec /bin/true/>
|
||
|
||
=over 4
|
||
|
||
B<backup_exec> simply runs the command listed. The second argument is not
|
||
required and defaults to a value of 'optional'. It specifies the importance
|
||
that the command return 0. Valid values are 'optional' and 'required'. If the
|
||
command is specified as optional, a non-zero exit status from the command will
|
||
result in a warning message being output. If the command is specified as
|
||
'required', a non-zero exit status from the command will result in an error
|
||
message being output and rsnapshot itself will exit with a non-zero exit
|
||
status.
|
||
|
||
=back
|
||
|
||
=back
|
||
|
||
Remember that tabs must separate all elements, and that
|
||
there must be a trailing slash on the end of every directory.
|
||
|
||
A hash mark (#) on the beginning of a line is treated
|
||
as a comment.
|
||
|
||
Putting it all together (an example file):
|
||
|
||
=over 4
|
||
|
||
# THIS IS A COMMENT, REMEMBER TABS MUST SEPARATE ALL ELEMENTS
|
||
|
||
config_version 1.2
|
||
|
||
snapshot_root /.snapshots/
|
||
|
||
cmd_rsync /usr/bin/rsync
|
||
cmd_ssh /usr/bin/ssh
|
||
#cmd_cp /bin/cp
|
||
cmd_rm /bin/rm
|
||
cmd_logger /usr/bin/logger
|
||
cmd_du /usr/bin/du
|
||
|
||
linux_lvm_cmd_lvcreate /sbin/lvcreate
|
||
linux_lvm_cmd_lvremove /sbin/lvremove
|
||
linux_lvm_cmd_mount /bin/mount
|
||
linux_lvm_cmd_umount /bin/umount
|
||
|
||
linux_lvm_snapshotsize 2G
|
||
linux_lvm_snapshotname rsnapshot
|
||
linux_lvm_vgpath /dev
|
||
linux_lvm_mountpath /mnt/lvm-snapshot
|
||
|
||
retain alpha 6
|
||
retain beta 7
|
||
retain gamma 7
|
||
retain delta 3
|
||
|
||
backup /etc/ localhost/
|
||
backup /home/ localhost/
|
||
backup_script /usr/local/bin/backup_mysql.sh mysql_backup/
|
||
|
||
backup root@foo.com:/etc/ foo.com/
|
||
backup root@foo.com:/home/ foo.com/
|
||
backup root@mail.foo.com:/home/ mail.foo.com/
|
||
backup rsync://example.com/pub/ example.com/pub/
|
||
backup lvm://vg0/xen-home/ lvm-vg0/xen-home/
|
||
backup_exec echo "backup finished!"
|
||
|
||
=back
|
||
|
||
=back
|
||
|
||
=head1 USAGE
|
||
|
||
B<rsnapshot> can be used by any user, but for system-wide backups
|
||
you will probably want to run it as root.
|
||
|
||
Since backups usually get neglected if human intervention is
|
||
required, the preferred way is to run it from cron.
|
||
|
||
When you are first setting up your backups, you will probably
|
||
also want to run it from the command line once or twice to get
|
||
a feel for what it's doing.
|
||
|
||
Here is an example crontab entry, assuming that backup levels B<alpha>,
|
||
B<beta>, B<gamma> and B<delta> have been defined in F</etc/rsnapshot.conf>
|
||
|
||
=over 4
|
||
|
||
B<0 */4 * * * /usr/local/bin/rsnapshot alpha>
|
||
|
||
B<50 23 * * * /usr/local/bin/rsnapshot beta>
|
||
|
||
B<40 23 * * 6 /usr/local/bin/rsnapshot gamma>
|
||
|
||
B<30 23 1 * * /usr/local/bin/rsnapshot delta>
|
||
|
||
=back
|
||
|
||
This example will do the following:
|
||
|
||
=over 4
|
||
|
||
6 alpha backups a day (once every 4 hours, at 0,4,8,12,16,20)
|
||
|
||
1 beta backup every day, at 11:50PM
|
||
|
||
1 gamma backup every week, at 11:40PM, on Saturdays (6th day of week)
|
||
|
||
1 delta backup every month, at 11:30PM on the 1st day of the month
|
||
|
||
=back
|
||
|
||
It is usually a good idea to schedule the larger backup levels to run a bit before the
|
||
lower ones. For example, in the crontab above, notice that "beta" runs 10 minutes
|
||
before "alpha". The main reason for this is that the beta rotate will
|
||
pull out the oldest alpha and make that the youngest beta (which means
|
||
that the next alpha rotate will not need to delete the oldest alpha),
|
||
which is more efficient. A secondary reason is that it is harder to
|
||
predict how long the lowest backup level will take, since it needs to actually
|
||
do an rsync of the source as well as the rotate that all backups do.
|
||
|
||
If rsnapshot takes longer than 10 minutes to do the "beta" rotate
|
||
(which usually includes deleting the oldest beta snapshot), then you
|
||
should increase the time between the backup levels.
|
||
Otherwise (assuming you have set the B<lockfile> parameter, as is recommended)
|
||
your alpha snapshot will fail sometimes because the beta still has the lock.
|
||
|
||
Remember that these are just the times that the program runs.
|
||
To set the number of backups stored, set the B<retain> numbers in
|
||
F</etc/rsnapshot.conf>
|
||
|
||
To check the disk space used by rsnapshot, you can call it with the "du" argument.
|
||
|
||
For example:
|
||
|
||
=over 4
|
||
|
||
B<rsnapshot du>
|
||
|
||
=back
|
||
|
||
This will show you exactly how much disk space is taken up in the snapshot root. This
|
||
feature requires the UNIX B<du> command to be installed on your system, for it to
|
||
support the "-csh" command line arguments, and to be in your path. You can also
|
||
override your path settings and the flags passed to du using the cmd_du and du_args
|
||
parameters.
|
||
|
||
It is also possible to pass a relative file path as a second argument, to get a report
|
||
on a particular file or subdirectory.
|
||
|
||
=over 4
|
||
|
||
B<rsnapshot du localhost/home/>
|
||
|
||
=back
|
||
|
||
The GNU version of "du" is preferred. The BSD version works well also, but does
|
||
not support the -h flag (use -k instead, to see the totals in kilobytes). Other
|
||
versions of "du", such as Solaris, may not work at all.
|
||
|
||
To check the differences between two directories, call rsnapshot with the "diff"
|
||
argument, followed by two backup levels or directory paths.
|
||
|
||
For example:
|
||
|
||
=over 4
|
||
|
||
B<rsnapshot diff beta.0 beta.1>
|
||
|
||
B<rsnapshot diff beta.0/localhost/etc beta.1/localhost/etc>
|
||
|
||
B<rsnapshot diff /.snapshots/beta.0 /.snapshots/beta.1>
|
||
|
||
=back
|
||
|
||
This will call the C<rsnapshot-diff> program, which will scan both directories
|
||
looking for differences (based on hard links).
|
||
|
||
B<rsnapshot sync>
|
||
|
||
=over 4
|
||
|
||
When B<sync_first> is enabled, rsnapshot must first be called with the B<sync>
|
||
argument, followed by the other usual cron entries. The sync should happen as
|
||
the lowest, most frequent backup level, and right before. For example:
|
||
|
||
=over 4
|
||
|
||
B<0 */4 * * * /usr/local/bin/rsnapshot sync && /usr/local/bin/rsnapshot alpha>
|
||
|
||
B<50 23 * * * /usr/local/bin/rsnapshot beta>
|
||
|
||
B<40 23 1,8,15,22 * * /usr/local/bin/rsnapshot gamma>
|
||
|
||
B<30 23 1 * * /usr/local/bin/rsnapshot delta>
|
||
|
||
=back
|
||
|
||
The sync operation simply runs rsync and all backup scripts. In this scenario, all
|
||
calls simply rotate directories, even the lowest backup level.
|
||
|
||
Please note, that the above "rsnapshot sync && rsnapshot alpha" command will
|
||
skip rotation, whenever rsnapshot finishes its sync operation "with warnings"
|
||
(e.g. some files vanished, while rsync was running).
|
||
If you want to ensure rotation even in case of warnings, then the following
|
||
command may be suitable for your cron job:
|
||
|
||
=over 4
|
||
|
||
B<0 */4 * * * /usr/local/bin/rsnapshot sync || [ $? -eq 2 ] && /usr/local/bin/rsnapshot alpha>
|
||
|
||
=back
|
||
|
||
=back
|
||
|
||
B<rsnapshot sync [dest]>
|
||
|
||
=over 4
|
||
|
||
When B<sync_first> is enabled, all sync behaviour happens during an additional
|
||
sync step (see above). When using the sync argument, it is also possible to specify
|
||
a backup point destination as an optional parameter. If this is done, only backup
|
||
points sharing that destination path will be synced.
|
||
|
||
For example, let's say that example.com is a destination path shared by one or more
|
||
of your backup points.
|
||
|
||
=over 4
|
||
|
||
rsnapshot sync example.com
|
||
|
||
=back
|
||
|
||
This command will only sync the files that normally get backed up into example.com.
|
||
It will NOT get any other backup points with slightly different values (like
|
||
example.com/etc/, for example). In order to sync example.com/etc, you would need to
|
||
run rsnapshot again, using example.com/etc as the optional parameter.
|
||
|
||
=back
|
||
|
||
B<rsnapshot configtest>
|
||
|
||
=over 4
|
||
|
||
Do a quick sanity check to make sure everything is ready to go.
|
||
|
||
=back
|
||
|
||
=head1 EXIT VALUES
|
||
|
||
=over 4
|
||
|
||
B<0> All operations completed successfully
|
||
|
||
B<1> A fatal error occurred
|
||
|
||
B<2> Some warnings occurred, but the backup still finished
|
||
|
||
=back
|
||
|
||
=head1 FILES
|
||
|
||
/etc/rsnapshot.conf
|
||
|
||
=head1 SEE ALSO
|
||
|
||
rsync(1), ssh(1), logger(1), sshd(1), ssh-keygen(1), perl(1), cp(1), du(1), crontab(1)
|
||
|
||
=head1 DIAGNOSTICS
|
||
|
||
Use the B<-t> flag to see what commands would have been executed. This will show
|
||
you the commands rsnapshot would try to run. There are a few minor differences
|
||
(for example, not showing an attempt to remove the lockfile because it wasn't
|
||
really created in the test), but should give you a very good idea what will happen.
|
||
|
||
Using the B<-v>, B<-V>, and B<-D> flags will print increasingly more information
|
||
to STDOUT.
|
||
|
||
Make sure you don't have spaces in the config file that you think are actually tabs.
|
||
|
||
Much other weird behavior can probably be attributed to plain old file system
|
||
permissions and ssh authentication issues.
|
||
|
||
=head1 BUGS
|
||
|
||
Please report bugs (and other comments) to the rsnapshot-discuss mailing list:
|
||
|
||
B<http://lists.sourceforge.net/lists/listinfo/rsnapshot-discuss>
|
||
|
||
=head1 NOTES
|
||
|
||
Make sure your F</etc/rsnapshot.conf> file has all elements separated by tabs.
|
||
See F</etc/rsnapshot.conf.default> for a working example file.
|
||
|
||
Make sure you put a trailing slash on the end of all directory references.
|
||
If you don't, you may have extra directories created in your snapshots.
|
||
For more information on how the trailing slash is handled, see the
|
||
L<rsync(1)> manpage.
|
||
|
||
Make sure to make the snapshot directory chmod 700 and owned by root
|
||
(assuming backups are made by the root user). If the snapshot directory
|
||
is readable by other users, they will be able to modify the snapshots
|
||
containing their files, thus destroying the integrity of the snapshots.
|
||
|
||
If you would like regular users to be able to restore their own backups,
|
||
there are a number of ways this can be accomplished. One such scenario
|
||
would be:
|
||
|
||
Set B<snapshot_root> to B</.private/.snapshots> in F</etc/rsnapshot.conf>
|
||
|
||
Set the file permissions on these directories as follows:
|
||
|
||
drwx------ /.private
|
||
|
||
drwxr-xr-x /.private/.snapshots
|
||
|
||
Export the /.private/.snapshots directory over read-only NFS, a read-only
|
||
Samba share, etc.
|
||
|
||
See the rsnapshot HOWTO for more information on making backups
|
||
accessible to non-privileged users.
|
||
|
||
For ssh to work unattended through cron, you will probably want to use
|
||
public key logins. Create an ssh key with no passphrase for root, and
|
||
install the public key on each machine you want to backup. If you are
|
||
backing up system files from remote machines, this probably means
|
||
unattended root logins. Another possibility is to create a second user
|
||
on the machine just for backups. Give the user a different name such
|
||
as "rsnapshot", but keep the UID and GID set to 0, to give root
|
||
privileges. However, make logins more restrictive, either through ssh
|
||
configuration, or using an alternate shell.
|
||
|
||
BE CAREFUL! If the private key is obtained by an attacker, they will
|
||
have free run of all the systems involved. If you are unclear on how
|
||
to do this, see L<ssh(1)>, L<sshd(1)>, and L<ssh-keygen(1)>.
|
||
|
||
Backup scripts are run as the same user that rsnapshot is running as.
|
||
Typically this is root. Make sure that all of your backup scripts are
|
||
only writable by root, and that they don't call any other programs
|
||
that aren't owned by root. If you fail to do this, anyone who can
|
||
write to the backup script or any program it calls can fully take
|
||
over the machine. Of course, this is not a situation unique to
|
||
rsnapshot.
|
||
|
||
By default, rsync transfers are done using the --numeric-ids option.
|
||
This means that user names and group names are ignored during transfers,
|
||
but the UID/GID information is kept intact. The assumption is that the
|
||
backups will be restored in the same environment they came from. Without
|
||
this option, restoring backups for multiple heterogeneous servers would
|
||
be unmanageable. If you are archiving snapshots with GNU tar, you may
|
||
want to use the --numeric-owner parameter. Also, keep a copy of the
|
||
archived system's F</etc/passwd> and F</etc/group> files handy for the UID/GID
|
||
to name mapping.
|
||
|
||
If you remove backup points in the config file, the previously archived
|
||
files under those points will permanently stay in the snapshots directory
|
||
unless you remove the files yourself. If you want to conserve disk space,
|
||
you will need to go into the <snapshot_root> directory and manually
|
||
remove the files from the smallest backup level's ".0" directory.
|
||
|
||
For example, if you were previously backing up /home/ with a destination
|
||
of localhost/, and alpha is your smallest backup level, you would need to do
|
||
the following to reclaim that disk space:
|
||
|
||
rm -rf <snapshot_root>/alpha.0/localhost/home/
|
||
|
||
Please note that the other snapshots previously made of /home/ will still
|
||
be using that disk space, but since the files are flushed out of alpha.0/,
|
||
they will no longer be copied to the subsequent directories, and will thus
|
||
be removed in due time as the rotations happen.
|
||
|
||
=head1 AUTHORS
|
||
|
||
Mike Rubel - B<http://www.mikerubel.org/computers/rsync_snapshots/>
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Created the original shell scripts on which this project is based
|
||
|
||
=back
|
||
|
||
Nathan Rosenquist (B<nathan@rsnapshot.org>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Primary author and original maintainer of rsnapshot.
|
||
|
||
=back
|
||
|
||
David Cantrell (B<david@cantrell.org.uk>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Previous maintainer of rsnapshot
|
||
|
||
=item -
|
||
Wrote the C<rsnapshot-diff> utility
|
||
|
||
=item -
|
||
Improved how use_lazy_deletes work so slow deletes don't screw up the next
|
||
backup at that backup level.
|
||
|
||
=back
|
||
|
||
David Keegel <djk@cybersource.com.au>
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Previous rsnapshot maintainer
|
||
|
||
=item -
|
||
Fixed race condition in lock file creation, improved error reporting
|
||
|
||
=item -
|
||
Allowed remote ssh directory paths starting with "~/" as well as "/"
|
||
|
||
=item -
|
||
Fixed a number of other bugs and buglets
|
||
|
||
=back
|
||
|
||
Benedikt Heine <bebe@bebehei.de>
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
ex-maintainer of rsnapshot (2015-2017)
|
||
|
||
=back
|
||
|
||
Carl Wilhelm Soderstrom (B<chrome@real-time.com>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Created the RPM .spec file which allowed the RPM package to be built, among
|
||
other things.
|
||
|
||
=back
|
||
|
||
Ted Zlatanov (B<tzz@lifelogs.com>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Added the one_fs feature, autoconf support, good advice, and much more.
|
||
|
||
=back
|
||
|
||
Ralf van Dooren (B<r.vdooren@snow.nl>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Added and maintains the rsnapshot entry in the FreeBSD ports tree.
|
||
|
||
=back
|
||
|
||
SlapAyoda
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Provided access to his computer museum for software testing.
|
||
|
||
=back
|
||
|
||
Carl Boe (B<boe@demog.berkeley.edu>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Found several subtle bugs and provided fixes for them.
|
||
|
||
=back
|
||
|
||
Shane Leibling (B<shane@cryptio.net>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Fixed a compatibility bug in utils/backup_smb_share.sh
|
||
|
||
=back
|
||
|
||
Christoph Wegscheider (B<christoph.wegscheider@wegi.net>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Added (and previously maintained) the Debian rsnapshot package.
|
||
|
||
=back
|
||
|
||
Bharat Mediratta (B<bharat@menalto.com>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Improved the exclusion rules to avoid backing up the snapshot root (among
|
||
other things).
|
||
|
||
=back
|
||
|
||
Peter Palfrader (B<weasel@debian.org>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Enhanced error reporting to include command line options.
|
||
|
||
=back
|
||
|
||
Nicolas Kaiser (B<nikai@nikai.net>)
|
||
|
||
=over 4
|
||
|
||
=item -
|
||
Fixed typos in program and man page
|
||
|
||
=back
|
||
|
||
Chris Petersen - (B<http://www.forevermore.net/>)
|
||
|
||
=over 4
|
||
|
||
Added cwrsync permanent-share support
|
||
|
||
=back
|
||
|
||
Robert Jackson (B<RobertJ@promedicalinc.com>)
|
||
|
||
=over 4
|
||
|
||
Added use_lazy_deletes feature
|
||
|
||
=back
|
||
|
||
Justin Grote (B<justin@grote.name>)
|
||
|
||
=over 4
|
||
|
||
Improved rsync error reporting code
|
||
|
||
=back
|
||
|
||
Anthony Ettinger (B<apwebdesign@yahoo.com>)
|
||
|
||
=over 4
|
||
|
||
Wrote the C<utils/mysqlbackup.pl> script
|
||
|
||
=back
|
||
|
||
Sherman Boyd
|
||
|
||
=over 4
|
||
|
||
Wrote C<utils/random_file_verify.sh> script
|
||
|
||
=back
|
||
|
||
William Bear (B<bear@umn.edu>)
|
||
|
||
=over 4
|
||
|
||
Wrote the C<utils/rsnapreport.pl> script (pretty summary of rsync stats)
|
||
|
||
=back
|
||
|
||
Eric Anderson (B<anderson@centtech.com>)
|
||
|
||
=over 4
|
||
|
||
Improvements to C<utils/rsnapreport.pl>.
|
||
|
||
=back
|
||
|
||
Alan Batie (B<alan@batie.org>)
|
||
|
||
=over 4
|
||
|
||
Bug fixes for include_conf
|
||
|
||
=back
|
||
|
||
Dieter Bloms (B<dieter@bloms.de>)
|
||
|
||
=over 4
|
||
|
||
Multi-line configuration options
|
||
|
||
=back
|
||
|
||
Henning Moll (B<newsScott@gmx.de>)
|
||
|
||
=over 4
|
||
|
||
stop_on_stale_lockfile
|
||
|
||
=back
|
||
|
||
Ben Low (B<ben@bdlow.net>)
|
||
|
||
=over 4
|
||
|
||
Linux LVM snapshot support
|
||
|
||
=back
|
||
|
||
=head1 COPYRIGHT
|
||
|
||
Copyright (C) 2003-2005 Nathan Rosenquist
|
||
|
||
Portions Copyright (C) 2002-2007 Mike Rubel, Carl Wilhelm Soderstrom,
|
||
Ted Zlatanov, Carl Boe, Shane Liebling, Bharat Mediratta, Peter Palfrader,
|
||
Nicolas Kaiser, David Cantrell, Chris Petersen, Robert Jackson, Justin Grote,
|
||
David Keegel, Alan Batie, Dieter Bloms, Henning Moll, Ben Low, Anthony
|
||
Ettinger
|
||
|
||
This man page is distributed under the same license as rsnapshot:
|
||
the GPL (see below).
|
||
|
||
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 2 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, write to the Free Software Foundation, Inc.,
|
||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||
|
||
=cut
|
||
|
||
# more emacs-appeasement
|
||
######################################################################
|
||
### Local Variables:
|
||
### tab-width: 4
|
||
### End:
|