← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/add-nodegroup-api-2 into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/add-nodegroup-api-2 into lp:maas with lp:~rvb/maas/add-nodegroup-api as a prerequisite.

Requested reviews:
  MAAS Maintainers (maas-maintainers)

For more details, see:
https://code.launchpad.net/~rvb/maas/add-nodegroup-api-2/+merge/124255

This branch adds the API call used to register a new NodeGroup with its interfaces.  This is an anonymous API method because a cluster controller need to contact the region controller in order to get the credentials it needs to function.  The new Nodegroup is then created in a 'PENDING' state.  An admin will have to accept it (via the UI or the API).

= Pre-imp =

This was discussed with Jeroen.

= Notes =

- The worflow is this: a new cluster controller registers itself with the MAAS server by calling this new method.
    - If the UUID for this cluster controller is unknown, then the server register this as a new 'pending' nodegroup with all its interfaces.  An admin then needs to accept this new Cluster controller by changing its status. The cluster controller polls at regular intervals to see if an admin has accepted it.  Once this is done, the MAAS server will send the RabbitMQ credentials and the Cluster controller will be able to connect to the MAAS server.
    - If the UUID corresponds to a known and validated cluster controller, the credentials needed to connect to RabbitMQ are returned by the MAAS server.
    - If the UUUID corresponds to a refused cluster controller, the request is denied.

- I thought that the http return code ACCEPTED was very well suited for what we're trying to accomplish here but the most important thing is for the Cluster controller to be able to tell when he should wait a bit a re-issue the request.
-- 
https://code.launchpad.net/~rvb/maas/add-nodegroup-api-2/+merge/124255
Your team MAAS Maintainers is requested to review the proposed merge of lp:~rvb/maas/add-nodegroup-api-2 into lp:maas.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py	2012-09-12 14:24:49 +0000
+++ src/maasserver/api.py	2012-09-13 16:44:38 +0000
@@ -108,6 +108,7 @@
     ARCHITECTURE,
     NODE_PERMISSION,
     NODE_STATUS,
+    NODEGROUP_STATUS,
     )
 from maasserver.exceptions import (
     MAASAPIBadRequest,
@@ -120,6 +121,7 @@
 from maasserver.forms import (
     get_node_create_form,
     get_node_edit_form,
+    NodeGroupWithInterfacesForm,
     )
 from maasserver.models import (
     Config,
@@ -893,6 +895,53 @@
         NodeGroup.objects.refresh_workers()
         return HttpResponse("Sending worker refresh.", status=httplib.OK)
 
+    @api_exported('POST')
+    def register(self, request):
+        """Register a new `NodeGroup`.
+
+        :param uuid: The UUID of the nodegroup.
+        :type name: basestring
+        :param name: The name of the nodegroup.
+        :type name: basestring
+        :param interfaces: The list of the interfaces' data.
+        :type interface: json string containing a list of dictionaries with
+            the data to initialize the interfaces.
+            e.g.: '[{"ip_range_high": "192.168.168.254",
+            "ip_range_low": "192.168.168.1", "broadcast_ip":
+            "192.168.168.255", "ip": "192.168.168.18", "subnet_mask":
+            "255.255.255.0", "router_ip": "192.168.168.1", "interface":
+            "eth0"}]'
+        """
+        uuid = get_mandatory_param(request.data, 'uuid')
+        existing_nodegroup = get_one(NodeGroup.objects.filter(uuid=uuid))
+        if existing_nodegroup is None:
+            # This nodegroup (identified by its uuid), does not exist yet,
+            # create it if the data validates.
+            form = NodeGroupWithInterfacesForm(request.data)
+            if form.is_valid():
+                form.save()
+                return HttpResponse(
+                    "Cluster registered.  Awaiting admin approval.",
+                    status=httplib.ACCEPTED)
+            else:
+                raise ValidationError(form.errors)
+        else:
+            if existing_nodegroup.status == NODEGROUP_STATUS.ACCEPTED:
+                # The nodegroup exists and is validated, return the RabbitMQ
+                # credentials as JSON.
+                # XXX: rvb 2012-09-13 bug=1050492: MAAS uses the 'guest'
+                # account to communicate with RabbitMQ, hence none of the
+                # connection information are defined.
+                return {
+                    # TODO: send RabbiMQ credentials.
+                    'test': 'test',
+                }
+            elif existing_nodegroup.status == NODEGROUP_STATUS.REJECTED:
+                raise PermissionDenied('Rejected cluster.')
+            elif existing_nodegroup.status == NODEGROUP_STATUS.PENDING:
+                return HttpResponse(
+                    "Awaiting admin approval.", status=httplib.ACCEPTED)
+
 
 def get_nodegroup_for_worker(request, uuid):
     """Get :class:`NodeGroup` by uuid, for access by its worker.

=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py	2012-09-12 15:54:38 +0000
+++ src/maasserver/tests/test_api.py	2012-09-13 16:44:38 +0000
@@ -48,6 +48,7 @@
     NODE_AFTER_COMMISSIONING_ACTION,
     NODE_STATUS,
     NODE_STATUS_CHOICES_DICT,
+    NODEGROUP_STATUS,
     )
 from maasserver.exceptions import Unauthorized
 from maasserver.fields import mac_error_msg
@@ -80,6 +81,7 @@
     LoggedInTestCase,
     TestCase,
     )
+from maasserver.tests.test_forms import make_interface_settings
 from maasserver.utils import map_enum
 from maasserver.utils.orm import get_one
 from maasserver.worker_user import get_worker_user
@@ -110,6 +112,7 @@
     Contains,
     Equals,
     MatchesListwise,
+    MatchesStructure,
     StartsWith,
     )
 
@@ -2402,6 +2405,102 @@
             (httplib.OK, "Sending worker refresh."),
             (response.status_code, response.content))
 
+    def test_register_nodegroup_creates_nodegroup_and_interfaces(self):
+        name = factory.make_name('name')
+        uuid = factory.getRandomUUID()
+        interface = make_interface_settings()
+        response = self.client.post(
+            reverse('nodegroups_handler'),
+            {
+                'op': 'register',
+                'name': name,
+                'uuid': uuid,
+                'interfaces': json.dumps([interface]),
+            })
+        nodegroup = NodeGroup.objects.get(uuid=uuid)
+        # The nodegroup was created with its interface.  Its status is
+        # 'PENDING'.
+        self.assertEqual(
+            (name, NODEGROUP_STATUS.PENDING),
+            (nodegroup.name, nodegroup.status))
+        self.assertThat(
+            nodegroup.nodegroupinterface_set.all()[0],
+            MatchesStructure.byEquality(**interface))
+        # The response code is 'ACCEPTED': the nodegroup now needs to be
+        # validated by an admin.
+        self.assertEqual(httplib.ACCEPTED, response.status_code)
+
+    def test_register_nodegroup_validates_data(self):
+        response = self.client.post(
+            reverse('nodegroups_handler'),
+            {
+                'op': 'register',
+                'name': factory.make_name('name'),
+                'uuid': factory.getRandomUUID(),
+                'interfaces': 'invalid data',
+            })
+        self.assertEqual(
+            (
+                httplib.BAD_REQUEST,
+                {'interfaces': ['Invalid json value.']},
+            ),
+            (response.status_code, json.loads(response.content)))
+
+    def test_register_nodegroup_twice_does_not_update_nodegroup(self):
+        nodegroup = factory.make_node_group()
+        nodegroup.status = NODEGROUP_STATUS.PENDING
+        nodegroup.save()
+        name = factory.make_name('name')
+        uuid = nodegroup.uuid
+        response = self.client.post(
+            reverse('nodegroups_handler'),
+            {
+                'op': 'register',
+                'name': name,
+                'uuid': uuid,
+                'interfaces': json.dumps([]),
+            })
+        new_nodegroup = NodeGroup.objects.get(uuid=uuid)
+        self.assertEqual(
+            (nodegroup.name, NODEGROUP_STATUS.PENDING),
+            (new_nodegroup.name, new_nodegroup.status))
+        # The response code is 'ACCEPTED': the nodegroup still needs to be
+        # validated by an admin.
+        self.assertEqual(httplib.ACCEPTED, response.status_code)
+
+    def test_register_rejected_nodegroup_fails(self):
+        nodegroup = factory.make_node_group()
+        nodegroup.status = NODEGROUP_STATUS.REJECTED
+        nodegroup.save()
+        response = self.client.post(
+            reverse('nodegroups_handler'),
+            {
+                'op': 'register',
+                'name': factory.make_name('name'),
+                'uuid': nodegroup.uuid,
+                'interfaces': json.dumps([]),
+            })
+        self.assertEqual(httplib.FORBIDDEN, response.status_code)
+
+    def test_register_accepted_cluster_gets_credentials(self):
+        nodegroup = factory.make_node_group()
+        nodegroup.status = NODEGROUP_STATUS.ACCEPTED
+        nodegroup.save()
+        response = self.client.post(
+            reverse('nodegroups_handler'),
+            {
+                'op': 'register',
+                'name': factory.make_name('name'),
+                'uuid': nodegroup.uuid,
+            })
+        self.assertEqual(httplib.OK, response.status_code)
+        parsed_result = json.loads(response.content)
+        self.assertIn('application/json', response['Content-Type'])
+        # XXX: rvb 2012-09-13 bug=1050492: MAAS uses the 'guest' account to
+        # communicate with RabbitMQ, hence none of the connection information
+        # are defined.
+        self.assertEqual({'test': 'test'}, parsed_result)
+
 
 def explain_unexpected_response(expected_status, response):
     """Return human-readable failure message: unexpected http response."""