← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/maas/use-real-cobbler into lp:maas

 

Gavin Panella has proposed merging lp:~allenap/maas/use-real-cobbler into lp:maas with lp:~allenap/maas/pserv-config-file as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/use-real-cobbler/+merge/93298

Use a real Cobbler instead of the fake in the maas-pserv Twisted plugin. This also makes logfile default to pserv.log.
-- 
https://code.launchpad.net/~allenap/maas/use-real-cobbler/+merge/93298
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/use-real-cobbler into lp:maas.
=== modified file 'HACKING.txt'
--- HACKING.txt	2012-02-10 16:59:35 +0000
+++ HACKING.txt	2012-02-16 12:20:11 +0000
@@ -72,6 +72,11 @@
 
     $ make sampledata
 
+Add yourself as a user in Cobbler. For convenience, give yourself the
+password ``test``::
+
+    $ sudo htdigest /etc/cobbler/users.digest Cobbler $USER
+
 Run the development webserver::
 
     $ make run

=== modified file 'etc/pserv.yaml'
--- etc/pserv.yaml	2012-02-15 17:13:37 +0000
+++ etc/pserv.yaml	2012-02-16 12:20:11 +0000
@@ -1,20 +1,40 @@
-# 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).
+##
+## 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).
+## Message broker configuration (optional, not currently used).
+#
 # broker:
 #   host: "localhost"
 #   port: 1543
 #   username: "lemmy"
 #   password: "overkill"
 #   vhost: "/hammersmith"
+#
+
+## Cobbler configuration.
+#
+# cobbler:
+#   url: "localhost"
+#   username: "admin"
+#   password: "test"
+#   fake: false
+#

=== modified file 'src/maasserver/provisioning.py'
--- src/maasserver/provisioning.py	2012-02-14 10:42:20 +0000
+++ src/maasserver/provisioning.py	2012-02-16 12:20:11 +0000
@@ -13,7 +13,7 @@
     'get_provisioning_api_proxy',
     ]
 
-from uuid import uuid1
+import warnings
 import xmlrpclib
 
 from django.conf import settings
@@ -29,14 +29,24 @@
 
 
 def get_provisioning_api_proxy():
-    """Return a proxy to the Provisioning API."""
-    # FIXME: This is a little ugly.
+    """Return a proxy to the Provisioning API.
+
+    If ``PSERV_URL`` is not set, we attempt to return a handle to a fake proxy
+    implementation. This will not be available in a packaged version of MaaS,
+    in which case an error is raised.
+    """
     url = settings.PSERV_URL
     if url is None:
-        from provisioningserver.testing.fakeapi import (
-            FakeSynchronousProvisioningAPI,
-            )
-        return FakeSynchronousProvisioningAPI()
+        try:
+            from maasserver import testing
+        except ImportError:
+            # This is probably in a package.
+            raise RuntimeError("PSERV_URL must be defined.")
+        else:
+            warnings.warn(
+                "PSERV_URL is None; using the fake Provisioning API.",
+                RuntimeWarning)
+            return testing.get_fake_provisioning_api_proxy()
     else:
         return xmlrpclib.ServerProxy(
             url, allow_none=True, use_datetime=True)
@@ -50,13 +60,12 @@
     if instance.system_id in nodes:
         profile = nodes[instance.system_id]["profile"]
     else:
-        # TODO: Get these from somewhere.
-        distro = papi.add_distro(
-            "distro-%s" % uuid1().get_hex(),
-            "initrd", "kernel")
-        profile = papi.add_profile(
-            "profile-%s" % uuid1().get_hex(),
-            distro)
+        # TODO: Choose a sensible profile.
+        profiles = papi.get_profiles()
+        assert len(profiles) >= 1, (
+            "No profiles defined in Cobbler; has "
+            "cobbler-ubuntu-import been run?")
+        profile = sorted(profiles)[0]
     papi.add_node(instance.system_id, profile)
 
 

=== modified file 'src/maasserver/testing/__init__.py'
--- src/maasserver/testing/__init__.py	2012-01-24 17:09:34 +0000
+++ src/maasserver/testing/__init__.py	2012-02-16 12:20:11 +0000
@@ -6,13 +6,43 @@
     unicode_literals,
     )
 
-"""Test maasserver API."""
+"""Tests for `maasserver`."""
 
 __metaclass__ = type
-__all__ = []
-
+__all__ = [
+    "get_fake_provisioning_api_proxy",
+    "LoggedInTestCase",
+    "TestCase",
+    ]
+
+from uuid import uuid1
+
+from fixtures import MonkeyPatch
 from maasserver.testing.factory import factory
