← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Gavin Panella has proposed merging lp:~allenap/maas/pserv-xmlrpc into lp:maas with lp:~allenap/maas/pserv-xmlrpc-cobblerobject-metaclass as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/pserv-xmlrpc/+merge/91632

Provide a "provisioning API" via XML-RPC for the MaaS web UI to consume.
-- 
https://code.launchpad.net/~allenap/maas/pserv-xmlrpc/+merge/91632
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/pserv-xmlrpc into lp:maas.
=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py	2012-02-02 16:13:07 +0000
+++ src/provisioningserver/plugin.py	2012-02-06 10:50:43 +0000
@@ -12,12 +12,21 @@
 __all__ = []
 
 from amqpclient import AMQFactory
+from provisioningserver.cobblerclient import CobblerSession
+from provisioningserver.remote import ProvisioningAPI
 from provisioningserver.services import (
     LogService,
     OOPSService,
     )
+from provisioningserver.testing.fakecobbler import (
+    FakeCobbler,
+    FakeTwistedProxy,
+    )
 import setproctitle
-from twisted.application.internet import TCPClient
+from twisted.application.internet import (
+    TCPClient,
+    TCPServer,
+    )
 from twisted.application.service import (
     IServiceMaker,
     MultiService,
@@ -27,6 +36,8 @@
     log,
     usage,
     )
+from twisted.web.resource import Resource
+from twisted.web.server import Site
 from zope.interface import implements
 
 
@@ -34,6 +45,7 @@
     """Command line options for the provisioning server."""
 
     optParameters = [
+        ["port", None, 8001, "Port to serve on."],
         ["logfile", "l", "provisioningserver.log", "Logfile name."],
         ["brokerport", "p", 5672, "Broker port"],
         ["brokerhost", "h", '127.0.0.1', "Broker host"],
@@ -45,7 +57,7 @@
         ]
 
     def postOptions(self):
-        for int_arg in ('brokerport',):
+        for int_arg in ('port', 'brokerport'):
             try:
                 self[int_arg] = int(self[int_arg])
             except (TypeError, ValueError):
@@ -82,11 +94,12 @@
 
         services = MultiService()
 
-        logging_service = LogService(options["logfile"])
-        logging_service.setServiceParent(services)
+        log_service = LogService(options["logfile"])
+        log_service.setServiceParent(services)
 
-        oops_service = OOPSService(
-            logging_service, options["oops-dir"], options["oops-reporter"])
+        oops_dir = options["oops-dir"]
+        oops_reporter = options["oops-reporter"]
+        oops_service = OOPSService(log_service, oops_dir, oops_reporter)
         oops_service.setServiceParent(services)
 
         broker_port = options["brokerport"]
@@ -110,4 +123,21 @@
             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
+
+        site_root = Resource()
+        site_root.putChild("api", ProvisioningAPI(session))
+        site = Site(site_root)
+        site_port = options["port"]
+        site_service = TCPServer(site_port, site)
+        site_service.setName("site")
+        site_service.setServiceParent(services)
+
         return services

=== added file 'src/provisioningserver/remote.py'
--- src/provisioningserver/remote.py	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/remote.py	2012-02-06 10:50:43 +0000
@@ -0,0 +1,75 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Provisioning API for use by the MaaS API server."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = [
+    "ProvisioningAPI",
+    ]
+
+from provisioningserver.cobblerclient import (
+    CobblerDistro,
+    CobblerProfile,
+    CobblerSystem,
+    )
+from twisted.internet.defer import (
+    inlineCallbacks,
+    returnValue,
+    )
+from twisted.web.xmlrpc import XMLRPC
+
+
+class ProvisioningAPI(XMLRPC):
+
+    def __init__(self, session):
+        XMLRPC.__init__(self, allowNone=True, useDateTime=True)
+        self.session = session
+
+    @inlineCallbacks
+    def xmlrpc_add_distro(self, name, initrd, kernel):
+        assert isinstance(name, basestring)
+        assert isinstance(initrd, basestring)
+        assert isinstance(kernel, basestring)
+        distro = yield CobblerDistro.new(
+            self.session, name, {
+                "initrd": initrd,
+                "kernel": kernel,
+                })
+        returnValue(distro.name)
+
+    @inlineCallbacks
+    def xmlrpc_get_distros(self):
+        distros = yield CobblerDistro.get_all_values(self.session)
+        returnValue(distros)
+
+    @inlineCallbacks
+    def xmlrpc_add_profile(self, name, distro):
+        assert isinstance(name, basestring)
+        assert isinstance(distro, basestring)
+        profile = yield CobblerProfile.new(
+            self.session, name, {"distro": distro})
+        returnValue(profile.name)
+
+    @inlineCallbacks
+    def xmlrpc_get_profiles(self):
+        profiles = yield CobblerProfile.get_all_values(self.session)
+        returnValue(profiles)
+
+    @inlineCallbacks
+    def xmlrpc_add_node(self, name, profile):
+        assert isinstance(name, basestring)
+        assert isinstance(profile, basestring)
+        system = yield CobblerSystem.new(
+            self.session, name, {"profile": profile})
+        returnValue(system.name)
+
+    @inlineCallbacks
+    def xmlrpc_get_nodes(self):
+        systems = yield CobblerSystem.get_all_values(self.session)
+        returnValue(systems)

