Merge lp:~julian-edwards/maas/rndc-crash-bug-1386488 into lp:~maas-committers/maas/trunk
- rndc-crash-bug-1386488
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | Approve | ||
Review via email: mp+239941@code.launchpad.net |
Commit message
Catch ExternalProcess
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.
Julian Edwards (julian-edwards) wrote : | # |
Julian Edwards (julian-edwards) wrote : | # |
Right, fixed, please to be reviewing.
Graham Binns (gmb) wrote : | # |
Looks good to me. Rambly thought inline, but no changes needed.
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/
> 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!
MAAS Lander (maas-lander) wrote : | # |
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://
Ign http://
Get:1 http://
Ign http://
Get:2 http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Hit http://
Hit http://
Get:5 http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:6 http://
Get:7 http://
Get:8 http://
Get:9 http://
Get:10 http://
Hit http://
Get:11 http://
Hit http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,171 kB in 2s (410 kB/s)
Reading package lists...
sudo DEBIAN_
--
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.
MAAS Lander (maas-lander) wrote : | # |
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://
Get:1 http://
Ign http://
Get:2 http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Get:7 http://
Get:8 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,171 kB in 2s (407 kB/s)
Reading package lists...
sudo DEBIAN_
--
Julian Edwards (julian-edwards) wrote : | # |
Lint, pointless fscking makework since 1984.
Preview Diff
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 |
Marking WIP, something is wrong with the diff.