Merge lp:~julian-edwards/maas/rndc-crash-bug-1386488 into lp:~maas-committers/maas/trunk

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: no longer in the source branch.
Merged at revision: 3313
Proposed branch: lp:~julian-edwards/maas/rndc-crash-bug-1386488
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 207 lines (+56/-14)
4 files modified
src/maasserver/middleware.py (+16/-0)
src/maasserver/node_action.py (+7/-6)
src/maasserver/tests/test_middleware.py (+23/-5)
src/maasserver/tests/test_node_action.py (+10/-3)
To merge this branch: bzr merge lp:~julian-edwards/maas/rndc-crash-bug-1386488
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+239941@code.launchpad.net

Commit message

Catch ExternalProcessError from rndc_command (and anywhere else that might raise it in the future) when dealing with node actions in the web UI and API. The API now returns a SERVICE_UNAVAILABLE instead of a 500 with a stacktrace in the appserver log.

Description of the change

I've done this with a middleware change for the API and followed the example of the RPC exceptions in the node actions and extended its concept to be a little more general, in particular the test.

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

Marking WIP, something is wrong with the diff.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Right, fixed, please to be reviewing.

Revision history for this message
Graham Binns (gmb) wrote :

Looks good to me. Rambly thought inline, but no changes needed.

review: Approve
Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Wednesday 29 Oct 2014 07:36:57 you wrote:
> Ultimately, when NodeActionErrors are caught (in the form from which the
> action originated), the error's message is added to the UI as a warning.
> In this case, you might see something like:
>
> "Command `rndc -c /etc/bind/maas/rndc.conf.maas reload maas` returned
> non-zero exit status 1: rndc: 'reload' failed: dynamic zone"
>
> In the UI.
>
> Actually, that's not so terrible (in that it can then be reported to an
> admin, or the admin can go and fix it… I wonder if we shouldn't be
> sanitising some of those kind of messages for non-admins, though. Not,
> however, a concern for this branch; just a rambly 7:30am thought.

I think that given the likelihood of this error ever happening I don't mind
dumping the whole thing there. If a user sees it, they'll just grab an
operator/admin and report what they see, which is actually very useful.

Cheers, thanks for the review!

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (19.1 KiB)

