#!/usr/bin/python # Last modified: 2011-09-18 13:05 EDT # Copyright (c) 2011 by Reid Priedhorsky, . # # This script is distributed under the terms of the GNU General Public # License; see http://www.gnu.org/licenses/gpl.txt for more information. # # Bugs / To Do: # # - Security: don't read config file with execfile() # - Delete old tar files if they are identical to the new one. (Alternately, # skip creating the new tar file if nothing changed?) # - Fix tape() to use proper interface for get_newest() # - Do the tar() excludes really work properly (e.g. ./foo/tmp -- removed?). # This is masked by the rsync excludes, I think. # - Recipes other mirror() don't honor excludes and NO_BACKUP import logging as log import os import os.path import subprocess import string import sys import time ### Backup recipes ### def cmd(note, c): log.info('Running command (%s)' % (note)) do_cmd(c) def mirror(src, dest): log.info('Mirroring %s to %s ...' % (src, dest)) excludes = get_excludes(src) if (not os.path.isdir(dest)): log.error('%s is not a directory, bailing!' % (dest)) sys.exit(1) #do_cmd(RSYNC_CMD % ('%s %s/ %s' % (excludes, src, dest))) do_cmd(RSYNC_CMD % (src, excludes, dest)) def squash(src): log.info('Squashing %s ...' % (src)) dest = time.strftime(SQUASH_NAME, time.localtime(time.time())) dest = dest % (os.path.split(src)[1]) do_cmd(SQUASH_CMD % (src, TMP_DIR + '/' + dest)) squashsize = long(os.path.getsize(TMP_DIR + '/' + dest)) log.info('Squashfs is %d MiB' % (squashsize / (1024**2))) ensure_available_tarspace(squashsize) do_cmd('mv %s/%s %s' % (TMP_DIR, dest, TAR_DIR)) def tar(src): raise Exception('Untested since 2011-09-18') log.info('Tarring %s ...' % (src)) # Too lazy to figure out if these excludes work for tar #excludes = get_excludes(src) excludes = '' dest = time.strftime(TAR_NAME, time.localtime(time.time())) dest = dest % (os.path.split(src)[1]) do_cmd(TAR_CMD % (src, excludes, TMP_DIR + '/' + dest)) tarsize = long(os.path.getsize(TMP_DIR + '/' + dest)) log.info('Tarfile is %s bytes.' % str(tarsize)) ensure_available_tarspace(tarsize) do_cmd('mv %s/%s %s' % (TMP_DIR, dest, TAR_DIR)) #do_cmd('cp %s/%s %s' % (TMP_DIR, dest, TAR_DIR)) #do_cmd('rm %s/%s' % (TMP_DIR, dest)) log.info('Verifying tarfile (errors are not automatically detected).') do_cmd(TAR_VERIFY_CMD % (src, TAR_DIR + '/' + dest)) def remind(email, text): log.info('Emailing %s ...' % (email)) p = subprocess.Popen(MAIL_CMD % email, shell=True, stdin=subprocess.PIPE) p.communicate(text) def burn(*basenames): # For each basename, select the newest squashfs or tarfile in TAR_DIR, and # burn those files to DVD. arg = ' '.join([get_newest('%s/%s*super*' % (TAR_DIR, b)) for b in basenames]) log.info('Burning %s' % (arg)) do_cmd(BURN_CMD % (arg)) # FIXME -- This is currently broken. It uses the wrong get_newest() interface. # Probably, the fix is to chdir() into the tar_dir before operating. # Also, it still uses the obsolete print logging. def tape(*basenames): print '>>> Writing to tape: ', for name in basenames: print name, print '...' files = '' for name in basenames: files = files + ' ' + get_newest('%s*super*' % (name), TAR_DIR) do_cmd(TAPE_CMD % (TAR_DIR, TAPE_DEV, files)) print '>>> Verifying tape (errors are not automatically detected).' do_cmd(TAPE_VERIFY_CMD % (TAR_DIR, TAPE_DEV)) ### Constants ### DAILY = 'DAILY' WHEN = 0 FUNC = 1 ARGS = 2 ### Main program ### def main(): os.umask(0077) # files accessible by script runner (root) only sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', 0) sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) log.basicConfig(level=log.INFO, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%b-%d %H:%M:%S') log.info('superbackup starting') execfile(sys.argv[1],globals()) if len(sys.argv) > 2: today = sys.argv[2] log.info('Debug mode, today is `%s\'...' % today) else: today = string.lower(time.strftime('%a', now)) log.info('Executing recipes for %s' % time.strftime('%A', now)) for recipe in SCHEDULE: if (recipe[WHEN] == DAILY or recipe[WHEN] == today): apply(recipe[FUNC], tuple(recipe[ARGS])); log.info('superbackup finished') def do_cmd(c, ignore_errs=0): if VERBOSE: print c if os.system(c + ' 2>&1') and not ignore_errs: log.error('command "' + c + '" failed, bailing!') sys.exit(1) def ensure_available_tarspace(needed_size): while True: freebytes = (TAR_LIMIT \ - long(string.split(os.popen('du -s --bytes %s' % (TAR_DIR)).readline())[0])) log.info('%d MiB available in %s.' % (freebytes / (1024**2), TAR_DIR)) if freebytes > needed_size: break victim = get_oldest('%s/*super*' % (TAR_DIR)) do_cmd('rm %s' % (victim)) def get_oldest(pattern): return os.popen('ls -1rt %s' % (pattern)).readlines()[0][:-1] def get_newest(pattern): return os.popen('ls -1t %s' % (pattern)).readlines()[0][:-1] def get_excludes(srcdir): ex = ' '.join(["--exclude '%s'" % (f) for f in EXCLUDE_FILES] + ["--exclude='%s/*' --exclude='%s/.*'" % (d, d) for d in EXCLUDE_DIRS]) if (os.path.exists('%s/%s' % (srcdir, 'NO_BACKUP'))): ex += ' --exclude-from=./NO_BACKUP' return ex now = time.localtime(time.time()) if __name__ == '__main__': main()