]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Ensure unique mac address allocation.
authorGary Kotton <gkotton@redhat.com>
Tue, 19 Jun 2012 23:32:22 +0000 (16:32 -0700)
committerDan Wendlandt <dan@nicira.com>
Tue, 19 Jun 2012 23:34:01 +0000 (16:34 -0700)
This is the first part of bug 1008029

If the port command does not contain a MAC address then Quantum will generate
a random MAC address. The mac address will be saved in the database to ensure
that it is not used by another port on the same network.

Added mock-based test for mac exhaustion.

Change-Id: I4d3fe12fd1e3c347b8e286d920a0609d0b3c4e8c

quantum/api/v2/base.py
quantum/common/exceptions.py
quantum/db/db_base_plugin_v2.py
quantum/tests/unit/test_db_plugin.py

index 82ee90df47aa1c51142cea3f37e1ef812e77f131..fbd92468c180a10c79b2f8dbf2281f86577819f8 100644 (file)
@@ -28,6 +28,8 @@ XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
 
 FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
              exceptions.InUse: webob.exc.HTTPConflict,
+             exceptions.MacAddressGenerationFailure:
+             webob.exc.HTTPServiceUnavailable,
              exceptions.StateInvalid: webob.exc.HTTPBadRequest}
 
 
index b726b49e85009927457327c7295daa0f1da99f53..c1a9fd3ad72d770cb1f07dec9d0eacf042642a70 100644 (file)
@@ -90,6 +90,11 @@ class PortInUse(InUse):
                 "is plugged into the logical port.")
 
 
