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

Last change on this file since 2974 was 2974, checked in by gdb, 14 years ago

Add status messages; continue after failed downloads.

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