Merge lp:~javier.collado/utah/trunk-0.8 into lp:~nuclearbob/utah/trunk

Proposed by Javier Collado
Status: Merged
Merged at revision: 267
Proposed branch: lp:~javier.collado/utah/trunk-0.8
Merge into: lp:~nuclearbob/utah/trunk
Diff against target: 826 lines (+401/-52)
14 files modified
debian/changelog (+17/-0)
examples/run_test_vm.py (+5/-1)
examples/run_utah_tests.py (+27/-1)
utah/client/battery.py (+83/-0)
utah/client/common.py (+15/-1)
utah/client/result.py (+9/-0)
utah/client/runner.py (+25/-2)
utah/provisioning/baremetal/cobbler.py (+12/-7)
utah/provisioning/provisioning.py (+32/-10)
utah/provisioning/ssh.py (+73/-12)
utah/retry.py (+20/-3)
utah/run.py (+24/-1)
utah/timeout.py (+38/-13)
utah/url.py (+21/-1)
To merge this branch: bzr merge lp:~javier.collado/utah/trunk-0.8
Reviewer Review Type Date Requested Status
Javier Collado (community) Approve
Max Brustkern Pending
Review via email: mp+148774@code.launchpad.net

Description of the change

This branch contains all the changes for version 0.8.

To post a comment you must log in.
Revision history for this message
Javier Collado (javier.collado) wrote :

