← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/maas/log-and-oops-services into lp:maas

 

Gavin Panella has proposed merging lp:~allenap/maas/log-and-oops-services into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/log-and-oops-services/+merge/91291

The getRotatableLogFileObserver() and setUpOOPSHandler() functions
could only be run once in a process, which makes them a little hard to
rest. So, I have changed them into Service()s, so they can be started
and stopped by Twisted, and by us in tests.

-- 
https://code.launchpad.net/~allenap/maas/log-and-oops-services/+merge/91291
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/log-and-oops-services into lp:maas.
=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py	2012-02-01 15:14:09 +0000
+++ src/provisioningserver/plugin.py	2012-02-02 15:19:18 +0000
@@ -8,75 +8,32 @@
     unicode_literals,
     )
 
-import signal
-import sys
+__metaclass__ = type
+__all__ = []
 
 from amqpclient import AMQFactory
-import oops
-from oops_datedir_repo import DateDirRepo
-from oops_twisted import (
-    Config as oops_config,
-    defer_publisher,
-    OOPSObserver,
+from provisioningserver.remote import Provisioning
+from provisioningserver.services import (
+    LogService,
+    OOPSService,
     )
 import setproctitle
-from twisted.application.internet import TCPClient
+from twisted.application.internet import (
+    TCPClient,
+    TCPServer,
+    )
 from twisted.application.service import (
     IServiceMaker,
     MultiService,
     )
-from twisted.internet import reactor
 from twisted.plugin import IPlugin
 from twisted.python import (
     log,
     usage,
     )
-from twisted.python.log import (
-    addObserver,
-    FileLogObserver,
-    )
-from twisted.python.logfile import LogFile
 from zope.interface import implements
 
 
-__metaclass__ = type
-__all__ = []
-
-
-def getRotatableLogFileObserver(filename):
-    """Setup a L{LogFile} for the given application."""
-    if filename != '-':
-        logfile = LogFile.fromFullPath(
-            filename, rotateLength=None, defaultMode=0644)
-
-        def signal_handler(sig, frame):
-            reactor.callFromThread(logfile.reopen)
-        signal.signal(signal.SIGUSR1, signal_handler)
-    else:
-        logfile = sys.stdout
-    return FileLogObserver(logfile)
-
-
-def setUpOOPSHandler(options, logfile):
-    """Add OOPS handling based on the passed command line options."""
-    config = oops_config()
-
-    # Add the oops publisher that writes files in the configured place
-    # if the command line option was set.
-
-    if options["oops-dir"]:
-        repo = DateDirRepo(options["oops-dir"])
-        config.publishers.append(
-            defer_publisher(oops.publish_new_only(repo.publish)))
-
-    if options["oops-reporter"]:
-        config.template['reporter'] = options["oops-reporter"]
-
-    observer = OOPSObserver(config, logfile.emit)
-    addObserver(observer.emit)
-    return observer
-
-
 class Options(usage.Options):
     """Command line options for the provisioning server."""
 
@@ -127,11 +84,15 @@
         if _set_proc_title:
             setproctitle.setproctitle("maas provisioning service")
 
-        logfile = getRotatableLogFileObserver(options["logfile"])
-        setUpOOPSHandler(options, logfile)
-
         services = MultiService()
 
+        logging_service = LogService(options["logfile"])
+        logging_service.setServiceParent(services)
+
+        oops_service = OOPSService(
+            logging_service, options["oops-dir"], options["oops-reporter"])
+        oops_service.setServiceParent(services)
+
         broker_port = options["brokerport"]
         broker_host = options["brokerhost"]
         broker_user = options["brokeruser"]
@@ -150,6 +111,7 @@
                 cb_connected, cb_disconnected, cb_failed)
             client_service = TCPClient(
                 broker_host, broker_port, client_factory)
-            services.addService(client_service)
+            client_service.setName("amqp")
+            client_service.setServiceParent(services)
 
         return services

