launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06251
[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