]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
ML2 binding:profile port attribute
authorBob Kukura <rkukura@redhat.com>
Thu, 13 Feb 2014 17:35:25 +0000 (12:35 -0500)
committerThomas Goirand <thomas@goirand.fr>
Thu, 13 Mar 2014 07:20:31 +0000 (15:20 +0800)
The ML2 plugin stores the binding:profile port attribute, defined as a
dictionary, in its ml2_port_bindings DB table. Since the plugin can
support a variety of MechanismDrivers with different needs for
binding:profile attribute content, the plugin will accept, store, and
return arbitrary key/value pairs within the attribute. As with the
binding:host_id attribute, updates to binding:profile trigger
rebinding.

Implements: blueprint ml2-binding-profile
Change-Id: I01cba8d09dde9de1c6160d0235b0d289eed91b29

neutron/db/migration/alembic_migrations/versions/157a5d299379_ml2_binding_profile.py [new file with mode: 0644]
neutron/plugins/ml2/db.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_ml2_plugin.py

diff --git a/neutron/db/migration/alembic_migrations/versions/157a5d299379_ml2_binding_profile.py b/neutron/db/migration/alembic_migrations/versions/157a5d299379_ml2_binding_profile.py
new file mode 100644 (file)
index 0000000..200589d
--- /dev/null
@@ -0,0 +1,55 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# 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 binding:profile
+
+Revision ID: 157a5d299379
+Revises: 50d5ba354c23
+Create Date: 2014-02-13 23:48:25.147279
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '157a5d299379'
+down_revision = '50d5ba354c23'
+
+# 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
+
+
+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('profile', sa.String(length=4095),
+                            nullable=False, server_default=''))
+
+
+def downgrade(active_plugins=None, options=None):
+    if not migration.should_run(active_plugins, migration_for_plugins):
+        return
+
+    op.drop_column('ml2_port_bindings', 'profile')
index 00475070dc18e24b06715ee734099de21f0ed02a..861042fea40dbe9e4af434914d6db950e70924db 100644 (file)
@@ -65,7 +65,6 @@ def ensure_port_binding(session, port_id):
         except exc.NoResultFound:
             record = models.PortBinding(
                 port_id=port_id,
-                host='',
                 vif_type=portbindings.VIF_TYPE_UNBOUND)
             session.add(record)
         return record
index 516beddb449a2c40350a73ebda650f546809e34a..e84f86f304271b2d6d31d33b52d4f8f2e2201f39 100644 (file)
@@ -438,10 +438,11 @@ class MechanismManager(stevedore.named.NamedExtensionManager):
         """
         binding = context._binding
         LOG.debug(_("Attempting to bind port %(port)s on host %(host)s "
-                    "for vnic_type %(vnic_type)s"),
+                    "for vnic_type %(vnic_type)s with profile %(profile)s"),
                   {'port': context._port['id'],
                    'host': binding.host,
-                   'vnic_type': binding.vnic_type})
+                   'vnic_type': binding.vnic_type,
+                   'profile': binding.profile})
         for driver in self.ordered_mech_drivers:
             try:
                 driver.obj.bind_port(context)
@@ -449,12 +450,14 @@ class MechanismManager(stevedore.named.NamedExtensionManager):
                     binding.driver = driver.name
                     LOG.debug(_("Bound port: %(port)s, host: %(host)s, "
                                 "vnic_type: %(vnic_type)s, "
+                                "profile: %(profile)s"
                                 "driver: %(driver)s, vif_type: %(vif_type)s, "
                                 "vif_details: %(vif_details)s, "
                                 "segment: %(segment)s"),
                               {'port': context._port['id'],
                                'host': binding.host,
                                'vnic_type': binding.vnic_type,
+                               'profile': binding.profile,
                                'driver': binding.driver,
                                'vif_type': binding.vif_type,
                                'vif_details': binding.vif_details,
index 26aa11cff2f08460e8616ef6cde885d3658a8df2..0ab805f1cdbeeb96b6c260c1469923fb0389fe7c 100644 (file)
@@ -20,6 +20,8 @@ from neutron.db import model_base
 from neutron.db import models_v2
 from neutron.extensions import portbindings
 
+BINDING_PROFILE_LEN = 4095
+
 
 class NetworkSegment(model_base.BASEV2, models_v2.HasId):
     """Represent persistent state of a network segment.
@@ -53,9 +55,11 @@ class PortBinding(model_base.BASEV2):
     port_id = sa.Column(sa.String(36),
                         sa.ForeignKey('ports.id', ondelete="CASCADE"),
                         primary_key=True)
-    host = sa.Column(sa.String(255), nullable=False)
+    host = sa.Column(sa.String(255), nullable=False, default='')
     vnic_type = sa.Column(sa.String(64), nullable=False,
                           default=portbindings.VNIC_NORMAL)
+    profile = sa.Column(sa.String(BINDING_PROFILE_LEN), nullable=False,
+                        default='')
     vif_type = sa.Column(sa.String(64), nullable=False)
     vif_details = sa.Column(sa.String(4095), nullable=False, default='')
     driver = sa.Column(sa.String(64))
index ddb5d62943d83c041e99bf486f51d9ac66c34673..0e07cff9a79d6db1a93e1d8c70e6aff651b61401 100644 (file)
@@ -207,24 +207,36 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
         binding = mech_context._binding
         port = mech_context.current
         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)
 
+        # CLI can't send {}, so treat None as {}
+        profile = attrs and attrs.get(portbindings.PROFILE)
+        profile_set = profile is not attributes.ATTR_NOT_SPECIFIED
+        if profile_set and not profile:
+            profile = {}
+
         if binding.vif_type != portbindings.VIF_TYPE_UNBOUND:
-            if (not host_set and not vnic_type_set and binding.segment and
+            if (not host_set and not vnic_type_set and not profile_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 or vnic_type was specified that
-        # differs from the current one. Note that host_set is True
+        # This will happen if a new host, vnic_type, or profile 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) or
-                     (vnic_type_set and binding.get('vnic_type') != vnic_type))
+                     (vnic_type_set and
+                      binding.get('vnic_type') != vnic_type) or
+                     (profile_set and self._get_profile(binding) != profile))
+
         if host_set:
             binding.host = host
             port[portbindings.HOST_ID] = host
@@ -233,6 +245,14 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
             binding.vnic_type = vnic_type
             port[portbindings.VNIC_TYPE] = vnic_type
 
+        if profile_set:
+            binding.profile = jsonutils.dumps(profile)
+            if len(binding.profile) > models.BINDING_PROFILE_LEN:
+                msg = _("binding:profile value too large")
+                raise exc.InvalidInput(error_message=msg)
+            port[portbindings.PROFILE] = profile
+
+        # To try to [re]bind if host is non-empty.
         if binding.host:
             self.mechanism_manager.bind_port(mech_context)
             self._update_port_dict_binding(port, binding)
@@ -242,6 +262,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.PROFILE] = self._get_profile(binding)
         port[portbindings.VIF_TYPE] = binding.vif_type
         port[portbindings.VIF_DETAILS] = self._get_vif_details(binding)
 
@@ -256,6 +277,17 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
                            'port': binding.port_id})
         return {}
 
+    def _get_profile(self, binding):
+        if binding.profile:
+            try:
+                return jsonutils.loads(binding.profile)
+            except Exception:
+                LOG.error(_("Serialized profile DB value '%(value)s' for "
+                            "port %(port)s is invalid"),
+                          {'value': binding.profile,
+                           'port': binding.port_id})
+        return {}
+
     def _delete_port_binding(self, mech_context):
         binding = mech_context._binding
         port = mech_context.current
