From 82ccdf893e7abc51924f6429123e3d22d6212c57 Mon Sep 17 00:00:00 2001 From: armando-migliaccio Date: Wed, 22 May 2013 22:43:17 -0700 Subject: [PATCH] Add API mac learning extension for NVP This commit adds an API extension for NVP where the NVP supported mac learning feature can be switched on/off for a specific port. The attribute can be True or False or omitted altogether. Implements blueprint nvp-mac-learning-extension Change-Id: I9173c7dfe0cf4a9ee7b0605722ce7fa01708f5ba --- etc/policy.json | 2 + .../versions/3cbf70257c28_nvp_mac_learning.py | 60 ++++++++ quantum/plugins/nicira/QuantumPlugin.py | 29 +++- quantum/plugins/nicira/dbexts/__init__.py | 0 quantum/plugins/nicira/dbexts/maclearning.py | 73 +++++++++ .../plugins/nicira/extensions/maclearning.py | 65 ++++++++ quantum/plugins/nicira/nvplib.py | 16 +- quantum/tests/unit/nicira/test_maclearning.py | 141 ++++++++++++++++++ 8 files changed, 378 insertions(+), 8 deletions(-) create mode 100644 quantum/db/migration/alembic_migrations/versions/3cbf70257c28_nvp_mac_learning.py create mode 100644 quantum/plugins/nicira/dbexts/__init__.py create mode 100644 quantum/plugins/nicira/dbexts/maclearning.py create mode 100644 quantum/plugins/nicira/extensions/maclearning.py create mode 100644 quantum/tests/unit/nicira/test_maclearning.py diff --git a/etc/policy.json b/etc/policy.json index 6e31a33c5..8b0bfec0a 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -41,6 +41,7 @@ "create_port:fixed_ips": "rule:admin_or_network_owner", "create_port:port_security_enabled": "rule:admin_or_network_owner", "create_port:binding:host_id": "rule:admin_only", + "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:vif_type": "rule:admin_only", @@ -51,6 +52,7 @@ "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:mac_learning_enabled": "rule:admin_or_network_owner", "delete_port": "rule:admin_or_owner", "create_service_type": "rule:admin_only", diff --git a/quantum/db/migration/alembic_migrations/versions/3cbf70257c28_nvp_mac_learning.py b/quantum/db/migration/alembic_migrations/versions/3cbf70257c28_nvp_mac_learning.py new file mode 100644 index 000000000..078455aab --- /dev/null +++ b/quantum/db/migration/alembic_migrations/versions/3cbf70257c28_nvp_mac_learning.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 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. +# + +"""nvp_mac_learning + +Revision ID: 3cbf70257c28 +Revises: 176a85fc7d79 +Create Date: 2013-05-15 10:15:50.875314 + +""" + +# revision identifiers, used by Alembic. +revision = '3cbf70257c28' +down_revision = '176a85fc7d79' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'quantum.plugins.nicira.QuantumPlugin.NvpPluginV2' +] + +from alembic import op +import sqlalchemy as sa + + +from quantum.db import migration + + +def upgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.create_table( + 'maclearningstates', + sa.Column('port_id', sa.String(length=36), nullable=False), + sa.Column('mac_learning_enabled', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint( + ['port_id'], ['ports.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('port_id')) + + +def downgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.drop_table('maclearningstates') diff --git a/quantum/plugins/nicira/QuantumPlugin.py b/quantum/plugins/nicira/QuantumPlugin.py index e7631cd78..9722042d5 100644 --- a/quantum/plugins/nicira/QuantumPlugin.py +++ b/quantum/plugins/nicira/QuantumPlugin.py @@ -57,6 +57,8 @@ from quantum.plugins.nicira.common import config # noqa from quantum.plugins.nicira.common import exceptions as nvp_exc from quantum.plugins.nicira.common import metadata_access as nvp_meta from quantum.plugins.nicira.common import securitygroups as nvp_sec +from quantum.plugins.nicira.dbexts import maclearning as mac_db +from quantum.plugins.nicira.extensions import maclearning as mac_ext from quantum.plugins.nicira.extensions import nvp_networkgw as networkgw from quantum.plugins.nicira.extensions import nvp_qos as ext_qos from quantum.plugins.nicira import nicira_db @@ -125,6 +127,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, l3_db.L3_NAT_db_mixin, portsecurity_db.PortSecurityDbMixin, securitygroups_db.SecurityGroupDbMixin, + mac_db.MacLearningDbMixin, networkgw_db.NetworkGatewayMixin, qos_db.NVPQoSDbMixin, nvp_sec.NVPSecurityGroups, @@ -136,7 +139,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, functionality using NVP. """ - supported_extension_aliases = ["network-gateway", + supported_extension_aliases = ["mac-learning", + "network-gateway", "nvp-qos", "port-security", "provider", @@ -363,7 +367,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, port_data['fixed_ips'], port_data[psec.PORTSECURITY], port_data[ext_sg.SECURITYGROUPS], - port_data[ext_qos.QUEUE]) + port_data[ext_qos.QUEUE], + port_data.get(mac_ext.MAC_LEARNING)) def _nvp_create_port(self, context, port_data): """Driver for creating a logical switch port on NVP platform.""" @@ -1056,6 +1061,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, context, filters) for quantum_lport in quantum_lports: self._extend_port_port_security_dict(context, quantum_lport) + self._extend_port_mac_learning_state(context, quantum_lport) if (filters.get('network_id') and len(filters.get('network_id')) and self._network_is_external(context, filters['network_id'][0])): # Do not perform check on NVP platform @@ -1192,6 +1198,10 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, port_data[ext_qos.QUEUE] = self._check_for_queue_and_create( context, port_data) self._process_port_queue_mapping(context, port_data) + if (isinstance(port_data.get(mac_ext.MAC_LEARNING), bool)): + self._create_mac_learning_state(context, port_data) + elif mac_ext.MAC_LEARNING in port_data: + port_data.pop(mac_ext.MAC_LEARNING) # provider networking extension checks # Fetch the network and network binding from Quantum db try: @@ -1274,6 +1284,17 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, ret_port[ext_qos.QUEUE] = self._check_for_queue_and_create( context, ret_port) + # Populate the mac learning attribute + new_mac_learning_state = port['port'].get(mac_ext.MAC_LEARNING) + old_mac_learning_state = self._get_mac_learning_state(context, id) + if (new_mac_learning_state is not None and + old_mac_learning_state != new_mac_learning_state): + self._update_mac_learning_state(context, id, + new_mac_learning_state) + ret_port[mac_ext.MAC_LEARNING] = new_mac_learning_state + elif (new_mac_learning_state is None and + old_mac_learning_state is not None): + ret_port[mac_ext.MAC_LEARNING] = old_mac_learning_state self._delete_port_queue_mapping(context, ret_port['id']) self._process_port_queue_mapping(context, ret_port) self._extend_port_port_security_dict(context, ret_port) @@ -1291,7 +1312,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, ret_port['fixed_ips'], ret_port[psec.PORTSECURITY], ret_port[ext_sg.SECURITYGROUPS], - ret_port[ext_qos.QUEUE]) + ret_port[ext_qos.QUEUE], + ret_port.get(mac_ext.MAC_LEARNING)) # Update the port status from nvp. If we fail here hide it # since the port was successfully updated but we were not @@ -1365,6 +1387,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2, id, fields) self._extend_port_port_security_dict(context, quantum_db_port) self._extend_port_qos_queue(context, quantum_db_port) + self._extend_port_mac_learning_state(context, quantum_db_port) if self._network_is_external(context, quantum_db_port['network_id']): diff --git a/quantum/plugins/nicira/dbexts/__init__.py b/quantum/plugins/nicira/dbexts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/quantum/plugins/nicira/dbexts/maclearning.py b/quantum/plugins/nicira/dbexts/maclearning.py new file mode 100644 index 000000000..44af13e1b --- /dev/null +++ b/quantum/plugins/nicira/dbexts/maclearning.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 VMware, Inc. 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 sqlalchemy as sa +from sqlalchemy.orm import exc + +from quantum.db import model_base +from quantum.openstack.common import log as logging +from quantum.plugins.nicira.extensions import maclearning as mac + +LOG = logging.getLogger(__name__) + + +class MacLearningState(model_base.BASEV2): + + port_id = sa.Column(sa.String(36), + sa.ForeignKey('ports.id', ondelete="CASCADE"), + primary_key=True) + mac_learning_enabled = sa.Column(sa.Boolean(), nullable=False) + + +class MacLearningDbMixin(object): + """Mixin class for mac learning.""" + + def _make_mac_learning_state_dict(self, port, fields=None): + res = {'port_id': port['port_id'], + mac.MAC_LEARNING: port[mac.MAC_LEARNING]} + return self._fields(res, fields) + + def _get_mac_learning_state(self, context, port_id): + try: + query = self._model_query(context, MacLearningState) + state = query.filter(MacLearningState.port_id == port_id).one() + except exc.NoResultFound: + return None + return state[mac.MAC_LEARNING] + + def _extend_port_mac_learning_state(self, context, port): + state = self._get_mac_learning_state(context, port['id']) + if state: + port[mac.MAC_LEARNING] = state + + def _update_mac_learning_state(self, context, port_id, enabled): + try: + query = self._model_query(context, MacLearningState) + state = query.filter(MacLearningState.port_id == port_id).one() + state.update({mac.MAC_LEARNING: enabled}) + except exc.NoResultFound: + self._create_mac_learning_state(context, + {'id': port_id, + mac.MAC_LEARNING: enabled}) + + def _create_mac_learning_state(self, context, port): + with context.session.begin(subtransactions=True): + enabled = port[mac.MAC_LEARNING] + state = MacLearningState(port_id=port['id'], + mac_learning_enabled=enabled) + context.session.add(state) + return self._make_mac_learning_state_dict(state) diff --git a/quantum/plugins/nicira/extensions/maclearning.py b/quantum/plugins/nicira/extensions/maclearning.py new file mode 100644 index 000000000..7fa103408 --- /dev/null +++ b/quantum/plugins/nicira/extensions/maclearning.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Nicira Networks, Inc. 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 quantum.api.v2 import attributes + + +MAC_LEARNING = 'mac_learning_enabled' +EXTENDED_ATTRIBUTES_2_0 = { + 'ports': { + MAC_LEARNING: {'allow_post': True, 'allow_put': True, + 'convert_to': attributes.convert_to_boolean, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'is_visible': True}, + } +} + + +class Maclearning(object): + """Extension class supporting port security.""" + + @classmethod + def get_name(cls): + return "MAC Learning" + + @classmethod + def get_alias(cls): + return "mac-learning" + + @classmethod + def get_description(cls): + return "Provides mac learning capabilities" + + @classmethod + def get_namespace(cls): + return "http://docs.openstack.org/ext/maclearning/api/v1.0" + + @classmethod + def get_updated(cls): + return "2013-05-1T10:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + return [] + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/quantum/plugins/nicira/nvplib.py b/quantum/plugins/nicira/nvplib.py index e6f6ff0d6..2072f6441 100644 --- a/quantum/plugins/nicira/nvplib.py +++ b/quantum/plugins/nicira/nvplib.py @@ -738,7 +738,8 @@ def get_port(cluster, network, port, relations=None): def _configure_extensions(lport_obj, mac_address, fixed_ips, - port_security_enabled, security_profiles, queue_id): + port_security_enabled, security_profiles, + queue_id, mac_learning_enabled): lport_obj['allowed_address_pairs'] = [] if port_security_enabled: for fixed_ip in fixed_ips: @@ -753,12 +754,16 @@ def _configure_extensions(lport_obj, mac_address, fixed_ips, "ip_address": "0.0.0.0"}) lport_obj['security_profiles'] = list(security_profiles or []) lport_obj['queue_uuid'] = queue_id + if mac_learning_enabled is not None: + lport_obj["mac_learning"] = mac_learning_enabled + lport_obj["type"] = "LogicalSwitchPortConfig" def update_port(cluster, lswitch_uuid, lport_uuid, quantum_port_id, tenant_id, display_name, device_id, admin_status_enabled, mac_address=None, fixed_ips=None, port_security_enabled=None, - security_profiles=None, queue_id=None): + security_profiles=None, queue_id=None, + mac_learning_enabled=None): # device_id can be longer than 40 so we rehash it hashed_device_id = hashlib.sha1(device_id).hexdigest() lport_obj = dict( @@ -771,7 +776,7 @@ def update_port(cluster, lswitch_uuid, lport_uuid, quantum_port_id, tenant_id, _configure_extensions(lport_obj, mac_address, fixed_ips, port_security_enabled, security_profiles, - queue_id) + queue_id, mac_learning_enabled) path = "/ws.v1/lswitch/" + lswitch_uuid + "/lport/" + lport_uuid try: @@ -791,7 +796,8 @@ def update_port(cluster, lswitch_uuid, lport_uuid, quantum_port_id, tenant_id, def create_lport(cluster, lswitch_uuid, tenant_id, quantum_port_id, display_name, device_id, admin_status_enabled, mac_address=None, fixed_ips=None, port_security_enabled=None, - security_profiles=None, queue_id=None): + security_profiles=None, queue_id=None, + mac_learning_enabled=None): """Creates a logical port on the assigned logical switch.""" # device_id can be longer than 40 so we rehash it hashed_device_id = hashlib.sha1(device_id).hexdigest() @@ -807,7 +813,7 @@ def create_lport(cluster, lswitch_uuid, tenant_id, quantum_port_id, _configure_extensions(lport_obj, mac_address, fixed_ips, port_security_enabled, security_profiles, - queue_id) + queue_id, mac_learning_enabled) path = _build_uri_path(LSWITCHPORT_RESOURCE, parent_resource_id=lswitch_uuid) diff --git a/quantum/tests/unit/nicira/test_maclearning.py b/quantum/tests/unit/nicira/test_maclearning.py new file mode 100644 index 000000000..8a7f5631a --- /dev/null +++ b/quantum/tests/unit/nicira/test_maclearning.py @@ -0,0 +1,141 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2013 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 contextlib +import mock +import os + +from oslo.config import cfg + +from quantum.api.v2 import attributes +from quantum.common.test_lib import test_config +from quantum import context +from quantum.extensions import agent +from quantum.openstack.common import log as logging +import quantum.plugins.nicira as nvp_plugin +from quantum.tests.unit.nicira import fake_nvpapiclient +from quantum.tests.unit import test_db_plugin + + +LOG = logging.getLogger(__name__) +NVP_MODULE_PATH = nvp_plugin.__name__ +NVP_FAKE_RESPS_PATH = os.path.join(os.path.dirname(__file__), 'etc') +NVP_INI_CONFIG_PATH = os.path.join(os.path.dirname(__file__), + 'etc/nvp.ini.full.test') +NVP_EXTENSIONS_PATH = os.path.join(os.path.dirname(__file__), + '../../../plugins/nicira/extensions') + + +class MacLearningExtensionManager(object): + + def get_resources(self): + # Add the resources to the global attribute map + # This is done here as the setup process won't + # initialize the main API router which extends + # the global attribute map + attributes.RESOURCE_ATTRIBUTE_MAP.update( + agent.RESOURCE_ATTRIBUTE_MAP) + return agent.Agent.get_resources() + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class MacLearningDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase): + fmt = 'json' + + def setUp(self): + self.adminContext = context.get_admin_context() + test_config['config_files'] = [NVP_INI_CONFIG_PATH] + test_config['plugin_name_v2'] = ( + 'quantum.plugins.nicira.QuantumPlugin.NvpPluginV2') + cfg.CONF.set_override('api_extensions_path', + NVP_EXTENSIONS_PATH) + # Save the original RESOURCE_ATTRIBUTE_MAP + self.saved_attr_map = {} + for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems(): + self.saved_attr_map[resource] = attrs.copy() + ext_mgr = MacLearningExtensionManager() + test_config['extension_manager'] = ext_mgr + # mock nvp api client + self.fc = fake_nvpapiclient.FakeClient(NVP_FAKE_RESPS_PATH) + self.mock_nvpapi = mock.patch('%s.NvpApiClient.NVPApiHelper' + % NVP_MODULE_PATH, autospec=True) + instance = self.mock_nvpapi.start() + + def _fake_request(*args, **kwargs): + return self.fc.fake_request(*args, **kwargs) + + # Emulate tests against NVP 2.x + instance.return_value.get_nvp_version.return_value = "2.999" + instance.return_value.request.side_effect = _fake_request + cfg.CONF.set_override('metadata_mode', None, 'NVP') + self.addCleanup(self.fc.reset_all) + self.addCleanup(self.mock_nvpapi.stop) + self.addCleanup(self.restore_resource_attribute_map) + self.addCleanup(cfg.CONF.reset) + super(MacLearningDBTestCase, self).setUp() + + def restore_resource_attribute_map(self): + # Restore the original RESOURCE_ATTRIBUTE_MAP + attributes.RESOURCE_ATTRIBUTE_MAP = self.saved_attr_map + + def test_create_with_mac_learning(self): + with self.port(arg_list=('mac_learning_enabled',), + mac_learning_enabled=True) as port: + req = self.new_show_request('ports', port['port']['id'], self.fmt) + sport = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertTrue(sport['port']['mac_learning_enabled']) + + def test_create_port_without_mac_learning(self): + with self.port() as port: + req = self.new_show_request('ports', port['port']['id'], self.fmt) + sport = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertNotIn('mac_learning', sport['port']) + + def test_update_port_with_mac_learning(self): + with self.port(arg_list=('mac_learning_enabled',), + mac_learning_enabled=False) as port: + data = {'port': {'mac_learning_enabled': True}} + req = self.new_update_request('ports', data, port['port']['id']) + res = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertTrue(res['port']['mac_learning_enabled']) + + def test_update_preexisting_port_with_mac_learning(self): + with self.port() as port: + req = self.new_show_request('ports', port['port']['id'], self.fmt) + sport = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertNotIn('mac_learning_enabled', sport['port']) + data = {'port': {'mac_learning_enabled': True}} + req = self.new_update_request('ports', data, port['port']['id']) + res = self.deserialize(self.fmt, req.get_response(self.api)) + self.assertTrue(res['port']['mac_learning_enabled']) + + def test_list_ports(self): + # for this test we need to enable overlapping ips + cfg.CONF.set_default('allow_overlapping_ips', True) + with contextlib.nested(self.port(arg_list=('mac_learning_enabled',), + mac_learning_enabled=True), + self.port(arg_list=('mac_learning_enabled',), + mac_learning_enabled=True), + self.port(arg_list=('mac_learning_enabled',), + mac_learning_enabled=True)): + for port in self._list('ports')['ports']: + self.assertTrue(port['mac_learning_enabled']) -- 2.45.2