]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
L2 Model additions to support DVR
authorVivekanandan Narasimhan <vivekanandan.narasimhan@hp.com>
Tue, 24 Jun 2014 01:53:32 +0000 (18:53 -0700)
committerarmando-migliaccio <armamig@gmail.com>
Thu, 17 Jul 2014 14:53:14 +0000 (07:53 -0700)
This patch introduces the models, the DB migrations
and the config options required by the L2 layer to
support DVR east/west traffic.

These changes will be used by the control-plane made
of ML2, L2pop and L2 agent.

Two new configuration options have been introduced:
'dvr_base_mac' is used to set DVR MAC addresses apart
from tenant ones (every distributed router will have
ports being created on compute hosts) and
'enable_distributed_routing' is used to enable dvr
support in the L2 agent. This gives the capability of
rolling out the dvr functionality in stages.

Partially-implements: blueprint neutron-ovs-dvr

DocImpact

Change-Id: Iab6505f239d2c4c9bcbf4e32a292d7b4b5320c8e
Authored-by: Vivekanandan Narasimhan <vivekanandan.narasimhan@hp.com>
Co-Authored-By: Armando Migliaccio <armamig@gmail.com>
15 files changed:
etc/neutron.conf
etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini
neutron/common/utils.py
neutron/db/dvr_mac_db.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/2026156eab2f_l2_dvr_models.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/HEAD
neutron/db/migration/models/head.py
neutron/extensions/dvr.py
neutron/plugins/ml2/db.py
neutron/plugins/ml2/drivers/l2pop/db.py
neutron/plugins/ml2/models.py
neutron/plugins/openvswitch/common/config.py
neutron/tests/unit/db/test_dvr_mac_db.py [new file with mode: 0644]
neutron/tests/unit/ml2/db/__init__.py [new file with mode: 0644]
neutron/tests/unit/ml2/db/test_ml2_dvr_db.py [new file with mode: 0644]

index 9eeced849c828e6b0f6a3adef385f31035f201c8..aedc0fa9882996c22409a471c5fe48d31749c240 100644 (file)
@@ -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
 
index 5a378d8b20a1dbf65b2decff50f4c4a43c5236da..9c8e6b5889465cc7e64fc0d40b0190c0269fa3bf 100644 (file)
 #
 # 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
index 4378218e4de026fd70d7162abcf31ed1f1d68985..8521ec7f92e4db5de5d772924adff2d1960fcea1 100644 (file)
@@ -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 (file)
index 0000000..1e0913e
--- /dev/null
@@ -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 (file)
index 0000000..165117d
--- /dev/null
@@ -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')
index 7c42ceaf7bdd59f22e7086807e015b68ee8d729b..da886bc554dd8957d59a219d66da4a7c3822546a 100644 (file)
@@ -1 +1 @@
-3927f7f7c456
+2026156eab2f
index 9a36d2107cb401812a0a19cfe00104d807ef78bc..895d77afbc0a80ab46635076510d90fa326a6bde 100644 (file)
@@ -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
index 1569492621d3b7b114a0ebd08b545ac386da6cf9..bb5720e9dad065ae8a62792e265653529f340d7b 100644 (file)
 #    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
index 176be08da2b75d9e61829819c7322df261c7acbb..fc942e1439f9d9fa9a84ca22662ecc37974c3104 100644 (file)
@@ -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
index 9f42f978a6fb9bc68b07b95a2fdfca0b8a5d339f..9c6355504cee2b760fe3157f96759d7fac0b0121 100644 (file)
@@ -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())
index 9ec9d627bf825b3cf404a0d3233666a87b346e0f..9903017236a79c68d2529e9596a7ffb97d4843dd 100644 (file)
@@ -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'))
index 849044db6975c1b6110169158b28e075169cc3d2..455cdb0bcdbe0066876e8f895e0dbc543323461c 100644 (file)
@@ -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 (file)
index 0000000..75b1b0b
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
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 (file)
index 0000000..772b778
--- /dev/null
@@ -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))