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