=== modified file 'src/provisioningserver/services.py'
--- src/provisioningserver/services.py	2012-02-02 23:54:13 +0000
+++ src/provisioningserver/services.py	2012-02-06 10:50:43 +0000
@@ -51,9 +51,8 @@
         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)
+            self.__previous_signal_handler = signal.signal(
+                signal.SIGUSR1, self._signal_handler)
         else:
             self.logfile = sys.stdout
         self.observer = FileLogObserver(self.logfile)
@@ -61,15 +60,18 @@
 
     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
+        if self.filename != '-':
+            signal.signal(signal.SIGUSR1, self.__previous_signal_handler)
+            del self.__previous_signal_handler
+            self.observer.stop()
+            self.observer = None
+            self.logfile.close()
+            self.logfile = None
+        else:
+            self.observer.stop()
+            self.observer = None
+            # Don't close stdout.
+            self.logfile = None
 
 
 class OOPSService(Service):

=== modified file 'src/provisioningserver/tests/test_plugin.py'
--- src/provisioningserver/tests/test_plugin.py	2012-02-03 00:06:57 +0000
+++ src/provisioningserver/tests/test_plugin.py	2012-02-06 10:50:43 +0000
@@ -43,6 +43,7 @@
             "logfile": "provisioningserver.log",
             "oops-dir": None,
             "oops-reporter": "MAAS-PS",
+            "port": 8001,
             }
         self.assertEqual(expected, options.defaults)
 
@@ -84,9 +85,11 @@
             "--brokerpassword", "Hoskins",
             "--brokerport", "4321",
             "--brokeruser", "Bob",
+            "--port", "3456",
             ]
         options.parseOptions(arguments)
         self.assertEqual(4321, options["brokerport"])
+        self.assertEqual(3456, options["port"])
 
     def test_parse_broken_int_options(self):
         # An error is raised if the integer options do not contain integers.
@@ -130,7 +133,7 @@
 
     def test_makeService(self):
         """
-        Only the log and oops services are created when no options are given.
+        Only the site service is created when no options are given.
         """
         options = Options()
         options["logfile"] = self.get_log_file()
@@ -138,16 +141,18 @@
         service = service_maker.makeService(options, _set_proc_title=False)
         self.assertIsInstance(service, MultiService)
         self.assertSequenceEqual(
-            ["log", "oops"],
+            ["log", "oops", "site"],
             sorted(service.namedServices))
         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])
 
     def test_makeService_with_broker(self):
         """
-        The log, oops and amqp services are created when the broker user and
-        password options are given.
+        The log, oops, site, and amqp services are created when the broker
+        user and password options are given.
         """
         options = Options()
         options["brokerpassword"] = "Hoskins"
@@ -157,7 +162,7 @@
         service = service_maker.makeService(options, _set_proc_title=False)
         self.assertIsInstance(service, MultiService)
         self.assertSequenceEqual(
-            ["amqp", "log", "oops"],
+            ["amqp", "log", "oops", "site"],
             sorted(service.namedServices))
         self.assertEqual(
             len(service.namedServices), len(service.services),
@@ -165,3 +170,5 @@
         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])

