Add SRIOV mirroring support to Tap as a Service.

The following patch allows VF to VF mirroring in Tap-as-a-Service
Code-changes are applicable for new tap agent driver for sriov on
Intel i40e nic.

Vlan Mirror input parameter is part of tap-flow-create API

The current TaaS SRIOV driver is based on Intel i40e NIC driver with
following requirements:-

hardware: Intel Ethernet Network Adapter XXV710 (25GbE)
Driver: Intel i40e v4.16.0

Ref Spec: openstack/neutron-specs/specs/rocky/port-mirroring-sriov-vfs.rst
Commit: https://review.openstack.org/#/c/574477/

Change-Id: Id3aa83d7e1e22ae1806cef0c93e5dd61169c6735
This commit is contained in:
Deepak Tiwari 2018-09-19 02:59:34 +05:30
parent 92263b67a2
commit 2c83eb26a5
37 changed files with 1743 additions and 61 deletions

View File

@ -69,7 +69,10 @@ TapFlow Represents the port from which the traffic needs to be mirrored.
'required_by_policy': True, 'is_visible': True}, 'required_by_policy': True, 'is_visible': True},
'direction': {'allow_post': True, 'allow_put': False, 'direction': {'allow_post': True, 'allow_put': False,
'validate': {'type:values': direction_enum}, 'validate': {'type:values': direction_enum},
'is_visible': True} 'is_visible': True},
'vlan_filter': {'allow_post': True, 'allow_put': False,
'validate': {'type:regex_or_none': RANGE_REGEX},
'is_visible': True, 'default': None}
} }
direction_enum = ['IN', 'OUT', 'BOTH'] direction_enum = ['IN', 'OUT', 'BOTH']
@ -172,7 +175,8 @@ extension
"name": "flow1", "name": "flow1",
"source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1", "source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1",
"tap_service_id": "69bd12b2-0e13-45ec-9045-b674fd9f0468", "tap_service_id": "69bd12b2-0e13-45ec-9045-b674fd9f0468",
"tenant_id": "97e1586d580745d7b311406697aaf097" "tenant_id": "97e1586d580745d7b311406697aaf097",
"vlan_filter": "9,18-27,36,45,54-63"
} }
} }
@ -190,7 +194,8 @@ extension
"name": "flow1", "name": "flow1",
"source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1", "source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1",
"tap_service_id": "69bd12b2-0e13-45ec-9045-b674fd9f0468", "tap_service_id": "69bd12b2-0e13-45ec-9045-b674fd9f0468",
"tenant_id": "97e1586d580745d7b311406697aaf097" "tenant_id": "97e1586d580745d7b311406697aaf097",
"vlan_filter": "9,18-27,36,45,54-63"
} }
} }
@ -215,7 +220,8 @@ extension
"name": "flow1", "name": "flow1",
"source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1", "source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1",
"tap_service_id": "c352f537-ad49-48eb-ab05-1c6b8cb900ff", "tap_service_id": "c352f537-ad49-48eb-ab05-1c6b8cb900ff",
"tenant_id": "97e1586d580745d7b311406697aaf097" "tenant_id": "97e1586d580745d7b311406697aaf097",
"vlan_filter": "9,18-27,36,45,54-63"
} }
] ]
} }

28
bin/i40e_sysfs_command Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Copyright (c) 2018 AT&T Corporation
# 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.
help_msg="Incorrect arguments supplied!! Aborting....\nUsage: 1. VLANs to VF mirroring: $0 <phy-device-name> <dest-vf-index> <'vlan_mirror'> <'add'|'rem'> <vlan_ranges_string>\nUsage: 2. VF to VF mirroring: $0 <phy-device-name> <src-vf-index> <'ingress_mirror'|'egress_mirror'> <'add'|'rem'> <dest-vf-index>"
(($#!=5)) && { echo -e ${help_msg} && exit 1; }
if [ -f /sys/class/net/${1}/device/sriov/${2}/${3} ]
then
echo ${4} ${5} > /sys/class/net/${1}/device/sriov/${2}/${3}
else
echo "Invalid sysfs path: /sys/class/net/${1}/device/sriov/${2}/${3}"
exit 1
fi

View File

@ -24,7 +24,7 @@ OVERRIDE_ENABLED_SERVICES+=,g-api,g-reg
OVERRIDE_ENABLED_SERVICES+=,n-api,n-cond,n-cpu,n-crt,n-sch,placement-api OVERRIDE_ENABLED_SERVICES+=,n-api,n-cond,n-cpu,n-crt,n-sch,placement-api
OVERRIDE_ENABLED_SERVICES+=,n-api-meta OVERRIDE_ENABLED_SERVICES+=,n-api-meta
OVERRIDE_ENABLED_SERVICES+=,q-agt,q-dhcp,q-l3,q-meta,q-metering,q-svc,quantum OVERRIDE_ENABLED_SERVICES+=,q-agt,q-dhcp,q-l3,q-meta,q-metering,q-svc,quantum
OVERRIDE_ENABLED_SERVICES+=,taas,taas_openvswitch_agent OVERRIDE_ENABLED_SERVICES+=,taas,taas_agent
OVERRIDE_ENABLED_SERVICES+=,tempest,dstat OVERRIDE_ENABLED_SERVICES+=,tempest,dstat
export OVERRIDE_ENABLED_SERVICES export OVERRIDE_ENABLED_SERVICES

View File

@ -26,6 +26,7 @@ function configure_taas_plugin {
cp $TAAS_PLUGIN_PATH/etc/taas_plugin.ini $TAAS_PLUGIN_CONF_FILE cp $TAAS_PLUGIN_PATH/etc/taas_plugin.ini $TAAS_PLUGIN_CONF_FILE
neutron_server_config_add $TAAS_PLUGIN_CONF_FILE neutron_server_config_add $TAAS_PLUGIN_CONF_FILE
neutron_service_plugin_class_add taas neutron_service_plugin_class_add taas
neutron_deploy_rootwrap_filters $TAAS_PLUGIN_PATH
} }
if is_service_enabled taas; then if is_service_enabled taas; then

View File

@ -2,5 +2,3 @@
ABSOLUTE_PATH=$(cd `dirname "${BASH_SOURCE[0]}"` && pwd) ABSOLUTE_PATH=$(cd `dirname "${BASH_SOURCE[0]}"` && pwd)
TAAS_PLUGIN_PATH=$ABSOLUTE_PATH/.. TAAS_PLUGIN_PATH=$ABSOLUTE_PATH/..
TAAS_PLUGIN_CONF_FILE="/etc/neutron/taas_plugin.ini" TAAS_PLUGIN_CONF_FILE="/etc/neutron/taas_plugin.ini"
TAAS_OVS_AGENT_BINARY="$NEUTRON_BIN_DIR/neutron-taas-openvswitch-agent"
TAAS_OVS_AGENT_CONF_FILE="/etc/neutron/taas.ini"

View File

@ -0,0 +1,9 @@
# taas-i40e-sysfs filters
# This file should be owned by (and only-writeable by) the root user
[Filters]
# This is needed to allow taas to insert/remove vlan id to the
# target vf under /sys/class/net/[device-name]/device/sriov/[vf-index]/[mirror]
i40e_sysfs_command: RegExpFilter, i40e_sysfs_command, root, i40e_sysfs_command, (?!.*\.\..*|.*\/.*).*, [0-9]+, (vlan|egress|ingress)_mirror, (?i)(add|rem), .*

View File

@ -53,7 +53,8 @@ msgpack==0.5.6
munch==2.2.0 munch==2.2.0
netaddr==0.7.19 netaddr==0.7.19
netifaces==0.10.6 netifaces==0.10.6
neutron-lib==1.20.0 neutron-lib==1.25.0
neutron==14.0.0.0b3
openstacksdk==0.12.0 openstacksdk==0.12.0
os-client-config==1.29.0 os-client-config==1.29.0
os-service-types==1.2.0 os-service-types==1.2.0

View File

@ -1,3 +1,4 @@
# Copyright (C) 2018 AT&T
# Copyright (C) 2015 Midokura SARL. # Copyright (C) 2015 Midokura SARL.
# All Rights Reserved. # All Rights Reserved.
# #
@ -14,3 +15,6 @@
# under the License. # under the License.
TAAS = 'TAAS' TAAS = 'TAAS'
# Complete VLAN Id Range
VLAN_RANGE = '0-4095'

View File

@ -0,0 +1,49 @@
# Copyright (C) 2018 AT&T
#
# 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.
def get_list_from_ranges_str(ranges_str):
"""Convert the range in string format to ranges list
And yield the merged ranges in order. The argument must be a
string having comma separated vlan and vlan-ranges.
get_list_from_ranges_str("4,6,10-13,25-27,100-103")
[4, 6, 10, 11, 12, 13, 25, 26, 27, 100, 101, 102, 103]
"""
return sum(((list(range(*[int(range_start) + range_index
for range_index, range_start in
enumerate(range_item.split('-'))]))
if '-' in range_item else [int(range_item)])
for range_item in ranges_str.split(',')), [])
def get_ranges_str_from_list(ranges):
"""Convert the ranges list to string format
And yield the merged ranges in order in string format.
The argument must be an iterable of pairs (start, stop).
get_ranges_str_from_list([4, 11, 12, 13, 25, 26, 27, 101, 102, 103])
"4,11-13,25-27,101-103"
"""
ranges_str = []
for val in sorted(ranges):
if not ranges_str or ranges_str[-1][-1] + 1 != val:
ranges_str.append([val])
else:
ranges_str[-1].append(val)
return ",".join([str(range_item[0]) if len(range_item) == 1
else str(range_item[0]) + "-" + str(range_item[-1])
for range_item in ranges_str])

View File

@ -1 +1 @@
fddbdec8711a ccbcc559d175

View File

@ -0,0 +1,35 @@
# Copyright (C) 2018 AT&T
#
# 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.
"""add_vlan_filter_to_tap_flow
Revision ID: ccbcc559d175
Revises: fddbdec8711a
Create Date: 2018-09-18 19:33:32.119458
"""
# revision identifiers, used by Alembic.
revision = 'ccbcc559d175'
down_revision = 'fddbdec8711a'
from alembic import op
import sqlalchemy as sa
TABLE_NAME = 'tap_flows'
def upgrade():
op.add_column(TABLE_NAME, sa.Column('vlan_filter', sa.String(1024),
nullable=True))

View File

@ -1,3 +1,4 @@
# Copyright (C) 2018 AT&T
# Copyright (C) 2015 Ericsson AB # Copyright (C) 2015 Ericsson AB
# Copyright (c) 2015 Gigamon # Copyright (c) 2015 Gigamon
# #
@ -60,6 +61,7 @@ class TapFlow(model_base.BASEV2, model_base.HasId,
nullable=False) nullable=False)
status = sa.Column(sa.String(16), nullable=False, status = sa.Column(sa.String(16), nullable=False,
server_default=constants.ACTIVE) server_default=constants.ACTIVE)
vlan_filter = sa.Column(sa.String(1024), nullable=True)
class TapIdAssociation(model_base.BASEV2): class TapIdAssociation(model_base.BASEV2):
@ -128,7 +130,8 @@ class Taas_db_Mixin(taas.TaasPluginBase):
'description': tap_flow['description'], 'description': tap_flow['description'],
'source_port': tap_flow['source_port'], 'source_port': tap_flow['source_port'],
'direction': tap_flow['direction'], 'direction': tap_flow['direction'],
'status': tap_flow['status']} 'status': tap_flow['status'],
'vlan_filter': tap_flow['vlan_filter']}
return db_utils.resource_fields(res, fields) return db_utils.resource_fields(res, fields)
@ -209,6 +212,7 @@ class Taas_db_Mixin(taas.TaasPluginBase):
source_port=t_f['source_port'], source_port=t_f['source_port'],
direction=t_f['direction'], direction=t_f['direction'],
status=constants.ACTIVE, status=constants.ACTIVE,
vlan_filter=t_f['vlan_filter'],
) )
context.session.add(tap_flow_db) context.session.add(tap_flow_db)

