├── config.example ├── rrb ├── rrb_cleanup ├── rrb_cleanup_helper ├── rrb_create_account ├── rrb_interval ├── sxc-rrb └── sxc-rrb-settings /config.example: -------------------------------------------------------------------------------- 1 | # Example configuration file for backing up a client using rrb. 2 | # Will be interpreted as a shell script. Comments start with #. 3 | # Use KEY=VALUE to assign a value to the key. No spaces around the =. 4 | # 5 | # ----------------------------------------------------------------------------- 6 | # Obligatory options 7 | # ----------------------------------------------------------------------------- 8 | 9 | # The root of the files to be backuped. Can be on a remote host or localhost. 10 | # Will be interpreted by rsync. 11 | 12 | # To backup a local directory (and create an additional directory level at the 13 | # destination): 14 | #SRC=/etc 15 | 16 | # In order not to create an additional directory level at the destination add a 17 | # trailing slash: 18 | #SRC=/etc/ 19 | 20 | # Remote access via remote shell. Note that you don't need to add a trailing 21 | # slash. Spaces in remote sources have to be escaped, so the remote shell can 22 | # interpret them. My tip: Don't use spaces at all: 23 | #SRC=example:'file\ name\ with\ spaces' 24 | 25 | # Remote access via rsync daemon. No need to add a trailing slash on remote 26 | # connections: 27 | #SRC=example::module 28 | 29 | # The destination directory to put the backups in: 30 | #DEST_DIR=/backup/example 31 | 32 | # ----------------------------------------------------------------------------- 33 | # Optional options 34 | # ----------------------------------------------------------------------------- 35 | 36 | # The path of a file that specifies the excludes. New line for every file to be 37 | # excluded. (Directories are files, too.) See man 1 rsync for more detail on 38 | # the layout of the file. 39 | #EXCLUDES_FILE=/etc/rrb/excludes.example 40 | 41 | # Same with spaces: 42 | #EXCLUDES_FILE="/path with spaces/excludes.example" 43 | 44 | # A command that will be executed before running the backup. Can be used to 45 | # mount a filesystem. 46 | #BEFORE_CMD="echo 'before'" 47 | 48 | # A command that will be executed after running the backup. Also if the backup 49 | # failed. Can be used to mount a filesystem that has been mounted before. Note 50 | # that you can't use variables to find out whether the filesystem was mounted 51 | # before BEFORE_CMD. 52 | #AFTER_CMD="echo 'after'" 53 | 54 | # Will only be executed if the backup failed. Might be handy to notify the 55 | # admin. 56 | #FAIL_CMD="echo 'fail'" 57 | 58 | # Will only be executed if the backup succeeded. 59 | #SUCCESS_CMD="echo 'success'" 60 | 61 | # Set additional rsync options. In this example we want to preserves hard 62 | # links. Preserving hard links is very expensive, and we don't want to hinder 63 | # the users of the client too much, so we set the niceness of the remote rsync 64 | # to a high value. 65 | #RSYNC_OPTS=( --hard-links --rsync-path='nice -n19 rsync' ) 66 | 67 | # Specify a remote shell for rsync. There are more rsync-related options, see 68 | # man 1 rsync. 69 | #RSYNC_RSH=ssh 70 | 71 | # ----------------------------------------------------------------------------- 72 | # Options for rrb-cleanup 73 | # ----------------------------------------------------------------------------- 74 | 75 | # Number of days to keep the backups and number of backups to keep in this 76 | # interval. One rule per line. Backups recognized by earlier lines are excluded 77 | # from following lines. Therefore you usually want to write the number of days 78 | # in ascending order. 79 | #KEEP_RULES=( \ 80 | # 7 7 \ 81 | # 31 8 \ 82 | # 365 11 \ 83 | #1825 4 \ 84 | #) 85 | -------------------------------------------------------------------------------- /rrb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This is the reverse rsync backup (rrb) utility. It has to be run on the 4 | # backup server and will then make a backup of a specified directory of a 5 | # client machine via ssh, rsh, or any other remote shell or the localhost using 6 | # rsync with hardlinks. See the config.example for more information about the 7 | # configuration of rrb for a client. 8 | # 9 | # Further features include an exclusion file, the resume of a failed transfer, 10 | # customizable commands to be executed before running, after running, after a 11 | # failure, after a success. 12 | # 13 | # You can use fdupes or a similar tool to hardlink between multiple machines. 14 | # 15 | # No backups will be done automatically. You have to run rrb by hand every time 16 | # you want to make a backup. Use cron to make regular backups, at to make 17 | # backups at a specific time, or a custom script using sxc, or portknocking, or 18 | # whatever to enable the client to request a backup. 19 | # 20 | # If you want to automize backups via ssh, you can use public key 21 | # authentication. So if you do full-system backups from your backup server, it 22 | # will have root access to all the backup clients. Root access on the server 23 | # means root access on all the clients and a loss of all the backups. Secure 24 | # the server and save backups offline, too. 25 | # 26 | # The backups can be made accessible in any manner, like NFS, SMB, or a custom 27 | # script that tars the files and sends them over the network. Please use your 28 | # creativity. 29 | # 30 | # This script should be compatible to zsh and bash. Tell me if it fails in your 31 | # favourite shell. 32 | 33 | # ----------------------------------------------------------------------------- 34 | 35 | set -e # Exit on every fail. 36 | renice -n 19 -p $$ > /dev/null 37 | ionice -n 3 -p $$ 38 | HELP="Usage: $0 config" 39 | 40 | # Exit codes 41 | EX_USAGE=64 42 | EX_SOFTWARE=70 43 | EX_CONFIG=78 44 | EX_FILE=100 45 | EX_RUNNING=101 46 | 47 | # Unset variables from config file. 48 | unset SRC EXCLUDES_FILE DEST_DIR BEFORE_CMD AFTER_CMD FAIL_CMD SUCCESS_CMD \ 49 | RSYNC_OPTS 50 | 51 | fail() 52 | { 53 | echo -e "$1" >&2 54 | exit $2 55 | } 56 | 57 | run() 58 | { 59 | if [ "$*" ]; then 60 | echo "$*" | sh 61 | fi 62 | } 63 | 64 | add_rsync_opt() 65 | { 66 | if [ "${RSYNC_OPTS}" ]; then 67 | RSYNC_OPTS=("${RSYNC_OPTS[@]}" "$*") 68 | else 69 | RSYNC_OPTS=("$*") 70 | fi 71 | } 72 | 73 | cleanup() 74 | { 75 | [ 0 -ne $? ] && run "$FAIL_CMD" 76 | run "$AFTER_CMD" 77 | rm -f "$LOCK_FILE" 78 | } 79 | 80 | [ "$#" -eq 1 ] || fail "Invalid usage.\n$HELP" $EX_USAGE 81 | [ "$1" ] || fail "Config file missing.\n$HELP" $EX_USAGE 82 | source $1 83 | [ "$SRC" -a "$DEST_DIR" ] || fail "Invalid config file, has to include at \ 84 | least \$SRC and \$DEST_DIR.\n$HELP" $EX_CONFIG 85 | 86 | cd $DEST_DIR 87 | 88 | LATEST_DIR="latest" 89 | TMP_DIR=".tmp" 90 | NEW_DIR="`date +%FT%T`" 91 | LOCK_FILE=".lock" 92 | 93 | [ -e "$NEW_DIR" ] && fail "Backup directory $NEW_DIR already exists." $EX_FILE 94 | 95 | [ "$EXCLUDES_FILE" ] && add_rsync_opt --exclude-from="$EXCLUDES_FILE" 96 | [ -L "$LATEST_DIR" ] && add_rsync_opt --link-dest="$PWD/$LATEST_DIR" 97 | 98 | # noclobber prevents the '>' from overwriting an existing lock file. 99 | if ! (set -o noclobber; echo $$ > "$LOCK_FILE"); then 100 | fail "$DEST_DIR is already locked by the process with the PID \ 101 | $(cat "$LOCK_FILE"). Remove $LOCK_FILE to unlock manually." $EX_RUNNING 102 | fi 103 | 104 | trap cleanup EXIT HUP INT QUIT TERM # Always call, even on success. 105 | run "$BEFORE_CMD" 106 | rsync -z -avP --delete --delete-excluded "${RSYNC_OPTS[@]}" "$SRC" "$TMP_DIR" \ 107 | || [ 24 -eq $? ] 108 | 109 | mv "$TMP_DIR" "$NEW_DIR" 110 | rm -f "$LATEST_DIR" 111 | ln -sf "$NEW_DIR" "$LATEST_DIR" 112 | run "$SUCCESS_CMD" 113 | # Call cleanup. 114 | -------------------------------------------------------------------------------- /rrb_cleanup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Cleanup a user's backup directory by printing no longer needed backups 4 | # (seperated by null charaters) using a defined policy. Pipe to xargs -0 rm -rf 5 | # to really delete them. 6 | 7 | set -e 8 | HELP="Usage: $0 config" 9 | EX_USAGE=64 10 | EX_CONFIG=78 11 | unset DEST_DIR KEEP_RULES 12 | 13 | fail() 14 | { 15 | echo -e "$1" >&2 16 | exit $2 17 | } 18 | 19 | CONFIG="$1" 20 | [ "$#" -eq 1 ] || fail "Invalid usage.\n$HELP" $EX_USAGE 21 | [ "$CONFIG" ] || fail "Config file missing.\n$HELP" $EX_USAGE 22 | source $CONFIG 23 | [ "$DEST_DIR" ] || fail "Invalid config file, missing \$DEST_DIR. 24 | $HELP" $EX_CONFIG 25 | [ "$KEEP_RULES" ] || fail "Invalid config file, missing \$KEEP_RULES. 26 | $HELP" $EX_CONFIG 27 | 28 | exec /usr/local/bin/rrb_cleanup_helper "$DEST_DIR" "${KEEP_RULES[@]}" 29 | -------------------------------------------------------------------------------- /rrb_cleanup_helper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import os.path 6 | import datetime 7 | 8 | def execRule(now, rule, backups): 9 | if len(backups) < rule[1]: 10 | del backups[:] 11 | print("Rule", rule, "keeps all", file=sys.stderr) 12 | return 13 | 14 | optDts = [now - datetime.timedelta(days=y) * rule[0] / rule[1] \ 15 | for y in sorted(range(rule[1]))] 16 | for optDt in optDts: 17 | diffs = [] 18 | for backup in backups: 19 | diffs.append((abs(optDt - backup[0]), backup[0], backup[1])) 20 | diffs.sort() 21 | print("Rule", rule, "tries to get", optDt.strftime("%Y-%m-%dT%H:%M:%S"), "and keeps", diffs[0][2], file=sys.stderr) 22 | backups.remove((diffs[0][1], diffs[0][2])) 23 | 24 | def main(argv=None): 25 | if not argv: 26 | argv = sys.argv 27 | 28 | if len(argv) < 5 or len(argv) % 2 != 0: 29 | print("Usage: " + argv[0] + " dir rule1_days rule1_nr [...]", file=sys.stderr) 30 | 31 | baseDir = argv[1] 32 | rules = map(lambda x: (int(x[0]), int(x[1])), zip(*[iter(argv[2:])]*2)) 33 | 34 | dirs = os.listdir(baseDir) 35 | backups = [] 36 | 37 | for dir in dirs: 38 | try: 39 | dt = datetime.datetime.strptime(dir, "%Y-%m-%dT%H:%M:%S") 40 | except ValueError: 41 | continue 42 | backups.append((dt, dir)) 43 | backups.sort(reverse=True) 44 | 45 | now = datetime.datetime.now() 46 | 47 | for rule in rules: 48 | execRule(now, rule, backups) 49 | 50 | # Print backups to be deleted. 51 | for backup in backups: 52 | sys.stdout.write(os.path.join(baseDir, backup[1]) + '\0') 53 | 54 | if __name__ == '__main__': 55 | main() 56 | -------------------------------------------------------------------------------- /rrb_create_account: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SETUP_CHROOT_PATH=/etc/scponly/chroot/ 4 | 5 | set -e 6 | HELP="Usage: $0 name backup-dir" 7 | 8 | EX_USAGE=64 9 | 10 | fail() 11 | { 12 | echo -e "$1" >&2 13 | exit $2 14 | } 15 | 16 | [ "$#" -eq 2 ] || fail "Invalid usage.\n$HELP" $EX_USAGE 17 | [ "$1" ] || fail "Username missing.\n$HELP" $EX_USAGE 18 | [ "$2" ] || fail "Backup directory missing.\n$HELP" $EX_USAGE 19 | NAME="$1" 20 | BACKUP_DIR="$2" 21 | cd "$SETUP_CHROOT_PATH" 22 | echo "$NAME" | ./setup_chroot.sh 23 | HOME_DIR="`grep "^$NAME" /etc/passwd | cut -d ":" -f6`" 24 | passwd "$NAME" 25 | rmdir "$HOME_DIR/incoming" 26 | mkdir "$HOME_DIR/backup" "$HOME_DIR/dev" 27 | mknod -m 666 "$HOME_DIR/dev/null" c 1 3 28 | echo "localhost:$BACKUP_DIR $HOME_DIR/backup nfs defaults 0 0" >> /etc/fstab 29 | mount "$HOME_DIR/backup" 30 | -------------------------------------------------------------------------------- /rrb_interval: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run rrb if it was last run more than the specified number of seconds ago. 4 | # Useful in cron-scripts for clients, that are not online all the time. 5 | 6 | set -e 7 | HELP="Usage: $0 interval config" 8 | EX_USAGE=64 9 | EX_CONFIG=78 10 | unset DEST_DIR 11 | 12 | fail() 13 | { 14 | echo -e "$1" >&2 15 | exit $2 16 | } 17 | do_rrb() 18 | { 19 | exec rrb $CONFIG 20 | } 21 | 22 | INTERVAL="$1" 23 | CONFIG="$2" 24 | [ "$#" -eq 2 ] || fail "Invalid usage.\n$HELP" $EX_USAGE 25 | [ "$INTERVAL" ] || fail "Interval missing.\n$HELP" $EX_USAGE 26 | [ "$INTERVAL" -ge 1 ] || fail "Invalid interval.\n$HELP" $EX_USAGE 27 | [ "$CONFIG" ] || fail "Config file missing.\n$HELP" $EX_USAGE 28 | source $CONFIG 29 | [ "$DEST_DIR" ] || fail "Invalid config file, missing \$DEST_DIR. 30 | $HELP" $EX_CONFIG 31 | 32 | LATEST_DIR="$DEST_DIR/latest" 33 | [ -e "$LATEST_DIR" ] || do_rrb 34 | # date does not parse datetime with 'T' as delimiter correctly, replace with 35 | # space. (Or it does it correctly by recognizing the timezone, but this is not 36 | # wanted here) 37 | DATE_LAST=`date --date="\`readlink $LATEST_DIR | tr T ' '\`" +%s` 38 | DATE_LIMIT=`echo "\`date +%s\` - $INTERVAL" | bc` 39 | [ $DATE_LAST -le $DATE_LIMIT ] && do_rrb 40 | -------------------------------------------------------------------------------- /sxc-rrb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Backups using sxc and rrb. 4 | 5 | HELP="Usage: $0 config" 6 | 7 | # Exit codes 8 | EX_USAGE=64 9 | 10 | fail() 11 | { 12 | echo -e "$1" >&2 13 | exit $2 14 | } 15 | 16 | [ "$#" -eq 1 ] || fail "Invalid usage.\n$HELP" $EX_USAGE 17 | [ "$1" ] || fail "Config file missing.\n$HELP" $EX_USAGE 18 | source $1 19 | 20 | main() 21 | { 22 | PID=$1 23 | 24 | while [ ! -p $JID/in ]; do 25 | echo "Waiting for $JID/in" 26 | sleep 0.5 27 | done 28 | 29 | echo -e "\0:pwd $PASSWORD\0" > $JID/in 30 | echo -e "\0:pri $PRIORITY\0" > $JID/in 31 | echo -e "\0:set available\0" > $JID/in 32 | 33 | while [ "`cat $JID/nfo/presence`" = "offline" ]; do 34 | sleep 1 35 | done 36 | 37 | LAST_USER= 38 | LAST_CONFIGS=() 39 | for ENTRY in ${USERS[@]}; do 40 | if echo "$ENTRY" | grep -qs "@"; then 41 | if [ "$LAST_USER" ]; then 42 | echo -e "\0:add $LAST_USER\0" > $JID/in 43 | handle_contact $PID "$LAST_USER" "${LAST_CONFIGS[@]}" & 44 | fi 45 | LAST_USER="$ENTRY" 46 | LAST_CONFIGS=() 47 | else 48 | if [ "${LAST_CONFIGS}" ]; then 49 | LAST_CONFIGS=("${LAST_CONFIGS[@]}" "$ENTRY") 50 | else 51 | LAST_CONFIGS=("$ENTRY") 52 | fi 53 | fi 54 | done 55 | 56 | echo -e "\0:add $LAST_USER\0" > $JID/in 57 | handle_contact $PID "$LAST_USER" "${LAST_CONFIGS[@]}" & 58 | 59 | tail --lines=0 --follow --pid $PID $JID/out 2> /dev/null | 60 | while read -r line; do 61 | OUT=`echo $line | cut -d" " -f 2-` # Remove date 62 | if echo $OUT | grep -qs "^Disconnected"; then 63 | ( sleep 5; echo -e "\0:set available\0" > $JID/in) & 64 | fi 65 | done 66 | } 67 | 68 | handle_contact() 69 | { 70 | PID=$1 71 | CONTACT=$2 72 | CONFIGS=${*:3} 73 | FILE_OUT=$JID/$CONTACT/out 74 | FILE_IN=$JID/$CONTACT/in 75 | 76 | while [ ! -p $FILE_IN -a -f $FILE_OUT ]; do 77 | echo "Waiting for $CONTACT/{in,out}" 78 | sleep 0.5 79 | done 80 | 81 | tail --lines=0 --follow --pid $PID $FILE_OUT 2> /dev/null | 82 | while read -r line; do 83 | OUT=`echo $line | cut -d" " -f 2-` # Remove date 84 | 85 | # We are only interested in messages from the other contact. 86 | if ! echo $OUT | grep -qs "^<$CONTACT> "; then 87 | continue 88 | fi 89 | 90 | MSG=`echo $OUT | cut -d" " -f 2-` 91 | 92 | CMD=`echo $MSG | cut -d" " -f 1` 93 | if [ ":help" = "$CMD" ]; then 94 | echo "\ 95 | Commands: 96 | :backup name 97 | :list" > $FILE_IN 98 | elif [ ":list" = "$CMD" ]; then 99 | echo "Valid hosts: ${CONFIGS[@]}" > $FILE_IN 100 | elif [ ":backup" = "$CMD" ]; then 101 | NAME=`echo $MSG | cut -d" " -f 2-` 102 | 103 | for CONFIG in ${CONFIGS[@]}; do 104 | if [ "$CONFIG" = "$NAME" ]; then 105 | VALID=true 106 | fi 107 | done 108 | if [ "$VALID" = "true" ]; then 109 | echo "Running: rrb \"$CONFIG_PREFIX$NAME$CONFIG_SUFFIX\"" > $FILE_IN 110 | sleep 0.1 111 | rrb "$CONFIG_PREFIX$NAME$CONFIG_SUFFIX" &> $FILE_IN 112 | echo "rrb \"$CONFIG_PREFIX$NAME$CONFIG_SUFFIX\" finished with exit code: $?" \ 113 | > $FILE_IN 114 | else 115 | echo "Invalid hostname $NAME" > $FILE_IN 116 | fi 117 | else 118 | echo "Invalid usage, see :help" > $FILE_IN 119 | fi 120 | done 121 | } 122 | 123 | sxc $JID --port $PORT & 124 | PID=$! 125 | main $PID & 126 | wait $PID 127 | -------------------------------------------------------------------------------- /sxc-rrb-settings: -------------------------------------------------------------------------------- 1 | JID=rrb@example.org 2 | PASSWORD=insecure 3 | PRIORITY=0 4 | PORT=5222 5 | 6 | # Prefix for every config file. 7 | CONFIG_PREFIX=/etc/rrb/config. 8 | # Suffix for every config file. 9 | CONFIG_SUFFIX= 10 | 11 | # Entries including an "@" are the JIDs, all following entries the config files 12 | # that user is allowed to request a backup for. 13 | USERS=( \ 14 | foo@example.org comp1 \ 15 | bar@example.org comp2 comp1 \ 16 | xyz@example.org comp2 comp3 \ 17 | ) 18 | --------------------------------------------------------------------------------