index 4852d565304f79a0c19c58527e5ea62e3e822725..45c04f6bd8e2e2fee3d6e120cb98cd3c89e65c0f 100644 (file)
@@ -90,7 +90,7 @@ class PortBindingsTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
             for non_admin_port in ports:
                 self._check_response_no_portbindings(non_admin_port)
 
-    def _check_default_port_binding_profile(self, port):
+    def _check_port_binding_profile(self, port, profile=None):
         # For plugins which does not use binding:profile attr
         # we just check an operation for the port succeed.
         self.assertIn('id', port)
@@ -99,7 +99,10 @@ class PortBindingsTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
         profile_arg = {portbindings.PROFILE: profile}
         with self.port(arg_list=(portbindings.PROFILE,),
                        **profile_arg) as port:
-            self._check_default_port_binding_profile(port['port'])
+            port_id = port['port']['id']
+            self._check_port_binding_profile(port['port'], profile)
+            port = self._show('ports', port_id)
+            self._check_port_binding_profile(port['port'], profile)
 
     def test_create_port_binding_profile_none(self):
         self._test_create_port_binding_profile(None)
@@ -111,12 +114,14 @@ class PortBindingsTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
         profile_arg = {portbindings.PROFILE: profile}
         with self.port() as port:
             # print "(1) %s" % port
-            self._check_default_port_binding_profile(port['port'])
+            self._check_port_binding_profile(port['port'])
             port_id = port['port']['id']
             ctx = context.get_admin_context()
             port = self._update('ports', port_id, {'port': profile_arg},
                                 neutron_context=ctx)['port']
-            self._check_default_port_binding_profile(port)
+            self._check_port_binding_profile(port, profile)
+            port = self._show('ports', port_id)['port']
+            self._check_port_binding_profile(port, profile)
 
     def test_update_port_binding_profile_none(self):
         self._test_update_port_binding_profile(None)
index a9d977af4d859bcae874b9af21bab8ac62279049..caeeb7a93bee04b867aa8e5ca2f13843d474693c 100644 (file)
@@ -15,6 +15,7 @@
 
 import mock
 import testtools
+import webob
 
 from neutron.common import exceptions as exc
 from neutron import context
@@ -130,6 +131,43 @@ class TestMl2PortBinding(Ml2PluginV2TestCase,
         test_sg_rpc.set_firewall_driver(self.FIREWALL_DRIVER)
         super(TestMl2PortBinding, self).setUp()
 
+    def _check_port_binding_profile(self, port, profile=None):
+        self.assertIn('id', port)
+        self.assertIn(portbindings.PROFILE, port)
+        value = port[portbindings.PROFILE]
+        self.assertEqual(profile or {}, value)
+
+    def test_create_port_binding_profile(self):
+        self._test_create_port_binding_profile({'a': 1, 'b': 2})
+
+    def test_update_port_binding_profile(self):
+        self._test_update_port_binding_profile({'c': 3})
+
+    def test_create_port_binding_profile_too_big(self):
+        s = 'x' * 5000
+        profile_arg = {portbindings.PROFILE: {'d': s}}
+        try:
+            with self.port(expected_res_status=400,
+                           arg_list=(portbindings.PROFILE,),
+                           **profile_arg):
+                pass
+        except webob.exc.HTTPClientError:
+            pass
+
+    def test_remove_port_binding_profile(self):
+        profile = {'e': 5}
+        profile_arg = {portbindings.PROFILE: profile}
+        with self.port(arg_list=(portbindings.PROFILE,),
+                       **profile_arg) as port:
+            self._check_port_binding_profile(port['port'], profile)
+            port_id = port['port']['id']
+            profile_arg = {portbindings.PROFILE: None}
+            port = self._update('ports', port_id,
+                                {'port': profile_arg})['port']
+            self._check_port_binding_profile(port)
+            port = self._show('ports', port_id)['port']
+            self._check_port_binding_profile(port)
+
 
 class TestMl2PortBindingNoSG(TestMl2PortBinding):
     HAS_PORT_FILTER = False