b6cb4316da
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
341 lines
14 KiB
Python
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)
|