diff options
Diffstat (limited to 'Tools/portbuild/scripts/zbackup')
-rwxr-xr-x | Tools/portbuild/scripts/zbackup | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/Tools/portbuild/scripts/zbackup b/Tools/portbuild/scripts/zbackup new file mode 100755 index 000000000000..c51b9342b5fe --- /dev/null +++ b/Tools/portbuild/scripts/zbackup @@ -0,0 +1,217 @@ +#!/usr/bin/env python + +# Back up a list of ZFS filesystems, doing a full backup periodically +# and using incremental diffs in between + +import zfs, commands, datetime, sys, os, bz2 + +from signal import * + +# List of filesystems to backup +backuplist=["a", "a/nfs", "a/src", "a/local", "a/ports", "a/portbuild", + "a/portbuild/amd64", "a/portbuild/i386", + "a/portbuild/sparc64", "a/portbuild/ia64"] + +# Directory to store backups +backupdir="/dumpster/pointyhat/backup" + +# How many days between full backups +fullinterval=14 + +def validate(): + fslist = zfs.getallfs() + + missing = set(backuplist).difference(set(fslist)) + if len(missing) > 0: + print "Backup list refers to filesystems that do not exist: %s" % missing + sys.exit(1) + +def mkdirp(path): + + plist = path.split("/") + + for i in xrange(2,len(plist)+1): + sofar = "/".join(plist[0:i]) + if not os.path.isdir(sofar): + os.mkdir(sofar) + +class node(object): + child=None + parent=None + name=None + visited=0 + + def __init__(self, name): + self.name = name + self.child = [] + self.parent = None + self.visited = 0 + +for fs in backuplist: + + dir = backupdir + "/" + fs + mkdirp(dir) + + snaplist = [snap[0] for snap in zfs.getallsnaps(fs) if snap[0].isdigit()] + + dofull = 0 + + # Mapping from backup date tag to node + backups={} + + # list of old-new pairs seen + seen=[] + + # Most recent snapshot date + latest = "0" + for j in os.listdir(dir): + (old, sep, new) = j.partition('-') + if not old.isdigit() or not new.isdigit(): + continue + + seen.append("%s-%s" % (old, new)) + + if int(old) >= int(new): + print "Warning: backup sequence not monotonic: %s >= %s" % (old, new) + continue + + try: + oldnode = backups[old] + except KeyError: + oldnode = node(old) + backups[old] = oldnode + + try: + newnode = backups[new] + except KeyError: + newnode = node(new) + backups[new] = newnode + + if int(new) > int(latest): + latest = new + + oldnode.child.append(newnode) + if newnode.parent: + # We are not a tree! + if not dofull: + print "Multiple backup sequences found, forcing full dump!" + dofull = 1 + continue + + newnode.parent = oldnode + + if not "0" in backups and not dofull: + # No root! + print "No full backup found!" + dofull = 1 + + if not latest in snaplist and not dofull: + print "Latest dumped snapshot no longer exists: forcing full dump" + dofull = 1 + + now = datetime.datetime.now() + nowdate = now.strftime("%Y%m%d%H%M") + + try: + prev = datetime.datetime.strptime(latest, "%Y%m%d%H%M") + except ValueError: + if not dofull: + print "Unable to parse latest snapshot as a date, forcing full dump!" + dofull = 1 + + print "Creating zfs snapshot %s@%s" % (fs, nowdate) + zfs.createsnap(fs, nowdate) + + # Find path from latest back to root + try: + cur = backups[latest] + except KeyError: + cur = None + + chain = [] + firstname = "0" + # Skip if latest doesn't exist or chain is corrupt + while cur: + chain.append("%s-%s" % (cur.parent.name, cur.name)) + par = cur.parent + + # Remove from the backup tree so we can delete the leftovers + # below + par.child.remove(cur) + cur.parent=None + + if par.name == "0": + firstname = cur.name + break + cur = par + + chain.reverse() + + print chain + + # Prune stale links not in the backup chain + for j in backups.iterkeys(): + cur = backups[j] + for k in cur.child: + stale="%s-%s" % (cur.name, k.name) + print "Deleting %s" % stale + os.remove("%s/%s/%s" % (backupdir, fs, stale)) + + # Lookup date of full dump + try: + first = datetime.datetime.strptime(firstname, "%Y%m%d%H%M") + except ValueError: + if not dofull: + print "Unable to parse first snapshot as a date, forcing full dump!" + dofull = 1 + + if not dofull and (now - first) > datetime.timedelta(days=fullinterval): + print "Previous full backup too old, forcing full dump!" + dofull = 1 + + # In case we are interrupted don't leave behind a truncated file + # that will corrupt the backup chain + + if dofull: + latest = "0" + + outfile="%s/%s/.%s-%s" % (backupdir, fs, latest, nowdate) + + # zfs send aborts on receiving a signal + signal(SIGTSTP, SIG_IGN) + if not dofull: + print "Doing incremental of %s: %s-%s" % (fs, latest, nowdate) + (err, out) = \ + commands.getstatusoutput("zfs send -i %s %s@%s | bzip2 > %s" % + (latest, fs, nowdate, outfile)) + else: + print "Doing full backup of %s" % fs + latest = "0" + (err, out) = \ + commands.getstatusoutput("zfs send %s@%s | bzip2 > %s" % + (fs, nowdate, outfile)) + signal(SIGTSTP, SIG_DFL) + + if err: + print "Error from snapshot: (%s, %s)" % (err, out) + try: + os.remove(outfile) + print "Deleted %s" % outfile + except OSError, err: + print repr(err) + if err.errno != 2: + raise + finally: + sys.exit(1) + + # We seem to be finished + try: + os.rename(outfile, "%s/%s/%s-%s" % (backupdir, fs, latest, nowdate)) + except: + print "Error renaming dump file!" + raise + + if dofull: + for i in seen: + print "Removing stale snapshot %s/%s" % (dir, i) + os.remove("%s/%s" % (dir, i)) |