Merge lp:~ltrager/maas/combined_result_refresh into lp:~maas-committers/maas/trunk

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: no longer in the source branch.
Merged at revision: 5780
Proposed branch: lp:~ltrager/maas/combined_result_refresh
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 772 lines (+359/-180)
8 files modified
src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py (+2/-51)
src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py (+0/-115)
src/provisioningserver/refresh/__init__.py (+48/-10)
src/provisioningserver/refresh/maas_api_helper.py (+52/-0)
src/provisioningserver/refresh/node_info_scripts.py (+26/-1)
src/provisioningserver/refresh/tests/test_maas_api_helper.py (+111/-0)
src/provisioningserver/refresh/tests/test_node_info_scripts.py (+26/-3)
src/provisioningserver/refresh/tests/test_refresh.py (+94/-0)
To merge this branch: bzr merge lp:~ltrager/maas/combined_result_refresh
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+318854@code.launchpad.net

Commit message

Send combined commissioning results in addtion to stdout and stderr when refreshing.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

lgtm! it mostly seems moving stuff around.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.8 MiB)

The attempt to merge lp:~ltrager/maas/combined_result_refresh into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease [102 kB]
Get:4 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
Fetched 306 kB in 0s (612 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind avahi-utils bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common isc-dhcp-server libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libnss-wrapper libpq-dev make nodejs-legacy npm postgresql psmisc pxelinux python3-all python3-apt python3-attr python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~rc+dfsg-1ubuntu2).
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is already the newest version (3:6.03+dfsg-11ubuntu1).
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5.0-1build1).
python-netaddr is already the newest version (0.7.18-1).
python-net...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py'
2--- src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py 2017-03-02 21:53:33 +0000
3+++ src/metadataserver/user_data/templates/snippets/maas_run_remote_scripts.py 2017-03-03 03:26:15 +0000
4@@ -4,7 +4,6 @@
5 from io import BytesIO
6 import json
7 import os
8-import selectors
9 from subprocess import (
10 PIPE,
11 Popen,
12@@ -17,11 +16,6 @@
13 )
14 import time
15
16-# See fcntl(2), re. F_SETPIPE_SZ. By requesting this many bytes from a pipe on
17-# each read we can be sure that we are always draining its buffer completely.
18-with open("/proc/sys/fs/pipe-max-size") as _pms:
19- PIPE_MAX_SIZE = int(_pms.read())
20-
21
22 try:
23 from maas_api_helper import (
24@@ -30,6 +24,7 @@
25 read_config,
26 signal,
27 SignalException,
28+ capture_script_output,
29 )
30 except ImportError:
31 # For running unit tests.
32@@ -39,6 +34,7 @@
33 read_config,
34 signal,
35 SignalException,
36+ capture_script_output,
37 )
38
39
40@@ -66,51 +62,6 @@
41 tar.extractall(scripts_dir)
42
43
44-def capture_script_output(proc, combined_path, stdout_path, stderr_path):
45- """Capture stdout and stderr from `proc`.
46-
47- Standard output is written to a file named by `stdout_path`, and standard
48- error is written to a file named by `stderr_path`. Both are also written
49- to a file named by `combined_path`.
50-
51- If the given subprocess forks additional processes, and these write to the
52- same stdout and stderr, their output will be captured only as long as
53- `proc` is running.
54-
55- :return: The exit code of `proc`.
56- """
57- with open(stdout_path, 'wb') as out, open(stderr_path, 'wb') as err:
58- with open(combined_path, 'wb') as combined:
59- with selectors.DefaultSelector() as selector:
60- selector.register(proc.stdout, selectors.EVENT_READ, out)
61- selector.register(proc.stderr, selectors.EVENT_READ, err)
62- while selector.get_map() and proc.poll() is None:
63- # Select with a short timeout so that we don't tight loop.
64- _select_script_output(selector, combined, 0.1)
65- else:
66- # Process has finished or has closed stdout and stderr.
67- # Process anything still sitting in the latter's buffers.
68- _select_script_output(selector, combined, 0.0)
69-
70- # Always wait for the process to finish.
71- return proc.wait()
72-
73-
74-def _select_script_output(selector, combined, timeout):
75- """Helper for `capture_script_output`."""
76- for key, event in selector.select(timeout):
77- if event & selectors.EVENT_READ:
78- # Read from the _raw_ file. Ordinarily Python blocks until a
79- # read(n) returns n bytes or the stream reaches end-of-file,
80- # but here we only want to get what's there without blocking.
81- chunk = key.fileobj.raw.read(PIPE_MAX_SIZE)
82- if len(chunk) == 0: # EOF
83- selector.unregister(key.fileobj)
84- else:
85- key.data.write(chunk)
86- combined.write(chunk)
87-
88-
89 def run_scripts(url, creds, scripts_dir, out_dir, scripts):
90 """Run and report results for the given scripts."""
91 fail_count = 0
92
93=== modified file 'src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py'
94--- src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py 2017-03-02 21:53:33 +0000
95+++ src/metadataserver/user_data/templates/snippets/tests/test_maas_run_remote_scripts.py 2017-03-03 03:26:15 +0000
96@@ -8,17 +8,10 @@
97 from io import BytesIO
98 import json
99 import os
100-from pathlib import Path
101 import random
102-from subprocess import (
103- PIPE,
104- Popen,
105-)
106 import tarfile
107-import time
108 from unittest.mock import ANY
109
110-from lxml import etree
111 from maastesting.factory import factory
112 from maastesting.fixtures import TempDirectory
113 from maastesting.matchers import (
114@@ -29,16 +22,10 @@
115 from maastesting.testcase import MAASTestCase
116 from snippets import maas_run_remote_scripts
117 from snippets.maas_run_remote_scripts import (
118- capture_script_output,
119 download_and_extract_tar,
120 run_scripts,
121 run_scripts_from_metadata,
122 )
123-from testtools.matchers import (
124- Equals,
125- MatchesAny,
126- MatchesListwise,
127-)
128
129
130 class TestMaasRunRemoteScripts(MAASTestCase):
131@@ -302,105 +289,3 @@
132 mock_signal,
133 MockAnyCall(
134 None, None, 'FAILED', '1 scripts failed to run'))
135-
136-
137-class TestCaptureScriptOutput(MAASTestCase):
138-
139- # Iterate multiple times to shake out spurious failures.
140- scenarios = [
141- ("iteration %d" % iteration, {})
142- for iteration in range(1, 21)
143- ]
144-
145- def capture(self, proc):
146- scripts_dir = Path(self.useFixture(TempDirectory()).path)
147- combined_path = scripts_dir.joinpath("combined")
148- stdout_path = scripts_dir.joinpath("stdout")
149- stderr_path = scripts_dir.joinpath("stderr")
150-
151- returncode = capture_script_output(
152- proc, str(combined_path), str(stdout_path), str(stderr_path))
153-
154- return (
155- returncode,
156- stdout_path.read_text(),
157- stderr_path.read_text(),
158- combined_path.read_text(),
159- )
160-
161- def test__captures_script_output(self):
162- proc = Popen(
163- 'echo "stdout"; echo "stderr" 1>&2', stdout=PIPE, stderr=PIPE,
164- shell=True)
165- self.assertThat(
166- self.capture(proc), MatchesListwise((
167- Equals(0), Equals("stdout\n"), Equals("stderr\n"),
168- # The writes to stdout and stderr occur so close in time that
169- # they may be received in any order.
170- MatchesAny(
171- Equals("stdout\nstderr\n"),
172- Equals("stderr\nstdout\n"),
173- ),
174- )))
175-
176- def test__does_not_wait_for_forked_process(self):
177- start_time = time.time()
178- proc = Popen('sleep 6 &', stdout=PIPE, stderr=PIPE, shell=True)
179- self.assertThat(
180- self.capture(proc), MatchesListwise((
181- Equals(0), Equals(""), Equals(""), Equals(""),
182- )))
183- # A forked process should continue running after capture_script_output
184- # returns. capture_script_output should not block on the forked call.
185- self.assertLess(time.time() - start_time, 3)
186-
187- def test__captures_output_from_completed_process(self):
188- # Write to both stdout and stderr.
189- proc = Popen(
190- 'echo -n foo >&1 && echo -n bar >&2',
191- stdout=PIPE, stderr=PIPE, shell=True)
192- # Wait for it to finish before capturing.
193- self.assertEquals(0, proc.wait())
194- # Capturing now still gets foo and bar.
195- self.assertThat(
196- self.capture(proc), MatchesListwise((
197- Equals(0), Equals("foo"), Equals("bar"),
198- # The writes to stdout and stderr occur so close in time that
199- # they may be received in any order.
200- MatchesAny(Equals("foobar"), Equals("barfoo")),
201- )))
202-
203- def test__captures_stderr_after_stdout_closes(self):
204- # Write to stdout, close stdout, then write to stderr.
205- proc = Popen(
206- 'echo -n foo >&1 && exec 1>&- && echo -n bar >&2',
207- stdout=PIPE, stderr=PIPE, shell=True)
208- # Capturing gets the bar even after stdout is closed.
209- self.assertThat(
210- self.capture(proc), MatchesListwise((
211- Equals(0), Equals("foo"), Equals("bar"),
212- # The writes to stdout and stderr occur so close in time that
213- # they may be received in any order.
214- MatchesAny(Equals("foobar"), Equals("barfoo")),
215- )))
216-
217- def test__captures_stdout_after_stderr_closes(self):
218- # Write to stderr, close stderr, then write to stdout.
219- proc = Popen(
220- 'echo -n bar >&2 && exec 2>&- && echo -n foo >&1',
221- stdout=PIPE, stderr=PIPE, shell=True)
222- # Capturing gets the foo even after stderr is closed.
223- self.assertThat(
224- self.capture(proc), MatchesListwise((
225- Equals(0), Equals("foo"), Equals("bar"),
226- # The writes to stdout and stderr occur so close in time that
227- # they may be received in any order.
228- MatchesAny(Equals("foobar"), Equals("barfoo")),
229- )))
230-
231- def test__captures_all_output(self):
232- proc = Popen(("lshw", "-xml"), stdout=PIPE, stderr=PIPE)
233- returncode, stdout, stderr, combined = self.capture(proc)
234- self.assertThat(returncode, Equals(0), stderr)
235- # This is a complete XML document; we've captured all output.
236- self.assertThat(etree.fromstring(stdout).tag, Equals("list"))
237
238=== modified file 'src/provisioningserver/refresh/__init__.py'
239--- src/provisioningserver/refresh/__init__.py 2017-01-28 00:51:47 +0000
240+++ src/provisioningserver/refresh/__init__.py 2017-03-03 03:26:15 +0000
241@@ -5,11 +5,15 @@
242 import os
243 import socket
244 import stat
245-import subprocess
246+from subprocess import (
247+ PIPE,
248+ Popen,
249+)
250 import tempfile
251
252 from provisioningserver.logger import get_maas_logger
253 from provisioningserver.refresh.maas_api_helper import (
254+ capture_script_output,
255 MD_VERSION,
256 signal,
257 SignalException,
258@@ -123,7 +127,10 @@
259 total_scripts = len(scripts)
260 current_script = 1
261 failed_scripts = []
262- for script_name, builtin_script in scripts.items():
263+ out_dir = os.path.join(tmpdir, 'out')
264+ os.makedirs(out_dir)
265+ for script_name in sorted(scripts.keys()):
266+ builtin_script = scripts[script_name]
267 signal_wrapper(
268 url, creds, 'WORKING', 'Starting %s [%d/%d]' %
269 (script_name, current_script, total_scripts))
270@@ -135,17 +142,48 @@
271 st = os.stat(script_path)
272 os.chmod(script_path, st.st_mode | stat.S_IEXEC)
273
274- # Execute script and store stdout/stderr
275- proc = subprocess.Popen(
276- script_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
277- stdout, stderr = proc.communicate()
278+ # If we pipe the output of the subprocess and the subprocess also
279+ # creates a subprocess we end up dead locking. Spawn a shell process
280+ # and capture the output to the filesystem to avoid that and help with
281+ # debugging.
282+ combined_path = os.path.join(out_dir, script_name)
283+ stdout_name = '%s.out' % script_name
284+ stdout_path = os.path.join(out_dir, stdout_name)
285+ stderr_name = '%s.err' % script_name
286+ stderr_path = os.path.join(out_dir, stderr_name)
287+
288+ try:
289+ proc = Popen(script_path, stdout=PIPE, stderr=PIPE)
290+ except OSError as e:
291+ if isinstance(e.errno, int) and e.errno != 0:
292+ exit_status = e.errno
293+ else:
294+ # 2 is the return code bash gives when it can't execute.
295+ exit_status = 2
296+ result = str(e).encode()
297+ if result == b'':
298+ result = b'Unable to execute script'
299+ files = {
300+ script_name: result,
301+ stderr_name: result,
302+ }
303+ else:
304+ capture_script_output(
305+ proc, combined_path, stdout_path, stderr_path)
306+
307+ exit_status = proc.returncode
308+ files = {
309+ script_name: open(combined_path, 'rb').read(),
310+ stdout_name: open(stdout_path, 'rb').read(),
311+ stderr_name: open(stderr_path, 'rb').read(),
312+ }
313+
314 signal_wrapper(
315 url, creds,
316 "WORKING", "Finished %s [%d/%d]: %d" %
317- (script_name, current_script, total_scripts, proc.returncode),
318- files={script_name: stdout, "%s.err" % script_name: stderr},
319- exit_status=proc.returncode)
320- if proc.returncode != 0:
321+ (script_name, current_script, total_scripts, exit_status),
322+ files=files, exit_status=exit_status)
323+ if exit_status != 0:
324 failed_scripts.append(script_name)
325 current_script += 1
326 return failed_scripts
327
328=== modified file 'src/provisioningserver/refresh/maas_api_helper.py'
329--- src/provisioningserver/refresh/maas_api_helper.py 2017-01-28 00:51:47 +0000
330+++ src/provisioningserver/refresh/maas_api_helper.py 2017-03-03 03:26:15 +0000
331@@ -14,6 +14,7 @@
332 import json
333 import mimetypes
334 import random
335+import selectors
336 import socket
337 import string
338 import sys
339@@ -29,6 +30,12 @@
340 MD_VERSION = '2012-03-01'
341
342
343+# See fcntl(2), re. F_SETPIPE_SZ. By requesting this many bytes from a pipe on
344+# each read we can be sure that we are always draining its buffer completely.
345+with open("/proc/sys/fs/pipe-max-size") as _pms:
346+ PIPE_MAX_SIZE = int(_pms.read())
347+
348+
349 def oauth_headers(url, consumer_key, token_key, token_secret, consumer_secret,
350 clockskew=0):
351 """Build OAuth headers using given credentials."""
352@@ -257,3 +264,48 @@
353 raise SignalException(str(exc))
354 except Exception as exc:
355 raise SignalException("Unexpected error [%s]" % exc)
356+
357+
358+def capture_script_output(proc, combined_path, stdout_path, stderr_path):
359+ """Capture stdout and stderr from `proc`.
360+
361+ Standard output is written to a file named by `stdout_path`, and standard
362+ error is written to a file named by `stderr_path`. Both are also written
363+ to a file named by `combined_path`.
364+
365+ If the given subprocess forks additional processes, and these write to the
366+ same stdout and stderr, their output will be captured only as long as
367+ `proc` is running.
368+
369+ :return: The exit code of `proc`.
370+ """
371+ with open(stdout_path, 'wb') as out, open(stderr_path, 'wb') as err:
372+ with open(combined_path, 'wb') as combined:
373+ with selectors.DefaultSelector() as selector:
374+ selector.register(proc.stdout, selectors.EVENT_READ, out)
375+ selector.register(proc.stderr, selectors.EVENT_READ, err)
376+ while selector.get_map() and proc.poll() is None:
377+ # Select with a short timeout so that we don't tight loop.
378+ _select_script_output(selector, combined, 0.1)
379+ else:
380+ # Process has finished or has closed stdout and stderr.
381+ # Process anything still sitting in the latter's buffers.
382+ _select_script_output(selector, combined, 0.0)
383+
384+ # Always wait for the process to finish.
385+ return proc.wait()
386+
387+
388+def _select_script_output(selector, combined, timeout):
389+ """Helper for `capture_script_output`."""
390+ for key, event in selector.select(timeout):
391+ if event & selectors.EVENT_READ:
392+ # Read from the _raw_ file. Ordinarily Python blocks until a
393+ # read(n) returns n bytes or the stream reaches end-of-file,
394+ # but here we only want to get what's there without blocking.
395+ chunk = key.fileobj.raw.read(PIPE_MAX_SIZE)
396+ if len(chunk) == 0: # EOF
397+ selector.unregister(key.fileobj)
398+ else:
399+ key.data.write(chunk)
400+ combined.write(chunk)
401
402=== modified file 'src/provisioningserver/refresh/node_info_scripts.py'
403--- src/provisioningserver/refresh/node_info_scripts.py 2017-02-27 14:19:44 +0000
404+++ src/provisioningserver/refresh/node_info_scripts.py 2017-03-03 03:26:15 +0000
405@@ -1,4 +1,4 @@
406-# Copyright 2016 Canonical Ltd. This software is licensed under the
407+# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
408 # GNU Affero General Public License version 3 (see the file LICENSE).
409
410 """Builtin node info scripts."""
411@@ -353,6 +353,7 @@
412 import json
413 import os
414 import shlex
415+ import sys
416 from subprocess import check_output
417
418 # XXX: Set LC_* and LANG environment variables to C.UTF-8 explicitly.
419@@ -366,6 +367,30 @@
420 return os.path.join(dev_disk_byid, link)
421 return None
422
423+ running_dir = os.path.dirname(__file__)
424+ # Results are stored differently when being run as part of node
425+ # commissioning vs controller refresh.
426+ virtuality_result_paths = [
427+ os.path.join(running_dir, '..', '..', 'out', '00-maas-02-virtuality'),
428+ os.path.join(running_dir, 'out', '00-maas-02-virtuality'),
429+ ]
430+ # This script doesn't work in containers as they don't have any block
431+ # device. If the virtuality script detected its in one don't report
432+ # anything.
433+ for virtuality_result_path in virtuality_result_paths:
434+ if not os.path.exists(virtuality_result_path):
435+ continue
436+ virtuality_result = open(virtuality_result_path, 'r').read().strip()
437+ # Names from man SYSTEMD-DETECT-VIRT(1)
438+ if virtuality_result in {
439+ 'openvz', 'lxc', 'lxc-libvirt', 'systemd-nspawn', 'docker',
440+ 'rkt'}:
441+ print(
442+ 'Unable to detect block devices while running in container!',
443+ file=sys.stderr)
444+ print('[]')
445+ return
446+
447 # Grab the block devices from lsblk. Excludes RAM devices
448 # (default for lsblk), floppy disks, and loopback devices.
449 blockdevs = []
450
451=== modified file 'src/provisioningserver/refresh/tests/test_maas_api_helper.py'
452--- src/provisioningserver/refresh/tests/test_maas_api_helper.py 2017-01-28 00:51:47 +0000
453+++ src/provisioningserver/refresh/tests/test_maas_api_helper.py 2017-03-03 03:26:15 +0000
454@@ -9,12 +9,19 @@
455 from email.utils import formatdate
456 from io import StringIO
457 import json
458+from pathlib import Path
459 import random
460 import re
461+from subprocess import (
462+ PIPE,
463+ Popen,
464+)
465 import time
466 import urllib
467
468+from lxml import etree
469 from maastesting.factory import factory
470+from maastesting.fixtures import TempDirectory
471 from maastesting.matchers import (
472 GreaterThanOrEqual,
473 LessThanOrEqual,
474@@ -28,7 +35,9 @@
475 AfterPreprocessing,
476 Equals,
477 MatchesAll,
478+ MatchesAny,
479 MatchesDict,
480+ MatchesListwise,
481 )
482
483
484@@ -481,3 +490,105 @@
485 self.assertRaises(
486 maas_api_helper.SignalException,
487 maas_api_helper.signal, None, None, status)
488+
489+
490+class TestCaptureScriptOutput(MAASTestCase):
491+
492+ # Iterate multiple times to shake out spurious failures.
493+ scenarios = [
494+ ("iteration %d" % iteration, {})
495+ for iteration in range(1, 21)
496+ ]
497+
498+ def capture(self, proc):
499+ scripts_dir = Path(self.useFixture(TempDirectory()).path)
500+ combined_path = scripts_dir.joinpath("combined")
501+ stdout_path = scripts_dir.joinpath("stdout")
502+ stderr_path = scripts_dir.joinpath("stderr")
503+
504+ returncode = maas_api_helper.capture_script_output(
505+ proc, str(combined_path), str(stdout_path), str(stderr_path))
506+
507+ return (
508+ returncode,
509+ stdout_path.read_text(),
510+ stderr_path.read_text(),
511+ combined_path.read_text(),
512+ )
513+
514+ def test__captures_script_output(self):
515+ proc = Popen(
516+ 'echo "stdout"; echo "stderr" 1>&2', stdout=PIPE, stderr=PIPE,
517+ shell=True)
518+ self.assertThat(
519+ self.capture(proc), MatchesListwise((
520+ Equals(0), Equals("stdout\n"), Equals("stderr\n"),
521+ # The writes to stdout and stderr occur so close in time that
522+ # they may be received in any order.
523+ MatchesAny(
524+ Equals("stdout\nstderr\n"),
525+ Equals("stderr\nstdout\n"),
526+ ),
527+ )))
528+
529+ def test__does_not_wait_for_forked_process(self):
530+ start_time = time.time()
531+ proc = Popen('sleep 6 &', stdout=PIPE, stderr=PIPE, shell=True)
532+ self.assertThat(
533+ self.capture(proc), MatchesListwise((
534+ Equals(0), Equals(""), Equals(""), Equals(""),
535+ )))
536+ # A forked process should continue running after capture_script_output
537+ # returns. capture_script_output should not block on the forked call.
538+ self.assertLess(time.time() - start_time, 3)
539+
540+ def test__captures_output_from_completed_process(self):
541+ # Write to both stdout and stderr.
542+ proc = Popen(
543+ 'echo -n foo >&1 && echo -n bar >&2',
544+ stdout=PIPE, stderr=PIPE, shell=True)
545+ # Wait for it to finish before capturing.
546+ self.assertEquals(0, proc.wait())
547+ # Capturing now still gets foo and bar.
548+ self.assertThat(
549+ self.capture(proc), MatchesListwise((
550+ Equals(0), Equals("foo"), Equals("bar"),
551+ # The writes to stdout and stderr occur so close in time that
552+ # they may be received in any order.
553+ MatchesAny(Equals("foobar"), Equals("barfoo")),
554+ )))
555+
556+ def test__captures_stderr_after_stdout_closes(self):
557+ # Write to stdout, close stdout, then write to stderr.
558+ proc = Popen(
559+ 'echo -n foo >&1 && exec 1>&- && echo -n bar >&2',
560+ stdout=PIPE, stderr=PIPE, shell=True)
561+ # Capturing gets the bar even after stdout is closed.
562+ self.assertThat(
563+ self.capture(proc), MatchesListwise((
564+ Equals(0), Equals("foo"), Equals("bar"),
565+ # The writes to stdout and stderr occur so close in time that
566+ # they may be received in any order.
567+ MatchesAny(Equals("foobar"), Equals("barfoo")),
568+ )))
569+
570+ def test__captures_stdout_after_stderr_closes(self):
571+ # Write to stderr, close stderr, then write to stdout.
572+ proc = Popen(
573+ 'echo -n bar >&2 && exec 2>&- && echo -n foo >&1',
574+ stdout=PIPE, stderr=PIPE, shell=True)
575+ # Capturing gets the foo even after stderr is closed.
576+ self.assertThat(
577+ self.capture(proc), MatchesListwise((
578+ Equals(0), Equals("foo"), Equals("bar"),
579+ # The writes to stdout and stderr occur so close in time that
580+ # they may be received in any order.
581+ MatchesAny(Equals("foobar"), Equals("barfoo")),
582+ )))
583+
584+ def test__captures_all_output(self):
585+ proc = Popen(("lshw", "-xml"), stdout=PIPE, stderr=PIPE)
586+ returncode, stdout, stderr, combined = self.capture(proc)
587+ self.assertThat(returncode, Equals(0), stderr)
588+ # This is a complete XML document; we've captured all output.
589+ self.assertThat(etree.fromstring(stdout).tag, Equals("list"))
590
591=== modified file 'src/provisioningserver/refresh/tests/test_node_info_scripts.py'
592--- src/provisioningserver/refresh/tests/test_node_info_scripts.py 2017-02-17 14:23:04 +0000
593+++ src/provisioningserver/refresh/tests/test_node_info_scripts.py 2017-03-03 03:26:15 +0000
594@@ -5,7 +5,6 @@
595
596 __all__ = []
597
598-from functools import partial
599 from inspect import getsource
600 from io import StringIO
601 import json
602@@ -18,11 +17,13 @@
603 check_output,
604 STDOUT,
605 )
606+import sys
607 from textwrap import dedent
608 import time
609 from unittest.mock import call
610
611 from maastesting.factory import factory
612+from maastesting.fixtures import TempDirectory
613 from maastesting.matchers import (
614 MockCalledOnceWith,
615 MockCallsMatch,
616@@ -115,6 +116,8 @@
617 source = dedent(getsource(function))
618 modcode = compile(source, "isolated.py", "exec")
619 namespace = {} if namespace is None else namespace
620+ if '__file__' not in namespace:
621+ namespace['__file__'] = __file__
622 exec(modcode, namespace)
623 return namespace[function.__name__]
624
625@@ -373,9 +376,18 @@
626 return output.encode("ascii")
627
628 def call_gather_physical_block_devices(
629- self, dev_disk_byid='/dev/disk/by-id/'):
630+ self, dev_disk_byid='/dev/disk/by-id/', file_path=None):
631 output = StringIO()
632- namespace = {"print": partial(print, file=output)}
633+
634+ def neutered_print(*args, **kwargs):
635+ file = kwargs.pop('file', None)
636+ if file is not None and file == sys.stderr:
637+ return
638+ return print(*args, **kwargs, file=output)
639+
640+ namespace = {"print": neutered_print}
641+ if file_path is not None:
642+ namespace['__file__'] = file_path
643 gather_physical_block_devices = isolate_function(
644 node_info_module.gather_physical_block_devices, namespace)
645 gather_physical_block_devices(dev_disk_byid=dev_disk_byid)
646@@ -885,6 +897,17 @@
647 ]
648 self.assertEqual(names, device_names)
649
650+ def test__quietly_exits_in_container(self):
651+ script_dir = self.useFixture(TempDirectory()).path
652+ script_path = os.path.join(script_dir, '00-maas-07-block-devices')
653+ virtuality_result_path = os.path.join(script_dir, 'out')
654+ os.makedirs(virtuality_result_path)
655+ virtuality_result_path = os.path.join(
656+ virtuality_result_path, '00-maas-02-virtuality')
657+ open(virtuality_result_path, 'w').write('lxc\n')
658+ self.assertItemsEqual(
659+ [], self.call_gather_physical_block_devices(file_path=script_path))
660+
661
662 class TestVirtualityScript(MAASTestCase):
663 """Tests for `VIRTUALITY_SCRIPT`."""
664
665=== modified file 'src/provisioningserver/refresh/tests/test_refresh.py'
666--- src/provisioningserver/refresh/tests/test_refresh.py 2017-01-28 00:51:47 +0000
667+++ src/provisioningserver/refresh/tests/test_refresh.py 2017-03-03 03:26:15 +0000
668@@ -236,10 +236,104 @@
669 exit_status=0,
670 files={
671 script_name: b'test script\n',
672+ '%s.out' % script_name: b'test script\n',
673 '%s.err' % script_name: b'',
674 },
675 ))
676
677+ def test_refresh_signals_failure_on_unexecutable_script(self):
678+ signal = self.patch(refresh, 'signal')
679+ script_name = factory.make_name('script_name')
680+ self.patch_scripts_failure(script_name)
681+ mock_popen = self.patch(refresh, 'Popen')
682+ mock_popen.side_effect = OSError(8, 'Exec format error')
683+
684+ system_id = factory.make_name('system_id')
685+ consumer_key = factory.make_name('consumer_key')
686+ token_key = factory.make_name('token_key')
687+ token_secret = factory.make_name('token_secret')
688+ url = factory.make_url()
689+
690+ refresh.refresh(system_id, consumer_key, token_key, token_secret, url)
691+ self.assertThat(signal, MockAnyCall(
692+ "%s/metadata/%s/" % (url, MD_VERSION),
693+ {
694+ 'consumer_secret': '',
695+ 'consumer_key': consumer_key,
696+ 'token_key': token_key,
697+ 'token_secret': token_secret,
698+ },
699+ 'WORKING',
700+ 'Finished %s [1/1]: 8' % script_name,
701+ exit_status=8,
702+ files={
703+ script_name: b'[Errno 8] Exec format error',
704+ '%s.err' % script_name: b'[Errno 8] Exec format error',
705+ },
706+ ))
707+
708+ def test_refresh_signals_failure_on_unexecutable_script_no_errno(self):
709+ signal = self.patch(refresh, 'signal')
710+ script_name = factory.make_name('script_name')
711+ self.patch_scripts_failure(script_name)
712+ mock_popen = self.patch(refresh, 'Popen')
713+ mock_popen.side_effect = OSError()
714+
715+ system_id = factory.make_name('system_id')
716+ consumer_key = factory.make_name('consumer_key')
717+ token_key = factory.make_name('token_key')
718+ token_secret = factory.make_name('token_secret')
719+ url = factory.make_url()
720+
721+ refresh.refresh(system_id, consumer_key, token_key, token_secret, url)
722+ self.assertThat(signal, MockAnyCall(
723+ "%s/metadata/%s/" % (url, MD_VERSION),
724+ {
725+ 'consumer_secret': '',
726+ 'consumer_key': consumer_key,
727+ 'token_key': token_key,
728+ 'token_secret': token_secret,
729+ },
730+ 'WORKING',
731+ 'Finished %s [1/1]: 2' % script_name,
732+ exit_status=2,
733+ files={
734+ script_name: b'Unable to execute script',
735+ '%s.err' % script_name: b'Unable to execute script',
736+ },
737+ ))
738+
739+ def test_refresh_signals_failure_on_unexecutable_script_baderrno(self):
740+ signal = self.patch(refresh, 'signal')
741+ script_name = factory.make_name('script_name')
742+ self.patch_scripts_failure(script_name)
743+ mock_popen = self.patch(refresh, 'Popen')
744+ mock_popen.side_effect = OSError(0, 'Exec format error')
745+
746+ system_id = factory.make_name('system_id')
747+ consumer_key = factory.make_name('consumer_key')
748+ token_key = factory.make_name('token_key')
749+ token_secret = factory.make_name('token_secret')
750+ url = factory.make_url()
751+
752+ refresh.refresh(system_id, consumer_key, token_key, token_secret, url)
753+ self.assertThat(signal, MockAnyCall(
754+ "%s/metadata/%s/" % (url, MD_VERSION),
755+ {
756+ 'consumer_secret': '',
757+ 'consumer_key': consumer_key,
758+ 'token_key': token_key,
759+ 'token_secret': token_secret,
760+ },
761+ 'WORKING',
762+ 'Finished %s [1/1]: 2' % script_name,
763+ exit_status=2,
764+ files={
765+ script_name: b'[Errno 0] Exec format error',
766+ '%s.err' % script_name: b'[Errno 0] Exec format error',
767+ },
768+ ))
769+
770 def test_refresh_signals_finished(self):
771 signal = self.patch(refresh, 'signal')
772 script_name = factory.make_name('script_name')