source: trunk/packages/invirt-images/invirt-images @ 2176

Last change on this file since 2176 was 1365, checked in by broder, 16 years ago

Add package invirt-images for managing CDROM images

  • Property svn:executable set to *
File size: 9.1 KB
Line 
1#!/usr/bin/python
2
3from invirt import database
4import os
5import subprocess
6import random
7import string
8import tempfile
9import urllib
10import math
11import optparse as op
12
13class InvirtImageException(Exception):
14    pass
15
16# verbosity = 0 means no output from the actual commands
17# verbosity = 1 means only errors from the actual commands
18# verbosity = 2 means all output from the actual commands
19verbosity = 0
20
21def getOutput():
22    global verbosity
23    return {
24        'stdout': subprocess.PIPE if verbosity < 2 else None,
25        'stderr': subprocess.PIPE if verbosity < 1 else None
26        }
27
28def lvcreate(name, size):
29    lvc = subprocess.Popen(['lvcreate', '-L', size, '-n', name, 'xenvg'],
30                           stderr=subprocess.PIPE,
31                           stdout=getOutput()['stdout'])
32    if not lvc.wait():
33        return 0
34    stderr = lvc.stderr.read()
35    if 'already exists in volume group' in stderr:
36        return 5
37    else:
38        if verbosity > 0:
39            print stderr
40        return 6
41
42def lvrename(dest, src):
43    lvr = subprocess.Popen(['lvrename', 'xenvg', src, dest],
44                           stderr=subprocess.PIPE,
45                           stdout=getOutput()['stdout'])
46    ret = lvr.wait()
47    if not ret:
48        return 0
49    stderr = lvr.stderr.read()
50    if 'not found in volume group' in stderr:
51        return 0
52    else:
53        if verbosity > 0:
54            print stderr
55        return ret
56
57def lv_random(func, pattern, *args):
58    """
59    Run some LVM-related command, optionally with a random string in
60    the LV name.
61   
62    func takes an LV name plus whatever's in *args and returns the
63    return code of some LVM command, such as lvcreate or lvrename
64   
65    pattern can contain at most one '%s' pattern, which will be
66    replaced by a 6-character random string.
67   
68    If pattern contains a '%s', the script will attempt to re-run
69    itself if the error code indicates that the destination already
70    exists
71    """
72    # Keep trying until it works
73    while True:
74        rand_string = ''.join(random.choice(string.ascii_letters) \
75                                  for i in xrange(6))
76        if '%s' in pattern:
77            name = pattern % rand_string
78        else:
79            name = pattern
80        ret = func(name, *args)
81        if ret == 0:
82            return name
83        # 5 is the return code if the destination already exists
84        elif '%s' not in pattern or ret != 5:
85            raise InvirtImageException, 'E: Error running %s with args %s' % (func.__name__, args)
86
87def lvcreate_random(pattern, size):
88    """
89    Creates an LV, optionally with a random string in the name.
90   
91    Call with a string formatting pattern with a single '%s' to use as
92    a pattern for the name of the new LV.
93    """
94    return lv_random(lvcreate, pattern, size)
95
96def lvrename_random(src, pattern):
97    """
98    Rename an LV to a new name with a random string incorporated.
99   
100    Call with a string formatting pattern with a single '%s' to use as
101    a pattern for the name of the new LV
102    """
103    return lv_random(lvrename, pattern, src)
104
105def fetch_image(cdrom):
106    """
107    Download a cdrom from a URI, shelling out to rsync if appropriate
108    and otherwise trying to use urllib
109    """
110    full_uri = os.path.join(cdrom.mirror.uri_prefix, cdrom.uri_suffix)
111    temp_file = tempfile.mkstemp()[1]
112    try:
113        if full_uri.startswith('rsync://'):
114            if subprocess.call(['rsync', '--no-motd', '-tLP', full_uri, temp_file],
115                               **getOutput()):
116                raise InvirtImageException, "E: Unable to download '%s'" % full_uri
117        else:
118            # I'm not going to look for errors here, because I bet it'll
119            # throw its own exceptions
120            urllib.urlretrieve(full_uri, temp_file)
121        return temp_file
122    except:
123        os.unlink(temp_file)
124        raise
125
126def copy_file(src, dest):
127    """
128    Copy a file from one location to another using dd
129    """
130    if subprocess.call(['dd', 'if=%s' % src, 'of=%s' % dest, 'bs=1M'],
131                       **getOutput()):
132        raise InvirtImageException, 'E: Unable to transfer %s into %s' % (src, dest)
133
134def load_image(cdrom):
135    """
136    Update a cdrom image by downloading the latest version,
137    transferring it into an LV, moving the old LV out of the way and
138    the new LV into place
139    """
140    if cdrom.mirror_id is None:
141        return
142    temp_file = fetch_image(cdrom)
143    try:
144        cdrom_size = '%sM' % math.ceil((float(os.stat(temp_file).st_size) / (1024 * 1024)))
145        new_lv = lvcreate_random('image-new_%s_%%s' % cdrom.cdrom_id, cdrom_size)
146        copy_file(temp_file, '/dev/xenvg/%s' % new_lv)
147        lvrename_random('image_%s' % cdrom.cdrom_id, 'image-old_%s_%%s' % cdrom.cdrom_id)
148        lvrename_random(new_lv, 'image_%s' % cdrom.cdrom_id)
149        reap_images()
150    finally:
151        os.unlink(temp_file)
152
153def reap_images():
154    """
155    Remove stale cdrom images that are no longer in use
156   
157    load_image doesn't attempt to remove the old image because it
158    might still be in use. reap_images attempts to delete any LVs
159    starting with 'image-old_', but ignores errors, in case they're
160    still being used.
161    """
162    lvm_list = subprocess.Popen(['lvs', '-o', 'lv_name', '--noheadings'],
163                               stdout=subprocess.PIPE,
164                               stdin=subprocess.PIPE)
165    lvm_list.wait()
166   
167    for lv in map(str.strip, lvm_list.stdout.read().splitlines()):
168        if lv.startswith('image-old_'):
169            subprocess.call(['lvchange', '-a', 'n', '/dev/xenvg/%s' % lv],
170                            **getOutput())
171            subprocess.call(['lvchange', '-a', 'n', '/dev/xenvg/%s' % lv],
172                            **getOutput())
173            subprocess.call(['lvchange', '-a', 'ey', '/dev/xenvg/%s' % lv],
174                            **getOutput())
175            subprocess.call(['lvremove', '--force', '/dev/xenvg/%s' % lv],
176                            **getOutput())
177
178def main():
179    global verbosity
180   
181    database.connect()
182   
183    usage = """%prog [options] --add [--cdrom] cdrom_id description mirror_id uri_suffix
184       %prog [options] --add --mirror mirror_id uri_prefix
185
186       %prog [options] --update [short_name1 [short_name2 ...]]
187       %prog [options] --reap"""
188   
189    parser = op.OptionParser(usage=usage)
190    parser.set_defaults(verbosity=0,
191                        item='cdrom')
192   
193    parser.add_option('-a', '--add', action='store_const',
194                      dest='action', const='add',
195                      help='Add a new item to the database')
196   
197    parser.add_option('-u', '--update', action='store_const',
198                      dest='action', const='update',
199                      help='Update all cdrom images in the database with the latest version')
200    parser.add_option('-r', '--reap', action='store_const',
201                      dest='action', const='reap',
202                      help='Reap stale cdrom images that are no longer in use')
203   
204    a_group = op.OptionGroup(parser, 'Adding new items')
205    a_group.add_option('-c', '--cdrom', action='store_const',
206                       dest='item', const='cdrom',
207                       help='Add a new cdrom to the database')
208    a_group.add_option('-m', '--mirror', action='store_const',
209                       dest='item', const='mirror',
210                       help='Add a new mirror to the database')
211    parser.add_option_group(a_group)
212   
213    v_group = op.OptionGroup(parser, "Verbosity levels")
214    v_group.add_option("-q", "--quiet", action='store_const',
215                       dest='verbosity', const=0,
216                       help='Show no output from commands this script runs (default)')
217    v_group.add_option("-v", "--verbose", action='store_const',
218                       dest='verbosity', const=1,
219                       help='Show only errors from commands this script runs')
220    v_group.add_option("--noisy", action='store_const',
221                       dest='verbosity', const=2,
222                       help='Show all output from commands this script runs')
223    parser.add_option_group(v_group)
224   
225    (options, args) = parser.parse_args()
226    verbosity = options.verbosity
227    if options.action is None:
228        print parser.format_help()
229    elif options.action == 'add':
230        if options.item == 'cdrom':
231            attrs = dict(zip(('cdrom_id', 'description', 'mirror_id', 'uri_suffix'),
232                             args))
233            cdrom = database.CDROM(**attrs)
234            database.session.save(cdrom)
235            database.session.flush()
236           
237            load_image(cdrom)
238       
239        elif options.item == 'mirror':
240            attrs = dict(zip(('mirror_id', 'uri_prefix'),
241                             args))
242            mirror = database.Mirror(**attrs)
243            database.session.save(mirror)
244            database.session.flush()
245    elif options.action == 'update':
246        if len(args) > 0:
247            images = [database.CDROM.query().get(arg) for arg in args]
248        else:
249            images = database.CDROM.query().all()
250        for cdrom in images:
251            if cdrom is not None:
252                load_image(cdrom)
253    elif options.action == 'reap':
254        reap_images()
255
256if __name__ == '__main__':
257    main()
Note: See TracBrowser for help on using the repository browser.