The attempt to merge lp:~julian-edwards/maas/rndc-crash-bug-1386488 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:2 http://security.ubuntu.com trusty-security Release [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Get:5 http://security.ubuntu.com trusty-security/main Sources [48.3 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:6 http://security.ubuntu.com trusty-security/universe Sources [11.2 kB]
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [151 kB]
Get:8 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [133 kB]
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [89.3 kB]
Get:10 http://security.ubuntu.com trusty-security/universe amd64 Packages [50.4 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [351 kB]
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [216 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,171 kB in 2s (410 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi ...

Revision history for this message
MAAS Lander (maas-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (19.1 KiB)

The attempt to merge lp:~julian-edwards/maas/rndc-crash-bug-1386488 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:2 http://security.ubuntu.com trusty-security Release [59.7 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [59.7 kB]
Get:5 http://security.ubuntu.com trusty-security/main Sources [48.3 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:6 http://security.ubuntu.com trusty-security/universe Sources [11.2 kB]
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [151 kB]
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [50.4 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [133 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [89.3 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [351 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [216 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,171 kB in 2s (407 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi ...

Revision history for this message
Julian Edwards (julian-edwards) wrote :

Lint, pointless fscking makework since 1984.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/middleware.py'
2--- src/maasserver/middleware.py 2014-10-28 20:55:30 +0000
3+++ src/maasserver/middleware.py 2014-10-30 05:37:05 +0000
4@@ -60,6 +60,7 @@
5 NoConnectionsAvailable,
6 PowerActionAlreadyInProgress,
7 )
8+from provisioningserver.utils.shell import ExternalProcessError
9
10
11 try:
12@@ -238,6 +239,21 @@
13 return HttpResponseForbidden(
14 content=unicode(exception).encode(encoding),
15 mimetype=b"text/plain; charset=%s" % encoding)
16+ elif isinstance(exception, ExternalProcessError):
17+ # Catch problems interacting with processes that the
18+ # appserver spawns, e.g. rndc.
19+ #
20+ # While this is a serious error, it should be a temporary
21+ # one as the admin should be checking and fixing, or it
22+ # could be spurious. There's no way of knowing, so the best
23+ # course of action is to ask the caller to repeat.
24+ response = HttpResponse(
25+ content=unicode(exception).encode(encoding),
26+ status=httplib.SERVICE_UNAVAILABLE,
27+ mimetype=b"text/plain; charset=%s" % encoding)
28+ response['Retry-After'] = (
29+ self.RETRY_AFTER_SERVICE_UNAVAILABLE)
30+ return response
31 else:
32 # Print a traceback.
33 self.log_exception(exception)
34
35=== modified file 'src/maasserver/node_action.py'
36--- src/maasserver/node_action.py 2014-10-23 05:52:37 +0000
37+++ src/maasserver/node_action.py 2014-10-30 05:37:05 +0000
38@@ -55,6 +55,7 @@
39 PowerActionAlreadyInProgress,
40 )
41 from provisioningserver.utils.enum import map_enum
42+from provisioningserver.utils.shell import ExternalProcessError
43
44 # All node statuses.
45 ALL_STATUSES = set(NODE_STATUS_CHOICES_DICT.keys())
46@@ -200,7 +201,7 @@
47 """See `NodeAction.execute`."""
48 try:
49 self.node.start_commissioning(self.user)
50- except RPC_EXCEPTIONS as exception:
51+ except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
52 raise NodeActionError(exception)
53 else:
54 return "Node commissioning started."
55@@ -219,7 +220,7 @@
56 """See `NodeAction.execute`."""
57 try:
58 self.node.abort_commissioning(self.user)
59- except RPC_EXCEPTIONS as exception:
60+ except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
61 raise NodeActionError(exception)
62 else:
63 return "Node commissioning aborted."
64@@ -238,7 +239,7 @@
65 """See `NodeAction.execute`."""
66 try:
67 self.node.abort_operation(self.user)
68- except RPC_EXCEPTIONS as exception:
69+ except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
70 raise NodeActionError(exception)
71 else:
72 return "Node operation aborted."
73@@ -347,7 +348,7 @@
74 raise NodeActionError(
75 "%s: Failed to start, static IP addresses are exhausted."
76 % self.node.hostname)
77- except RPC_EXCEPTIONS as exception:
78+ except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
79 raise NodeActionError(exception)
80 else:
81 return "This node has been asked to start up."
82@@ -376,7 +377,7 @@
83 """See `NodeAction.execute`."""
84 try:
85 self.node.stop(self.user)
86- except RPC_EXCEPTIONS as exception:
87+ except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
88 raise NodeActionError(exception)
89 else:
90 return "This node has been asked to shut down."
91@@ -398,7 +399,7 @@
92 """See `NodeAction.execute`."""
93 try:
94 self.node.release_or_erase()
95- except RPC_EXCEPTIONS as exception:
96+ except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
97 raise NodeActionError(exception)
98 else:
99 return "This node is no longer allocated to you."
100
101=== modified file 'src/maasserver/tests/test_middleware.py'
102--- src/maasserver/tests/test_middleware.py 2014-10-28 20:55:30 +0000
103+++ src/maasserver/tests/test_middleware.py 2014-10-30 05:37:05 +0000
104@@ -67,9 +67,11 @@
105 NoConnectionsAvailable,
106 PowerActionAlreadyInProgress,
107 )
108+from provisioningserver.utils.shell import ExternalProcessError
109 from provisioningserver.utils.text import normalise_whitespace
110 from testtools.matchers import (
111 Contains,
112+ Equals,
113 Not,
114 )
115 from twisted.python.failure import Failure
116@@ -81,23 +83,29 @@
117 """Return a path to handle exceptions for."""
118 return "/%s" % factory.make_string()
119
120- def make_middleware(self, base_path):
121+ def make_middleware(self, base_path, retry_after=None):
122 """Create an ExceptionMiddleware for base_path."""
123 class TestingExceptionMiddleware(ExceptionMiddleware):
124 path_regex = base_path
125
126- return TestingExceptionMiddleware()
127-
128- def process_exception(self, exception):
129+ testing_middleware = TestingExceptionMiddleware()
130+ if retry_after is not None:
131+ testing_middleware.RETRY_AFTER_SERVICE_UNAVAILABLE = retry_after
132+
133+ return testing_middleware
134+
135+ def process_exception(self, exception, retry_after=None):
136 """Run a given exception through a fake ExceptionMiddleware.
137
138 :param exception: The exception to simulate.
139 :type exception: Exception
140+ :param retry_after: Value of the RETRY_AFTER_SERVICE_UNAVAILABLE to
141+ use in the fake middleware.
142 :return: The response as returned by the ExceptionMiddleware.
143 :rtype: HttpResponse or None.
144 """
145 base_path = self.make_base_path()
146- middleware = self.make_middleware(base_path)
147+ middleware = self.make_middleware(base_path, retry_after)
148 request = factory.make_fake_request(base_path)
149 return middleware.process_exception(request, exception)
150
151@@ -170,6 +178,16 @@
152 self.process_exception(Exception(error_text))
153 self.assertThat(logger.output, Contains(error_text))
154
155+ def test_reports_ExternalProcessError_as_ServiceUnavailable(self):
156+ error_text = factory.make_string()
157+ exception = ExternalProcessError(1, ["cmd"], error_text)
158+ retry_after = random.randint(0, 10)
159+ response = self.process_exception(exception, retry_after)
160+ self.expectThat(
161+ response.status_code, Equals(httplib.SERVICE_UNAVAILABLE))
162+ self.expectThat(response.content, Equals(unicode(exception)))
163+ self.expectThat(response['Retry-After'], Equals("%s" % retry_after))
164+
165
166 class APIErrorsMiddlewareTest(MAASServerTestCase):
167
168
169=== modified file 'src/maasserver/tests/test_node_action.py'
170--- src/maasserver/tests/test_node_action.py 2014-10-23 05:55:44 +0000
171+++ src/maasserver/tests/test_node_action.py 2014-10-30 05:37:05 +0000
172@@ -57,6 +57,7 @@
173 from maastesting.matchers import MockCalledOnceWith
174 from mock import ANY
175 from provisioningserver.rpc.exceptions import MultipleFailures
176+from provisioningserver.utils.shell import ExternalProcessError
177 from testtools.matchers import Equals
178 from twisted.python.failure import Failure
179
180@@ -561,18 +562,24 @@
181 self.assertItemsEqual([], actions)
182
183
184-class TestRPCActionsErrorHandling(MAASServerTestCase):
185- """Tests for error handling in actions that need RPC."""
186+class TestActionsErrorHandling(MAASServerTestCase):
187+ """Tests for error handling in actions.
188
189+ This covers RPC exceptions and `ExternalProcessError`s.
190+ """
191+ exceptions = RPC_EXCEPTIONS + (ExternalProcessError,)
192 scenarios = [
193 (exception_class.__name__, {"exception_class": exception_class})
194- for exception_class in RPC_EXCEPTIONS
195+ for exception_class in exceptions
196 ]
197
198 def make_exception(self):
199 if self.exception_class is MultipleFailures:
200 exception = self.exception_class(
201 Failure(Exception(factory.make_name("exception"))))
202+ elif self.exception_class is ExternalProcessError:
203+ exception = self.exception_class(
204+ 1, ["cmd"], factory.make_name("exception"))
205 else:
206 exception = self.exception_class(factory.make_name("exception"))
207 return exception