← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Raphaël Badin has proposed merging lp:~rvb/maas/add-nodegroup-api into lp:maas.

Requested reviews:
  MAAS Maintainers (maas-maintainers)

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

This branch adds the forms used to register a new nodegroup along with its interfaces.  A follow-up branch will use them to in the API code to allow a cluster to register itself via an anonymous API call.

= Pre-imp =

This was pre-implemented with Julian and Jeroen.

= Notes =

- I had to change the 'editable' value of some fields on the models but this is required in order to be able to create forms with these fields from the model.  Note that 'editable'=True only means:
    - include this field when creating a form for that model and
    - include that field on the admin site.
-- 
https://code.launchpad.net/~rvb/maas/add-nodegroup-api/+merge/124251
Your team MAAS Maintainers is requested to review the proposed merge of lp:~rvb/maas/add-nodegroup-api into lp:maas.
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py	2012-09-05 13:30:21 +0000
+++ src/maasserver/forms.py	2012-09-13 16:35:22 +0000
@@ -19,12 +19,17 @@
     "HostnameFormField",
     "MACAddressForm",
     "MAASAndNetworkForm",
+    "NodeGroupInterfaceForm",
+    "NodeGroupWithInterfacesForm",
     "NodeWithMACAddressesForm",
     "SSHKeyForm",
     "UbuntuForm",
     "AdminNodeForm",
     ]
 
+import collections
+import json
+
 from django import forms
 from django.contrib import messages
 from django.contrib.auth.forms import (
@@ -53,6 +58,8 @@
     DNS_DHCP_MANAGEMENT_CHOICES,
     NODE_AFTER_COMMISSIONING_ACTION,
     NODE_AFTER_COMMISSIONING_ACTION_CHOICES,
+    NODEGROUP_STATUS,
+    NODEGROUPINTERFACE_MANAGEMENT,
     )
 from maasserver.fields import MACAddressFormField
 from maasserver.models import (
@@ -60,6 +67,7 @@
     MACAddress,
     Node,
     NodeGroup,
+    NodeGroupInterface,
     SSHKey,
     )
 from maasserver.node_action import compile_node_actions
@@ -598,3 +606,83 @@
         archives = Config.objects.get_config('update_from_choice')
         archives.append([archive_name, archive_name])
         Config.objects.set_config('update_from_choice', archives)