-from maastesting import TestCase
+import maastesting
+from provisioningserver.testing import fakeapi
+
+
+def get_fake_provisioning_api_proxy():
+    papi_fake = fakeapi.FakeSynchronousProvisioningAPI()
+    distro = papi_fake.add_distro(
+        "distro-%s" % uuid1().get_hex(),
+        "initrd", "kernel")
+    papi_fake.add_profile(
+        "profile-%s" % uuid1().get_hex(),
+        distro)
+    return papi_fake
+
+
+class TestCase(maastesting.TestCase):
+
+    def setUp(self):
+        super(TestCase, self).setUp()
+        papi_fake = get_fake_provisioning_api_proxy()
+        papi_fake_fixture = MonkeyPatch(
+            "maasserver.provisioning.get_provisioning_api_proxy",
+            lambda: papi_fake)
+        self.useFixture(papi_fake_fixture)
 
 
 class LoggedInTestCase(TestCase):

=== added directory 'src/maasserver/testing/tests'
=== added file 'src/maasserver/testing/tests/__init__.py'
=== added file 'src/maasserver/testing/tests/test_module.py'
--- src/maasserver/testing/tests/test_module.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/testing/tests/test_module.py	2012-02-16 12:20:11 +0000
@@ -0,0 +1,52 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `maasserver.testing`."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+from maasserver import provisioning
+from maasserver.testing import TestCase
+from provisioningserver.testing import fakeapi
+
+
+class TestTestCase(TestCase):
+    """Tests for `TestCase`."""
+
+    def test_patched_in_fake_papi(self):
+        # TestCase.setUp() patches in a fake provisioning API so that we can
+        # observe what the signal handlers are doing.
+        papi_fake = provisioning.get_provisioning_api_proxy()
+        self.assertIsInstance(
+            papi_fake, fakeapi.FakeSynchronousProvisioningAPI)
+        # The fake has some limited, automatically generated, sample
+        # data. This is required for many tests to run. First there is a
+        # sample distro.
+        self.assertEqual(1, len(papi_fake.distros))
+        [distro_name] = papi_fake.distros
+        expected_distros = {
+            distro_name: {
+                u'initrd': u'initrd',
+                u'kernel': u'kernel',
+                u'name': distro_name,
+                },
+            }
+        self.assertEqual(expected_distros, papi_fake.distros)
+        # Second there is a sample profile, referring to the distro.
+        self.assertEqual(1, len(papi_fake.profiles))
+        [profile_name] = papi_fake.profiles
+        expected_profiles = {
+            profile_name: {
+                u'distro': distro_name,
+                u'name': profile_name,
+                },
+            }
+        self.assertEqual(expected_profiles, papi_fake.profiles)
+        # There are no nodes.
+        self.assertEqual({}, papi_fake.nodes)

=== modified file 'src/maasserver/tests/test_auth.py'
--- src/maasserver/tests/test_auth.py	2012-02-14 15:53:08 +0000
+++ src/maasserver/tests/test_auth.py	2012-02-16 12:20:11 +0000
@@ -19,8 +19,8 @@
     Node,
     NODE_STATUS,
     )
+from maasserver.testing import TestCase
 from maasserver.testing.factory import factory
-from maastesting import TestCase
 
 
 class LoginLogoutTest(TestCase):

=== modified file 'src/maasserver/tests/test_macaddressfield.py'
--- src/maasserver/tests/test_macaddressfield.py	2012-01-23 12:43:28 +0000
+++ src/maasserver/tests/test_macaddressfield.py	2012-02-16 12:20:11 +0000
@@ -14,8 +14,8 @@
 from django.core.exceptions import ValidationError
 from maasserver.macaddress import validate_mac
 from maasserver.models import MACAddress
+from maasserver.testing import TestCase
 from maasserver.testing.factory import factory
-from maastesting import TestCase
 
 
 class TestMACAddressField(TestCase):

=== modified file 'src/maasserver/tests/test_models.py'
--- src/maasserver/tests/test_models.py	2012-02-14 15:53:08 +0000
+++ src/maasserver/tests/test_models.py	2012-02-16 12:20:11 +0000
@@ -26,8 +26,8 @@
     NODE_STATUS_CHOICES_DICT,
     UserProfile,
     )
+from maasserver.testing import TestCase
 from maasserver.testing.factory import factory
