← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/maas/pserv-config-file into lp:maas

 

Gavin Panella has proposed merging lp:~allenap/maas/pserv-config-file into lp:maas with lp:~allenap/maas/disable-rabbit-in-pserv as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/pserv-config-file/+merge/93256

This cleans up some stuff - like purging setproctitle from the project - and offloads configuration validation to formencode.
-- 
https://code.launchpad.net/~allenap/maas/pserv-config-file/+merge/93256
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/pserv-config-file into lp:maas.
=== modified file 'Makefile'
--- Makefile	2012-02-14 14:35:07 +0000
+++ Makefile	2012-02-15 17:33:31 +0000
@@ -77,7 +77,7 @@
 	$(RM) -r docs/_build/
 
 pserv.pid: bin/twistd.pserv
-	bin/twistd.pserv --pidfile=$@ maas-pserv --port=8001
+	bin/twistd.pserv --pidfile=$@ maas-pserv --config-file=etc/pserv.yaml
 
 pserv-start: pserv.pid
 

=== modified file 'buildout.cfg'
--- buildout.cfg	2012-02-15 10:58:09 +0000
+++ buildout.cfg	2012-02-15 17:33:31 +0000
@@ -68,9 +68,10 @@
 [pserv]
 recipe = zc.recipe.egg
 eggs =
+  formencode
   oops-datedir-repo
   oops-twisted
-  setproctitle
+  pyyaml
   twisted
   txamqp
 entry-points =

=== added directory 'etc'
=== added file 'etc/pserv.yaml'
--- etc/pserv.yaml	1970-01-01 00:00:00 +0000
+++ etc/pserv.yaml	2012-02-15 17:33:31 +0000
@@ -0,0 +1,20 @@
+# Provisioning Server (pserv) configuration.
+
+# The port on which the Provisioning API will be made available.
+#port: 8001
+# Where to log. This log can be rotated by sending SIGUSR1 to the
+# running server.
+logfile: "pserv.log"
+
+# OOPS configuration (optional).
+# oops:
+#   directory: "/tmp/killed/by"
+#   reporter: "death"
+
+# Message broker configuration (optional, not currently used).
+# broker:
+#   host: "localhost"
+#   port: 1543
+#   username: "lemmy"
+#   password: "overkill"
+#   vhost: "/hammersmith"

=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py	2012-02-15 17:33:31 +0000
+++ src/provisioningserver/plugin.py	2012-02-15 17:33:31 +0000
@@ -12,6 +12,12 @@
 __all__ = []
 
 from amqpclient import AMQFactory
+from formencode import Schema
+from formencode.validators import (
+    Int,
+    RequireIfPresent,
+    String,
+    )
 from provisioningserver.cobblerclient import CobblerSession
 from provisioningserver.remote import ProvisioningAPI_XMLRPC
 from provisioningserver.services import (
@@ -22,7 +28,6 @@
     FakeCobbler,
     FakeTwistedProxy,
     )
-import setproctitle
 from twisted.application.internet import (
     TCPClient,
     TCPServer,
@@ -38,39 +43,63 @@
     )
 from twisted.web.resource import Resource
 from twisted.web.server import Site
+import yaml
 from zope.interface import implements
 
 