=== added file 'src/provisioningserver/services.py'
--- src/provisioningserver/services.py	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/services.py	2012-02-02 15:19:18 +0000
@@ -0,0 +1,104 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""..."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = [
+    "LogService",
+    "OOPSService",
+    ]
+
+import signal
+import sys
+
+import oops
+from oops_datedir_repo import DateDirRepo
+from oops_twisted import (
+    Config as oops_config,
+    defer_publisher,
+    OOPSObserver,
+    )
+from twisted.application.service import Service
+from twisted.internet import reactor
+from twisted.python.log import (
+    addObserver,
+    FileLogObserver,
+    removeObserver,
+    )
+from twisted.python.logfile import LogFile
+
+
+class LogService(Service):
+
+    name = "log"
+
+    def __init__(self, filename):
+        self.filename = filename
+        self.logfile = None
+        self.observer = None
+
+    def _signal_handler(self, sig, frame):
+        reactor.callFromThread(self.logfile.reopen)
+
+    def startService(self):
+        Service.startService(self)
+        if self.filename != '-':
+            self.logfile = LogFile.fromFullPath(
+                self.filename, rotateLength=None, defaultMode=0644)
+            assert signal.getsignal(signal.SIGUSR1) is signal.SIG_DFL, (
+                "A signal handler is already installed for SIGUSR1.")
+            signal.signal(signal.SIGUSR1, self._signal_handler)
+        else:
+            self.logfile = sys.stdout
+        self.observer = FileLogObserver(self.logfile)
+        self.observer.start()
+
+    def stopService(self):
+        Service.stopService(self)
+        # Must use == here; the handler returned from getsignal() is not the
+        # same object as self._signal_handler, even though im_class, im_func,
+        # and im_self *are* all identical. Don't know why this should be.
+        if signal.getsignal(signal.SIGUSR1) == self._signal_handler:
+            signal.signal(signal.SIGUSR1, signal.SIG_DFL)
+        self.observer.stop()
+        self.observer = None
+        self.logfile.close()
+        self.logfile = None
+
+
+class OOPSService(Service):
+
+    name = "oops"
+
+    def __init__(self, log_service, oops_dir, oops_reporter):
+        self.config = None
+        self.log_service = log_service
+        self.oops_dir = oops_dir
+        self.oops_reporter = oops_reporter
+
+    def startService(self):
+        Service.startService(self)
+        self.config = oops_config()
+        # Add the oops publisher that writes files in the configured place if
+        # the command line option was set.
+        if self.oops_dir:
+            repo = DateDirRepo(self.oops_dir)
+            self.config.publishers.append(
+                defer_publisher(oops.publish_new_only(repo.publish)))
+        if self.oops_reporter:
+            self.config.template['reporter'] = self.oops_reporter
+        self.observer = OOPSObserver(
+            self.config, self.log_service.observer.emit)
+        addObserver(self.observer.emit)
+
+    def stopService(self):
+        Service.stopService(self)
+        removeObserver(self.observer.emit)
+        self.observer = None
+        self.config = None

=== modified file 'src/provisioningserver/tests/test_plugin.py'
--- src/provisioningserver/tests/test_plugin.py	2012-02-01 15:24:22 +0000
+++ src/provisioningserver/tests/test_plugin.py	2012-02-02 15:19:18 +0000
@@ -11,32 +11,21 @@
 __metaclass__ = type
 __all__ = []
 
-from cStringIO import StringIO
 from functools import partial
 import os
 from unittest import skip
 
 from fixtures import TempDir
-from oops_twisted import OOPSObserver
 from provisioningserver.plugin import (
     Options,
     ProvisioningServiceMaker,
-    setUpOOPSHandler,
     )
 from testtools import TestCase
-from testtools.content import (
-    Content,
-    UTF8_TEXT,
-    )
 from testtools.matchers import (
     MatchesException,
     Raises,
     )
 from twisted.application.service import MultiService
-from twisted.python.log import (
-    FileLogObserver,
-    theLogPublisher,
-    )
 from twisted.python.usage import UsageError
 
 
@@ -85,10 +74,7 @@
     def test_parse_minimal_options(self):
         options = Options()
         # The minimal set of options that must be provided.
-        arguments = [
-            "--brokerpassword", "Hoskins",
-            "--brokeruser", "Bob",
-            ]
+        arguments = []
         options.parseOptions(arguments)  # No error.
 
     def test_parse_int_options(self):
@@ -129,72 +115,53 @@
             Raises(expected))
 
 
