Merge lp:~nuclearbob/utah/iso-download into lp:utah

Proposed by Max Brustkern
Status: Merged
Merged at revision: 741
Proposed branch: lp:~nuclearbob/utah/iso-download
Merge into: lp:utah
Diff against target: 437 lines (+196/-35)
7 files modified
debian/control (+1/-1)
examples/run_utah_tests.py (+5/-10)
utah/config.py (+6/-0)
utah/iso.py (+147/-9)
utah/provisioning/inventory/sqlite.py (+2/-2)
utah/provisioning/provisioning.py (+4/-5)
utah/provisioning/vm/libvirtvm.py (+31/-8)
To merge this branch: bzr merge lp:~nuclearbob/utah/iso-download
Reviewer Review Type Date Requested Status
Joe Talbott (community) Approve
Review via email: mp+133554@code.launchpad.net

Description of the change

This branch adds support for automatically downloading an image by passing series, installtype, and arch to the ISO class. It also changes CustomVM to create a temporary copy of the image it uses to install so multiple simultaneous runs don't get hung up with libvirt trying to lock the image. It also sets CustomVM as the default VM provisioning method for all cases, putting us in a position to deprecate VMToolsVM since vm-tools has been replaced.

To post a comment you must log in.
lp:~nuclearbob/utah/iso-download updated
705. By Max Brustkern

Added comments to new config options

Revision history for this message
Joe Talbott (joetalbott) wrote :

