#!/bin/bash # # ppsnapback - push or pull snapshot backups using rsync # # Copyright (C) 2006-2007 Nicolas Schodet. # # See end of file for documentation, copyright and license. # # Man page can be generated using: # # $ pod2man -c '' -r '' ppsnapback > ppsnapback.1 # set -e MODULE=${1:-none} BASEDIR=${2:-$HOME/backups} MD=$BASEDIR/$MODULE # Read configuration files. if [[ ! -f $MD/config ]]; then echo "no module '$MODULE'" >&2 echo "Usage: $0 MODULE [BACKUP_DIR]" >&2 exit 1 fi [[ -f $BASEDIR/default-config ]] && source $BASEDIR/default-config source $MD/config backlog=${backlog:-3} rotate () { # Rotate backlog directories if [[ -d $MD/$backlog ]]; then # Use rsync to handle write protected directories. [[ -d $MD/empty ]] || mkdir $MD/empty rsync -a --delete $MD/empty/ $MD/$backlog rmdir $MD/$backlog $MD/empty fi for ((i=backlog-1; i >= 0; i--)); do [[ -d $MD/$i ]] && mv $MD/$i $MD/$((i+1)) done mv $MD/new $MD/0 } if [[ $path ]]; then # The tabulation is a rsync quirk to disable word splitting. RSH="ssh${privkey:+ -o IdentitiesOnly yes -i $MD/$privkey} -S none" [[ -f $BASEDIR/default-exclude ]] && \ DEFAULT_EXCLUDES="--exclude-from $BASEDIR/default-exclude" [[ -f $MD/exclude ]] && \ EXCLUDES="--exclude-from $MD/exclude" if [[ $remote ]]; then if [[ $remote == *:* ]]; then # Push backup to a remote server. rsync -a --delete-excluded --rsh="$RSH" \ $DEFAULT_EXCLUDES $EXCLUDES $extra "$path" "$remote" else # Push backup to a remote ppsnapback. rsync -a --delete-excluded --rsh="$RSH" \ --rsync-path=please-configure-ssh-key \ $DEFAULT_EXCLUDES $EXCLUDES $extra "$path" "$remote":invalid fi else # Store backup to the local repository. rsync -a --delete-excluded --rsh="$RSH" \ $DEFAULT_EXCLUDES $EXCLUDES $extra \ --link-dest=../0 "$path" $MD/new touch $MD/new # Rotate. rotate fi elif [[ $fakeroot && -w $MD/$fakeroot && $UID -ne 0 ]]; then # Restart using fakeroot. fakeroot -i $MD/$fakeroot -s $MD/$fakeroot -- $0 $MODULE $BASEDIR else # Receiver mode, called from ssh. rsync --server -a --delete-excluded \ $extra --link-dest=../0 . $MD/new touch $MD/new # Rotate. rotate fi exit 0 __END__ =head1 NAME ppsnapback - push or pull snapshot backups using rsync =head1 SYNOPSIS B I [I] =head1 DESCRIPTION B makes backup snapshots using rsync, possibly pulling or pushing data from or to a B connection. When B is pushing data, it can use fakeroot to save owners, groups and permissions. B reads its configuration from a local hierarchy of files based at I. If I is not specified, it default to F<~/backups/>. Each backup module should have its own directory located under I. A default configuration is read from F/default-config> and the module configuration is read from F/I/config>. Additional exclude lists using the rsync(1) format are read from F/default-exclude> and F/I/exclude>. The configuration files are shell files sourced by B. They are supposed to set configuration variables but can use any bash(1) shell constructions to do so. B will construct a set of directly accessible snapshots, using hardlinks to limit hard disk usage. It is based on a Mike Rubel's article "Easy Automated Snapshot-Style Backups with Linux and Rsync" (see L). The F<0> directory in the module directory will contain the most recent snapshot. The F<1> directory will contain the previous snapshot and so on... B aims to keep a simple and readable source code in order to allow its users to hack any new or local features in it. =head1 EXAMPLES =head2 Local Backups This case shows a configuration in which data to backup and backup repository is on the same host. Content of F/local_example/config>: path=/home That's all! Run B using: ppsnapback local_example backup_dir Be careful not to backup the backup repository! =head2 Pull Remote Data to Backup This case shows a configuration in which data to backup is located on a remote host. Ssh private key is used to secure the connection. B do not need to be installed on the remote host. Content of F/pull_example/config>: path=myuser@myhost.example.com:/home/myuser/ privkey=myhost-myuser-backup Content of F (line breaks introduced by an anti-slash are provided for readability): from="backuphost.example.com",\ command="/usr/bin/rsync --server --sender -logDtpr \ . /home/myuser/",\ no-pty,no-port-forwarding,no-x11-forwarding,no-agent-forwarding \ ssh-rsa AAA...rest of public key... The private key has been generated using ssh-keygen(1). See L for more details. Run B using: ppsnapback pull_example backup_dir =head2 Push Remote Backup Repository With No Snapshot Rotation This case shows a configuration in which local data to backup is pushed on a remote host, but with no snapshot rotation. Ssh private key is used to secure the connection. B do not need to be installed on the remote host. Content of F/push_nosnap_example/config>: path=/home/myuser remote=myuser@backup.example.com:backups/push_nosnap_example privkey=myhost-myuser-backup Content of F (line breaks introduced by an anti-slash are provided for readability): from="myhost.example.com",\ command="/usr/bin/rsync --server -logDtpr --delete-excluded \ . backups/push_nosnap_example",\ no-pty,no-port-forwarding,no-x11-forwarding,no-agent-forwarding \ ssh-rsa AAA...rest of public key... The private key has been generated using ssh-keygen(1). See L for more details. Run B using: ppsnapback push_nosnap_example backup_dir =head2 Push Remote Backup Repository With Snapshot Rotation This case is similar to the previous case, but requires that B is installed on the remote host to perform the snapshots rotation. Content of F/push_example/config>: path=/home/myuser remote=myuser@backup.example.com privkey=myhost-myuser-backup The file F/push_example/config> can be empty but it must exist. Content of F (line breaks introduced by an anti-slash are provided for readability): from="myhost.example.com",\ command="/path/to/ppsnapback push_example",\ no-pty,no-port-forwarding,no-x11-forwarding,no-agent-forwarding \ ssh-rsa AAA...rest of public key... The private key has been generated using ssh-keygen(1). See L for more details. Run B using: ppsnapback push_example backup_dir =head2 Push Remote Backup Repository With Fakeroot This case is similar to the previous case, but requires that fakeroot(1) is installed on the remote host. The advantage of using fakeroot(1) is that file owners, groups and permissions are also saved. The only difference is in the file F/push_example/config>: fakeroot=fakeroot.dat If users and groups do not match at the backup host, add the following option to B sides: extra=--numeric-ids =head1 CONFIGURATION VARIABLES =over =item I This is the path to data to backup. Path can be a local path or remote path using the rsync(1) syntax. A trailing slash changes the B behavior to avoid creating an additional directory level at the destination. You can find more about this in the rsync(1) manual. =item I This variable is passed to rsync(1) as additional options. Be careful, most of these options must also be used at the remote end of the ssh(1) connection. =item I This is the path to an optional ssh(1) private key, relative to the module directory. =item I If this variable is defined, backups are not stored locally but on a remote host. In this case, this variable is the path to the remote backup repository. If the remote host does not override the sent command, a simple rsync(1) will be used with no snapshot rotation. See L for more informations. If the remote host does override the sent command (this is the recommended usage) only the host part of this variable does really matter. =item I This variable is the number of older snapshots to keep in the backup repository. It defaults to 3. =item I As B use normal files to store the backups, permissions, owners and groups can only be saved if it is run as root. Another alternative is to use fakeroot(1) to simulate a root environment. If this variable is set, it gives the path relative to the module directory to the file used by fakeroot(1) to save additional information like owners, groups and permissions. This file must exist prior to be used by B, you can create it using touch(1). =back =head1 SECURING SSH CONNECTION Doing backups should be an automated process and therefore, there can not be a person to type a ssh(1) password when they are done. The solution is to use a password less ssh(1) private key. Please see ssh-keygen(1) for details about how to use it. The procedure to install a key is something like: ssh-keygen -t rsa -f backup-key -C "backup key" -N "" cat backup-key.pub | ssh remotehost "cat >> .ssh/authorized_keys" One problem with this system is that anyone owning the private key file can connect using ssh on the remotehost and do anything he want. Happily, ssh(1) provide a way to restrict commands executed on the remote host. In order to do this, edit the .ssh/authorized_keys on the remote host and add the following before the key you have just copied (line breaks introduced by an anti-slash are provided for readability): from="myhost.example.com",\ command="/path/to/my/command",\ no-pty,no-port-forwarding,no-x11-forwarding,no-agent-forwarding \ ssh-rsa AAA...rest of public key... Now, the private key only works from myhost.example.com to run the command indicated. This method can be used to limit the usage of the private key to backup. =head1 AUTOMATING EXECUTION To automatically execute the backup, place B in your crontab(5). For example: 0 * * * * /path/to/ppsnapback my_backup_every_4_hours 4 42 * * 0 /path/to/ppsnapback my_weekly_backup See your crontab(1), cron(8) and crontab(5) local documentation for more details. =head1 ENVIRONMENT VARIABLE =over =item HOME Used to deduce I if not provided on the command line. =back =head1 FILES =over =item F<$HOME/backups/> Default directory for I. =item F/default-config> Default configuration, sourced for every module. =item F/default-exclude> Default exclude list, used for every module, using the rsync(1) format. =item F/I/config> Module configuration. =item F/I/exclude> Module exclude, using the rsync(1) format. =item F<~/.ssh/authorized_keys> Not a B file, but used by ssh(1) to authorize and limit connections using private/public keys. =back =head1 BUGS B does not read rsync(1) options passed by the ssh connection. This means that any extra option added on one end should also be added at the other end. The variables SSH_ORIGINAL_COMMAND and SSH2_ORIGINAL_COMMAND could be read to fix this issue, taking care to accept only reasonable options. =head1 RESTRICTIONS B does not handle spaces in I and I arguments or in the I and I configuration variables. Support for spaces could be added easily but will clutter the source code with double-quotes. The I or I variables however accept spaces. =head1 SEE ALSO rsync(1), ssh(1), ssh-keygen(1), bash(1), fakeroot(1), crontab(5) Mike Rubel's article "Easy Automated Snapshot-Style Backups with Linux and Rsync" http://www.mikerubel.org/computers/rsync_snapshots/ =head1 AUTHOR Nicolas Schodet, http://ni.fr.eu.org/ppsnapback =head1 COPYRIGHT AND LICENSE Copyright (C) 2006 Nicolas Schodet. 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. =cut