-class TestSetUpOOPSHandler(TestCase):
-    """Tests for `provisioningserver.plugin.setUpOOPSHandler`."""
-
-    def setUp(self):
-        super(TestSetUpOOPSHandler, self).setUp()
-        self.observers = theLogPublisher.observers[:]
-        self.logfile = StringIO()
-        self.addDetail("log", Content(UTF8_TEXT, self.logfile.getvalue))
-        self.log = FileLogObserver(self.logfile)
-
-    def tearDown(self):
-        super(TestSetUpOOPSHandler, self).tearDown()
-        theLogPublisher.observers[:] = self.observers
-
-    def makeObserver(self, settings):
-        options = Options()
-        options["brokerpassword"] = "Hoskins"
-        options["brokeruser"] = "Bob"
-        options.update(settings)
-        observer = setUpOOPSHandler(options, self.log)
-        return options, observer
-
-    def test_minimal(self):
-        options, observer = self.makeObserver({})
-        self.assertIsInstance(observer, OOPSObserver)
-        self.assertEqual([], observer.config.publishers)
-        self.assertEqual(
-            {"reporter": options.defaults["oops-reporter"]},
-            observer.config.template)
-
-    def test_with_all_params(self):
-        settings = {
-            "oops-reporter": "Sidebottom",
-            "oops-dir": self.useFixture(TempDir()).path,
-            }
-        options, observer = self.makeObserver(settings)
-        self.assertIsInstance(observer, OOPSObserver)
-        self.assertEqual(1, len(observer.config.publishers))
-        self.assertEqual(
-            {"reporter": "Sidebottom"},
-            observer.config.template)
-
-
 class TestProvisioningServiceMaker(TestCase):
     """Tests for `provisioningserver.plugin.ProvisioningServiceMaker`."""
 
+    def get_log_file(self):
+        return os.path.join(
+            self.useFixture(TempDir()).path,
+            "provisioningserver.log")
+
     def test_init(self):
         service_maker = ProvisioningServiceMaker("Harry", "Hill")
         self.assertEqual("Harry", service_maker.tapname)
         self.assertEqual("Hill", service_maker.description)
 
-    def makeOptions(self, settings):
-        options = Options()
-        options["brokerpassword"] = "Hoskins"
-        options["brokeruser"] = "Bob"
-        options.update(settings)
-        return options
-
     def test_makeService(self):
-        logfile = os.path.join(
-            self.useFixture(TempDir()).path, "provisioningserver.log")
-        options = self.makeOptions({"logfile": logfile})
-        service_maker = ProvisioningServiceMaker("Harry", "Hill")
-        service = service_maker.makeService(options, _set_proc_title=False)
-        self.assertIsInstance(service, MultiService)
-        self.assertEqual(1, len(service.services))
-        [client_service] = service.services
-        self.assertEqual(options["brokerhost"], client_service.args[0])
-        self.assertEqual(options["brokerport"], client_service.args[1])
+        """
+        Only the log and oops services are created when no options are given.
+        """
+        options = Options()
+        options["logfile"] = self.get_log_file()
+        service_maker = ProvisioningServiceMaker("Harry", "Hill")
+        service = service_maker.makeService(options, _set_proc_title=False)
+        self.assertIsInstance(service, MultiService)
+        self.assertEqual(
+            ["log", "oops"],
+            sorted(service.namedServices))
+        self.assertEqual(
+            len(service.namedServices), len(service.services),
+            "Not all services are named.")
+
+    def test_makeService_with_broker(self):
+        """
+        The log, oops and amqp services are created when no the broker user
+        and password options are given.
+        """
+        options = Options()
+        options["brokerpassword"] = "Hoskins"
+        options["brokeruser"] = "Bob"
+        options["logfile"] = self.get_log_file()
+        service_maker = ProvisioningServiceMaker("Harry", "Hill")
+        service = service_maker.makeService(options, _set_proc_title=False)
+        self.assertIsInstance(service, MultiService)
+        self.assertEqual(
+            ["amqp", "log", "oops"],
+            sorted(service.namedServices))
+        self.assertEqual(
+            len(service.namedServices), len(service.services),
+            "Not all services are named.")
+        amqp_client_service = service.getServiceNamed("amqp")
+        self.assertEqual(options["brokerhost"], amqp_client_service.args[0])
+        self.assertEqual(options["brokerport"], amqp_client_service.args[1])

