Merge ~blake-rouse/maas:revert-fix-1789315 into maas:master

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: 21b957c1336b04ece39cd605fda962589c2b4c01
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~blake-rouse/maas:revert-fix-1789315
Merge into: maas:master
Diff against target: 212 lines (+111/-12)
2 files modified
src/maasserver/tests/test_webapp.py (+73/-0)
src/maasserver/webapp.py (+38/-12)
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+354726@code.launchpad.net

Commit message

Revert 868d6ce2c7be21babc2afb04c5828512206f9a5f.

"Fix LP: #1789315 - Change webapp start-up to start the django application before opening the socket. Remove the start-up and failure pages."

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

lgtm, please reference the commit rev on the new commit message.

Revision history for this message
Andres Rodriguez (andreserl) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/tests/test_webapp.py b/src/maasserver/tests/test_webapp.py
2index 6edf28a..bbf11bd 100644
3--- a/src/maasserver/tests/test_webapp.py
4+++ b/src/maasserver/tests/test_webapp.py
5@@ -7,9 +7,11 @@
6 __all__ = []
7
8 import random
9+from textwrap import dedent
10 from unittest.mock import sentinel
11
12 from django.core.handlers.wsgi import WSGIHandler
13+from lxml import html
14 from maasserver import (
15 eventloop,
16 webapp,
17@@ -20,8 +22,10 @@ from maasserver.websockets.protocol import WebSocketFactory
18 from maastesting.factory import factory
19 from maastesting.matchers import MockCalledOnceWith
20 from maastesting.testcase import MAASTestCase
21+from maastesting.twisted import TwistedLoggerFixture
22 from provisioningserver.utils.twisted import reducedWebLogFormatter
23 from testtools.matchers import (
24+ Equals,
25 Is,
26 IsInstance,
27 MatchesStructure,
28@@ -209,6 +213,25 @@ class TestWebApplicationService(MAASTestCase):
29 ))
30 self.assertThat(service.websocket, IsInstance(WebSocketFactory))
31
32+ def test__default_site_renders_starting_page(self):
33+ service = self.make_webapp()
34+ request = DummyRequest("any/where".split("/"))
35+ resource = service.site.getResourceFor(request)
36+ content = resource.render(request)
37+ page = html.fromstring(content)
38+ self.expectThat(
39+ page.find(".//title").text_content(),
40+ Equals("503 - MAAS is starting"))
41+ self.expectThat(
42+ page.find(".//h1").text_content(),
43+ Equals("MAAS is starting"))
44+ self.expectThat(
45+ page.find(".//p").text_content(),
46+ Equals("Please try again in a few seconds."))
47+ self.assertEqual(
48+ ['5'],
49+ request.responseHeaders.getRawHeaders("retry-after"))
50+
51 def test__startService_starts_application(self):
52 service = self.make_webapp()
53 self.addCleanup(service.stopService)
54@@ -217,6 +240,56 @@ class TestWebApplicationService(MAASTestCase):
55
56 self.assertTrue(service.running)
57
58+ def test__error_when_starting_is_logged(self):
59+ service = self.make_webapp()
60+ self.addCleanup(service.stopService)
61+
62+ mock_prepare = self.patch_autospec(service, "prepareApplication")
63+ mock_prepare.side_effect = factory.make_exception()
64+
65+ # The failure is logged.
66+ with TwistedLoggerFixture() as logger:
67+ service.startService()
68+
69+ self.assertDocTestMatches(
70+ dedent("""\
71+ MAAS web application failed to start
72+ Traceback (most recent call last):
73+ ...
74+ maastesting.factory.TestException#...
75+ """),
76+ logger.output)
77+
78+ def test__error_when_starting_changes_page_to_error(self):
79+ service = self.make_webapp()
80+ self.addCleanup(service.stopService)
81+
82+ mock_prepare = self.patch_autospec(service, "prepareApplication")
83+ mock_prepare.side_effect = factory.make_exception()
84+
85+ # No error is returned.
86+ with TwistedLoggerFixture():
87+ service.startService()
88+
89+ # The site's page (for any path) shows the error.
90+ request = DummyRequest("any/where".split("/"))
91+ resource = service.site.getResourceFor(request)
92+ content = resource.render(request)
93+ page = html.fromstring(content)
94+ self.expectThat(
95+ page.find(".//title").text_content(),
96+ Equals("503 - MAAS failed to start"))
97+ self.expectThat(
98+ page.find(".//h1").text_content(),
99+ Equals("MAAS failed to start"))
100+ self.assertDocTestMatches(
101+ dedent("""\
102+ Traceback (most recent call last):
103+ ...
104+ maastesting.factory.TestException#...
105+ """),
106+ page.find(".//pre").text_content())
107+
108 def test__successful_start_installs_wsgi_resource(self):
109 service = self.make_webapp()
110 self.addCleanup(service.stopService)
111diff --git a/src/maasserver/webapp.py b/src/maasserver/webapp.py
112index 04a5b83..ae57a20 100644
113--- a/src/maasserver/webapp.py
114+++ b/src/maasserver/webapp.py
115@@ -9,9 +9,11 @@ __all__ = [
116
117 import copy
118 from functools import partial
119+from http.client import SERVICE_UNAVAILABLE
120 import re
121
122 from django.conf import settings
123+from lxml import html
124 from maasserver import concurrency
125 from maasserver.utils.threads import deferToDatabase
126 from maasserver.utils.views import WebApplicationHandler
127@@ -30,8 +32,10 @@ from provisioningserver.utils.twisted import (
128 from twisted.application.internet import StreamServerEndpointService
129 from twisted.internet import reactor
130 from twisted.internet.defer import inlineCallbacks
131+from twisted.python import failure
132 from twisted.web.error import UnsupportedMethod
133 from twisted.web.resource import (
134+ ErrorPage,
135 NoResource,
136 Resource,
137 )
138@@ -47,6 +51,27 @@ from twisted.web.wsgi import WSGIResource
139 log = LegacyLogger()
140
141
142+class StartPage(ErrorPage, object):
143+ def __init__(self):
144+ super(StartPage, self).__init__(
145+ status=int(SERVICE_UNAVAILABLE), brief="MAAS is starting",
146+ detail="Please try again in a few seconds.")
147+
148+ def render(self, request):
149+ request.setHeader(b"Retry-After", b"5")
150+ return super(StartPage, self).render(request)
151+
152+
153+class StartFailedPage(ErrorPage, object):
154+
155+ def __init__(self, failure):
156+ traceback = html.Element("pre")
157+ traceback.text = failure.getTraceback()
158+ super(StartFailedPage, self).__init__(
159+ status=int(SERVICE_UNAVAILABLE), brief="MAAS failed to start",
160+ detail=html.tostring(traceback, encoding=str))
161+
162+
163 class CleanPathRequest(Request, object):
164 """A request that supports '/+' in the path.
165
166@@ -159,11 +184,8 @@ class WebApplicationService(StreamServerEndpointService):
167 """
168
169 def __init__(self, endpoint, listener, status_worker):
170- # Start with an empty `Resource`, `installApplication` will configure
171- # the root resource. This must be seperated because Django must be
172- # start from inside a thread with database access.
173 self.site = OverlaySite(
174- Resource(), logFormatter=reducedWebLogFormatter, timeout=None)
175+ StartPage(), logFormatter=reducedWebLogFormatter, timeout=None)
176 self.site.requestFactory = CleanPathRequest
177 super(WebApplicationService, self).__init__(endpoint, self.site)
178 self.websocket = WebSocketFactory(listener)
179@@ -220,21 +242,25 @@ class WebApplicationService(StreamServerEndpointService):
180 self.site.resource = root
181 self.site.underlay = underlay_site
182
183+ def installFailed(self, failure):
184+ """Display a page explaining why the web app could not start."""
185+ self.site.resource = StartFailedPage(failure)
186+ log.err(failure, "MAAS web application failed to start")
187+
188 @inlineCallbacks
189 def startApplication(self):
190 """Start the Django application, and install it."""
191- application = yield deferToDatabase(self.prepareApplication)
192- self.startWebsocket()
193- self.installApplication(application)
194+ try:
195+ application = yield deferToDatabase(self.prepareApplication)
196+ self.startWebsocket()
197+ self.installApplication(application)
198+ except:
199+ self.installFailed(failure.Failure())
200
201 @asynchronous(timeout=30)
202- @inlineCallbacks
203 def startService(self):
204- # Start the application first before starting the service. This ensures
205- # that the application is running correctly before any requests
206- # can be handled.
207- yield self.startApplication()
208 super(WebApplicationService, self).startService()
209+ return self.startApplication()
210
211 @asynchronous(timeout=30)
212 def stopService(self):

Subscribers

People subscribed via source and target branches