[NSXv3]: Add support for L3SPAN

This patch adds support for L3SPAN to the existing tap as a service
NSXv3 driver.
If the source and destination port are not on the same host,
the mirror session is L3SPAN. Backend supports L3SPAN with
SwitchingProfiles of type PortMirroring. Hence, creation of a
tap-flow will result in creation of a switching profile with the
destination port's IP address and then updates the source port
with this newly created switching profile.

Change-Id: I74ea40f8b9c9d1c343a4d9681c3a9ec77b521b6e
This commit is contained in:
Abhishek Raut 2016-07-09 20:14:23 -07:00
parent 7a9336f09f
commit 3eb8e148d4
3 changed files with 214 additions and 45 deletions

View File

@ -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 = []

View File

@ -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)

View File

@ -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: