Merge lp:~jtv/maas/bug-1324237 into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: 2387
Proposed branch: lp:~jtv/maas/bug-1324237
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 297 lines (+64/-70)
7 files modified
src/provisioningserver/boot/utils.py (+3/-5)
src/provisioningserver/dns/config.py (+2/-4)
src/provisioningserver/import_images/tests/test_boot_resources.py (+1/-1)
src/provisioningserver/omshell.py (+3/-3)
src/provisioningserver/tests/test_omshell.py (+3/-2)
src/provisioningserver/utils/__init__.py (+15/-21)
src/provisioningserver/utils/tests/test_utils.py (+37/-34)
To merge this branch: bzr merge lp:~jtv/maas/bug-1324237
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+221469@code.launchpad.net

Commit message

Report stderr output from failing commands as part of the exception, so that we can see what went wrong. Fold call_capture_and_check into call_and_check, since they do the same thing except that one returns meaningful data.

Description of the change

There really was no need for call_capture_and_check: it did the exact same thing as call_and_check, except it returned standard output where call_and_check returned the command's return code. Both raise ExternalProcessError if the return code is nonzero, so there was no point in returning it. Just return stdout and now we have only one function.

In one place, when verifying GPG signatures for downloaded boot loaders, we passed stderr=subprocess.STDOUT. I'm not sure if that actually went anywhere. At any rate it should no longer be needed: a traceback will now show the error text.

Jeroen

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

 review: approve

Tip top.
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1
Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/

