#!/usr/bin/python # Joe G - 9/11/14 # LVM VM backup script whipped up in like 20 minutes. Accesses VM partitions inside LVs # (named like vm-debianshiz) and copies files out of ones for linux and bsd with rsync # # TODO: # - better error handling # - create lv snapshot for each VM, fsck it, and go off that so this can run # while the VMs are mounted & being used # - suck less in general # - make algorithms and flow more pythonic / less code # - run rsync with ionice/nice # - make this pep8/flake compatible (with exception for 2 spaces instead of 4) # - use convenience shorter subprocess methods instead of just Popen import subprocess import time import re import os FILESYSTEMS = ['ext4', 'jfs', 'ufs', 'ext3', 'ext2', 'btrfs', 'xfs'] MOUNTOPTIONS = ['ro', 'noatime'] FSOPTIONS = {'ufs': ['ufstype=ufs2']} # XXX: freebsd uses ufs2 TMPMOUNT = '/mnt/tmp' BAKDEST = '/srv/media/backup/vm' def getVMs(): cmd = ['lvs'] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False) except OSError: raise output = proc.communicate()[0] for line in output.split('\n')[1:]: m = re.search('^(vm\-\S+)\s+(\S+)', line.strip()) if m: vm = m.group(1)[3:] yield (vm, '/dev/{1}/{0}'.format(*m.groups())) def getPartitions(lv): parts = [] # XXX: Get list of partitions inside LV cmd = ['kpartx', '-l', lv] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) except OSError: raise output = proc.communicate()[0] for line in output.split('\n'): try: parts.append(line.strip().split()[0]) except IndexError: pass if not len(parts): return # XXX: Make them accessible under /dev/mapper cmd = ['kpartx', '-a', lv] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) except OSError: raise proc.wait() if proc.returncode != 0: cleanupPartitions(lv) return # XXX: Use blkid to get the filesystem of each partition cmd = ['blkid'] + ['/dev/mapper/{0}'.format(name) for name in parts] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) except OSError: raise output = proc.communicate()[0] if proc.returncode != 0: cleanupPartitions(lv) return # TODO: make following code smaller for line in output.split('\n'): halves = line.split(':') path = halves[0] if not len(path) or len(halves) != 2: continue options = {} for pair in halves[1].split(): parts = pair.replace('"', '').split('=') try: options[parts[0]] = parts[1] except IndexError: pass try: yield (path, options['TYPE']) except KeyError: pass def cleanupPartitions(lv): # TODO: make this give up after 3 tries while True: time.sleep(1) cmd = ['kpartx', '-d', lv] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) except OSError: raise out = proc.communicate()[0] if proc.returncode == 0: break print 'Trying again to unbind {0} {1} "{2}"..'.format(lv, proc.returncode, out) def mount(src, dest, fs): options = MOUNTOPTIONS if fs in FSOPTIONS: options += FSOPTIONS[fs] cmd = ['mount', '-o', ','.join(options), src, dest] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) except OSError: raise proc.wait() if proc.returncode != 0: raise Exception('Failed mounting') def umount(path): # TODO: make this try 3 times, and run fuser -mvk $path on # subsequent runs to kill open handles cmd = ['umount', path] try: proc = subprocess.Popen(cmd, shell=False) except OSError: raise out = proc.communicate()[0] if proc.returncode != 0: raise Exception('Failed umounting: {0}'.format(out)) def sync(src, dest): # TODO: make rsync exclude /usr/ports on ufs and the cache dirs for # yum/apt-get on linux FSs. if not src.endswith('/'): src += '/' if not dest.endswith('/'): dest += '/' cmd = ['rsync', '-avP', src, dest] try: try: proc = subprocess.Popen(cmd, shell=False) except OSError: raise proc.wait() if proc.returncode == 0: raise Exception('Failed syncing') except KeyboardInterrupt: print 'asshole' for vm, lv in getVMs(): for path, fs in getPartitions(lv): if fs not in FILESYSTEMS: continue tmpdest = TMPMOUNT bakroot = os.path.join(BAKDEST, vm, path[-1]) if not os.path.exists(bakroot): os.makedirs(bakroot, 0700) try: mount(path, tmpdest, fs) except Exception as e: print e umount(tmpdest) continue try: sync(tmpdest, bakroot) except Exception as e: print e umount(tmpdest) cleanupPartitions(lv)