From 7984544610ed949414967cfe950f18336fa21880 Mon Sep 17 00:00:00 2001 From: elajkat Date: Mon, 5 Jun 2023 13:08:04 +0200 Subject: [PATCH] GRE/ERSPAN mirroring for taas Server side of the Tap Mirroring feature. The RPC and other plugin parts will be in the next patch for the OVS driver. Related-Bug: #2015471 Change-Id: I9a38d9aedbfcec13636420de0b739277a1a8f691 --- devstack/plugin.sh | 3 + etc/neutron/policy.yaml.sample | 21 +++ .../expand/f8f1f10ebaf9_mirroring_for_taas.py | 49 ++++++ .../alembic_migration/versions/EXPAND_HEAD | 2 +- neutron_taas/db/tap_mirror_db.py | 139 ++++++++++++++++++ neutron_taas/extensions/tap_mirror.py | 78 ++++++++++ neutron_taas/policies/__init__.py | 2 + neutron_taas/policies/tap_mirror.py | 78 ++++++++++ .../services/taas/tap_mirror_plugin.py | 128 ++++++++++++++++ .../tests/unit/db/test_tap_mirror_db.py | 119 +++++++++++++++ .../tests/unit/extensions/test_tap_mirror.py | 79 ++++++++++ .../services/taas/test_tap_mirror_plugin.py | 98 ++++++++++++ setup.cfg | 1 + 13 files changed, 796 insertions(+), 1 deletion(-) create mode 100644 neutron_taas/db/migration/alembic_migration/versions/2025.1/expand/f8f1f10ebaf9_mirroring_for_taas.py create mode 100644 neutron_taas/db/tap_mirror_db.py create mode 100644 neutron_taas/extensions/tap_mirror.py create mode 100644 neutron_taas/policies/tap_mirror.py create mode 100644 neutron_taas/services/taas/tap_mirror_plugin.py create mode 100644 neutron_taas/tests/unit/db/test_tap_mirror_db.py create mode 100644 neutron_taas/tests/unit/extensions/test_tap_mirror.py create mode 100644 neutron_taas/tests/unit/services/taas/test_tap_mirror_plugin.py diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 829ca786..a337c41d 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -25,6 +25,9 @@ function install_taas { function configure_taas_plugin { echo "Configuring taas" neutron_service_plugin_class_add taas + if is_service_enabled tap_mirror; then + neutron_service_plugin_class_add tapmirror + fi iniadd /$Q_PLUGIN_CONF_FILE service_providers service_provider "TAAS:TAAS:neutron_taas.services.taas.service_drivers.taas_rpc.TaasRpcDriver:default" } diff --git a/etc/neutron/policy.yaml.sample b/etc/neutron/policy.yaml.sample index 7a9c9346..00b78898 100644 --- a/etc/neutron/policy.yaml.sample +++ b/etc/neutron/policy.yaml.sample @@ -30,3 +30,24 @@ # DELETE /taas/tap_services/{id} #"delete_tap_service": "rule:admin_or_owner" +# Create a Tap Mirror +# POST /taas/tap_mirrors +# Intended scope(s): project +#"create_tap_mirror": "(rule:admin_only) or (role:member and project_id:%(project_id)s)" + +# Update a Tap Mirror +# PUT /taas/tap_mirrors/{id} +# Intended scope(s): project +#"update_tap_mirror": "(rule:admin_only) or (role:member and project_id:%(project_id)s)" + +# Show a Tap Mirror +# GET /taas/tap_mirrors +# GET /taas/tap_mirrors/{id} +# Intended scope(s): project +#"get_tap_mirror": "(rule:admin_only) or (role:reader and project_id:%(project_id)s)" + +# Delete a Tap Mirror +# DELETE /taas/tap_mirrors/{id} +# Intended scope(s): project +#"delete_tap_mirror": "(rule:admin_only) or (role:member and project_id:%(project_id)s)" + diff --git a/neutron_taas/db/migration/alembic_migration/versions/2025.1/expand/f8f1f10ebaf9_mirroring_for_taas.py b/neutron_taas/db/migration/alembic_migration/versions/2025.1/expand/f8f1f10ebaf9_mirroring_for_taas.py new file mode 100644 index 00000000..ad87478d --- /dev/null +++ b/neutron_taas/db/migration/alembic_migration/versions/2025.1/expand/f8f1f10ebaf9_mirroring_for_taas.py @@ -0,0 +1,49 @@ +# 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 alembic import op +from neutron_lib.db import constants as db_const + +import sqlalchemy as sa + + +# ERSPAN or GRE mirroring for taas + +# Revision ID: f8f1f10ebaf9 +# Revises: ccbcc559d175 +# Create Date: 2023-05-05 11:59:10.007052 + +# revision identifiers, used by Alembic. +revision = 'f8f1f10ebaf9' +down_revision = 'ccbcc559d175' + + +mirror_type_enum = sa.Enum('erspanv1', 'gre', name='tapmirrors_type') + + +def upgrade(): + op.create_table( + 'tap_mirrors', + sa.Column('id', sa.String(length=db_const.UUID_FIELD_SIZE), + primary_key=True), + sa.Column('project_id', sa.String( + length=db_const.PROJECT_ID_FIELD_SIZE), nullable=True), + sa.Column('name', sa.String(length=db_const.NAME_FIELD_SIZE), + nullable=True), + sa.Column('description', sa.String( + length=db_const.DESCRIPTION_FIELD_SIZE), nullable=True), + sa.Column('port_id', sa.String(db_const.UUID_FIELD_SIZE), + nullable=False), + sa.Column('directions', sa.String(255), nullable=False), + sa.Column('remote_ip', sa.String(db_const.IP_ADDR_FIELD_SIZE)), + sa.Column('mirror_type', mirror_type_enum, nullable=False) + ) diff --git a/neutron_taas/db/migration/alembic_migration/versions/EXPAND_HEAD b/neutron_taas/db/migration/alembic_migration/versions/EXPAND_HEAD index 61ad9afa..9f4df2e2 100644 --- a/neutron_taas/db/migration/alembic_migration/versions/EXPAND_HEAD +++ b/neutron_taas/db/migration/alembic_migration/versions/EXPAND_HEAD @@ -1 +1 @@ -ccbcc559d175 +f8f1f10ebaf9 diff --git a/neutron_taas/db/tap_mirror_db.py b/neutron_taas/db/tap_mirror_db.py new file mode 100644 index 00000000..6fe1c82d --- /dev/null +++ b/neutron_taas/db/tap_mirror_db.py @@ -0,0 +1,139 @@ +# 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 neutron_lib.api.definitions import tap_mirror as mirror_extension +from neutron_lib.db import api as db_api +from neutron_lib.db import constants as db_const +from neutron_lib.db import model_base +from neutron_lib.db import model_query +from neutron_lib.db import utils as db_utils +from neutron_lib.exceptions import taas as taas_exc +from neutron_lib.plugins import directory +from oslo_log import helpers as log_helpers +from oslo_log import log as logging +from oslo_serialization import jsonutils +from oslo_utils import uuidutils + +from neutron_taas.extensions import tap_mirror as tap_m_extension + +LOG = logging.getLogger(__name__) + + +class TapMirror(model_base.BASEV2, model_base.HasId, + model_base.HasProjectNoIndex): + """Represents a Tap Mirror + + A Tap Mirror can be a GRE or ERSPAN tunnel representation. + """ + + __tablename__ = 'tap_mirrors' + + name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE)) + description = sa.Column(sa.String(db_const.DESCRIPTION_FIELD_SIZE)) + port_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE), + nullable=False) + directions = sa.Column(sa.String(255), nullable=False) + remote_ip = sa.Column(sa.String(db_const.IP_ADDR_FIELD_SIZE), + nullable=False) + mirror_type = sa.Column(sa.Enum('erspanv1', 'gre', + name='tapmirrors_type'), + nullable=False) + api_collections = [mirror_extension.COLLECTION_NAME] + collection_resource_map = { + mirror_extension.COLLECTION_NAME: mirror_extension.RESOURCE_NAME} + + +class Taas_mirror_db_mixin(tap_m_extension.TapMirrorBase): + + def _make_tap_mirror_dict(self, tap_mirror, fields=None): + res = { + 'id': tap_mirror.get('id'), + 'project_id': tap_mirror.get('project_id'), + 'name': tap_mirror.get('name'), + 'description': tap_mirror.get('description'), + 'port_id': tap_mirror.get('port_id'), + 'directions': jsonutils.loads(tap_mirror.get('directions')), + 'remote_ip': tap_mirror.get('remote_ip'), + 'mirror_type': tap_mirror.get('mirror_type'), + } + return db_utils.resource_fields(res, fields) + + @db_api.retry_if_session_inactive() + def get_port_details(self, context, port_id): + with db_api.CONTEXT_READER.using(context): + core_plugin = directory.get_plugin() + return core_plugin.get_port(context, port_id) + + @db_api.retry_if_session_inactive() + @log_helpers.log_method_call + def create_tap_mirror(self, context, tap_mirror): + fields = tap_mirror['tap_mirror'] + project_id = fields.get('project_id') + with db_api.CONTEXT_WRITER.using(context): + tap_mirror_db = TapMirror( + id=uuidutils.generate_uuid(), + project_id=project_id, + name=fields.get('name'), + description=fields.get('description'), + port_id=fields.get('port_id'), + directions=jsonutils.dumps(fields.get('directions')), + remote_ip=fields.get('remote_ip'), + mirror_type=fields.get('mirror_type'), + ) + # TODO(lajoskatona): Check tunnel_id... + context.session.add(tap_mirror_db) + + return self._make_tap_mirror_dict(tap_mirror_db) + + def _get_tap_mirror(self, context, id): + with db_api.CONTEXT_READER.using(context): + try: + return model_query.get_by_id(context, TapMirror, id) + except exc.NoResultFound: + raise taas_exc.TapMirrorNotFound(mirror_id=id) + + @log_helpers.log_method_call + def get_tap_mirror(self, context, id, fields=None): + with db_api.CONTEXT_READER.using(context): + t_m = self._get_tap_mirror(context, id) + return self._make_tap_mirror_dict(t_m, fields) + + @db_api.retry_if_session_inactive() + @log_helpers.log_method_call + def get_tap_mirrors(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + with db_api.CONTEXT_READER.using(context): + return model_query.get_collection(context, TapMirror, + self._make_tap_mirror_dict, + filters=filters, fields=fields) + + @log_helpers.log_method_call + def delete_tap_mirror(self, context, id): + with db_api.CONTEXT_WRITER.using(context): + count = context.session.query(TapMirror).filter_by(id=id).delete() + + if not count: + raise taas_exc.TapMirrorNotFound(mirror_id=id) + + @db_api.retry_if_session_inactive() + @log_helpers.log_method_call + def update_tap_mirror(self, context, id, tap_mirror): + t_m = tap_mirror['tap_mirror'] + with db_api.CONTEXT_WRITER.using(context): + tap_mirror_db = self._get_tap_mirror(context, id) + tap_mirror_db.update(t_m) + return self._make_tap_mirror_dict(tap_mirror_db) diff --git a/neutron_taas/extensions/tap_mirror.py b/neutron_taas/extensions/tap_mirror.py new file mode 100644 index 00000000..ca1786f6 --- /dev/null +++ b/neutron_taas/extensions/tap_mirror.py @@ -0,0 +1,78 @@ +# 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 abc + +from neutron_lib.api.definitions import tap_mirror as tap_mirror_api_def +from neutron_lib.api import extensions as api_extensions +from neutron_lib.services import base as service_base + +from neutron.api.v2 import resource_helper + + +class Tap_mirror(api_extensions.APIExtensionDescriptor): + + api_definition = tap_mirror_api_def + + @classmethod + def get_resources(cls): + plural_mappings = resource_helper.build_plural_mappings( + {}, tap_mirror_api_def.RESOURCE_ATTRIBUTE_MAP) + resources = resource_helper.build_resource_info( + plural_mappings, + tap_mirror_api_def.RESOURCE_ATTRIBUTE_MAP, + tap_mirror_api_def.ALIAS, + translate_name=False, + allow_bulk=False) + + return resources + + @classmethod + def get_plugin_interface(cls): + return TapMirrorBase + + +class TapMirrorBase(service_base.ServicePluginBase, metaclass=abc.ABCMeta): + + def get_plugin_description(self): + return tap_mirror_api_def.DESCRIPTION + + @classmethod + def get_plugin_type(cls): + return tap_mirror_api_def.ALIAS + + @abc.abstractmethod + def create_tap_mirror(self, context, tap_mirror): + """Create a Tap Mirror.""" + pass + + @abc.abstractmethod + def get_tap_mirror(self, context, id, fields=None): + """Get a Tap Mirror.""" + pass + + @abc.abstractmethod + def get_tap_mirrors(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + """List all Tap Mirrors.""" + pass + + @abc.abstractmethod + def delete_tap_mirror(self, context, id): + """Delete a Tap Mirror.""" + pass + + @abc.abstractmethod + def update_tap_mirror(self, context, id, tap_mirror): + """Update a Tap Mirror.""" + pass diff --git a/neutron_taas/policies/__init__.py b/neutron_taas/policies/__init__.py index 5aa05c5f..a5e25d16 100644 --- a/neutron_taas/policies/__init__.py +++ b/neutron_taas/policies/__init__.py @@ -13,6 +13,7 @@ import itertools from neutron_taas.policies import tap_flow +from neutron_taas.policies import tap_mirror from neutron_taas.policies import tap_service @@ -20,4 +21,5 @@ def list_rules(): return itertools.chain( tap_flow.list_rules(), tap_service.list_rules(), + tap_mirror.list_rules(), ) diff --git a/neutron_taas/policies/tap_mirror.py b/neutron_taas/policies/tap_mirror.py new file mode 100644 index 00000000..c5ae7999 --- /dev/null +++ b/neutron_taas/policies/tap_mirror.py @@ -0,0 +1,78 @@ +# 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_policy import policy + +from neutron.conf.policies import base + + +COLLECTION_PATH = '/taas/tap_mirrors' +RESOURCE_PATH = '/taas/tap_mirrors/{id}' + +rules = [ + policy.DocumentedRuleDefault( + name='create_tap_mirror', + check_str=base.ADMIN_OR_PROJECT_MEMBER, + scope_types=['project'], + description='Create a Tap Mirror', + operations=[ + { + 'method': 'POST', + 'path': COLLECTION_PATH + } + ], + ), + policy.DocumentedRuleDefault( + name='update_tap_mirror', + check_str=base.ADMIN_OR_PROJECT_MEMBER, + scope_types=['project'], + description='Update a Tap Mirror', + operations=[ + { + 'method': 'PUT', + 'path': RESOURCE_PATH + } + ], + ), + policy.DocumentedRuleDefault( + name='get_tap_mirror', + check_str=base.ADMIN_OR_PROJECT_READER, + scope_types=['project'], + description='Show a Tap Mirror', + operations=[ + { + 'method': 'GET', + 'path': COLLECTION_PATH + }, + { + 'method': 'GET', + 'path': RESOURCE_PATH + }, + ] + ), + policy.DocumentedRuleDefault( + name='delete_tap_mirror', + check_str=base.ADMIN_OR_PROJECT_MEMBER, + scope_types=['project'], + description='Delete a Tap Mirror', + operations=[ + { + 'method': 'DELETE', + 'path': RESOURCE_PATH, + } + ] + ), +] + + +def list_rules(): + return rules diff --git a/neutron_taas/services/taas/tap_mirror_plugin.py b/neutron_taas/services/taas/tap_mirror_plugin.py new file mode 100644 index 00000000..71ed9507 --- /dev/null +++ b/neutron_taas/services/taas/tap_mirror_plugin.py @@ -0,0 +1,128 @@ +# 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.db import servicetype_db as st_db +from neutron.services import provider_configuration as pconf +from neutron.services import service_base +from neutron_lib.api.definitions import portbindings +from neutron_lib.api.definitions import tap_mirror as t_m_api_def +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib.db import api as db_api +from neutron_lib import exceptions as n_exc +from neutron_lib.exceptions import taas as taas_exc +from oslo_log import helpers as log_helpers +from oslo_log import log as logging + +from neutron_taas.common import constants as taas_consts +from neutron_taas.db import tap_mirror_db + + +LOG = logging.getLogger(__name__) + + +@registry.has_registry_receivers +class TapMirrorPlugin(tap_mirror_db.Taas_mirror_db_mixin): + + supported_extension_aliases = [t_m_api_def.ALIAS] + path_prefix = "/taas" + + def __init__(self): + super().__init__() + self.service_type_manager = st_db.ServiceTypeManager.get_instance() + self.service_type_manager.add_provider_configuration( + taas_consts.TAAS, + pconf.ProviderConfiguration('neutron_taas')) + + # Load the service driver from neutron.conf. + self.drivers, self.default_provider = service_base.load_drivers( + taas_consts.TAAS, self) + # Associate driver names to driver objects + for driver_name, driver in self.drivers.items(): + driver.name = driver_name + + self.driver = self._get_driver_for_provider(self.default_provider) + + LOG.info(("Tap Mirror plugin using service drivers: " + "%(service_drivers)s, default: %(default_driver)s"), + {'service_drivers': self.drivers.keys(), + 'default_driver': self.default_provider}) + + def _get_driver_for_provider(self, provider): + if provider in self.drivers: + return self.drivers[provider] + raise n_exc.Invalid("Error retrieving driver for provider %s" % + provider) + + @log_helpers.log_method_call + def create_tap_mirror(self, context, tap_mirror): + t_m = tap_mirror['tap_mirror'] + port_id = t_m['port_id'] + project_id = t_m['project_id'] + + with db_api.CONTEXT_READER.using(context): + # Get port details + port = self.get_port_details(context, port_id) + if port['tenant_id'] != project_id: + raise taas_exc.PortDoesNotBelongToTenant() + + host = port[portbindings.HOST_ID] + if host is not None: + LOG.debug("Host on which the port is created = %s", host) + else: + LOG.debug("Host could not be found, Port Binding disabled!") + + with db_api.CONTEXT_WRITER.using(context): + self._validate_tap_tunnel_id(context, t_m['directions']) + tm = super().create_tap_mirror(context, tap_mirror) + + return tm + + def _validate_tap_tunnel_id(self, context, mirror_directions): + mirrors = self.get_tap_mirrors(context) + for mirror in mirrors: + for direction, tunnel_id in mirror['directions'].items(): + if tunnel_id in mirror_directions.values(): + raise taas_exc.TapMirrorTunnelConflict( + tunnel_id=tunnel_id) + + @log_helpers.log_method_call + def delete_tap_mirror(self, context, id): + with db_api.CONTEXT_WRITER.using(context): + tm = self.get_tap_mirror(context, id) + if tm: + # check if tunnel id was really deleted + super().delete_tap_mirror(context, id) + + @registry.receives(resources.PORT, [events.PRECOMMIT_DELETE]) + @log_helpers.log_method_call + def handle_delete_port(self, resource, event, trigger, payload): + context = payload.context + deleted_port = payload.latest_state + if not deleted_port: + LOG.error("Tap Mirror: Handle Delete Port: " + "Invalid port object received") + return + + deleted_port_id = deleted_port['id'] + LOG.info("Tap Mirror: Handle Delete Port: %s", deleted_port_id) + + tap_mirrors = self.get_tap_mirrors( + context, + filters={'port_id': [deleted_port_id]}, fields=['id']) + + for t_m in tap_mirrors: + try: + self.delete_tap_mirror(context, t_m['id']) + except taas_exc.TapMirrorNotFound: + LOG.debug("Tap Mirror not found: %s", t_m['id']) diff --git a/neutron_taas/tests/unit/db/test_tap_mirror_db.py b/neutron_taas/tests/unit/db/test_tap_mirror_db.py new file mode 100644 index 00000000..0ce3f4ae --- /dev/null +++ b/neutron_taas/tests/unit/db/test_tap_mirror_db.py @@ -0,0 +1,119 @@ +# 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.tests.unit import testlib_api + +from neutron_lib import context +from neutron_lib.exceptions import taas as taas_exc + +from oslo_utils import importutils +from oslo_utils import uuidutils + +from neutron_taas.db import tap_mirror_db + + +DB_PLUGIN_KLAAS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2' +_uuid = uuidutils.generate_uuid + + +class TapMirrorDbTestCase(testlib_api.SqlTestCase): + + """Unit test for Tap Mirror DB support.""" + + def setUp(self): + super().setUp() + self.ctx = context.get_admin_context() + self.db_mixin = tap_mirror_db.Taas_mirror_db_mixin() + self.plugin = importutils.import_object(DB_PLUGIN_KLAAS) + self.project_id = 'fake-project-id' + + def _get_tap_mirror_data(self, name='tm-1', port_id=None, + directions='{"IN": "99"}', remote_ip='10.99.8.3', + mirror_type='erspanv1'): + port_id = port_id or _uuid() + return {"tap_mirror": {"name": name, + "project_id": self.project_id, + "description": "test tap mirror", + "port_id": port_id, + 'directions': directions, + 'remote_ip': remote_ip, + 'mirror_type': mirror_type + } + } + + def _get_tap_mirror(self, tap_mirror_id): + """Helper method to retrieve tap Mirror.""" + with self.ctx.session.begin(): + return self.db_mixin.get_tap_mirror(self.ctx, tap_mirror_id) + + def _get_tap_mirrors(self): + """Helper method to retrieve all tap Mirror.""" + with self.ctx.session.begin(): + return self.db_mixin.get_tap_mirrors(self.ctx) + + def _create_tap_mirror(self, tap_mirror): + """Helper method to create tap Mirror.""" + with self.ctx.session.begin(): + return self.db_mixin.create_tap_mirror(self.ctx, tap_mirror) + + def _update_tap_mirror(self, tap_mirror_id, tap_mirror): + """Helper method to update tap Mirror.""" + with self.ctx.session.begin(): + return self.db_mixin.update_tap_mirror(self.ctx, + tap_mirror_id, + tap_mirror) + + def _delete_tap_mirror(self, tap_mirror_id): + """Helper method to delete tap Mirror.""" + with self.ctx.session.begin(): + return self.db_mixin.delete_tap_mirror(self.ctx, tap_mirror_id) + + def test_tap_mirror_get(self): + name = 'test-tap-mirror' + data = self._get_tap_mirror_data(name=name) + result = self._create_tap_mirror(data) + get_result = self._get_tap_mirror(result['id']) + self.assertEqual(name, get_result['name']) + + def test_tap_mirror_create(self): + name = 'test-tap-mirror' + port_id = _uuid() + data = self._get_tap_mirror_data(name=name, port_id=port_id) + result = self._create_tap_mirror(data) + self.assertEqual(name, result['name']) + self.assertEqual(port_id, result['port_id']) + + def test_tap_mirror_list(self): + name_1 = "tm-1" + data_1 = self._get_tap_mirror_data(name=name_1) + name_2 = "tm-2" + data_2 = self._get_tap_mirror_data(name=name_2) + self._create_tap_mirror(data_1) + self._create_tap_mirror(data_2) + tap_mirrors = self._get_tap_mirrors() + self.assertEqual(2, len(tap_mirrors)) + + def test_tap_mirror_update(self): + original_name = "tm-1" + updated_name = "tm-1-got-updated" + data = self._get_tap_mirror_data(name=original_name) + tm = self._create_tap_mirror(data) + updated_data = self._get_tap_mirror_data(name=updated_name) + tm_updated = self._update_tap_mirror(tm['id'], updated_data) + self.assertEqual(updated_name, tm_updated['name']) + + def test_tap_mirror_delete(self): + data = self._get_tap_mirror_data() + result = self._create_tap_mirror(data) + self._delete_tap_mirror(result['id']) + self.assertRaises(taas_exc.TapMirrorNotFound, + self._get_tap_mirror, result['id']) diff --git a/neutron_taas/tests/unit/extensions/test_tap_mirror.py b/neutron_taas/tests/unit/extensions/test_tap_mirror.py new file mode 100644 index 00000000..ee2c01d1 --- /dev/null +++ b/neutron_taas/tests/unit/extensions/test_tap_mirror.py @@ -0,0 +1,79 @@ +# 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 copy +from unittest import mock +from webob import exc + +from oslo_utils import uuidutils + +from neutron.api import extensions +from neutron.conf import common as conf_common +from neutron.tests.unit.api.v2 import test_base as test_api_v2 +from neutron.tests.unit.extensions import base as test_extensions_base + +from neutron_lib.api.definitions import tap_mirror as tap_mirror_api + +from neutron_taas import extensions as taas_extensions + + +TAP_MIRROR_PATH = 'taas/tap_mirrors' + + +class TapMirrorExtensionTestCase(test_extensions_base.ExtensionTestCase): + + def setUp(self): + conf_common.register_core_common_config_opts() + extensions.append_api_extensions_path(taas_extensions.__path__) + super().setUp() + plural_mappings = {'tap_mirror': 'tap_mirrors'} + self.setup_extension( + '%s.%s' % (taas_extensions.tap_mirror.TapMirrorBase.__module__, + taas_extensions.tap_mirror.TapMirrorBase.__name__), + tap_mirror_api.ALIAS, + taas_extensions.tap_mirror.Tap_mirror, + 'taas', + plural_mappings=plural_mappings, + translate_resource_name=False) + self.instance = self.plugin.return_value + + def test_create_tap_mirror(self): + project_id = uuidutils.generate_uuid() + tap_mirror_data = { + 'project_id': project_id, + 'tenant_id': project_id, + 'name': 'MyMirror', + 'description': 'This is my Tap Mirror', + 'port_id': uuidutils.generate_uuid(), + 'directions': {"IN": 101}, + 'remote_ip': '10.99.8.3', + 'mirror_type': 'gre', + } + data = {'tap_mirror': tap_mirror_data} + expected_ret_val = copy.copy(tap_mirror_data) + expected_ret_val.update({'id': uuidutils.generate_uuid()}) + self.instance.create_tap_mirror.return_value = expected_ret_val + + res = self.api.post(test_api_v2._get_path(TAP_MIRROR_PATH, + fmt=self.fmt), + self.serialize(data), + content_type='application/%s' % self.fmt) + self.instance.create_tap_mirror.assert_called_with( + mock.ANY, + tap_mirror=data) + self.assertEqual(exc.HTTPCreated.code, res.status_int) + res = self.deserialize(res) + self.assertIn('tap_mirror', res) + self.assertEqual(expected_ret_val, res['tap_mirror']) + + def test_delete_tap_mirror(self): + self._test_entity_delete('tap_mirror') diff --git a/neutron_taas/tests/unit/services/taas/test_tap_mirror_plugin.py b/neutron_taas/tests/unit/services/taas/test_tap_mirror_plugin.py new file mode 100644 index 00000000..388fe190 --- /dev/null +++ b/neutron_taas/tests/unit/services/taas/test_tap_mirror_plugin.py @@ -0,0 +1,98 @@ +# 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 testtools +from unittest import mock + +from neutron.tests.unit import testlib_api +from neutron_lib import context +from neutron_lib.exceptions import taas as taas_exc +from neutron_lib import rpc as n_rpc +from neutron_lib.utils import net as n_utils +from oslo_utils import uuidutils + +from neutron_taas.services.taas import tap_mirror_plugin + + +class TestTapMirrorPlugin(testlib_api.SqlTestCase): + def setUp(self): + super().setUp() + mock.patch.object(n_rpc, 'Connection', spec=object).start() + + self.driver = mock.MagicMock() + mock.patch('neutron.services.service_base.load_drivers', + return_value=({'dummy_provider': self.driver}, + 'dummy_provider')).start() + mock.patch('neutron.db.servicetype_db.ServiceTypeManager.get_instance', + return_value=mock.MagicMock()).start() + self._plugin = tap_mirror_plugin.TapMirrorPlugin() + self._context = context.get_admin_context() + + self._project_id = self._tenant_id = uuidutils.generate_uuid() + self._network_id = uuidutils.generate_uuid() + self._host_id = 'host-A' + self._port_id = uuidutils.generate_uuid() + self._port_details = { + 'tenant_id': self._tenant_id, + 'binding:host_id': self._host_id, + 'mac_address': n_utils.get_random_mac( + 'fa:16:3e:00:00:00'.split(':')), + } + self._tap_mirror = { + 'project_id': self._project_id, + 'tenant_id': self._tenant_id, + 'name': 'MyMirror', + 'description': 'This is my Tap Mirror', + 'port_id': self._port_id, + 'directions': {"IN": 101}, + 'remote_ip': '10.99.8.3', + 'mirror_type': 'gre', + } + + @contextlib.contextmanager + def tap_mirror(self): + req = { + 'tap_mirror': self._tap_mirror, + } + with mock.patch.object(self._plugin, 'get_port_details', + return_value=self._port_details): + mirror = self._plugin.create_tap_mirror(self._context, req) + self._tap_mirror['id'] = mock.ANY + + # TODO(lajoskatona): Add more checks for the pre/post phases + # (check the next patches) + + yield self._plugin.get_tap_mirror(self._context, + mirror['id']) + + def test_create_tap_mirror(self): + with self.tap_mirror(): + pass + + def test_create_tap_mirror_wrong_project_id(self): + self._port_details['project_id'] = 'other-tenant' + self._port_details['tenant_id'] = 'other-tenant' + with testtools.ExpectedException(taas_exc.PortDoesNotBelongToTenant), \ + self.tap_mirror(): + pass + self.assertEqual([], self.driver.mock_calls) + + def test_delete_tap_mrror(self): + with self.tap_mirror() as tm: + self._plugin.delete_tap_mirror(self._context, tm['id']) + self._tap_mirror['id'] = tm['id'] + + def test_delete_tap_mirror_non_existent(self): + with testtools.ExpectedException(taas_exc.TapMirrorNotFound): + self._plugin.delete_tap_mirror(self._context, 'non-existent') diff --git a/setup.cfg b/setup.cfg index a290030b..f91961f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,7 @@ neutron_taas.taas.agent_drivers = sriov = neutron_taas.services.taas.drivers.linux.sriov_nic_taas:SriovNicTaasDriver neutron.service_plugins = taas = neutron_taas.services.taas.taas_plugin:TaasPlugin + tapmirror = neutron_taas.services.taas.tap_mirror_plugin:TapMirrorPlugin neutron.db.alembic_migrations = tap-as-a-service = neutron_taas.db.migration:alembic_migration oslo.config.opts =