View File

@ -0,0 +1,59 @@
# Copyright (C) 2018 AT&T
#
# 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_lib.api import extensions
# Regex for a comma-seperate list of integer values (VLANs)
# For ex. "9,18,27-36,45-54" or "0-4095" or "9,18,27,36"
RANGE_REGEX = r"^([0-9]+(-[0-9]+)?)(,([0-9]+(-[0-9]+)?))*$"
EXTENDED_ATTRIBUTES_2_0 = {
'tap_flows': {
'vlan_filter': {'allow_post': True, 'allow_put': False,
'validate': {'type:regex_or_none': RANGE_REGEX},
'is_visible': True, 'default': None}
}
}
class Vlan_filter(extensions.ExtensionDescriptor):
"""Extension class supporting vlan_filter for tap_flows."""
@classmethod
def get_name(cls):
return "TaaS Vlan Filter Extension"
@classmethod
def get_alias(cls):
return 'taas-vlan-filter'
@classmethod
def get_description(cls):
return "Vlan Filter support for Tap Flows."
@classmethod
def get_updated(cls):
return "2019-01-23T00:00:00-00:00"
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
return {}
def get_required_extensions(self):
return ["taas"]
def get_optional_extensions(self):
return []

View File

@ -1,3 +1,4 @@
# Copyright (C) 2018 AT&T
# Copyright (C) 2015 Ericsson AB # Copyright (C) 2015 Ericsson AB
# Copyright (c) 2015 Gigamon # Copyright (c) 2015 Gigamon
# #
@ -15,35 +16,54 @@
from neutron import manager from neutron import manager
from neutron_taas.services.taas.drivers.linux \
import ovs_constants as taas_ovs_consts
from neutron_taas.common import topics from neutron_taas.common import topics
from neutron_taas.services.taas.agents import taas_agent_api as api from neutron_taas.services.taas.agents import taas_agent_api as api
from neutron_lib import context as neutron_context
from neutron_lib import rpc as n_rpc from neutron_lib import rpc as n_rpc
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
import oslo_messaging as messaging
from oslo_service import service from oslo_service import service
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class TaasOvsPluginApi(api.TaasPluginApiMixin): class TaasPluginApi(api.TaasPluginApiMixin):
# Currently there are not any APIs from the the agent towards plugin
def __init__(self, topic, host): def __init__(self, topic, host):
super(TaasOvsPluginApi, self).__init__(topic, host) super(TaasPluginApi, self).__init__(topic, host)
target = messaging.Target(topic=topic, version='1.0')
self.client = n_rpc.get_client(target)
return
def sync_tap_resources(self, sync_tap_res, host):
"""Send Rpc to plugin to recreate pre-existing tap resources."""
LOG.debug("In RPC Call for Sync Tap Resources: Host=%s, MSG=%s" %
(host, sync_tap_res))
context = neutron_context.get_admin_context()
cctxt = self.client.prepare(fanout=False)
cctxt.cast(context, 'sync_tap_resources', sync_tap_res=sync_tap_res,
host=host)
return return
class TaasOvsAgentRpcCallback(api.TaasAgentRpcCallbackMixin): class TaasAgentRpcCallback(api.TaasAgentRpcCallbackMixin):
def __init__(self, conf, driver_type): def __init__(self, conf, driver_type):
LOG.debug("TaaS OVS Agent initialize called")
LOG.debug("TaaS Agent initialize called")
self.conf = conf self.conf = conf
self.driver_type = driver_type self.driver_type = driver_type
super(TaasOvsAgentRpcCallback, self).__init__() super(TaasAgentRpcCallback, self).__init__()
def initialize(self): def initialize(self):
self.taas_driver = manager.NeutronManager.load_class_for_provider( self.taas_driver = manager.NeutronManager.load_class_for_provider(
@ -52,7 +72,7 @@ class TaasOvsAgentRpcCallback(api.TaasAgentRpcCallbackMixin):
self.taas_driver.initialize() self.taas_driver.initialize()
self._taas_rpc_setup() self._taas_rpc_setup()
TaasOvsAgentService(self).start() TaasAgentService(self).start(self.taas_plugin_rpc, self.conf.host)
def consume_api(self, agent_api): def consume_api(self, agent_api):
self.agent_api = agent_api self.agent_api = agent_api
@ -104,6 +124,9 @@ class TaasOvsAgentRpcCallback(api.TaasAgentRpcCallbackMixin):
def delete_tap_flow(self, context, tap_flow_msg, host): def delete_tap_flow(self, context, tap_flow_msg, host):
if host != self.conf.host: if host != self.conf.host:
LOG.debug("RPC Call for Delete Tap Flow. Host value [%s]"
"(received in RPC) doesn't match the host value "
"stored in agent [%s]" % (host, self.conf.host))
return return
LOG.debug("In RPC Call for Delete Tap Flow: MSG=%s" % tap_flow_msg) LOG.debug("In RPC Call for Delete Tap Flow: MSG=%s" % tap_flow_msg)
@ -114,7 +137,7 @@ class TaasOvsAgentRpcCallback(api.TaasAgentRpcCallbackMixin):
def _taas_rpc_setup(self): def _taas_rpc_setup(self):
# setup RPC to msg taas plugin # setup RPC to msg taas plugin
self.taas_plugin_rpc = TaasOvsPluginApi( self.taas_plugin_rpc = TaasPluginApi(
topics.TAAS_PLUGIN, self.conf.host) topics.TAAS_PLUGIN, self.conf.host)
endpoints = [self] endpoints = [self]
@ -123,22 +146,31 @@ class TaasOvsAgentRpcCallback(api.TaasAgentRpcCallbackMixin):
conn.consume_in_threads() conn.consume_in_threads()
def periodic_tasks(self): def periodic_tasks(self):
# return self._invoke_driver_for_plugin_api(
# Regenerate the flow in br-tun's TAAS_SEND_FLOOD table context=None,
# to ensure all existing tunnel ports are included. args=None,
# func_name='periodic_tasks')
self.taas_driver.update_tunnel_flood_flow()
def get_driver_type(self):
return self.driver_type
class TaasOvsAgentService(service.Service): class TaasAgentService(service.Service):
def __init__(self, driver): def __init__(self, driver):
super(TaasOvsAgentService, self).__init__() super(TaasAgentService, self).__init__()
self.driver = driver self.driver = driver
def start(self): def start(self, taas_plugin_rpc, host):
super(TaasOvsAgentService, self).start() super(TaasAgentService, self).start()
if self.driver.get_driver_type() == \
taas_ovs_consts.EXTENSION_DRIVER_TYPE:
self.tg.add_timer( self.tg.add_timer(
int(cfg.CONF.taas_agent_periodic_interval), int(cfg.CONF.taas_agent_periodic_interval),
self.driver.periodic_tasks, self.driver.periodic_tasks,
None None
) )
# Indicate the TaaS plugin to recreate the taas resources
rpc_msg = {'host_id': host}
taas_plugin_rpc.sync_tap_resources(rpc_msg, host)

View File

@ -18,14 +18,13 @@ import six
from neutron_lib.agent import l2_extension from neutron_lib.agent import l2_extension
from neutron_taas.services.taas.agents.ovs import taas_ovs_agent from neutron_taas.services.taas.agents.common import taas_agent
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
OPTS = [ OPTS = [
cfg.IntOpt( cfg.IntOpt(
'taas_agent_periodic_interval', 'taas_agent_periodic_interval',
@ -71,7 +70,7 @@ class TaasAgentExtension(l2_extension.L2AgentExtension):
def initialize(self, connection, driver_type): def initialize(self, connection, driver_type):
"""Initialize agent extension.""" """Initialize agent extension."""
self.taas_agent = taas_ovs_agent.TaasOvsAgentRpcCallback( self.taas_agent = taas_agent.TaasAgentRpcCallback(
cfg.CONF, driver_type) cfg.CONF, driver_type)
self.taas_agent.consume_api(self.agent_api) self.taas_agent.consume_api(self.agent_api)
self.taas_agent.initialize() self.taas_agent.initialize()

View File

@ -26,3 +26,6 @@ TAAS_DST_CHECK = 36
TAAS_SRC_CHECK = 37 TAAS_SRC_CHECK = 37
TAAS_DST_RESPOND = 38 TAAS_DST_RESPOND = 38
TAAS_SRC_RESPOND = 39 TAAS_SRC_RESPOND = 39
# OVS TaaS extension driver type
EXTENSION_DRIVER_TYPE = 'ovs'

View File

@ -56,6 +56,13 @@ class OvsTaasDriver(taas_base.TaasAgentDriver):
# Setup key-value manager for ingress BCMC flows # Setup key-value manager for ingress BCMC flows
self.bcmc_kvm = taas_ovs_utils.key_value_mgr(4096) self.bcmc_kvm = taas_ovs_utils.key_value_mgr(4096)
def periodic_tasks(self, args=None):
#
# Regenerate the flow in br-tun's TAAS_SEND_FLOOD table
# to ensure all existing tunnel ports are included.
#
self.update_tunnel_flood_flow()
def setup_ovs_bridges(self): def setup_ovs_bridges(self):
# #
# br-int : Integration Bridge # br-int : Integration Bridge

View File

@ -0,0 +1,32 @@
# Copyright (C) 2018 AT&T
#
# 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_lib import exceptions as qexception
# TaaS SR-IOV driver exception handling classes
class SriovNicSwitchDriverInvocationError(qexception.Invalid):
message = _("Failed to invoke SR-IOV TaaS driver command: "
"%(tap_service_pf_device)s, %(tap_service_vf_index)s, "
"%(source_vf_index)s, %(vlan_filter)s, "
"%(vf_to_vf_all_vlans)s, %(direction)s")
class PciDeviceNotFoundById(qexception.NotFound):
message = _("PCI device %(id)s not found")
class PciSlotNotFound(qexception.NotFound):
message = _("PCI slot (Port-id, MAC): %(port_id)s, %(mac)s not found")

View File

@ -0,0 +1,364 @@
# Copyright (C) 2018 AT&T
#
# 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.conf.agent import common
from neutron_taas.common import constants as taas_consts
from neutron_taas.common import utils as common_utils
from neutron_taas.services.taas.agents.extensions import taas as taas_base
from neutron_taas.services.taas.drivers.linux import sriov_nic_exceptions \
as taas_exc
from neutron_taas.services.taas.drivers.linux import sriov_nic_utils \
as sriov_utils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
import threading
LOG = logging.getLogger(__name__)
TaaS_DRIVER_NAME = 'Taas SRIOV NIC Switch driver'
class SriovNicTaasDriver(taas_base.TaasAgentDriver):
def __init__(self):
super(SriovNicTaasDriver, self).__init__()
LOG.debug("Initializing Taas SRIOV NIC Switch Driver")
self.agent_api = None
self.root_helper = common.get_root_helper(cfg.CONF)
self.lock = threading.Lock()
def initialize(self):
LOG.debug("Initialize routine called for Taas SRIOV NIC Switch Driver")
self.sriov_utils = sriov_utils.SriovNicUtils()
return
def consume_api(self, agent_api):
self.agent_api = agent_api
def create_tap_service(self, tap_service):
ts_port = tap_service['port']
LOG.debug("SRIOV Driver: Inside create_tap_service: "
"Port-id: %(port_id)s",
{'port_id': ts_port['id']})
port_params = self.sriov_utils.get_sriov_port_params(ts_port)
LOG.info("TaaS SRIOV: create_tap_service RPC invoked for "
"port %(id)s, MAC %(ts_port_mac)s, PCI %(ts_pci_slot)s, "
"VF-Index %(vf_index)s, PF-Device %(pf_device)s, "
"src_vlans %(src_vlans)s; ",
{'id': ts_port['id'],
'ts_port_mac': port_params['mac'],
'ts_pci_slot': port_params['pci_slot'],
'vf_index': port_params['vf_index'],
'pf_device': port_params['pf_device'],
'src_vlans': port_params['src_vlans']})
return
def delete_tap_service(self, tap_service):
ts_port = tap_service['port']
LOG.debug("SRIOV Driver: Inside delete_tap_service: "
"Port-id: %(port_id)s",
{'port_id': ts_port['id']})
port_params = self.sriov_utils.get_sriov_port_params(ts_port)
LOG.info("TaaS SRIOV: delete_tap_service RPC invoked for "
"port %(id)s, MAC %(ts_port_mac)s, PCI %(ts_pci_slot)s, "
"VF-Index %(vf_index)s, PF-Device %(pf_device)s, "
"src_vlans %(src_vlans)s; ",
{'id': ts_port['id'],
'ts_port_mac': port_params['mac'],
'ts_pci_slot': port_params['pci_slot'],
'vf_index': port_params['vf_index'],
'pf_device': port_params['pf_device'],
'src_vlans': port_params['src_vlans']})
return
def create_tap_flow(self, tap_flow):
source_port = tap_flow['port']
ts_port = tap_flow['tap_service_port']
direction = tap_flow['tap_flow']['direction']
vlan_filter = tap_flow['tap_flow']['vlan_filter']
vf_to_vf_all_vlans = False
LOG.debug("SRIOV Driver: Inside create_tap_flow: "
"SRC Port-id: %(src_port_id)s, "
"DEST Port-id: %(dest_port_id)s "
"Direction: %(direction)s",
{'src_port_id': source_port['id'],
'dest_port_id': ts_port['id'],
'direction': direction})
src_port_params = self.sriov_utils.get_sriov_port_params(source_port)
ts_port_params = self.sriov_utils.get_sriov_port_params(ts_port)
LOG.info("TaaS src_port_params "
"port %(id)s, MAC %(port_mac)s, PCI %(pci_slot)s, "
"VF-Index %(vf_index)s, PF-Device %(pf_device)s, "
"src_vlans %(src_vlans)s; ",
{'id': source_port['id'],
'port_mac': src_port_params['mac'],
'pci_slot': src_port_params['pci_slot'],
'vf_index': src_port_params['vf_index'],
'pf_device': src_port_params['pf_device'],
'src_vlans': src_port_params['src_vlans']})
LOG.info("TaaS ts_port_params "
"port %(id)s, MAC %(port_mac)s, PCI %(pci_slot)s, "
"VF-Index %(vf_index)s, PF-Device %(pf_device)s, "
"VLAN-Filter %(vlan_filter)s, src_vlans %(src_vlans)s; ",
{'id': ts_port['id'],
'port_mac': ts_port_params['mac'],
'pci_slot': ts_port_params['pci_slot'],
'vf_index': ts_port_params['vf_index'],
'pf_device': ts_port_params['pf_device'],
'vlan_filter': vlan_filter,
'src_vlans': ts_port_params['src_vlans']})
# If no VLAN filter configured on source port, then include all vlans
if not src_port_params['src_vlans'] or \
src_port_params['src_vlans'] == '0':
src_port_params['src_vlans'] = taas_consts.VLAN_RANGE
LOG.debug("TaaS no src_vlans in src_port")
# If no VLAN filter configured on probe port, then include all vlans
if not vlan_filter:
vlan_filter = taas_consts.VLAN_RANGE
vf_to_vf_all_vlans = True
LOG.debug("VF to VF mirroring for all VLANs. "
"Direction %(direction)s",
{'direction': direction})
if not src_port_params['pci_slot']:
LOG.error("No PCI Slot for source_port %(id)s with MAC %(mac)s; ",
{'id': source_port['id'],
'mac': src_port_params['mac'],
'source_vlans': src_port_params['src_vlans']})
raise taas_exc.PciSlotNotFound(port_id=source_port['id'],
mac=src_port_params['mac'])
if not ts_port_params['pci_slot']:
LOG.error("No PCI Slot for ts_port %(id)s with MAC %(mac)s; ",
{'id': ts_port['id'], 'mac': ts_port_params['mac'],
'vlan_filter': vlan_filter})
raise taas_exc.PciSlotNotFound(port_id=ts_port['id'],
mac=ts_port_params['mac'])
if src_port_params['pf_device'] != ts_port_params['pf_device']:
LOG.error("SRIOV NIC Driver only supports mirroring b/w "
"VF_src %(VF_src)s -> VF_probe %(VF_probe)s on same PF. "
"PF_src %(PF_src)s and PF_probe %(PF_probe)s "
"are different; ",
{'VF_src': src_port_params['vf_index'],
'VF_probe': ts_port_params['vf_index'],
'PF_src': src_port_params['pf_device'],
'PF_probe': ts_port_params['pf_device']})
return
# Fetch common VLAN tags
src_vlans_list = sorted(set(common_utils.get_list_from_ranges_str(
src_port_params['src_vlans'])))
vlan_filter_list = sorted(set(
common_utils.get_list_from_ranges_str(vlan_filter)))
common_vlans_list = list(set(src_vlans_list).intersection(
vlan_filter_list))
common_vlans_rng_str = common_utils.get_ranges_str_from_list(
common_vlans_list)
LOG.info("TaaS src_vlans_list %(src_vlans_list)s, "
"vlan_filter_list %(vlan_filter_list)s, "
"common_vlans_list %(common_vlans_list)s, "
"common_vlans_rng_str %(common_vlans_rng_str)s; ",
{'src_vlans_list': src_vlans_list,
'vlan_filter_list': vlan_filter_list,
'common_vlans_list': common_vlans_list,
'common_vlans_rng_str': common_vlans_rng_str})
if ts_port_params['pf_device'] and \
ts_port_params['vf_index'] and \
src_port_params['vf_index']:
with self.lock:
try:
LOG.info("TaaS invoking execute_sysfs_command")
self.sriov_utils.execute_sysfs_command(
'add',
ts_port_params,
src_port_params,
common_vlans_rng_str,
vf_to_vf_all_vlans,
direction)
except Exception:
LOG.error("TaaS error in invoking execute_sysfs_command")
with excutils.save_and_reraise_exception():
raise taas_exc.SriovNicSwitchDriverInvocationError(
ts_pf_dev=ts_port_params['pf_device'],
ts_vf_index=ts_port_params['vf_index'],
source_vf_index=src_port_params['vf_index'],
common_vlans_rng_str=common_vlans_rng_str,
vf_to_vf_all_vlans=vf_to_vf_all_vlans,
direction=direction)
return
def delete_tap_flow(self, tap_flow):
source_port = tap_flow['port']
ts_port = tap_flow['tap_service_port']
vlan_filter = tap_flow['tap_flow']['vlan_filter']
direction = tap_flow['tap_flow']['direction']
LOG.debug("SRIOV Driver: Inside delete_tap_flow: "
"SRC Port-id: %(src_port_id)s, "
"DEST Port-id: %(dest_port_id)s "
"Direction: %(direction)s",
{'src_port_id': source_port['id'],
'dest_port_id': ts_port['id'],
'direction': direction})
src_port_params = self.sriov_utils.get_sriov_port_params(source_port)
ts_port_params = self.sriov_utils.get_sriov_port_params(ts_port)
LOG.info("TaaS src_port_params "
"port %(id)s, MAC %(port_mac)s, PCI %(pci_slot)s, "
"VF-Index %(vf_index)s, PF-Device %(pf_device)s, "
"src_vlans %(src_vlans)s; ",
{'id': source_port['id'],
'port_mac': src_port_params['mac'],
'pci_slot': src_port_params['pci_slot'],
'vf_index': src_port_params['vf_index'],
'pf_device': src_port_params['pf_device'],
'src_vlans': src_port_params['src_vlans']})
LOG.info("TaaS ts_port_params "
"port %(id)s, MAC %(port_mac)s, PCI %(pci_slot)s, "
"VF-Index %(vf_index)s, PF-Device %(pf_device)s, "
"VLAN-Filter %(vlan_filter)s, src_vlans %(src_vlans)s; ",
{'id': ts_port['id'],
'port_mac': ts_port_params['mac'],
'pci_slot': ts_port_params['pci_slot'],
'vf_index': ts_port_params['vf_index'],
'pf_device': ts_port_params['pf_device'],
'vlan_filter': vlan_filter,
'src_vlans': ts_port_params['src_vlans']})
# If no VLAN filter configured on source port, then include all vlans
if not src_port_params['src_vlans']:
src_port_params['src_vlans'] = taas_consts.VLAN_RANGE
LOG.debug("TaaS no src_vlans in src_port")
# If no VLAN filter configured on probe port, then include all vlans
if not vlan_filter:
vf_to_vf_all_vlans = True
LOG.debug("VF to VF mirroring for all VLANs. "
"Direction %(direction)s",
{'direction': direction})
if not src_port_params['pci_slot']:
LOG.error("No PCI Slot for source_port %(id)s with MAC %(mac)s; ",
{'id': source_port['id'],
'mac': src_port_params['mac'],
'source_vlans': src_port_params['src_vlans']})
raise taas_exc.PciSlotNotFound(port_id=source_port['id'],
mac=src_port_params['mac'])
if not ts_port_params['pci_slot']:
LOG.error("No PCI Slot for ts_port %(id)s with MAC %(mac)s; ",
{'id': ts_port['id'], 'mac': ts_port_params['mac']})
raise taas_exc.PciSlotNotFound(port_id=ts_port['id'],
mac=ts_port_params['mac'])
# Fetch common VLAN tags
src_vlans_list = []
for src_vlans_str in tap_flow['source_vlans_list']:
src_vlans_list.extend(common_utils.get_list_from_ranges_str(
src_vlans_str))
src_vlans_list = sorted(set(src_vlans_list))
vlan_filter_list = []
for vlan_filter_str in tap_flow['vlan_filter_list']:
vlan_filter_list.extend(common_utils.get_list_from_ranges_str(
vlan_filter_str))
vlan_filter_list = sorted(set(vlan_filter_list))
common_vlans_list = \
list(set(src_vlans_list).intersection(vlan_filter_list))
common_vlans_rng_str = \
common_utils.get_ranges_str_from_list(common_vlans_list)
LOG.info("TaaS src_vlans_list %(src_vlans_list)s, "
"vlan_filter_list %(vlan_filter_list)s, "
"common_vlans_list %(common_vlans_list)s, "
"common_vlans_rng_str %(common_vlans_rng_str)s; ",
{'src_vlans_list': src_vlans_list,
'vlan_filter_list': vlan_filter_list,
'common_vlans_list': common_vlans_list,
'common_vlans_rng_str': common_vlans_rng_str})
if ts_port_params['pf_device'] and \
ts_port_params['vf_index'] and \
src_port_params['vf_index']:
with self.lock:
try:
LOG.info("TaaS invoking execute_sysfs_command")
self.sriov_utils.execute_sysfs_command('rem',
ts_port_params,
src_port_params,
taas_consts.
VLAN_RANGE,
False,
'BOTH')
except Exception:
LOG.error("TaaS error in invoking execute_sysfs_command")
with excutils.save_and_reraise_exception():
raise taas_exc.SriovNicSwitchDriverInvocationError(
ts_pf_dev=ts_port_params['pf_device'],
ts_vf_index=ts_port_params['vf_index'],
source_vf_index=src_port_params['vf_index'],
common_vlans_rng_str=taas_consts.VLAN_RANGE,
vf_to_vf_all_vlans=vf_to_vf_all_vlans,
direction=direction)
if common_vlans_rng_str:
try:
LOG.info("TaaS invoking execute_sysfs_command")
self.sriov_utils.execute_sysfs_command(
'add',
ts_port_params,
src_port_params,
common_vlans_rng_str,
False,
'BOTH')
except Exception:
LOG.error("TaaS error in invoking "
"execute_sysfs_command")
with excutils.save_and_reraise_exception():
raise taas_exc.SriovNicSwitchDriverInvocationError(
ts_pf_dev=ts_port_params['pf_device'],
ts_vf_index=ts_port_params['vf_index'],
source_vf_index=src_port_params['vf_index'],
common_vlans_rng_str=common_vlans_rng_str,
vf_to_vf_all_vlans=vf_to_vf_all_vlans,
direction=direction)
return

View File

@ -0,0 +1,268 @@
# Copyright (C) 2018 AT&T
#
# 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.
#
# This class implements a utility functions for SRIOV NIC Switch Driver
#
import os
from oslo_log import log as logging
from neutron.agent.linux import utils
from neutron_lib.api.definitions import portbindings
from neutron_taas.services.taas.drivers.linux import sriov_nic_exceptions \
as taas_exc
import glob
import re
LOG = logging.getLogger(__name__)
class SriovNicUtils(object):
#
# Initializes internal state for specified # keys
#
def __init__(self):
LOG.debug("SriovNicUtils: init called")
return
#
# Returns specified key-value affilation, if it exists.
#
def execute_sysfs_command(self, command, ts_port_params,
src_port_params,
common_vlans_ranges_str,
vf_to_vf_all_vlans, direction):
"""Execute the SRIOV NIC Switch Driver's SysFs command.
# Mirror traffic from VF0 to VF3 on interface p2p1, ex.
echo add 3 > /sys/class/net/p2p1/device/sriov/0/ingress_mirror
echo add 3 > /sys/class/net/p2p1/device/sriov/0/egress_mirror
# Remove traffic mirroring from VF0 to VF3 on interface p2p1, ex.
echo rem 3 > /sys/class/net/p2p1/device/sriov/0/ingress_mirror
echo rem 3 > /sys/class/net/p2p1/device/sriov/0/egress_mirror
# Add VLANs 2,6,18-22 to Mirror traffic to VF3 (port p2p1), ex.
echo add 2,6,18-22 > /sys/class/net/p2p1/device/sriov/3/vlan_mirror
# Remove VLANs 2,6,18-22 to Mirror traffic to VF3 (port p2p1), ex.
echo rem 2,6,18-22 > /sys/class/net/p2p1/device/sriov/3/vlan_mirror
# Remove all VLANs from mirroring at VF3, ex.
echo rem 0-4095 > /sys/class/net/p1p1/device/sriov/3/vlan_mirror
"""
LOG.debug("TaaS sysfs command params %(command)s, "
"ts_port_params %(ts_port_params)s, "
"src_port_params %(src_port_params)s, "
"common_vlans_ranges_str %(common_vlans_ranges_str)s; "
"vf_to_vf_all_vlans %(vf_to_vf_all_vlans)s; "
"direction %(direction)s; ",
{'command': command,
'ts_port_params': ts_port_params,
'src_port_params': src_port_params,
'common_vlans_ranges_str': common_vlans_ranges_str,
'vf_to_vf_all_vlans': vf_to_vf_all_vlans,
'direction': direction})
if vf_to_vf_all_vlans:
if direction in ['OUT', 'BOTH']:
commit_cmd = ['i40e_sysfs_command',
ts_port_params['pf_device'],
src_port_params['vf_index'],
'egress_mirror',
command,
ts_port_params['vf_index']]
try:
LOG.info("TaaS executing sysfs_command %(command)s",
{'command': commit_cmd})
utils.execute(commit_cmd, run_as_root=True)
except (OSError, RuntimeError, IndexError, ValueError) as e:
LOG.error("Exception while executing Sysfs command "
"Exception: %s", e)
return
if direction in ['IN', 'BOTH']:
commit_cmd = ['i40e_sysfs_command',
ts_port_params['pf_device'],
src_port_params['vf_index'],
'ingress_mirror',
command,
ts_port_params['vf_index']]
try:
LOG.info("TaaS executing sysfs_command %(command)s",
{'command': commit_cmd})
utils.execute(commit_cmd, run_as_root=True)
except (OSError, RuntimeError, IndexError, ValueError) as e:
LOG.error("Exception while executing Sysfs command "
"Exception: %s", e)
return
else:
if direction != 'BOTH':
LOG.warning("SRIOV NIC Switch driver only supports"
"direction=BOTH for specific VLANs' mirroring")
commit_cmd = ['i40e_sysfs_command',
ts_port_params['pf_device'],
ts_port_params['vf_index'],
'vlan_mirror',
command,
common_vlans_ranges_str]
try:
LOG.info("TaaS executing sysfs_command %(command)s",
{'command': commit_cmd})
utils.execute(commit_cmd, run_as_root=True)
except (OSError, RuntimeError, IndexError, ValueError) as e:
LOG.error("Exception while executing Sysfs command "
"Exception: %s", e)
return
def _get_sysfs_netdev_path(self, pci_addr, pf_interface):
"""Get the sysfs path based on the PCI address of the device.
Assumes a networking device - will not check for the existence
of the path.
"""
if pf_interface:
return "/sys/bus/pci/devices/%s/physfn/net" % pci_addr
return "/sys/bus/pci/devices/%s/net" % pci_addr
def get_ifname_by_pci_address(self, pci_addr, pf_interface=False):
"""Get the interface name based on a VF's pci address.
The returned interface name is either the parent PF's or that of
the VF itself based on the argument of pf_interface.
"""
dev_path = self._get_sysfs_netdev_path(pci_addr, pf_interface)
try:
dev_info = os.listdir(dev_path)
return dev_info.pop()
except Exception:
raise taas_exc.PciDeviceNotFoundById(id=pci_addr)
def get_mac_by_pci_address(self, pci_addr, pf_interface=False):
"""Get the MAC address of the nic based on its PCI address.
Raises PciDeviceNotFoundById in case the pci device is not a NIC
"""
dev_path = self._get_sysfs_netdev_path(pci_addr, pf_interface)
if_name = self.get_ifname_by_pci_address(pci_addr, pf_interface)
addr_file = os.path.join(dev_path, if_name, 'address')
try:
with open(addr_file) as f:
mac = next(f).strip()
return mac
except (IOError, StopIteration) as e:
LOG.warning("Could not find the expected sysfs file for "
"determining the MAC address of the PCI device "
"%(addr)s. May not be a NIC. Error: %(e)s",
{'addr': pci_addr, 'e': e})
raise taas_exc.PciDeviceNotFoundById(id=pci_addr)
def get_vf_num_by_pci_address(self, pci_addr):
"""Get the VF number based on a VF's pci address
A VF is associated with an VF number, which ip link command uses to
configure it. This can be obtained from the PCI device filesystem.
"""
VIRTFN_RE = re.compile("virtfn(\d+)")
virtfns_path = "/sys/bus/pci/devices/%s/physfn/virtfn*" % (pci_addr)
vf_num = None
LOG.debug("TaaS: pci_addr: %(pci_addr)s "
"virtfns_path: %(virtfns_path)s",
{'pci_addr': pci_addr,
'virtfns_path': virtfns_path})
try:
for vf_path in glob.iglob(virtfns_path):
if re.search(pci_addr, os.readlink(vf_path)):
t = VIRTFN_RE.search(vf_path)
vf_num = t.group(1)
break
except Exception:
pass
if vf_num is None:
LOG.warning("TaaS: No net device was found for pci: %(pci_addr)s "
"virtfns_path: %(virtfns_path)s",
{'pci_addr': pci_addr,
'virtfns_path': virtfns_path})
raise taas_exc.PciDeviceNotFoundById(id=pci_addr)
return vf_num
def get_net_name_by_vf_pci_address(self, vfaddress, pf_interface=False):
"""Given the VF PCI address, returns the net device name.
Every VF is associated to a PCI network device. This function
returns the libvirt name given to this network device; e.g.:
<device>
<name>net_enp8s0f0_90_e2_ba_5e_a6_40</name>
...
In the libvirt parser information tree, the network device stores the
network capabilities associated to this device.
"""
LOG.debug("TaaS: vfaddr: %(vfaddr)s ",
{'vfaddr': vfaddress})
try:
mac = self.get_mac_by_pci_address(vfaddress,
pf_interface).split(':')
ifname = self.get_ifname_by_pci_address(vfaddress, pf_interface)
LOG.debug("TaaS: mac: %(mac)s, ifname: %(ifname)s",
{'mac': mac, 'ifname': ifname})
return ("net_%(ifname)s_%(mac)s" %
{'ifname': ifname, 'mac': '_'.join(mac)})
except Exception:
LOG.warning("No net device was found for VF %(vfaddress)s",
{'vfaddress': vfaddress})
return
def get_sriov_port_params(self, sriov_port):
"""Returns a dict of common SRIOV parameters for a given SRIOV port
"""
LOG.debug("TaaS: sriov_port %(id)s; ",
{'id': sriov_port['id']})
port_mac = sriov_port['mac_address']
pci_slot = None
src_vlans = None
if sriov_port.get(portbindings.PROFILE):
pci_slot = sriov_port[portbindings.PROFILE].get('pci_slot')
if sriov_port.get(portbindings.VIF_DETAILS):
src_vlans = sriov_port[portbindings.VIF_DETAILS].get('vlan')
LOG.debug("TaaS: pci_slot %(pci_slot)s; "
"src_vlans %(src_vlans)s; ",
{'pci_slot': pci_slot,
'src_vlans': src_vlans})
if not pci_slot:
LOG.error("No PCI Slot for sriov_port %(id)s with MAC %(mac)s; ",
{'id': sriov_port['id'], 'mac': port_mac})
return
vf_index = self.get_vf_num_by_pci_address(pci_slot)
pf_device = self.get_ifname_by_pci_address(pci_slot, True)
return {'mac': port_mac, 'pci_slot': pci_slot,
'vf_index': vf_index, 'pf_device': pf_device,
'src_vlans': src_vlans}

View File

@ -21,15 +21,6 @@ import oslo_messaging as messaging
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class TaasCallbacks(object):
"""Currently there are no callbacks to the Taas Plugin."""
def __init__(self, plugin):
super(TaasCallbacks, self).__init__()
self.plugin = plugin
return
class TaasAgentApi(object): class TaasAgentApi(object):
"""RPC calls to agent APIs""" """RPC calls to agent APIs"""

View File

@ -1,3 +1,4 @@
# Copyright (C) 2018 AT&T
# Copyright (C) 2016 Midokura SARL. # Copyright (C) 2016 Midokura SARL.
# Copyright (C) 2015 Ericsson AB # Copyright (C) 2015 Ericsson AB
# Copyright (c) 2015 Gigamon # Copyright (c) 2015 Gigamon
@ -14,25 +15,89 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants
from neutron_lib import exceptions as n_exc from neutron_lib import exceptions as n_exc
from neutron_lib import rpc as n_rpc from neutron_lib import rpc as n_rpc
from neutron_taas.common import constants as taas_consts
from neutron_taas.common import topics from neutron_taas.common import topics
from neutron_taas.services.taas import service_drivers from neutron_taas.services.taas import service_drivers
from neutron_taas.services.taas.service_drivers import (service_driver_context
as sd_context)
from neutron_taas.services.taas.service_drivers import taas_agent_api from neutron_taas.services.taas.service_drivers import taas_agent_api
from neutron_taas.services.taas.taas_plugin import TaasPlugin
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import excutils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class TaasCallbacks(object):
def __init__(self, rpc_driver, plugin):
super(TaasCallbacks, self).__init__()
self.rpc_driver = rpc_driver
self.plugin = plugin
return
def sync_tap_resources(self, context, sync_tap_res, host):
"""Handle Rpc from Agent to sync up Tap resources."""
LOG.debug("In RPC Call for Sync Tap Resources: MSG=%s" % sync_tap_res)
# Get list of configured tap-services
active_tss = self.plugin.get_tap_services(
context,
filters={'status': [constants.ACTIVE]})
for ts in active_tss:
# If tap-service port is bound to a different host than the one
# which sent this RPC, then continue.
ts_port = self.plugin._get_port_details(
context, ts['port_id'])
if ts_port['binding:host_id'] != host:
continue
driver_context = sd_context.TapServiceContext(self.plugin,
context, ts)
try:
self.rpc_driver.create_tap_service_postcommit(driver_context)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error("Failed to create tap service on driver,"
"deleting tap_service %s", ts['id'])
super(TaasPlugin, self.plugin).delete_tap_service(
context, ts['id'])
# Get all the active tap flows for current tap-service
active_tfs = self.plugin.get_tap_flows(
context,
filters={'tap_service_id': [ts['id']],
'status': [constants.ACTIVE]})
# Filter out the tap flows associated with distinct tap services
for tf in active_tfs:
driver_context = sd_context.TapFlowContext(self.plugin,
context, tf)
try:
self.rpc_driver.create_tap_flow_postcommit(driver_context)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error("Failed to create tap flow on driver,"
"deleting tap_flow %s", tf['id'])
super(TaasPlugin, self.plugin).delete_tap_flow(
context, tf['id'])
class TaasRpcDriver(service_drivers.TaasBaseDriver): class TaasRpcDriver(service_drivers.TaasBaseDriver):
"""Taas Rpc Service Driver class""" """Taas Rpc Service Driver class"""
def __init__(self, service_plugin): def __init__(self, service_plugin):
LOG.debug("Loading TaasRpcDriver.") LOG.debug("Loading TaasRpcDriver.")
super(TaasRpcDriver, self).__init__(service_plugin) super(TaasRpcDriver, self).__init__(service_plugin)
self.endpoints = [taas_agent_api.TaasCallbacks(service_plugin)] self.endpoints = [TaasCallbacks(self, service_plugin)]
self.conn = n_rpc.Connection() self.conn = n_rpc.Connection()
self.conn.create_consumer(topics.TAAS_PLUGIN, self.conn.create_consumer(topics.TAAS_PLUGIN,
self.endpoints, fanout=False) self.endpoints, fanout=False)
@ -125,12 +190,19 @@ class TaasRpcDriver(service_drivers.TaasBaseDriver):
tf['source_port']) tf['source_port'])
host = port['binding:host_id'] host = port['binding:host_id']
port_mac = port['mac_address'] port_mac = port['mac_address']
# Extract the tap-service port
ts = self.service_plugin.get_tap_service(context._plugin_context,
tf['tap_service_id'])
ts_port = self.service_plugin._get_port_details(
context._plugin_context, ts['port_id'])
# Send RPC message to both the source port host and # Send RPC message to both the source port host and
# tap service(destination) port host # tap service(destination) port host
rpc_msg = {'tap_flow': tf, rpc_msg = {'tap_flow': tf,
'port_mac': port_mac, 'port_mac': port_mac,
'taas_id': taas_id, 'taas_id': taas_id,
'port': port} 'port': port,
'tap_service_port': ts_port}
self.agent_rpc.create_tap_flow(context._plugin_context, rpc_msg, host) self.agent_rpc.create_tap_flow(context._plugin_context, rpc_msg, host)
return return
@ -147,12 +219,59 @@ class TaasRpcDriver(service_drivers.TaasBaseDriver):
tf['source_port']) tf['source_port'])
host = port['binding:host_id'] host = port['binding:host_id']
port_mac = port['mac_address'] port_mac = port['mac_address']
# Extract the tap-service port
ts = self.service_plugin.get_tap_service(context._plugin_context,
tf['tap_service_id'])
ts_port = self.service_plugin._get_port_details(
context._plugin_context, ts['port_id'])
src_vlans_list = []
vlan_filter_list = []
if port.get(portbindings.VNIC_TYPE) == portbindings.VNIC_DIRECT:
# Get all the tap Flows that are associated with the Tap service
active_tfs = self.service_plugin.get_tap_flows(
context._plugin_context,
filters={'tap_service_id': [tf['tap_service_id']],
'status': [constants.ACTIVE]},
fields=['source_port', 'vlan_filter'])
for tap_flow in active_tfs:
source_port = self.service_plugin._get_port_details(
context._plugin_context, tap_flow['source_port'])
LOG.debug("taas: active TF's source_port %(source_port)s",
{'source_port': source_port})
src_vlans = ""
if source_port.get(portbindings.VIF_DETAILS):
src_vlans = source_port[portbindings.VIF_DETAILS].get(
portbindings.VIF_DETAILS_VLAN)
# If no VLAN filter configured on source port,
# then include all vlans
if not src_vlans or src_vlans == '0':
src_vlans = taas_consts.VLAN_RANGE
src_vlans_list.append(src_vlans)
vlan_filter = tap_flow['vlan_filter']
# If no VLAN filter configured for tap-flow,
# then include all vlans
if not vlan_filter:
vlan_filter = taas_consts.VLAN_RANGE
vlan_filter_list.append(vlan_filter)
# Send RPC message to both the source port host and # Send RPC message to both the source port host and
# tap service(destination) port host # tap service(destination) port host
rpc_msg = {'tap_flow': tf, rpc_msg = {'tap_flow': tf,
'port_mac': port_mac, 'port_mac': port_mac,
'taas_id': taas_id, 'taas_id': taas_id,
'port': port} 'port': port,
'tap_service_port': ts_port,
'source_vlans_list': src_vlans_list,
'vlan_filter_list': vlan_filter_list}
self.agent_rpc.delete_tap_flow(context._plugin_context, rpc_msg, host) self.agent_rpc.delete_tap_flow(context._plugin_context, rpc_msg, host)
return return

View File

@ -22,7 +22,7 @@ from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources from neutron_lib.callbacks import resources
from neutron_lib import exceptions as n_exc from neutron_lib import exceptions as n_exc
from neutron_taas.common import constants from neutron_taas.common import constants as taas_consts
from neutron_taas.db import taas_db from neutron_taas.db import taas_db
from neutron_taas.extensions import taas as taas_ex from neutron_taas.extensions import taas as taas_ex
from neutron_taas.services.taas.service_drivers import (service_driver_context from neutron_taas.services.taas.service_drivers import (service_driver_context
@ -43,14 +43,16 @@ def add_provider_configuration(type_manager, service_type):
@registry.has_registry_receivers @registry.has_registry_receivers
class TaasPlugin(taas_db.Taas_db_Mixin): class TaasPlugin(taas_db.Taas_db_Mixin):
supported_extension_aliases = ["taas"] supported_extension_aliases = ["taas",
"taas-vlan-filter"]
path_prefix = "/taas" path_prefix = "/taas"
def __init__(self): def __init__(self):
LOG.debug("TAAS PLUGIN INITIALIZED") LOG.debug("TAAS PLUGIN INITIALIZED")
self.service_type_manager = st_db.ServiceTypeManager.get_instance() self.service_type_manager = st_db.ServiceTypeManager.get_instance()
add_provider_configuration(self.service_type_manager, constants.TAAS) add_provider_configuration(self.service_type_manager,
taas_consts.TAAS)
self._load_drivers() self._load_drivers()
self.driver = self._get_driver_for_provider(self.default_provider) self.driver = self._get_driver_for_provider(self.default_provider)
@ -59,7 +61,7 @@ class TaasPlugin(taas_db.Taas_db_Mixin):
def _load_drivers(self): def _load_drivers(self):
"""Loads plugin-drivers specified in configuration.""" """Loads plugin-drivers specified in configuration."""
self.drivers, self.default_provider = service_base.load_drivers( self.drivers, self.default_provider = service_base.load_drivers(
'TAAS', self) taas_consts.TAAS, self)
def _get_driver_for_provider(self, provider): def _get_driver_for_provider(self, provider):
if provider in self.drivers: if provider in self.drivers:

View File

@ -1,3 +1,4 @@
# Copyright (C) 2018 AT&T
# Copyright 2015 NEC Corporation # Copyright 2015 NEC Corporation
# All Rights Reserved # All Rights Reserved
# #
@ -47,7 +48,8 @@ class ListTapFlow(extension.ClientExtensionList, TapFlow):
"""List tap flows.""" """List tap flows."""
shell_command = 'tap-flow-list' shell_command = 'tap-flow-list'
list_columns = ['id', 'name', 'source_port', 'tap_service_id', 'status'] list_columns = ['id', 'name', 'source_port', 'tap_service_id', 'status',
'vlan_filter']
pagination_support = True pagination_support = True
sorting_support = True sorting_support = True
@ -77,6 +79,11 @@ class CreateTapFlow(extension.ClientExtensionCreate, TapFlow):
choices=['IN', 'OUT', 'BOTH'], choices=['IN', 'OUT', 'BOTH'],
type=utils.convert_to_uppercase, type=utils.convert_to_uppercase,
help=_('Direction of the Tap flow.')) help=_('Direction of the Tap flow.'))
parser.add_argument(
'--vlan-filter',
required=False,
metavar="VLAN_FILTER",
help=_('VLAN Ids to be mirrored in the form of range string.'))
def args2body(self, parsed_args): def args2body(self, parsed_args):
client = self.get_client() client = self.get_client()
@ -88,7 +95,8 @@ class CreateTapFlow(extension.ClientExtensionCreate, TapFlow):
parsed_args.tap_service) parsed_args.tap_service)
body = {'source_port': source_port, body = {'source_port': source_port,
'tap_service_id': tap_service_id} 'tap_service_id': tap_service_id}
neutronv20.update_dict(parsed_args, body, ['tenant_id', 'direction']) neutronv20.update_dict(parsed_args, body, ['tenant_id', 'direction',
'vlan_filter'])
_updatable_args2body(parsed_args, body) _updatable_args2body(parsed_args, body)
return {self.resource: body} return {self.resource: body}

View File

@ -0,0 +1,18 @@
# Copyright (C) 2018 AT&T
#
# 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 oslotest import base
class TaasTestCase(base.BaseTestCase):
"""Test case base class for all unit tests."""

View File

@ -46,14 +46,16 @@ class TaaSDbTestCase(testlib_api.SqlTestCase):
"port_id": port_id}} "port_id": port_id}}
def _get_tap_flow_data(self, tap_service_id, name='tf-1', def _get_tap_flow_data(self, tap_service_id, name='tf-1',
direction='BOTH', source_port=None): direction='BOTH', source_port=None,
vlan_filter=None):
source_port = source_port or _uuid() source_port = source_port or _uuid()
return {"tap_flow": {"name": name, return {"tap_flow": {"name": name,
"tenant_id": self.tenant_id, "tenant_id": self.tenant_id,
"description": "test tap flow", "description": "test tap flow",
"tap_service_id": tap_service_id, "tap_service_id": tap_service_id,
"source_port": source_port, "source_port": source_port,
"direction": direction}} "direction": direction,
"vlan_filter": vlan_filter}}
def _get_tap_service(self, tap_service_id): def _get_tap_service(self, tap_service_id):
"""Helper method to retrieve tap service.""" """Helper method to retrieve tap service."""
@ -180,6 +182,25 @@ class TaaSDbTestCase(testlib_api.SqlTestCase):
self.assertEqual(tf_direction, tf['direction']) self.assertEqual(tf_direction, tf['direction'])
self.assertEqual(tf_source_port, tf['source_port']) self.assertEqual(tf_source_port, tf['source_port'])
def test_tap_flow_create_with_vlan_filter(self):
"""Test to create a tap flow (with vlan_filter) in the database."""
ts_data = self._get_tap_service_data()
ts = self._create_tap_service(ts_data)
tf_name = 'test-tap-flow'
tf_direction = 'IN'
tf_source_port = _uuid()
tf_vlan_filter = '9-18,27,36-45'
tf_data = self._get_tap_flow_data(tap_service_id=ts['id'],
name=tf_name,
source_port=tf_source_port,
direction=tf_direction,
vlan_filter=tf_vlan_filter)
tf = self._create_tap_flow(tf_data)
self.assertEqual(tf_name, tf['name'])
self.assertEqual(tf_direction, tf['direction'])
self.assertEqual(tf_source_port, tf['source_port'])
self.assertEqual(tf_vlan_filter, tf['vlan_filter'])
def test_tap_flow_list(self): def test_tap_flow_list(self):
"""Test to retrieve all tap flows from the database.""" """Test to retrieve all tap flows from the database."""
ts_data = self._get_tap_service_data() ts_data = self._get_tap_service_data()

View File

@ -72,6 +72,9 @@ class TaasExtensionTestCase(test_api_v2_extension.ExtensionTestCase):
def test_delete_tap_service(self): def test_delete_tap_service(self):
self._test_entity_delete('tap_service') self._test_entity_delete('tap_service')
def _get_expected_tap_flow(self, data):
return data
def test_create_tap_flow(self): def test_create_tap_flow(self):
tenant_id = _uuid() tenant_id = _uuid()
tap_flow_data = { tap_flow_data = {
@ -84,7 +87,8 @@ class TaasExtensionTestCase(test_api_v2_extension.ExtensionTestCase):
'project_id': tenant_id, 'project_id': tenant_id,
} }
data = {'tap_flow': tap_flow_data} data = {'tap_flow': tap_flow_data}
expected_ret_val = copy.copy(data['tap_flow']) expected_data = self._get_expected_tap_flow(data)
expected_ret_val = copy.copy(expected_data['tap_flow'])
expected_ret_val.update({'id': _uuid()}) expected_ret_val.update({'id': _uuid()})
instance = self.plugin.return_value instance = self.plugin.return_value
instance.create_tap_flow.return_value = expected_ret_val instance.create_tap_flow.return_value = expected_ret_val
@ -94,7 +98,7 @@ class TaasExtensionTestCase(test_api_v2_extension.ExtensionTestCase):
content_type='application/%s' % self.fmt) content_type='application/%s' % self.fmt)
instance.create_tap_flow.assert_called_with( instance.create_tap_flow.assert_called_with(
mock.ANY, mock.ANY,
tap_flow=data) tap_flow=expected_data)
self.assertEqual(exc.HTTPCreated.code, res.status_int) self.assertEqual(exc.HTTPCreated.code, res.status_int)
res = self.deserialize(res) res = self.deserialize(res)
self.assertIn('tap_flow', res) self.assertIn('tap_flow', res)

View File

@ -0,0 +1,105 @@
# Copyright (C) 2018 AT&T
# 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 copy
import mock
from neutron_taas.common import constants as taas_consts
from neutron_taas.extensions import taas as taas_ext
from neutron_taas.extensions import vlan_filter as vlan_filter_ext
from neutron_taas.tests.unit.extensions import test_taas as test_taas_ext
from oslo_utils import uuidutils
from webob import exc
from neutron.tests.unit.api.v2 import test_base as test_api_v2
import webtest
_uuid = uuidutils.generate_uuid
_get_path = test_api_v2._get_path
class VlanFilterExtensionTestCase(test_taas_ext.TaasExtensionTestCase):
def setUp(self):
super(test_taas_ext.TaasExtensionTestCase, self).setUp()
attr_map = taas_ext.RESOURCE_ATTRIBUTE_MAP
attr_map['tap_flows'].update(
vlan_filter_ext.EXTENDED_ATTRIBUTES_2_0['tap_flows'])
self.setup_extension(
'neutron_taas.extensions.taas.TaasPluginBase',
'TAAS',
taas_ext.Taas,
'taas',
plural_mappings={}
)
def _get_expected_tap_flow(self, data):
ret = super(VlanFilterExtensionTestCase,
self)._get_expected_tap_flow(data)
ret['tap_flow'].update(
vlan_filter=data['tap_flow'].get('vlan_filter', None))
return ret
def test_create_tap_flow_with_vlan_filter(self):
tenant_id = _uuid()
tap_flow_data = {
'tenant_id': tenant_id,
'name': 'MyTapFlow',
'description': 'This is my tap flow',
'direction': 'BOTH',
'tap_service_id': _uuid(),
'source_port': _uuid(),
'project_id': tenant_id,
'vlan_filter': taas_consts.VLAN_RANGE,
}
data = {'tap_flow': tap_flow_data}
expected_data = self._get_expected_tap_flow(data)
expected_ret_val = copy.copy(expected_data['tap_flow'])
expected_ret_val.update({'id': _uuid()})
instance = self.plugin.return_value
instance.create_tap_flow.return_value = expected_ret_val
res = self.api.post(
_get_path(test_taas_ext.TAP_FLOW_PATH, fmt=self.fmt),
self.serialize(data),
content_type='application/%s' % self.fmt)
instance.create_tap_flow.assert_called_with(
mock.ANY,
tap_flow=expected_data)
self.assertEqual(exc.HTTPCreated.code, res.status_int)
res = self.deserialize(res)
self.assertIn('tap_flow', res)
self.assertEqual(expected_ret_val, res['tap_flow'])
def test_create_tap_flow_invalid_vlan_filter_value(self):
tenant_id = _uuid()
tap_flow_data = {
'tenant_id': tenant_id,
'name': 'MyTapFlow',
'description': 'This is my tap flow',
'direction': 'BOTH',
'tap_service_id': _uuid(),
'source_port': _uuid(),
'project_id': tenant_id,
'vlan_filter': '10-25,',
}
data = {'tap_flow': tap_flow_data}
self.assertRaises(
webtest.app.AppError,
self.api.post,
_get_path(test_taas_ext.TAP_FLOW_PATH, fmt=self.fmt),
self.serialize(data),
content_type='application/%s' % self.fmt)