+class MacAddressInUse(InUse):
+    message = _("Unable to complete operation for network %(net_id)s. "
+                "The mac address %(mac)s is in use.")
+
+
 class AlreadyAttached(QuantumException):
     message = _("Unable to plug the attachment %(att_id)s into port "
                 "%(port_id)s for network %(net_id)s. The attachment is "
@@ -115,3 +120,7 @@ class NotImplementedError(Error):
 class FixedIPNotAvailable(QuantumException):
     message = _("Fixed IP (%(ip)s) unavailable for network "
                 "%(network_uuid)s")
+
+
+class MacAddressGenerationFailure(QuantumException):
+    message = _("Unable to generate unique mac on network %(net_id)s.")
index 94c767814dc25ebd35f2d5a2d020a19c2909fd65..ffeae64f756b23323128526a8ea462a309ba1c17 100644 (file)
@@ -130,6 +130,37 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
                     collection = collection.filter(column.in_(value))
         return [dict_func(c, fields) for c in collection.all()]
 
+    @staticmethod
+    def _generate_mac(context, network_id):
+        # TODO(garyk) read from configuration file (CONF)
+        max_retries = 16
+        for i in range(max_retries):
+            # TODO(garyk) read base mac from configuration file (CONF)
+            mac = [0xfa, 0x16, 0x3e, random.randint(0x00, 0x7f),
+                   random.randint(0x00, 0xff), random.randint(0x00, 0xff)]
+            mac_address = ':'.join(map(lambda x: "%02x" % x, mac))
+            if QuantumDbPluginV2._check_unique_mac(context, network_id,
+                                                   mac_address):
+                LOG.debug("Generated mac for network %s is %s",
+                          network_id, mac_address)
+                return mac_address
+            else:
+                LOG.debug("Generated mac %s exists. Remaining attempts %s.",
+                          mac_address, max_retries - (i + 1))
+        LOG.error("Unable to generate mac address after %s attempts",
+                  max_retries)
+        raise q_exc.MacAddressGenerationFailure(net_id=network_id)
+
+    @staticmethod
+    def _check_unique_mac(context, network_id, mac_address):
+        mac_qry = context.session.query(models_v2.Port)
+        try:
+            mac_qry.filter_by(network_id=network_id,
+                              mac_address=mac_address).one()
+        except exc.NoResultFound:
+            return True
+        return False
+
     def _make_network_dict(self, network, fields=None):
         res = {'id': network['id'],
                'name': network['name'],
@@ -252,17 +283,22 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
         #                unneeded db action if the operation raises
         tenant_id = self._get_tenant_id_for_create(context, p)
 
-        if p['mac_address'] == api_router.ATTR_NOT_SPECIFIED:
-            #FIXME(danwent): this is exact Nova mac generation logic
-            # we will want to provide more flexibility and to check
-            # for uniqueness.
-            mac = [0xfa, 0x16, 0x3e, random.randint(0x00, 0x7f),
-                   random.randint(0x00, 0xff), random.randint(0x00, 0xff)]
-            p['mac_address'] = ':'.join(map(lambda x: "%02x" % x, mac))
-
         with context.session.begin():
             network = self._get_network(context, p["network_id"])
 
+            # Ensure that a MAC address is defined and it is unique on the
+            # network
+            if p['mac_address'] == api_router.ATTR_NOT_SPECIFIED:
+                p['mac_address'] = QuantumDbPluginV2._generate_mac(
+                    context, p["network_id"])
+            else:
+                # Ensure that the mac on the network is unique
+                if not QuantumDbPluginV2._check_unique_mac(context,
+                                                           p["network_id"],
+                                                           p['mac_address']):
+                    raise q_exc.MacAddressInUse(net_id=p["network_id"],
+                                                mac=p['mac_address'])
+
             port = models_v2.Port(tenant_id=tenant_id,
                                   network_id=p['network_id'],
                                   mac_address=p['mac_address'],
index 55b137803c5d42fae0bc98fefb661b09d65e91dd..46a22f95302ab24a86a8597b03d0056e9b9a7a8e 100644 (file)
 import logging
 import unittest
 import contextlib
+import mock
 
+import quantum
 from quantum.api.v2.router import APIRouter
+from quantum.common import exceptions as q_exc
 from quantum.db import api as db
 from quantum.tests.unit.testlib_api import create_request
 from quantum.wsgi import Serializer, JSONDeserializer
@@ -234,6 +237,38 @@ class TestPortsV2(QuantumDbPluginV2TestCase):
             self.assertEqual(res['port']['admin_state_up'],
                              data['port']['admin_state_up'])
 
+    def test_requested_duplicate_mac(self):
+        fmt = 'json'
+        with self.port() as port:
+            mac = port['port']['mac_address']
+            # check that MAC address matches base MAC
+            # TODO(garyk) read base mac from configuration file (CONF)
+            base_mac = [0xfa, 0x16, 0x3e]
+            base_mac_address = ':'.join(map(lambda x: "%02x" % x, base_mac))
+            self.assertTrue(mac.startswith(base_mac_address))
+            kwargs = {"mac_address": mac}
+            net_id = port['port']['network_id']
+            res = self._create_port(fmt, net_id=net_id, **kwargs)
+            port2 = self.deserialize(fmt, res)
+            self.assertEquals(res.status_int, 409)
+
+    def test_mac_exhaustion(self):
+        # rather than actually consuming all MAC (would take a LONG time)
+        # we just raise the exception that would result.
+        @staticmethod
+        def fake_gen_mac(context, net_id):
+            raise q_exc.MacAddressGenerationFailure(net_id=net_id)
+
+        fmt = 'json'
+        with mock.patch.object(quantum.db.db_base_plugin_v2.QuantumDbPluginV2,
+                               '_generate_mac', new=fake_gen_mac):
+            res = self._create_network(fmt=fmt, name='net1',
+                                       admin_status_up=True)
+            network = self.deserialize(fmt, res)
+            net_id = network['network']['id']
+            res = self._create_port(fmt, net_id=net_id)
+            self.assertEquals(res.status_int, 503)
+
 
 class TestNetworksV2(QuantumDbPluginV2TestCase):
     # NOTE(cerberus): successful network update and delete are