PageRenderTime 69ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/rsync-backup/rsync-backup.pl

https://github.com/evilchili/automagickus
Perl | 1429 lines | 1091 code | 215 blank | 123 comment | 114 complexity | 9154b07c1921fdee2fe99cac93697ab5 MD5 | raw file
  1. #!/usr/bin/perl -I .
  2. #
  3. # rsync-backup.pl
  4. # - use rsync to manage remote backups
  5. #
  6. # Author: Greg Boyington <greg@dautomagick.us>.
  7. # Based on the mirror bash script by Stu Sheldon <stu@actusa.net>.
  8. #
  9. use strict;
  10. use File::Rsync;
  11. use File::stat;
  12. use Getopt::Std;
  13. use Sys::Syslog;
  14. use POSIX qw/:sys_wait_h/;
  15. if ( $^O =~ /bsd|darwin/i ) {
  16. require Proc::ProcessTable;
  17. }
  18. $|=1;
  19. my %ARGS;
  20. my $TODAY;
  21. my $HOUR;
  22. my $CURRENT_LABEL;
  23. my %spawn;
  24. # where to find things
  25. #
  26. my $cp_cmd = '/bin/cp -alf';
  27. my $touch_cmd = '/usr/bin/touch';
  28. my $ssh_cmd = '/usr/bin/ssh';
  29. my $mount_cmd = '/bin/mount';
  30. my $umount_cmd = '/bin/umount';
  31. my $tar_cmd = '/bin/tar';
  32. my $find_cmd = '/usr/bin/find';
  33. # Set this to non-zero if your copy command cannot
  34. # intelligently deal with symlinks (if it can, please
  35. # email me and tell me about it! :P)
  36. #
  37. my $recreate_symlinks = 1;
  38. # format today's date.
  39. my ($sec,$min,$h,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  40. $TODAY = sprintf('%04d-%02d-%02d',$year+=1900,++$mon,$mday);
  41. $HOUR = $h;
  42. # log output to syslog
  43. sub log() {
  44. my $ident = shift;
  45. openlog("rsync-backup.pl", "ndelay,perror", "error");
  46. syslog( $ident, join( " ", @_) );
  47. closelog();
  48. }
  49. # log output to a filehandle, if given, syslog, if in debug mode, and
  50. # STDOUT if in verbose or debug mode.
  51. sub notify {
  52. # if we were handed a filehandle, direct the output there first
  53. my $fh;
  54. if ( ref $_[0] ) {
  55. $fh = shift @_;
  56. print $fh @_;
  57. }
  58. # if the debug flag is set, echo to syslog
  59. &log("LOG_NOTICE", @_)
  60. if $ARGS{'D'};
  61. # finally, send it to STDOUT for verbose logging
  62. print @_ if $ARGS{'v'};
  63. }
  64. # log errors to a filehandle, if any, and syslog, and STDERR.
  65. sub fail {
  66. # if we were handed a filehandle, direct the output there first
  67. my $fh;
  68. if ( ref $_[0] ) {
  69. $fh = shift @_;
  70. print $fh @_;
  71. }
  72. # now log the error to syslog and exit.
  73. &log("LOG_ERR", @_ );
  74. warn "ERROR: " . join "\n",@_;
  75. exit 1;
  76. }
  77. # set up a zombie reaper to clean up after children and
  78. # clear up a slot for a new child to be spawned.
  79. $SIG{CHLD} = \&REAPER;
  80. sub REAPER {
  81. my $pid;
  82. while ( ( $pid = waitpid(-1, &WNOHANG) ) > 0 ) {
  83. notify "Child process $pid has exited.\n"
  84. if delete $spawn{ $pid } && $ARGS{'v'};
  85. }
  86. $SIG{CHLD} = \&REAPER;
  87. }
  88. # an INT to the parent process should reap the children.
  89. $SIG{INT} = $SIG{TERM} = sub {
  90. $SIG{CHLD} = 'IGNORE';
  91. fail "Interrupt detected! Terminating all child processes...";
  92. kill('TERM',$_) foreach keys %spawn;
  93. };
  94. # parse command-line arguments
  95. getopts ('n:htvDf:',\%ARGS);
  96. if ( $ARGS{'D'} && ! $ARGS{'v'} ) {
  97. $ARGS{'v'}++;
  98. }
  99. if ( $ARGS{'v'} ) {
  100. print "rsync-backup.pl Copyright (c) 2003-2009 Gold & Appel Development.\n";
  101. print "Beginning test run as UID $>...\n" if $ARGS{'t'};
  102. }
  103. if ( $ARGS{'h'} ) {
  104. die "Usage: $0 [-v | -D ] [ -t ] [ -h ] [ -n num ] [ -f config_file ] [ label... ]\nperldoc $0 for detailed help.\n";
  105. }
  106. # how many processes will we fork by default?
  107. my $max_children;
  108. if (! defined $ARGS{'n'} ) {
  109. $max_children = 5;
  110. } elsif ( $ARGS{'n'} == 1 ) {
  111. warn "Warning: Refusing to spawn one child process at a time. Increase -n to 2 or more.\n";
  112. $max_children = 0;
  113. } else {
  114. $max_children = $ARGS{'n'};
  115. }
  116. if ( $max_children && $ARGS{'t'} ) {
  117. warn "Warning: Threaded mode is disabled for test runs.\n";
  118. $max_children=0;
  119. }
  120. # read the configuration file
  121. my $config_file = $ARGS{'f'}||'rsync-targets';
  122. print "Parsing configuration file '$config_file'...\n" if $ARGS{'v'};
  123. my $conf = &readConfig($config_file);
  124. foreach (@ARGV) {
  125. die "Error: unknown config block '$_'\n" unless exists $conf->{$_};
  126. }
  127. # do we have something to do?
  128. die "$config_file contains no valid config blocks; exiting.\n"
  129. unless keys %$conf;
  130. # ensure the executables exist and are executable
  131. print "Checking for required executables...\n" if $ARGS{'v'};
  132. foreach ( $cp_cmd, $touch_cmd, $ssh_cmd, $mount_cmd, $umount_cmd, $tar_cmd, $find_cmd ) {
  133. /(.+?)(?:\s+[\-\w]+)?$/;
  134. die "Required executable $1 doesn't exist or isn't executable!" unless -e $1;
  135. }
  136. # create an rsync object
  137. my $rsync = File::Rsync->new( {
  138. # 'rsh' => "trickle -s -d 500 -u 500 $ssh_cmd",
  139. 'archive' => 1,
  140. 'compress' => 1,
  141. 'relative' => 1,
  142. 'delete' => 1,
  143. 'quiet' => 0,
  144. } );
  145. my @tarballs_to_make;
  146. my @umounts_to_do;
  147. my @labels = @ARGV ? @ARGV : sort keys %$conf;
  148. $max_children = scalar @labels unless $max_children < scalar @labels;
  149. $max_children = 0 if $max_children == 1;
  150. # if we're not going to spawn children, step through each label
  151. # and perform the requested backup operation.
  152. if ( $max_children==0 ) {
  153. &launch_backup( $_ ) while ( $_ = shift @labels );
  154. print "Done!\n" if $ARGS{'v'};
  155. # If we are spawning children, we'll fork $max_children times
  156. # and then sleep until a child exits. When it does, we'll spawn
  157. # a new process, and continue until all backup operations have
  158. # completed.
  159. } else {
  160. while ( @labels ) {
  161. # spawn a new child process if we have space for another
  162. if ( keys %spawn < $max_children ) {
  163. my $label = shift @labels;
  164. chomp $label;
  165. my $pid;
  166. if ( !defined( $pid = fork() ) ) {
  167. fail "Couldn't spawn a child process! ARRGH!";
  168. # child process executes the backup
  169. } elsif ( ! $pid ) {
  170. $0 = "rsync-backup.pl [$label]";
  171. $SIG{INT} = 'DEFAULT';
  172. &launch_backup( $label );
  173. exit;
  174. # parent remembers how many children have spawned
  175. } else {
  176. $spawn{ $pid }++;
  177. print "spawned child $pid " . ( scalar keys %spawn ) . " of $max_children for $label.\n"
  178. if $ARGS{'v'};
  179. }
  180. # maximum number of processes have been spawned, so try again later.
  181. } else {
  182. sleep 1;
  183. }
  184. }
  185. while (scalar keys %spawn ) {
  186. sleep 1;
  187. }
  188. print "Done!\n"
  189. if $ARGS{'v'};
  190. }
  191. exit;
  192. ######################################################################
  193. #
  194. # sub roll_tarball( %args )
  195. # - create an archive in tar format of the given directory,
  196. # automatically splitting it into chunks as needed.
  197. #
  198. # %args - hash containing the following keys:
  199. # label - the config block's label
  200. # c - the config block hashref
  201. # wdir - the working directory
  202. # cmd - the tar command to execute
  203. # destname - the archive's filename
  204. #
  205. # XXX: This whole routine is crufty and should be rewritten.
  206. #
  207. ######################################################################
  208. sub roll_tarball() {
  209. my %args = @_;
  210. my $pwd = `pwd`;
  211. my $label = $args{'c'}->{'label'};
  212. if ( $ARGS{'t'} ) {
  213. notify "[$label] Would create a tarball of $args{'wdir'}" if $ARGS{'v'};
  214. next;
  215. }
  216. unless ( chdir $args{'wdir'} ) {
  217. notify "[$label] Couldn't change to $args{'wdir'}; skipping this tarball.";
  218. }
  219. # execute the tar and split
  220. system $args{cmd};
  221. # did we create only one file? If so, remove the split extension
  222. my $tar_dir = $args{'c'}->{'snapshot-path'}.'/'.$args{'label'}.'/tarballs';
  223. if ( ! -e $tar_dir.'/'.$args{'destname'}.'aab' ) {
  224. my $tgt = $tar_dir.'/'.$args{'destname'};
  225. chop $tgt;
  226. rename $tar_dir.'/'.$args{'destname'}.'aaa', $tgt
  227. or notify "[$label] Couldn't rename $tar_dir/$args{'destname'}aaa $tgt: $!";
  228. }
  229. chdir $pwd;
  230. }
  231. print "Done!\n" if $ARGS{'v'};
  232. # Time for Jell-O(tm)
  233. exit;
  234. ######################################################################
  235. #
  236. # launch_backup( $label )
  237. # - perform a complete backup operation for the given config block.
  238. #
  239. # $label - the config block for which to execute a backup.
  240. #
  241. ######################################################################
  242. sub launch_backup() {
  243. my $label = shift;
  244. $CURRENT_LABEL = $label;
  245. my $c = $conf->{ $label };
  246. my $lockfile = $c->{'snapshot-path'}.'/'.$label.'/syncing_now';
  247. my $lastrunfile = $c->{'snapshot-path'}.'/'.$label.'/last_run';
  248. my $backupconfigfile = $c->{'snapshot-path'}.'/'.$label.'/backup.config';
  249. # create the directory hierarchy if need be.
  250. foreach my $d ( $c->{'snapshot-path'},
  251. $c->{'snapshot-path'}.'/'.$label,
  252. $c->{'snapshot-path'}.'/'.$label.'/working',
  253. $c->{'snapshot-path'}.'/'.$label.'/hourly',
  254. $c->{'snapshot-path'}.'/'.$label.'/daily',
  255. $c->{'snapshot-path'}.'/'.$label.'/weekly',
  256. $c->{'snapshot-path'}.'/'.$label.'/monthly',
  257. $c->{'snapshot-path'}.'/'.$label.'/tarballs' ) {
  258. if ( ! -d $d ) {
  259. mkdir $d or die( "[$label] Couldn't create $d: $!");
  260. }
  261. }
  262. my $logfile = $c->{'snapshot-path'} . '/' . $label . '/backup.log';
  263. open (LOG, '>' . $logfile)
  264. or fail "[$label] Couldn't open $logfile for writing: $!";
  265. my $isMounted=0;
  266. # We do the work in an eval {} block so that if there's
  267. # an unrecoverable error on one block, we don't lose them all.
  268. #
  269. eval {
  270. # if we have mount-* options, verify the specified mount exists.
  271. if ($c->{'mount-point'} && $c->{'mount-dev'} ) {
  272. notify \*LOG, "[$label] Checking mount-* options...\n";
  273. $isMounted = &isMounted( $c->{'mount-dev'}, $c->{'mount-point'}, $c->{'mount-type'} );
  274. # If we also have mount-on-startup, try to mount the filesystem ourselves.
  275. if ( $c->{'mount-on-startup'} eq 'yes' && ! $isMounted ) {
  276. fail (\*LOG, "[$label] Cannot mount on startup without a mount-type.")
  277. unless $c->{'mount-type'};
  278. if ( $c->{'mount-flags'} ) {
  279. system $mount_cmd, $c->{'mount-flags'}, '-t', $c->{'mount-type'}, $c->{'mount-dev'}, $c->{'mount-point'};
  280. } else {
  281. system $mount_cmd, '-t', $c->{'mount-type'}, $c->{'mount-dev'}, $c->{'mount-point'};
  282. }
  283. if ( $@ ) {
  284. fail \*LOG, "[$label] Couldn't mount $c->{'mount-dev'} on $c->{'mount-point'}: $@";
  285. }
  286. # make sure the mount actually succeeded -- sometimes mount exits with success
  287. # even though some sort of error occurred (eg. with BSD vfs.usermount).
  288. #
  289. $isMounted = &isMounted( $c->{'mount-dev'}, $c->{'mount-point'}, $c->{'mount-type'} );
  290. fail ( \*LOG, "[$label] Mount of $c->{'mount-dev'} failed!" ) unless $isMounted;
  291. }
  292. # squawk if we don't see our mount point.
  293. #
  294. fail ( \*LOG, "[$label] $c->{'mount-dev'} is not mounted on $c->{'mount-point'}, or is the wrong FS type." )
  295. unless $isMounted;
  296. }
  297. if ( $c->{'use-rsyncd'} eq 'yes' ) {
  298. foreach my $d ( split /:/, $c->{'path'} ) {
  299. my $dir = join '/', $c->{'snapshot-path'}, $label, 'working', $d;
  300. if ( ! -d $dir ) {
  301. mkdir $dir or fail ( \*LOG, "[$label] Couldn't create $dir: $!" );
  302. }
  303. }
  304. }
  305. # check for an existing lock file, and create one if necessary.
  306. unless ( $ARGS{'t'} ) {
  307. notify \*LOG, "[$label] Checking lock file $lockfile...\n";
  308. system ( $touch_cmd, $lockfile ) unless -e $lockfile;
  309. fail ( \*LOG, "[$label] Unable to create $lockfile!" ) unless -e $lockfile;
  310. # open up the lockfile and look for a process id.
  311. open (LOCKFILE, "+<".$lockfile) or fail ( \*LOG, "[$label] Couldn't open existing lockfile: $!" );
  312. flock(LOCKFILE,2)
  313. or fail ( \*LOG, "[$label] Couldn't flock $lockfile: $!" );
  314. my $pid = <LOCKFILE>;
  315. chomp $pid;
  316. # if we have a process id, check the process table to see if it's running.
  317. if ( $pid ) {
  318. # BSD systems can use Proc::ProcessTable
  319. #
  320. if ( $^O =~ /bsd|darwin/i ) {
  321. my $t = new Proc::ProcessTable;
  322. foreach ( @{$t->table} ) {
  323. if ( $_->pid == $pid ) {
  324. fail ( \*LOG, "[$label] Lockfile exists for running process $pid: ".$_->cmndline );
  325. }
  326. }
  327. } else {
  328. my $running = `ps --no-heading --pid $pid`;
  329. fail ( \*LOG, "[$label] Lockfile existws for running process $pid: $running" )
  330. if $running;
  331. }
  332. }
  333. # we didn't find the old pid in the process table, so we can steal the lock.
  334. seek(LOCKFILE,0,0);
  335. print LOCKFILE $$;
  336. close LOCKFILE;
  337. }
  338. # back up our config block
  339. notify \*LOG, "[$label] Checking that $backupconfigfile is writable...\n";
  340. fail ( \*LOG, "[$label] $backupconfigfile is not writable by uid $>!") unless ( ! -e $backupconfigfile || -w _ );
  341. if ( ! $ARGS{'t'} ) {
  342. notify \*LOG, "[$label] Backing up config to $backupconfigfile...\n";
  343. open (BACKUPCONFIG, '>'.$backupconfigfile)
  344. or fail "[$label] Couldn't open $backupconfigfile for output: $!";
  345. print BACKUPCONFIG $c->{__CONFIG_BLOCK};
  346. close(BACKUPCONFIG);
  347. }
  348. # do the sync, unless we're in test mode
  349. notify \*LOG, "[$label] Beginning rsync transfer...\n" if ! $ARGS{'t'};
  350. unless ( $ARGS{'t'} ) {
  351. my @excludes = split(/:/,$c->{'excludes'});
  352. foreach my $p ( split( /:/, $c->{'path'} ) ) {
  353. notify \*LOG, "[$label] source: " . $c->{'hostname'}. ( $c->{'use-rsyncd'} eq 'yes' ? '::' : ':' ) . $p . "\n";
  354. notify \*LOG, "[$label] dest: " . $c->{'snapshot-path'}.'/'.$label.'/working' . ( $c->{'use-rsyncd'} eq 'yes' ? '/' . $p : '' ) . "\n";
  355. $rsync->exec( {
  356. ( $c->{'bandwidth-limit'} ? ( bwlimit => $c->{'bandwidth-limit'} ) : () ),
  357. source => $c->{'hostname'}. ( $c->{'use-rsyncd'} eq 'yes' ? '::' : ':' ) . $p,
  358. destination => $c->{'snapshot-path'}.'/'.$label.'/working' . ( $c->{'use-rsyncd'} eq 'yes' ? '/' . $p : '' ),
  359. exclude => \@excludes,
  360. } );
  361. my $err = &parse_rsync_errors( $rsync->err );
  362. fail (\*LOG, "[$label] " . $err) if $err;
  363. }
  364. }
  365. notify \*LOG, "[$label] rsync transfer OK.\n" if ! $ARGS{'t'};
  366. notify \*LOG, "[$label] Beginning snapshot rotations...\n";
  367. # figure out if we've already run today.
  368. my $lastruntime;
  369. my $lastrunday;
  370. my $stat = stat($lastrunfile);
  371. if ( -e _ ) {
  372. # get the day of the year of the modification time of lastrunfile.
  373. $lastrunday = (localtime($stat->mtime))[7];
  374. }
  375. # if it's the first run on the first of the month, do a monthly rotation.
  376. if ( $mday==1 && $lastrunday != $yday ) {
  377. if ( $ARGS{'t'} ) {
  378. notify \*LOG, "[$label] Would do monthly rotation.\n";
  379. } else {
  380. &rotate_snapshot('monthly',$label,$c, \*LOG)
  381. }
  382. }
  383. # if it's the first run on sunday, do a weekly rotation.
  384. if ( $wday==0 && $lastrunday != $yday ) {
  385. if ( $ARGS{'t'} ) {
  386. notify \*LOG, "[$label] Would do a weekly rotation.\n";
  387. } else {
  388. &rotate_snapshot('weekly',$label,$c, \*LOG);
  389. }
  390. }
  391. # if it's the first run on a new day, create a daily snapshot.
  392. if ( $lastrunday != $yday ) {
  393. if ( $ARGS{'t'} ) {
  394. notify \*LOG, "[$label] Would do a daily rotation.\n";
  395. } else {
  396. &rotate_snapshot('daily',$label,$c, \*LOG);
  397. }
  398. }
  399. notify \*LOG, "[$label] Snapshot rotations OK.\n";
  400. # create the hourly snapshot
  401. unless ( $ARGS{'t'} ) {
  402. notify \*LOG, "[$label] Creating an hourly snapshot...\n";
  403. &rotate_snapshot('hourly',$label,$c, \*LOG);
  404. }
  405. # remember when we completed this run
  406. unless ( $ARGS{'t'} ) {
  407. notify \*LOG, "[$label] Updating last_run date...\n";
  408. system $touch_cmd, '-t', sprintf('%04d%02d%02d%02d%02d',$year,$mon,$mday,$HOUR,$min), $lastrunfile;
  409. if ( $@ ) {
  410. fail \*LOG, "[$label] Couldn't touch -t $year$mon$mday$HOUR$min $lastrunfile: $!";
  411. }
  412. }
  413. };
  414. if ($@) {
  415. notify \*LOG, "[$label] Backup of '$label' encountered errors:\n$@";
  416. }
  417. # remove the lock file
  418. notify \*LOG, "[$label] Removing lock file...\n";
  419. if ( -e $lockfile ) {
  420. unlink $lockfile or fail( \*LOG, "[$label] Couldn't remove $lockfile!" );
  421. }
  422. # unmount the filesystem, if need be.
  423. if ( $c->{'umount-on-shutdown'} eq 'yes' && $isMounted ) {
  424. fail ( \*LOG, "[$label] Cannot unmount without a mount point; how did this happen?" )
  425. unless $c->{'mount-point'};
  426. notify \*LOG, "[$label] Unmounting $c->{'mount-point'}...\n";
  427. system $umount_cmd, $c->{'mount-point'};
  428. if ( $@ ) {
  429. fail \*LOG, "[$label] Couldn't unmount $c->{'mount-point'}: $@";
  430. }
  431. }
  432. # If we're in test mode, the final thing we test is that the backups
  433. # are uptodate. We do this by looking at the last_run file, and making
  434. # sure that a successful run of this config block has happened within
  435. # the allotted timespan. If it hasn't, we immediately raise the alarm.
  436. #
  437. if ( $ARGS{'t'} ) {
  438. notify \*LOG, "[$label] Comparing last run date to backup schedule...\n";
  439. my $stat = stat($lastrunfile);
  440. if ( -e _ ) {
  441. my $hdir = $c->{'snapshot-path'}.'/'.$label.'/hourly';
  442. opendir ( HDIR, $hdir )
  443. or fail "[$label] Couldn't open hourly dir: $!";
  444. my $count = grep { !/^\./ } readdir(HDIR);
  445. closedir(DIR);
  446. if ( $count < $c->{'snapshot-hourly'} ) {
  447. my $max_time = 3600 * 24 / $c->{'snapshot-hourly'};
  448. fail "[$label] backup is out-of-date!" if ( time - $stat->mtime ) > $max_time;
  449. }
  450. }
  451. }
  452. notify \*LOG, "[$label] " . ( $ARGS{'t'} ? 'Test run' : 'Backup' ) . " complete!\n";
  453. close(LOG);
  454. }
  455. ######################################################################
  456. #
  457. # sub isMounted( $dev, $point, $type )
  458. #
  459. # determine if a given device is mounted on the specified mount point
  460. #
  461. ######################################################################
  462. sub isMounted() {
  463. my ($dev,$point,$type) = @_;
  464. return 0, "Must have a device and a mount point!"
  465. unless $dev && $point;
  466. my @mounts = `$mount_cmd`;
  467. my $m=0;
  468. foreach ( @mounts ) {
  469. if ( /^$dev on $point/ ) {
  470. next if ( $type && ! /\($type\b/ );
  471. $m=1;
  472. last;
  473. }
  474. }
  475. return $m;
  476. }
  477. ######################################################################
  478. #
  479. # sub rotate_snapshot( $type, $label, $c, LOG )
  480. #
  481. # uses hardlinks to move around snapshots of the working directory
  482. #
  483. # $type - must be one of 'hourly','daily','weekly', or 'monthly'
  484. # $label - name of the config block we're working with
  485. # $c - the config block itself
  486. # LOG - filehandle of log to write to
  487. #
  488. # dies on error
  489. #
  490. ######################################################################
  491. sub rotate_snapshot() {
  492. my $type = shift;
  493. my $label = shift;
  494. my $c = shift;
  495. my $LOG = shift;
  496. my ($src,@dirs);
  497. # create the snapshot
  498. #
  499. if ( $type eq 'hourly' ) {
  500. my $wdir = $c->{'snapshot-path'}.'/'.$label.'/working';
  501. my $hourly_dir = sprintf('%s/%s/%s/%s_%02d/', $c->{'snapshot-path'},$label,$type,$TODAY,$HOUR);
  502. my $err = `$cp_cmd "$wdir" "$hourly_dir" 2>&1 |grep -vi "operation not permitted"`;
  503. #my $err = `$cp_cmd "$wdir" "$hourly_dir" 2>&1`;
  504. fail ( $LOG, "[$label] $cp_cmd failed? $err" ) if $err;
  505. # Since we can't create a hard link of a symlink, the cp command above will fail
  506. # We therefore use rsync to copy the symlinks into the snapshot dir.
  507. #
  508. if ( $recreate_symlinks ) {
  509. # this is the sneaky bit. the 'infun' coderef
  510. # will use $find_cmd to locate all the symbolic links
  511. # in the working directory and return them as a
  512. # null-delimited list. We remove the $wdir from the
  513. # pathname of each symlink and print the list, for use
  514. # as input to the files-from option of rsync. See
  515. # perldoc File::Rsync and man rsync for details.
  516. #
  517. my $symlinker = File::Rsync->new( {
  518. 'archive' => 1,
  519. 'relative' => 1,
  520. 'quiet' => 1,
  521. 'from0' => 1,
  522. 'files-from' => '-',
  523. 'infun' => sub { print map { s[$wdir][]g; $_ } `$find_cmd $wdir -type l -print0` },
  524. } );
  525. $symlinker->exec( {
  526. source => $wdir,
  527. destination => $hourly_dir
  528. } ) or fail ( $LOG, "[$label] symlink failed:\n".join("\n",$symlinker->err) );
  529. }
  530. # if we're creating a daily snapshot, take the newest hourly snapshot
  531. # and move it into the daily/ directory.
  532. #
  533. } elsif ( $type eq 'daily' ) {
  534. opendir(DIR,$c->{'snapshot-path'}.'/'.$label.'/hourly')
  535. or fail ( $LOG, "[$label] Couldn't open hourly: $!" );
  536. @dirs = grep { !/^\./ } sort readdir(DIR);
  537. closedir(DIR);
  538. $src = pop @dirs;
  539. if ( $src ) {
  540. rename "$c->{'snapshot-path'}/$label/hourly/$src", "$c->{'snapshot-path'}/$label/daily/$src"
  541. or fail ( $LOG, "[$label] Couldn't move $c->{'snapshot-path'}/$label/hourly/$src to $c->{'snapshot-path'}/$label/daily/$src: $!");
  542. } else {
  543. notify "[$label] No hourly snapshots found for $label." if $ARGS{'v'};
  544. }
  545. # if we're creating a weekly snapshot, take the newest daily snapshot
  546. # and move it into the weekly/ directory.
  547. #
  548. } elsif ( $type eq 'weekly' ) {
  549. opendir(DIR,$c->{'snapshot-path'}.'/'.$label.'/daily')
  550. or fail $LOG, "[$label] Couldn't open daily: $!";
  551. @dirs = grep { !/^\./ } sort readdir(DIR);
  552. closedir(DIR);
  553. $src = pop @dirs;
  554. if ( $src ) {
  555. rename "$c->{'snapshot-path'}/$label/daily/$src", "$c->{'snapshot-path'}/$label/weekly/$src"
  556. or fail $LOG, "[$label] Couldn't move $label/daily/$src to $label/weekly/: $!";
  557. } else {
  558. notify "[$label] No daily snapshots found for $label." if $ARGS{'v'};
  559. }
  560. # if we're creating a monthly snapshot, take the newest weekly snapshot
  561. # and move it into the monthly/ directory.
  562. #
  563. } elsif ( $type eq 'monthly' ) {
  564. opendir(DIR,$c->{'snapshot-path'}.'/'.$label.'/weekly')
  565. or fail $LOG, "[$label] Couldn't open weekly: $!";
  566. @dirs = grep { !/^\./ } sort readdir(DIR);
  567. closedir(DIR);
  568. $src = pop @dirs;
  569. if ( $src ) {
  570. rename "$c->{'snapshot-path'}/$label/weekly/$src", "$c->{'snapshot-path'}/$label/monthly/$src"
  571. or fail $LOG, "[$label] Couldn't move $label/weekly/$src to $label/monthly/: $!";
  572. # do we need to create a tarball?
  573. if ( lc $c->{'create-tarballs'} eq 'yes' ) {
  574. # create an appropriate file name for the slices
  575. my ($src_y,$src_m,$src_d) = ( $src =~ /(\d{4})-(\d\d)-(\d\d)/ );
  576. my $destname = sprintf('%s_%04d-%02d-%02d.tgz_',$label,$src_y,$src_m,$src_d);
  577. $destname =~ s/\//_/g;
  578. my $wdir = "$c->{'snapshot-path'}/$label/monthly";
  579. # what we're actually going to do.
  580. my $cmd = sprintf( '%s --preserve -czvf - %s | split -a3 -b %s - %s/%s',
  581. $tar_cmd,
  582. $src,
  583. $c->{'tarball-size'},
  584. $c->{'snapshot-path'}.'/'.$label.'/tarballs',
  585. $destname );
  586. # now do it
  587. &roll_tarball(
  588. {
  589. label => $label,
  590. c => $c,
  591. destname => $destname,
  592. wdir => $wdir,
  593. cmd => $cmd
  594. }
  595. );
  596. }
  597. } else {
  598. notify "[$label] No weekly snapshots found for $label.\n" if $ARGS{'v'};
  599. }
  600. }
  601. # count the number of snapshots in the directory.
  602. #
  603. opendir(DIR,$c->{'snapshot-path'}.'/'.$label.'/'.$type)
  604. or fail $LOG, "[$label] Couldn't open $type: $!";
  605. @dirs = grep { !/^\./ } sort readdir(DIR);
  606. my $count = scalar @dirs;
  607. closedir(DIR);
  608. # if we now have too many snapshots,
  609. # delete the oldest until we're within the limit.
  610. #
  611. while ( scalar(@dirs) > $c->{'snapshots-'.$type} ) {
  612. my $d = shift @dirs;
  613. system "rm", '-rf', $c->{'snapshot-path'}.'/'.$label.'/'.$type.'/'.$d;
  614. fail ( $LOG, "[$label] Couldn't remove stale $type snapshot $d: $!" ) if $@;
  615. }
  616. }
  617. ######################################################################
  618. #
  619. # sub parse_rsync_errors()
  620. #
  621. # Step through the errors generated by rsync and determine if any of
  622. # them should be considered fatal. Errors treated as non-fatal
  623. # include:
  624. #
  625. # - device busy
  626. # - file vanished
  627. #
  628. ######################################################################
  629. sub parse_rsync_errors() {
  630. my @lines = @_;
  631. my @err;
  632. foreach ( @lines ) {
  633. chomp;
  634. next unless $_;
  635. next if /\(16\)$/; # 16 - device or resource busy
  636. next if /^file has vanished:/;
  637. push @err, $_;
  638. }
  639. pop @err;
  640. return @err ? "rsync failed:\n" . join (" ", @err) : "";
  641. }
  642. ######################################################################
  643. #
  644. # sub readConfig( $config_file )
  645. #
  646. # Parses the named configuration file. dies if errors encountered.
  647. # Returns a hashref of config blocks from the file.
  648. #
  649. ######################################################################
  650. sub readConfig {
  651. my $file = shift;
  652. fail "Couldn't read config file $file: $!"
  653. unless -f $file;
  654. my %valid;
  655. $valid{ "$_" } = 1
  656. foreach qw/
  657. hostname
  658. path
  659. use-rsyncd
  660. snapshots-hourly
  661. snapshots-daily
  662. snapshots-weekly
  663. snapshots-monthly
  664. snapshot-path
  665. mount-dev
  666. mount-point
  667. mount-type
  668. mount-flags
  669. mount-on-startup
  670. umount-on-shutdown
  671. excludes
  672. create-tarballs
  673. tarball-size
  674. bandwidth-limit
  675. /;
  676. open (CONF,$file)
  677. or fail "Couldn't open config file for reading: $!";
  678. my $label='';
  679. my $conf;
  680. my $line=0;
  681. my @config_block;
  682. while ( <CONF> ) {
  683. push(@config_block,$_);
  684. chomp;
  685. $line++;
  686. # skip blank lines and comments
  687. next if ( /^\s*(?:\#.*)?$/ );
  688. # start of a new config block
  689. #
  690. if ( /([\.\w]+)\s*\{/ ) {
  691. # and we haven't closed the last one, stop now.
  692. fail "syntax error on line $line of $file: '$label' block missing curly brace?\n"
  693. if $label;
  694. # no errors? then we're all good.
  695. $label = $1;
  696. # end of the current config block; check for required values
  697. } elsif ( /\s*\}/ ) {
  698. fail "Syntax error on line $line of $file: $label block missing required definition: 'hostname'\n"
  699. unless exists $conf->{$label}->{ 'hostname' };
  700. fail "Syntax error on line $line of $file: $label block missing required definition: 'snapshots-hourly'\n"
  701. unless exists $conf->{$label}->{ 'snapshots-hourly' };
  702. fail "Syntax error on line $line of $file: $label block missing required definition: 'snapshots-daily'\n"
  703. unless exists $conf->{$label}->{ 'snapshots-daily' };
  704. fail "Syntax error on line $line of $file: $label block missing required definition: 'snapshots-weekly'\n"
  705. unless exists $conf->{$label}->{ 'snapshots-weekly' };
  706. fail "Syntax error on line $line of $file: $label block missing required definition: 'snapshots-monthly'\n"
  707. unless exists $conf->{$label}->{ 'snapshots-monthly' };
  708. notify "No tarball-size for $label specified; tarballs will not be created!\n"
  709. if lc $conf->{$label}->{'create-tarballs'} eq 'yes' && ! $conf->{$label}->{'tarball-size'};
  710. $conf->{$label}->{__CONFIG_BLOCK} = join("",@config_block);
  711. @config_block=();
  712. $label='';
  713. # everything else is considered to be an option line
  714. } elsif ( my ($key,$val) = /\s*(\S+)\s+['"]?(\S+)['"]?/ ) {
  715. # make sure it's in a config block.
  716. fail "syntax error on line $line of $file: option not inside a config block.\n"
  717. unless $label;
  718. fail "syntax error on line $line of $file: unrecognized option '$key'\n"
  719. unless exists $valid{ lc $key };
  720. # remember this option
  721. $conf->{$label}->{$1}=$2;
  722. }
  723. }
  724. # if we still have a label at this point, we didn't close the last block
  725. fail "syntax error on line $line of $file: '$label' block missing curly brace?\n"
  726. if $label;
  727. close CONF;
  728. return $conf;
  729. }
  730. __END__
  731. =head1 NAME
  732. rsync-backup.pl -- manage backups of remote systems via rsync
  733. =head1 SYNOPSIS
  734. rsync-backup.pl [ SWITCHES ] [ -f /path/to/config_file ] [ label label... ]
  735. =head1 DESCRIPTION
  736. rsync-backup.pl uses rsync over ssh to perform backups of remote systems.
  737. It supports multiple host definitions, allowing you to specify unique
  738. remote paths, exclusions, local backup targets and so on. You can even
  739. mount a filesystem before starting the backup, and unmount it upon
  740. completion (dangerous for multiuser environments, but handy for toasters).
  741. rsync-backup.pl is designed to be run from cron, for multiple daily
  742. backups and optional archiving of daily, weekly and monthly snapshots.
  743. Snapshots are done via hard links, so disk usage is minimal, and since
  744. rsync only transfers changes since the last run, and uses compression to
  745. boot, bandwidth requirements are light too.
  746. =head1 PREREQUISITES
  747. This script requires the following packages:
  748. =over
  749. =item * rsync, available from http://rsync.samba.org/
  750. =item * perl 5.x
  751. =item * File::Rsync
  752. =item * Proc::ProcessTable (for BSD systems)
  753. =item * Sys::Syslog
  754. =item * gnu cp
  755. =item * gnu tar
  756. =back
  757. Note: On BSD systems, install the coreutils port to get gnu cp and tar.
  758. =head1 COMMAND-LINE SWITCHES
  759. The following switches are supported:
  760. =over
  761. =item B<-h> Display short help summary
  762. =item B<-f> Path to the configuration file
  763. =item B<-v> Enable verbose logging
  764. =item B<-D> Enable debug logging (implies -v)
  765. =item B<-t> Run configuration tests; no transfers
  766. =item B<-n> The number of backup operations to perform at once
  767. =item B<labels> Execute only the named backup configurations
  768. =back
  769. =head1 CONFIGURATION
  770. rsync-backup.pl requires a configuration file containing one or more
  771. "config blocks", which define a remote host targeted for backup. Here's a
  772. sample config block:
  773. =over
  774. =item example {
  775. hostname eg.mydomain.com
  776. path /
  777. snapshots-hourly 4
  778. snapshots-daily 7
  779. snapshots-weekly 4
  780. snapshots-monthly 1
  781. snapshot-path /mnt/backups
  782. excludes /backups/:/proc/:/dev/:tmp/:/usr/src/:/var/db/mysql/
  783. mount-dev /dev/da0s1a
  784. mount-point /mnt/backups
  785. mount-type ufs
  786. mount-flags -fu
  787. mount-on-startup yes
  788. umount-on-shutdown yes
  789. create-tarballs yes
  790. tarball-size 4000m
  791. }
  792. =back
  793. This block tells rsync-backup.pl to backup the entire contents of host
  794. 'eg.mydomain.com' 4 times daily. This configuration would preseve one
  795. monthly backup, plus the most recent 4 weeks and the last 7 days. It
  796. would be advisable with this setup to archive the monthly backup to
  797. permanent media, before it is overwritten the following month. :) If
  798. your snapshots are small (or your disks are large), you might save 12
  799. monthly backups, giving you a year's history at a glance.
  800. You may have as many such config blocks in your config file as you like;
  801. rsync-backup.pl will process each one in turn. Note that each block must
  802. begin with a label, used to identify this backup configuration.
  803. A description of the configuration options follow:
  804. =over
  805. =item B<* hostname>
  806. The fully-qualified domain name, or IP address, of the host to backup.
  807. The host must have rsync installed, and be configuration to allow the
  808. ssh user to run rsync. See I<SSH Configuration>, below.
  809. =item B<* path>
  810. The path on the remote host you wish to back up. You may specify multiple
  811. paths with a colon-separated list; one rsync call will be made for each
  812. path in the list.
  813. =item B<* snapshots-hourly>
  814. Save at most this many snapshots of the remote host in the hourly/
  815. directory. You must run rsync-backup.pl at least this many times per
  816. day. If you run it more times than you've specified here, the oldest
  817. hourly snapshot will be removed.
  818. =item B<* snapshots-daily>
  819. The number of daily snapshots to retain. Each time the script runs, it
  820. checks to see when it last ran -- if it was any day other than today,
  821. the most recent hourly snapshot is moved into the daily/ directory. If
  822. more than snapshots-daily snapshots already exist, the oldest is removed.
  823. =item B<* snapshots-weekly>
  824. The number of weekly snapshots to retain. Weekly snapshots are created
  825. by the first run of the script on sundays, by moving the newest existing
  826. daily snapshot into the weekly/ directory. If more than snapshots-weekly
  827. snapshots already exist, the oldest is removed.
  828. =item B<* snapshots-monthly>
  829. The number of monthly snapshots to retain. Monthly snapshots are created
  830. on the first of the month, by moving the newest weekly snapshot into the
  831. monthly/ directory. If more than snapshots-monthly snapshots already
  832. exist, the oldest is removed.
  833. =item B<* snapshot-path>
  834. The local path where snapshots will be stored. The backups for each
  835. host will be created as subdirectories inside snapshot-path, using the
  836. label from the config block. So in the example above, the backups for
  837. eg.mydomain.com would be created in S</mnt/backups/example>.
  838. =item B<* excludes>
  839. A colon (:)-separated list of path names rsync should not attempt to
  840. backup. Sensible things to include on this list are things like open
  841. database files, tmp directories, and so forth.
  842. =item B<* mount-dev, mount-point>
  843. If values are given for both, rsync-backup.pl will try to verify that
  844. the specified device is mounted on the specified mount point before
  845. beginning the backup sequence.
  846. =item B<* mount-type>
  847. Optional; if specified along with B<mount-dev> and B<mount-point>,
  848. rsync-backup.pl will only launch the backup sequence if the specified
  849. mount's filesystem matches B<mount-type>.
  850. This value is also passed to I<mount(1)> when attempting to mount
  851. filesystems (see B<mount-on-startup>).
  852. =item B<* mount-flags>
  853. Optional additional flags to pass to I<mount(1)> eg., C<-u>.
  854. =item B<* mount-on-startup>
  855. Optional; if set to C<yes>, try to mount B<mount-dev> on B<mount-point>,
  856. using the B<mount-type> and B<mount-flags>, if specified, before
  857. launching rsync. This attempt is only done if the filesystem in
  858. question isn't already mounted.
  859. Ignored unless both B<mount-dev> and B<mount-point> are defined.
  860. =item B<* umount-on-shutdown>
  861. Optional; if set to C<yes>, rsync-backup.pl will unmount the filesystem
  862. specified by B<mount-point> when the backup sequence is complete.
  863. =item B<* create-tarballs>
  864. Optional; if set to C<yes>, when rsync-backup creates a monthly snapshot,
  865. it will also create a gzipped tar file of that snapshot, and place it in
  866. the tarballs/ directory. If the resulting archive is greater in size
  867. than the value of tarball-size, the archive will be split into chunks
  868. of tarball-size size. See I<split(1)>.
  869. =item B<* tarball-size>
  870. The size of the files, in bytes, a tarball should be split into. If a
  871. 'k' is appended to the value, tarball-size is interpretted as kilobytes.
  872. If an 'm' is appended to the value, tarball-size is interpretted as
  873. megabytes.
  874. Note: A tarball-size of 4000m would be a good size for writing DVD-Rs. :)
  875. =item B<* use-rsyncd>
  876. Optional; if set to C<yes>, rsync-backup.pl will attempt to contact the
  877. hose via rsyncd on the default port (879), rather than using SSH to
  878. initiate the connection.
  879. =back
  880. =head1 USAGE NOTES
  881. =head2 Local Configuration
  882. Before running rsync-backup.pl, edit the script and alter the values
  883. of the *_cmd variables to match your specific system layout. The
  884. defaults should work for most systems, but was only tested on a standard
  885. FreeBSD 6.0 installation:
  886. =over
  887. =item my $cp_cmd = '/usr/local/bin/cp -alf';
  888. =item my $touch_cmd = '/usr/bin/touch';
  889. =item my $ssh_cmd = '/usr/bin/ssh';
  890. =item my $mount_cmd = '/sbin/mount';
  891. =item my $umount_cmd = '/sbin/umount';
  892. =item my $tar_cmd = '/usr/local/bin/bin/tar';
  893. =item my $find_cmd = '/usr/bin/find';
  894. =back
  895. =head2 cron
  896. rsync-backup.pl is designed to run from cron. Furthermore, to properly
  897. manage weekly and monthly snapshots, the script needs to run at least on
  898. sundays, and on the first of every month. Thus it is recommended that
  899. you create a cron job to run the script daily, as many times as is needed
  900. by the highest value of snapshots-daily in your config file. For
  901. example, the config block shown above would suggest the following crontab
  902. entry:
  903. =over
  904. =item 0 0,6,12,18 * * * /path/to/rsync-backups.pl -f conf_file
  905. =back
  906. See I<crontab(5)> for details.
  907. =head2 Logging
  908. As of version 2.0, a seperate log file (called backup.log) is created in
  909. the snapshot directory for each host. The log files are truncated at each
  910. run, so no rotating is necessary.
  911. By default no output is sent to STDOUT. You may override this behaviour
  912. by specifying the -v switch; this will cause a copy of entries in each
  913. host's log file to be echoed to STDOUT.
  914. Also new with version 2.0 is support for logging to syslog via Sys::Syslog.
  915. By default only errors are directed there; this can be overridden by enabling
  916. debug output (via the -D switch). Doing so will cause all output to be
  917. copied to syslog in addition to STDOUT/STDERR and the host log files, as well
  918. as enabling various debug-only messages.
  919. Fatal errors are always sent to syslog and to STDERR.
  920. =head2 SSH and rsync
  921. Unless you want to hang around and enter a password every time
  922. rsync-backups.pl launches rsync to back up a remote host, you're going to
  923. want to use certificate-based authentication for the ssh user.
  924. Additionally, if you want to do full system backups with rsync, you're
  925. probably going to need to run rsync-backup.pl as root, and allow root to
  926. ssh into the remote host and run rsync. Allowing remote logins by root
  927. can be dangerous, however. What follows is an overview of my solution
  928. to this problem; B<I strongly recommend you familiarize yourself with the
  929. security implications of this setup before blindly charging forth>. The
  930. author will accept no responsibility for your being foolish, yadda yadda
  931. yadda.
  932. =over
  933. =item B<1. Allow root SSH for authorized commands only>
  934. To do this, simply set C<PermitRootLogin> to C<forced-commands-only> in
  935. your remote host's F<sshd_config>. Now the root user will be permitted
  936. to login via SSH, but may only execute the command you specify in the
  937. F<authorized_keys file>.
  938. =item B<2. Configure root's authorized commands>
  939. Edit root's F<authorized_keys> file on the remote host, and modify the
  940. line containing your backup host's key thusly:
  941. command="/root/bin/ssh_allowed.sh", ssh-dss ... root@backup-host
  942. This will force every root login from backup-host to run the shell script
  943. F<ssh_allowed.sh>. By interrogating the C<$SSH_ORIGINAL_COMMAND>
  944. environment variable in this script, we can decide whether or not to
  945. permit the command to be executed. Here's a simple F<ssh_allowed.sh>:
  946. #!/bin/sh
  947. #
  948. # spawned by ssh to execute valid commands remotely
  949. #
  950. case "$SSH_ORIGINAL_COMMAND" in
  951. *\&*)
  952. echo "Rejected"
  953. ;;
  954. *\;*)
  955. echo "Rejected"
  956. ;;
  957. rsync\ --server\ --sender\ -logDtprRz\ .\ /*)
  958. $SSH_ORIGINAL_COMMAND
  959. ;;
  960. *)
  961. echo "$SSH_ORIGINAL_COMMAND" >> /var/log/root_ssh_rejected.log
  962. echo "Rejected"
  963. ;;
  964. esac
  965. Note: depending on your calling parameters and rsync version, the exact
  966. sequence of arguments on the C<rsync --server> line may or may not match
  967. this example; if your rsyncs are failing, check the rejected log to see
  968. what args are bing passed and modify the script accordingly.
  969. And of course, ensure your F<ssh_allowed.sh>'s permissions are set to 500 :)
  970. =back
  971. =head2 Restoring a split tarball
  972. If you find yourself in the position of needing to restore a backup from
  973. a tarball which has been split into chunks, simply copy all the pieces of
  974. the tarball into a directory, and execute:
  975. % cat tarball.tgz_* | gnu-tar --preserve -xzf -
  976. =head2 Backing up mysql databases
  977. Trying to rsync mysql databases while mysql is running on the remote host
  978. will result in broken tables (and kvetching lusers). It is recommended
  979. the remote host run mysqlhotcopy from a cron job some time before the
  980. rsync backup is scheduled, such that rsync can backup copies of the
  981. databases rather than the databases themselves. Such a crontab entry
  982. might look like this:
  983. 2 3 * * * mysqlhotcopy --addtodest -u user --password=... dbname \
  984. /path/to/backups
  985. Consult the mysql documentation for details.
  986. =head1 CAVEATS
  987. At this time, rsync-backup.pl has no brains for checking disk space
  988. before engaging in possibly-dangerous things like creating multiple
  989. gigantic tarballs of whole filesystems. Be thou therefore careful with
  990. thine tars.
  991. =head1 VERSION
  992. This is version 2.1 of rsync-backup.pl.
  993. =head1 CHANGES SINCE 2.0
  994. =over
  995. =item -- fixed a bug where log was attempted before logfile's path existed.
  996. =back
  997. =head1 CHANGES SINCE 1.7
  998. =over
  999. =item -- rsync-backup now forks to execute multiple backups simultaneously.
  1000. =item -- only tries to load Proc::ProcessTable on bsd-like systems.
  1001. =item -- tarball creation and unmounts now happen per-label, to play nice with threads.
  1002. =item -- verbose/debug logging reimplemented, including syslog support
  1003. =item -- added -n switch to limit number of child processes
  1004. =item -- added -D switch to enable debug output
  1005. =back
  1006. =head1 CHANGES SINCE 1.6
  1007. =over
  1008. =item -- Added support for multiple paths.
  1009. =item -- Added support for rsyncd servers with the use-rsyncd flag.
  1010. =item -- Added support for the bandwidth-limit flag.
  1011. =back
  1012. =head1 CHANGES SINCE 1.5
  1013. =over
  1014. =item -- Fixed a bug where args in the *_cmd variables would be stripped
  1015. =item -- Improved rsync error message parsing
  1016. =back
  1017. =head1 CHANGES SINCE 1.4
  1018. =over
  1019. =item -- The recreate-symlinks business introduced in 1.3 was slow (!)
  1020. and prone to breakage; we now use rsync to copy symbolic links
  1021. from the working directory to the snapshot directory.
  1022. =item -- Added test mode and the -v switch
  1023. =back
  1024. =head1 CHANGES SINCE 1.3
  1025. Modified behaviour of the mount options. We can now:
  1026. =over
  1027. =item -- mount a filesystem, do the backup, then umount the filesystem;
  1028. =item -- verify a filesystem is mounted before the backup;
  1029. =item -- mount a filesystem if it isn't already mounted;
  1030. =item -- unmount a filesystem when the backup is complete, regardless of whether or not we mounted it.
  1031. =back
  1032. =head1 CHANGES SINCE 1.2
  1033. =over
  1034. =item -- Since one cannot create a hard link of a symlink, snapshots
  1035. contained none of the symlinks in the working directory. To resolve
  1036. this, rsync-backup.pl now does a find of all symlinks in the working
  1037. directory and recreates them in the newly-created hourly snapshot.
  1038. The mtime, modes and ownership of the symlinks are all preserved.
  1039. =back
  1040. =head1 CHANGES SINCE 1.1
  1041. =over
  1042. =item -- Changed the aging scheme to move the newest snapshot from daily
  1043. to weekly, and weekly to monthly. Prior versions moved the oldest, which
  1044. seems dumb.
  1045. =back
  1046. =head1 CHANGES SINCE 1.0
  1047. =over
  1048. =item -- Added support for labels on the command-line
  1049. =item -- Minor additions to documentation
  1050. =item -- Removed unused '$date' variable
  1051. =back
  1052. =head1 TO DO
  1053. =over
  1054. =item -- bandwidth-aware threading
  1055. =item -- automagick writing of monthly tarballs to cd/dvd would rock
  1056. =back
  1057. =head1 AUTHOR
  1058. rsync-backup.pl was written by Greg Boyington <greg@regex.ca>.
  1059. =head1 ACKNOWLEDGEMENTS
  1060. The basic structure of the backup scheme isn't mine; it belongs to Stu
  1061. Sheldon, <stu@actusa.net>, whose C<mirror> script I found linked on
  1062. Mike Rubel's excellent article, "Easy Automated Snapshot-Style Backups
  1063. with Linux And Rsync." You can read the article here:
  1064. http://www.mikerubel.org/computers/rsync_snapshots/
  1065. =head1 LICENSE AND COPYRIGHT
  1066. Copyright 2010 Greg Boyington. All rights reserved.
  1067. Redistribution and use in source and binary forms, with or without modification, are
  1068. permitted provided that the following conditions are met:
  1069. 1. Redistributions of source code must retain the above copyright notice, this list of
  1070. conditions and the following disclaimer.
  1071. 2. Redistributions in binary form must reproduce the above copyright notice, this list
  1072. of conditions and the following disclaimer in the documentation and/or other materials
  1073. provided with the distribution.
  1074. THIS SOFTWARE IS PROVIDED BY GREG BOYINGTON ``AS IS'' AND ANY EXPRESS OR IMPLIED
  1075. WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  1076. FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
  1077. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  1078. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  1079. SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
  1080. ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  1081. NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  1082. ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  1083. The views and conclusions contained in the software and documentation are those of the
  1084. authors and should not be interpreted as representing official policies, either expressed
  1085. or implied, of Greg Boyington.