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
=== modified file 'debian/control'
--- debian/control 2012-10-25 20:07:09 +0000
+++ debian/control 2012-11-08 20:12:18 +0000
@@ -17,7 +17,7 @@
17 python-apt, python-libvirt,17 python-apt, python-libvirt,
18 python-netifaces, python-paramiko, python-psutil,18 python-netifaces, python-paramiko, python-psutil,
19 utah-client (=${binary:Version})19 utah-client (=${binary:Version})
20Recommends: kvm20Recommends: dl-ubuntu-test-iso, kvm
21Suggests: cobbler, u-boot-tools, vm-tools21Suggests: cobbler, u-boot-tools, vm-tools
22Description: Ubuntu Test Automation Harness22Description: Ubuntu Test Automation Harness
23 Automation framework for testing in Ubuntu23 Automation framework for testing in Ubuntu
2424
=== modified file 'examples/run_utah_tests.py'
--- examples/run_utah_tests.py 2012-10-19 21:57:01 +0000
+++ examples/run_utah_tests.py 2012-11-08 20:12:18 +0000
@@ -7,7 +7,7 @@
7from utah.group import check_user_group, print_group_error_message7from utah.group import check_user_group, print_group_error_message
8from utah.timeout import timeout, UTAHTimeout8from utah.timeout import timeout, UTAHTimeout
9from utah import config9from utah import config
10from run_test_vm import run_test_vm10from run_install_test import run_install_test
1111
1212
13def get_parser():13def get_parser():
@@ -87,21 +87,16 @@
87 if args is None:87 if args is None:
88 args = get_parser().parse_args()88 args = get_parser().parse_args()
8989
90 # Default is now CustomVM
91 function = run_install_test
90 if args.arch is not None and 'arm' in args.arch:92 if args.arch is not None and 'arm' in args.arch:
93 # If arch is arm, use BambooFeederMachine
91 from run_test_bamboo_feeder import run_test_bamboo_feeder94 from run_test_bamboo_feeder import run_test_bamboo_feeder
92 function = run_test_bamboo_feeder95 function = run_test_bamboo_feeder
93 elif args.machinetype == 'physical':96 elif args.machinetype == 'physical':
97 # If machinetype is physical but arch isn't arm, use CobblerMachine
94 from run_test_cobbler import run_test_cobbler98 from run_test_cobbler import run_test_cobbler
95 function = run_test_cobbler99 function = run_test_cobbler
96 else:
97 function = run_test_vm
98
99 for option in [args.boot, args.emulator, args.image, args.initrd,
100 args.kernel, args.preseed, args.xml]:
101 if option is not None:
102 from run_install_test import run_install_test
103 function = run_install_test
104 break
105 100
106 function(args=args)101 function(args=args)
107102
108103
=== modified file 'utah/config.py'
--- utah/config.py 2012-11-08 16:27:38 +0000
+++ utah/config.py 2012-11-08 20:12:18 +0000
@@ -31,6 +31,10 @@
31 checktimeout=15,31 checktimeout=15,
32 consoleloglevel=logging.WARNING,32 consoleloglevel=logging.WARNING,
33 debug=False,33 debug=False,
34 # Command to download test images
35 dlcommand='dl-ubuntu-test-iso',
36 # Number of times to retry image download
37 dlretries=10,
34 fileloglevel=logging.INFO,38 fileloglevel=logging.INFO,
35 group='utah',39 group='utah',
36 image=None,40 image=None,
@@ -38,6 +42,8 @@
38 installpackages=['openssh-server', 'gdebi'],42 installpackages=['openssh-server', 'gdebi'],
39 installtimeout=3600,43 installtimeout=3600,
40 installtype='desktop',44 installtype='desktop',
45 # Directory to store local images
46 isodir='/var/cache/utah/iso',
41 kernel=None,47 kernel=None,
42 logpath=os.path.join('/', 'var', 'log', 'utah'),48 logpath=os.path.join('/', 'var', 'log', 'utah'),
43 machineid=None,49 machineid=None,
4450
=== modified file 'utah/iso.py'
--- utah/iso.py 2012-08-30 08:45:59 +0000
+++ utah/iso.py 2012-11-08 20:12:18 +0000
@@ -1,20 +1,24 @@
1"""1"""
2All commands use bsdtar and accept a logmethod to use for logging.2All commands use bsdtar and accept a logmethod to use for logging.
3"""3"""
4#TODO: Make an iso class that can return things like arch, series, type, etc.
5#TODO: Add the existing functions to the iso class.
6#TODO: Convert the code to use the class instead of the existing functions.
7import logging4import logging
8import logging.handlers5import logging.handlers
9import os6import os
7import shutil
10import subprocess8import subprocess
11import sys9import sys
12import urllib10import urllib
1311
12from hashlib import md5
13
14from utah import config14from utah import config
15from utah.exceptions import UTAHException15from utah.exceptions import UTAHException
1616
1717
18# TODO: eliminate usage of the non-class functions
19# TODO: move the non-class functions into the class
20
21
18def listfiles(image, logmethod=None, returnlist=False):22def listfiles(image, logmethod=None, returnlist=False):
19 """23 """
20 Return either a subprocess instance listing the contents of an iso, or a24 Return either a subprocess instance listing the contents of an iso, or a
@@ -115,18 +119,28 @@
115 """119 """
116 Provide a simplified method of interfacing with images.120 Provide a simplified method of interfacing with images.
117 """121 """
118 def __init__(self, image, dlpercentincrement=1, logger=None):122 def __init__(self, arch=None, dlpercentincrement=1, dlretries=None,
123 image=None, installtype=None, logger=None, series=None):
119 self.dlpercentincrement = dlpercentincrement124 self.dlpercentincrement = dlpercentincrement
125 if dlretries is None:
126 self.dlretries = config.dlretries
127 else:
128 self.dlretries = dlretries
120 if logger is None:129 if logger is None:
121 self._loggersetup()130 self._loggersetup()
122 else:131 else:
123 self.logger = logger132 self.logger = logger
124 if image.startswith('~'):133 if image is None:
125 path = os.path.expanduser(image)134 self.logger.debug('Need to download image')
126 self.logger.debug('Rewriting ~-based path: ' + image +135 path = self.downloadiso(arch=arch, installtype=installtype,
127 ' to: ' + path)136 series=series)
128 else:137 else:
129 path = image138 if image.startswith('~'):
139 path = os.path.expanduser(image)
140 self.logger.debug('Rewriting ~-based path: ' + image +
141 ' to: ' + path)
142 else:
143 path = image
130144
131 self.percent = 0145 self.percent = 0
132 self.logger.info('Preparing image: ' + path)146 self.logger.info('Preparing image: ' + path)
@@ -218,6 +232,9 @@
218 return series232 return series
219233
220 def dldisplay(self, blocks, size, total):234 def dldisplay(self, blocks, size, total):
235 """
236 Log download information.
237 """
221 read = blocks * size238 read = blocks * size
222 percent = 100 * read / total239 percent = 100 * read / total
223 if percent >= self.percent:240 if percent >= self.percent:
@@ -226,19 +243,140 @@
226 self.logger.debug("%s read, %s%% of %s total" % (read, percent, total))243 self.logger.debug("%s read, %s%% of %s total" % (read, percent, total))
227244
228 def listfiles(self, returnlist=False):245 def listfiles(self, returnlist=False):
246 """
247 List all files in the image.
248 """
229 return listfiles(self.image, self.logger.debug, returnlist=returnlist)249 return listfiles(self.image, self.logger.debug, returnlist=returnlist)
230250
231 def getrealfile(self, filename, outdir=None):251 def getrealfile(self, filename, outdir=None):
252 """
253 Return the real path to a file (following links if needed.)
254 """
232 return getrealfile(self.image, filename, outdir=outdir,255 return getrealfile(self.image, filename, outdir=outdir,
233 logmethod=self.logger.debug)256 logmethod=self.logger.debug)
234257
235 def extract(self, filename, outdir=None, outfile=None, **kw):258 def extract(self, filename, outdir=None, outfile=None, **kw):
259 """
260 Extract a file.
261 """
236 return extract(self.image, filename, outdir, outfile,262 return extract(self.image, filename, outdir, outfile,
237 logmethod=self.logger.debug, **kw)263 logmethod=self.logger.debug, **kw)
238264
239 def dump(self, filename, **kw):265 def dump(self, filename, **kw):
266 """
267 Dump a file.
268 """
240 return dump(self.image, filename, logmethod=self.logger.debug, **kw)269 return dump(self.image, filename, logmethod=self.logger.debug, **kw)
241270
242 def extractall(self):271 def extractall(self):
272 """
273 Extract all files in the image.
274 """
243 for myfile in self.listfiles(returnlist=True):275 for myfile in self.listfiles(returnlist=True):
244 self.extract(myfile)276 self.extract(myfile)
277
278 def getmd5(self, path):
279 """
280 Return the md5 checksum of a file.
281 """
282 if path is None:
283 path = self.image
284 self.logger.debug('Getting md5 of ' + path)
285 isohash = md5()
286 with open(path) as myfile:
287 for block in iter(lambda: myfile.read(128), ""):
288 isohash.update(block)
289 filemd5 = isohash.hexdigest()
290 self.logger.debug('md5 of ' + path + ' is ' + filemd5)
291 return filemd5
292
293 def downloadiso(self, arch=None, installtype=None, series=None):
294 """
295 Download an ISO given series, type, and arch.
296 """
297 if arch is None:
298 arch = config.arch
299 if installtype is None:
300 installtype = config.installtype
301 if series is None:
302 series = config.series
303 filename = '-'.join([series, installtype, arch]) + '.iso'
304 self.logger.info('Attempting to download ' + filename)
305 with open(os.devnull, "w") as fnull:
306 # If dlcommand (default dl-ubuntu-test-iso) is available, use it
307 if subprocess.call(['which', config.dlcommand],
308 stdout=fnull, stderr=fnull) == 0:
309 self.logger.debug('Using ' + config.dlcommand)
310 if installtype == 'server':
311 flavor = 'ubuntu-server'
312 else:
313 flavor = 'ubuntu'
314 cmd = [config.dlcommand,
315 '-q',
316 '--flavor=' + flavor,
317 '--release=' + series,
318 '--variant=' + installtype,
319 '--arch=' + arch,
320 '--isoroot=' + config.isodir]
321 self.logger.info('Downloading ISO')
322 self.logger.debug(' '.join(cmd))
323 subprocess.check_call(cmd)
324 path = os.path.join(config.isodir, flavor, filename)
325 if os.path.isfile(path):
326 return path
327
328 # If we haven't returned, dlcommand didn't give us an image
329 # We'll need to use the utah image cache in config.isodir
330 path = os.path.join(config.isodir, filename)
331 # We want to verify our image matches the latest md5 from the server
332 # To do this, we'll loop
333 # First, we'll check the image. If it matches, we return
334 # If not, we'll download it, and start the loop over
335 for attempt in range(config.dlretries):
336 self.logger.info('Download attempt ' + str(attempt))
337 # Set the path to our files and the name of the iso we want
338 if installtype == 'mini':
339 remotepath = ('http://archive.ubuntu.com/ubuntu/dists/'
340 '{series}/main/installer-{arch}/current/'
341 'images/'.format(arch=arch, series=series))
342 isopattern = 'netboot/mini.iso'
343 else:
344 remotepath = ('http://releases.ubuntu.com/'
345 '{series}/'.format(series=series))
346 isopattern = installtype + '-' + arch + '.iso'
347
348 # Scan the MD5SUMS file for our file
349 servermd5 = None
350 for line in urllib.urlopen(remotepath + '/MD5SUMS'):
351 if isopattern in line:
352 servermd5, isofile = line.split()
353 isofile = isofile.strip('*')
354 break
355
356 # If we can't find the ISO, raise an exception
357 if servermd5 is None:
358 raise UTAHException('Specified ISO: ' + filename
359 + ' not found on mirrors.')
360 self.logger.debug('Server md5 is ' + servermd5)
361
362 # If the ISO exists locally and the md5 matches, use it
363 if os.path.isfile(path) and servermd5 == self.getmd5(path):
364 self.logger.info('Using ISO at ' + path)
365 return path
366
367 # Try to download the ISO
368 isopath = os.path.join(remotepath, isofile)
369 self.percent = 0
370 self.logger.info('Attempting to download ' + isopath)
371 temppath = urllib.urlretrieve(isopath, reporthook=self.dldisplay)[0]
372
373 # Try to copy it into our cache
374 self.logger.debug('Copying ' + temppath + ' to ' + path)
375 shutil.copyfile(temppath, path)
376 else:
377 if os.path.isfile(path) and servermd5 == self.getmd5(path):
378 return path
379 else:
380 raise UTAHException('Image failed to download after ' +
381 config.dlretries + ' tries')
382
245383
=== modified file 'utah/provisioning/inventory/sqlite.py'
--- utah/provisioning/inventory/sqlite.py 2012-10-19 13:04:40 +0000
+++ utah/provisioning/inventory/sqlite.py 2012-11-08 20:12:18 +0000
@@ -5,7 +5,7 @@
5from utah.provisioning.inventory.exceptions import \5from utah.provisioning.inventory.exceptions import \
6 UTAHProvisioningInventoryException6 UTAHProvisioningInventoryException
7from utah.provisioning.inventory.inventory import Inventory7from utah.provisioning.inventory.inventory import Inventory
8from utah.provisioning.vm.libvirtvm import VMToolsVM8from utah.provisioning.vm.libvirtvm import CustomVM
9from utah.provisioning.baremetal.cobbler import CobblerMachine9from utah.provisioning.baremetal.cobbler import CobblerMachine
1010
1111
@@ -42,7 +42,7 @@
42 'CREATE TABLE IF NOT EXISTS '42 'CREATE TABLE IF NOT EXISTS '
43 'machines(machineid INTEGER PRIMARY KEY, state TEXT)')43 'machines(machineid INTEGER PRIMARY KEY, state TEXT)')
4444
45 def request(self, machinetype=VMToolsVM, *args, **kw):45 def request(self, machinetype=CustomVM, *args, **kw):
46 """46 """
47 Takes a Machine class as machinetype, and passes the newly generated47 Takes a Machine class as machinetype, and passes the newly generated
48 machineid along with all other arguments to that class's constructor,48 machineid along with all other arguments to that class's constructor,
4949
=== modified file 'utah/provisioning/provisioning.py'
--- utah/provisioning/provisioning.py 2012-10-25 20:07:44 +0000
+++ utah/provisioning/provisioning.py 2012-11-08 20:12:18 +0000
@@ -137,12 +137,11 @@
137137
138 fileargs = ['preseed', 'xml', 'kernel', 'initrd']138 fileargs = ['preseed', 'xml', 'kernel', 'initrd']
139139
140 if image is None:140 if image is None or image.endswith('.iso'):
141 self.image = None141 self.image = ISO(arch=arch,
142 elif image.endswith('.iso'):
143 self.image = ISO(image,
144 dlpercentincrement=self.dlpercentincrement,142 dlpercentincrement=self.dlpercentincrement,
145 logger=self.logger)143 image=image, installtype=installtype, logger=self.logger,
144 series=series)
146 else:145 else:
147 fileargs.append('image')146 fileargs.append('image')
148147
149148
=== modified file 'utah/provisioning/vm/libvirtvm.py'
--- utah/provisioning/vm/libvirtvm.py 2012-10-19 21:57:01 +0000
+++ utah/provisioning/vm/libvirtvm.py 2012-11-08 20:12:18 +0000
@@ -454,7 +454,7 @@
454 return True454 return True
455 return False455 return False
456456
457 def _installxml(self, cmdline=None, initrd=None,457 def _installxml(self, cmdline=None, image=None, initrd=None,
458 kernel=None, tmpdir=None, xml=None):458 kernel=None, tmpdir=None, xml=None):
459 """459 """
460 Return the XML tree to be passed to libvirt for VM installation.460 Return the XML tree to be passed to libvirt for VM installation.
@@ -462,6 +462,8 @@
462 self.logger.info('Creating installation XML')462 self.logger.info('Creating installation XML')
463 if cmdline is None:463 if cmdline is None:
464 cmdline = self.cmdline464 cmdline = self.cmdline
465 if image is None:
466 image = self.image.image
465 if initrd is None:467 if initrd is None:
466 initrd = self.initrd468 initrd = self.initrd
467 if kernel is None:469 if kernel is None:
@@ -475,7 +477,8 @@
475 self.logger.debug('Setting VM to shutdown on reboot')477 self.logger.debug('Setting VM to shutdown on reboot')
476 xmlt.find('on_reboot').text = 'destroy'478 xmlt.find('on_reboot').text = 'destroy'
477 if self.rewrite == 'all':479 if self.rewrite == 'all':
478 self._installxml_rewrite_all(cmdline, initrd, kernel, xmlt)480 self._installxml_rewrite_all(cmdline, image, initrd, kernel,
481 xmlt)
479 else:482 else:
480 self.logger.info('Not rewriting XML because rewrite is ' +483 self.logger.info('Not rewriting XML because rewrite is ' +
481 self.rewrite)484 self.rewrite)
@@ -484,8 +487,8 @@
484 self.logger.info('Installation XML ready')487 self.logger.info('Installation XML ready')
485 return xmlt488 return xmlt
486489
487 def _installxml_rewrite_all(self, cmdline_txt, initrd_txt, kernel_txt,490 def _installxml_rewrite_all(self, cmdline_txt, image, initrd_txt,
488 xmlt):491 kernel_txt, xmlt):
489 """492 """
490 Rewrite the whole configuration file for the VM493 Rewrite the whole configuration file for the VM
491 """494 """
@@ -528,11 +531,11 @@
528 #TODO: Add a cdrom if none exists531 #TODO: Add a cdrom if none exists
529 if disk.get('device') == 'cdrom':532 if disk.get('device') == 'cdrom':
530 if disk.find('source') is not None:533 if disk.find('source') is not None:
531 disk.find('source').set('file', self.image.image)534 disk.find('source').set('file', image)
532 self.logger.debug('Rewrote existing CD-ROM')535 self.logger.debug('Rewrote existing CD-ROM')
533 else:536 else:
534 source = ElementTree.Element('source')537 source = ElementTree.Element('source')
535 source.set('file', self.image.image)538 source.set('file', image)
536 disk.append(source)539 disk.append(source)
537 self.logger.debug('Added source to existing '540 self.logger.debug('Added source to existing '
538 'CD-ROM')541 'CD-ROM')
@@ -631,6 +634,9 @@
631 ose.remove(cmdline)634 ose.remove(cmdline)
632 devices = xml.find('devices')635 devices = xml.find('devices')
633 devices.remove(devices.find('serial'))636 devices.remove(devices.find('serial'))
637 for disk in list(devices.iterfind('disk')):
638 if disk.get('device') == 'cdrom':
639 disk.remove(disk.find('source'))
634 else:640 else:
635 self.logger.info('Not rewriting XML because rewrite is ' +641 self.logger.info('Not rewriting XML because rewrite is ' +
636 self.rewrite)642 self.rewrite)
@@ -638,6 +644,21 @@
638 xml.write(os.path.join(tmpdir, 'final.xml'))644 xml.write(os.path.join(tmpdir, 'final.xml'))
639 return xml645 return xml
640646
647 def _tmpimage(self, image=None, tmpdir=None):
648 """
649 Create a temporary copy of the image so libvirt will lock that copy.
650 This allows other simultaneous processes to update the cached image.
651 """
652 if image is None:
653 image = self.image.image
654 if tmpdir is None:
655 tmpdir = self.tmpdir
656 self.logger.info('Making temp copy of install image')
657 tmpimage = os.path.join(tmpdir, os.path.basename(image))
658 self.logger.debug('Copying ' + image + ' to ' + tmpimage)
659 shutil.copyfile(image, tmpimage)
660 return tmpimage
661
641 def _create(self):662 def _create(self):
642 """663 """
643 Create the VM, install the system, and prepare it to boot.664 Create the VM, install the system, and prepare it to boot.
@@ -669,8 +690,10 @@
669690
670 self._createdisks()691 self._createdisks()
671692
672 xml = self._installxml(cmdline=self.cmdline, initrd=initrd,693 image = self._tmpimage(image=self.image.image, tmpdir=tmpdir)
673 kernel=kernel, tmpdir=tmpdir, xml=self.xml)694
695 xml = self._installxml(cmdline=self.cmdline, image=image,
696 initrd=initrd, kernel=kernel, tmpdir=tmpdir, xml=self.xml)
674697
675 self._installvm(lv=self.lv, tmpdir=tmpdir, xml=xml)698 self._installvm(lv=self.lv, tmpdir=tmpdir, xml=xml)
676699

Subscribers

People subscribed via source and target branches