-from maastesting import TestCase
 from piston.models import (
     Consumer,
     KEY_SIZE,

=== modified file 'src/maasserver/tests/test_provisioning.py'
--- src/maasserver/tests/test_provisioning.py	2012-02-13 16:21:23 +0000
+++ src/maasserver/tests/test_provisioning.py	2012-02-16 12:20:11 +0000
@@ -11,12 +11,10 @@
 __metaclass__ = type
 __all__ = []
 
-from fixtures import MonkeyPatch
 from maasserver import provisioning
 from maasserver.models import Node
+from maasserver.testing import TestCase
 from maasserver.testing.factory import factory
-from maastesting import TestCase
-from provisioningserver.testing.fakeapi import FakeSynchronousProvisioningAPI
 
 
 class ProvisioningTests:
@@ -97,35 +95,9 @@
         self.assertEqual([], node["mac_addresses"])
 
 
-def patch_in_fake_papi(test):
-    """Patch in a fake Provisioning API for the duration of the test."""
-    papi_fake = FakeSynchronousProvisioningAPI()
-    patch = MonkeyPatch(
-        "maasserver.provisioning.get_provisioning_api_proxy",
-        lambda: papi_fake)
-    test.useFixture(patch)
-    return papi_fake
-
-
-class TestProvisioningFake(TestCase):
-    """Tests for `patch_in_fake_papi`."""
-
-    def test_patch_in_fake_papi(self):
-        # patch_in_fake_papi() patches in a fake provisioning API so that we
-        # can observe what the signal handlers are doing.
-        papi = provisioning.get_provisioning_api_proxy()
-        papi_fake = patch_in_fake_papi(self)
-        self.assertIsNot(provisioning.get_provisioning_api_proxy(), papi)
-        self.assertIs(provisioning.get_provisioning_api_proxy(), papi_fake)
-        # The fake is pristine; it does not contain sample data.
-        self.assertEqual({}, papi_fake.distros)
-        self.assertEqual({}, papi_fake.profiles)
-        self.assertEqual({}, papi_fake.nodes)
-
-
 class TestProvisioningWithFake(ProvisioningTests, TestCase):
     """Tests for the Provisioning API using a fake."""
 
     def setUp(self):
         super(TestProvisioningWithFake, self).setUp()
-        self.papi = patch_in_fake_papi(self)
+        self.papi = provisioning.get_provisioning_api_proxy()

=== modified file 'src/maasserver/tests/test_runserver.py'
--- src/maasserver/tests/test_runserver.py	2012-02-07 16:11:46 +0000
+++ src/maasserver/tests/test_runserver.py	2012-02-16 12:20:11 +0000
@@ -12,7 +12,7 @@
 __all__ = []
 
 from maasserver.management.commands.runserver import render_error
-from maastesting import TestCase
+from maasserver.testing import TestCase
 
 
 class TestRunServer(TestCase):

=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py	2012-02-15 17:20:40 +0000
+++ src/provisioningserver/plugin.py	2012-02-16 12:20:11 +0000
@@ -11,12 +11,15 @@
 __metaclass__ = type
 __all__ = []
 
+from getpass import getuser
+
 from amqpclient import AMQFactory
 from formencode import Schema
 from formencode.validators import (
     Int,
     RequireIfPresent,
     String,
+    URL,
     )
 from provisioningserver.cobblerclient import CobblerSession
 from provisioningserver.remote import ProvisioningAPI_XMLRPC
@@ -24,10 +27,6 @@
     LogService,
     OOPSService,
     )
-from provisioningserver.testing.fakecobbler import (
-    FakeCobbler,
-    FakeTwistedProxy,
-    )
 from twisted.application.internet import (
     TCPClient,
     TCPServer,
@@ -67,20 +66,34 @@
 
     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)
+    username = String(if_missing=getuser())
+    password = String(if_missing=b"test")
     vhost = String(if_missing="/")
 
 
+class ConfigCobbler(Schema):
+    """Configuration validator for connecting to Cobbler."""
+
+    if_key_missing = None
+
+    url = URL(
+        add_http=True, require_tld=False,
+        if_missing=b"http://localhost/cobbler_api";,
+        )
+    username = String(if_missing=getuser())
+    password = String(if_missing=b"test")
+
+
 class Config(Schema):
     """Configuration validator."""
 
     if_key_missing = None
 
     port = Int(min=1, max=65535, if_missing=8001)
-    logfile = String(not_empty=True)
+    logfile = String(if_empty=b"pserv.log", if_missing=b"pserv.log")
     oops = ConfigOops
     broker = ConfigBroker
+    cobbler = ConfigCobbler
 
     @classmethod
     def parse(cls, stream):