Looks good to me.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2012-10-25 20:07:09 +0000
3+++ debian/control 2012-11-08 20:12:18 +0000
4@@ -17,7 +17,7 @@
5 python-apt, python-libvirt,
6 python-netifaces, python-paramiko, python-psutil,
7 utah-client (=${binary:Version})
8-Recommends: kvm
9+Recommends: dl-ubuntu-test-iso, kvm
10 Suggests: cobbler, u-boot-tools, vm-tools
11 Description: Ubuntu Test Automation Harness
12 Automation framework for testing in Ubuntu
13
14=== modified file 'examples/run_utah_tests.py'
15--- examples/run_utah_tests.py 2012-10-19 21:57:01 +0000
16+++ examples/run_utah_tests.py 2012-11-08 20:12:18 +0000
17@@ -7,7 +7,7 @@
18 from utah.group import check_user_group, print_group_error_message
19 from utah.timeout import timeout, UTAHTimeout
20 from utah import config
21-from run_test_vm import run_test_vm
22+from run_install_test import run_install_test
23
24
25 def get_parser():
26@@ -87,21 +87,16 @@
27 if args is None:
28 args = get_parser().parse_args()
29
30+ # Default is now CustomVM
31+ function = run_install_test
32 if args.arch is not None and 'arm' in args.arch:
33+ # If arch is arm, use BambooFeederMachine
34 from run_test_bamboo_feeder import run_test_bamboo_feeder
35 function = run_test_bamboo_feeder
36 elif args.machinetype == 'physical':
37+ # If machinetype is physical but arch isn't arm, use CobblerMachine
38 from run_test_cobbler import run_test_cobbler
39 function = run_test_cobbler
40- else:
41- function = run_test_vm
42-
43- for option in [args.boot, args.emulator, args.image, args.initrd,
44- args.kernel, args.preseed, args.xml]:
45- if option is not None:
46- from run_install_test import run_install_test
47- function = run_install_test
48- break
49
50 function(args=args)
51
52
53=== modified file 'utah/config.py'
54--- utah/config.py 2012-11-08 16:27:38 +0000
55+++ utah/config.py 2012-11-08 20:12:18 +0000
56@@ -31,6 +31,10 @@
57 checktimeout=15,
58 consoleloglevel=logging.WARNING,
59 debug=False,
60+ # Command to download test images
61+ dlcommand='dl-ubuntu-test-iso',
62+ # Number of times to retry image download
63+ dlretries=10,
64 fileloglevel=logging.INFO,
65 group='utah',
66 image=None,
67@@ -38,6 +42,8 @@
68 installpackages=['openssh-server', 'gdebi'],
69 installtimeout=3600,
70 installtype='desktop',
71+ # Directory to store local images
72+ isodir='/var/cache/utah/iso',
73 kernel=None,
74 logpath=os.path.join('/', 'var', 'log', 'utah'),
75 machineid=None,
76
77=== modified file 'utah/iso.py'
78--- utah/iso.py 2012-08-30 08:45:59 +0000
79+++ utah/iso.py 2012-11-08 20:12:18 +0000
80@@ -1,20 +1,24 @@
81 """
82 All commands use bsdtar and accept a logmethod to use for logging.
83 """
84-#TODO: Make an iso class that can return things like arch, series, type, etc.
85-#TODO: Add the existing functions to the iso class.
86-#TODO: Convert the code to use the class instead of the existing functions.
87 import logging
88 import logging.handlers
89 import os
90+import shutil
91 import subprocess
92 import sys
93 import urllib
94
95+from hashlib import md5
96+
97 from utah import config
98 from utah.exceptions import UTAHException
99
100
101+# TODO: eliminate usage of the non-class functions
102+# TODO: move the non-class functions into the class
103+
104+
105 def listfiles(image, logmethod=None, returnlist=False):
106 """
107 Return either a subprocess instance listing the contents of an iso, or a
108@@ -115,18 +119,28 @@
109 """
110 Provide a simplified method of interfacing with images.
111 """
112- def __init__(self, image, dlpercentincrement=1, logger=None):
113+ def __init__(self, arch=None, dlpercentincrement=1, dlretries=None,
114+ image=None, installtype=None, logger=None, series=None):
115 self.dlpercentincrement = dlpercentincrement
116+ if dlretries is None:
117+ self.dlretries = config.dlretries
118+ else:
119+ self.dlretries = dlretries
120 if logger is None:
121 self._loggersetup()
122 else:
123 self.logger = logger
124- if image.startswith('~'):
125- path = os.path.expanduser(image)
126- self.logger.debug('Rewriting ~-based path: ' + image +
127- ' to: ' + path)
128+ if image is None:
129+ self.logger.debug('Need to download image')
130+ path = self.downloadiso(arch=arch, installtype=installtype,
131+ series=series)
132 else:
133- path = image
134+ if image.startswith('~'):
135+ path = os.path.expanduser(image)
136+ self.logger.debug('Rewriting ~-based path: ' + image +
137+ ' to: ' + path)
138+ else:
139+ path = image
140
141 self.percent = 0
142 self.logger.info('Preparing image: ' + path)
143@@ -218,6 +232,9 @@
144 return series
145
146 def dldisplay(self, blocks, size, total):
147+ """
148+ Log download information.
149+ """
150 read = blocks * size
151 percent = 100 * read / total
152 if percent >= self.percent:
153@@ -226,19 +243,140 @@
154 self.logger.debug("%s read, %s%% of %s total" % (read, percent, total))
155
156 def listfiles(self, returnlist=False):
157+ """
158+ List all files in the image.
159+ """
160 return listfiles(self.image, self.logger.debug, returnlist=returnlist)
161
162 def getrealfile(self, filename, outdir=None):
163+ """
164+ Return the real path to a file (following links if needed.)
165+ """
166 return getrealfile(self.image, filename, outdir=outdir,
167 logmethod=self.logger.debug)
168
169 def extract(self, filename, outdir=None, outfile=None, **kw):
170+ """
171+ Extract a file.
172+ """
173 return extract(self.image, filename, outdir, outfile,
174 logmethod=self.logger.debug, **kw)
175
176 def dump(self, filename, **kw):
177+ """
178+ Dump a file.
179+ """
180 return dump(self.image, filename, logmethod=self.logger.debug, **kw)
181
182 def extractall(self):
183+ """
184+ Extract all files in the image.
185+ """
186 for myfile in self.listfiles(returnlist=True):
187 self.extract(myfile)
188+
189+ def getmd5(self, path):
190+ """
191+ Return the md5 checksum of a file.
192+ """
193+ if path is None:
194+ path = self.image
195+ self.logger.debug('Getting md5 of ' + path)
196+ isohash = md5()
197+ with open(path) as myfile:
198+ for block in iter(lambda: myfile.read(128), ""):
199+ isohash.update(block)
200+ filemd5 = isohash.hexdigest()
201+ self.logger.debug('md5 of ' + path + ' is ' + filemd5)
202+ return filemd5
203+
204+ def downloadiso(self, arch=None, installtype=None, series=None):
205+ """
206+ Download an ISO given series, type, and arch.
207+ """
208+ if arch is None:
209+ arch = config.arch
210+ if installtype is None:
211+ installtype = config.installtype
212+ if series is None:
213+ series = config.series
214+ filename = '-'.join([series, installtype, arch]) + '.iso'
215+ self.logger.info('Attempting to download ' + filename)
216+ with open(os.devnull, "w") as fnull:
217+ # If dlcommand (default dl-ubuntu-test-iso) is available, use it
218+ if subprocess.call(['which', config.dlcommand],
219+ stdout=fnull, stderr=fnull) == 0:
220+ self.logger.debug('Using ' + config.dlcommand)
221+ if installtype == 'server':
222+ flavor = 'ubuntu-server'
223+ else:
224+ flavor = 'ubuntu'
225+ cmd = [config.dlcommand,
226+ '-q',
227+ '--flavor=' + flavor,
228+ '--release=' + series,
229+ '--variant=' + installtype,
230+ '--arch=' + arch,
231+ '--isoroot=' + config.isodir]
232+ self.logger.info('Downloading ISO')
233+ self.logger.debug(' '.join(cmd))
234+ subprocess.check_call(cmd)
235+ path = os.path.join(config.isodir, flavor, filename)
236+ if os.path.isfile(path):
237+ return path
238+
239+ # If we haven't returned, dlcommand didn't give us an image
240+ # We'll need to use the utah image cache in config.isodir
241+ path = os.path.join(config.isodir, filename)
242+ # We want to verify our image matches the latest md5 from the server
243+ # To do this, we'll loop
244+ # First, we'll check the image. If it matches, we return
245+ # If not, we'll download it, and start the loop over
246+ for attempt in range(config.dlretries):
247+ self.logger.info('Download attempt ' + str(attempt))
248+ # Set the path to our files and the name of the iso we want
249+ if installtype == 'mini':
250+ remotepath = ('http://archive.ubuntu.com/ubuntu/dists/'
251+ '{series}/main/installer-{arch}/current/'
252+ 'images/'.format(arch=arch, series=series))
253+ isopattern = 'netboot/mini.iso'
254+ else:
255+ remotepath = ('http://releases.ubuntu.com/'
256+ '{series}/'.format(series=series))
257+ isopattern = installtype + '-' + arch + '.iso'
258+
259+ # Scan the MD5SUMS file for our file
260+ servermd5 = None
261+ for line in urllib.urlopen(remotepath + '/MD5SUMS'):
262+ if isopattern in line:
263+ servermd5, isofile = line.split()
264+ isofile = isofile.strip('*')
265+ break
266+
267+ # If we can't find the ISO, raise an exception
268+ if servermd5 is None:
269+ raise UTAHException('Specified ISO: ' + filename
270+ + ' not found on mirrors.')
271+ self.logger.debug('Server md5 is ' + servermd5)
272+
273+ # If the ISO exists locally and the md5 matches, use it
274+ if os.path.isfile(path) and servermd5 == self.getmd5(path):
275+ self.logger.info('Using ISO at ' + path)
276+ return path
277+
278+ # Try to download the ISO
279+ isopath = os.path.join(remotepath, isofile)
280+ self.percent = 0
281+ self.logger.info('Attempting to download ' + isopath)
282+ temppath = urllib.urlretrieve(isopath, reporthook=self.dldisplay)[0]
283+
284+ # Try to copy it into our cache
285+ self.logger.debug('Copying ' + temppath + ' to ' + path)
286+ shutil.copyfile(temppath, path)
287+ else:
288+ if os.path.isfile(path) and servermd5 == self.getmd5(path):
289+ return path
290+ else:
291+ raise UTAHException('Image failed to download after ' +
292+ config.dlretries + ' tries')
293+
294
295=== modified file 'utah/provisioning/inventory/sqlite.py'
296--- utah/provisioning/inventory/sqlite.py 2012-10-19 13:04:40 +0000
297+++ utah/provisioning/inventory/sqlite.py 2012-11-08 20:12:18 +0000
298@@ -5,7 +5,7 @@
299 from utah.provisioning.inventory.exceptions import \
300 UTAHProvisioningInventoryException
301 from utah.provisioning.inventory.inventory import Inventory
302-from utah.provisioning.vm.libvirtvm import VMToolsVM
303+from utah.provisioning.vm.libvirtvm import CustomVM
304 from utah.provisioning.baremetal.cobbler import CobblerMachine
305
306
307@@ -42,7 +42,7 @@
308 'CREATE TABLE IF NOT EXISTS '
309 'machines(machineid INTEGER PRIMARY KEY, state TEXT)')
310
311- def request(self, machinetype=VMToolsVM, *args, **kw):
312+ def request(self, machinetype=CustomVM, *args, **kw):
313 """
314 Takes a Machine class as machinetype, and passes the newly generated
315 machineid along with all other arguments to that class's constructor,
316
317=== modified file 'utah/provisioning/provisioning.py'
318--- utah/provisioning/provisioning.py 2012-10-25 20:07:44 +0000
319+++ utah/provisioning/provisioning.py 2012-11-08 20:12:18 +0000
320@@ -137,12 +137,11 @@
321
322 fileargs = ['preseed', 'xml', 'kernel', 'initrd']
323
324- if image is None:
325- self.image = None
326- elif image.endswith('.iso'):
327- self.image = ISO(image,
328+ if image is None or image.endswith('.iso'):
329+ self.image = ISO(arch=arch,
330 dlpercentincrement=self.dlpercentincrement,
331- logger=self.logger)
332+ image=image, installtype=installtype, logger=self.logger,
333+ series=series)
334 else:
335 fileargs.append('image')
336
337
338=== modified file 'utah/provisioning/vm/libvirtvm.py'
339--- utah/provisioning/vm/libvirtvm.py 2012-10-19 21:57:01 +0000
340+++ utah/provisioning/vm/libvirtvm.py 2012-11-08 20:12:18 +0000
341@@ -454,7 +454,7 @@
342 return True
343 return False
344
345- def _installxml(self, cmdline=None, initrd=None,
346+ def _installxml(self, cmdline=None, image=None, initrd=None,
347 kernel=None, tmpdir=None, xml=None):
348 """
349 Return the XML tree to be passed to libvirt for VM installation.
350@@ -462,6 +462,8 @@
351 self.logger.info('Creating installation XML')
352 if cmdline is None:
353 cmdline = self.cmdline
354+ if image is None:
355+ image = self.image.image
356 if initrd is None:
357 initrd = self.initrd
358 if kernel is None:
359@@ -475,7 +477,8 @@
360 self.logger.debug('Setting VM to shutdown on reboot')
361 xmlt.find('on_reboot').text = 'destroy'
362 if self.rewrite == 'all':
363- self._installxml_rewrite_all(cmdline, initrd, kernel, xmlt)
364+ self._installxml_rewrite_all(cmdline, image, initrd, kernel,
365+ xmlt)
366 else:
367 self.logger.info('Not rewriting XML because rewrite is ' +
368 self.rewrite)
369@@ -484,8 +487,8 @@
370 self.logger.info('Installation XML ready')
371 return xmlt
372
373- def _installxml_rewrite_all(self, cmdline_txt, initrd_txt, kernel_txt,
374- xmlt):
375+ def _installxml_rewrite_all(self, cmdline_txt, image, initrd_txt,
376+ kernel_txt, xmlt):
377 """
378 Rewrite the whole configuration file for the VM
379 """
380@@ -528,11 +531,11 @@
381 #TODO: Add a cdrom if none exists
382 if disk.get('device') == 'cdrom':
383 if disk.find('source') is not None:
384- disk.find('source').set('file', self.image.image)
385+ disk.find('source').set('file', image)
386 self.logger.debug('Rewrote existing CD-ROM')
387 else:
388 source = ElementTree.Element('source')
389- source.set('file', self.image.image)
390+ source.set('file', image)
391 disk.append(source)
392 self.logger.debug('Added source to existing '
393 'CD-ROM')
394@@ -631,6 +634,9 @@
395 ose.remove(cmdline)
396 devices = xml.find('devices')
397 devices.remove(devices.find('serial'))
398+ for disk in list(devices.iterfind('disk')):
399+ if disk.get('device') == 'cdrom':
400+ disk.remove(disk.find('source'))
401 else:
402 self.logger.info('Not rewriting XML because rewrite is ' +
403 self.rewrite)
404@@ -638,6 +644,21 @@
405 xml.write(os.path.join(tmpdir, 'final.xml'))
406 return xml
407
408+ def _tmpimage(self, image=None, tmpdir=None):
409+ """
410+ Create a temporary copy of the image so libvirt will lock that copy.
411+ This allows other simultaneous processes to update the cached image.
412+ """
413+ if image is None:
414+ image = self.image.image
415+ if tmpdir is None:
416+ tmpdir = self.tmpdir
417+ self.logger.info('Making temp copy of install image')
418+ tmpimage = os.path.join(tmpdir, os.path.basename(image))
419+ self.logger.debug('Copying ' + image + ' to ' + tmpimage)
420+ shutil.copyfile(image, tmpimage)
421+ return tmpimage
422+
423 def _create(self):
424 """
425 Create the VM, install the system, and prepare it to boot.
426@@ -669,8 +690,10 @@
427
428 self._createdisks()
429
430- xml = self._installxml(cmdline=self.cmdline, initrd=initrd,
431- kernel=kernel, tmpdir=tmpdir, xml=self.xml)
432+ image = self._tmpimage(image=self.image.image, tmpdir=tmpdir)
433+
434+ xml = self._installxml(cmdline=self.cmdline, image=image,
435+ initrd=initrd, kernel=kernel, tmpdir=tmpdir, xml=self.xml)
436
437 self._installvm(lv=self.lv, tmpdir=tmpdir, xml=xml)
438

Subscribers

People subscribed via source and target branches