From 165e534b291d3048f6e9e1bc4567670014cce51a Mon Sep 17 00:00:00 2001 From: "Adrian C. (anrxc)" Date: Sat, 29 May 2010 02:21:21 +0200 Subject: rybackup: imported first version --- rybackup.py | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100755 rybackup.py (limited to 'rybackup.py') diff --git a/rybackup.py b/rybackup.py new file mode 100755 index 0000000..eab93dd --- /dev/null +++ b/rybackup.py @@ -0,0 +1,171 @@ +#!/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" : "nfs", # file-system type + "opt" : "async,vers=3", # mount options + "dev" : "192.168.0.10", # device node or NFS server + "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(val): + 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'], val, count[val])): + rmtree("%s/%s.%s" % (bak['dst'], val, count[val])) + + # Shift the middle snapshot(s) back by one + for i in reversed(range(2, count[val]+1)): + if i-1 == 1 and val == "hourly": break + + if path.isdir("%s/%s.%s" % (bak['dst'], val, i-1)): + rename("%s/%s.%s" % (bak['dst'], val, i-1), + "%s/%s.%s" % (bak['dst'], val, i)) + + # Make a hard-link-only copy of the latest snapshot + if path.isdir("%s/%s.%s" % (bak['dst'], rotation[val][0], rotation[val][1])): + call("%s -al %s/%s.%s %s/%s.%s" % (bin['cp'], bak['dst'], rotation[val][0], + rotation[val][1], bak['dst'], val, + rotation[val][2]), shell=True) + +# Rsync backup synchronization +def sync(val): + rsyncmd = ( # 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'], val, i[1:])): + makedirs("%s/%s.1/%s" % (bak['dst'], val, i[1:])) + + # Mirror directories with rsync (auto. unlinks files that changed) + call("%s %s %s %s/%s.1/%s" % (bin['rsync'], " ".join(rsyncmd), + i, bak['dst'], val, i[1:]), shell=True) + + # Make the backup reflect the snapshot time + if path.isdir("%s/%s.1" % (bak['dst'], val)): + call("%s %s/%s.1" % (bin['touch'], bak['dst'], val), shell=True) + + +# Managing mount points +def mount(action): + if bak['efs']: + # Exit with EAGAIN if eCryptfs is mounted + if call("%s -qs ecryptfs %s" + % (bin['grep'], files['mounts']), shell=True) == 0: + raise SystemExit(11) + + if action == "mount": + # Include the export for NFS + if rmt['fst'] == "nfs": + rmt['dev'] = "%s:%s" % (rmt['dev'], rmt['dst']) + + # Attempt to mount the destination + if call("%s -t %s -o %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 + 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") + 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() -- cgit v1.2.3