← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Gavin Panella has proposed merging lp:~allenap/maas/maas-pserv-add-node into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~allenap/maas/maas-pserv-add-node/+merge/92446
-- 
https://code.launchpad.net/~allenap/maas/maas-pserv-add-node/+merge/92446
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/maas/maas-pserv-add-node into lp:maas.
=== modified file 'Makefile'
--- Makefile	2012-02-01 12:39:54 +0000
+++ Makefile	2012-02-10 11:36:18 +0000
@@ -46,7 +46,7 @@
 lint: sources = setup.py src templates utilities
 lint: bin/flake8
 	@bin/flake8 $(sources) | \
-	    (! fgrep -v "from maas.settings import *")
+	    (! egrep -v "from maas[.](settings|development) import [*]")
 
 check: clean test
 
@@ -72,8 +72,16 @@
 	$(RM) docs/api.rst
 	$(RM) -r docs/_build/
 
-run: bin/maas dev-db
-	bin/maas runserver 8000
+pserv.pid: bin/twistd.pserv
+	bin/twistd.pserv --pidfile=$@ maas-pserv --port=8001
+
+pserv-start: pserv.pid
+
+pserv-stop:
+	{ test -e pserv.pid && cat pserv.pid; } | xargs --no-run-if-empty kill
+
+run: bin/maas dev-db pserv.pid
+	bin/maas runserver 8000 --settings=maas.demo
 
 harness: bin/maas dev-db
 	bin/maas shell
@@ -83,4 +91,5 @@
 
 .PHONY: \
     build check clean dev-db distclean doc \
-    harness lint run syncdb test sampledata
+    harness lint pserv-start pserv-stop run \
+    syncdb test sampledata

=== added file 'src/maas/demo.py'
--- src/maas/demo.py	1970-01-01 00:00:00 +0000
+++ src/maas/demo.py	2012-02-10 11:36:18 +0000
@@ -0,0 +1,18 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Django DEMO settings for maas project."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+
+from maas.development import *
+from maas.settings import *
+
+
+# This should match the setting in Makefile:pserv.pid.
+PSERV_URL = "http://localhost:8001/api";

=== modified file 'src/maas/settings.py'
--- src/maas/settings.py	2012-02-09 07:43:25 +0000
+++ src/maas/settings.py	2012-02-10 11:36:18 +0000
@@ -201,3 +201,7 @@
         },
     }
 }
+
+# The location of the Provisioning API XML-RPC endpoint. If PSERV_URL is None,
+# use the fake Provisioning API.
+PSERV_URL = None

=== modified file 'src/maasserver/__init__.py'
--- src/maasserver/__init__.py	2012-02-08 14:21:17 +0000
+++ src/maasserver/__init__.py	2012-02-10 11:36:18 +0000
@@ -0,0 +1,18 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""MaaS Server application."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+from maasserver import provisioning
+
+# This has been imported so that it can register its signal handlers early on,
+# before it misses anything. (Mentioned below to silence lint warnings.)
+provisioning

=== added file 'src/maasserver/provisioning.py'
--- src/maasserver/provisioning.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/provisioning.py	2012-02-10 11:36:18 +0000
@@ -0,0 +1,78 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Interact with the Provisioning API."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+from uuid import uuid1
+import xmlrpclib
+
+from django.conf import settings
+from django.db.models.signals import (
+    post_delete,
+    post_save,
+    )
+from django.dispatch import receiver
+from maasserver.models import (
+    MACAddress,
+    Node,
+    )
+
+
+def get_provisioning_api_proxy():
+    """Return a proxy to the Provisioning API."""
+    # FIXME: This is a little ugly.
+    url = settings.PSERV_URL
+    if url is None:
+        from provisioningserver.testing.fakeapi import (
+            FakeSynchronousProvisioningAPI,
+            )
+        return FakeSynchronousProvisioningAPI()
+    else:
+        return xmlrpclib.ServerProxy(
+            url, allow_none=True, use_datetime=True)
+
+
+@receiver(post_save, sender=Node)
+def provision_post_save_Node(sender, instance, created, **kwargs):
+    """Create or update nodes in the provisioning server."""
+    # Create or update the node in the provisioning prov.
+    papi = get_provisioning_api_proxy()
+    nodes = papi.get_nodes_by_name([instance.system_id])
+    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)
+    papi.add_node(instance.system_id, profile)
+
+
+@receiver(post_save, sender=MACAddress)
+def provision_post_save_MACAddress(sender, instance, created, **kwargs):
+    """Create or update MACs in the provisioning server."""
+    # TODO
+
+
+@receiver(post_delete, sender=Node)
+def provision_post_delete_Node(sender, instance, **kwargs):
+    """Delete nodes in the provisioning server."""
+    papi = get_provisioning_api_proxy()
+    papi.delete_nodes_by_name([instance.system_id])
+
+
+@receiver(post_delete, sender=MACAddress)
+def provision_post_delete_MACAddress(sender, instance, **kwargs):
+    """Delete MACs in the provisioning server."""
+    # TODO

