From 6083d67bc2c449e1814a54e300a457c1aacc56c9 Mon Sep 17 00:00:00 2001 From: Bob Kukura Date: Thu, 13 Feb 2014 12:35:25 -0500 Subject: [PATCH] ML2 binding:profile port attribute 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 --- .../157a5d299379_ml2_binding_profile.py | 55 +++++++++++++++++++ neutron/plugins/ml2/db.py | 1 - neutron/plugins/ml2/managers.py | 7 ++- neutron/plugins/ml2/models.py | 6 +- neutron/plugins/ml2/plugin.py | 40 ++++++++++++-- .../unit/_test_extension_portbindings.py | 13 +++-- neutron/tests/unit/ml2/test_ml2_plugin.py | 38 +++++++++++++ 7 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/157a5d299379_ml2_binding_profile.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 index 0000000000..200589d16f --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/157a5d299379_ml2_binding_profile.py @@ -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') diff --git a/neutron/plugins/ml2/db.py b/neutron/plugins/ml2/db.py index 00475070dc..861042fea4 100644 --- a/neutron/plugins/ml2/db.py +++ b/neutron/plugins/ml2/db.py @@ -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 diff --git a/neutron/plugins/ml2/managers.py b/neutron/plugins/ml2/managers.py index 516beddb44..e84f86f304 100644 --- a/neutron/plugins/ml2/managers.py +++ b/neutron/plugins/ml2/managers.py @@ -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, diff --git a/neutron/plugins/ml2/models.py b/neutron/plugins/ml2/models.py index 26aa11cff2..0ab805f1cd 100644 --- a/neutron/plugins/ml2/models.py +++ b/neutron/plugins/ml2/models.py @@ -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)) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index ddb5d62943..0e07cff9a7 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -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 diff --git a/neutron/tests/unit/_test_extension_portbindings.py b/neutron/tests/unit/_test_extension_portbindings.py index 4852d56530..45c04f6bd8 100644 --- a/neutron/tests/unit/_test_extension_portbindings.py +++ b/neutron/tests/unit/_test_extension_portbindings.py @@ -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) diff --git a/neutron/tests/unit/ml2/test_ml2_plugin.py b/neutron/tests/unit/ml2/test_ml2_plugin.py index a9d977af4d..caeeb7a93b 100644 --- a/neutron/tests/unit/ml2/test_ml2_plugin.py +++ b/neutron/tests/unit/ml2/test_ml2_plugin.py @@ -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