diff --git a/vmware_nsx/nsxlib/v3/resources.py b/vmware_nsx/nsxlib/v3/resources.py index a5804b42ef..74e7a6c18a 100644 --- a/vmware_nsx/nsxlib/v3/resources.py +++ b/vmware_nsx/nsxlib/v3/resources.py @@ -166,6 +166,15 @@ class SwitchingProfile(AbstractRESTResource): mac_learning=mac_learning, source_mac_change_allowed=True) + def create_port_mirror_profile(self, display_name, description, + direction, destinations, tags=None): + return self.create(SwitchingProfileTypes.PORT_MIRRORING, + display_name=display_name, + description=description, + tags=tags or [], + direction=direction, + destinations=destinations) + @classmethod def build_switch_profile_ids(cls, client, *profiles): ids = [] diff --git a/vmware_nsx/services/neutron_taas/nsx_v3/driver.py b/vmware_nsx/services/neutron_taas/nsx_v3/driver.py index 8b85553f95..a82ab31b82 100644 --- a/vmware_nsx/services/neutron_taas/nsx_v3/driver.py +++ b/vmware_nsx/services/neutron_taas/nsx_v3/driver.py @@ -14,6 +14,10 @@ # License for the specific language governing permissions and limitations # under the License. +import netaddr + +from neutron import manager + from neutron_taas.db import taas_db from neutron_taas.services.taas import service_drivers as base_driver @@ -21,11 +25,12 @@ from oslo_db import exception as db_exc from oslo_log import log as logging from oslo_utils import excutils -from vmware_nsx._i18n import _, _LE +from vmware_nsx._i18n import _, _LE, _LW from vmware_nsx.common import exceptions as nsx_exc from vmware_nsx.common import utils as nsx_utils from vmware_nsx.db import db as nsx_db from vmware_nsx.nsxlib import v3 as nsxlib +from vmware_nsx.nsxlib.v3 import resources as nsx_resources LOG = logging.getLogger(__name__) @@ -39,14 +44,11 @@ class NsxV3Driver(base_driver.TaasBaseDriver, LOG.debug("Loading TaaS NsxV3Driver.") super(NsxV3Driver, self).__init__(service_plugin) + @property + def _nsx_plugin(self): + return manager.NeutronManager.get_plugin() + def _validate_tap_flow(self, source_port, dest_port): - # Verify whether the source port and monitored port belong to the - # same network. - if source_port['network_id'] != dest_port['network_id']: - msg = (_("Destination port %(dest)s and source port %(src)s " - "should be on the same network") % - {'dest': dest_port['id'], 'src': source_port['id']}) - raise nsx_exc.NsxTaaSDriverException(msg=msg) # Verify whether the source port is not same as the destination port if source_port['id'] == dest_port['id']: msg = (_("Destination port %(dest)s is same as source port " @@ -99,32 +101,181 @@ class NsxV3Driver(base_driver.TaasBaseDriver, return {"resource_type": "LogicalPortMirrorDestination", "port_ids": [nsx_port_id]} + def _is_local_span(self, context, src_port_id, dest_port_id): + """Verify whether the mirror session is Local or L3SPAN.""" + # TODO(abhiraut): Create only L3SPAN until we find a way to + # detect local SPAN support from backend. + return False + + def _update_port_at_backend(self, context, port_id, switching_profile, + delete_profile): + """Update a logical port on the backend.""" + port = self._get_port_details(context._plugin_context, + port_id) + # Retrieve logical port ID based on neutron port ID. + nsx_port_id = nsx_db.get_nsx_switch_and_port_id( + session=context._plugin_context.session, + neutron_id=port_id)[1] + # Retrieve source logical port from the backend. + nsx_port = self._nsx_plugin._port_client.get(nsx_port_id) + if delete_profile: + # Prepare switching profile resources retrieved from backend + # and pop the port mirror switching profile. + switching_profile_ids = self._prepare_switch_profiles( + nsx_port.get('switching_profile_ids', []), + switching_profile) + else: + # Prepare switching profile resources retrieved from backend. + switching_profile_ids = self._prepare_switch_profiles( + nsx_port.get('switching_profile_ids', [])) + # Update body with PortMirroring switching profile. + switching_profile_ids.append( + self._get_switching_profile_resource( + switching_profile['id'], + nsx_resources.SwitchingProfileTypes.PORT_MIRRORING)) + address_bindings = self._nsx_plugin._build_address_bindings(port) + #NOTE(abhiraut): Consider passing attachment_type + self._nsx_plugin._port_client.update( + lport_id=nsx_port.get('id'), + vif_uuid=port_id, + name=nsx_port.get('display_name'), + admin_state=nsx_port.get('admin_state'), + address_bindings=address_bindings, + switch_profile_ids=switching_profile_ids,) + + def _prepare_switch_profiles(self, profiles, deleted_profile=None): + switch_profile_ids = [] + if not deleted_profile: + for profile in profiles: + # profile is a dict of type {'key': profile_type, + # 'value': profile_id} + profile_resource = self._get_switching_profile_resource( + profile_id=profile['value'], + profile_type=profile['key']) + switch_profile_ids.append(profile_resource) + else: + for profile in profiles: + if profile['value'] == deleted_profile['id']: + continue + profile_resource = self._get_switching_profile_resource( + profile_id=profile['value'], + profile_type=profile['key']) + switch_profile_ids.append(profile_resource) + return switch_profile_ids + + def _get_switching_profile_resource(self, profile_id, profile_type): + return nsx_resources.SwitchingProfileTypeId( + profile_type=profile_type, + profile_id=profile_id) + def create_tap_flow_postcommit(self, context): """Create tap flow and port mirror session on NSX backend.""" tf = context.tap_flow # Retrieve tap service. ts = self._get_tap_service(context._plugin_context, tf.get('tap_service_id')) + src_port_id = tf.get('source_port') + dest_port_id = ts.get('port_id') tags = nsx_utils.build_v3_tags_payload( tf, resource_type='os-neutron-mirror-id', project_name=context._plugin_context.tenant_name) nsx_direction = self._convert_to_backend_direction( tf.get('direction')) + # Create a port mirroring session object if local SPAN. Otherwise + # create a port mirroring switching profile for L3SPAN. + if self._is_local_span(context, src_port_id, dest_port_id): + self._create_local_span(context, src_port_id, dest_port_id, + nsx_direction, tags) + else: + self._create_l3span(context, src_port_id, dest_port_id, + nsx_direction, tags) + + def _create_l3span(self, context, src_port_id, dest_port_id, direction, + tags): + """Create a PortMirroring SwitchingProfile for L3SPAN.""" + tf = context.tap_flow + dest_port = self._get_port_details(context._plugin_context, + dest_port_id) + destinations = [] + # Retrieve destination port's IP addresses and add it to the list + # since the backend expects a list of IP addresses. + for fixed_ip in dest_port['fixed_ips']: + # NOTE(abhiraut): nsx-v3 doesn't seem to handle ipv6 addresses + # currently so for now we remove them here and do not pass + # them to the backend which would raise an error. + if netaddr.IPNetwork(fixed_ip['ip_address']).version == 6: + LOG.warning(_LW("Skipping IPv6 address %(ip)s for L3SPAN " + "tap flow: %(tap_flow)s"), + {'tap_flow': tf['id'], + 'ip': fixed_ip['ip_address']}) + continue + destinations.append(fixed_ip['ip_address']) + # Create a switch profile in the backend. + try: + port_mirror_profile = (self._nsx_plugin._switching_profiles. + create_port_mirror_profile( + display_name=tf.get('name'), + description=tf.get('description'), + direction=direction, + destinations=destinations, + tags=tags)) + except nsx_exc.ManagerError: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Unable to create port mirror switch profile " + "for tap flow %s on NSX backend, rolling back " + "changes on neutron."), tf['id']) + # Create internal mappings between tap flow and port mirror switch + # profile. Ideally DB transactions must take place in precommit, but + # we rely on the NSX backend to retrieve the port mirror profile UUID, + # we perform the create action in postcommit. + try: + nsx_db.add_port_mirror_session_mapping( + session=context._plugin_context.session, + tf_id=tf['id'], + pm_session_id=port_mirror_profile['id']) + except db_exc.DBError: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Unable to create port mirror session db " + "mappings for tap flow %s. Rolling back " + "changes in Neutron."), tf['id']) + self._nsx_plugin._switching_profiles.delete( + port_mirror_profile['id']) + # Update the source port to include the port mirror switch profile. + try: + self._update_port_at_backend(context=context, port_id=src_port_id, + switching_profile=port_mirror_profile, + delete_profile=False) + except nsx_exc.ManagerError: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Unable to update source port %(port)s with " + "switching profile %(profile) for tap flow " + "%(tap_flow)s on NSX backend, rolling back " + "changes on neutron."), + {'tap_flow': tf['id'], + 'port': src_port_id, + 'profile': port_mirror_profile['id']}) + self._nsx_plugin._switching_profiles.delete( + port_mirror_profile['id']) + + def _create_local_span(self, context, src_port_id, dest_port_id, + direction, tags): + """Create a PortMirroring session on the backend for local SPAN.""" + tf = context.tap_flow # Backend expects a list of source ports and destination ports. # Due to TaaS API requirements, we are only able to add one port # as a source port and one port as a destination port in a single # request. Hence we send a list of one port for source_ports # and dest_ports. nsx_src_ports = self._convert_to_backend_source_port( - context._plugin_context.session, tf.get('source_port')) + context._plugin_context.session, src_port_id) nsx_dest_ports = self._convert_to_backend_dest_port( - context._plugin_context.session, ts.get('port_id')) + context._plugin_context.session, dest_port_id) # Create port mirror session on the backend try: pm_session = nsxlib.create_port_mirror_session( source_ports=nsx_src_ports, dest_ports=nsx_dest_ports, - direction=nsx_direction, + direction=direction, description=tf.get('description'), name=tf.get('name'), tags=tags) @@ -155,19 +306,20 @@ class NsxV3Driver(base_driver.TaasBaseDriver, def delete_tap_flow_postcommit(self, context): """Delete tap flow and port mirror session on NSX backend.""" tf = context.tap_flow + ts = self._get_tap_service(context._plugin_context, + tf.get('tap_service_id')) # Retrieve port mirroring session mappings. pm_session_mapping = nsx_db.get_port_mirror_session_mapping( session=context._plugin_context.session, tf_id=tf['id']) - # Delete port mirroring session on the backend - try: - nsxlib.delete_port_mirror_session( - pm_session_mapping['port_mirror_session_id']) - except nsx_exc.ManagerError: - with excutils.save_and_reraise_exception(): - LOG.error(_LE("Unable to delete port mirror session %s " - "on NSX backend."), - pm_session_mapping['port_mirror_session_id']) + src_port_id = tf.get('source_port') + dest_port_id = ts.get('port_id') + if self._is_local_span(context, src_port_id, dest_port_id): + self._delete_local_span( + context, pm_session_mapping['port_mirror_session_id']) + else: + self._delete_l3span( + context, pm_session_mapping['port_mirror_session_id']) # Delete internal mappings between tap flow and port mirror session. # Ideally DB transactions must take place in precommit, but since we # rely on the DB mapping to retrieve NSX backend UUID for the port @@ -179,4 +331,36 @@ class NsxV3Driver(base_driver.TaasBaseDriver, except db_exc.DBError: with excutils.save_and_reraise_exception(): LOG.error(_LE("Unable to delete port mirror session db " - "mappings for tap flow %s"), tf['id']) + "mappings %(pm)s for tap flow %(tf)s"), tf['id']) + + def _delete_l3span(self, context, pm_profile_id): + tf = context.tap_flow + src_port_id = tf.get('source_port') + port_mirror_profile = self._nsx_plugin._switching_profiles.get( + uuid=pm_profile_id) + try: + # Update source port and remove L3 switching profile. + self._update_port_at_backend(context=context, port_id=src_port_id, + switching_profile=port_mirror_profile, + delete_profile=True) + except nsx_exc.ManagerError: + LOG.error(_LE("Unable to update source port %(port)s " + "to delete port mirror profile %(pm)s on NSX " + "backend."), + {'pm': pm_profile_id, + 'port': src_port_id}) + try: + # Delete port mirroring switching profile + self._nsx_plugin._switching_profiles.delete(uuid=pm_profile_id) + except nsx_exc.ManagerError: + LOG.error(_LE("Unable to delete port mirror switching profile " + "%s on NSX backend."), pm_profile_id) + + def _delete_local_span(self, context, pm_session_id): + # Delete port mirroring session on the backend + try: + nsxlib.delete_port_mirror_session(pm_session_id) + except nsx_exc.ManagerError: + with excutils.save_and_reraise_exception(): + LOG.error(_LE("Unable to delete port mirror session %s " + "on NSX backend."), pm_session_id) diff --git a/vmware_nsx/tests/unit/services/neutron_taas/test_nsxv3_driver.py b/vmware_nsx/tests/unit/services/neutron_taas/test_nsxv3_driver.py index bc10221816..32295b8563 100644 --- a/vmware_nsx/tests/unit/services/neutron_taas/test_nsxv3_driver.py +++ b/vmware_nsx/tests/unit/services/neutron_taas/test_nsxv3_driver.py @@ -53,13 +53,6 @@ class TestNsxV3TaaSDriver(test_taas_db.TaaSDbTestCase, self.driver._validate_tap_flow, src_port['port'], src_port['port']) - def test_validate_tap_flow_different_network_different_port_fail(self): - with self.port() as src_port, self.port() as dest_port: - self.assertRaises(nsx_exc.NsxTaaSDriverException, - self.driver._validate_tap_flow, - src_port['port'], - dest_port['port']) - def test_validate_tap_flow_same_network_different_port(self): with self.network() as network: with self.subnet(network=network) as subnet: @@ -169,23 +162,6 @@ class TestNsxV3TaaSDriver(test_taas_db.TaaSDbTestCase, self.ctx, tf_data) - def test_create_tap_flow_different_network_different_port_fail(self): - tf_name = 'test-tap-flow' - with self.port(tenant_id=self.tenant_id) as src_port: - with self.port(tenant_id=self.tenant_id) as dest_port: - ts_data = self._get_tap_service_data( - port_id=dest_port['port']['id']) - ts = self.taas_plugin.create_tap_service( - self.ctx, ts_data) - tf_data = self._get_tap_flow_data( - tap_service_id=ts['id'], - source_port=src_port['port']['id'], - name=tf_name) - self.assertRaises(nsx_exc.NsxTaaSDriverException, - self.taas_plugin.create_tap_flow, - self.ctx, - tf_data) - def test_delete_tap_flow(self): tf_name = 'test-tap-flow' with self.network() as network: