]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add support to request vnic type on port
authorIrena Berezovsky <irenab@mellanox.com>
Mon, 10 Feb 2014 12:55:49 +0000 (14:55 +0200)
committerThomas Goirand <thomas@goirand.fr>
Thu, 13 Mar 2014 07:20:25 +0000 (15:20 +0800)
This patch adds support for requested vnic_type to be plugged to neutron port to ML2 plugin.
This patch contains:
1. New attribute 'binding:vnic_type' added to port binding extension.
   Possible values are 'direct', 'macvtap' and 'normal'.
   'binding:vnic_type' is allowed to be defined on port creation or changed
   on port update by admin or tenant user.
   'binding:vnic_type' can be also skipped in port defintion
2. Management of vnic_type by ML2 plugin, assuming default
vnic_type=normal
3. Add 'vnic_type' to ml2_port_bindings DB table
4. Add supported vnic_types for MechanismDrivers that are capable to bind
port.
5. Add DB migration script for ml2_vnic_type.

DocImpact: Need to update portbindings API docs and include in SR-IOV user docs

Change-Id: Ic88708fa9ece742f807c1d09bb49e499f99bd092
Implements: blueprint ml2-request-vnic-type

etc/policy.json
neutron/db/migration/alembic_migrations/versions/27cc183af192_ml2_vnic_type.py [new file with mode: 0644]
neutron/db/portbindings_db.py
neutron/extensions/portbindings.py
neutron/plugins/ml2/drivers/mech_agent.py
neutron/plugins/ml2/managers.py
neutron/plugins/ml2/models.py
neutron/plugins/ml2/plugin.py
neutron/tests/unit/_test_extension_portbindings.py
neutron/tests/unit/ml2/_test_mech_agent.py
neutron/tests/unit/ml2/test_ml2_plugin.py

index a72d3a93d1623078af9bd0dd348dcf8daf580d71..bd0bc9274c704bc4d730b54575ed580944c2a897 100644 (file)
@@ -47,6 +47,7 @@
     "create_port:port_security_enabled": "rule:admin_or_network_owner",
     "create_port:binding:host_id": "rule:admin_only",
     "create_port:binding:profile": "rule:admin_only",
+    "create_port:binding:vnic_type": "rule:admin_or_owner",
     "create_port:mac_learning_enabled": "rule:admin_or_network_owner",
     "get_port": "rule:admin_or_owner",
     "get_port:queue_id": "rule:admin_only",
     "get_port:binding:capabilities": "rule:admin_only",
     "get_port:binding:host_id": "rule:admin_only",
     "get_port:binding:profile": "rule:admin_only",
+    "get_port:binding:vnic_type": "rule:admin_or_owner",
     "update_port": "rule:admin_or_owner",
     "update_port:fixed_ips": "rule:admin_or_network_owner",
     "update_port:port_security_enabled": "rule:admin_or_network_owner",
     "update_port:binding:host_id": "rule:admin_only",
     "update_port:binding:profile": "rule:admin_only",
+    "update_port:binding:vnic_type": "rule:admin_or_owner",
     "update_port:mac_learning_enabled": "rule:admin_or_network_owner",
     "delete_port": "rule:admin_or_owner",
 
diff --git a/neutron/db/migration/alembic_migrations/versions/27cc183af192_ml2_vnic_type.py b/neutron/db/migration/alembic_migrations/versions/27cc183af192_ml2_vnic_type.py
new file mode 100644 (file)
index 0000000..00cf029
--- /dev/null
@@ -0,0 +1,55 @@
+# 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.
+#
+
+"""ml2_vnic_type
+
+Revision ID: 27cc183af192
+Revises: 4ca36cfc898c
+Create Date: 2014-02-09 12:19:21.362967
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '27cc183af192'
+down_revision = '4ca36cfc898c'
+
+# Change to ['*'] if this migration applies to all plugins
+
+migration_for_plugins = [
+    'neutron.plugins.ml2.plugin.Ml2Plugin'
+]
+
+from alembic import op
+import sqlalchemy as sa
+
+from neutron.db import migration
+from neutron.extensions import portbindings
+
+
+def upgrade(active_plugins=None, options=None):
+    if not migration.should_run(active_plugins, migration_for_plugins):
+        return
+
+    op.add_column('ml2_port_bindings',
+                  sa.Column('vnic_type', sa.String(length=64),
+                            nullable=False,
+                            server_default=portbindings.VNIC_NORMAL))
+
+
+def downgrade(active_plugins=None, options=None):
+    if not migration.should_run(active_plugins, migration_for_plugins):
+        return
+
+    op.drop_column('ml2_port_bindings', 'vnic_type')
index c7ab4d37e2b2a27ec22deca18ce7a1b1d91a39e2..9eacaedf86889d8e55cfcc91cbb691d1bb858b0c 100644 (file)
@@ -71,6 +71,16 @@ class PortBindingMixin(portbindings_base.PortBindingBaseMixin):
         binding_profile_set = attributes.is_attr_set(binding_profile)
         if not binding_profile_set and binding_profile is not None:
             del port[portbindings.PROFILE]
+
+        binding_vnic = port.get(portbindings.VNIC_TYPE)
+        binding_vnic_set = attributes.is_attr_set(binding_vnic)
+        if not binding_vnic_set and binding_vnic is not None:
+            del port[portbindings.VNIC_TYPE]
+        # REVISIT(irenab) Add support for vnic_type for plugins that
+        # can handle more than one type.
+        # Currently implemented for ML2 plugin that does not use
+        # PortBindingMixin.
+
         host = port_data.get(portbindings.HOST_ID)
         host_set = attributes.is_attr_set(host)
         with context.session.begin(subtransactions=True):
index dbef59289c0f96322a8c19fb9f05da54ecf85920..4ed38fc239023556d659275ea64a5ccc9da3a33e 100644 (file)
@@ -18,7 +18,8 @@
 from neutron.api import extensions
 from neutron.api.v2 import attributes
 
-
+# The type of vnic that this port should be attached to
+VNIC_TYPE = 'binding:vnic_type'
 # The service will return the vif type for the specific port.
 VIF_TYPE = 'binding:vif_type'
 # In some cases different implementations may be run on different hosts.
@@ -51,6 +52,10 @@ VIF_TYPES = [VIF_TYPE_UNBOUND, VIF_TYPE_BINDING_FAILED, VIF_TYPE_OVS,
              VIF_TYPE_802_QBH, VIF_TYPE_HYPERV, VIF_TYPE_MIDONET,
              VIF_TYPE_OTHER]
 
+VNIC_NORMAL = 'normal'
+VNIC_DIRECT = 'direct'
+VNIC_MACVTAP = 'macvtap'
+VNIC_TYPES = [VNIC_NORMAL, VNIC_DIRECT, VNIC_MACVTAP]
 
 EXTENDED_ATTRIBUTES_2_0 = {
     'ports': {
@@ -58,6 +63,11 @@ EXTENDED_ATTRIBUTES_2_0 = {
                    'default': attributes.ATTR_NOT_SPECIFIED,
                    'enforce_policy': True,
                    'is_visible': True},
+        VNIC_TYPE: {'allow_post': True, 'allow_put': True,
+                    'default': VNIC_NORMAL,
+                    'is_visible': True,
+                    'validate': {'type:values': VNIC_TYPES},
+                    'enforce_policy': True},
         HOST_ID: {'allow_post': True, 'allow_put': True,
                   'default': attributes.ATTR_NOT_SPECIFIED,
                   'is_visible': True,
index 7bf21a49752b440e895b8114ecde8ac2f2ec9e66..2081057dad755675e307c49a96fa28f16b63cf3c 100644 (file)
@@ -17,6 +17,7 @@ from abc import ABCMeta, abstractmethod
 
 import six
 
+from neutron.extensions import portbindings
 from neutron.openstack.common import log
 from neutron.plugins.ml2 import driver_api as api
 
@@ -38,7 +39,8 @@ class AgentMechanismDriverBase(api.MechanismDriver):
     check_segment_for_agent().
     """
 
-    def __init__(self, agent_type, vif_type, cap_port_filter):
+    def __init__(self, agent_type, vif_type, cap_port_filter,
+                 supported_vnic_types=[portbindings.VNIC_NORMAL]):
         """Initialize base class for specific L2 agent type.
 
         :param agent_type: Constant identifying agent type in agents_db
@@ -47,6 +49,7 @@ class AgentMechanismDriverBase(api.MechanismDriver):
         self.agent_type = agent_type
         self.vif_type = vif_type
         self.cap_port_filter = cap_port_filter
+        self.supported_vnic_types = supported_vnic_types
 
     def initialize(self):
         pass
@@ -56,6 +59,12 @@ class AgentMechanismDriverBase(api.MechanismDriver):
                     "network %(network)s"),
                   {'port': context.current['id'],
                    'network': context.network.current['id']})
+        vnic_type = context.current.get(portbindings.VNIC_TYPE,
+                                        portbindings.VNIC_NORMAL)
+        if vnic_type not in self.supported_vnic_types:
+            LOG.debug(_("Refusing to bind due to unsupported vnic_type: %s"),
+                      vnic_type)
+            return
         for agent in context.host_agents(self.agent_type):
             LOG.debug(_("Checking agent: %s"), agent)
             if agent['alive']:
index a60e20987b84b778b202688bf105a7702c000190..424f9f5d096dadcc44f6192799ac13e1a4f8ea6c 100644 (file)
@@ -437,15 +437,18 @@ class MechanismManager(stevedore.named.NamedExtensionManager):
         attempt to establish a port binding.
         """
         binding = context._binding
-        LOG.debug(_("Attempting to bind port %(port)s on host %(host)s"),
+        LOG.debug(_("Attempting to bind port %(port)s on host %(host)s "
+                    "for vnic_type %(vnic_type)s"),
                   {'port': context._port['id'],
-                   'host': binding.host})
+                   'host': binding.host,
+                   'vnic_type': binding.vnic_type})
         for driver in self.ordered_mech_drivers:
             try:
                 driver.obj.bind_port(context)
                 if binding.segment:
                     binding.driver = driver.name
                     LOG.debug(_("Bound port: %(port)s, host: %(host)s, "
+                                "vnic_type: %(vnic_type)s, "
                                 "driver: %(driver)s, vif_type: %(vif_type)s, "
                                 "cap_port_filter: %(cap_port_filter)s, "
                                 "segment: %(segment)s"),
@@ -453,6 +456,7 @@ class MechanismManager(stevedore.named.NamedExtensionManager):
                                'host': binding.host,
                                'driver': binding.driver,
                                'vif_type': binding.vif_type,
+                               'vnic_type': binding.vnic_type,
                                'cap_port_filter': binding.cap_port_filter,
                                'segment': binding.segment})
                     return
index 9b215be2740e9e0c8813d0e57001edda74f9e8a1..f17fa1cdb588bb7de02fed5463863dff4ca07899 100644 (file)
@@ -18,6 +18,7 @@ from sqlalchemy import orm
 
 from neutron.db import model_base
 from neutron.db import models_v2
+from neutron.extensions import portbindings
 
 
 class NetworkSegment(model_base.BASEV2, models_v2.HasId):
@@ -53,6 +54,8 @@ class PortBinding(model_base.BASEV2):
                         sa.ForeignKey('ports.id', ondelete="CASCADE"),
                         primary_key=True)
     host = sa.Column(sa.String(255), nullable=False)
+    vnic_type = sa.Column(sa.String(64), nullable=False,
+                          default=portbindings.VNIC_NORMAL)
     vif_type = sa.Column(sa.String(64), nullable=False)
     cap_port_filter = sa.Column(sa.Boolean, nullable=False)
     driver = sa.Column(sa.String(64))
index 8ca62d7df85725c9ce61a62095fdeb12c480f84a..dcfcade1fbb13a873117fd4c8edbe3f2b8816c50 100644 (file)
@@ -208,23 +208,30 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
         self._update_port_dict_binding(port, binding)
         host = attrs and attrs.get(portbindings.HOST_ID)
         host_set = attributes.is_attr_set(host)
+        vnic_type = attrs and attrs.get(portbindings.VNIC_TYPE)
+        vnic_type_set = attributes.is_attr_set(vnic_type)
 
         if binding.vif_type != portbindings.VIF_TYPE_UNBOUND:
-            if (not host_set and binding.segment and
+            if (not host_set and not vnic_type_set and binding.segment and
                 self.mechanism_manager.validate_port_binding(mech_context)):
                 return False
             self.mechanism_manager.unbind_port(mech_context)
             self._update_port_dict_binding(port, binding)
 
         # Return True only if an agent notification is needed.
-        # This will happen if a new host was specified and that host
+        # This will happen if a new host or vnic_type was specified that
         # differs from the current one. Note that host_set is True
         # even if the host is an empty string
-        ret_value = host_set and binding.get('host') != host
+        ret_value = ((host_set and binding.get('host') != host) or
+                     (vnic_type_set and binding.get('vnic_type') != vnic_type))
         if host_set:
             binding.host = host
             port[portbindings.HOST_ID] = host
 
+        if vnic_type_set:
+            binding.vnic_type = vnic_type
+            port[portbindings.VNIC_TYPE] = vnic_type
+
         if binding.host:
             self.mechanism_manager.bind_port(mech_context)
             self._update_port_dict_binding(port, binding)
@@ -233,6 +240,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
 
     def _update_port_dict_binding(self, port, binding):
         port[portbindings.HOST_ID] = binding.host
+        port[portbindings.VNIC_TYPE] = binding.vnic_type
         port[portbindings.VIF_TYPE] = binding.vif_type
         port[portbindings.CAPABILITIES] = {
             portbindings.CAP_PORT_FILTER: binding.cap_port_filter}
index f362f92eadc0835f9fc2eb2b56445087b0f12291..5e52629a574318767426c4804ee99bd1f8370163 100644 (file)
@@ -19,6 +19,7 @@
 #
 
 import contextlib
+import httplib
 
 from oslo.config import cfg
 from webob import exc
@@ -279,3 +280,89 @@ class PortBindingsHostTestCaseMixin(object):
             self._test_list_resources(
                 'port', (port1, port3),
                 query_params='%s=%s' % (portbindings.HOST_ID, self.hostname))
+
+
+class PortBindingsVnicTestCaseMixin(object):
+    fmt = 'json'
+    vnic_type = portbindings.VNIC_NORMAL
+
+    def _check_response_portbindings_vnic_type(self, port):
+        self.assertIn('status', port)
+        self.assertEqual(port[portbindings.VNIC_TYPE], self.vnic_type)
+
+    def test_port_vnic_type_non_admin(self):
+        with self.network(set_context=True,
+                          tenant_id='test') as net1:
+            with self.subnet(network=net1) as subnet1:
+                vnic_arg = {portbindings.VNIC_TYPE: self.vnic_type}
+                with self.port(subnet=subnet1,
+                               expected_res_status=httplib.CREATED,
+                               arg_list=(portbindings.VNIC_TYPE,),
+                               set_context=True,
+                               tenant_id='test',
+                               **vnic_arg) as port:
+                    # Check a response of create_port
+                    self._check_response_portbindings_vnic_type(port['port'])
+
+    def test_port_vnic_type(self):
+        vnic_arg = {portbindings.VNIC_TYPE: self.vnic_type}
+        with self.port(name='name', arg_list=(portbindings.VNIC_TYPE,),
+                       **vnic_arg) as port:
+            port_id = port['port']['id']
+            # Check a response of create_port
+            self._check_response_portbindings_vnic_type(port['port'])
+            # Check a response of get_port
+            ctx = context.get_admin_context()
+            port = self._show('ports', port_id, neutron_context=ctx)['port']
+            self._check_response_portbindings_vnic_type(port)
+            # By default user is admin - now test non admin user
+            ctx = context.Context(user_id=None,
+                                  tenant_id=self._tenant_id,
+                                  is_admin=False,
+                                  read_deleted="no")
+            non_admin_port = self._show(
+                'ports', port_id, neutron_context=ctx)['port']
+            self._check_response_portbindings_vnic_type(non_admin_port)
+
+    def test_ports_vnic_type(self):
+        cfg.CONF.set_default('allow_overlapping_ips', True)
+        vnic_arg = {portbindings.VNIC_TYPE: self.vnic_type}
+        with contextlib.nested(
+            self.port(name='name1',
+                      arg_list=(portbindings.VNIC_TYPE,),
+                      **vnic_arg),
+            self.port(name='name2')):
+            ctx = context.get_admin_context()
+            ports = self._list('ports', neutron_context=ctx)['ports']
+            self.assertEqual(2, len(ports))
+            for port in ports:
+                if port['name'] == 'name1':
+                    self._check_response_portbindings_vnic_type(port)
+                else:
+                    self.assertEqual(portbindings.VNIC_NORMAL,
+                                     port[portbindings.VNIC_TYPE])
+            # By default user is admin - now test non admin user
+            ctx = context.Context(user_id=None,
+                                  tenant_id=self._tenant_id,
+                                  is_admin=False,
+                                  read_deleted="no")
+            ports = self._list('ports', neutron_context=ctx)['ports']
+            self.assertEqual(2, len(ports))
+            for non_admin_port in ports:
+                self._check_response_portbindings_vnic_type(non_admin_port)
+
+    def test_ports_vnic_type_list(self):
+        cfg.CONF.set_default('allow_overlapping_ips', True)
+        vnic_arg = {portbindings.VNIC_TYPE: self.vnic_type}
+        with contextlib.nested(
+            self.port(name='name1',
+                      arg_list=(portbindings.VNIC_TYPE,),
+                      **vnic_arg),
+            self.port(name='name2'),
+            self.port(name='name3',
+                      arg_list=(portbindings.VNIC_TYPE,),
+                      **vnic_arg),) as (port1, port2, port3):
+            self._test_list_resources(
+                'port', (port1, port2, port3),
+                query_params='%s=%s' % (portbindings.VNIC_TYPE,
+                                        self.vnic_type))
index 83e772b7bf216fd5a7fdc3f2963eb7afc719aff3..876cb1d754259acf1711442d1334921797250c80 100644 (file)
@@ -14,6 +14,7 @@
 #    under the License.
 
 
+from neutron.extensions import portbindings
 from neutron.plugins.ml2 import driver_api as api
 from neutron.tests import base
 
@@ -45,6 +46,7 @@ class FakePortContext(api.PortContext):
         self._network_context = FakeNetworkContext(segments)
         self._bound_segment_id = None
         self._bound_vif_type = None
+        self._bound_vnic_type = portbindings.VNIC_NORMAL
         self._bound_cap_port_filter = None
 
     @property
index f9898dff8c508cc04dc355275e858ade7c8eba68..a9d977af4d859bcae874b9af21bab8ac62279049 100644 (file)
@@ -141,6 +141,11 @@ class TestMl2PortBindingHost(Ml2PluginV2TestCase,
     pass
 
 
+class TestMl2PortBindingVnicType(Ml2PluginV2TestCase,
+                                 test_bindings.PortBindingsVnicTestCaseMixin):
+    pass
+
+
 class TestMultiSegmentNetworks(Ml2PluginV2TestCase):
 
     def setUp(self, plugin=None):