diff --git a/etc/neutron.conf b/etc/neutron.conf index 9eeced849c8..aedc0fa9882 100644 --- a/etc/neutron.conf +++ b/etc/neutron.conf @@ -87,6 +87,14 @@ lock_path = $state_path/lock # 4 octet # base_mac = fa:16:3e:4f:00:00 +# DVR Base MAC address. The first 3 octets will remain unchanged. If the +# 4th octet is not 00, it will also be used. The others will be randomly +# generated. The 'dvr_base_mac' *must* be different from 'base_mac' to +# avoid mixing them up with MAC's allocated for tenant ports. +# A 4 octet example would be dvr_base_mac = fa:16:3f:4f:00:00 +# The default is 3 octet +# dvr_base_mac = fa:16:3f:00:00:00 + # Maximum amount of retries to generate a unique MAC address # mac_generation_retries = 16 diff --git a/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini b/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini index 5a378d8b20a..9c8e6b58894 100644 --- a/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini +++ b/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini @@ -144,6 +144,11 @@ # # dont_fragment = True +# (BoolOpt) Set to True on L2 agents to enable support +# for distributed virtual routing. +# +# enable_distributed_routing = False + [securitygroup] # Firewall driver for realizing neutron security group function. # firewall_driver = neutron.agent.firewall.NoopFirewallDriver diff --git a/neutron/common/utils.py b/neutron/common/utils.py index 4378218e4de..8521ec7f92e 100644 --- a/neutron/common/utils.py +++ b/neutron/common/utils.py @@ -271,6 +271,15 @@ def is_valid_vlan_tag(vlan): return q_const.MIN_VLAN_TAG <= vlan <= q_const.MAX_VLAN_TAG +def get_random_mac(base_mac): + mac = [int(base_mac[0], 16), int(base_mac[1], 16), + int(base_mac[2], 16), random.randint(0x00, 0xff), + random.randint(0x00, 0xff), random.randint(0x00, 0xff)] + if base_mac[3] != '00': + mac[3] = int(base_mac[3], 16) + return ':'.join(["%02x" % x for x in mac]) + + def get_random_string(length): """Get a random hex string of the specified length. diff --git a/neutron/db/dvr_mac_db.py b/neutron/db/dvr_mac_db.py new file mode 100644 index 00000000000..1e0913e1db4 --- /dev/null +++ b/neutron/db/dvr_mac_db.py @@ -0,0 +1,157 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# All rights reserved. +# +# 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. + +from oslo.db import exception as db_exc + +import sqlalchemy as sa + +from neutron.common import exceptions as q_exc +from neutron.common import log +from neutron.common import utils +from neutron.db import model_base +from neutron.extensions import dvr as ext_dvr +from neutron import manager +from neutron.openstack.common import log as logging +from oslo.config import cfg +from sqlalchemy.orm import exc + +LOG = logging.getLogger(__name__) + +dvr_mac_address_opts = [ + cfg.StrOpt('dvr_base_mac', + default="fa:16:3f:00:00:00", + help=_('The base mac address used for unique ' + 'DVR instances by Neutron')), +] +cfg.CONF.register_opts(dvr_mac_address_opts) + + +class DistributedVirtualRouterMacAddress(model_base.BASEV2): + """Represents a v2 neutron distributed virtual router mac address.""" + + __tablename__ = 'dvr_host_macs' + + host = sa.Column(sa.String(255), primary_key=True, nullable=False) + mac_address = sa.Column(sa.String(32), nullable=False, unique=True) + + +class DVRDbMixin(ext_dvr.DVRMacAddressPluginBase): + """Mixin class to add dvr mac address to db_plugin_base_v2.""" + + @property + def plugin(self): + try: + if self._plugin is not None: + return self._plugin + except AttributeError: + pass + self._plugin = manager.NeutronManager.get_plugin() + return self._plugin + + def _get_dvr_mac_address_by_host(self, context, host): + try: + query = context.session.query(DistributedVirtualRouterMacAddress) + dvrma = query.filter( + DistributedVirtualRouterMacAddress.host == host).one() + except exc.NoResultFound: + raise ext_dvr.DVRMacAddressNotFound(host=host) + return dvrma + + def _create_dvr_mac_address(self, context, host): + """Create dvr mac address for a given host.""" + base_mac = cfg.CONF.dvr_base_mac.split(':') + max_retries = cfg.CONF.mac_generation_retries + for attempt in reversed(range(max_retries)): + try: + with context.session.begin(subtransactions=True): + mac_address = utils.get_random_mac(base_mac) + dvr_mac_binding = DistributedVirtualRouterMacAddress( + host=host, mac_address=mac_address) + context.session.add(dvr_mac_binding) + LOG.debug("Generated DVR mac for host %(host)s " + "is %(mac_address)s", + {'host': host, 'mac_address': mac_address}) + return self._make_dvr_mac_address_dict(dvr_mac_binding) + except db_exc.DBDuplicateEntry: + LOG.debug("Generated DVR mac %(mac)s exists." + " Remaining attempts %(attempts_left)s.", + {'mac': mac_address, 'attempts_left': attempt}) + LOG.error(_("MAC generation error after %s attempts"), max_retries) + raise ext_dvr.MacAddressGenerationFailure(host=host) + + def delete_dvr_mac_address(self, context, host): + query = context.session.query(DistributedVirtualRouterMacAddress) + (query. + filter(DistributedVirtualRouterMacAddress.host == host). + delete(synchronize_session=False)) + + def get_dvr_mac_address_list(self, context): + with context.session.begin(subtransactions=True): + return (context.session. + query(DistributedVirtualRouterMacAddress).all()) + + def get_dvr_mac_address_by_host(self, context, host): + """Determine the MAC for the DVR port associated to host.""" + if not host: + return + + try: + return self._get_dvr_mac_address_by_host(context, host) + except ext_dvr.DVRMacAddressNotFound: + return self._create_dvr_mac_address(context, host) + + def _make_dvr_mac_address_dict(self, dvr_mac_entry, fields=None): + return {'host': dvr_mac_entry['host'], + 'mac_address': dvr_mac_entry['mac_address']} + + @log.log + def get_compute_ports_on_host_by_subnet(self, context, host, subnet): + # FIXME(vivek, salv-orlando): improve this query by adding the + # capability of filtering by binding:host_id + vm_ports_by_host = [] + filter = {'fixed_ips': {'subnet_id': [subnet]}} + ports = self.plugin.get_ports(context, filters=filter) + LOG.debug("List of Ports on subnet %(subnet)s received as %(ports)s", + {'subnet': subnet, 'ports': ports}) + for port in ports: + if 'compute:' in port['device_owner']: + if port['binding:host_id'] == host: + port_dict = self.plugin._make_port_dict( + port, process_extensions=False) + vm_ports_by_host.append(port_dict) + LOG.debug("Returning list of VM Ports on host %(host)s for subnet " + "%(subnet)s ports %(ports)s", + {'host': host, 'subnet': subnet, 'ports': vm_ports_by_host}) + return vm_ports_by_host + + @log.log + def get_subnet_for_dvr(self, context, subnet): + try: + subnet_info = self.plugin.get_subnet(context, subnet) + except q_exc.SubnetNotFound: + return {} + else: + # retrieve the gateway port on this subnet + filter = {'fixed_ips': {'subnet_id': [subnet], + 'ip_address': [subnet_info['gateway_ip']]}} + internal_gateway_ports = self.plugin.get_ports( + context, filters=filter) + if not internal_gateway_ports: + LOG.error(_("Could not retrieve gateway port " + "for subnet %s"), subnet_info) + return {} + internal_port = internal_gateway_ports[0] + subnet_info['gateway_mac'] = internal_port['mac_address'] + return subnet_info diff --git a/neutron/db/migration/alembic_migrations/versions/2026156eab2f_l2_dvr_models.py b/neutron/db/migration/alembic_migrations/versions/2026156eab2f_l2_dvr_models.py new file mode 100644 index 00000000000..165117d1fd9 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/2026156eab2f_l2_dvr_models.py @@ -0,0 +1,78 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. +# + +"""L2 models to support DVR + +Revision ID: 2026156eab2f +Revises: 3927f7f7c456 +Create Date: 2014-06-23 19:12:43.392912 + +""" + +# revision identifiers, used by Alembic. +revision = '2026156eab2f' +down_revision = '3927f7f7c456' + +migration_for_plugins = [ + '*' +] + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration + + +def upgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.create_table( + 'dvr_host_macs', + sa.Column('host', sa.String(length=255), nullable=False), + sa.Column('mac_address', sa.String(length=32), + nullable=False, unique=True), + sa.PrimaryKeyConstraint('host') + ) + op.create_table( + 'ml2_dvr_port_bindings', + sa.Column('port_id', sa.String(length=36), nullable=False), + sa.Column('host', sa.String(length=255), nullable=False), + sa.Column('router_id', sa.String(length=36), nullable=True), + sa.Column('vif_type', sa.String(length=64), nullable=False), + sa.Column('vif_details', sa.String(length=4095), + nullable=False, server_default=''), + sa.Column('vnic_type', sa.String(length=64), + nullable=False, server_default='normal'), + sa.Column('profile', sa.String(length=4095), + nullable=False, server_default=''), + sa.Column('cap_port_filter', sa.Boolean(), nullable=False), + sa.Column('driver', sa.String(length=64), nullable=True), + sa.Column('segment', sa.String(length=36), nullable=True), + sa.Column(u'status', sa.String(16), nullable=False), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], + ondelete='CASCADE'), + sa.ForeignKeyConstraint(['segment'], ['ml2_network_segments.id'], + ondelete='SET NULL'), + sa.PrimaryKeyConstraint('port_id', 'host') + ) + + +def downgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.drop_table('ml2_dvr_port_bindings') + op.drop_table('dvr_host_macs') diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index 7c42ceaf7bd..da886bc554d 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -3927f7f7c456 +2026156eab2f diff --git a/neutron/db/migration/models/head.py b/neutron/db/migration/models/head.py index 9a36d2107cb..895d77afbc0 100644 --- a/neutron/db/migration/models/head.py +++ b/neutron/db/migration/models/head.py @@ -24,6 +24,7 @@ Based on this comparison database can be healed with healing migration. from neutron.db import agents_db # noqa from neutron.db import agentschedulers_db # noqa from neutron.db import allowedaddresspairs_db # noqa +from neutron.db import dvr_mac_db # noqa from neutron.db import external_net_db # noqa from neutron.db import extradhcpopt_db # noqa from neutron.db import extraroute_db # noqa diff --git a/neutron/extensions/dvr.py b/neutron/extensions/dvr.py index 1569492621d..bb5720e9dad 100644 --- a/neutron/extensions/dvr.py +++ b/neutron/extensions/dvr.py @@ -12,8 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. +import abc + +import six + from neutron.api.v2 import attributes from neutron.common import constants +from neutron.common import exceptions DISTRIBUTED = 'distributed' EXTENDED_ATTRIBUTES_2_0 = { @@ -28,6 +33,15 @@ EXTENDED_ATTRIBUTES_2_0 = { } +class DVRMacAddressNotFound(exceptions.NotFound): + message = _("Distributed Virtual Router Mac Address for " + "host %(host)s does not exist.") + + +class MacAddressGenerationFailure(exceptions.ServiceUnavailable): + message = _("Unable to generate unique DVR mac for host %(host)s.") + + class Dvr(object): """Extension class supporting distributed virtual router.""" @@ -65,3 +79,19 @@ class Dvr(object): return EXTENDED_ATTRIBUTES_2_0 else: return {} + + +@six.add_metaclass(abc.ABCMeta) +class DVRMacAddressPluginBase(object): + + @abc.abstractmethod + def delete_dvr_mac_address(self, context, host): + pass + + @abc.abstractmethod + def get_dvr_mac_address_list(self, context): + pass + + @abc.abstractmethod + def get_dvr_mac_address_by_host(self, context, host): + pass diff --git a/neutron/plugins/ml2/db.py b/neutron/plugins/ml2/db.py index 176be08da2b..fc942e1439f 100644 --- a/neutron/plugins/ml2/db.py +++ b/neutron/plugins/ml2/db.py @@ -15,6 +15,7 @@ from sqlalchemy.orm import exc +from neutron.common import constants as n_const from neutron.db import api as db_api from neutron.db import models_v2 from neutron.db import securitygroups_db as sg_db @@ -88,6 +89,36 @@ def get_locked_port_and_binding(session, port_id): return None, None +def ensure_dvr_port_binding(session, port_id, host, router_id=None): + # FIXME(armando-migliaccio): take care of LP #1335226 + # DVR ports are slightly different from the others in + # that binding happens at a later stage via L3 agent + # therefore we need to keep this logic of creation on + # missing binding. + with session.begin(subtransactions=True): + try: + record = (session.query(models.DVRPortBinding). + filter_by(port_id=port_id, host=host).one()) + except exc.NoResultFound: + record = models.DVRPortBinding( + port_id=port_id, + host=host, + router_id=router_id, + vif_type=portbindings.VIF_TYPE_UNBOUND, + vnic_type=portbindings.VNIC_NORMAL, + cap_port_filter=False, + status=n_const.PORT_STATUS_DOWN) + session.add(record) + return record + + +def delete_dvr_port_binding(session, port_id, host): + with session.begin(subtransactions=True): + (session.query(models.DVRPortBinding). + filter_by(port_id=port_id, host=host). + delete(synchronize_session=False)) + + def get_port(session, port_id): """Get port record for update within transcation.""" @@ -156,3 +187,37 @@ def get_port_binding_host(port_id): port_id) return return query.host + + +def generate_dvr_port_status(session, port_id): + # an OR'ed value of status assigned to parent port from the + # dvrportbinding bucket + query = session.query(models.DVRPortBinding) + final_status = n_const.PORT_STATUS_BUILD + for bind in query.filter(models.DVRPortBinding.port_id == port_id): + if bind.status == n_const.PORT_STATUS_ACTIVE: + return bind.status + elif bind.status == n_const.PORT_STATUS_DOWN: + final_status = bind.status + return final_status + + +def get_dvr_port_binding_by_host(session, port_id, host): + with session.begin(subtransactions=True): + binding = (session.query(models.DVRPortBinding). + filter(models.DVRPortBinding.port_id.startswith(port_id), + models.DVRPortBinding.host == host).first()) + if not binding: + LOG.debug("No binding for DVR port %(port_id)s with host " + "%(host)s", {'port_id': port_id, 'host': host}) + return binding + + +def get_dvr_port_bindings(session, port_id): + with session.begin(subtransactions=True): + bindings = (session.query(models.DVRPortBinding). + filter(models.DVRPortBinding.port_id.startswith(port_id)). + all()) + if not bindings: + LOG.debug("No bindings for DVR port %s", port_id) + return bindings diff --git a/neutron/plugins/ml2/drivers/l2pop/db.py b/neutron/plugins/ml2/drivers/l2pop/db.py index 9f42f978a6f..9c6355504ce 100644 --- a/neutron/plugins/ml2/drivers/l2pop/db.py +++ b/neutron/plugins/ml2/drivers/l2pop/db.py @@ -70,14 +70,44 @@ class L2populationDbMixin(base_db.CommonDbMixin): l2_const.SUPPORTED_AGENT_TYPES)) return query + def get_nondvr_network_ports(self, session, network_id): + query = self.get_network_ports(session, network_id) + return query.filter(models_v2.Port.device_owner != + const.DEVICE_OWNER_DVR_INTERFACE) + + def get_dvr_network_ports(self, session, network_id): + with session.begin(subtransactions=True): + query = session.query(ml2_models.DVRPortBinding, + agents_db.Agent) + query = query.join(agents_db.Agent, + agents_db.Agent.host == + ml2_models.DVRPortBinding.host) + query = query.join(models_v2.Port) + query = query.filter(models_v2.Port.network_id == network_id, + models_v2.Port.admin_state_up == sql.true(), + models_v2.Port.device_owner == + const.DEVICE_OWNER_DVR_INTERFACE, + agents_db.Agent.agent_type.in_( + l2_const.SUPPORTED_AGENT_TYPES)) + return query + def get_agent_network_active_port_count(self, session, agent_host, network_id): with session.begin(subtransactions=True): query = session.query(models_v2.Port) - - query = query.join(ml2_models.PortBinding) - query = query.filter(models_v2.Port.network_id == network_id, - models_v2.Port.status == - const.PORT_STATUS_ACTIVE, - ml2_models.PortBinding.host == agent_host) - return query.count() + query1 = query.join(ml2_models.PortBinding) + query1 = query1.filter(models_v2.Port.network_id == network_id, + models_v2.Port.status == + const.PORT_STATUS_ACTIVE, + models_v2.Port.device_owner != + const.DEVICE_OWNER_DVR_INTERFACE, + ml2_models.PortBinding.host == agent_host) + query2 = query.join(ml2_models.DVRPortBinding) + query2 = query2.filter(models_v2.Port.network_id == network_id, + ml2_models.DVRPortBinding.status == + const.PORT_STATUS_ACTIVE, + models_v2.Port.device_owner == + const.DEVICE_OWNER_DVR_INTERFACE, + ml2_models.DVRPortBinding.host == + agent_host) + return (query1.count() + query2.count()) diff --git a/neutron/plugins/ml2/models.py b/neutron/plugins/ml2/models.py index 9ec9d627bf8..9903017236a 100644 --- a/neutron/plugins/ml2/models.py +++ b/neutron/plugins/ml2/models.py @@ -77,3 +77,40 @@ class PortBinding(model_base.BASEV2): backref=orm.backref("port_binding", lazy='joined', uselist=False, cascade='delete')) + + +class DVRPortBinding(model_base.BASEV2): + """Represent binding-related state of a DVR port. + + Port binding for all the ports associated to a DVR identified by router_id. + """ + + __tablename__ = 'ml2_dvr_port_bindings' + + port_id = sa.Column(sa.String(36), + sa.ForeignKey('ports.id', ondelete="CASCADE"), + primary_key=True) + host = sa.Column(sa.String(255), nullable=False, primary_key=True) + router_id = sa.Column(sa.String(36), nullable=True) + vif_type = sa.Column(sa.String(64), nullable=False) + vif_details = sa.Column(sa.String(4095), nullable=False, default='', + server_default='') + vnic_type = sa.Column(sa.String(64), nullable=False, + default=portbindings.VNIC_NORMAL, + server_default=portbindings.VNIC_NORMAL) + profile = sa.Column(sa.String(BINDING_PROFILE_LEN), nullable=False, + default='', server_default='') + cap_port_filter = sa.Column(sa.Boolean, nullable=False) + driver = sa.Column(sa.String(64)) + segment = sa.Column(sa.String(36), + sa.ForeignKey('ml2_network_segments.id', + ondelete="SET NULL")) + status = sa.Column(sa.String(16), nullable=False) + + # Add a relationship to the Port model in order to instruct SQLAlchemy to + # eagerly load port bindings + port = orm.relationship( + models_v2.Port, + backref=orm.backref("dvr_port_binding", + lazy='joined', uselist=False, + cascade='delete')) diff --git a/neutron/plugins/openvswitch/common/config.py b/neutron/plugins/openvswitch/common/config.py index 849044db697..455cdb0bcdb 100644 --- a/neutron/plugins/openvswitch/common/config.py +++ b/neutron/plugins/openvswitch/common/config.py @@ -86,6 +86,8 @@ agent_opts = [ cfg.BoolOpt('dont_fragment', default=True, help=_("Set or un-set the don't fragment (DF) bit on " "outgoing IP packet carrying GRE/VXLAN tunnel")), + cfg.BoolOpt('enable_distributed_routing', default=False, + help=_("Make the l2 agent run in DVR mode ")), ] diff --git a/neutron/tests/unit/db/test_dvr_mac_db.py b/neutron/tests/unit/db/test_dvr_mac_db.py new file mode 100644 index 00000000000..75b1b0b65d5 --- /dev/null +++ b/neutron/tests/unit/db/test_dvr_mac_db.py @@ -0,0 +1,102 @@ +# Copyright (c) 2014 OpenStack Foundation, all rights reserved. +# +# 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 mock +from oslo.config import cfg + +from neutron import context +from neutron.db import api as db +from neutron.db import dvr_mac_db +from neutron.extensions import dvr +from neutron.tests import base + + +class DVRDbMixinImpl(dvr_mac_db.DVRDbMixin): + + def __init__(self, notifier): + self.notifier = notifier + + +class DvrDbMixinTestCase(base.BaseTestCase): + + def setUp(self): + super(DvrDbMixinTestCase, self).setUp() + db.configure_db() + self.ctx = context.get_admin_context() + self.addCleanup(db.clear_db) + self.mixin = DVRDbMixinImpl(mock.Mock()) + + def _create_dvr_mac_entry(self, host, mac_address): + with self.ctx.session.begin(subtransactions=True): + entry = dvr_mac_db.DistributedVirtualRouterMacAddress( + host=host, mac_address=mac_address) + self.ctx.session.add(entry) + + def test__get_dvr_mac_address_by_host(self): + with self.ctx.session.begin(subtransactions=True): + entry = dvr_mac_db.DistributedVirtualRouterMacAddress( + host='foo_host', mac_address='foo_mac_address') + self.ctx.session.add(entry) + result = self.mixin._get_dvr_mac_address_by_host(self.ctx, 'foo_host') + self.assertEqual(entry, result) + + def test__get_dvr_mac_address_by_host_not_found(self): + self.assertRaises(dvr.DVRMacAddressNotFound, + self.mixin._get_dvr_mac_address_by_host, + self.ctx, 'foo_host') + + def test__create_dvr_mac_address_success(self): + entry = {'host': 'foo_host', 'mac_address': '00:11:22:33:44:55:66'} + with mock.patch.object(dvr_mac_db.utils, 'get_random_mac') as f: + f.return_value = entry['mac_address'] + expected = self.mixin._create_dvr_mac_address( + self.ctx, entry['host']) + self.assertEqual(expected, entry) + + def test__create_dvr_mac_address_retries_exceeded_retry_logic(self): + new_retries = 8 + cfg.CONF.set_override('mac_generation_retries', new_retries) + self._create_dvr_mac_entry('foo_host_1', 'non_unique_mac') + with mock.patch.object(dvr_mac_db.utils, 'get_random_mac') as f: + f.return_value = 'non_unique_mac' + self.assertRaises(dvr.MacAddressGenerationFailure, + self.mixin._create_dvr_mac_address, + self.ctx, "foo_host_2") + self.assertEqual(new_retries, f.call_count) + + def test_delete_dvr_mac_address(self): + self._create_dvr_mac_entry('foo_host', 'foo_mac_address') + self.mixin.delete_dvr_mac_address(self.ctx, 'foo_host') + count = self.ctx.session.query( + dvr_mac_db.DistributedVirtualRouterMacAddress).count() + self.assertFalse(count) + + def test_get_dvr_mac_address_list(self): + self._create_dvr_mac_entry('host_1', 'mac_1') + self._create_dvr_mac_entry('host_2', 'mac_2') + mac_list = self.mixin.get_dvr_mac_address_list(self.ctx) + self.assertEqual(2, len(mac_list)) + + def test_get_dvr_mac_address_by_host_existing_host(self): + self._create_dvr_mac_entry('foo_host', 'foo_mac') + with mock.patch.object(self.mixin, + '_get_dvr_mac_address_by_host') as f: + self.mixin.get_dvr_mac_address_by_host(self.ctx, 'foo_host') + self.assertEqual(1, f.call_count) + + def test_get_dvr_mac_address_by_host_missing_host(self): + with mock.patch.object(self.mixin, '_create_dvr_mac_address') as f: + self.mixin.get_dvr_mac_address_by_host(self.ctx, 'foo_host') + self.assertEqual(1, f.call_count) diff --git a/neutron/tests/unit/ml2/db/__init__.py b/neutron/tests/unit/ml2/db/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/ml2/db/test_ml2_dvr_db.py b/neutron/tests/unit/ml2/db/test_ml2_dvr_db.py new file mode 100644 index 00000000000..772b778f9e7 --- /dev/null +++ b/neutron/tests/unit/ml2/db/test_ml2_dvr_db.py @@ -0,0 +1,130 @@ +# Copyright (c) 2014 OpenStack Foundation, all rights reserved. +# +# 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. + +from neutron import context +from neutron.db import api as db_api +from neutron.db import l3_db +from neutron.db import models_v2 +from neutron.extensions import portbindings +from neutron.plugins.ml2 import db as ml2_db +from neutron.plugins.ml2 import models as ml2_models +from neutron.tests import base + + +class Ml2DBTestCase(base.BaseTestCase): + + def setUp(self): + super(Ml2DBTestCase, self).setUp() + db_api.configure_db() + self.ctx = context.get_admin_context() + self.addCleanup(db_api.clear_db) + + def _setup_neutron_network(self, network_id, port_ids): + with self.ctx.session.begin(subtransactions=True): + self.ctx.session.add(models_v2.Network(id=network_id)) + ports = [] + for port_id in port_ids: + port = models_v2.Port(id=port_id, + network_id=network_id, + mac_address='foo_mac_address', + admin_state_up=True, + status='ACTIVE', + device_id='', + device_owner='') + self.ctx.session.add(port) + ports.append(port) + return ports + + def _setup_neutron_router(self): + with self.ctx.session.begin(subtransactions=True): + router = l3_db.Router() + self.ctx.session.add(router) + return router + + def _setup_dvr_binding(self, network_id, port_id, router_id, host_id): + with self.ctx.session.begin(subtransactions=True): + record = ml2_models.DVRPortBinding( + port_id=port_id, + host=host_id, + router_id=router_id, + vif_type=portbindings.VIF_TYPE_UNBOUND, + vnic_type=portbindings.VNIC_NORMAL, + cap_port_filter=False, + status='DOWN') + self.ctx.session.add(record) + return record + + def test_ensure_dvr_port_binding(self): + network_id = 'foo_network_id' + port_id = 'foo_port_id' + self._setup_neutron_network(network_id, [port_id]) + router = self._setup_neutron_router() + ml2_db.ensure_dvr_port_binding( + self.ctx.session, port_id, 'foo_host', router.id) + expected = (self.ctx.session.query(ml2_models.DVRPortBinding). + filter_by(port_id=port_id).one()) + self.assertEqual(expected.port_id, port_id) + + def test_ensure_dvr_port_binding_multiple_bindings(self): + network_id = 'foo_network_id' + port_id = 'foo_port_id' + self._setup_neutron_network(network_id, [port_id]) + router = self._setup_neutron_router() + ml2_db.ensure_dvr_port_binding( + self.ctx.session, port_id, 'foo_host_1', router.id) + ml2_db.ensure_dvr_port_binding( + self.ctx.session, port_id, 'foo_host_2', router.id) + bindings = (self.ctx.session.query(ml2_models.DVRPortBinding). + filter_by(port_id=port_id).all()) + self.assertEqual(2, len(bindings)) + + def test_delete_dvr_port_binding(self): + network_id = 'foo_network_id' + port_id = 'foo_port_id' + self._setup_neutron_network(network_id, [port_id]) + router = self._setup_neutron_router() + binding = self._setup_dvr_binding( + network_id, port_id, router.id, 'foo_host_id') + ml2_db.delete_dvr_port_binding( + self.ctx.session, port_id, 'foo_host_id') + count = (self.ctx.session.query(ml2_models.DVRPortBinding). + filter_by(port_id=binding.port_id).count()) + self.assertFalse(count) + + def test_delete_dvr_port_binding_not_found(self): + ml2_db.delete_dvr_port_binding( + self.ctx.session, 'foo_port_id', 'foo_host') + + def test_get_dvr_port_binding_by_host_not_found(self): + port = ml2_db.get_dvr_port_binding_by_host( + self.ctx.session, 'foo_port_id', 'foo_host_id') + self.assertIsNone(port) + + def test_get_dvr_port_bindings_not_found(self): + port = ml2_db.get_dvr_port_bindings(self.ctx.session, 'foo_port_id') + self.assertFalse(len(port)) + + def test_get_dvr_port_bindings(self): + network_id = 'foo_network_id' + port_id_1 = 'foo_port_id_1' + port_id_2 = 'foo_port_id_2' + self._setup_neutron_network(network_id, [port_id_1, port_id_2]) + router = self._setup_neutron_router() + self._setup_dvr_binding( + network_id, port_id_1, router.id, 'foo_host_id_1') + self._setup_dvr_binding( + network_id, port_id_1, router.id, 'foo_host_id_2') + ports = ml2_db.get_dvr_port_bindings(self.ctx.session, 'foo_port_id') + self.assertEqual(2, len(ports))