← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-unique-mac-bug-960694 into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-unique-mac-bug-960694 into lp:maas.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #960694 in MAAS: "Adding nodes are not mac validated"
  https://bugs.launchpad.net/maas/+bug/960694

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-unique-mac-bug-960694/+merge/98826

This branch adds unique=True to the mac_address field inside the MACAddress object.  South created the transition.  The only trick is that the validation error (that happens when one is trying to save a duplicated mac) is triggered internally by Django; since the form uses a MultipleMACAddressField I had to catch the ValidationError to re-raise a proper error.

Drive-by fix: I refactored the tests to remove the setUp method (which was creating objects for many tests that simply were not using them).
-- 
https://code.launchpad.net/~rvb/maas/maas-unique-mac-bug-960694/+merge/98826
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-unique-mac-bug-960694 into lp:maas.
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py	2012-03-21 07:30:59 +0000
+++ src/maasserver/forms.py	2012-03-22 12:18:31 +0000
@@ -120,10 +120,23 @@
                 ['One or more MAC Addresses is invalid.'])
         return valid
 
+    def add_mac(self, node, mac):
+        try:
+            node.add_mac_address(mac)
+        except ValidationError, e:
+            is_unique_validation_error = (
+                'mac_address' in e.message_dict and
+                e.message_dict['mac_address'] == [
+                    'Mac address with this Mac address already exists.'])
+            if is_unique_validation_error:
+                raise ValidationError(
+                    {'mac_addresses': [
+                        'Mac address %s already in use.' % mac]})
+
     def save(self):
         node = super(NodeWithMACAddressesForm, self).save()
         for mac in self.cleaned_data['mac_addresses']:
-            node.add_mac_address(mac)
+            self.add_mac(node, mac)
         if self.cleaned_data['hostname'] == "":
             node.set_mac_based_hostname(self.cleaned_data['mac_addresses'][0])
         return node

