Index: package_tags/invirt-images/0.0.2/debian/changelog
===================================================================
--- package_tags/invirt-images/0.0.2/debian/changelog	(revision 1856)
+++ package_tags/invirt-images/0.0.2/debian/changelog	(revision 1856)
@@ -0,0 +1,11 @@
+invirt-images (0.0.2) unstable; urgency=low
+
+  * Architecture: all
+
+ -- Greg Price <price@mit.edu>  Fri, 19 Dec 2008 21:25:40 -0500
+
+invirt-images (0.0.1) unstable; urgency=low
+
+  * Initial Release.
+
+ -- Evan Broder <broder@mit.edu>  Mon, 27 Oct 2008 21:38:15 -0400
Index: package_tags/invirt-images/0.0.2/debian/compat
===================================================================
--- package_tags/invirt-images/0.0.2/debian/compat	(revision 1856)
+++ package_tags/invirt-images/0.0.2/debian/compat	(revision 1856)
@@ -0,0 +1,1 @@
+5
Index: package_tags/invirt-images/0.0.2/debian/control
===================================================================
--- package_tags/invirt-images/0.0.2/debian/control	(revision 1856)
+++ package_tags/invirt-images/0.0.2/debian/control	(revision 1856)
@@ -0,0 +1,15 @@
+Source: invirt-images
+Section: base
+Priority: extra
+Maintainer: Invirt project <invirt@mit.edu>
+Build-Depends: cdbs, debhelper (>= 5)
+Standards-Version: 3.8.0
+
+Package: invirt-images
+Architecture: all
+Depends: ${shlibs:Depends}, ${misc:Depends}, python, invirt-database
+Description: Invirt's tools for managing disk images for VMs
+ These install the tools for managing disk images for Invirt
+ VMs. Images can share common mirrors. The tools include mechanisms
+ for adding new images, and for refreshing images that have already
+ been downloaded.
Index: package_tags/invirt-images/0.0.2/debian/copyright
===================================================================
--- package_tags/invirt-images/0.0.2/debian/copyright	(revision 1856)
+++ package_tags/invirt-images/0.0.2/debian/copyright	(revision 1856)
@@ -0,0 +1,16 @@
+This software was written as part of the Invirt project <invirt@mit.edu>.
+
+Copyright :
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+On Debian systems, the complete text of the GNU General Public License
+can be found in the file /usr/share/common-licenses/GPL.
Index: package_tags/invirt-images/0.0.2/debian/invirt-images.install
===================================================================
--- package_tags/invirt-images/0.0.2/debian/invirt-images.install	(revision 1856)
+++ package_tags/invirt-images/0.0.2/debian/invirt-images.install	(revision 1856)
@@ -0,0 +1,1 @@
+invirt-images usr/sbin
Index: package_tags/invirt-images/0.0.2/debian/rules
===================================================================
--- package_tags/invirt-images/0.0.2/debian/rules	(revision 1856)
+++ package_tags/invirt-images/0.0.2/debian/rules	(revision 1856)
@@ -0,0 +1,3 @@
+#!/usr/bin/make -f
+
+include /usr/share/cdbs/1/rules/debhelper.mk
Index: package_tags/invirt-images/0.0.2/invirt-images
===================================================================
--- package_tags/invirt-images/0.0.2/invirt-images	(revision 1856)
+++ package_tags/invirt-images/0.0.2/invirt-images	(revision 1856)
@@ -0,0 +1,257 @@
+#!/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()