+class ConfigOops(Schema):
+    """Configuration validator for OOPS options."""
+
+    if_key_missing = None
+
+    directory = String(if_missing=b"")
+    reporter = String(if_missing=b"")
+
+    chained_validators = (
+        RequireIfPresent("reporter", present="directory"),
+        )
+
+
+class ConfigBroker(Schema):
+    """Configuration validator for message broker options."""
+
+    if_key_missing = None
+
+    host = String(if_missing=b"localhost")
+    port = Int(min=1, max=65535, if_missing=5673)
+    username = String(if_missing=None)
+    password = String(if_missing=None)
+    vhost = String(if_missing="/")
+
+
+class Config(Schema):
+    """Configuration validator."""
+
+    if_key_missing = None
+
+    port = Int(min=1, max=65535, if_missing=8001)
+    logfile = String(not_empty=True)
+    oops = ConfigOops
+    broker = ConfigBroker
+
+    @classmethod
+    def parse(cls, stream):
+        """Load a YAML configuration from `stream` and validate."""
+        return cls().to_python(yaml.load(stream))
+
+    @classmethod
+    def load(cls, filename):
+        """Load a YAML configuration from `filename` and validate."""
+        with open(filename, "rb") as stream:
+            return cls.parse(stream)
+
+
 class Options(usage.Options):
     """Command line options for the provisioning server."""
 
     optParameters = [
-        ["port", None, 8001, "Port to serve on."],
-        ["logfile", "l", "pserv.log", "Logfile name."],
-        ["oops-dir", "r", None, "Where to write OOPS reports"],
-        ["oops-reporter", "o", "MAAS-PS", "String identifying this service."],
-        ]
-
-    # Move these back into optParameters when RabbitMQ is a required component
-    # of a running MaaS installation.
-    optParameters_FOR_RABBIT = [
-        ["brokerport", "p", 5672, "Broker port"],
-        ["brokerhost", "h", '127.0.0.1', "Broker host"],
-        ["brokeruser", "u", None, "Broker user"],
-        ["brokerpassword", "a", None, "Broker password"],
-        ["brokervhost", "v", '/', "Broker vhost"],
-        ]
-
-    def postOptions(self):
-        for int_arg in ('port',):
-            try:
-                self[int_arg] = int(self[int_arg])
-            except (TypeError, ValueError):
-                raise usage.UsageError("--%s must be an integer." % int_arg)
-        if not self["oops-reporter"] and self["oops-dir"]:
-            raise usage.UsageError(
-                "A reporter must be supplied to identify reports "
-                "from this service from other OOPS reports.")
+        ["config-file", "c", "pserv.yaml", "Configuration file to load."],
+        ]
 
 
 class ProvisioningServiceMaker(object):
@@ -84,44 +113,41 @@
         self.tapname = name
         self.description = description
 
-    def makeService(self, options, _set_proc_title=True):
-        """Construct a service.
-
-        :param _set_proc_title: For testing; if `False` this will stop the
-            obfuscation of command-line parameters in the process title.
-        """
-        # Required to hide the command line options that include a password.
-        # There is a small window where it can be seen though, between
-        # invocation and when this code runs. TODO: Make this optional (so
-        # that we don't override process title in tests).
-        if _set_proc_title:
-            setproctitle.setproctitle("maas provisioning service")
-
+    def makeService(self, options):
+        """Construct a service."""
         services = MultiService()
 
-        log_service = LogService(options["logfile"])
+        config_file = options["config-file"]
+        if config_file is None:
+            config = Config.parse(b"")
+        else:
+            config = Config.load(config_file)
+
+        log_service = LogService(config["logfile"])
         log_service.setServiceParent(services)
 
-        oops_dir = options["oops-dir"]
-        oops_reporter = options["oops-reporter"]
+        oops_config = config["oops"]
+        oops_dir = oops_config["directory"]
+        oops_reporter = oops_config["reporter"]
         oops_service = OOPSService(log_service, oops_dir, oops_reporter)
         oops_service.setServiceParent(services)
 
-        broker_port = options.get("brokerport")
-        broker_host = options.get("brokerhost")
-        broker_user = options.get("brokeruser")
-        broker_password = options.get("brokerpassword")
-        broker_vhost = options.get("brokervhost")
+        broker_config = config["broker"]
+        broker_port = broker_config["port"]
+        broker_host = broker_config["host"]
+        broker_username = broker_config["username"]
+        broker_password = broker_config["password"]
+        broker_vhost = broker_config["vhost"]
 
         # Connecting to RabbitMQ is optional; it is not yet a required
         # component of a running MaaS installation.
-        if broker_user is not None and broker_password is not None:
+        if broker_username is not None and broker_password is not None:
             cb_connected = lambda ignored: None  # TODO
             cb_disconnected = lambda ignored: None  # TODO
             cb_failed = lambda (connector, reason): (
                 log.err(reason, "Connection failed"))
             client_factory = AMQFactory(
-                broker_user, broker_password, broker_vhost,
+                broker_username, broker_password, broker_vhost,
                 cb_connected, cb_disconnected, cb_failed)
             client_service = TCPClient(
                 broker_host, broker_port, client_factory)
@@ -140,7 +166,7 @@
         site_root = Resource()
         site_root.putChild("api", ProvisioningAPI_XMLRPC(session))
         site = Site(site_root)
-        site_port = options["port"]
+        site_port = config["port"]
         site_service = TCPServer(site_port, site)
         site_service.setName("site")
         site_service.setServiceParent(services)

=== modified file 'src/provisioningserver/tests/test_plugin.py'
--- src/provisioningserver/tests/test_plugin.py	2012-02-15 17:33:31 +0000
+++ src/provisioningserver/tests/test_plugin.py	2012-02-15 17:33:31 +0000
@@ -13,10 +13,11 @@
 
 from functools import partial
 import os
-from unittest import skip
 
 from fixtures import TempDir
+import formencode
 from provisioningserver.plugin import (
+    Config,
     Options,
     ProvisioningServiceMaker,
     )
@@ -27,39 +28,76 @@
     )
 from twisted.application.service import MultiService
 from twisted.python.usage import UsageError
+import yaml
+
+
+class TestConfig(TestCase):
+    """Tests for `provisioningserver.plugin.Config`."""
+
+    def test_defaults(self):
+        expected = {
+            'broker': {
+                'host': 'localhost',
+                'password': None,
+                'port': 5673,
+                'username': None,
+                'vhost': u'/',
+                },
+            'logfile': '/some/where.log',
+            'oops': {
+                'directory': '',
+                'reporter': '',
+                },
+            'port': 8001,
+            }
+        mandatory_arguments = {
+            "logfile": "/some/where.log",
+            }
+        observed = Config.to_python(mandatory_arguments)
+        self.assertEqual(expected, observed)
+
+    def test_parse(self):
+        # Configuration can be parsed from a snipped of YAML.
+        observed = Config.parse(
+            b'logfile: "/some/where.log"')
+        self.assertEqual("/some/where.log", observed["logfile"])
+
+    def test_load(self):
+        # Configuration can be loaded and parsed from a file.
+        filename = os.path.join(
+            self.useFixture(TempDir()).path, "config.yaml")
+        with open(filename, "wb") as stream:
+            stream.write(b'logfile: "/some/where.log"')
+        observed = Config.load(filename)
+        self.assertEqual("/some/where.log", observed["logfile"])
+
+    def test_load_example(self):
+        # The example configuration can be loaded and validated.
+        filename = os.path.join(
+            os.path.dirname(__file__), os.pardir,
+            os.pardir, os.pardir, "etc", "pserv.yaml")
+        Config.load(filename)
+
+    def test_oops_directory_without_reporter(self):
+        # It is an error to omit the OOPS reporter if directory is specified.
+        config = (
+            'logfile: "/some/where.log"\n'
+            'oops:\n'
+            '  directory: /tmp/oops\n'
+            )
+        expected = MatchesException(
+            formencode.Invalid, "oops: You must give a value for reporter")
+        self.assertThat(
+            partial(Config.parse, config),
+            Raises(expected))
 
 
 class TestOptions(TestCase):
     """Tests for `provisioningserver.plugin.Options`."""
 