=== added file 'src/maasserver/tests/test_provisioning.py'
--- src/maasserver/tests/test_provisioning.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_provisioning.py	2012-02-10 11:36:18 +0000
@@ -0,0 +1,86 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `maasserver.provisioning`."""
+
+from __future__ import (
+    print_function,
+    unicode_literals,
+    )
+
+__metaclass__ = type
+__all__ = []
+
+from fixtures import MonkeyPatch
+from maasserver import provisioning
+from maasserver.models import Node
+from maastesting import TestCase
+from provisioningserver.testing.fakeapi import FakeSynchronousProvisioningAPI
+
+
+class TestProvisioning(TestCase):
+
+    def patch_in_fake_papi(self):
+        papi_fake = FakeSynchronousProvisioningAPI()
+        patch = MonkeyPatch(
+            "maasserver.provisioning.get_provisioning_api_proxy",
+            lambda: papi_fake)
+        self.useFixture(patch)
+        return papi_fake
+
+    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 = self.patch_in_fake_papi()
+        self.assertIsNot(provisioning.get_provisioning_api_proxy(), papi)
+        self.assertIs(provisioning.get_provisioning_api_proxy(), papi_fake)
+        # The fake has small database, and it's empty to begin with.
+        self.assertEqual({}, papi_fake.distros)
+        self.assertEqual({}, papi_fake.profiles)
+        self.assertEqual({}, papi_fake.nodes)
+
+    def test_provision_post_save_Node_create(self):
+        # Creating and saving a node automatically creates a dummy distro and
+        # profile too, and associates it with the new node.
+        papi_fake = self.patch_in_fake_papi()
+        node_model = Node(system_id="frank")
+        provisioning.provision_post_save_Node(
+            sender=Node, instance=node_model, created=True)
+        self.assertEqual(["frank"], sorted(papi_fake.nodes))
+        node = papi_fake.nodes["frank"]
+        profile_name = node["profile"]
+        self.assertIn(profile_name, papi_fake.profiles)
+        profile = papi_fake.profiles[profile_name]
+        distro_name = profile["distro"]
+        self.assertIn(distro_name, papi_fake.distros)
+
+    def test_provision_post_save_Node_update(self):
+        # Saving an existing node does not change the profile or distro
+        # associated with it.
+        papi_fake = self.patch_in_fake_papi()
+        node_model = Node(system_id="frank")
+        provisioning.provision_post_save_Node(
+            sender=Node, instance=node_model, created=True)
+        # Record the current profile name.
+        node = papi_fake.nodes["frank"]
+        profile_name1 = node["profile"]
+        # Update the model node.
+        provisioning.provision_post_save_Node(
+            sender=Node, instance=node_model, created=False)
+        # The profile name is unchanged.
+        node = papi_fake.nodes["frank"]
+        profile_name2 = node["profile"]
+        self.assertEqual(profile_name1, profile_name2)
+
+    def test_provision_post_delete_Node(self):
+        papi_fake = self.patch_in_fake_papi()
+        node_model = Node(system_id="frank")
+        provisioning.provision_post_save_Node(
+            sender=Node, instance=node_model, created=True)
+        provisioning.provision_post_delete_Node(
+            sender=Node, instance=node_model)
+        # The node is deleted, but the profile and distro remain.
+        self.assertNotEqual({}, papi_fake.distros)
+        self.assertNotEqual({}, papi_fake.profiles)
+        self.assertEqual({}, papi_fake.nodes)

=== modified file 'src/provisioningserver/testing/fakeapi.py'
--- src/provisioningserver/testing/fakeapi.py	2012-02-09 12:35:21 +0000
+++ src/provisioningserver/testing/fakeapi.py	2012-02-10 11:36:18 +0000
@@ -27,6 +27,7 @@
 from provisioningserver.interfaces import IProvisioningAPI
 from twisted.internet import defer
 from zope.interface import implementer
+from zope.interface.interface import Method
 
 
 class FakeProvisioningDatabase(dict):
@@ -65,6 +66,8 @@
 @implementer(IProvisioningAPI)
 class FakeSynchronousProvisioningAPI:
 
+    # TODO: Referential integrity might be a nice thing.
+
     def __init__(self):
         super(FakeSynchronousProvisioningAPI, self).__init__()
         self.distros = FakeProvisioningDatabase()
@@ -125,4 +128,5 @@
     b"FakeAsynchronousProvisioningAPI", (FakeSynchronousProvisioningAPI,), {
         name: async(getattr(FakeSynchronousProvisioningAPI, name))
         for name in IProvisioningAPI.names(all=True)
+        if isinstance(IProvisioningAPI[name], Method)
         })

=== modified file 'src/provisioningserver/testing/fakecobbler.py'
--- src/provisioningserver/testing/fakecobbler.py	2012-02-08 21:31:17 +0000
+++ src/provisioningserver/testing/fakecobbler.py	2012-02-10 11:36:18 +0000
@@ -208,7 +208,7 @@
 
     def _api_get_object(self, object_type, name):
         """Get object's attributes by name."""
-        location = self.store[None][object_type]
+        location = self.store[None].get(object_type, {})
         matches = [obj for obj in location.values() if obj['name'] == name]
         assert len(matches) <= 1, (
             "Multiple %s objects are called '%s'." % (object_type, name))

=== modified file 'src/provisioningserver/tests/test_fakecobbler.py'
--- src/provisioningserver/tests/test_fakecobbler.py	2012-02-03 10:58:38 +0000
+++ src/provisioningserver/tests/test_fakecobbler.py	2012-02-10 11:36:18 +0000
@@ -282,6 +282,13 @@
         self.assertEqual(comment, found_obj['comment'])
 
     @inlineCallbacks
+    def test_get_values_returns_None_for_non_existent_object(self):
+        session = yield fake_cobbler_session()
+        name = self.make_name()
+        values = yield self.cobbler_class(session, name).get_values()
+        self.assertIsNone(values)
+
+    @inlineCallbacks
     def test_get_handle_finds_handle(self):
         session = yield fake_cobbler_session()
         name = self.make_name()