View File

@ -0,0 +1,224 @@
# Copyright (C) 2018 AT&T
#
# 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
import mock
from neutron_taas.services.taas.drivers.linux import sriov_nic_exceptions \
as taas_exc
from neutron_taas.services.taas.drivers.linux import sriov_nic_taas
from neutron_taas.tests import base
FAKE_PORT_PARAMS = {
'mac': '52:54:00:12:35:02', 'pci_slot': 3, 'vf_index': '89',
'pf_device': 'net_enp0s3_52_54_00_12_35_02', 'src_vlans': '20'}
FAKE_TAP_SERVICE = {'port': {
'id': 'fake_1', 'mac_address': "52:54:00:12:35:02",
'binding:profile': {'pci_slot': 3},
'binding:vif_details': {'vlan': '20'}}}
FAKE_TAP_FLOW = {'port': FAKE_TAP_SERVICE['port'],
'ts_port': FAKE_TAP_SERVICE['port'],
'source_vlans_list': ['4-6', '8-10', '15-18,20'],
'vlan_filter_list': '1-5,9,18,20,27-30,4000-4095',
'tap_flow': {'direction': 'IN', 'vlan_filter': '20'}}
class TestSriovNicTaas(base.TaasTestCase):
def setUp(self):
super(TestSriovNicTaas, self).setUp()
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_create_tap_service(self, mock_sriov_utils):
tap_service = FAKE_TAP_SERVICE
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
return_value = FAKE_PORT_PARAMS
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
obj.create_tap_service(tap_service)
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
assert_called_once_with(tap_service['port'])
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_create_tap_service_no_pf_device_and_vf_index(
self, mock_sriov_utils):
tap_service = FAKE_TAP_SERVICE
temp_fake_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
temp_fake_port_params['pf_device'] = None
temp_fake_port_params['vf_index'] = None
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
return_value = FAKE_PORT_PARAMS
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
self.assertIsNone(obj.create_tap_service(tap_service))
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
assert_called_once_with(tap_service['port'])
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_delete_tap_service(self, mock_sriov_utils):
tap_service = FAKE_TAP_SERVICE
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
return_value = FAKE_PORT_PARAMS
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
obj.create_tap_service(tap_service)
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
assert_called_once_with(tap_service['port'])
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_delete_tap_service_no_pf_device_and_vf_index(
self, mock_sriov_utils):
tap_service = FAKE_TAP_SERVICE
temp_fake_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
temp_fake_port_params['pf_device'] = None
temp_fake_port_params['vf_index'] = None
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
return_value = FAKE_PORT_PARAMS
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
self.assertIsNone(obj.create_tap_service(tap_service))
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
assert_called_once_with(tap_service['port'])
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_create_tap_flow(self, mock_sriov_utils):
tap_flow = {'port': FAKE_TAP_SERVICE['port'],
'tap_service_port': FAKE_TAP_SERVICE['port'],
'vlan_filter_list': '1-5,9,18,20,27-30,4000-4095',
'tap_flow': {'direction': 'IN', 'vlan_filter': '20'}}
src_port_params = ts_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
side_effect = [src_port_params, ts_port_params]
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
obj.create_tap_flow(tap_flow)
mock_sriov_utils.SriovNicUtils().execute_sysfs_command.\
assert_called_once_with('add', ts_port_params, src_port_params,
'20', False, 'IN')
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_create_tap_flow_no_vlan_filter_on_source_and_probe(
self, mock_sriov_utils):
tap_flow = {'port': FAKE_TAP_SERVICE['port'],
'tap_service_port': FAKE_TAP_SERVICE['port'],
'tap_flow': {'direction': 'IN', 'vlan_filter': '20'}}
src_port_params = ts_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
ts_port_params['vlan_filter'] = None
src_port_params['src_vlans'] = None
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
side_effect = [src_port_params, ts_port_params]
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
obj.create_tap_flow(tap_flow)
mock_sriov_utils.SriovNicUtils().execute_sysfs_command.\
assert_called_once_with('add', ts_port_params, src_port_params,
'20', False, 'IN')
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_create_tap_flow_no_source_pci_slot(
self, mock_sriov_utils):
tap_flow = {'port': FAKE_TAP_SERVICE['port'],
'tap_service_port': FAKE_TAP_SERVICE['port'],
'tap_flow': {'direction': 'IN', 'vlan_filter': 20}}
src_port_params = ts_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
src_port_params['pci_slot'] = None
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
side_effect = [src_port_params, ts_port_params]
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
self.assertRaises(
taas_exc.PciSlotNotFound, obj.create_tap_flow, tap_flow)
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_create_tap_flow_no_ts_pci_slot(
self, mock_sriov_utils):
tap_flow = {'port': FAKE_TAP_SERVICE['port'],
'tap_service_port': FAKE_TAP_SERVICE['port'],
'tap_flow': {'direction': 'IN', 'vlan_filter': 20}}
src_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
ts_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
ts_port_params['pci_slot'] = None
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
side_effect = [src_port_params, ts_port_params]
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
self.assertRaises(
taas_exc.PciSlotNotFound, obj.create_tap_flow, tap_flow)
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_create_tap_flow_different_pf_devices(
self, mock_sriov_utils):
tap_flow = {'port': FAKE_TAP_SERVICE['port'],
'tap_service_port': FAKE_TAP_SERVICE['port'],
'tap_flow': {'direction': 'IN', 'vlan_filter': 20}}
src_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
ts_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
ts_port_params['pf_device'] = 'net_enp0s3_52_54_00_12_35_02'
src_port_params['pf_device'] = 'net_enp0s8_52_54_00_12_35_01'
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
side_effect = [src_port_params, ts_port_params]
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
self.assertIsNotNone(obj.create_tap_flow, tap_flow)
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_delete_tap_flow(self, mock_sriov_utils):
tap_flow = {'port': FAKE_TAP_SERVICE['port'],
'tap_service_port': FAKE_TAP_SERVICE['port'],
'source_vlans_list': ['4-6', '8-10', '15-18,20'],
'vlan_filter_list': ['1-5,9,18,20,27-30,4000-4095'],
'tap_flow': {'direction': 'IN', 'vlan_filter': '20'}}
src_port_params = ts_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
side_effect = [src_port_params, ts_port_params]
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
self.assertIsNone(obj.delete_tap_flow(tap_flow))
self.assertEqual(2, mock_sriov_utils.SriovNicUtils().
execute_sysfs_command.call_count)
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_delete_tap_flow_no_source_pci_slot(
self, mock_sriov_utils):
tap_flow = {'port': FAKE_TAP_SERVICE['port'],
'tap_service_port': FAKE_TAP_SERVICE['port'],
'source_vlans_list': [4, 5, 9],
'tap_flow': {'direction': 'IN', 'vlan_filter': 20}}
src_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
ts_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
src_port_params['pci_slot'] = None
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
side_effect = [src_port_params, ts_port_params]
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
self.assertRaises(taas_exc.PciSlotNotFound, obj.delete_tap_flow,
tap_flow)
@mock.patch.object(sriov_nic_taas, 'sriov_utils')
def test_delete_tap_flow_no_ts_pci_slot(
self, mock_sriov_utils):
tap_flow = {'port': FAKE_TAP_SERVICE['port'],
'tap_service_port': FAKE_TAP_SERVICE['port'],
'tap_flow': {'direction': 'IN', 'vlan_filter': 20}}
src_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
ts_port_params = copy.deepcopy(FAKE_PORT_PARAMS)
ts_port_params['pci_slot'] = None
mock_sriov_utils.SriovNicUtils().get_sriov_port_params.\
side_effect = [src_port_params, ts_port_params]
obj = sriov_nic_taas.SriovNicTaasDriver()
obj.initialize()
self.assertRaises(taas_exc.PciSlotNotFound, obj.delete_tap_flow,
tap_flow)

View File

@ -0,0 +1,269 @@
# Copyright (C) 2018 AT&T
#
# 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.
#
# This class implements a utility functions for SRIOV NIC Switch Driver
#
import copy
import mock
import re
from neutron_taas.common import utils as common_utils
from neutron_taas.services.taas.drivers.linux import sriov_nic_exceptions \
as taas_exc
from neutron_taas.services.taas.drivers.linux import sriov_nic_utils
from neutron_taas.tests import base
FAKE_SRIOV_PORT = {
'id': 'fake_1', 'mac_address': "52:54:00:12:35:02",
'binding:profile': {
'pci_slot': None}, 'binding:vif_details': {'vlan': 20}
}
class TestSriovNicUtils(base.TaasTestCase):
def setUp(self):
super(TestSriovNicUtils, self).setUp()
def test_get_sysfs_netdev_path_with_pf_interface(self):
self.assertEqual(
"/sys/bus/pci/devices/12/physfn/net",
sriov_nic_utils.SriovNicUtils().
_get_sysfs_netdev_path(12, True))
def test_get_sysfs_netdev_path_without_pf_interface(self):
self.assertEqual(
"/sys/bus/pci/devices/12/net",
sriov_nic_utils.SriovNicUtils().
_get_sysfs_netdev_path(12, False))
@mock.patch.object(sriov_nic_utils, 'os')
def test_get_ifname_by_pci_address(self, mock_os):
mock_os.listdir.return_value = ['random1', 'random2']
self.assertEqual(sriov_nic_utils.SriovNicUtils().
get_ifname_by_pci_address(12, False), 'random2')
@mock.patch.object(sriov_nic_utils, 'os')
def test_get_ifname_by_pci_address_no_dev_info(self, mock_os):
mock_os.listdir.return_value = list()
self.assertRaises(
taas_exc.PciDeviceNotFoundById,
sriov_nic_utils.SriovNicUtils().get_ifname_by_pci_address, 12, 9)
@mock.patch.object(sriov_nic_utils, 'os')
@mock.patch.object(sriov_nic_utils, 'open', create=True)
def test_get_mac_by_pci_address(self, mock_open, mock_os):
mock_os.listdir.return_value = ['random1', 'random2']
mock_os.path.join.return_value = 'random'
fake_file_handle = ["52:54:00:12:35:02"]
fake_file_iter = fake_file_handle.__iter__()
mock_open.return_value.__enter__.return_value = fake_file_iter
self.assertEqual(
"52:54:00:12:35:02", sriov_nic_utils.SriovNicUtils().
get_mac_by_pci_address(12, False))
@mock.patch.object(sriov_nic_utils, 'os')
@mock.patch.object(sriov_nic_utils, 'open', create=True)
def test_get_mac_by_pci_address_no_content(self, mock_open, mock_os):
mock_os.listdir.return_value = ['random1', 'random2']
mock_os.path.join.return_value = 'random'
fake_file_handle = []
fake_file_iter = fake_file_handle.__iter__()
mock_open.return_value.__enter__.return_value = fake_file_iter
self.assertRaises(
taas_exc.PciDeviceNotFoundById,
sriov_nic_utils.SriovNicUtils().get_mac_by_pci_address, 12, False)
@mock.patch.object(sriov_nic_utils, 'os')
def test_get_mac_by_pci_address_wrong_dev_path(self, mock_os):
mock_os.listdir.return_value = ['random1', 'random2']
mock_os.path.join.return_value = 'random'
self.assertRaises(
taas_exc.PciDeviceNotFoundById,
sriov_nic_utils.SriovNicUtils().get_mac_by_pci_address, 12, False)
@mock.patch.object(sriov_nic_utils, 'os')
@mock.patch.object(sriov_nic_utils, 'open', create=True)
def test_get_net_name_by_vf_pci_address(self, mock_open, mock_os):
mock_os.listdir.return_value = ['enp0s3', 'enp0s2']
mock_os.path.join.return_value = 'random'
fake_file_handle = ["52:54:00:12:35:02"]
fake_file_iter = fake_file_handle.__iter__()
mock_open.return_value.__enter__.return_value = fake_file_iter
self.assertEqual(
'net_enp0s3_52_54_00_12_35_02',
sriov_nic_utils.SriovNicUtils().
get_net_name_by_vf_pci_address(12))
def _common_merge_utility(self, value):
output_list = list()
for v in value:
output_list.append(v)
return output_list
def test_get_ranges_str_from_list(self):
input_list = [4, 11, 12, 13, 25, 26, 27]
self.assertEqual("4,11-13,25-27", common_utils.
get_ranges_str_from_list(input_list))
def test_get_list_from_ranges_str(self):
input_str = "4,6,10-13,25-27"
expected_output = [4, 6, 10, 11, 12, 13, 25, 26, 27]
self.assertEqual(expected_output, common_utils.
get_list_from_ranges_str(input_str))
def test_get_vf_num_by_pci_address_neg(self):
self.assertRaises(
taas_exc.PciDeviceNotFoundById,
sriov_nic_utils.SriovNicUtils().get_vf_num_by_pci_address, 12)
@mock.patch.object(sriov_nic_utils, 'glob')
@mock.patch.object(sriov_nic_utils, 're')
@mock.patch.object(sriov_nic_utils, 'os')
def test_get_vf_num_by_pci_address(self, mock_os, mock_re, mock_glob):
mock_glob.iglob.return_value = ['file1']
mock_os.readlink.return_value = 12
mock_re.compile().search.return_value = re.match(r"(\d+)", "89")
self.assertEqual(
'89', sriov_nic_utils.SriovNicUtils().
get_vf_num_by_pci_address(12))
@mock.patch.object(sriov_nic_utils, 'glob')
@mock.patch.object(sriov_nic_utils, 're')
@mock.patch.object(sriov_nic_utils, 'os')
@mock.patch.object(sriov_nic_utils, 'open', create=True)
@mock.patch.object(sriov_nic_utils, 'portbindings')
def test_get_sriov_port_params(self, mock_port_bindings, mock_open,
mock_os, mock_re, mock_glob):
sriov_port = copy.deepcopy(FAKE_SRIOV_PORT)
fake_profile = mock_port_bindings.PROFILE = 'binding:profile'
mock_port_bindings.VIF_DETAILS = 'binding:vif_details'
sriov_port[fake_profile]['pci_slot'] = 3
mock_glob.iglob.return_value = ['file1']
mock_os.readlink.return_value = 12
mock_re.compile().search.return_value = re.match(r"(\d+)", "89")
mock_os.listdir.return_value = ['net_enp0s2_52_54_00_12_35_02',
'net_enp0s3_52_54_00_12_35_02']
mock_os.path.join.return_value = 'random'
fake_file_handle = ["52:54:00:12:35:02"]
fake_file_iter = fake_file_handle.__iter__()
mock_open.return_value.__enter__.return_value = fake_file_iter
expected_output = {
'mac': '52:54:00:12:35:02', 'pci_slot': 3, 'vf_index': '89',
'pf_device': 'net_enp0s3_52_54_00_12_35_02', 'src_vlans': 20}
self.assertEqual(
expected_output, sriov_nic_utils.SriovNicUtils().
get_sriov_port_params(sriov_port))
@mock.patch.object(sriov_nic_utils, 'glob')
@mock.patch.object(sriov_nic_utils, 're')
@mock.patch.object(sriov_nic_utils, 'os')
@mock.patch.object(sriov_nic_utils, 'open', create=True)
@mock.patch.object(sriov_nic_utils, 'portbindings')
def test_get_sriov_port_params_no_pci_slot(self, mock_port_bindings,
mock_open, mock_os, mock_re,
mock_glob):
sriov_port = copy.deepcopy(FAKE_SRIOV_PORT)
mock_port_bindings.PROFILE = 'binding:profile'
mock_port_bindings.VIF_DETAILS = 'binding:vif_details'
mock_glob.iglob.return_value = ['file1']
mock_os.readlink.return_value = 12
mock_re.compile().search.return_value = re.match(r"(\d+)", "89")
mock_os.listdir.return_value = ['enp0s3', 'enp0s2']
mock_os.path.join.return_value = 'random'
fake_file_handle = ["52:54:00:12:35:02"]
fake_file_iter = fake_file_handle.__iter__()
mock_open.return_value.__enter__.return_value = fake_file_iter
self.assertIsNone(sriov_nic_utils.SriovNicUtils().
get_sriov_port_params(sriov_port))
@mock.patch.object(sriov_nic_utils, 'utils')
@mock.patch.object(sriov_nic_utils, 'os')
def test_execute_sysfs_command_egress_add(self, mock_os,
mock_neutron_utils):
sriov_nic_utils.SriovNicUtils().execute_sysfs_command(
'add', {'pf_device': 'p2p1', 'vf_index': '9'}, {'vf_index': '18'},
"4,11-13", True, "OUT")
egress_cmd = ['i40e_sysfs_command', 'p2p1', '18',
'egress_mirror', 'add', '9']
mock_neutron_utils.execute.assert_called_once_with(
egress_cmd, run_as_root=True)
@mock.patch.object(sriov_nic_utils, 'utils')
@mock.patch.object(sriov_nic_utils, 'os')
def test_execute_sysfs_command_ingress_add(self, mock_os,
mock_neutron_utils):
sriov_nic_utils.SriovNicUtils().execute_sysfs_command(
'add', {'pf_device': 'p2p1', 'vf_index': '9'}, {'vf_index': '18'},
"4,11-13", True, "IN")
ingress_cmd = ['i40e_sysfs_command', 'p2p1', '18',
'ingress_mirror', 'add', '9']
mock_neutron_utils.execute.assert_called_once_with(
ingress_cmd, run_as_root=True)
@mock.patch.object(sriov_nic_utils, 'utils')
@mock.patch.object(sriov_nic_utils, 'os')
def test_execute_sysfs_command_both_add(
self, mock_os, mock_neutron_utils):
sriov_nic_utils.SriovNicUtils().execute_sysfs_command(
'add', {'pf_device': 'p2p1', 'vf_index': '9'}, {'vf_index': '18'},
"4,11-13", True, "BOTH")
self.assertEqual(2, mock_neutron_utils.execute.call_count)
@mock.patch.object(sriov_nic_utils, 'utils')
@mock.patch.object(sriov_nic_utils, 'os')
def test_execute_sysfs_command_egress_rem(self, mock_os,
mock_neutron_utils):
sriov_nic_utils.SriovNicUtils().execute_sysfs_command(
'rem', {'pf_device': 'p2p1', 'vf_index': '9'}, {'vf_index': '18'},
"4,11-13", True, "OUT")
egress_cmd = ['i40e_sysfs_command', 'p2p1', '18',
'egress_mirror', 'rem', '9']
mock_neutron_utils.execute.assert_called_once_with(
egress_cmd, run_as_root=True)
@mock.patch.object(sriov_nic_utils, 'utils')
@mock.patch.object(sriov_nic_utils, 'os')
def test_execute_sysfs_command_ingress_rem(self, mock_os,
mock_neutron_utils):
sriov_nic_utils.SriovNicUtils().execute_sysfs_command(
'rem', {'pf_device': 'p2p1', 'vf_index': '9'}, {'vf_index': '18'},
"4,11-13", True, "IN")
ingress_cmd = ['i40e_sysfs_command', 'p2p1', '18',
'ingress_mirror', 'rem', '9']
mock_neutron_utils.execute.assert_called_once_with(
ingress_cmd, run_as_root=True)
@mock.patch.object(sriov_nic_utils, 'utils')
@mock.patch.object(sriov_nic_utils, 'os')
def test_execute_sysfs_command_both_rem(
self, mock_os, mock_neutron_utils):
sriov_nic_utils.SriovNicUtils().execute_sysfs_command(
'rem', {'pf_device': 'p2p1', 'vf_index': '9'}, {'vf_index': '18'},
"4,11-13", True, "BOTH")
self.assertEqual(2, mock_neutron_utils.execute.call_count)
@mock.patch.object(sriov_nic_utils, 'utils')
@mock.patch.object(sriov_nic_utils, 'os')
def test_execute_sysfs_command_not_both_vf_to_vf_all_vlans_False(
self, mock_os, mock_neutron_utils):
cmd = ['i40e_sysfs_command', 'p2p1', '9',
'vlan_mirror', 'rem', '4,11-13']
sriov_nic_utils.SriovNicUtils().execute_sysfs_command(
'rem', {'pf_device': 'p2p1', 'vf_index': '9'}, {'vf_index': '18'},
"4,11-13", False, "FAKE")
mock_neutron_utils.execute.assert_called_once_with(
cmd, run_as_root=True)

