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
This commit is contained in:
Gary Kotton 2012-06-19 16:32:22 -07:00 committed by Dan Wendlandt
parent cabd706b48
commit b6cb4316da
4 changed files with 90 additions and 8 deletions

View 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}

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

View 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'],

View File

@ -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