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