Merge lp:~nuclearbob/utah/exception-overhaul into lp:utah
- exception-overhaul
- Merge into dev
Status: | Merged |
---|---|
Merged at revision: | 838 |
Proposed branch: | lp:~nuclearbob/utah/exception-overhaul |
Merge into: | lp:utah |
Diff against target: |
1192 lines (+419/-202) 17 files modified
debian/changelog (+1/-0) tests/test_rsyslog.py (+2/-1) utah/client/exceptions.py (+1/-1) utah/client/runner.py (+74/-38) utah/client/testcase.py (+3/-1) utah/client/testsuite.py (+29/-16) utah/config.py (+29/-21) utah/exceptions.py (+1/-0) utah/iso.py (+85/-45) utah/parser.py (+10/-1) utah/provisioning/baremetal/bamboofeeder.py (+25/-11) utah/provisioning/baremetal/cobbler.py (+15/-11) utah/provisioning/inventory/sqlite.py (+2/-2) utah/provisioning/provisioning.py (+27/-10) utah/provisioning/ssh.py (+65/-26) utah/run.py (+37/-11) utah/timeout.py (+13/-7) |
To merge this branch: | bzr merge lp:~nuclearbob/utah/exception-overhaul |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Max Brustkern (community) | Needs Resubmitting | ||
Javier Collado (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
I've made a lot of changes to exception handling to attempt to address a number of issues raised in the code review.
- 833. By Javier Collado
-
Merged changes to add a timeout parameter to the retry function
Instead of calling time.sleep in every function that is called through retry,
now is retry itself that takes care of that using a new parameter.Source branch: lp:~javier.collado/utah/retry-timeout
- 834. By Javier Collado
-
Merged changes to add reboot support (LP: #1086450)
Source branch: lp:~javier.collado/utah/reboot-support
Based on: lp:~nuclearbob/utah/reboot-support
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
- 835. By Max Brustkern
-
Rebasing on dev branch
- 836. By Max Brustkern
-
Resolved merge conflicts
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
Merge conflicts should be fixed. Javier's suggestion should be addressed. I also went around and added handling for most of the rest of the subprocess calls. I thought about trying to find every OS call, but I think those generally give reasonably clear error on their own. If we think it would be beneficial to wrap those, I can.
- 837. By Max Brustkern
-
Overhauled exception handling based on code review
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
I've tested the changes running the `pass.run` runlist with a desktop amd64
image and everything worked fine.
Anyway, I'd like to make a couple of comments:
- I don't think we should create a new version (0.9ubuntu2) in the changelog.
Since the old one (0.9ubuntu1) is still unreleased (i.e. not available in the
stable PPA), what I think we should do is keep on adding entries to the same
version.
- I'm confused about the commit messages in revs. 835 and 836. If you run `bzr
rebase /path/to/dev` what you get is all the revisions in your branch
replayed on top of the new version of the dev branch, that is, including all
changes that were added since you started working on your own branch. Once the
rebasing is done, there are not conflicts because otherwise the rebasing
doesn't succeed, so no merging is needed.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
I'll decrement the version. I may have done the rebase incorrectly. I tried running the rebase command and then pushing my local branch to launchpad, and it said that the branches had diverged, so I merged my launchpad branch into my local branch, which generated conflicts. Do I need to just overwrite the launchpad branch when doing that in the future?
- 838. By Max Brustkern
-
Edited changelod
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
Yes, after the rebase you need to overwrite the launchpad branch.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
I think everything looks nice. Thanks.
- 839. By Max Brustkern
-
Currently, unpacking the initrd sometimes exits with status 512 even if it works. We'll want to address this later, and already have a plan to do so, but for now, we'll just log that so things continue to work
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
I ran into problems testing this; the pipe to unpack the initrd was exiting with status 512 for a gzipped initrd, even when things unpacked fine. Since we already have a plan to overhaul the use of pipes, I changed the exception to a log message there, and I'm retesting now.
- 840. By Max Brustkern
-
Added initrd exception handling to BambooFeederMachine
- 841. By Max Brustkern
-
Added handling on IOError in NFS-editing pipe
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
I fixed the issue I had in testing and added handling for possible pipe exceptions in other places. I think this should be good to go if nobody has further objections.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
> I ran into problems testing this; the pipe to unpack the initrd was exiting
> with status 512 for a gzipped initrd, even when things unpacked fine. Since
> we already have a plan to overhaul the use of pipes, I changed the exception
> to a log message there, and I'm retesting now.
It looks like your patch ignores all non-zero codes. That seems a little heavy handed - should we just ensure the exit code is 0 or 512?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
Since we're planning to get rid of pipes soon anyway, and we've been ignoring all codes up to this point since we started using pipes, I think just logging any non-zero code is okay. If we want to track other codes, we should check all currently supported images on a couple of different systems and see what they return.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
On 03/18/2013 10:26 AM, Max Brustkern wrote:
> Since we're planning to get rid of pipes soon anyway, and we've been ignoring all codes up to this point since we started using pipes, I think just logging any non-zero code is okay. If we want to track other codes, we should check all currently supported images on a couple of different systems and see what they return.
>
Good point - your approach limits the chance of a regression.
Preview Diff
1 | === modified file 'debian/changelog' |
2 | --- debian/changelog 2013-03-01 14:18:10 +0000 |
3 | +++ debian/changelog 2013-03-18 14:45:38 +0000 |
4 | @@ -12,6 +12,7 @@ |
5 | |
6 | [ Max Brustkern ] |
7 | * Fixed ProvisionedMachine template support (LP: #1147306) |
8 | + * Overhauled exception handling based on code review |
9 | |
10 | |
11 | -- Max Brustkern <max@canonical.com> Thu, 07 Mar 2013 17:11:12 -0500 |
12 | |
13 | === modified file 'tests/test_rsyslog.py' |
14 | --- tests/test_rsyslog.py 2013-03-05 21:09:24 +0000 |
15 | +++ tests/test_rsyslog.py 2013-03-18 14:45:38 +0000 |
16 | @@ -202,7 +202,8 @@ |
17 | with tempfile.NamedTemporaryFile() as f: |
18 | r = RSyslog("utah-test", '/tmp', f.name) |
19 | threading.Thread( |
20 | - target=self.file_producer, args=(f, messages, truncate)).start() |
21 | + target=self.file_producer, |
22 | + args=(f, messages, truncate)).start() |
23 | r.wait_for_booted(steps) |
24 | |
25 | def test_usefile(self): |
26 | |
27 | === modified file 'utah/client/exceptions.py' |
28 | --- utah/client/exceptions.py 2012-12-03 14:02:18 +0000 |
29 | +++ utah/client/exceptions.py 2013-03-18 14:45:38 +0000 |
30 | @@ -27,7 +27,7 @@ |
31 | class BadDir(UTAHClientError): |
32 | """ |
33 | Raised when some test directory isn't found |
34 | - or and error happens when trying to change to it |
35 | + or an error happens when trying to change to it |
36 | """ |
37 | |
38 | |
39 | |
40 | === modified file 'utah/client/runner.py' |
41 | --- utah/client/runner.py 2013-03-15 13:31:22 +0000 |
42 | +++ utah/client/runner.py 2013-03-18 14:45:38 +0000 |
43 | @@ -13,41 +13,38 @@ |
44 | # You should have received a copy of the GNU General Public License along |
45 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
46 | |
47 | + |
48 | import datetime |
49 | +import jsonschema |
50 | +import os |
51 | +import shutil |
52 | +import stat |
53 | import subprocess |
54 | +import urllib |
55 | |
56 | +from utah.client import exceptions |
57 | +from utah.client.battery import battery |
58 | from utah.client.common import ( |
59 | + BzrHandler, |
60 | CMD_TC_REBOOT, |
61 | DATE_FORMAT, |
62 | + DEFAULT_STATE_FILE, |
63 | DEFAULT_TSLIST, |
64 | DefaultValidator, |
65 | + DevHandler, |
66 | + GitHandler, |
67 | make_result, |
68 | + MASTER_RUNLIST, |
69 | + parse_yaml_file, |
70 | + ReturnCodes, |
71 | + UTAH_DIR, |
72 | ) |
73 | from utah.client.result import Result |
74 | +from utah.client.state_agent import StateAgentYAML |
75 | from utah.client.testsuite import TestSuite |
76 | -from utah.client.state_agent import StateAgentYAML |
77 | -from utah.client import exceptions |
78 | from utah.exceptions import UTAHException |
79 | +from utah.retry import retry |
80 | from utah.timeout import timeout |
81 | -from utah.retry import retry |
82 | -from utah.client.battery import battery |
83 | - |
84 | -import os |
85 | -import shutil |
86 | -import stat |
87 | -import urllib |
88 | -import jsonschema |
89 | - |
90 | -from utah.client.common import ( |
91 | - MASTER_RUNLIST, |
92 | - UTAH_DIR, |
93 | - DEFAULT_STATE_FILE, |
94 | - ReturnCodes, |
95 | - parse_yaml_file, |
96 | - BzrHandler, |
97 | - DevHandler, |
98 | - GitHandler, |
99 | -) |
100 | |
101 | RC_LOCAL = '/etc/rc.local' |
102 | RC_LOCAL_BACKUP = '%s-utah.bak' % RC_LOCAL |
103 | @@ -167,11 +164,17 @@ |
104 | |
105 | # create the testsuite directory |
106 | if not os.path.exists(self.testsuitedir): |
107 | - os.mkdir(self.testsuitedir) |
108 | + try: |
109 | + os.mkdir(self.testsuitedir) |
110 | + except OSError as err: |
111 | + raise exceptions.BadDir(err) |
112 | |
113 | # If no runlist is supplied look for one in the testdir |
114 | if runlist is None: |
115 | runlist = os.path.join(testdir, 'master.run') |
116 | + if not os.path.isfile(runlist): |
117 | + raise exceptions.MissingFile( |
118 | + 'Runlist {} does not exist'.format(runlist)) |
119 | |
120 | self.master_runlist = runlist |
121 | self.suites = [] |
122 | @@ -222,8 +225,9 @@ |
123 | try: |
124 | os.rename(RC_LOCAL, RC_LOCAL_BACKUP) |
125 | except OSError as e: |
126 | - if e.errno in [13]: |
127 | - pass |
128 | + if e.errno != 13: |
129 | + raise exceptions.UTAHClientError( |
130 | + 'Failed to backup rc.local: {}'.format(e)) |
131 | |
132 | def reset_rc_local(self): |
133 | """ |
134 | @@ -239,8 +243,9 @@ |
135 | elif os.path.exists(RC_LOCAL): |
136 | os.remove(RC_LOCAL) |
137 | except OSError as e: |
138 | - if e.errno in [13]: |
139 | - pass |
140 | + if e.errno != 13: |
141 | + raise exceptions.UTAHClientError( |
142 | + 'Failed to backup rc.local: {}'.format(e)) |
143 | |
144 | def setup_rc_local(self, rc_local=RC_LOCAL, runlist=None): |
145 | """ |
146 | @@ -250,11 +255,14 @@ |
147 | |
148 | self.rc_local_content = rc_local_content |
149 | |
150 | - fp = open(rc_local, 'w') |
151 | - fp.write(self.rc_local_content.format(runlist=runlist)) |
152 | - fp.close() |
153 | - |
154 | - os.chmod(rc_local, stat.S_IRWXU | stat.S_IROTH | stat.S_IRGRP) |
155 | + try: |
156 | + with open(rc_local, 'w') as fp: |
157 | + fp.write(self.rc_local_content.format(runlist=runlist)) |
158 | + fp.close() |
159 | + os.fchmod(fp, stat.S_IRWXU | stat.S_IROTH | stat.S_IRGRP) |
160 | + except (IOError, OSError) as err: |
161 | + raise exceptions.UTAHClientError( |
162 | + 'Error setting up rc.local: {}'.format(err)) |
163 | |
164 | def process_results(self): |
165 | # Add stats to the results |
166 | @@ -281,7 +289,10 @@ |
167 | self.status = "RUN" |
168 | for suite in self.suites: |
169 | # Start in self.testdir. |
170 | - os.chdir(self.testsuitedir) |
171 | + try: |
172 | + os.chdir(self.testsuitedir) |
173 | + except OSError as err: |
174 | + raise exceptions.BadDir(err) |
175 | |
176 | if not suite.is_done(): |
177 | keep_going = suite.run() |
178 | @@ -349,7 +360,12 @@ |
179 | if (os.path.exists( |
180 | self.master_runlist) |
181 | and self.master_runlist != self.backup_runlist): |
182 | - shutil.copyfile(self.master_runlist, self.backup_runlist) |
183 | + try: |
184 | + shutil.copyfile(self.master_runlist, self.backup_runlist) |
185 | + except OSError as err: |
186 | + raise exceptions.UTAHClientError( |
187 | + 'Failed to backup master runlist from {} to {}: {}' |
188 | + .format(self.master_runlist, self.backup_runlist, err)) |
189 | |
190 | state = { |
191 | 'master_runlist': self.backup_runlist, |
192 | @@ -397,6 +413,10 @@ |
193 | local_filename = urllib.urlretrieve(runlist)[0] |
194 | except IOError: |
195 | raise exceptions.MissingFile(runlist) |
196 | + except urllib.ContentTooShortError as err: |
197 | + raise exceptions.MissingFile( |
198 | + 'Error when downloading {} (probably interrupted): {}' |
199 | + .format(runlist, err)) |
200 | |
201 | data = parse_yaml_file(local_filename) |
202 | validator = DefaultValidator() |
203 | @@ -440,7 +460,10 @@ |
204 | self.process_master_runlist(suite['include']) |
205 | continue |
206 | |
207 | - os.chdir(orig_dir) |
208 | + try: |
209 | + os.chdir(orig_dir) |
210 | + except OSError as err: |
211 | + raise exceptions.BadDir(err) |
212 | fetch_method = suite['fetch_method'] |
213 | fetch_location = suite['fetch_location'] |
214 | |
215 | @@ -466,7 +489,8 @@ |
216 | suite_runlist = suite.get('runlist', DEFAULT_TSLIST) |
217 | |
218 | if name in seen: |
219 | - raise Exception("%s duplicated in runlist" % name) |
220 | + raise exceptions.BadMasterRunlist( |
221 | + "{} duplicated in runlist".format(name)) |
222 | |
223 | # Fetch the testsuite. On resume don't remove the testsuite |
224 | # directory. |
225 | @@ -475,9 +499,17 @@ |
226 | # except on failures where it's easier |
227 | # to find troubleshoot permission problems |
228 | absolute_name = os.path.abspath(name) |
229 | - shutil.rmtree(absolute_name) |
230 | + try: |
231 | + shutil.rmtree(absolute_name) |
232 | + except OSError as err: |
233 | + raise exceptions.UTAHClientError( |
234 | + 'Error removing the testsuite directory {}: {}' |
235 | + .format(absolute_name, err)) |
236 | if not os.path.exists(name): |
237 | - os.mkdir(name) |
238 | + try: |
239 | + os.mkdir(name) |
240 | + except OSError as err: |
241 | + raise exceptions.BadDir(err) |
242 | |
243 | def vcs_get_retriable(): |
244 | result = vcs_handler.get(directory=name) |
245 | @@ -572,7 +604,11 @@ |
246 | self.backup_rc_local() |
247 | self.setup_rc_local(runlist=self.backup_runlist) |
248 | |
249 | - subprocess.call(['shutdown', '-r', 'now']) |
250 | + try: |
251 | + subprocess.call(['shutdown', '-r', 'now']) |
252 | + except OSError as err: |
253 | + raise exceptions.UTAHClientError('Failed to reboot: {}' |
254 | + .format(err)) |
255 | |
256 | # End of execution |
257 | |
258 | |
259 | === modified file 'utah/client/testcase.py' |
260 | --- utah/client/testcase.py 2013-02-11 19:56:06 +0000 |
261 | +++ utah/client/testcase.py 2013-03-18 14:45:38 +0000 |
262 | @@ -16,7 +16,10 @@ |
263 | """ |
264 | Testcase specific code. |
265 | """ |
266 | + |
267 | + |
268 | import jsonschema |
269 | +import os |
270 | |
271 | from utah.client.common import ( |
272 | run_cmd, |
273 | @@ -125,7 +128,6 @@ |
274 | self.save_state_callback = _save_state_callback |
275 | |
276 | control_data = None |
277 | - import os |
278 | if _control_data is None: |
279 | if os.path.exists(self.filename): |
280 | try: |
281 | |
282 | === modified file 'utah/client/testsuite.py' |
283 | --- utah/client/testsuite.py 2013-02-11 21:47:33 +0000 |
284 | +++ utah/client/testsuite.py 2013-03-18 14:45:38 +0000 |
285 | @@ -21,12 +21,18 @@ |
286 | import os |
287 | import yaml |
288 | |
289 | -from .common import run_cmd, parse_control_file |
290 | -from .common import DEFAULT_TSLIST, DEFAULT_TSCONTROL |
291 | -from .common import CMD_TS_BUILD, CMD_TS_SETUP, CMD_TS_CLEANUP |
292 | -from .common import do_nothing |
293 | -from .testcase import TestCase |
294 | -from .exceptions import MissingFile, ValidationError, YAMLEmptyFile |
295 | +from utah.client import exceptions |
296 | +from utah.client.common import ( |
297 | + CMD_TS_BUILD, |
298 | + CMD_TS_CLEANUP, |
299 | + CMD_TS_SETUP, |
300 | + DEFAULT_TSCONTROL, |
301 | + DEFAULT_TSLIST, |
302 | + do_nothing, |
303 | + parse_control_file, |
304 | + run_cmd, |
305 | +) |
306 | +from utah.client.testcase import TestCase |
307 | |
308 | |
309 | def parse_runlist_file(runlist_file): |
310 | @@ -94,12 +100,15 @@ |
311 | self.timeout = timeout |
312 | self.battery_measurements = battery_measurements |
313 | |
314 | - # work from within the testsuite's directory. |
315 | - # eg. /var/lib/utah/examples |
316 | - if not os.path.exists(self.path): |
317 | - os.mkdir(self.path) |
318 | + try: |
319 | + # work from within the testsuite's directory. |
320 | + # eg. /var/lib/utah/examples |
321 | + if not os.path.exists(self.path): |
322 | + os.mkdir(self.path) |
323 | |
324 | - os.chdir(self.path) |
325 | + os.chdir(self.path) |
326 | + except OSError as err: |
327 | + raise exceptions.BadDir(err) |
328 | # Build a TestSuite from a control file's data. |
329 | control_file = os.path.join(name, control_file) |
330 | |
331 | @@ -123,12 +132,12 @@ |
332 | try: |
333 | control_data = parse_control_file(self.control_file, |
334 | self.CONTROL_SCHEMA) |
335 | - except YAMLEmptyFile: |
336 | + except exceptions.YAMLEmptyFile: |
337 | # Skip schema validation for empty file |
338 | # Schema allows an empty dictionary, but not None |
339 | control_data = None |
340 | except jsonschema.ValidationError as exception: |
341 | - raise ValidationError( |
342 | + raise exceptions.ValidationError( |
343 | '{!r} test suite control file invalid: {!r}\n1' |
344 | 'Detailed information: {}' |
345 | .format(self.name, self.control_file, exception)) |
346 | @@ -146,12 +155,13 @@ |
347 | try: |
348 | runlist_data = parse_runlist_file(self.runlist_file) |
349 | except jsonschema.ValidationError as exception: |
350 | - raise ValidationError( |
351 | + raise exceptions.ValidationError( |
352 | '{!r} test suite runlist invalid: {!r}\n' |
353 | 'Detailed information: {}' |
354 | .format(self.name, self.runlist_file, exception)) |
355 | else: |
356 | - raise MissingFile('File not found: {}'.format(self.runlist_file)) |
357 | + raise exceptions.MissingFile( |
358 | + 'File not found: {}'.format(self.runlist_file)) |
359 | |
360 | for test in runlist_data: |
361 | name = test['test'] |
362 | @@ -243,7 +253,10 @@ |
363 | keep_going = True |
364 | |
365 | # Work from the testsuite directory |
366 | - os.chdir(self.path) |
367 | + try: |
368 | + os.chdir(self.path) |
369 | + except OSError as err: |
370 | + raise exceptions.BadDir(err) |
371 | |
372 | result = self.result |
373 | |
374 | |
375 | === modified file 'utah/config.py' |
376 | --- utah/config.py 2013-03-15 13:31:22 +0000 |
377 | +++ utah/config.py 2013-03-18 14:45:38 +0000 |
378 | @@ -42,8 +42,12 @@ |
379 | """ |
380 | dns = os.path.join('/', 'etc', 'utah', 'dns') |
381 | if os.path.isfile(dns): |
382 | - return (subprocess.Popen([dns], stdout=subprocess.PIPE) |
383 | - .communicate()[0].strip()) |
384 | + try: |
385 | + process = subprocess.Popen([dns], stdout=subprocess.PIPE) |
386 | + output = process.communicate()[0].strip() |
387 | + return output |
388 | + except OSError: |
389 | + return 'virbr0' |
390 | else: |
391 | return 'virbr0' |
392 | |
393 | @@ -166,7 +170,8 @@ |
394 | 'pattern': [ |
395 | '.*ubiquity reboot', |
396 | '.*log-output -t ubiquity umount /target$', |
397 | - '.*finish-install.d/94save-logs', # needed for server bootspeed |
398 | + # needed for server bootspeed |
399 | + '.*finish-install.d/94save-logs', |
400 | '.*finish-install: umount', |
401 | '.*rsyslogd:.*exiting on signal 15.', |
402 | ], |
403 | @@ -237,25 +242,28 @@ |
404 | # http://stackoverflow.com/questions/699325/ |
405 | # suppress-output-in-python-calls-to-executables |
406 | with open(os.devnull, 'w') as fnull: |
407 | - if subprocess.call(['which', 'dpkg-architecture'], |
408 | - stdout=fnull, stderr=fnull) == 0: |
409 | - # If dpkg-architecture is available, default to current host arch |
410 | - LOCALDEFAULTS['arch'] = subprocess.check_output( |
411 | - ['dpkg-architecture', '-qDEB_HOST_ARCH']).strip() |
412 | - else: |
413 | - # Otherwise, default to i386 |
414 | - LOCALDEFAULTS['arch'] = 'i386' |
415 | + # Default to i386 |
416 | + LOCALDEFAULTS['arch'] = 'i386' |
417 | + try: |
418 | + if subprocess.call(['which', 'dpkg-architecture'], |
419 | + stdout=fnull, stderr=fnull) == 0: |
420 | + # If dpkg-architecture is available, default to current host arch |
421 | + LOCALDEFAULTS['arch'] = subprocess.check_output( |
422 | + ['dpkg-architecture', '-qDEB_HOST_ARCH']).strip() |
423 | + except OSError: |
424 | + pass |
425 | |
426 | -# I'm not sure lsb_release is always around |
427 | -with open(os.devnull, 'w') as fnull: |
428 | - if subprocess.call(['which', 'lsb_release'], |
429 | - stdout=fnull, stderr=fnull) == 0: |
430 | - # If lsb_release is available, default to current series |
431 | - LOCALDEFAULTS['series'] = subprocess.check_output( |
432 | - ['lsb_release', '-sc']).strip() |
433 | - else: |
434 | - # Otherwise, default to precise |
435 | - LOCALDEFAULTS['series'] = 'precise' |
436 | + # Default to precise |
437 | + LOCALDEFAULTS['series'] = 'precise' |
438 | + # I'm not sure lsb_release is always around |
439 | + try: |
440 | + if subprocess.call(['which', 'lsb_release'], |
441 | + stdout=fnull, stderr=fnull) == 0: |
442 | + # If lsb_release is available, default to current series |
443 | + LOCALDEFAULTS['series'] = subprocess.check_output( |
444 | + ['lsb_release', '-sc']).strip() |
445 | + except (OSError, subprocess.CalledProcessError): |
446 | + pass |
447 | |
448 | DEFAULTS.update(LOCALDEFAULTS) |
449 | |
450 | |
451 | === modified file 'utah/exceptions.py' |
452 | --- utah/exceptions.py 2013-03-13 16:29:27 +0000 |
453 | +++ utah/exceptions.py 2013-03-18 14:45:38 +0000 |
454 | @@ -25,4 +25,5 @@ |
455 | """ |
456 | def __init__(self, *args, **kw): |
457 | self.retry = kw.pop('retry', False) |
458 | + self.external = kw.pop('external', False) |
459 | super(UTAHException, self).__init__(*args, **kw) |
460 | |
461 | === modified file 'utah/iso.py' |
462 | --- utah/iso.py 2013-01-23 17:17:23 +0000 |
463 | +++ utah/iso.py 2013-03-18 14:45:38 +0000 |
464 | @@ -32,6 +32,24 @@ |
465 | from utah.exceptions import UTAHException |
466 | |
467 | |
468 | +class UTAHISOException(UTAHException): |
469 | + pass |
470 | + |
471 | + |
472 | +def get_resource(method, url, *args, **kw): |
473 | + try: |
474 | + resource = method(url, *args, **kw) |
475 | + return resource |
476 | + except IOError as err: |
477 | + raise UTAHISOException( |
478 | + 'IOError when downloading {}: {}' |
479 | + .format(url, err), external=True) |
480 | + except urllib.ContentTooShortError as err: |
481 | + raise UTAHISOException( |
482 | + 'Error when downloading {} (probably interrupted): {}' |
483 | + .format(url, err), external=True) |
484 | + |
485 | + |
486 | class ISO(object): |
487 | """ |
488 | Provide a simplified method of interfacing with images. |
489 | @@ -64,7 +82,8 @@ |
490 | |
491 | self.percent = 0 |
492 | self.logger.info('Preparing image: ' + path) |
493 | - self.image = urllib.urlretrieve(path, reporthook=self.dldisplay)[0] |
494 | + self.image = get_resource(urllib.urlretrieve, path, |
495 | + reporthook=self.dldisplay)[0] |
496 | self.logger.debug(path + ' is locally available as ' + self.image) |
497 | self.installtype = self.getinstalltype() |
498 | if self.installtype == 'mini': |
499 | @@ -178,10 +197,19 @@ |
500 | cmd = ['bsdtar', '-t', '-f', self.image] |
501 | self.logger.debug('bsdtar list command: ' + ' '.join(cmd)) |
502 | if returnlist: |
503 | - return subprocess.check_output(cmd).strip().split('\n') |
504 | + try: |
505 | + proc = subprocess.check_output(cmd).strip().split('\n') |
506 | + except (OSError, subprocess.CalledProcessError) as err: |
507 | + raise UTAHISOException('Error listing files in ISO: {}' |
508 | + .format(err)) |
509 | else: |
510 | - return subprocess.Popen(cmd, stdout=subprocess.PIPE, |
511 | - stderr=subprocess.PIPE).communicate() |
512 | + try: |
513 | + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, |
514 | + stderr=subprocess.PIPE).communicate() |
515 | + except OSError as err: |
516 | + raise UTAHISOException('Error listing files in ISO: {}' |
517 | + .format(err)) |
518 | + return proc |
519 | |
520 | def getrealfile(self, filename): |
521 | """Return a command to safely extract a file from an ISO. |
522 | @@ -198,17 +226,16 @@ |
523 | self.logger.debug('bsdtar list command: ' + ' '.join(cmd)) |
524 | try: |
525 | output = subprocess.check_output(cmd) |
526 | - except subprocess.CalledProcessError: |
527 | - raise UTAHException('Cannot list {path} in {image}' |
528 | - .format(path=filename, image=self.image)) |
529 | + except (OSError, subprocess.CalledProcessError) as err: |
530 | + raise UTAHISOException('Cannot list {} in {}: {}' |
531 | + .format(filename, self.image, err)) |
532 | try: |
533 | columns = output.splitlines()[0].split() |
534 | realfile = columns[-1] |
535 | - except (IndexError, subprocess.CalledProcessError): |
536 | - raise UTAHException('Cannot parse bsdtar list output ' |
537 | - 'for {path} in {image}: {output}' |
538 | - .format(path=filename, image=self.image, |
539 | - output=output)) |
540 | + except IndexError as err: |
541 | + raise UTAHISOException('Cannot parse bsdtar list output ' |
542 | + 'for {} in {}: {}' |
543 | + .format(filename, self.image, output)) |
544 | cmd = ['bsdtar', '-x', '-f', self.image, '-O', realfile] |
545 | self.logger.debug('bsdtar extract command: ' + ' '.join(cmd)) |
546 | return cmd |
547 | @@ -231,9 +258,9 @@ |
548 | cmd = self.getrealfile(filename) |
549 | try: |
550 | stdout = subprocess.check_output(cmd, **kw) |
551 | - except subprocess.CalledProcessError: |
552 | - raise UTAHException('Cannot extract {path} from {image}' |
553 | - .format(path=filename, image=self.image)) |
554 | + except (OSError, subprocess.CalledProcessError) as err: |
555 | + raise UTAHISOException('Cannot extract {} from {}: {}' |
556 | + .format(filename, self.image, err)) |
557 | |
558 | if outfile is None: |
559 | outfile = os.path.join(outdir, filename) |
560 | @@ -260,9 +287,9 @@ |
561 | cmd = self.getrealfile(filename) |
562 | try: |
563 | stdout = subprocess.check_output(cmd, **kw) |
564 | - except subprocess.CalledProcessError: |
565 | - raise UTAHException('Cannot extract {path} from {image}' |
566 | - .format(path=filename, image=self.image)) |
567 | + except (OSError, subprocess.CalledProcessError) as err: |
568 | + raise UTAHISOException('Cannot extract {} from {}: {}' |
569 | + .format(filename, self.image, err)) |
570 | |
571 | return stdout |
572 | |
573 | @@ -296,26 +323,35 @@ |
574 | self.logger.info('Attempting to retrieve ' + filename) |
575 | with open(os.devnull, "w") as fnull: |
576 | # If dlcommand (default dl-ubuntu-test-iso) is available, use it |
577 | - if subprocess.call(['which', config.dlcommand], |
578 | - stdout=fnull, stderr=fnull) == 0: |
579 | - self.logger.debug('Using ' + config.dlcommand) |
580 | - if installtype == 'server': |
581 | - flavor = 'ubuntu-server' |
582 | - else: |
583 | - flavor = 'ubuntu' |
584 | - cmd = [config.dlcommand, |
585 | - '-q', |
586 | - '--flavor=' + flavor, |
587 | - '--release=' + series, |
588 | - '--variant=' + installtype, |
589 | - '--arch=' + arch, |
590 | - '--isoroot=' + config.isodir] |
591 | - self.logger.info('Downloading ISO') |
592 | - self.logger.debug(' '.join(cmd)) |
593 | - subprocess.check_call(cmd) |
594 | - path = os.path.join(config.isodir, flavor, filename) |
595 | - if os.path.isfile(path): |
596 | - return path |
597 | + cmd = ['which', config.dlcommand] |
598 | + try: |
599 | + if subprocess.call(cmd, |
600 | + stdout=fnull, stderr=fnull) == 0: |
601 | + self.logger.debug('Using ' + config.dlcommand) |
602 | + if installtype == 'server': |
603 | + flavor = 'ubuntu-server' |
604 | + else: |
605 | + flavor = 'ubuntu' |
606 | + cmd = [config.dlcommand, |
607 | + '-q', |
608 | + '--flavor=' + flavor, |
609 | + '--release=' + series, |
610 | + '--variant=' + installtype, |
611 | + '--arch=' + arch, |
612 | + '--isoroot=' + config.isodir] |
613 | + self.logger.info('Downloading ISO') |
614 | + self.logger.debug(' '.join(cmd)) |
615 | + try: |
616 | + subprocess.check_call(cmd) |
617 | + except (OSError, subprocess.CalledProcessError) as err: |
618 | + raise UTAHISOException( |
619 | + "Failed to download ISO using '{}': {}" |
620 | + .format(' '.join(cmd), err)) |
621 | + path = os.path.join(config.isodir, flavor, filename) |
622 | + if os.path.isfile(path): |
623 | + return path |
624 | + except OSError as err: |
625 | + self.logger.warning('{} failed: {}'.format(cmd, err)) |
626 | |
627 | # If we haven't returned, dlcommand didn't give us an image |
628 | # We'll need to use the utah image cache in config.isodir |
629 | @@ -339,7 +375,9 @@ |
630 | |
631 | # Scan the MD5SUMS file for our file |
632 | servermd5 = None |
633 | - for line in urllib.urlopen(remotepath + '/MD5SUMS'): |
634 | + md5path = '{}/MD5SUMS'.format(remotepath) |
635 | + md5list = get_resource(urllib.urlopen, md5path) |
636 | + for line in md5list: |
637 | if isopattern in line: |
638 | servermd5, isofile = line.split() |
639 | isofile = isofile.strip('*') |
640 | @@ -347,8 +385,9 @@ |
641 | |
642 | # If we can't find the ISO, raise an exception |
643 | if servermd5 is None: |
644 | - raise UTAHException('Specified ISO: ' + filename |
645 | - + ' not found on mirrors.') |
646 | + raise UTAHISOException( |
647 | + 'Specified ISO: {} not found on mirrors.' |
648 | + .format(filename), external=True) |
649 | self.logger.debug('Server md5 is ' + servermd5) |
650 | |
651 | # If the ISO exists locally and the md5 matches, use it |
652 | @@ -360,8 +399,8 @@ |
653 | isopath = os.path.join(remotepath, isofile) |
654 | self.percent = 0 |
655 | self.logger.info('Attempting to download ' + isopath) |
656 | - temppath = urllib.urlretrieve(isopath, |
657 | - reporthook=self.dldisplay)[0] |
658 | + temppath = get_resource(urllib.urlretrieve(isopath, |
659 | + reporthook=self.dldisplay))[0] |
660 | |
661 | # Try to copy it into our cache |
662 | self.logger.debug('Copying ' + temppath + ' to ' + path) |
663 | @@ -370,8 +409,9 @@ |
664 | if os.path.isfile(path) and servermd5 == self.getmd5(path): |
665 | return path |
666 | else: |
667 | - raise UTAHException('Image failed to download after ' + |
668 | - config.dlretries + ' tries') |
669 | + raise UTAHISOException( |
670 | + 'Image failed to download after {} tries' |
671 | + .format(config.dlretries), external=True) |
672 | |
673 | def kernelpath(self): |
674 | kernelpath = './install/vmlinuz' |
675 | |
676 | === modified file 'utah/parser.py' |
677 | --- utah/parser.py 2012-12-06 16:30:34 +0000 |
678 | +++ utah/parser.py 2013-03-18 14:45:38 +0000 |
679 | @@ -164,7 +164,16 @@ |
680 | data = None |
681 | |
682 | if logfile.startswith('http'): |
683 | - fp = urllib2.urlopen(logfile) |
684 | + try: |
685 | + fp = urllib2.urlopen(logfile) |
686 | + except IOError as err: |
687 | + raise ParserError( |
688 | + 'IOError when downloading {}: {}' |
689 | + .format(logfile, err)) |
690 | + except urllib2.ContentTooShortError as err: |
691 | + raise ParserError( |
692 | + 'Error when downloading {} (probably interrupted): {}' |
693 | + .format(logfile, err)) |
694 | data = self._parse_stream(fp) |
695 | else: |
696 | with open(logfile, 'r') as fp: |
697 | |
698 | === modified file 'utah/provisioning/baremetal/bamboofeeder.py' |
699 | --- utah/provisioning/baremetal/bamboofeeder.py 2013-03-13 16:36:02 +0000 |
700 | +++ utah/provisioning/baremetal/bamboofeeder.py 2013-03-18 14:45:38 +0000 |
701 | @@ -95,10 +95,12 @@ |
702 | self.imagedir = os.path.join(self.tmpdir, 'image.d') |
703 | if not os.path.isdir(self.imagedir): |
704 | os.makedirs(self.imagedir) |
705 | - self.sector = int( |
706 | - subprocess.check_output( |
707 | - ['file', self.wwwimage] |
708 | - ).strip().split(';')[1].split(',')[3].split()[1]) |
709 | + try: |
710 | + output = subprocess.check_output(['file', self.wwwimage]).strip() |
711 | + self.sector = output.split(';')[1].split(',')[3].split()[1] |
712 | + except (OSError, subprocess.CalledProcessError) as err: |
713 | + raise UTAHBMProvisioningException('Failed to get image info: {}' |
714 | + .format(err)) |
715 | self.logger.debug('Image start sector is ' + str(self.sector)) |
716 | self.cleanfunction(self._umountimage) |
717 | self.logger.debug('Mounting ' + self.wwwimage + ' at ' + |
718 | @@ -126,11 +128,15 @@ |
719 | os.makedirs(os.path.join(self.tmpdir, 'initrd.d')) |
720 | os.chdir(os.path.join(self.tmpdir, 'initrd.d')) |
721 | filesize = os.path.getsize(self.initrd) |
722 | - for line in subprocess.check_output( |
723 | - ['mkimage', '-l', self.initrd]).splitlines(): |
724 | - if 'Data Size:' in line: |
725 | - datasize = int(line.split()[2]) |
726 | - break |
727 | + try: |
728 | + for line in subprocess.check_output( |
729 | + ['mkimage', '-l', self.initrd]).splitlines(): |
730 | + if 'Data Size:' in line: |
731 | + datasize = int(line.split()[2]) |
732 | + break |
733 | + except (OSError, subprocess.CalledProcessError) as err: |
734 | + raise UTAHBMProvisioningException('Failed to get initrd info: {}' |
735 | + .format(err)) |
736 | headersize = filesize - datasize |
737 | self.logger.debug('uInitrd header size is ' + str(headersize)) |
738 | pipe = pipes.Template() |
739 | @@ -138,7 +144,12 @@ |
740 | ' 2>/dev/null', 'f-') |
741 | pipe.append('gunzip', '--') |
742 | pipe.append('cpio -ivd 2>/dev/null', '-.') |
743 | - pipe.copy(self.initrd, '/dev/null') |
744 | + exitstatus = pipe.copy(self.initrd, '/dev/null') |
745 | + if exitstatus != 0: |
746 | + # Currently this comes up as 512 when things seem fine |
747 | + # TODO: refactor this and check for codes at all stages |
748 | + self.logger.debug( |
749 | + 'Unpacking initrd exited with status {}'.format(exitstatus)) |
750 | |
751 | def _repackinitrd(self): |
752 | """ |
753 | @@ -152,7 +163,10 @@ |
754 | pipe.append('gzip -9fc', '--') |
755 | initrd = os.path.join(self.tmpdir, 'initrd.gz') |
756 | self.logger.debug('Repacking compressed initrd') |
757 | - pipe.copy('/dev/null', initrd) |
758 | + exitstatus = pipe.copy('/dev/null', initrd) |
759 | + if exitstatus != 0: |
760 | + raise UTAHBMProvisioningException( |
761 | + 'Unpacking initrd exited with status: {}'.format(exitstatus)) |
762 | self.logger.debug('Creating uInitrd with mkimage') |
763 | self._runargs(['mkimage', |
764 | '-A', 'arm', |
765 | |
766 | === modified file 'utah/provisioning/baremetal/cobbler.py' |
767 | --- utah/provisioning/baremetal/cobbler.py 2013-03-13 16:36:46 +0000 |
768 | +++ utah/provisioning/baremetal/cobbler.py 2013-03-18 14:45:38 +0000 |
769 | @@ -84,10 +84,8 @@ |
770 | # (only if we'll be using cobbler for a while longer) |
771 | machines = self._cobble(['system', 'find'])['output'].splitlines() |
772 | if self.name not in machines: |
773 | - raise UTAHBMProvisioningException('No machine named ' + self.name |
774 | - + ' exists in cobbler') |
775 | - else: |
776 | - return True |
777 | + raise UTAHBMProvisioningException( |
778 | + 'No machine named {} exists in cobbler'.format(self.name)) |
779 | |
780 | def _create(self, checktimeout=config.checktimeout, |
781 | installtimeout=config.installtimeout): |
782 | @@ -165,8 +163,12 @@ |
783 | pipe = pipes.Template() |
784 | pipe.append('sudo tee -a ' + str(config.nfsconfigfile) + |
785 | ' >/dev/null', '-.') |
786 | - pipe.open('/dev/null', 'w').write(self.isodir + ' ' + |
787 | - config.nfsoptions + "\n") |
788 | + try: |
789 | + pipe.open('/dev/null', 'w').write(self.isodir + ' ' + |
790 | + config.nfsoptions + "\n") |
791 | + except IOError as err: |
792 | + raise UTAHBMProvisioningException( |
793 | + 'Failed to setup NFS: {}'.format(err)) |
794 | self.logger.debug('Reloading NFS config') |
795 | self._runargs(config.nfscommand + ['reload']) |
796 | self.logger.debug('Adding NFS boot options') |
797 | @@ -191,7 +193,8 @@ |
798 | for arg in self.machineinfo]) |
799 | |
800 | self.restart() |
801 | - self.rsyslog.wait_for_install(config.install_steps, self._disable_netboot) |
802 | + self.rsyslog.wait_for_install(config.install_steps, |
803 | + self._disable_netboot) |
804 | self.rsyslog.wait_for_booted(config.boot_steps) |
805 | |
806 | if self.installtype == 'desktop': |
807 | @@ -216,11 +219,10 @@ |
808 | Pull out all cobbler commands so that we can later support changing |
809 | users, remote login, not sudo, etc. |
810 | """ |
811 | - tail_process = \ |
812 | - subprocess.Popen(['tail', '-n0', '-f', |
813 | - '/var/log/cobbler/cobbler.log'], |
814 | - stdout=subprocess.PIPE) |
815 | try: |
816 | + tail_process = subprocess.Popen( |
817 | + ['tail', '-n0', '-f', '/var/log/cobbler/cobbler.log'], |
818 | + stdout=subprocess.PIPE) |
819 | command_results = self._runargs(['sudo', 'cobbler'] + cmd) |
820 | retcode = command_results['returncode'] |
821 | if retcode != 0: |
822 | @@ -233,6 +235,8 @@ |
823 | raise UTAHBMProvisioningException(error_msg) |
824 | else: |
825 | return command_results |
826 | + except (OSError, subprocess.CalledProcessError) as err: |
827 | + self.logger.error('Failed to get cobbler logs: {}'.format(err)) |
828 | finally: |
829 | # tail process will wait forever because of the -f/--follow option, |
830 | # so it has to be terminated in order to read from stdout. |
831 | |
832 | === modified file 'utah/provisioning/inventory/sqlite.py' |
833 | --- utah/provisioning/inventory/sqlite.py 2013-01-24 16:47:04 +0000 |
834 | +++ utah/provisioning/inventory/sqlite.py 2013-03-18 14:45:38 +0000 |
835 | @@ -32,8 +32,8 @@ |
836 | db = os.path.expanduser(db) |
837 | super(SQLiteInventory, self).__init__(*args, uniqueid=db, **kw) |
838 | self.db = db |
839 | - self._connection = sqlite3.connect(self.db, |
840 | - config.sqlite3_db_connection_timeout) |
841 | + self._connection = sqlite3.connect( |
842 | + self.db, config.sqlite3_db_connection_timeout) |
843 | self._connection.isolation_level = None |
844 | self._connection.row_factory = sqlite3.Row |
845 | |
846 | |
847 | === modified file 'utah/provisioning/provisioning.py' |
848 | --- utah/provisioning/provisioning.py 2013-03-13 16:56:34 +0000 |
849 | +++ utah/provisioning/provisioning.py 2013-03-18 14:45:38 +0000 |
850 | @@ -755,9 +755,13 @@ |
851 | initrd = self.initrd |
852 | if tmpdir is None: |
853 | tmpdir = self.tmpdir |
854 | - if not os.path.isdir(os.path.join(tmpdir, 'initrd.d')): |
855 | - os.makedirs(os.path.join(tmpdir, 'initrd.d')) |
856 | - os.chdir(os.path.join(tmpdir, 'initrd.d')) |
857 | + try: |
858 | + if not os.path.isdir(os.path.join(tmpdir, 'initrd.d')): |
859 | + os.makedirs(os.path.join(tmpdir, 'initrd.d')) |
860 | + os.chdir(os.path.join(tmpdir, 'initrd.d')) |
861 | + except OSError as err: |
862 | + raise UTAHProvisioningException( |
863 | + 'Error using temp directory {}: {}'.format(tmpdir, err)) |
864 | pipe = pipes.Template() |
865 | if os.path.splitext(initrd)[1] == '.gz': |
866 | self.logger.debug('Using gzip based on file extension') |
867 | @@ -765,8 +769,17 @@ |
868 | elif os.path.splitext(initrd)[1] == '.lz': |
869 | self.logger.debug('Using lzma based on file extension') |
870 | pipe.prepend('lzcat -S .lz $IN', 'f-') |
871 | + else: |
872 | + raise UTAHProvisioningException( |
873 | + 'initrd file does have have gz or lz extension: {}' |
874 | + .format(initrd)) |
875 | pipe.append('cpio -ivd 2>/dev/null', '-.') |
876 | - pipe.copy(initrd, '/dev/null') |
877 | + exitstatus = pipe.copy(initrd, '/dev/null') |
878 | + if exitstatus != 0: |
879 | + # Currently this comes up as 512 when things seem fine |
880 | + # TODO: refactor this and check for codes at all stages |
881 | + self.logger.debug( |
882 | + 'Unpacking initrd exited with status {}'.format(exitstatus)) |
883 | |
884 | def _setuplatecommand(self, tmpdir=None): |
885 | """ |
886 | @@ -943,11 +956,14 @@ |
887 | os.chmod(filename, 0755) |
888 | |
889 | orderfilename = os.path.join(casper_dir, 'ORDER') |
890 | - self._runargs(['sed', '-i', |
891 | - '1i/scripts/casper-bottom/' |
892 | - 'utah\\n' |
893 | - '[ -e /conf/param.conf ] && . /conf/param.conf', |
894 | - orderfilename]) |
895 | + exitstatus = self._runargs( |
896 | + ['sed', '-i', |
897 | + '1i/scripts/casper-bottom/' |
898 | + 'utah\\n' |
899 | + '[ -e /conf/param.conf ] && . /conf/param.conf', |
900 | + orderfilename])['returncode'] |
901 | + if exitstatus != 0: |
902 | + raise UTAHProvisioningException('Failed to setup casper script') |
903 | |
904 | casper_file = os.path.join(tmpdir, 'initrd.d', 'etc', 'casper.conf') |
905 | tmpfilename = casper_file + '.tmp' |
906 | @@ -1010,7 +1026,8 @@ |
907 | self.logger.debug('Using gzip because installtype is not desktop') |
908 | pipe.append('gzip -9fc ', '--') |
909 | initrd = os.path.join(tmpdir, 'initrd.gz') |
910 | - pipe.copy('/dev/null', initrd) |
911 | + if pipe.copy('/dev/null', initrd) != 0: |
912 | + raise UTAHProvisioningException('Failed to repack initrd') |
913 | return initrd |
914 | |
915 | def _cmdlinesetup(self, boot=None): |
916 | |
917 | === modified file 'utah/provisioning/ssh.py' |
918 | --- utah/provisioning/ssh.py 2013-03-15 13:31:22 +0000 |
919 | +++ utah/provisioning/ssh.py 2013-03-18 14:45:38 +0000 |
920 | @@ -95,24 +95,37 @@ |
921 | # http://od-eon.com/blogs/ |
922 | # stefan/automating-remote-commands-over-ssh-paramiko/ |
923 | self.ssh_logger.debug('Connecting SSH') |
924 | - self.ssh_client.connect(self.name, |
925 | - username=user, |
926 | - key_filename=config.sshprivatekey) |
927 | - |
928 | - self.ssh_logger.debug('Opening SSH session') |
929 | - channel = self.ssh_client.get_transport().open_session() |
930 | - |
931 | - self.ssh_logger.info('Running command through SSH: ' + commandstring) |
932 | - stdout = channel.makefile('rb') |
933 | - stderr = channel.makefile_stderr('rb') |
934 | - if timeout is None: |
935 | - channel.exec_command(commandstring) |
936 | - else: |
937 | - utah.timeout.timeout(timeout, channel.exec_command, commandstring) |
938 | - retval = channel.recv_exit_status() |
939 | - |
940 | - self.ssh_logger.debug('Closing SSH connection') |
941 | - self.ssh_client.close() |
942 | + retval = 'not run' |
943 | + try: |
944 | + self.ssh_client.connect(self.name, |
945 | + username=user, |
946 | + key_filename=config.sshprivatekey) |
947 | + |
948 | + self.ssh_logger.debug('Opening SSH session') |
949 | + channel = self.ssh_client.get_transport().open_session() |
950 | + |
951 | + self.ssh_logger.info('Running command through SSH: {}' |
952 | + .format(commandstring)) |
953 | + stdout = channel.makefile('rb') |
954 | + stderr = channel.makefile_stderr('rb') |
955 | + if timeout is None: |
956 | + channel.exec_command(commandstring) |
957 | + else: |
958 | + utah.timeout.timeout(timeout, channel.exec_command, |
959 | + commandstring) |
960 | + retval = channel.recv_exit_status() |
961 | + |
962 | + except paramiko.BadHostKeyException as err: |
963 | + raise UTAHProvisioningException( |
964 | + 'Host key exception encountered; ' |
965 | + 'machine may be in use by another process: {}' |
966 | + .format(err), external=True) |
967 | + except (paramiko.AuthenticationException, paramiko.SSHException, |
968 | + socket.error) as err: |
969 | + raise UTAHProvisioningException(err) |
970 | + finally: |
971 | + self.ssh_logger.debug('Closing SSH connection') |
972 | + self.ssh_client.close() |
973 | |
974 | if retval == 0: |
975 | self.ssh_logger.debug('Return code: {}'.format(retval)) |
976 | @@ -146,12 +159,12 @@ |
977 | files = [files] |
978 | |
979 | self.activecheck() |
980 | - self.ssh_client.connect(self.name, |
981 | - username=config.user, |
982 | - key_filename=config.sshprivatekey) |
983 | - sftp_client = self.ssh_client.open_sftp() |
984 | failed = [] |
985 | try: |
986 | + self.ssh_client.connect(self.name, |
987 | + username=config.user, |
988 | + key_filename=config.sshprivatekey) |
989 | + sftp_client = self.ssh_client.open_sftp() |
990 | for localpath in files: |
991 | if os.path.isfile(localpath): |
992 | self.ssh_logger.info('Uploading {} from the host ' |
993 | @@ -162,6 +175,14 @@ |
994 | sftp_client.put(localpath, remotepath) |
995 | else: |
996 | failed.append(localpath) |
997 | + except paramiko.BadHostKeyException as err: |
998 | + raise UTAHProvisioningException( |
999 | + 'Host key exception encountered; ' |
1000 | + 'machine may be in use by another process: {}' |
1001 | + .format(err), external=True) |
1002 | + except (paramiko.AuthenticationException, paramiko.SSHException, |
1003 | + socket.error) as err: |
1004 | + raise UTAHProvisioningException(err) |
1005 | finally: |
1006 | sftp_client.close() |
1007 | if len(failed) > 0: |
1008 | @@ -180,10 +201,6 @@ |
1009 | files = [files] |
1010 | |
1011 | self.activecheck() |
1012 | - self.ssh_client.connect(self.name, |
1013 | - username=config.user, |
1014 | - key_filename=config.sshprivatekey) |
1015 | - sftp_client = self.ssh_client.open_sftp() |
1016 | if os.path.isdir(target): |
1017 | get_localpath = lambda remotepath: \ |
1018 | os.path.join(target, os.path.basename(remotepath)) |
1019 | @@ -191,12 +208,24 @@ |
1020 | get_localpath = lambda remotepath: target |
1021 | |
1022 | try: |
1023 | + self.ssh_client.connect(self.name, |
1024 | + username=config.user, |
1025 | + key_filename=config.sshprivatekey) |
1026 | + sftp_client = self.ssh_client.open_sftp() |
1027 | for remotepath in files: |
1028 | localpath = get_localpath(remotepath) |
1029 | self.ssh_logger.info('Downloading {} from the machine ' |
1030 | 'to {} on the host' |
1031 | .format(remotepath, target)) |
1032 | sftp_client.get(remotepath, localpath) |
1033 | + except paramiko.BadHostKeyException as err: |
1034 | + raise UTAHProvisioningException( |
1035 | + 'Host key exception encountered; ' |
1036 | + 'machine may be in use by another process: {}' |
1037 | + .format(err), external=True) |
1038 | + except (paramiko.AuthenticationException, paramiko.SSHException, |
1039 | + socket.error) as err: |
1040 | + raise UTAHProvisioningException(err) |
1041 | finally: |
1042 | sftp_client.close() |
1043 | |
1044 | @@ -270,8 +299,18 @@ |
1045 | self.ssh_client.connect(self.name, |
1046 | username=config.user, |
1047 | key_filename=config.sshprivatekey) |
1048 | + except paramiko.BadHostKeyException as err: |
1049 | + raise UTAHProvisioningException( |
1050 | + 'Host key exception encountered; ' |
1051 | + 'machine may be in use by another process: {}' |
1052 | + .format(err), external=True) |
1053 | + except (paramiko.AuthenticationException, |
1054 | + paramiko.SSHException) as err: |
1055 | + raise UTAHProvisioningException(err) |
1056 | except socket.error as err: |
1057 | raise UTAHProvisioningException(str(err), retry=True) |
1058 | + finally: |
1059 | + self.ssh_client.close() |
1060 | |
1061 | def sshpoll(self, timeout=None, |
1062 | checktimeout=config.checktimeout, logmethod=None): |
1063 | |
1064 | === modified file 'utah/run.py' |
1065 | --- utah/run.py 2013-03-13 17:13:32 +0000 |
1066 | +++ utah/run.py 2013-03-18 14:45:38 +0000 |
1067 | @@ -151,7 +151,16 @@ |
1068 | def _run(runlist): |
1069 | """Run a single runlist.""" |
1070 | try: |
1071 | - runlist_local_path = urllib.urlretrieve(runlist)[0] |
1072 | + try: |
1073 | + runlist_local_path = urllib.urlretrieve(runlist)[0] |
1074 | + except IOError as err: |
1075 | + raise UTAHException( |
1076 | + 'IOError when downloading {}: {}' |
1077 | + .format(runlist, err), external=True) |
1078 | + except urllib.ContentTooShortError as err: |
1079 | + raise UTAHException( |
1080 | + 'Error when downloading {} (probably interrupted): {}' |
1081 | + .format(runlist, err), external=True) |
1082 | target_dir = os.path.normpath('/tmp') |
1083 | machine.uploadfiles([runlist_local_path], target_dir) |
1084 | |
1085 | @@ -205,12 +214,20 @@ |
1086 | suffix=report_type)) |
1087 | report_local_path = os.path.join(config.logpath, log_filename) |
1088 | report_local_path = os.path.normpath(report_local_path) |
1089 | - machine.downloadfiles(report_remote_path, report_local_path) |
1090 | - logging.info('Test log copied to ' + report_local_path) |
1091 | - locallogs.append(report_local_path) |
1092 | - if args.dumplogs: |
1093 | - with open(report_local_path, 'r') as f: |
1094 | - print(f.read()) |
1095 | + try: |
1096 | + machine.downloadfiles(report_remote_path, report_local_path) |
1097 | + except UTAHException: |
1098 | + logging.error('Test log not retrieved') |
1099 | + else: |
1100 | + logging.info('Test log copied to ' + report_local_path) |
1101 | + locallogs.append(report_local_path) |
1102 | + if args.dumplogs: |
1103 | + try: |
1104 | + with open(report_local_path, 'r') as f: |
1105 | + print(f.read()) |
1106 | + except IOError as err: |
1107 | + logging.warning('Test log could not be dumped: {}' |
1108 | + .format(err)) |
1109 | |
1110 | # Write the machine name to standard out for log gathering |
1111 | print('Running on machine: ' + machine.name) |
1112 | @@ -232,7 +249,7 @@ |
1113 | if args.files is not None: |
1114 | try: |
1115 | locallogs += getfiles(args, machine) |
1116 | - except Exception as err: |
1117 | + except UTAHException as err: |
1118 | logging.warning('Failed to download files: ' + str(err)) |
1119 | |
1120 | # Provisioned systems have an image already installed |
1121 | @@ -245,8 +262,13 @@ |
1122 | logging.debug('Capturing preseed due to config option') |
1123 | preseedfile = os.path.join(config.logpath, |
1124 | machine.name + '-preseed.cfg') |
1125 | - shutil.copyfile(machine.finalpreseed, preseedfile) |
1126 | - locallogs.append(preseedfile) |
1127 | + try: |
1128 | + shutil.copyfile(machine.finalpreseed, preseedfile) |
1129 | + except (IOError, shutil.Error) as err: |
1130 | + logging.warning('Failed to copy preseed file: {}' |
1131 | + .format(err)) |
1132 | + else: |
1133 | + locallogs.append(preseedfile) |
1134 | |
1135 | return exitstatus, locallogs |
1136 | |
1137 | @@ -257,7 +279,11 @@ |
1138 | else: |
1139 | outdir = args.outdir |
1140 | if not os.path.isdir(outdir): |
1141 | - os.makedirs(outdir) |
1142 | + try: |
1143 | + os.makedirs(outdir) |
1144 | + except OSError as err: |
1145 | + raise UTAHException('Failed to create output directory {}: {}' |
1146 | + .format(outdir, err), external=True) |
1147 | |
1148 | machine.downloadfilesrecursive(args.files, outdir) |
1149 | |
1150 | |
1151 | === modified file 'utah/timeout.py' |
1152 | --- utah/timeout.py 2013-03-04 13:09:18 +0000 |
1153 | +++ utah/timeout.py 2013-03-18 14:45:38 +0000 |
1154 | @@ -17,8 +17,9 @@ |
1155 | Provide functionality to execute command with timeouts. |
1156 | """ |
1157 | import os |
1158 | +import signal |
1159 | import subprocess |
1160 | -import signal |
1161 | +import sys |
1162 | |
1163 | from utah.exceptions import UTAHException |
1164 | from utah.commandstr import commandstr |
1165 | @@ -141,7 +142,7 @@ |
1166 | raise UTAHTimeout(msg) |
1167 | |
1168 | |
1169 | -def get_process_children(pid): |
1170 | +def get_process_children(pid, logmethod=sys.stderr.write): |
1171 | """Find process children so they can be killed when the timeout expires. |
1172 | |
1173 | :param pid: Process ID for the parent process |
1174 | @@ -150,8 +151,13 @@ |
1175 | :rtype: list(int) |
1176 | |
1177 | """ |
1178 | - p = subprocess.Popen('ps --no-headers -o pid --ppid %d' % pid, shell=True, |
1179 | - stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
1180 | - stdout, _stderr = p.communicate() |
1181 | - |
1182 | - return [int(pid) for pid in stdout.split()] |
1183 | + try: |
1184 | + p = subprocess.Popen('ps --no-headers -o pid --ppid %d' % pid, |
1185 | + shell=True, stdout=subprocess.PIPE, |
1186 | + stderr=subprocess.PIPE) |
1187 | + stdout, _stderr = p.communicate() |
1188 | + pids = [int(pid) for pid in stdout.split()] |
1189 | + return pids |
1190 | + except OSError as err: |
1191 | + logmethod('Could not kill process children: {}'.format(err)) |
1192 | + return [] |
It looks nice.
The only issue I've seen that was partially fixed in another merge proposal
is that this:
try:
self.external = kw.pop('external')
except KeyError:
self.external = False
can be replaced with this:
self.external = kw.pop('external', False)