From: Gary Kotton Date: Tue, 19 Jun 2012 23:32:22 +0000 (-0700) Subject: Ensure unique mac address allocation. X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=b6cb4316da8559398653e24de550b321f12e3e86;p=openstack-build%2Fneutron-build.git Ensure unique mac address allocation. 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 --- diff --git a/quantum/api/v2/base.py b/quantum/api/v2/base.py index 82ee90df4..fbd92468c 100644 --- a/quantum/api/v2/base.py +++ b/quantum/api/v2/base.py @@ -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} diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index b726b49e8..c1a9fd3ad 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -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.") diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index 94c767814..ffeae64f7 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -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'], diff --git a/quantum/tests/unit/test_db_plugin.py b/quantum/tests/unit/test_db_plugin.py index 55b137803..46a22f953 100644 --- a/quantum/tests/unit/test_db_plugin.py +++ b/quantum/tests/unit/test_db_plugin.py @@ -16,8 +16,11 @@ 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