=== added file 'src/provisioningserver/tests/test_remote.py'
--- src/provisioningserver/tests/test_remote.py	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/tests/test_remote.py	2012-02-06 10:50:43 +0000
@@ -0,0 +1,137 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `provisioningserver.remote`."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+from provisioningserver.cobblerclient import CobblerSession
+from provisioningserver.remote import ProvisioningAPI
+from provisioningserver.testing.fakecobbler import (
+    FakeCobbler,
+    FakeTwistedProxy,
+    )
+from testtools import TestCase
+from testtools.deferredruntest import AsynchronousDeferredRunTest
+from twisted.internet.defer import inlineCallbacks
+
+
+class TestProvisioningAPI(TestCase):
+    """Tests for `provisioningserver.remote.ProvisioningAPI`."""
+
+    run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
+
+    def get_cobbler_session(self):
+        cobbler_session = CobblerSession(
+            "http://localhost/does/not/exist";, "user", "password")
+        cobbler_fake = FakeCobbler({"user": "password"})
+        cobbler_proxy = FakeTwistedProxy(cobbler_fake)
+        cobbler_session.proxy = cobbler_proxy
+        return cobbler_session
+
+    @inlineCallbacks
+    def test_add_distro(self):
+        cobbler_session = self.get_cobbler_session()
+        # Create a distro via the Provisioning API.
+        prov = ProvisioningAPI(cobbler_session)
+        distro = yield prov.xmlrpc_add_distro(
+            "distro", "an_initrd", "a_kernel")
+        self.assertEqual("distro", distro)
+
+    @inlineCallbacks
+    def test_get_distros(self):
+        cobbler_session = self.get_cobbler_session()
+        prov = ProvisioningAPI(cobbler_session)
+        distros = yield prov.xmlrpc_get_distros()
+        self.assertEqual({}, distros)
+        # Create some distros via the Provisioning API.
+        yield prov.xmlrpc_add_distro(
+            "distro3", "an_initrd", "a_kernel")
+        yield prov.xmlrpc_add_distro(
+            "distro1", "an_initrd", "a_kernel")
+        yield prov.xmlrpc_add_distro(
+            "distro2", "an_initrd", "a_kernel")
+        distros = yield prov.xmlrpc_get_distros()
+        expected = {
+            u'distro1': {
+                u'initrd': u'an_initrd',
+                u'kernel': u'a_kernel',
+                u'name': u'distro1'},
+            u'distro2': {
+                u'initrd': u'an_initrd',
+                u'kernel': u'a_kernel',
+                u'name': u'distro2'},
+            u'distro3': {
+                u'initrd': u'an_initrd',
+                u'kernel': u'a_kernel',
+                u'name': u'distro3'},
+            }
+        self.assertEqual(expected, distros)
+
+    @inlineCallbacks
+    def test_add_profile(self):
+        cobbler_session = self.get_cobbler_session()
+        # Create a profile via the Provisioning API.
+        prov = ProvisioningAPI(cobbler_session)
+        distro = yield prov.xmlrpc_add_distro(
+            "distro", "an_initrd", "a_kernel")
+        profile = yield prov.xmlrpc_add_profile("profile", distro)
+        self.assertEqual("profile", profile)
+
+    @inlineCallbacks
+    def test_get_profiles(self):
+        cobbler_session = self.get_cobbler_session()
+        prov = ProvisioningAPI(cobbler_session)
+        distro = yield prov.xmlrpc_add_distro(
+            "distro", "an_initrd", "a_kernel")
+        profiles = yield prov.xmlrpc_get_profiles()
+        self.assertEqual({}, profiles)
+        # Create some profiles via the Provisioning API.
+        yield prov.xmlrpc_add_profile("profile3", distro)
+        yield prov.xmlrpc_add_profile("profile1", distro)
+        yield prov.xmlrpc_add_profile("profile2", distro)
+        profiles = yield prov.xmlrpc_get_profiles()
+        expected = {
+            u'profile1': {u'distro': u'distro', u'name': u'profile1'},
+            u'profile2': {u'distro': u'distro', u'name': u'profile2'},
+            u'profile3': {u'distro': u'distro', u'name': u'profile3'},
+            }
+        self.assertEqual(expected, profiles)
+
+    @inlineCallbacks
+    def test_add_node(self):
+        cobbler_session = self.get_cobbler_session()
+        # Create a system/node via the Provisioning API.
+        prov = ProvisioningAPI(cobbler_session)
+        distro = yield prov.xmlrpc_add_distro(
+            "distro", "an_initrd", "a_kernel")
+        profile = yield prov.xmlrpc_add_profile("profile", distro)
+        node = yield prov.xmlrpc_add_node("node", profile)
+        self.assertEqual("node", node)
+
+    @inlineCallbacks
+    def test_get_nodes(self):
+        cobbler_session = self.get_cobbler_session()
+        prov = ProvisioningAPI(cobbler_session)
+        distro = yield prov.xmlrpc_add_distro(
+            "distro", "an_initrd", "a_kernel")
+        profile = yield prov.xmlrpc_add_profile("profile", distro)
+        nodes = yield prov.xmlrpc_get_nodes()
+        self.assertEqual({}, nodes)
+        # Create some nodes via the Provisioning API.
+        yield prov.xmlrpc_add_node("node3", profile)
+        yield prov.xmlrpc_add_node("node1", profile)
+        yield prov.xmlrpc_add_node("node2", profile)
+        nodes = yield prov.xmlrpc_get_nodes()
+        expected = {
+            u'node1': {u'name': u'node1', u'profile': u'profile'},
+            u'node2': {u'name': u'node2', u'profile': u'profile'},
+            u'node3': {u'name': u'node3', u'profile': u'profile'},
+            }
+        self.assertEqual(expected, nodes)


Follow ups