=== added file 'src/maasserver/migrations/0002_macaddress_unique.py'
--- src/maasserver/migrations/0002_macaddress_unique.py	1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/0002_macaddress_unique.py	2012-03-22 12:18:31 +0000
@@ -0,0 +1,104 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding unique constraint on 'MACAddress', fields ['mac_address']
+        db.create_unique('maasserver_macaddress', ['mac_address'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'MACAddress', fields ['mac_address']
+        db.delete_unique('maasserver_macaddress', ['mac_address'])
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'maasserver.config': {
+            'Meta': {'object_name': 'Config'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'value': ('maasserver.fields.JSONObjectField', [], {'null': 'True'})
+        },
+        'maasserver.filestorage': {
+            'Meta': {'object_name': 'FileStorage'},
+            'data': ('django.db.models.fields.files.FileField', [], {'max_length': '255'}),
+            'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'maasserver.macaddress': {
+            'Meta': {'object_name': 'MACAddress'},
+            'created': ('django.db.models.fields.DateField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'mac_address': ('maasserver.fields.MACAddressField', [], {'unique': 'True'}),
+            'node': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.Node']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {})
+        },
+        'maasserver.node': {
+            'Meta': {'object_name': 'Node'},
+            'after_commissioning_action': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'architecture': ('django.db.models.fields.CharField', [], {'default': "u'i386'", 'max_length': '10'}),
+            'created': ('django.db.models.fields.DateField', [], {}),
+            'hostname': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '255', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'power_type': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '10', 'blank': 'True'}),
+            'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '10'}),
+            'system_id': ('django.db.models.fields.CharField', [], {'default': "u'node-ce4489fe-73f9-11e1-a631-00219bd0a2de'", 'unique': 'True', 'max_length': '41'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {})
+        },
+        'maasserver.sshkeys': {
+            'Meta': {'object_name': 'SSHKeys'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.TextField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['maasserver.UserProfile']"})
+        },
+        'maasserver.userprofile': {
+            'Meta': {'object_name': 'UserProfile'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
+        }
+    }
+
+    complete_apps = ['maasserver']

=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py	2012-03-22 09:43:50 +0000
+++ src/maasserver/models.py	2012-03-22 12:18:31 +0000
@@ -464,7 +464,7 @@
     :ivar node: The `Node` related to this `MACAddress`.
 
     """
-    mac_address = MACAddressField()
+    mac_address = MACAddressField(unique=True)
     node = models.ForeignKey(Node, editable=False)
 
     class Meta:

=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py	2012-03-21 05:56:36 +0000
+++ src/maasserver/testing/factory.py	2012-03-22 12:18:31 +0000
@@ -69,6 +69,7 @@
         node = Node()
         node.save()
         mac = MACAddress(mac_address=address, node=node)
+        mac.save()
         return mac
 
     def make_user(self, username=None, password=None, email=None):

=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py	2012-03-21 05:56:36 +0000
+++ src/maasserver/tests/test_api.py	2012-03-22 12:18:31 +0000
@@ -182,6 +182,27 @@
         self.assertIn('text/html', response['Content-Type'])
         self.assertEqual("Unknown operation.", response.content)
 
+    def test_POST_fails_if_mac_duplicated(self):
+        # Mac Addresses should be unique.
+        mac = 'aa:bb:cc:dd:ee:ff'
+        factory.make_mac_address(mac)
+        architecture = factory.getRandomChoice(ARCHITECTURE_CHOICES)
+        response = self.client.post(
+            self.get_uri('nodes/'),
+            {
+                'op': 'new',
+                'architecture': architecture,
+                'hostname': factory.getRandomString(),
+                'mac_addresses': [mac],
+            })
+        parsed_result = json.loads(response.content)
+
+        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
+        self.assertIn('application/json', response['Content-Type'])
+        self.assertEqual(
+            ["Mac address %s already in use." % mac],
+            parsed_result['mac_addresses'])
+
     def test_POST_fails_with_bad_operation(self):
         # If the operation ('op=operation_name') specified in the
         # request data is unknown, a 'Bad request' response is returned.
@@ -667,24 +688,25 @@
 
 class MACAddressAPITest(APITestCase):
 
-    def setUp(self):
-        super(MACAddressAPITest, self).setUp()
-        self.node = factory.make_node()
-        self.mac1 = self.node.add_mac_address('aa:bb:cc:dd:ee:ff')
-        self.mac2 = self.node.add_mac_address('22:bb:cc:dd:aa:ff')
+    def createNodeWithMacs(self):
+        node = factory.make_node()
+        mac1 = node.add_mac_address('aa:bb:cc:dd:ee:ff')
+        mac2 = node.add_mac_address('22:bb:cc:dd:aa:ff')
+        return node, mac1, mac2
 
     def test_macs_GET(self):
         # The api allows for fetching the list of the MAC Addresss for a node.
+        node, mac1, mac2 = self.createNodeWithMacs()
         response = self.client.get(
-            self.get_uri('nodes/%s/macs/') % self.node.system_id)
+            self.get_uri('nodes/%s/macs/') % node.system_id)
         parsed_result = json.loads(response.content)
 
         self.assertEqual(httplib.OK, response.status_code)
         self.assertEqual(2, len(parsed_result))
         self.assertEqual(
-            self.mac1.mac_address, parsed_result[0]['mac_address'])
+            mac1.mac_address, parsed_result[0]['mac_address'])
         self.assertEqual(
-            self.mac2.mac_address, parsed_result[1]['mac_address'])
+            mac2.mac_address, parsed_result[1]['mac_address'])
 
     def test_macs_GET_forbidden(self):
         # When fetching MAC Addresses, the api returns a 'Forbidden' (403)
@@ -706,9 +728,10 @@
     def test_macs_GET_node_not_found(self):
         # When fetching a MAC Address, the api returns a 'Not Found' (404)
         # error if the MAC Address does not exist.
+        node = factory.make_node()
         response = self.client.get(
             self.get_uri(
-                'nodes/%s/macs/00-aa-22-cc-44-dd/') % self.node.system_id)
+                'nodes/%s/macs/00-aa-22-cc-44-dd/') % node.system_id)
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
 
@@ -726,30 +749,33 @@
     def test_macs_GET_node_bad_request(self):
         # When fetching a MAC Address, the api returns a 'Bad Request' (400)
         # error if the MAC Address is not valid.
+        node = factory.make_node()
         response = self.client.get(
-            self.get_uri('nodes/%s/macs/invalid-mac/') % self.node.system_id)
+            self.get_uri('nodes/%s/macs/invalid-mac/') % node.system_id)
 
         self.assertEqual(400, response.status_code)
 
     def test_macs_POST_add_mac(self):
         # The api allows to add a MAC Address to an existing node.
-        nb_macs = MACAddress.objects.filter(node=self.node).count()
+        node = factory.make_node()
+        nb_macs = MACAddress.objects.filter(node=node).count()
         response = self.client.post(
-            self.get_uri('nodes/%s/macs/') % self.node.system_id,
-            {'mac_address': 'AA:BB:CC:DD:EE:FF'})
+            self.get_uri('nodes/%s/macs/') % node.system_id,
+            {'mac_address': '01:BB:CC:DD:EE:FF'})
         parsed_result = json.loads(response.content)
 
         self.assertEqual(httplib.OK, response.status_code)
-        self.assertEqual('AA:BB:CC:DD:EE:FF', parsed_result['mac_address'])
+        self.assertEqual('01:BB:CC:DD:EE:FF', parsed_result['mac_address'])
         self.assertEqual(
             nb_macs + 1,
-            MACAddress.objects.filter(node=self.node).count())
+            MACAddress.objects.filter(node=node).count())
 
     def test_macs_POST_add_mac_invalid(self):
         # A 'Bad Request' response is returned if one tries to add an invalid
         # MAC Address to a node.
+        node = self.createNodeWithMacs()[0]
         response = self.client.post(
-            self.get_uri('nodes/%s/macs/') % self.node.system_id,
+            self.get_uri('nodes/%s/macs/') % node.system_id,
             {'mac_address': 'invalid-mac'})
         parsed_result = json.loads(response.content)
 
@@ -761,42 +787,46 @@
 
     def test_macs_DELETE_mac(self):
         # The api allows to delete a MAC Address.
-        nb_macs = self.node.macaddress_set.count()
+        node, mac1, mac2 = self.createNodeWithMacs()
+        nb_macs = node.macaddress_set.count()
         response = self.client.delete(
             self.get_uri('nodes/%s/macs/%s/') % (
-                self.node.system_id, self.mac1.mac_address))
+                node.system_id, mac1.mac_address))
 
         self.assertEqual(204, response.status_code)
         self.assertEqual(
             nb_macs - 1,
-            self.node.macaddress_set.count())
+            node.macaddress_set.count())
 
     def test_macs_DELETE_mac_forbidden(self):
         # When deleting a MAC Address, the api returns a 'Forbidden' (403)
         # error if the node is not visible to the logged-in user.
+        node, mac1, _ = self.createNodeWithMacs()
         other_node = factory.make_node(
             status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
         response = self.client.delete(
             self.get_uri('nodes/%s/macs/%s/') % (
-                other_node.system_id, self.mac1.mac_address))
+                other_node.system_id, mac1.mac_address))
 
         self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
     def test_macs_DELETE_not_found(self):
         # When deleting a MAC Address, the api returns a 'Not Found' (404)
         # error if no existing MAC Address is found.
+        node  = factory.make_node()
         response = self.client.delete(
             self.get_uri('nodes/%s/macs/%s/') % (
-                self.node.system_id, '00-aa-22-cc-44-dd'))
+                node.system_id, '00-aa-22-cc-44-dd'))
 
         self.assertEqual(httplib.NOT_FOUND, response.status_code)
 
     def test_macs_DELETE_bad_request(self):
         # When deleting a MAC Address, the api returns a 'Bad Request' (400)
         # error if the provided MAC Address is not valid.
+        node  = factory.make_node()
         response = self.client.delete(
             self.get_uri('nodes/%s/macs/%s/') % (
-                self.node.system_id, 'invalid-mac'))
+                node.system_id, 'invalid-mac'))
 
         self.assertEqual(httplib.BAD_REQUEST, response.status_code)