View File

@ -40,8 +40,6 @@ class TestTaasPlugin(testlib_api.SqlTestCase):
def setUp(self): def setUp(self):
super(TestTaasPlugin, self).setUp() super(TestTaasPlugin, self).setUp()
mock.patch.object(n_rpc, 'Connection', auto_spec=True).start() mock.patch.object(n_rpc, 'Connection', auto_spec=True).start()
mock.patch.object(taas_agent_api,
'TaasCallbacks', auto_spec=True).start()
mock.patch.object(taas_agent_api, mock.patch.object(taas_agent_api,
'TaasAgentApi', auto_spec=True).start() 'TaasAgentApi', auto_spec=True).start()
self.driver = mock.MagicMock() self.driver = mock.MagicMock()
@ -70,6 +68,7 @@ class TestTaasPlugin(testlib_api.SqlTestCase):
'port_id': self._port_id, 'port_id': self._port_id,
'project_id': self._project_id, 'project_id': self._project_id,
} }
self.vlan_filter = "1-5,9,18,27-30,99-108,4000-4095"
self._tap_flow = { self._tap_flow = {
'description': 'This is my tap flow', 'description': 'This is my tap flow',
'direction': 'BOTH', 'direction': 'BOTH',
@ -77,6 +76,7 @@ class TestTaasPlugin(testlib_api.SqlTestCase):
'source_port': self._port_id, 'source_port': self._port_id,
'tenant_id': self._tenant_id, 'tenant_id': self._tenant_id,
'project_id': self._project_id, 'project_id': self._project_id,
'vlan_filter': self.vlan_filter,
} }
@contextlib.contextmanager @contextlib.contextmanager
@ -115,6 +115,7 @@ class TestTaasPlugin(testlib_api.SqlTestCase):
self._tap_flow['id'] = mock.ANY self._tap_flow['id'] = mock.ANY
self._tap_flow['status'] = 'ACTIVE' self._tap_flow['status'] = 'ACTIVE'
self._tap_service['id'] = mock.ANY self._tap_service['id'] = mock.ANY
self._tap_flow['vlan_filter'] = mock.ANY
self.driver.assert_has_calls([ self.driver.assert_has_calls([
mock.call.create_tap_flow_precommit(mock.ANY), mock.call.create_tap_flow_precommit(mock.ANY),

View File

@ -51,7 +51,7 @@
# Enable tap-as-a-service # Enable tap-as-a-service
export PROJECTS="openstack/tap-as-a-service $PROJECTS" export PROJECTS="openstack/tap-as-a-service $PROJECTS"
export ENABLED_SERVICES=taas,taas_openvswitch_agent export ENABLED_SERVICES=taas,taas_agent
export DEVSTACK_GATE_SETTINGS=/opt/stack/new/tap-as-a-service/devstack/devstackgaterc export DEVSTACK_GATE_SETTINGS=/opt/stack/new/tap-as-a-service/devstack/devstackgaterc

View File

@ -0,0 +1,13 @@
---
prelude: >
Neutron TaaS is integrated with new TaaS Agent driver (i.e. SRIOV) for
Intel i40e driver backend.
features:
- |
[`blueprint port-mirroring-sriov-vf <https://blueprints.launchpad.net/neutron/+spec/port-mirroring-sriov-vf>`_]
* Adds a new TaaS Agent driver (i.e. SRIOV) for Intel i40e driver backend.
* Neutron TaaS Agent is refactored to decouple its tight binding with ovs
driver. This prepares the taas agent code-base for addition of a new
TaaS Agent driver (SRIOV).
* Adds a new API extension, i.e. taas-vlan-filter for adding a new
attribute (vlan_filter) in tap-flow data model.

View File

@ -5,7 +5,7 @@
pbr!=2.1.0,>=2.0.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0
Babel!=2.4.0,>=2.3.4 # BSD Babel!=2.4.0,>=2.3.4 # BSD
neutron>=12.0.0 # Apache-2.0 neutron>=12.0.0 # Apache-2.0
neutron-lib>=1.20.0 # Apache-2.0 neutron-lib>=1.25.0 # Apache-2.0
# Opt-in for neutron-lib consumption patches # Opt-in for neutron-lib consumption patches
# http://lists.openstack.org/pipermail/openstack-dev/2018-September/135063.html # http://lists.openstack.org/pipermail/openstack-dev/2018-September/135063.html

View File

@ -22,6 +22,13 @@ classifier =
packages = packages =
neutron_taas neutron_taas
data_files =
etc/neutron/rootwrap.d =
etc/neutron/rootwrap.d/taas-i40e-sysfs.filters
scripts =
bin/i40e_sysfs_command
[build_sphinx] [build_sphinx]
source-dir = doc/source source-dir = doc/source
build-dir = doc/build build-dir = doc/build
@ -54,6 +61,7 @@ neutron.agent.l2.extensions =
taas = neutron_taas.services.taas.agents.extensions.taas:TaasAgentExtension taas = neutron_taas.services.taas.agents.extensions.taas:TaasAgentExtension
neutron_taas.taas.agent_drivers = neutron_taas.taas.agent_drivers =
ovs = neutron_taas.services.taas.drivers.linux.ovs_taas:OvsTaasDriver ovs = neutron_taas.services.taas.drivers.linux.ovs_taas:OvsTaasDriver
sriov = neutron_taas.services.taas.drivers.linux.sriov_nic_taas:SriovNicTaasDriver
neutron.service_plugins = neutron.service_plugins =
taas = neutron_taas.services.taas.taas_plugin:TaasPlugin taas = neutron_taas.services.taas.taas_plugin:TaasPlugin
neutron.db.alembic_migrations = neutron.db.alembic_migrations =