-    @skip(
-        "RabbitMQ is not yet a required component "
-        "of a running MaaS installation. Re-enable "
-        "when RabbitMQ is once again needed; remove "
-        "the other similarly named test.")
-    def test_defaults_SKIPPED(self):
-        options = Options()
-        expected = {
-            "brokerhost": "127.0.0.1",
-            "brokerpassword": None,
-            "brokerport": 5672,
-            "brokeruser": None,
-            "brokervhost": "/",
-            "logfile": "pserv.log",
-            "oops-dir": None,
-            "oops-reporter": "MAAS-PS",
-            "port": 8001,
-            }
-        self.assertEqual(expected, options.defaults)
-
     def test_defaults(self):
         options = Options()
-        expected = {
-            "logfile": "pserv.log",
-            "oops-dir": None,
-            "oops-reporter": "MAAS-PS",
-            "port": 8001,
-            }
+        expected = {"config-file": "pserv.yaml"}
         self.assertEqual(expected, options.defaults)
 
     def check_exception(self, options, message, *arguments):
@@ -68,123 +106,28 @@
             partial(options.parseOptions, arguments),
             Raises(MatchesException(UsageError, message)))
 
-    @skip(
-        "RabbitMQ is not yet a required component "
-        "of a running MaaS installation.")
-    def test_option_brokeruser_required(self):
-        options = Options()
-        self.check_exception(
-            options,
-            "--brokeruser must be specified")
-
-    @skip(
-        "RabbitMQ is not yet a required component "
-        "of a running MaaS installation.")
-    def test_option_brokerpassword_required(self):
-        options = Options()
-        self.check_exception(
-            options,
-            "--brokerpassword must be specified",
-            "--brokeruser", "Bob")
-
     def test_parse_minimal_options(self):
         options = Options()
         # The minimal set of options that must be provided.
         arguments = []
         options.parseOptions(arguments)  # No error.
 
-    @skip(
-        "RabbitMQ is not yet a required component "
-        "of a running MaaS installation. Re-enable "
-        "when RabbitMQ is once again needed; remove "
-        "the other similarly named test.")
-    def test_parse_int_options_SKIPPED(self):
-        # Some options are converted to ints.
-        options = Options()
-        arguments = [
-            "--brokerpassword", "Hoskins",
-            "--brokerport", "4321",
-            "--brokeruser", "Bob",
-            "--port", "3456",
-            ]
-        options.parseOptions(arguments)
-        self.assertEqual(4321, options["brokerport"])
-        self.assertEqual(3456, options["port"])
-
-    def test_parse_int_options(self):
-        # Some options are converted to ints.
-        options = Options()
-        arguments = [
-            "--port", "3456",
-            ]
-        options.parseOptions(arguments)
-        self.assertEqual(3456, options["port"])
-
-    @skip(
-        "RabbitMQ is not yet a required component "
-        "of a running MaaS installation. Re-enable "
-        "when RabbitMQ is once again needed; remove "
-        "the other similarly named test.")
-    def test_parse_broken_int_options_SKIPPED(self):
-        # An error is raised if the integer options do not contain integers.
-        options = Options()
-        arguments = [
-            "--brokerpassword", "Hoskins",
-            "--brokerport", "Jr.",
-            "--brokeruser", "Bob",
-            ]
-        self.assertRaises(
-            UsageError, options.parseOptions, arguments)
-
-    def test_parse_broken_int_options(self):
-        # An error is raised if the integer options do not contain integers.
-        options = Options()
-        arguments = [
-            "--port", "Metallica",
-            ]
-        self.assertRaises(
-            UsageError, options.parseOptions, arguments)
-
-    @skip(
-        "RabbitMQ is not yet a required component "
-        "of a running MaaS installation. Re-enable "
-        "when RabbitMQ is once again needed; remove "
-        "the other similarly named test.")
-    def test_oops_dir_without_reporter_SKIPPED(self):
-        # It is an error to omit the OOPS reporter if directory is specified.
-        options = Options()
-        arguments = [
-            "--brokerpassword", "Hoskins",
-            "--brokeruser", "Bob",
-            "--oops-dir", "/some/where",
-            "--oops-reporter", "",
-            ]
-        expected = MatchesException(
-            UsageError, "A reporter must be supplied")
-        self.assertThat(
-            partial(options.parseOptions, arguments),
-            Raises(expected))
-
-    def test_oops_dir_without_reporter(self):
-        # It is an error to omit the OOPS reporter if directory is specified.
-        options = Options()
-        arguments = [
-            "--oops-dir", "/some/where",
-            "--oops-reporter", "",
-            ]
-        expected = MatchesException(
-            UsageError, "A reporter must be supplied")
-        self.assertThat(
-            partial(options.parseOptions, arguments),
-            Raises(expected))
-
 
 class TestProvisioningServiceMaker(TestCase):
     """Tests for `provisioningserver.plugin.ProvisioningServiceMaker`."""
 