iEYEARECAAYFAlOH5RUACgkQWhGlTF8G/HcPxgCfaZoiHpq9rsdRdzL2enKjRr4s
QGoAn3ei7ULYIgqgRJHWyXHwmA3IHOqz
=cKta
-----END PGP SIGNATURE-----

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/provisioningserver/boot/utils.py'
--- src/provisioningserver/boot/utils.py 2014-03-28 04:06:27 +0000
+++ src/provisioningserver/boot/utils.py 2014-05-29 23:37:13 +0000
@@ -24,11 +24,10 @@
24from platform import linux_distribution24from platform import linux_distribution
25import re25import re
26import StringIO26import StringIO
27import subprocess
28import urllib227import urllib2
2928
30from provisioningserver.utils import (29from provisioningserver.utils import (
31 call_capture_and_check,30 call_and_check,
32 tempdir,31 tempdir,
33 )32 )
3433
@@ -71,14 +70,13 @@
71 with open(data_out, 'wb') as stream:70 with open(data_out, 'wb') as stream:
72 stream.write(data_file)71 stream.write(data_file)
7372
74 args = [73 call_and_check([
75 "gpgv",74 "gpgv",
76 "--keyring",75 "--keyring",
77 "/etc/apt/trusted.gpg",76 "/etc/apt/trusted.gpg",
78 sig_out,77 sig_out,
79 data_out78 data_out
80 ]79 ])
81 call_capture_and_check(args, stderr=subprocess.STDOUT)
8280
8381
84def decompress_packages(packages):82def decompress_packages(packages):
8583
=== modified file 'src/provisioningserver/dns/config.py'
--- src/provisioningserver/dns/config.py 2014-02-14 10:47:19 +0000
+++ src/provisioningserver/dns/config.py 2014-05-29 23:37:13 +0000
@@ -39,7 +39,6 @@
39from provisioningserver.utils import (39from provisioningserver.utils import (
40 atomic_write,40 atomic_write,
41 call_and_check,41 call_and_check,
42 call_capture_and_check,
43 incremental_write,42 incremental_write,
44 locate_config,43 locate_config,
45 )44 )
@@ -102,7 +101,7 @@
102 # Generate the configuration:101 # Generate the configuration:
103 # - 256 bits is the recommended size for the key nowadays.102 # - 256 bits is the recommended size for the key nowadays.
104 # - Use urandom to avoid blocking on the random generator.103 # - Use urandom to avoid blocking on the random generator.
105 rndc_content = call_capture_and_check(104 rndc_content = call_and_check(
106 ['rndc-confgen', '-b', '256', '-r', '/dev/urandom',105 ['rndc-confgen', '-b', '256', '-r', '/dev/urandom',
107 '-k', key_name, '-p', unicode(port).encode("ascii")])106 '-k', key_name, '-p', unicode(port).encode("ascii")])
108 named_comment = extract_suggested_named_conf(rndc_content)107 named_comment = extract_suggested_named_conf(rndc_content)
@@ -149,8 +148,7 @@
149 rndc_conf = get_rndc_conf_path()148 rndc_conf = get_rndc_conf_path()
150 rndc_cmd = ['rndc', '-c', rndc_conf]149 rndc_cmd = ['rndc', '-c', rndc_conf]
151 rndc_cmd.extend(arguments)150 rndc_cmd.extend(arguments)
152 with open(os.devnull, "ab") as devnull:151 call_and_check(rndc_cmd)
153 call_and_check(rndc_cmd, stdout=devnull)
154152
155153
156# Location of DNS templates, relative to the configuration directory.154# Location of DNS templates, relative to the configuration directory.
157155
=== modified file 'src/provisioningserver/import_images/tests/test_boot_resources.py'
--- src/provisioningserver/import_images/tests/test_boot_resources.py 2014-05-27 08:21:33 +0000
+++ src/provisioningserver/import_images/tests/test_boot_resources.py 2014-05-29 23:37:13 +0000
@@ -272,7 +272,7 @@
272 # at a low level, so that we exercise all the function calls that a272 # at a low level, so that we exercise all the function calls that a
273 # unit test might not put to the test.273 # unit test might not put to the test.
274 self.patch_logger()274 self.patch_logger()
275 self.patch(boot_resources, 'call_and_check').return_code = 0275 self.patch(boot_resources, 'call_and_check')
276276
277 # We'll go through installation of a PXE boot loader here, but skip277 # We'll go through installation of a PXE boot loader here, but skip
278 # all other boot loader types. Testing them all is a job for proper278 # all other boot loader types. Testing them all is a job for proper
279279
=== modified file 'src/provisioningserver/omshell.py'
--- src/provisioningserver/omshell.py 2014-02-27 03:30:05 +0000
+++ src/provisioningserver/omshell.py 2014-05-29 23:37:13 +0000
@@ -1,4 +1,4 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the1# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Python wrapper around the `omshell` utility which amends objects4"""Python wrapper around the `omshell` utility which amends objects
@@ -28,7 +28,7 @@
28from textwrap import dedent28from textwrap import dedent
2929
30from provisioningserver.utils import (30from provisioningserver.utils import (
31 call_capture_and_check,31 call_and_check,
32 ExternalProcessError,32 ExternalProcessError,
33 parse_key_value_file,33 parse_key_value_file,
34 tempdir,34 tempdir,
@@ -42,7 +42,7 @@
42 path = os.environ.get("PATH", "").split(os.pathsep)42 path = os.environ.get("PATH", "").split(os.pathsep)
43 path.append("/usr/sbin")43 path.append("/usr/sbin")
44 env = dict(os.environ, PATH=os.pathsep.join(path))44 env = dict(os.environ, PATH=os.pathsep.join(path))
45 return call_capture_and_check(45 return call_and_check(
46 ['dnssec-keygen', '-r', '/dev/urandom', '-a', 'HMAC-MD5',46 ['dnssec-keygen', '-r', '/dev/urandom', '-a', 'HMAC-MD5',
47 '-b', '512', '-n', 'HOST', '-K', tmpdir, '-q', 'omapi_key'],47 '-b', '512', '-n', 'HOST', '-K', tmpdir, '-q', 'omapi_key'],
48 env=env)48 env=env)
4949
=== modified file 'src/provisioningserver/tests/test_omshell.py'
--- src/provisioningserver/tests/test_omshell.py 2014-02-27 03:29:12 +0000
+++ src/provisioningserver/tests/test_omshell.py 2014-05-29 23:37:13 +0000
@@ -271,12 +271,13 @@
271 """Tests for omshell.call_dnssec_keygen."""271 """Tests for omshell.call_dnssec_keygen."""
272272
273 def test_runs_external_script(self):273 def test_runs_external_script(self):
274 check_output = self.patch(subprocess, 'check_output')274 call_and_check = self.patch(
275 provisioningserver.omshell, 'call_and_check')
275 target_dir = self.make_dir()276 target_dir = self.make_dir()
276 path = os.environ.get("PATH", "").split(os.pathsep)277 path = os.environ.get("PATH", "").split(os.pathsep)
277 path.append("/usr/sbin")278 path.append("/usr/sbin")
278 call_dnssec_keygen(target_dir)279 call_dnssec_keygen(target_dir)
279 check_output.assert_called_once_with(280 call_and_check.assert_called_once_with(
280 ['dnssec-keygen', '-r', '/dev/urandom', '-a', 'HMAC-MD5',281 ['dnssec-keygen', '-r', '/dev/urandom', '-a', 'HMAC-MD5',
281 '-b', '512', '-n', 'HOST', '-K', target_dir, '-q', 'omapi_key'],282 '-b', '512', '-n', 'HOST', '-K', target_dir, '-q', 'omapi_key'],
282 env=ANY)283 env=ANY)
283284
=== modified file 'src/provisioningserver/utils/__init__.py'
--- src/provisioningserver/utils/__init__.py 2014-05-05 23:55:46 +0000
+++ src/provisioningserver/utils/__init__.py 2014-05-29 23:37:13 +0000
@@ -15,14 +15,15 @@
15__all__ = [15__all__ = [
16 "ActionScript",16 "ActionScript",
17 "atomic_write",17 "atomic_write",
18 "call_and_check",
18 "deferred",19 "deferred",
20 "ensure_dir",
19 "filter_dict",21 "filter_dict",
20 "find_ip_via_arp",22 "find_ip_via_arp",
21 "import_settings",23 "import_settings",
22 "incremental_write",24 "incremental_write",
23 "locate_config",25 "locate_config",
24 "MainScript",26 "MainScript",
25 "ensure_dir",
26 "parse_key_value_file",27 "parse_key_value_file",
27 "read_text_file",28 "read_text_file",
28 "ShellTemplate",29 "ShellTemplate",
@@ -115,25 +116,18 @@
115def call_and_check(command, *args, **kwargs):116def call_and_check(command, *args, **kwargs):
116 """A wrapper around subprocess.check_call().117 """A wrapper around subprocess.check_call().
117118
118 When an error occurs, raise an ExternalProcessError.119 :param command: Command line, as a list of strings.
119 """120 :return: The command's output from standard output.
120 try:121 :raise ExternalProcessError: If the command returns nonzero.
121 return subprocess.check_call(command, *args, **kwargs)122 """
122 except subprocess.CalledProcessError as error:123 process = subprocess.Popen(
123 error.__class__ = ExternalProcessError124 command, *args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
124 raise125 **kwargs)
125126 stdout, stderr = process.communicate()
126127 stderr = stderr.strip()
127def call_capture_and_check(command, *args, **kwargs):128 if process.returncode != 0:
128 """A wrapper around subprocess.check_output().129 raise ExternalProcessError(process.returncode, command, output=stderr)
129130 return stdout
130 When an error occurs, raise an ExternalProcessError.
131 """
132 try:
133 return subprocess.check_output(command, *args, **kwargs)
134 except subprocess.CalledProcessError as error:
135 error.__class__ = ExternalProcessError
136 raise
137131
138132
139def locate_config(*path):133def locate_config(*path):
@@ -822,7 +816,7 @@
822 :param mac: The mac address, e.g. '1c:6f:65:d5:56:98'.816 :param mac: The mac address, e.g. '1c:6f:65:d5:56:98'.
823 """817 """
824818
825 output = call_capture_and_check(['arp', '-n']).split('\n')819 output = call_and_check(['arp', '-n']).split('\n')
826820
827 for line in sorted(output):821 for line in sorted(output):
828 columns = line.split()822 columns = line.split()
829823
=== modified file 'src/provisioningserver/utils/tests/test_utils.py'
--- src/provisioningserver/utils/tests/test_utils.py 2014-05-19 12:40:56 +0000
+++ src/provisioningserver/utils/tests/test_utils.py 2014-05-29 23:37:13 +0000
@@ -68,7 +68,6 @@
68 atomic_write,68 atomic_write,
69 AtomicWriteScript,69 AtomicWriteScript,
70 call_and_check,70 call_and_check,
71 call_capture_and_check,
72 classify,71 classify,
73 ensure_dir,72 ensure_dir,
74 escape_py_literal,73 escape_py_literal,
@@ -1204,33 +1203,39 @@
1204 classify(is_even, subjects))1203 classify(is_even, subjects))
12051204
12061205
1207class TestSubprocessWrappers(MAASTestCase):1206class TestCallAndCheck(MAASTestCase):
1208 """Tests for the subprocess.* wrapper functions."""1207 """Tests `call_and_check`."""
12091208
1210 def test_call_and_check_returns_returncode(self):1209 def patch_popen(self, returncode=0, stderr=''):
1211 self.patch(subprocess, 'check_call', FakeMethod(0))1210 """Replace `subprocess.Popen` with a mock."""
1212 self.assertEqual(0, call_and_check('some_command'))1211 popen = self.patch(subprocess, 'Popen')
12131212 process = popen.return_value
1214 def test_call_and_check_raises_ExternalProcessError_on_failure(self):1213 process.communicate.return_value = (None, stderr)
1215 self.patch(subprocess, 'check_call').side_effect = (1214 process.returncode = returncode
1216 CalledProcessError('-1', 'some_command'))1215 return process
1217 error = self.assertRaises(1216
1218 ExternalProcessError, call_and_check, "some command")1217 def test__returns_standard_output(self):
1219 self.assertEqual('-1', error.returncode)1218 output = factory.getRandomString()
1220 self.assertEqual('some_command', error.cmd)1219 self.assertEqual(output, call_and_check(['/bin/echo', '-n', output]))
12211220
1222 def test_call_capture_and_check_returns_returncode(self):1221 def test__raises_ExternalProcessError_on_failure(self):
1223 self.patch(subprocess, 'check_output', FakeMethod("Some output"))1222 command = factory.make_name('command')
1224 self.assertEqual("Some output", call_capture_and_check('some_command'))1223 message = factory.getRandomString()
12251224 self.patch_popen(returncode=1, stderr=message)
1226 def test_call_capture_and_check_raises_ExternalProcessError_on_fail(self):1225 error = self.assertRaises(
1227 self.patch(subprocess, 'check_output').side_effect = (1226 ExternalProcessError, call_and_check, command)
1228 CalledProcessError('-1', 'some_command', "Some output"))1227 self.assertEqual(1, error.returncode)
1229 error = self.assertRaises(1228 self.assertEqual(command, error.cmd)
1230 ExternalProcessError, call_capture_and_check, "some command")1229 self.assertEqual(message, error.output)
1231 self.assertEqual('-1', error.returncode)1230
1232 self.assertEqual('some_command', error.cmd)1231 def test__reports_stderr_on_failure(self):
1233 self.assertEqual("Some output", error.output)1232 nonfile = os.path.join(self.make_dir(), factory.make_name('nonesuch'))
1233 error = self.assertRaises(
1234 ExternalProcessError,
1235 call_and_check, ['/bin/cat', nonfile], env={'LC_ALL': 'C'})
1236 self.assertEqual(
1237 "/bin/cat: %s: No such file or directory" % nonfile,
1238 error.output)
12341239
12351240
1236class TestExternalProcessError(MAASTestCase):1241class TestExternalProcessError(MAASTestCase):
@@ -1303,8 +1308,8 @@
1303class TestFindIPViaARP(MAASTestCase):1308class TestFindIPViaARP(MAASTestCase):
13041309
1305 def patch_call(self, output):1310 def patch_call(self, output):
1306 """Replace `call_capture_and_check` with one that returns `output`."""1311 """Replace `call_and_check` with one that returns `output`."""
1307 fake = self.patch(provisioningserver.utils, 'call_capture_and_check')1312 fake = self.patch(provisioningserver.utils, 'call_and_check')
1308 fake.return_value = output1313 fake.return_value = output
1309 return fake1314 return fake
13101315
@@ -1321,11 +1326,9 @@
1321 192.168.0.1 ether 90:f6:52:f6:17:92 C eth01326 192.168.0.1 ether 90:f6:52:f6:17:92 C eth0
1322 """1327 """
13231328
1324 call_capture_and_check = self.patch_call(sample)1329 call_and_check = self.patch_call(sample)
1325 ip_address_observed = find_ip_via_arp("90:f6:52:f6:17:92")1330 ip_address_observed = find_ip_via_arp("90:f6:52:f6:17:92")
1326 self.assertThat(1331 self.assertThat(call_and_check, MockCalledOnceWith(['arp', '-n']))
1327 call_capture_and_check,
1328 MockCalledOnceWith(['arp', '-n']))
1329 self.assertEqual("192.168.0.1", ip_address_observed)1332 self.assertEqual("192.168.0.1", ip_address_observed)
13301333
1331 def test__returns_consistent_output(self):1334 def test__returns_consistent_output(self):