@@ -118,10 +131,7 @@
         services = MultiService()
 
         config_file = options["config-file"]
-        if config_file is None:
-            config = Config.parse(b"")
-        else:
-            config = Config.load(config_file)
+        config = Config.load(config_file)
 
         log_service = LogService(config["logfile"])
         log_service.setServiceParent(services)
@@ -139,9 +149,9 @@
         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_username is not None and broker_password is not None:
+        # Connecting to RabbitMQ is not yet a required component of a running
+        # MaaS installation; skip unless the password has been set explicitly.
+        if broker_password is not b"test":
             cb_connected = lambda ignored: None  # TODO
             cb_disconnected = lambda ignored: None  # TODO
             cb_failed = lambda (connector, reason): (
@@ -154,17 +164,14 @@
             client_service.setName("amqp")
             client_service.setServiceParent(services)
 
-        session = CobblerSession(
-            # TODO: Get these values from command-line arguments.
-            "http://localhost/does/not/exist";, "user", "password")
-
-        # TODO: Remove this.
-        fake_cobbler = FakeCobbler({"user": "password"})
-        fake_cobbler_proxy = FakeTwistedProxy(fake_cobbler)
-        session.proxy = fake_cobbler_proxy
+        cobbler_config = config["cobbler"]
+        cobbler_session = CobblerSession(
+            cobbler_config["url"], cobbler_config["username"],
+            cobbler_config["password"])
+        papi_xmlrpc = ProvisioningAPI_XMLRPC(cobbler_session)
 
         site_root = Resource()
-        site_root.putChild("api", ProvisioningAPI_XMLRPC(session))
+        site_root.putChild("api", papi_xmlrpc)
         site = Site(site_root)
         site_port = config["port"]
         site_service = TCPServer(site_port, site)

=== modified file 'src/provisioningserver/testing/fakecobbler.py'
--- src/provisioningserver/testing/fakecobbler.py	2012-02-13 14:54:34 +0000
+++ src/provisioningserver/testing/fakecobbler.py	2012-02-16 12:20:11 +0000
@@ -20,7 +20,10 @@
 from fnmatch import fnmatch
 from itertools import count
 from random import randint
-from xmlrpclib import Fault
+from xmlrpclib import (
+    dumps,
+    Fault,
+    )
 
 from provisioningserver.cobblerclient import CobblerSession
 from twisted.internet.defer import (
@@ -53,6 +56,11 @@
 
     @inlineCallbacks
     def callRemote(self, method, *args):
+        # Dump the call information as an XML-RPC request to ensure that it
+        # will travel over the wire. Cobbler does not allow None so we forbid
+        # it here too.
+        dumps(args, method, allow_none=False)
+        # Continue to forward the call to fake_cobbler.
         callee = getattr(self.fake_cobbler, method, None)
         assert callee is not None, "Unknown Cobbler method: %s" % method
         result = yield callee(*args)

=== modified file 'src/provisioningserver/tests/test_plugin.py'
--- src/provisioningserver/tests/test_plugin.py	2012-02-16 09:24:41 +0000
+++ src/provisioningserver/tests/test_plugin.py	2012-02-16 12:20:11 +0000
@@ -11,6 +11,7 @@
 __metaclass__ = type
 __all__ = []
 
+from getpass import getuser
 from functools import partial
 import os
 
@@ -38,28 +39,29 @@
         expected = {
             'broker': {
                 'host': 'localhost',
-                'password': None,
                 'port': 5673,
-                'username': None,
+                'username': getuser(),
+                'password': 'test',
                 'vhost': u'/',
                 },
-            'logfile': '/some/where.log',
+            'cobbler': {
+                'url': 'http://localhost/cobbler_api',
+                'username': getuser(),
+                'password': 'test',
+                },
+            'logfile': 'pserv.log',
             'oops': {
                 'directory': '',
                 'reporter': '',
                 },
             'port': 8001,
             }
-        mandatory_arguments = {
-            "logfile": "/some/where.log",
-            }
-        observed = Config.to_python(mandatory_arguments)
+        observed = Config.to_python({})
         self.assertEqual(expected, observed)
 
     def test_parse(self):
         # Configuration can be parsed from a snippet of YAML.
-        observed = Config.parse(
-            b'logfile: "/some/where.log"')
+        observed = Config.parse(b'logfile: "/some/where.log"')
         self.assertEqual("/some/where.log", observed["logfile"])
 
     def test_load(self):
@@ -81,7 +83,6 @@
     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'
             )
@@ -121,9 +122,6 @@
         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)