--- /dev/null
+#!/usr/bin/python
+
+from invirt import database
+import os
+import subprocess
+import random
+import string
+import tempfile
+import urllib
+import math
+import optparse as op
+
+class InvirtImageException(Exception):
+ pass
+
+# verbosity = 0 means no output from the actual commands
+# verbosity = 1 means only errors from the actual commands
+# verbosity = 2 means all output from the actual commands
+verbosity = 0
+
+def getOutput():
+ global verbosity
+ return {
+ 'stdout': subprocess.PIPE if verbosity < 2 else None,
+ 'stderr': subprocess.PIPE if verbosity < 1 else None
+ }
+
+def lvcreate(name, size):
+ lvc = subprocess.Popen(['lvcreate', '-L', size, '-n', name, 'xenvg'],
+ stderr=subprocess.PIPE,
+ stdout=getOutput()['stdout'])
+ if not lvc.wait():
+ return 0
+ stderr = lvc.stderr.read()
+ if 'already exists in volume group' in stderr:
+ return 5
+ else:
+ if verbosity > 0:
+ print stderr
+ return 6
+
+def lvrename(dest, src):
+ lvr = subprocess.Popen(['lvrename', 'xenvg', src, dest],
+ stderr=subprocess.PIPE,
+ stdout=getOutput()['stdout'])
+ ret = lvr.wait()
+ if not ret:
+ return 0
+ stderr = lvr.stderr.read()
+ if 'not found in volume group' in stderr:
+ return 0
+ else:
+ if verbosity > 0:
+ print stderr
+ return ret
+
+def lv_random(func, pattern, *args):
+ """
+ Run some LVM-related command, optionally with a random string in
+ the LV name.
+
+ func takes an LV name plus whatever's in *args and returns the
+ return code of some LVM command, such as lvcreate or lvrename
+
+ pattern can contain at most one '%s' pattern, which will be
+ replaced by a 6-character random string.
+
+ If pattern contains a '%s', the script will attempt to re-run
+ itself if the error code indicates that the destination already
+ exists
+ """
+ # Keep trying until it works
+ while True:
+ rand_string = ''.join(random.choice(string.ascii_letters) \
+ for i in xrange(6))
+ if '%s' in pattern:
+ name = pattern % rand_string
+ else:
+ name = pattern
+ ret = func(name, *args)
+ if ret == 0:
+ return name
+ # 5 is the return code if the destination already exists
+ elif '%s' not in pattern or ret != 5:
+ raise InvirtImageException, 'E: Error running %s with args %s' % (func.__name__, args)
+
+def lvcreate_random(pattern, size):
+ """
+ Creates an LV, optionally with a random string in the name.
+
+ Call with a string formatting pattern with a single '%s' to use as
+ a pattern for the name of the new LV.
+ """
+ return lv_random(lvcreate, pattern, size)
+
+def lvrename_random(src, pattern):
+ """
+ Rename an LV to a new name with a random string incorporated.
+
+ Call with a string formatting pattern with a single '%s' to use as
+ a pattern for the name of the new LV
+ """
+ return lv_random(lvrename, pattern, src)
+
+def fetch_image(cdrom):
+ """
+ Download a cdrom from a URI, shelling out to rsync if appropriate
+ and otherwise trying to use urllib
+ """
+ full_uri = os.path.join(cdrom.mirror.uri_prefix, cdrom.uri_suffix)
+ temp_file = tempfile.mkstemp()[1]
+ try:
+ if full_uri.startswith('rsync://'):
+ if subprocess.call(['rsync', '--no-motd', '-tLP', full_uri, temp_file],
+ **getOutput()):
+ raise InvirtImageException, "E: Unable to download '%s'" % full_uri
+ else:
+ # I'm not going to look for errors here, because I bet it'll
+ # throw its own exceptions
+ urllib.urlretrieve(full_uri, temp_file)
+ return temp_file
+ except:
+ os.unlink(temp_file)
+ raise
+
+def copy_file(src, dest):
+ """
+ Copy a file from one location to another using dd
+ """
+ if subprocess.call(['dd', 'if=%s' % src, 'of=%s' % dest, 'bs=1M'],
+ **getOutput()):
+ raise InvirtImageException, 'E: Unable to transfer %s into %s' % (src, dest)
+
+def load_image(cdrom):
+ """
+ Update a cdrom image by downloading the latest version,
+ transferring it into an LV, moving the old LV out of the way and
+ the new LV into place
+ """
+ if cdrom.mirror_id is None:
+ return
+ temp_file = fetch_image(cdrom)
+ try:
+ cdrom_size = '%sM' % math.ceil((float(os.stat(temp_file).st_size) / (1024 * 1024)))
+ new_lv = lvcreate_random('image-new_%s_%%s' % cdrom.cdrom_id, cdrom_size)
+ copy_file(temp_file, '/dev/xenvg/%s' % new_lv)
+ lvrename_random('image_%s' % cdrom.cdrom_id, 'image-old_%s_%%s' % cdrom.cdrom_id)
+ lvrename_random(new_lv, 'image_%s' % cdrom.cdrom_id)
+ reap_images()
+ finally:
+ os.unlink(temp_file)
+
+def reap_images():
+ """
+ Remove stale cdrom images that are no longer in use
+
+ load_image doesn't attempt to remove the old image because it
+ might still be in use. reap_images attempts to delete any LVs
+ starting with 'image-old_', but ignores errors, in case they're
+ still being used.
+ """
+ lvm_list = subprocess.Popen(['lvs', '-o', 'lv_name', '--noheadings'],
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE)
+ lvm_list.wait()
+
+ for lv in map(str.strip, lvm_list.stdout.read().splitlines()):
+ if lv.startswith('image-old_'):
+ subprocess.call(['lvchange', '-a', 'n', '/dev/xenvg/%s' % lv],
+ **getOutput())
+ subprocess.call(['lvchange', '-a', 'n', '/dev/xenvg/%s' % lv],
+ **getOutput())
+ subprocess.call(['lvchange', '-a', 'ey', '/dev/xenvg/%s' % lv],
+ **getOutput())
+ subprocess.call(['lvremove', '--force', '/dev/xenvg/%s' % lv],
+ **getOutput())
+
+def main():
+ global verbosity
+
+ database.connect()
+
+ usage = """%prog [options] --add [--cdrom] cdrom_id description mirror_id uri_suffix
+ %prog [options] --add --mirror mirror_id uri_prefix
+
+ %prog [options] --update [short_name1 [short_name2 ...]]
+ %prog [options] --reap"""
+
+ parser = op.OptionParser(usage=usage)
+ parser.set_defaults(verbosity=0,
+ item='cdrom')
+
+ parser.add_option('-a', '--add', action='store_const',
+ dest='action', const='add',
+ help='Add a new item to the database')
+
+ parser.add_option('-u', '--update', action='store_const',
+ dest='action', const='update',
+ help='Update all cdrom images in the database with the latest version')
+ parser.add_option('-r', '--reap', action='store_const',
+ dest='action', const='reap',
+ help='Reap stale cdrom images that are no longer in use')
+
+ a_group = op.OptionGroup(parser, 'Adding new items')
+ a_group.add_option('-c', '--cdrom', action='store_const',
+ dest='item', const='cdrom',
+ help='Add a new cdrom to the database')
+ a_group.add_option('-m', '--mirror', action='store_const',
+ dest='item', const='mirror',
+ help='Add a new mirror to the database')
+ parser.add_option_group(a_group)
+
+ v_group = op.OptionGroup(parser, "Verbosity levels")
+ v_group.add_option("-q", "--quiet", action='store_const',
+ dest='verbosity', const=0,
+ help='Show no output from commands this script runs (default)')
+ v_group.add_option("-v", "--verbose", action='store_const',
+ dest='verbosity', const=1,
+ help='Show only errors from commands this script runs')
+ v_group.add_option("--noisy", action='store_const',
+ dest='verbosity', const=2,
+ help='Show all output from commands this script runs')
+ parser.add_option_group(v_group)
+
+ (options, args) = parser.parse_args()
+ verbosity = options.verbosity
+ if options.action is None:
+ print parser.format_help()
+ elif options.action == 'add':
+ if options.item == 'cdrom':
+ attrs = dict(zip(('cdrom_id', 'description', 'mirror_id', 'uri_suffix'),
+ args))
+ cdrom = database.CDROM(**attrs)
+ database.session.save(cdrom)
+ database.session.flush()
+
+ load_image(cdrom)
+
+ elif options.item == 'mirror':
+ attrs = dict(zip(('mirror_id', 'uri_prefix'),
+ args))
+ mirror = database.Mirror(**attrs)
+ database.session.save(mirror)
+ database.session.flush()
+ elif options.action == 'update':
+ if len(args) > 0:
+ images = [database.CDROM.query().get(arg) for arg in args]
+ else:
+ images = database.CDROM.query().all()
+ for cdrom in images:
+ if cdrom is not None:
+ load_image(cdrom)
+ elif options.action == 'reap':
+ reap_images()
+
+if __name__ == '__main__':
+ main()