I see the changes merged, so this request can be approved.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/changelog'
2--- debian/changelog 2013-01-29 19:18:51 +0000
3+++ debian/changelog 2013-02-15 17:42:27 +0000
4@@ -1,3 +1,20 @@
5+utah (0.8ubuntu1) quantal; urgency=low
6+
7+ [Max Brustkern]
8+ * Raise exception on mount ISO error (LP: #1110550, LP: #1123899)
9+
10+ [Javier Collado]
11+ * SIGTERM signal captured to ensure cleanup happens (LP: #1100550, LP: #1123899)
12+ * Warning messages contain command on failure return code (LP: #1123896)
13+ * Battery information added to test report if available
14+ * /etc/rc.local truncation problems fixed (LP: #1118581)
15+ * Ability to execute runlists in an already provisioned system
16+
17+ [Joe Talbott]
18+ * Fixed client behavior on `bzr revno` failure (LP: #1110310, LP: #1123303)
19+
20+ -- Javier Collado <javier.collado@canonical.com> Fri, 15 Feb 2013 18:32:06 +0100
21+
22 utah (0.7ubuntu1) precise; urgency=low
23
24 * Updating to version 0.7
25
26=== modified file 'examples/run_test_vm.py'
27--- examples/run_test_vm.py 2013-01-23 12:26:38 +0000
28+++ examples/run_test_vm.py 2013-02-15 17:42:27 +0000
29@@ -21,7 +21,11 @@
30 from utah.exceptions import UTAHException
31 from utah.group import check_user_group, print_group_error_message
32 from utah.provisioning.vm.vm import TinySQLiteInventory
33-from utah.run import common_arguments, run_tests, configure_logging
34+from utah.run import (
35+ common_arguments,
36+ run_tests,
37+ configure_logging,
38+)
39
40
41 def get_parser():
42
43=== modified file 'examples/run_utah_tests.py'
44--- examples/run_utah_tests.py 2013-01-23 12:26:38 +0000
45+++ examples/run_utah_tests.py 2013-02-15 17:42:27 +0000
46@@ -26,10 +26,13 @@
47 file_arguments,
48 name_argument,
49 virtual_arguments,
50- configure_logging
51+ configure_logging,
52+ run_tests,
53 )
54 from utah.timeout import timeout, UTAHTimeout
55 from run_install_test import run_install_test
56+from utah.provisioning.ssh import ProvisionedMachine
57+from utah.exceptions import UTAHException
58
59
60 def get_parser():
61@@ -50,6 +53,9 @@
62 help='Type of machine to provision (%(choices)s)')
63 parser.add_argument('-v', '--variant',
64 help='Variant of architecture, i.e., armel, armhf')
65+ parser.add_argument('--skip-provisioning', action='store_true',
66+ help=('Reuse a system that is already provisioned '
67+ '(name argument must be passed)'))
68 parser = common_arguments(parser)
69 parser = custom_arguments(parser)
70 parser = file_arguments(parser)
71@@ -76,6 +82,26 @@
72 # Default is now CustomVM
73 function = run_install_test
74
75+ if args.skip_provisioning:
76+ def run_provisioned_tests(args):
77+ """Run test cases in a provisioned machine."""
78+ locallogs = []
79+ exitstatus = 0
80+ try:
81+ # TBD: Inventory should be used to verify machine
82+ # is not running other tests
83+ machine = ProvisionedMachine(name=args.name)
84+ exitstatus, locallogs = run_tests(args, machine)
85+ except UTAHException as error:
86+ sys.stderr.write('Exception: ' + str(error))
87+ exitstatus = 2
88+ finally:
89+ if len(locallogs) != 0:
90+ print('Test logs copied to the following files:')
91+ print("\t" + "\n\t".join(locallogs))
92+ sys.exit(exitstatus)
93+
94+ function = run_provisioned_tests
95 if args.arch is not None and 'arm' in args.arch:
96 # If arch is arm, use BambooFeederMachine
97 from run_test_bamboo_feeder import run_test_bamboo_feeder
98
99=== added file 'utah/client/battery.py'
100--- utah/client/battery.py 1970-01-01 00:00:00 +0000
101+++ utah/client/battery.py 2013-02-15 17:42:27 +0000
102@@ -0,0 +1,83 @@
103+"""Helpers for battery measurements."""
104+import os
105+import re
106+import logging
107+
108+
109+class _Battery(object):
110+ """Battery information gathering."""
111+ POWER_SUPPLY_DIR = '/sys/class/power_supply'
112+ BATTERY_DIR_REGEX = re.compile(r'bat.*', re.IGNORECASE)
113+ BATTERY_FILE_REGEX = re.compile(r'(.*_now|capacity|status)')
114+
115+ def __init__(self):
116+ self.filenames = self._get_files()
117+
118+ def get_data(self):
119+ """Get data from the battery information files
120+
121+ Every file is read and the contents of the file is written to a
122+ dictionary using as key the basename of the file.
123+
124+ :returns: Data as read from the battery information files
125+ :rtype: dict
126+ """
127+ def read_file(filename):
128+ """Read contents of a file and try to convert data to integer
129+
130+ :param filename: Path to file to be read
131+ :type filename: str
132+ :returns: File contents converted to integer if possible
133+ :rtype: str | int
134+ """
135+ with open(filename, 'r') as f:
136+ data_str = f.read().strip()
137+ try:
138+ data_int = int(data_str)
139+ return data_int
140+ except ValueError:
141+ return data_str
142+
143+ return {os.path.basename(filename): read_file(filename)
144+ for filename in self.filenames}
145+
146+ def _get_files(self):
147+ """Figure out which files to look at.
148+
149+ :returns: Paths to files with battery information.
150+ :rtype: list(str)
151+
152+ """
153+ if not os.path.isdir(self.POWER_SUPPLY_DIR):
154+ logging.warning('Power supply directory not found: {!r}'
155+ .format(self.POWER_SUPPLY_DIR))
156+ return []
157+
158+ entries = [os.path.join(self.POWER_SUPPLY_DIR, entry)
159+ for entry in os.listdir(self.POWER_SUPPLY_DIR)
160+ if self.BATTERY_DIR_REGEX.match(entry)]
161+ subdirectories = [entry for entry in entries
162+ if os.path.isdir(entry)]
163+
164+ if not subdirectories:
165+ logging.warning('Battery information directory not found')
166+ return []
167+
168+ if len(subdirectories) > 1:
169+ logging.error('Multiple battery information directories found: '
170+ '{}'.format(', '.join(subdirectories)))
171+ return []
172+
173+ subdirectory = subdirectories[0]
174+ entries = [os.path.join(subdirectory, filename)
175+ for filename in os.listdir(subdirectory)
176+ if self.BATTERY_FILE_REGEX.match(filename)]
177+ filenames = [filename for filename in entries
178+ if os.path.isfile(filename)]
179+
180+ logging.debug('Battery information files found: {}'
181+ .format(', '.join(filenames)))
182+ return filenames
183+
184+# Use singleton object to avoid multiple unneeded initializations
185+battery = _Battery()
186
187=== modified file 'utah/client/common.py'
188--- utah/client/common.py 2012-12-10 14:07:05 +0000
189+++ utah/client/common.py 2013-02-15 17:42:27 +0000
190@@ -35,6 +35,8 @@
191 YAMLEmptyFile,
192 )
193
194+from utah.client.battery import battery
195+
196
197 PASS = 0
198 FAIL = 1
199@@ -101,6 +103,7 @@
200 def alarm_handler(_signum, _frame):
201 raise TimeoutAlarm
202
203+ start_battery = battery.get_data()
204 start_time = datetime.datetime.now()
205 p = subprocess.Popen("{} {}".format(cmd_prefix, command),
206 shell=True, cwd=cwd,
207@@ -141,6 +144,7 @@
208 )
209
210 time_delta = datetime.datetime.now() - start_time
211+ end_battery = battery.get_data()
212
213 return make_result(command=command,
214 retcode=p.returncode,
215@@ -150,6 +154,8 @@
216 time_delta=str(time_delta),
217 cmd_type=cmd_type,
218 user=run_as,
219+ start_battery=start_battery,
220+ end_battery=end_battery,
221 )
222
223
224@@ -172,7 +178,8 @@
225 # TODO: it might make sense to have a result object that keeps track of
226 # test pass, fail, error and serializes it's data.
227 def make_result(command, retcode, stdout='', stderr='', start_time='',
228- time_delta='', cmd_type=CMD_TC_TEST, user="unknown"):
229+ time_delta='', cmd_type=CMD_TC_TEST, user="unknown",
230+ start_battery=None, end_battery=None):
231 """
232 Make a result dictionary.
233 """
234@@ -187,6 +194,13 @@
235 'user': user,
236 }
237
238+ if start_battery or end_battery:
239+ res['battery'] = {}
240+ if start_battery:
241+ res['battery']['start'] = start_battery
242+ if end_battery:
243+ res['battery']['end'] = end_battery
244+
245 return res
246
247
248
249=== modified file 'utah/client/result.py'
250--- utah/client/result.py 2012-12-08 02:10:12 +0000
251+++ utah/client/result.py 2013-02-15 17:42:27 +0000
252@@ -71,6 +71,8 @@
253 self.publish_type = publish_type
254 self.publish = None
255 self.install_type = install_type
256+ self.start_battery = None
257+ self.end_battery = None
258
259 self.errors = 0
260 self.fetch_errors = 0
261@@ -207,6 +209,13 @@
262 if self.publish is not None:
263 data['publish'] = self.publish
264
265+ if self.start_battery or self.end_battery:
266+ data['battery'] = {}
267+ if self.start_battery:
268+ data['battery']['start'] = self.start_battery
269+ if self.end_battery:
270+ data['battery']['end'] = self.end_battery
271+
272 return data
273
274
275
276=== modified file 'utah/client/runner.py'
277--- utah/client/runner.py 2012-12-12 11:21:10 +0000
278+++ utah/client/runner.py 2013-02-15 17:42:27 +0000
279@@ -18,12 +18,17 @@
280 from utah.client.testsuite import TestSuite
281 from utah.client.state_agent import StateAgentYAML
282 from utah.client import exceptions
283+from utah.exceptions import UTAHException
284+from utah.timeout import timeout
285+from utah.retry import retry
286+from utah.client.battery import battery
287
288 import os
289 import shutil
290 import stat
291 import urllib
292 import jsonschema
293+import time
294
295 from utah.client.common import (
296 MASTER_RUNLIST,
297@@ -251,6 +256,7 @@
298 """
299 Run the test suites we've parsed.
300 """
301+ self.result.start_battery = battery.get_data()
302
303 # Return value to indicate whether processing of a Runner should
304 # continue. This is to avoid a shutdown race on reboot cases.
305@@ -273,6 +279,7 @@
306 self.status = "DONE"
307 self.save_state()
308
309+ self.result.end_battery = battery.get_data()
310 return self.process_results()
311
312 def add_suite(self, suite):
313@@ -443,9 +450,20 @@
314 if not os.path.exists(name):
315 os.mkdir(name)
316
317+ def vcs_get_retriable():
318+ result = vcs_handler.get(directory=name)
319+ if (isinstance(vcs_handler, BzrHandler) and
320+ result['returncode'] == 3 and
321+ 'Unable to handle http code 502' in result['stderr']):
322+ # Separate every retry attempt with a timeout of 3 seconds
323+ time.sleep(3)
324+ raise UTAHException('Launchpad temporary error detected',
325+ retry=True)
326+ return result
327+
328 if name not in self.fetched_suites:
329-
330- res = vcs_handler.get(directory=name)
331+ # Retry bzr export on http errors for 60 seconds
332+ res = timeout(60, retry, vcs_get_retriable)
333 self.result.add_result(res)
334
335 # If fetch failed move on to the next testsuite.
336@@ -457,6 +475,11 @@
337 res = vcs_handler.revision(directory=rev_dir)
338 self.result.add_result(res)
339
340+ # If we're unable to get the revision number indicate a
341+ # fetch failure but allow the test runs to proceed.
342+ if self.result.status != 'PASS':
343+ self.result.status = 'PASS'
344+
345 self.fetched_suites.append(name)
346
347 # Create a TestSuite
348
349=== modified file 'utah/provisioning/baremetal/cobbler.py'
350--- utah/provisioning/baremetal/cobbler.py 2013-01-29 16:43:59 +0000
351+++ utah/provisioning/baremetal/cobbler.py 2013-02-15 17:42:27 +0000
352@@ -20,7 +20,6 @@
353 import netifaces
354 import os
355 import pipes
356-import shutil
357 import subprocess
358 import tempfile
359 import time
360@@ -107,8 +106,14 @@
361 self.cleanfunction(self._removenfs)
362 os.makedirs(self.isodir, mode=0775)
363 self.logger.debug('Mounting ISO')
364- self._runargs(['sudo', 'mount', '-o', 'loop', self.image.image,
365- self.isodir])
366+ result = self._runargs(['sudo', 'mount', '-o', 'loop',
367+ self.image.image,
368+ self.isodir])
369+ returncode = result['returncode']
370+ if returncode != 0:
371+ raise UTAHBMProvisioningException(
372+ 'Mount failed with return code: {} Output: {}'
373+ .format(returncode, result['output']))
374
375 kernel = self._preparekernel()
376 if self.cinitrd is None:
377@@ -282,14 +287,14 @@
378
379 def _depcheck(self):
380 """
381- Check for NFS if installtype is desktop.
382+ Check for NFS if installtype requires it.
383 """
384 super(CobblerMachine, self)._depcheck()
385- if self.installtype == 'desktop':
386+ if self.installtype in ['alternate', 'desktop', 'server']:
387 cmd = config.nfscommand + ['status']
388 if self._runargs(cmd)['returncode'] != 0:
389- raise UTAHBMProvisioningException('NFS needed for desktop'
390- ' install')
391+ raise UTAHBMProvisioningException(
392+ 'NFS needed for {} install'.format(self.installtype))
393 if not os.path.isfile(config.nfsconfigfile):
394 raise UTAHBMProvisioningException(
395 'NFS config file: {nfsconfigfile} not available'
396
397=== modified file 'utah/provisioning/provisioning.py'
398--- utah/provisioning/provisioning.py 2013-01-24 16:31:58 +0000
399+++ utah/provisioning/provisioning.py 2013-02-15 17:42:27 +0000
400@@ -302,15 +302,29 @@
401 self._start()
402
403 def pingcheck(self, timeout=config.checktimeout):
404- """Check network connectivity using ping."""
405- self.logger.info('Sleeping {timeout} seconds'
406- .format(timeout=timeout))
407- time.sleep(timeout)
408+ """Check network connectivity using ping.
409+
410+ :param timeout: Amount of time in seconds to sleep after a failure
411+ :type timeout: int
412+ :raises: UTAHProvisioningException
413+
414+ If there's a network connectivity failure, then sleep ``timeout``
415+ seconds and raise a retriable exception.
416+
417+ .. seealso:: :func:`utah.retry.retry`, :meth:`pingpoll`
418+
419+ """
420 self.logger.info('Checking network connectivity (ping)')
421 returncode = \
422 self._runargs(['ping', '-c1', '-w5', self.name])['returncode']
423 if returncode != 0:
424 err = 'Ping returned {0}'.format(returncode)
425+
426+ if timeout > 0:
427+ self.logger.info('Sleeping {timeout} seconds'
428+ .format(timeout=timeout))
429+ time.sleep(timeout)
430+
431 raise UTAHProvisioningException(err, retry=True)
432
433 def pingpoll(self,
434@@ -551,9 +565,11 @@
435 output = '\n'.join(output)
436
437 returncode = p.returncode
438- log_level = logging.DEBUG if returncode == 0 else logging.WARNING
439- log_message = 'Return code: {}'.format(returncode)
440- self.logger.log(log_level, log_message)
441+ if returncode == 0:
442+ self.logger.debug('Return code: {}'.format(returncode))
443+ else:
444+ self.logger.warning('Command ({}) failed with return code: {}'
445+ .format(' '.join(arglist), returncode))
446
447 return {'returncode': returncode,
448 'output': output}
449@@ -854,12 +870,18 @@
450 .format(self.CHECK_LOCK_COMMAND, pkg_name, log_file)
451 for pkg_name in config.installpackages]))
452 postinstall_commands = (
453- 'sed -i -e "/^exit 0$/i{}" /etc/rc.local;'
454+ 'sed -i -e \'/^exit 0$/i{}\' /etc/rc.local;'
455 .format(install_commands)
456 )
457 chroot_command = ' '.join([install_commands, postinstall_commands])
458- question.value.prepend('in-target sh -c \'{}\'; '
459- .format(chroot_command))
460+ # Note that since the chroot command is passed to sh within single
461+ # quotes, the single quotes inside the chroot command have to be
462+ # escaped using '"'"'
463+ # For more information, please refer to:
464+ # http://stackoverflow.com/q/1250079/183066
465+ question.value.prepend(
466+ 'in-target sh -c \'{}\'; '
467+ .format(chroot_command.replace('\'', '\'"\'"\'')))
468 question.prepend("ubiquity ubiquity/summary note")
469 question.prepend("ubiquity ubiquity/reboot boolean true")
470 else:
471
472=== modified file 'utah/provisioning/ssh.py'
473--- utah/provisioning/ssh.py 2013-01-23 09:10:16 +0000
474+++ utah/provisioning/ssh.py 2013-02-15 17:42:27 +0000
475@@ -14,7 +14,8 @@
476 # with this program. If not, see <http://www.gnu.org/licenses/>.
477
478 """
479-Provide a mixin class for machines with SSH support.
480+SSH based machine class for a provisioned system
481+and SSHMixin for every machine class that needs SSH support.
482 """
483
484 import logging
485@@ -29,6 +30,7 @@
486
487 from utah import config
488 from utah.provisioning.exceptions import UTAHProvisioningException
489+from utah.provisioning.provisioning import Machine
490 from utah.retry import retry
491
492
493@@ -40,6 +42,15 @@
494 # Note: Since this is a mixin it doesn't expect any argument
495 # However, it calls super to initialize any other mixins in the mro
496 super(SSHMixin, self).__init__(*args, **kwargs)
497+ self.initialize()
498+
499+ def initialize(self):
500+ """SSH mixin initialization
501+
502+ Use this method when it isn't appropriate to follow the MRO as in
503+ __init__
504+
505+ """
506 ssh_client = paramiko.SSHClient()
507 ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
508 self.ssh_client = ssh_client
509@@ -85,9 +96,12 @@
510 self.ssh_logger.debug('Closing SSH connection')
511 self.ssh_client.close()
512
513- log_level = logging.DEBUG if retval == 0 else logging.WARNING
514- log_message = 'Return code: {}'.format(retval)
515- self.ssh_logger.log(log_level, log_message)
516+ if retval == 0:
517+ self.ssh_logger.debug('Return code: {}'.format(retval))
518+ else:
519+ self.ssh_logger.warning(
520+ 'SSH command ({}) failed with return code: {}'
521+ .format(commandstring, retval))
522
523 self.ssh_logger.debug('Standard output follows:')
524 stdout_lines = stdout.readlines()
525@@ -223,20 +237,29 @@
526 super(SSHMixin, self).destroy(*args, **kw)
527
528 def sshcheck(self, timeout=config.checktimeout):
529- """
530- Sleep for a while and check if the machine is available via ssh.
531- Return a retryable exception if it is not.
532- Intended for use with retry.
533- """
534- self.ssh_logger.info('Sleeping {timeout} seconds'
535- .format(timeout=timeout))
536- time.sleep(timeout)
537+ """Check if the machine is available via ssh.
538+
539+ :param timeout: Amount of time in seconds to sleep after a failure
540+ :type timeout: int
541+ :raises: UTAHProvisioningException
542+
543+ If there's a network connectivity failure, then sleep ``timeout``
544+ seconds and raise a retriable exception.
545+
546+ .. seealso:: :func:`utah.retry.retry`, :meth:`sshpoll`
547+
548+ """
549 self.ssh_logger.info('Checking for ssh availability')
550 try:
551 self.ssh_client.connect(self.name,
552 username=config.user,
553 key_filename=config.sshprivatekey)
554 except socket.error as err:
555+ if timeout > 0:
556+ self.ssh_logger.info('Sleeping {timeout} seconds'
557+ .format(timeout=timeout))
558+ time.sleep(timeout)
559+
560 raise UTAHProvisioningException(str(err), retry=True)
561
562 def sshpoll(self, timeout=None,
563@@ -260,3 +283,41 @@
564 if not self.active:
565 self._start()
566 self.sshcheck()
567+
568+
569+class ProvisionedMachine(SSHMixin, Machine):
570+ """A machine that is provisioned and can be accessed through ssh."""
571+ def __init__(self, name, installtype=None):
572+ SSHMixin.initialize(self)
573+ self.name = name
574+ self._loggersetup()
575+
576+ # No cleanup needed for systems that are already provisioned
577+ self.clean = False
578+
579+ # System is expected to be available already, so there's no need to
580+ # wait before trying to connect through ssh
581+ self.check_timeout = 3
582+ self.connectivity_timeout = 60
583+
584+ # TBD: Figure out install type by getting information through ssh
585+ if installtype is None:
586+ self.installtype = config.installtype
587+
588+ def activecheck(self):
589+ """Check if machine is active.
590+
591+ Given that the machine is already provisioned, it's considered to be
592+ active as long as it's reachable through ssh
593+
594+ """
595+ try:
596+ self.pingpoll(timeout=self.connectivity_timeout,
597+ checktimeout=self.check_timeout)
598+ except utah.timeout.UTAHTimeout:
599+ # Ignore timeout for ping, since depending on the network
600+ # configuration ssh might still work despite of the ping failure.
601+ self.logger.warning('Network connectivity (ping) failure')
602+
603+ self.sshpoll(timeout=self.connectivity_timeout,
604+ checktimeout=self.check_timeout)
605
606=== modified file 'utah/retry.py'
607--- utah/retry.py 2012-12-03 14:02:18 +0000
608+++ utah/retry.py 2013-02-15 17:42:27 +0000
609@@ -23,9 +23,26 @@
610
611
612 def retry(command, *args, **kw):
613- """
614- Retry an operation until it completes or an exception is raised without
615- retry set to True.
616+ """Retry a command as long as a retriable exception is captured.
617+
618+ A retriable exception is considered to be a
619+ ``UTAHException`` that has the attribute ``retry`` set to
620+ ``True``.
621+
622+ :param command: Command to be executed.
623+ :type command: callable
624+ :param args: Positional arguments to be passed to the callable.
625+ :param kwargs:
626+ Keyword arguments to be passed to the callable.
627+
628+ .. note::
629+ If a keyword argument named ``logmethod`` is found, it will be
630+ extracted (i.e. it won't be passed to the callable) and used to log
631+ every retry attempt (using ``sys.stderr`` by default).
632+ :returns: The value returned by the callable.
633+
634+ .. seealso:: :func:`utah.timeout.timeout`
635+
636 """
637 try:
638 logmethod = kw.pop('logmethod')
639
640=== modified file 'utah/run.py'
641--- utah/run.py 2013-01-23 12:26:38 +0000
642+++ utah/run.py 2013-02-15 17:42:27 +0000
643@@ -22,10 +22,12 @@
644 import urllib
645 import traceback
646 import shutil
647+import signal
648
649 from utah import config
650 from utah.exceptions import UTAHException
651 from utah.url import url_argument
652+from utah.provisioning.ssh import ProvisionedMachine
653
654
655 def common_arguments(parser):
656@@ -111,6 +113,8 @@
657 """
658 Run some tests and retrieve results.
659 """
660+ install_sigterm_handler()
661+
662 if args.json:
663 report_type = 'json'
664 else:
665@@ -193,7 +197,10 @@
666 except Exception as err:
667 logging.warning('Failed to download files: ' + str(err))
668
669- if args.outputpreseed or config.outputpreseed:
670+ # Provisioned systems have an image already installed
671+ # and the preseed file is no longer available
672+ if (not isinstance(machine, ProvisionedMachine) and
673+ (args.outputpreseed or config.outputpreseed)):
674 if args.outputpreseed:
675 logging.debug('Capturing preseed due to command line option')
676 elif config.outputpreseed:
677@@ -287,3 +294,19 @@
678 logger = logging.getLogger('paramiko')
679 logger.propagate = False
680 logger.addHandler(ssh_file_handler)
681+
682+
683+def install_sigterm_handler():
684+ """Capture SIGTERM signal to avoid abrupt process termination.
685+
686+ This function registers a handler that raises an exception when SIGTERM is
687+ received to ensure that machine object cleanup methods are called.
688+ Otherwise, cleanup methods aren't called when the process is abruptly
689+ terminated.
690+
691+ """
692+ def handler(signum, frame):
693+ """Raise exception when signal is received."""
694+ raise UTAHException('Signal received: {}'.format(signum))
695+
696+ signal.signal(signal.SIGTERM, handler)
697
698=== modified file 'utah/timeout.py'
699--- utah/timeout.py 2012-12-08 02:10:12 +0000
700+++ utah/timeout.py 2013-02-15 17:42:27 +0000
701@@ -25,18 +25,29 @@
702
703
704 class UTAHTimeout(UTAHException):
705- """
706- Provide a special exception to indicate an operation timed out.
707- """
708+ """Provide a special exception to indicate an operation timed out."""
709
710
711 # Mostly pulled from utah/client/common.py
712 # Inspired by:
713 # http://stackoverflow.com/a/3326559
714 def timeout(timeout, command, *args, **kw):
715- """
716- Run command with all other arguments for up to timeout seconds, after with
717- a UTAHTimeout exception is returned.
718+ """Run a command for up to ``timeout`` seconds.
719+
720+ :param timeout:
721+ Maximum amount of time to wait for the command to complete in seconds.
722+ :type timeout: int
723+ :param command:
724+ Command whose execution should complete before timeout expires.
725+ :type command: callable
726+ :param args: Positional arguments to be passed to the callable.
727+ :param kwargs: Keyword arguments to be passed to the callable.
728+ :returns: The value returned by the callable.
729+ :raises UTAHTimeout:
730+ If command execution hasn't finished before ``timeout`` seconds.
731+
732+ .. seealso:: :func:`utah.retry.retry`
733+
734 """
735 #TODO: Better support for nested timeouts.
736 if command is None:
737@@ -71,11 +82,20 @@
738
739
740 def subprocesstimeout(timeout, *args, **kw):
741- """
742- Pass all arguments except timeout to subprocess.Popen and run for timeout
743- seconds.
744- After the timeout is reached, kill all subprocesses and return a
745- UTAHTimeout exception.
746+ """Run command through ``subprocess.Popen`` for up to ``timeout`` seconds.
747+
748+ Command termination is checked using ``subprocess.Popen.poll``.
749+
750+ :param timeout:
751+ Maximum amount of time to wait for the command to complete in seconds.
752+ :type timeout: int
753+ :param args: Positional arguments to be passed to ``subprocess.Popen``.
754+ :param kwargs: Keyword arguments to be passed to the ``subprocess.Popen``.
755+ :returns: The subprocess object
756+ :rtype: ``subprocess.Popen``
757+ :raises UTAHTimeout:
758+ If command execution hasn't finished before ``timeout`` seconds.
759+
760 """
761 if args is None:
762 return
763@@ -118,8 +138,13 @@
764
765
766 def get_process_children(pid):
767- """
768- Find process children so they can be killed when the timeout expires.
769+ """Find process children so they can be killed when the timeout expires.
770+
771+ :param pid: Process ID for the parent process
772+ :type pid: int
773+ :returns: The pid for each children process
774+ :rtype: list(int)
775+
776 """
777 p = subprocess.Popen('ps --no-headers -o pid --ppid %d' % pid, shell=True,
778 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
779
780=== modified file 'utah/url.py'
781--- utah/url.py 2012-12-13 13:48:54 +0000
782+++ utah/url.py 2013-02-15 17:42:27 +0000
783@@ -24,6 +24,7 @@
784 import urllib2
785 import tempfile
786 import logging
787+import time
788 from urlparse import urlparse
789 from argparse import ArgumentTypeError
790
791@@ -32,6 +33,8 @@
792 import bzrlib.errors
793
794 from utah.exceptions import UTAHException
795+from utah.timeout import timeout
796+from utah.retry import retry
797
798
799 # Inspired by: http://stackoverflow.com/a/2070916/183066
800@@ -212,8 +215,25 @@
801 cmd = bzrlib.builtins.cmd_export()
802 assert cmd is not None
803 tmp_dir = tempfile.mkdtemp(prefix='utah_')
804+
805+ def bzr_export_retriable():
806+ """bzr export a URL retrying on http errors
807+
808+ This is a workaround to launchpad problems with http URLs that
809+ happen from time to time
810+
811+ """
812+ try:
813+ cmd.run(tmp_dir, url)
814+ except bzrlib.errors.InvalidHttpResponse as exception:
815+ # Separate every retry attempt with a timeout of 3 seconds
816+ time.sleep(3)
817+ raise UTAHException(exception.path, exception.msg, retry=True)
818+
819 try:
820- cmd.run(tmp_dir, url)
821+ # Retry bzr export on http errors for 60 seconds
822+ timeout(60, retry, bzr_export_retriable,
823+ logmethod=bzr_logger.debug)
824 except bzrlib.errors.BzrError as exception:
825 raise ArgumentTypeError('Bazaar export error: {}'
826 .format(exception))

Subscribers

People subscribed via source and target branches

to all changes: