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."""

_______________________________________________
Mailing list: https://launchpad.net/~launchpad-reviewers
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~launchpad-reviewers
More help   : https://help.launchpad.net/ListHelp

Reply via email to