+
+
+class NodeGroupInterfaceForm(ModelForm):
+
+    class Meta:
+        model = NodeGroupInterface
+        fields = (
+            'ip',
+            'interface',
+            'subnet_mask',
+            'broadcast_ip',
+            'router_ip',
+            'ip_range_low',
+            'ip_range_high',
+            )
+
+    def save(self, nodegroup, management, *args, **kwargs):
+        interface = super(NodeGroupInterfaceForm, self).save(commit=False)
+        interface.nodegroup = nodegroup
+        interface.management = management
+        if kwargs.get('commit', True):
+            interface.save(*args, **kwargs)
+        return interface
+
+
+INTERFACES_VALIDATION_ERROR_MESSAGE = (
+    "Invalid json value: should be a list of dictionaries, each containing "
+    "the information needed to initialize an interface.")
+
+
+class NodeGroupWithInterfacesForm(ModelForm):
+    """Create a pending NodeGroup with unmanaged interfaces."""
+
+    interfaces = forms.CharField(required=False)
+
+    class Meta:
+        model = NodeGroup
+        fields = (
+            'name',
+            'uuid',
+            )
+
+    def clean_interfaces(self):
+        data = self.cleaned_data['interfaces']
+        # Stop here if the data is empty.
+        if data is '':
+            return data
+        try:
+            interfaces = json.loads(data)
+        except ValueError:
+            raise forms.ValidationError("Invalid json value.")
+        else:
+            # Raise an exception if the interfaces json object is not a list.
+            if not isinstance(interfaces, collections.Iterable):
+                raise forms.ValidationError(
+                    INTERFACES_VALIDATION_ERROR_MESSAGE)
+            for interface in interfaces:
+                # Raise an exception if the interface object is not a dict.
+                if not isinstance(interface, dict):
+                    raise forms.ValidationError(
+                        INTERFACES_VALIDATION_ERROR_MESSAGE)
+                form = NodeGroupInterfaceForm(data=interface)
+                if not form.is_valid():
+                    raise forms.ValidationError(
+                        "Invalid interface: %r (%r)." % (
+                            interface, form._errors))
+        return interfaces
+
+    def save(self):
+        nodegroup = super(NodeGroupWithInterfacesForm, self).save()
+        for interface in self.cleaned_data['interfaces']:
+            # Create an unmanaged interface.
+            form = NodeGroupInterfaceForm(data=interface)
+            form.save(
+                nodegroup=nodegroup,
+                management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
+        # Set the nodegroup to be 'PENDING'.
+        nodegroup.status = NODEGROUP_STATUS.PENDING
+        nodegroup.save()
+        return nodegroup

=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py	2012-09-13 10:40:38 +0000
+++ src/maasserver/models/nodegroup.py	2012-09-13 16:35:22 +0000
@@ -62,10 +62,6 @@
         - create the related NodeGroupInterface.
         - generate API credentials for the nodegroup's worker to use.
         """
-        # Avoid circular imports.
-        from maasserver.models.user import create_auth_token
-        from maasserver.worker_user import get_worker_user
-
         dhcp_values = [
             interface,
             subnet_mask,
@@ -77,10 +73,7 @@
         assert all(dhcp_values) or not any(dhcp_values), (
             "Provide all DHCP settings, or none at all.")
 
-        api_token = create_auth_token(get_worker_user())
-        nodegroup = NodeGroup(
-            name=name, uuid=uuid,
-            api_token=api_token, api_key=api_token.key, dhcp_key=dhcp_key)
+        nodegroup = NodeGroup(name=name, uuid=uuid, dhcp_key=dhcp_key)
         nodegroup.save()
         nginterface = NodeGroupInterface(
             nodegroup=nodegroup, ip=ip, subnet_mask=subnet_mask,
@@ -144,11 +137,22 @@
 
     # Unique identifier of the worker.
     uuid = CharField(
-        max_length=36, unique=True, null=False, blank=False, editable=False)
+        max_length=36, unique=True, null=False, blank=False, editable=True)
 
     def __repr__(self):
         return "<NodeGroup %r>" % self.name
 
+    def save(self, *args, **kwargs):
+        if self.api_token_id is None:
+            # Avoid circular imports.
+            from maasserver.models.user import create_auth_token
+            from maasserver.worker_user import get_worker_user
+
+            api_token = create_auth_token(get_worker_user())
+            self.api_token = api_token
+            self.api_key = api_token.key
+        return super(NodeGroup, self).save(*args, **kwargs)
+
     def get_managed_interface(self):
         """Return the interface for which MAAS managed the DHCP service.
 

=== modified file 'src/maasserver/models/nodegroupinterface.py'
--- src/maasserver/models/nodegroupinterface.py	2012-09-12 17:37:51 +0000
+++ src/maasserver/models/nodegroupinterface.py	2012-09-13 16:35:22 +0000
@@ -47,7 +47,7 @@
 
     # DHCP server settings.
     interface = CharField(
-        blank=True, editable=False, max_length=255, default='')
+        blank=True, editable=True, max_length=255, default='')
     subnet_mask = IPAddressField(
         editable=True, unique=False, blank=True, null=True, default=None)
     broadcast_ip = IPAddressField(

=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py	2012-08-10 14:55:04 +0000
+++ src/maasserver/tests/test_forms.py	2012-09-13 16:35:22 +0000
@@ -12,6 +12,8 @@
 __metaclass__ = type
 __all__ = []
 
+import json
+
 from django import forms
 from django.contrib.auth.models import User
 from django.core.exceptions import (
@@ -21,9 +23,11 @@
 from django.http import QueryDict
 from maasserver.enum import (
     ARCHITECTURE,
+    NODEGROUP_STATUS,
     ARCHITECTURE_CHOICES,
     NODE_AFTER_COMMISSIONING_ACTION_CHOICES,
     NODE_STATUS,
+    NODEGROUPINTERFACE_MANAGEMENT,
     )
 from maasserver.forms import (
     AdminNodeForm,
@@ -35,10 +39,13 @@
     get_node_edit_form,
     HostnameFormField,
     initialize_node_group,
+    INTERFACES_VALIDATION_ERROR_MESSAGE,
     MACAddressForm,
     NewUserCreationForm,
     NodeActionForm,
     NodeForm,
+    NodeGroupInterfaceForm,
+    NodeGroupWithInterfacesForm,
     NodeWithMACAddressesForm,
     ProfileForm,
     remove_None_values,
@@ -58,6 +65,7 @@
 from maasserver.testing.factory import factory
 from maasserver.testing.testcase import TestCase
 from provisioningserver.enum import POWER_TYPE_CHOICES
+from testtools.matchers import MatchesStructure
 from testtools.testcase import ExpectedException
 
 
@@ -565,3 +573,131 @@
             form._errors)
         self.assertFalse(
             MACAddress.objects.filter(node=node, mac_address=mac).exists())
+
+
+def make_interface_settings():
+    """Create a dict of arbitrary interface configuration parameters."""
+    return {
+        'ip': factory.getRandomIPAddress(),
+        'interface': factory.make_name('interface'),
+        'subnet_mask': factory.getRandomIPAddress(),
+        'broadcast_ip': factory.getRandomIPAddress(),
+        'router_ip': factory.getRandomIPAddress(),
+        'ip_range_low': factory.getRandomIPAddress(),
+        'ip_range_high': factory.getRandomIPAddress(),
+    }
+
+
+class TestNodeGroupInterfaceForm(TestCase):
+
+    def test_NodeGroupInterfaceForm_uses_initial_parameters(self):
+        nodegroup = factory.make_node_group()
+        management = factory.getRandomEnum(NODEGROUPINTERFACE_MANAGEMENT)
+        form = NodeGroupInterfaceForm(data=make_interface_settings())
+        interface = form.save(nodegroup=nodegroup, management=management)
+        self.assertEqual(
+            (nodegroup, management),
+            (interface.nodegroup, interface.management))
+
+    def test_NodeGroupInterfaceForm_validates_parameters(self):
+        form = NodeGroupInterfaceForm(data={'ip': factory.getRandomString()})
+        self.assertFalse(form.is_valid())
+        self.assertEquals(
+            {'ip': ['Enter a valid IPv4 address.']}, form._errors)
+
+
+class TestNodeGroupWithInterfacesForm(TestCase):
+
+    def test_NodeGroupWithInterfacesForm_creates_pending_nodegroup(self):
+        name = factory.make_name('name')
+        uuid = factory.getRandomUUID()
+        form = NodeGroupWithInterfacesForm(
+            data={'name': name, 'uuid': uuid})
+        self.assertTrue(form.is_valid(), form._errors)
+        nodegroup = form.save()
+        self.assertEqual(
+            (uuid, name, NODEGROUP_STATUS.PENDING, 0),
+            (
+                nodegroup.uuid,
+                nodegroup.name,
+                nodegroup.status,
+                nodegroup.nodegroupinterface_set.count(),
+            ))
+
+    def test_NodeGroupWithInterfacesForm_validates_parameters(self):
+        name = factory.make_name('name')
+        too_long_uuid = 'test' * 30
+        form = NodeGroupWithInterfacesForm(
+            data={'name': name, 'uuid': too_long_uuid})
+        self.assertFalse(form.is_valid())
+        self.assertEquals(
+            {'uuid':
+                ['Ensure this value has at most 36 characters (it has 120).']},
+            form._errors)
+
+    def test_NodeGroupWithInterfacesForm_rejects_invalid_json_interfaces(self):
+        name = factory.make_name('name')
+        uuid = factory.getRandomUUID()
+        invalid_interfaces = factory.make_name('invalid_json_interfaces')
+        form = NodeGroupWithInterfacesForm(
+            data={
+                'name': name, 'uuid': uuid, 'interfaces': invalid_interfaces})
+        self.assertFalse(form.is_valid())
+        self.assertEquals(
+            {'interfaces': ['Invalid json value.']},
+            form._errors)
+
+    def test_NodeGroupWithInterfacesForm_rejects_invalid_list_interfaces(self):
+        name = factory.make_name('name')
+        uuid = factory.getRandomUUID()
+        invalid_interfaces = json.dumps('invalid interface list')
+        form = NodeGroupWithInterfacesForm(
+            data={
+                'name': name, 'uuid': uuid, 'interfaces': invalid_interfaces})
+        self.assertFalse(form.is_valid())
+        self.assertEquals(
+            {'interfaces': [INTERFACES_VALIDATION_ERROR_MESSAGE]},
+            form._errors)
+
+    def test_NodeGroupWithInterfacesForm_rejects_invalid_interface(self):
+        name = factory.make_name('name')
+        uuid = factory.getRandomUUID()
+        interface = make_interface_settings()
+        # Make the interface invalid.
+        interface['ip_range_high'] = 'invalid IP address'
+        interfaces = json.dumps([interface])
+        form = NodeGroupWithInterfacesForm(
+            data={'name': name, 'uuid': uuid, 'interfaces': interfaces})
+        self.assertFalse(form.is_valid())
+        self.assertIn(
+            "Enter a valid IPv4 address", form._errors['interfaces'][0])
+
+    def test_NodeGroupWithInterfacesForm_creates_interface_from_params(self):
+        name = factory.make_name('name')
+        uuid = factory.getRandomUUID()
+        interface = make_interface_settings()
+        interfaces = json.dumps([interface])
+        form = NodeGroupWithInterfacesForm(
+            data={'name': name, 'uuid': uuid, 'interfaces': interfaces})
+        self.assertTrue(form.is_valid())
+        form.save()
+        nodegroup = NodeGroup.objects.get(uuid=uuid)
+        self.assertThat(
+            nodegroup.nodegroupinterface_set.all()[0],
+            MatchesStructure.byEquality(**interface))
+        self.assertEqual(
+            NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED,
+            nodegroup.nodegroupinterface_set.all()[0].management)
+
+    def test_NodeGroupWithInterfacesForm_creates_multiple_interfaces(self):
+        name = factory.make_name('name')
+        uuid = factory.getRandomUUID()
+        interface1 = make_interface_settings()
+        interface2 = make_interface_settings()
+        interfaces = json.dumps([interface1, interface2])
+        form = NodeGroupWithInterfacesForm(
+            data={'name': name, 'uuid': uuid, 'interfaces': interfaces})
+        self.assertTrue(form.is_valid())
+        form.save()
+        nodegroup = NodeGroup.objects.get(uuid=uuid)
+        self.assertEqual(2,  nodegroup.nodegroupinterface_set.count())