-    def get_log_file(self):
-        return os.path.join(
-            self.useFixture(TempDir()).path, "pserv.log")
+    def setUp(self):
+        super(TestProvisioningServiceMaker, self).setUp()
+        self.tempdir = self.useFixture(TempDir()).path
+
+    def write_config(self, config):
+        if "logfile" not in config:
+            logfile = os.path.join(self.tempdir, "pserv.log")
+            config = dict(config, logfile=logfile)
+        config_filename = os.path.join(self.tempdir, "config.yaml")
+        with open(config_filename, "wb") as stream:
+            yaml.dump(config, stream)
+        return config_filename
 
     def test_init(self):
         service_maker = ProvisioningServiceMaker("Harry", "Hill")
@@ -196,9 +139,9 @@
         Only the site service is created when no options are given.
         """
         options = Options()
-        options["logfile"] = self.get_log_file()
+        options["config-file"] = self.write_config({})
         service_maker = ProvisioningServiceMaker("Harry", "Hill")
-        service = service_maker.makeService(options, _set_proc_title=False)
+        service = service_maker.makeService(options)
         self.assertIsInstance(service, MultiService)
         self.assertSequenceEqual(
             ["log", "oops", "site"],
@@ -206,23 +149,17 @@
         self.assertEqual(
             len(service.namedServices), len(service.services),
             "Not all services are named.")
-        site_service = service.getServiceNamed("site")
-        self.assertEqual(options["port"], site_service.args[0])
 
-    @skip(
-        "RabbitMQ is not yet a required component "
-        "of a running MaaS installation.")
     def test_makeService_with_broker(self):
         """
         The log, oops, site, and amqp services are created when the broker
         user and password options are given.
         """
         options = Options()
-        options["brokerpassword"] = "Hoskins"
-        options["brokeruser"] = "Bob"
-        options["logfile"] = self.get_log_file()
+        options["config-file"] = self.write_config(
+            {"broker": {"username": "Bob", "password": "Hoskins"}})
         service_maker = ProvisioningServiceMaker("Harry", "Hill")
-        service = service_maker.makeService(options, _set_proc_title=False)
+        service = service_maker.makeService(options)
         self.assertIsInstance(service, MultiService)
         self.assertSequenceEqual(
             ["amqp", "log", "oops", "site"],
@@ -230,8 +167,3 @@
         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])
-        site_service = service.getServiceNamed("site")
-        self.assertEqual(options["port"], site_service.args[0])

=== modified file 'versions.cfg'
--- versions.cfg	2012-02-15 10:37:18 +0000
+++ versions.cfg	2012-02-15 17:33:31 +0000
@@ -7,6 +7,7 @@
 django-piston =
 docutils =
 fixtures =
+formencode = 1.2.4
 ipython =
 nose =
 nose-subunit =
@@ -19,8 +20,8 @@
 # https://code.djangoproject.com/ticket/16250
 psycopg2 = 2.4.1
 python-subunit =
+pyyaml = 3.10
 rabbitfixture = 0.3.2
-setproctitle =
 South =
 testresources >= 0.2.4-r58
 testtools =