Merge lp:~nuclearbob/utah/iso-download into lp:utah
- iso-download
- Merge into dev
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Joe Talbott (community) | Approve | ||
Review via email:
|
Commit message
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
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 |
Looks good to me.