#!/usr/bin/env python # rybackup -- rsync based backup script with rotating backup-snapshots, # based on the idea and implementation found on mikerubel.org # Copyright (C) 2010 Adrian C. # 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. # crontab # 0 */4 * * * ~/.backup/rybackup.py hourly # every 4 hours # 0 18 * * * ~/.backup/rybackup.py daily # once a day at 18h # 58 3 * * 0 ~/.backup/rybackup.py weekly # every sunday at 3:58 # 53 3 1 * * ~/.backup/rybackup.py monthly # first day of every month # # crontab eCryptfs (i.e. less frequent) # @daily ID=bak-daily ~/.backup/rybackup.py hourly # job runs first chance, # @weekly ID=bak-weekly ~/.backup/rybackup.py daily # and retries on EAGAIN from sys import argv from shutil import rmtree from subprocess import call from os import getuid from os import path, rename from os import listdir, makedirs # Configuration # # Local backup mount point and sources to backup bak = { "dst" : "/mnt/nfs/backup", "src" : [ "/etc/", "/root/", "/home/user/", "/mnt/storage/Documents/" ], "efs" : False, # eCryptfs support } # Remote backup server, or removable storage rmt = { "fst" : "-t nfs", # file-system type "opt" : "-o async,vers=3", # mount options, if any "dev" : "192.168.0.10", # device node or NFS host "dst" : "/mnt/backup/clientname" # remote NFS export } # Number of backup snapshots to keep count = { "hourly" : 4, "daily" : 3, "weekly" : 2, "monthly" : 2, } # Files with a role in the backup procedure files = { "mounts" : "/proc/mounts", "exclude" : "/root/.backup/exclude.lst", } # System commands used by this program bin = { "cp" : "/bin/cp", "mount" : "/bin/mount", "umount" : "/bin/umount", "grep" : "/bin/grep", "touch" : "/bin/touch", "rsync" : "/usr/bin/rsync", } # Functions # # Snapshot rotation def rotate(arg): rotation = { # Interval relations "hourly" : ["hourly", 1, 2], "daily" : ["hourly", count['hourly'], 1], "weekly" : ["daily", count['daily'], 1], "monthly" : ["weekly", count['weekly'], 1], } # Delete the oldest snapshot if path.isdir("%s/%s.%s" % (bak['dst'], arg, count[arg])): rmtree("%s/%s.%s" % (bak['dst'], arg, count[arg])) # Shift the middle snapshot(s) back by one for i in reversed(range(2, count[arg]+1)): if i-1 == 1 and arg == "hourly": break if path.isdir("%s/%s.%s" % (bak['dst'], arg, i-1)): rename("%s/%s.%s" % (bak['dst'], arg, i-1), "%s/%s.%s" % (bak['dst'], arg, i)) # Make a hard-link-only copy of the latest snapshot if path.isdir("%s/%s.%s" % (bak['dst'], rotation[arg][0], rotation[arg][1])): call("%s -al %s/%s.%s %s/%s.%s" % (bin['cp'], bak['dst'], rotation[arg][0], rotation[arg][1], bak['dst'], arg, rotation[arg][2]), shell=True) # Rsync backup synchronization def sync(arg): rsyncopt = ( # Mirroring options: "--archive", # use the archive mode, "--verbose", # increase verbosity, "--delete", # delete extraneous files, "--delete-excluded", # delete excluded files "--exclude-from=%s" % files['exclude'], ) for i in bak['src']: # Create directories from source names if not path.isdir("%s/%s.1/%s" % (bak['dst'], arg, i[1:])): makedirs("%s/%s.1/%s" % (bak['dst'], arg, i[1:])) # Mirror directories with rsync and unlink files that changed call("%s %s %s %s/%s.1/%s" % (bin['rsync'], " ".join(rsyncopt), i, bak['dst'], arg, i[1:]), shell=True) # Make the backup reflect the snapshot time if path.isdir("%s/%s.1" % (bak['dst'], arg)): call("%s %s/%s.1" % (bin['touch'], bak['dst'], arg), shell=True) # Managing mount points def mount(action, arg=""): if action == "mount": if bak['efs'] and arg == "hourly": # Exit with EAGAIN if eCryptfs is mounted if call("%s -qs ecryptfs %s" % (bin['grep'], files['mounts']), shell=True) == 0: raise SystemExit(11) # Include the export for NFS if rmt['fst'] == "-t nfs": rmt['dev'] = "%s:%s" % (rmt['dev'], rmt['dst']) # Attempt to mount the destination if call("%s %s %s %s %s" % (bin[action], rmt['fst'], rmt['opt'], rmt['dev'], bak['dst']), shell=True) != 0: raise SystemExit("Error: could not mount destination") elif action == "umount": # Unmount the snapshot mountpoint (should never be writeable) if call("%s %s" % (bin[action], bak['dst']), shell=True) != 0: raise SystemExit("Error: could not unmount destination") # Backup procedure def main(): usage = "Usage: %s {hourly|daily|weekly|monthly}" % path.split(argv[0])[1] if getuid() != 0: raise SystemExit("Error: super user privileges required") try: if argv[1] in count: mount("mount", argv[1]) rotate(argv[1]) if argv[1] == "hourly": sync(argv[1]) mount("umount") else: raise SystemExit(usage) except IndexError: raise SystemExit(usage) if __name__ == "__main__": main()