#!/usr/bin/python2.2 from __future__ import generators, division import os, sys, shutil import re import time import atexit import mx.DateTime BackupsDir = '/backups' PidFile = '/var/run/rbackup.pid' PasswordFile = '/etc/rsync.password' #BackupsDir = '/main/work/users/time/tmp/backups' #PidFile = '/main/work/users/time/tmp/backups/rbackup.pid' Months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] Filename = re.compile(r'([^.]+)\.(\d{4})-(%s)-(\d{2})' % '|'.join(Months)) Names = {'d1':None, 'd2':None, 'd3':None, 'd4':None, 'd5':None, 'd6':None, 'd7':None, 'w01':None, 'w02':None, 'w04':None, 'w08':None, 'w16':None, 'w32':None, 'w64':None} Now = mx.DateTime.now() FirstSunday = mx.DateTime.Date(2002, 1, 1) WeekNum = (Now - FirstSunday).day // 7 class BackupError(Exception): pass def log(*args): message = ' '.join(args) print message def run(program, *args, **kw): sys.stdout.flush() sys.stderr.flush() log('run:', program, *args) args = (program,) + args s = os.spawnvpe(os.P_WAIT, program, args, kw) if s > 0: raise BackupError('%s failed with error code %d' % (program, s)) elif s < 0: raise BackupError('%s killed by signal %d' % (program, -s)) class Backup(object): def __init__(self, name, year, month, day): self.name = name self.date = mx.DateTime.Date(year, month, day) self.age = (Now - self.date).day self.age_weeks = self.age // 7 self._cmp = (self.age, self.name) def path(self): dstr = self.date.strftime('%Y-%b-%d') name = '%s.%s' % (self.name, dstr) return os.path.join(BackupsDir, name) def exists(self): return os.path.exists(self.path()) def __cmp__(self, other): if isinstance(other, Backup): return cmp(self._cmp, other._cmp) else: raise TypeError('Backup objects cannot be compared with other types') def rename(self, newname): oldname = self.name oldpath = self.path() oldexists = self.exists() self.name = newname if self.exists(): self.name = oldname raise BackupError( 'cannot rename "%s" to "%s", destination exists' % (oldname, newname)) if oldexists: log('move', oldname, newname) os.rename(oldpath, self.path()) def copy_to_today(self): today = Backup('d1', Now.year, Now.month, Now.day) if today.exists(): raise BackupError('backup already exists for today') log('copy', self.name, today.name) run('cp', '-al', self.path(), today.path()) return today def rsync(self): if not self.exists(): raise BackupError('rsync is only valid on existing backups') passwd = open(PasswordFile, 'r').read().strip() log('rsync', self.name) run('/usr/bin/rsync', '--archive', '--delete', '--numeric-ids', '--verbose', 'backup@islay::backup/', self.path(), RSYNC_PASSWORD=passwd) def delete(self): log('remove', self.name) #shutil.rmtree(self.path()) run('rm', '-rf', self.path()) def get_backup_list(): backups = [] for fname in os.listdir(BackupsDir): m = Filename.match(fname) if m: name, year, month, day = m.groups() if name in Names: b = Backup(name, int(year), Months.index(month)+1, int(day)) backups.append(b) backups.sort() return backups class BackupSet(object): def __init__(self, get_list_func=get_backup_list): self.get_list_func = get_list_func self.backups = [] self.byname = Names.copy() def __getitem__(self, name): return self.byname[name] def __setitem__(self, name, backup): self.byname[name] = backup def reload(self): self.backups = self.get_list_func() self.refresh() def refresh(self): self.byname = Names.copy() for b in self.backups: self.byname[b.name] = b def rename(self, oldname, newname): if self.byname[newname]: raise BackupError( 'cannot rename "%s" to "%s", destination exists' % (oldname, newname)) backup = self.byname[oldname] if backup: backup.rename(newname) self.byname[newname] = backup self.byname[oldname] = None def delete(self, name): backup = self.byname[name] if backup: backup.delete() self.byname[name] = None def move_weekly_down(self, w): old = 'w%02d' % w new = 'w%02d' % (2*w) if w == 64: self[old].delete() else: if self[new]: self.delete(new) self.rename(old, new) def shuffle_down(self): # move weekly backups if it's sunday if Now.day_of_week == 6: for w in 64,32,16,8,4,2,1: b = self['w%02d' % w] if b and WeekNum % (2*w) == 0: self.move_weekly_down(w) if self['d7']: if self['w01']: self.delete('w01') self.rename('d7', 'w01') else: self.delete('d7') # move daily backups for d in 6,5,4,3,2,1: self.rename('d%d' % d, 'd%d' % (d+1)) def create_today(self): if self['d1']: raise BackupError('today\'s Backup already exists') today = self.backups[0].copy_to_today() today.rsync() def run(self): self.reload() if self.backups[0].age < 1: raise BackupError('backup already exists for today') log('[', ' '.join([ '%s(%d)' % (b.name,b.age) for b in self.backups ]), ']') self.shuffle_down() self.create_today() self.reload() def _pidfile_exitfunc(): try: os.unlink(PidFile) except OSError, e: if e.errno == 2: pass else: raise def pidfile(): # NOTE: O_CREAT|O_EXCL is not enough for locking on an # NFS filesystem, but it's easy and we're not using NFS. fd = None try: fd = os.open(PidFile, os.O_WRONLY|os.O_CREAT|os.O_EXCL) except OSError, e: if e.errno == 17: raise BackupError('backup already running') else: raise try: atexit.register(_pidfile_exitfunc) os.write(fd, '%d\n' % os.getpid()) finally: os.close(fd) def main(): pidfile() b = BackupSet() b.run() def main_rsync(): pidfile() b = BackupSet() b.reload() today = b.backups[0] if today.age > 1: raise BackupError('no backup exists for today') today.rsync() ##def test(): ## global Now, WeekNum ## real_now = Now ## ## for i in range(100): ## Now = real_now + i ## WeekNum = (Now - FirstSunday).day // 7 ## ## log(Now.strftime('%Y-%b-%d (%a)')) ## log('week =', str(WeekNum)) ## main() ## os.unlink(PidFile) ## log()