vmware-nsx/quantum/db/db_base_plugin_v2.py
Gary Kotton b6cb4316da 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
2012-06-19 16:34:01 -07:00

341 lines
14 KiB
Python

# Copyright (c) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import random
import netaddr
from sqlalchemy import orm
from sqlalchemy.orm import exc
from quantum import quantum_plugin_base_v2
from quantum.api.v2 import router as api_router
from quantum.common import exceptions as q_exc
from quantum.db import api as db
from quantum.db import models_v2
LOG = logging.getLogger(__name__)
class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
""" A class that implements the v2 Quantum plugin interface
using SQLAlchemy models. Whenever a non-read call happens
the plugin will call an event handler class method (e.g.,
network_created()). The result is that this class can be
sub-classed by other classes that add custom behaviors on
certain events.
"""
def __init__(self):
# NOTE(jkoelker) This is an incomlete implementation. Subclasses
# must override __init__ and setup the database
# and not call into this class's __init__.
# This connection is setup as memory for the tests.
sql_connection = 'sqlite:///:memory:'
db.configure_db({'sql_connection': sql_connection,
'base': models_v2.model_base.BASEV2})
def _get_tenant_id_for_create(self, context, resource):
if context.is_admin and 'tenant_id' in resource:
tenant_id = resource['tenant_id']
elif ('tenant_id' in resource and
resource['tenant_id'] != context.tenant_id):
reason = _('Cannot create resource for another tenant')
raise q_exc.AdminRequired(reason=reason)
else:
tenant_id = context.tenant_id
return tenant_id
def _model_query(self, context, model):
query = context.session.query(model)
# NOTE(jkoelker) non-admin queries are scoped to their tenant_id
if not context.is_admin and hasattr(model.tenant_id):
query = query.filter(tenant_id=context.tenant_id)
return query
def _get_by_id(self, context, model, id, joins=(), verbose=None):
query = self._model_query(context, model)
if verbose:
if verbose and isinstance(verbose, list):
options = [orm.joinedload(join) for join in joins
if join in verbose]
else:
options = [orm.joinedload(join) for join in joins]
query = query.options(*options)
return query.filter_by(id=id).one()
def _get_network(self, context, id, verbose=None):
try:
network = self._get_by_id(context, models_v2.Network, id,
joins=('subnets',), verbose=verbose)
except exc.NoResultFound:
raise q_exc.NetworkNotFound(net_id=id)
except exc.MultipleResultsFound:
LOG.error('Multiple networks match for %s' % id)
raise q_exc.NetworkNotFound(net_id=id)
return network
def _get_subnet(self, context, id, verbose=None):
try:
subnet = self._get_by_id(context, models_v2.Subnet, id,
verbose=verbose)
except exc.NoResultFound:
raise q_exc.SubnetNotFound(subnet_id=id)
except exc.MultipleResultsFound:
LOG.error('Multiple subnets match for %s' % id)
raise q_exc.SubnetNotFound(subnet_id=id)
return subnet
def _get_port(self, context, id, verbose=None):
try:
port = self._get_by_id(context, models_v2.Port, id,
verbose=verbose)
except exc.NoResultFound:
# NOTE(jkoelker) The PortNotFound exceptions requires net_id
# kwarg in order to set the message correctly
raise q_exc.PortNotFound(port_id=id, net_id=None)
except exc.MultipleResultsFound:
LOG.error('Multiple ports match for %s' % id)
raise q_exc.PortNotFound(port_id=id)
return port
def _fields(self, resource, fields):
if fields:
return dict(((key, item) for key, item in resource.iteritems()
if key in fields))
return resource
def _get_collection(self, context, model, dict_func, filters=None,
fields=None, verbose=None):
collection = self._model_query(context, model)
if filters:
for key, value in filters.iteritems():
column = getattr(model, key, None)
if column:
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'],
'tenant_id': network['tenant_id'],
'admin_state_up': network['admin_state_up'],
'status': network['status'],
'subnets': [subnet['id']
for subnet in network['subnets']]}
return self._fields(res, fields)
def _make_subnet_dict(self, subnet, fields=None):
res = {'id': subnet['id'],
'network_id': subnet['network_id'],
'ip_version': subnet['ip_version'],
'cidr': subnet['cidr'],
'gateway_ip': subnet['gateway_ip']}
return self._fields(res, fields)
def _make_port_dict(self, port, fields=None):
res = {"id": port["id"],
"network_id": port["network_id"],
'tenant_id': port['tenant_id'],
"mac_address": port["mac_address"],
"admin_state_up": port["admin_state_up"],
"status": port["status"],
"fixed_ips": [ip["address"] for ip in port["fixed_ips"]],
"device_id": port["device_id"]}
return self._fields(res, fields)
def create_network(self, context, network):
n = network['network']
# NOTE(jkoelker) Get the tenant_id outside of the session to avoid
# unneeded db action if the operation raises
tenant_id = self._get_tenant_id_for_create(context, n)
with context.session.begin():
network = models_v2.Network(tenant_id=tenant_id,
name=n['name'],
admin_state_up=n['admin_state_up'],
status="ACTIVE")
context.session.add(network)
return self._make_network_dict(network)
def update_network(self, context, id, network):
n = network['network']
with context.session.begin():
network = self._get_network(context, id)
network.update(n)
return self._make_network_dict(network)
def delete_network(self, context, id):
with context.session.begin():
network = self._get_network(context, id)
# TODO(anyone) Delegation?
ports_qry = context.session.query(models_v2.Port)
ports_qry.filter_by(network_id=id).delete()
subnets_qry = context.session.query(models_v2.Subnet)
subnets_qry.filter_by(network_id=id).delete()
context.session.delete(network)
def get_network(self, context, id, fields=None, verbose=None):
network = self._get_network(context, id, verbose=verbose)
return self._make_network_dict(network, fields)
def get_networks(self, context, filters=None, fields=None, verbose=None):
return self._get_collection(context, models_v2.Network,
self._make_network_dict,
filters=filters, fields=fields,
verbose=verbose)
def create_subnet(self, context, subnet):
s = subnet['subnet']
if s['gateway_ip'] == api_router.ATTR_NOT_SPECIFIED:
net = netaddr.IPNetwork(s['cidr'])
s['gateway_ip'] = str(netaddr.IPAddress(net.first + 1))
with context.session.begin():
subnet = models_v2.Subnet(network_id=s['network_id'],
ip_version=s['ip_version'],
cidr=s['cidr'],
gateway_ip=s['gateway_ip'])
context.session.add(subnet)
return self._make_subnet_dict(subnet)
def update_subnet(self, context, id, subnet):
s = subnet['subnet']
with context.session.begin():
subnet = self._get_subnet(context, id)
subnet.update(s)
return self._make_subnet_dict(subnet)
def delete_subnet(self, context, id):
with context.session.begin():
subnet = self._get_subnet(context, id)
allocations_qry = context.session.query(models_v2.IPAllocation)
allocations_qry.filter_by(subnet_id=id).delete()
context.session.delete(subnet)
def get_subnet(self, context, id, fields=None, verbose=None):
subnet = self._get_subnet(context, id, verbose=verbose)
return self._make_subnet_dict(subnet, fields)
def get_subnets(self, context, filters=None, fields=None, verbose=None):
return self._get_collection(context, models_v2.Subnet,
self._make_subnet_dict,
filters=filters, fields=fields,
verbose=verbose)
def create_port(self, context, port):
p = port['port']
# NOTE(jkoelker) Get the tenant_id outside of the session to avoid
# unneeded db action if the operation raises
tenant_id = self._get_tenant_id_for_create(context, p)
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'],
admin_state_up=p['admin_state_up'],
status="ACTIVE",
device_id=p['device_id'])
context.session.add(port)
# TODO(anyone) ip allocation
#for subnet in network["subnets"]:
# pass
return self._make_port_dict(port)
def update_port(self, context, id, port):
p = port['port']
with context.session.begin():
port = self._get_port(context, id)
port.update(p)
return self._make_port_dict(port)
def delete_port(self, context, id):
with context.session.begin():
port = self._get_port(context, id)
allocations_qry = context.session.query(models_v2.IPAllocation)
allocations_qry.filter_by(port_id=id).delete()
context.session.delete(port)
def get_port(self, context, id, fields=None, verbose=None):
port = self._get_port(context, id, verbose=verbose)
return self._make_port_dict(port, fields)
def get_ports(self, context, filters=None, fields=None, verbose=None):
return self._get_collection(context, models_v2.Port,
self._make_port_dict,
filters=filters, fields=fields,
verbose=verbose)