class WebApplicationService(StreamServerEndpointService): """Service encapsulating the Django web application. This shows a default "MAAS is starting" web page until Django is up. If Django cannot be started, the page is replaced by the error that caused start-up to fail. :ivar site: The site object that wraps a WSGI resource. :ivar threadpool: The thread-pool used for servicing requests to the web application. """ def __init__(self, port, listener, status_worker): self.port = port self.starting = False # Start with an empty `Resource`, `installApplication` will configure # the root resource. This must be seperated because Django must be # start from inside a thread with database access. self.site = OverlaySite( Resource(), logFormatter=reducedWebLogFormatter, timeout=None ) self.site.requestFactory = CleanPathRequest # `endpoint` is set in `privilegedStartService`, at this point the # `endpoint` is None. super(WebApplicationService, self).__init__(None, self.site) self.websocket = WebSocketFactory(listener) self.threadpool = ThreadPoolLimiter( reactor.threadpoolForDatabase, concurrency.webapp ) self.status_worker = status_worker def prepareApplication(self): """Return the WSGI application. If we run servers on multiple endpoints this ought to be extracted into a separate function, so that each server uses the same application. """ return WebApplicationHandler() def startWebsocket(self): """Start the websocket factory for the `WebSocketsResource`.""" self.websocket.startFactory() def installApplication(self, application): """Install the WSGI application into the Twisted site. It's installed as a child with path "MAAS". This matches the default front-end configuration (i.e. Apache) so that there's no need to force script names. """ # Setup resources to process paths that twisted handles. metadata = Resource() metadata.putChild(b"status", StatusHandlerResource(self.status_worker)) maas = Resource() maas.putChild(b"metadata", metadata) maas.putChild(b"static", File(settings.STATIC_ROOT)) maas.putChild( b"ws", WebSocketsResource(lookupProtocolForFactory(self.websocket)) ) root = Resource() root.putChild(b"", Redirect(b"MAAS/")) root.putChild(b"MAAS", maas) # Setup the resources to process paths that django handles. underlay_maas = ResourceOverlay( WSGIResource(reactor, self.threadpool, application) ) underlay_root = Resource() underlay_root.putChild(b"MAAS", underlay_maas) underlay_site = Site( underlay_root, logFormatter=reducedWebLogFormatter ) underlay_site.requestFactory = CleanPathRequest # Setup the main resource as the twisted handler and the underlay # resource as the django handler. self.site.resource = root self.site.underlay = underlay_site @inlineCallbacks def startApplication(self): """Start the Django application, and install it.""" application = yield deferToDatabase(self.prepareApplication) self.startWebsocket() self.installApplication(application) def _makeEndpoint(self): """Make the endpoint for the webapp.""" # Make a socket with SO_REUSEPORT set so that we can run multiple web # applications. This is easier to do from outside of Twisted as there's # not yet official support for setting socket options. s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) # N.B, using the IPv6 INADDR_ANY means that getpeername() returns # something like: ('::ffff:192.168.133.32', 40588, 0, 0) s.bind(("::", self.port)) # Use a backlog of 50, which seems to be fairly common. s.listen(50) # Adopt this socket into Twisted's reactor setting the endpoint. endpoint = AdoptedStreamServerEndpoint(reactor, s.fileno(), s.family) endpoint.port = self.port # Make it easy to get the port number. endpoint.socket = s # Prevent garbage collection. return endpoint @asynchronous(timeout=30) @inlineCallbacks def privilegedStartService(self): # Twisted will call `privilegedStartService` followed by `startService` # that also calls `privilegedStartService`. Since this method now # performs start-up work its possible that it will be called twice. # Then endpoint can only be created once or a bad file descriptor # error will occur. if self.starting: return self.starting = True # Start the application first before starting the service. This ensures # that the application is running correctly before any requests # can be handled. yield self.startApplication() # Create the endpoint now that the application is started. self.endpoint = self._makeEndpoint() # Start the service now that the endpoint has been created. super(WebApplicationService, self).privilegedStartService() @asynchronous(timeout=30) def stopService(self): def _cleanup(_): self.starting = False d = super(WebApplicationService, self).stopService() d.addCallback(lambda _: self.websocket.stopFactory()) d.addCallback(_cleanup) return d
class WebApplicationService(StreamServerEndpointService): """Service encapsulating the Django web application. This shows a default "MAAS is starting" web page until Django is up. If Django cannot be started, the page is replaced by the error that caused start-up to fail. :ivar site: The site object that wraps a WSGI resource. :ivar threadpool: The thread-pool used for servicing requests to the web application. """ def __init__(self, endpoint, listener, status_worker): self.site = OverlaySite(StartPage(), logFormatter=reducedWebLogFormatter) self.site.requestFactory = CleanPathRequest super(WebApplicationService, self).__init__(endpoint, self.site) self.websocket = WebSocketFactory(listener) self.threadpool = ThreadPoolLimiter(reactor.threadpoolForDatabase, concurrency.webapp) self.status_worker = status_worker def prepareApplication(self): """Return the WSGI application. If we run servers on multiple endpoints this ought to be extracted into a separate function, so that each server uses the same application. """ return WebApplicationHandler() def startWebsocket(self): """Start the websocket factory for the `WebSocketsResource`.""" self.websocket.startFactory() def installApplication(self, application): """Install the WSGI application into the Twisted site. It's installed as a child with path "MAAS". This matches the default front-end configuration (i.e. Apache) so that there's no need to force script names. """ # Setup resources to process paths that twisted handles. metadata = Resource() metadata.putChild(b'status', StatusHandlerResource(self.status_worker)) maas = Resource() maas.putChild(b'metadata', metadata) maas.putChild(b'static', File(settings.STATIC_ROOT)) maas.putChild( b'ws', WebSocketsResource(lookupProtocolForFactory(self.websocket))) root = Resource() root.putChild(b'', Redirect(b"MAAS/")) root.putChild(b'MAAS', maas) # Setup the resources to process paths that django handles. underlay_maas = ResourceOverlay( WSGIResource(reactor, self.threadpool, application)) underlay_root = Resource() underlay_root.putChild(b'MAAS', underlay_maas) underlay_site = Site(underlay_root, logFormatter=reducedWebLogFormatter) underlay_site.requestFactory = CleanPathRequest # Setup the main resource as the twisted handler and the underlay # resource as the django handler. self.site.resource = root self.site.underlay = underlay_site def installFailed(self, failure): """Display a page explaining why the web app could not start.""" self.site.resource = StartFailedPage(failure) log.err(failure, "MAAS web application failed to start") def startApplication(self): """Start the Django application, and install it.""" try: application = self.prepareApplication() self.startWebsocket() self.installApplication(application) except: self.installFailed(failure.Failure()) return defer.succeed(None) @asynchronous(timeout=30) def startService(self): super(WebApplicationService, self).startService() return self.startApplication() @asynchronous(timeout=30) def stopService(self): d = super(WebApplicationService, self).stopService() d.addCallback(lambda _: self.websocket.stopFactory()) return d