summaryrefslogtreecommitdiff
path: root/rybackup.py
diff options
context:
space:
mode:
Diffstat (limited to 'rybackup.py')
-rwxr-xr-xrybackup.py171
1 files changed, 171 insertions, 0 deletions
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. <anrxc sysphere.org>
+
+# 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()