=== added file 'src/provisioningserver/tests/test_services.py'
--- src/provisioningserver/tests/test_services.py	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/tests/test_services.py	2012-02-02 15:19:18 +0000
@@ -0,0 +1,120 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `provisioningserver.services`."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+import os
+import signal
+import sys
+
+from fixtures import TempDir
+from oops_twisted import OOPSObserver
+from provisioningserver.services import (
+    LogService,
+    OOPSService,
+    )
+from testtools import TestCase
+from testtools.content import content_from_file
+from testtools.deferredruntest import AsynchronousDeferredRunTest
+from twisted.application.service import MultiService
+from twisted.python.log import (
+    FileLogObserver,
+    theLogPublisher,
+    )
+from twisted.python.logfile import LogFile
+
+
+class TestServicesBase:
+
+    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
+
+    def setUp(self):
+        super(TestServicesBase, self).setUp()
+        self.observers = theLogPublisher.observers[:]
+        self.services = MultiService()
+        self.services.privilegedStartService()
+        self.services.startService()
+
+    def tearDown(self):
+        super(TestServicesBase, self).tearDown()
+        d = self.services.stopService()
+        # The log file must be read in right after services have stopped,
+        # before the temporary directory where the log lives is removed.
+        d.addBoth(lambda ignore: self.addDetailFromLog())
+        d.addBoth(lambda ignore: self.assertNoObserversLeftBehind())
+        return d
+
+    def addDetailFromLog(self):
+        content = content_from_file(self.log_filename, buffer_now=True)
+        self.addDetail("log", content)
+
+    def assertNoObserversLeftBehind(self):
+        self.assertEqual(self.observers, theLogPublisher.observers)
+
+
+class TestLogService(TestServicesBase, TestCase):
+    """Tests for `provisioningserver.services.LogService`."""
+
+    def test_log_to_stdout(self):
+        log_service = LogService("-")
+        log_service.setServiceParent(self.services)
+        self.assertIsInstance(log_service.observer, FileLogObserver)
+        self.assertEqual("-", log_service.filename)
+        self.assertEqual(sys.stdout, log_service.logfile)
+        # The SIGUSR1 signal handler is untouched.
+        self.assertEqual(
+            signal.getsignal(signal.SIGUSR1),
+            signal.SIG_DFL)
+
+    def test_log_to_file(self):
+        tempdir = self.useFixture(TempDir()).path
+        log_filename = os.path.join(tempdir, "test.log")
+        log_service = LogService(log_filename)
+        log_service.setServiceParent(self.services)
+        self.assertIsInstance(log_service.observer, FileLogObserver)
+        self.assertEqual(log_filename, log_service.filename)
+        self.assertIsInstance(log_service.logfile, LogFile)
+        self.assertEqual(log_filename, log_service.logfile.path)
+        # The SIGUSR1 signal handler is set.
+        self.assertEqual(
+            signal.getsignal(signal.SIGUSR1),
+            log_service._signal_handler)
+
+
+class TestOOPSService(TestServicesBase, TestCase):
+    """Tests for `provisioningserver.services.OOPSService`."""
+
+    def setUp(self):
+        super(TestOOPSService, self).setUp()
+        # OOPSService relies upon LogService.
+        self.tempdir = self.useFixture(TempDir()).path
+        self.log_filename = os.path.join(self.tempdir, "test.log")
+        self.log_service = LogService(self.log_filename)
+        self.log_service.setServiceParent(self.services)
+
+    def test_minimal(self):
+        oops_service = OOPSService(self.log_service, None, None)
+        oops_service.setServiceParent(self.services)
+        observer = oops_service.observer
+        self.assertIsInstance(observer, OOPSObserver)
+        self.assertEqual([], observer.config.publishers)
+        self.assertEqual({}, observer.config.template)
+
+    def test_with_all_params(self):
+        oops_dir = os.path.join(self.tempdir, "oops")
+        oops_service = OOPSService(self.log_service, oops_dir, "Sidebottom")
+        oops_service.setServiceParent(self.services)
+        observer = oops_service.observer
+        self.assertIsInstance(observer, OOPSObserver)
+        self.assertEqual(1, len(observer.config.publishers))
+        self.assertEqual(
+            {"reporter": "Sidebottom"},
+            observer.config.template)