diff --git a/API_REFERENCE.rst b/API_REFERENCE.rst index ac2826e5..e608a385 100644 --- a/API_REFERENCE.rst +++ b/API_REFERENCE.rst @@ -69,7 +69,10 @@ TapFlow Represents the port from which the traffic needs to be mirrored. 'required_by_policy': True, 'is_visible': True}, 'direction': {'allow_post': True, 'allow_put': False, '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'] @@ -172,7 +175,8 @@ extension "name": "flow1", "source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1", "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", "source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1", "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", "source_port": "775a58bb-e2c6-4529-a918-2f019169b5b1", "tap_service_id": "c352f537-ad49-48eb-ab05-1c6b8cb900ff", - "tenant_id": "97e1586d580745d7b311406697aaf097" + "tenant_id": "97e1586d580745d7b311406697aaf097", + "vlan_filter": "9,18-27,36,45,54-63" } ] } diff --git a/bin/i40e_sysfs_command b/bin/i40e_sysfs_command new file mode 100644 index 00000000..11bc9716 --- /dev/null +++ b/bin/i40e_sysfs_command @@ -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 <'vlan_mirror'> <'add'|'rem'> \nUsage: 2. VF to VF mirroring: $0 <'ingress_mirror'|'egress_mirror'> <'add'|'rem'> " + +(($#!=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 diff --git a/devstack/devstackgaterc b/devstack/devstackgaterc index 41dc97c8..3f37f073 100644 --- a/devstack/devstackgaterc +++ b/devstack/devstackgaterc @@ -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-meta 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 export OVERRIDE_ENABLED_SERVICES diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 993ab597..9366b817 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -26,6 +26,7 @@ function configure_taas_plugin { cp $TAAS_PLUGIN_PATH/etc/taas_plugin.ini $TAAS_PLUGIN_CONF_FILE neutron_server_config_add $TAAS_PLUGIN_CONF_FILE neutron_service_plugin_class_add taas + neutron_deploy_rootwrap_filters $TAAS_PLUGIN_PATH } if is_service_enabled taas; then diff --git a/devstack/settings b/devstack/settings index 12f469b4..c40ed293 100644 --- a/devstack/settings +++ b/devstack/settings @@ -2,5 +2,3 @@ ABSOLUTE_PATH=$(cd `dirname "${BASH_SOURCE[0]}"` && pwd) TAAS_PLUGIN_PATH=$ABSOLUTE_PATH/.. 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" diff --git a/etc/neutron/rootwrap.d/taas-i40e-sysfs.filters b/etc/neutron/rootwrap.d/taas-i40e-sysfs.filters new file mode 100644 index 00000000..dddd036f --- /dev/null +++ b/etc/neutron/rootwrap.d/taas-i40e-sysfs.filters @@ -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), .* diff --git a/lower-constraints.txt b/lower-constraints.txt index 43441373..7ced64db 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -53,7 +53,8 @@ msgpack==0.5.6 munch==2.2.0 netaddr==0.7.19 netifaces==0.10.6 -neutron-lib==1.20.0 +neutron-lib==1.25.0 +neutron==14.0.0.0b3 openstacksdk==0.12.0 os-client-config==1.29.0 os-service-types==1.2.0 diff --git a/neutron_taas/common/constants.py b/neutron_taas/common/constants.py index 50a5ec13..4c7a6306 100644 --- a/neutron_taas/common/constants.py +++ b/neutron_taas/common/constants.py @@ -1,3 +1,4 @@ +# Copyright (C) 2018 AT&T # Copyright (C) 2015 Midokura SARL. # All Rights Reserved. # @@ -14,3 +15,6 @@ # under the License. TAAS = 'TAAS' + +# Complete VLAN Id Range +VLAN_RANGE = '0-4095' diff --git a/neutron_taas/common/utils.py b/neutron_taas/common/utils.py new file mode 100644 index 00000000..3f437c3a --- /dev/null +++ b/neutron_taas/common/utils.py @@ -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]) diff --git a/neutron_taas/db/migration/alembic_migration/versions/EXPAND_HEAD b/neutron_taas/db/migration/alembic_migration/versions/EXPAND_HEAD index 4cb3f9fd..61ad9afa 100644 --- a/neutron_taas/db/migration/alembic_migration/versions/EXPAND_HEAD +++ b/neutron_taas/db/migration/alembic_migration/versions/EXPAND_HEAD @@ -1 +1 @@ -fddbdec8711a +ccbcc559d175 diff --git a/neutron_taas/db/migration/alembic_migration/versions/stein/expand/ccbcc559d175_add_vlan_filter_to_tap_flow.py b/neutron_taas/db/migration/alembic_migration/versions/stein/expand/ccbcc559d175_add_vlan_filter_to_tap_flow.py new file mode 100644 index 00000000..6c958413 --- /dev/null +++ b/neutron_taas/db/migration/alembic_migration/versions/stein/expand/ccbcc559d175_add_vlan_filter_to_tap_flow.py @@ -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)) diff --git a/neutron_taas/db/taas_db.py b/neutron_taas/db/taas_db.py index 29dfcb71..965de28b 100644 --- a/neutron_taas/db/taas_db.py +++ b/neutron_taas/db/taas_db.py @@ -1,3 +1,4 @@ +# Copyright (C) 2018 AT&T # Copyright (C) 2015 Ericsson AB # Copyright (c) 2015 Gigamon # @@ -60,6 +61,7 @@ class TapFlow(model_base.BASEV2, model_base.HasId, nullable=False) status = sa.Column(sa.String(16), nullable=False, server_default=constants.ACTIVE) + vlan_filter = sa.Column(sa.String(1024), nullable=True) class TapIdAssociation(model_base.BASEV2): @@ -128,7 +130,8 @@ class Taas_db_Mixin(taas.TaasPluginBase): 'description': tap_flow['description'], 'source_port': tap_flow['source_port'], '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) @@ -209,6 +212,7 @@ class Taas_db_Mixin(taas.TaasPluginBase): source_port=t_f['source_port'], direction=t_f['direction'], status=constants.ACTIVE, + vlan_filter=t_f['vlan_filter'], ) context.session.add(tap_flow_db) diff --git a/neutron_taas/extensions/vlan_filter.py b/neutron_taas/extensions/vlan_filter.py new file mode 100644 index 00000000..28a9d0bc --- /dev/null +++ b/neutron_taas/extensions/vlan_filter.py @@ -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 [] diff --git a/neutron_taas/services/taas/agents/ovs/__init__.py b/neutron_taas/services/taas/agents/common/__init__.py similarity index 100% rename from neutron_taas/services/taas/agents/ovs/__init__.py rename to neutron_taas/services/taas/agents/common/__init__.py diff --git a/neutron_taas/services/taas/agents/ovs/taas_ovs_agent.py b/neutron_taas/services/taas/agents/common/taas_agent.py similarity index 62% rename from neutron_taas/services/taas/agents/ovs/taas_ovs_agent.py rename to neutron_taas/services/taas/agents/common/taas_agent.py index 41a05eea..40678270 100644 --- a/neutron_taas/services/taas/agents/ovs/taas_ovs_agent.py +++ b/neutron_taas/services/taas/agents/common/taas_agent.py @@ -1,3 +1,4 @@ +# Copyright (C) 2018 AT&T # Copyright (C) 2015 Ericsson AB # Copyright (c) 2015 Gigamon # @@ -15,35 +16,54 @@ 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.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 oslo_config import cfg from oslo_log import log as logging +import oslo_messaging as messaging from oslo_service import service LOG = logging.getLogger(__name__) -class TaasOvsPluginApi(api.TaasPluginApiMixin): - # Currently there are not any APIs from the the agent towards plugin +class TaasPluginApi(api.TaasPluginApiMixin): 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 -class TaasOvsAgentRpcCallback(api.TaasAgentRpcCallbackMixin): +class TaasAgentRpcCallback(api.TaasAgentRpcCallbackMixin): def __init__(self, conf, driver_type): - LOG.debug("TaaS OVS Agent initialize called") + + LOG.debug("TaaS Agent initialize called") self.conf = conf self.driver_type = driver_type - super(TaasOvsAgentRpcCallback, self).__init__() + super(TaasAgentRpcCallback, self).__init__() def initialize(self): self.taas_driver = manager.NeutronManager.load_class_for_provider( @@ -52,7 +72,7 @@ class TaasOvsAgentRpcCallback(api.TaasAgentRpcCallbackMixin): self.taas_driver.initialize() self._taas_rpc_setup() - TaasOvsAgentService(self).start() + TaasAgentService(self).start(self.taas_plugin_rpc, self.conf.host) def consume_api(self, 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): 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 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): # setup RPC to msg taas plugin - self.taas_plugin_rpc = TaasOvsPluginApi( + self.taas_plugin_rpc = TaasPluginApi( topics.TAAS_PLUGIN, self.conf.host) endpoints = [self] @@ -123,22 +146,31 @@ class TaasOvsAgentRpcCallback(api.TaasAgentRpcCallbackMixin): conn.consume_in_threads() def periodic_tasks(self): - # - # Regenerate the flow in br-tun's TAAS_SEND_FLOOD table - # to ensure all existing tunnel ports are included. - # - self.taas_driver.update_tunnel_flood_flow() + return self._invoke_driver_for_plugin_api( + context=None, + args=None, + func_name='periodic_tasks') + + def get_driver_type(self): + return self.driver_type -class TaasOvsAgentService(service.Service): +class TaasAgentService(service.Service): def __init__(self, driver): - super(TaasOvsAgentService, self).__init__() + super(TaasAgentService, self).__init__() self.driver = driver - def start(self): - super(TaasOvsAgentService, self).start() - self.tg.add_timer( - int(cfg.CONF.taas_agent_periodic_interval), - self.driver.periodic_tasks, - None - ) + def start(self, taas_plugin_rpc, host): + super(TaasAgentService, self).start() + + if self.driver.get_driver_type() == \ + taas_ovs_consts.EXTENSION_DRIVER_TYPE: + self.tg.add_timer( + int(cfg.CONF.taas_agent_periodic_interval), + self.driver.periodic_tasks, + None + ) + + # Indicate the TaaS plugin to recreate the taas resources + rpc_msg = {'host_id': host} + taas_plugin_rpc.sync_tap_resources(rpc_msg, host) diff --git a/neutron_taas/services/taas/agents/extensions/taas.py b/neutron_taas/services/taas/agents/extensions/taas.py index 8d0f549c..7605c2a9 100644 --- a/neutron_taas/services/taas/agents/extensions/taas.py +++ b/neutron_taas/services/taas/agents/extensions/taas.py @@ -18,14 +18,13 @@ import six 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_log import log as logging LOG = logging.getLogger(__name__) - OPTS = [ cfg.IntOpt( 'taas_agent_periodic_interval', @@ -71,7 +70,7 @@ class TaasAgentExtension(l2_extension.L2AgentExtension): def initialize(self, connection, driver_type): """Initialize agent extension.""" - self.taas_agent = taas_ovs_agent.TaasOvsAgentRpcCallback( + self.taas_agent = taas_agent.TaasAgentRpcCallback( cfg.CONF, driver_type) self.taas_agent.consume_api(self.agent_api) self.taas_agent.initialize() diff --git a/neutron_taas/services/taas/drivers/linux/ovs_constants.py b/neutron_taas/services/taas/drivers/linux/ovs_constants.py index 09fea874..ba6129ed 100644 --- a/neutron_taas/services/taas/drivers/linux/ovs_constants.py +++ b/neutron_taas/services/taas/drivers/linux/ovs_constants.py @@ -26,3 +26,6 @@ TAAS_DST_CHECK = 36 TAAS_SRC_CHECK = 37 TAAS_DST_RESPOND = 38 TAAS_SRC_RESPOND = 39 + +# OVS TaaS extension driver type +EXTENSION_DRIVER_TYPE = 'ovs' diff --git a/neutron_taas/services/taas/drivers/linux/ovs_taas.py b/neutron_taas/services/taas/drivers/linux/ovs_taas.py index e5f8241c..e151a68a 100644 --- a/neutron_taas/services/taas/drivers/linux/ovs_taas.py +++ b/neutron_taas/services/taas/drivers/linux/ovs_taas.py @@ -56,6 +56,13 @@ class OvsTaasDriver(taas_base.TaasAgentDriver): # Setup key-value manager for ingress BCMC flows 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): # # br-int : Integration Bridge diff --git a/neutron_taas/services/taas/drivers/linux/sriov_nic_exceptions.py b/neutron_taas/services/taas/drivers/linux/sriov_nic_exceptions.py new file mode 100644 index 00000000..3369ff59 --- /dev/null +++ b/neutron_taas/services/taas/drivers/linux/sriov_nic_exceptions.py @@ -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") diff --git a/neutron_taas/services/taas/drivers/linux/sriov_nic_taas.py b/neutron_taas/services/taas/drivers/linux/sriov_nic_taas.py new file mode 100644 index 00000000..c57d0ce6 --- /dev/null +++ b/neutron_taas/services/taas/drivers/linux/sriov_nic_taas.py @@ -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 diff --git a/neutron_taas/services/taas/drivers/linux/sriov_nic_utils.py b/neutron_taas/services/taas/drivers/linux/sriov_nic_utils.py new file mode 100644 index 00000000..d31ef125 --- /dev/null +++ b/neutron_taas/services/taas/drivers/linux/sriov_nic_utils.py @@ -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.: + + + net_enp8s0f0_90_e2_ba_5e_a6_40 + ... + + 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} diff --git a/neutron_taas/services/taas/service_drivers/taas_agent_api.py b/neutron_taas/services/taas/service_drivers/taas_agent_api.py index 4d3fa8eb..d9571516 100644 --- a/neutron_taas/services/taas/service_drivers/taas_agent_api.py +++ b/neutron_taas/services/taas/service_drivers/taas_agent_api.py @@ -21,15 +21,6 @@ import oslo_messaging as messaging 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): """RPC calls to agent APIs""" diff --git a/neutron_taas/services/taas/service_drivers/taas_rpc.py b/neutron_taas/services/taas/service_drivers/taas_rpc.py index b8efee1e..f4634b84 100644 --- a/neutron_taas/services/taas/service_drivers/taas_rpc.py +++ b/neutron_taas/services/taas/service_drivers/taas_rpc.py @@ -1,3 +1,4 @@ +# Copyright (C) 2018 AT&T # Copyright (C) 2016 Midokura SARL. # Copyright (C) 2015 Ericsson AB # Copyright (c) 2015 Gigamon @@ -14,25 +15,89 @@ # License for the specific language governing permissions and limitations # 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 rpc as n_rpc + +from neutron_taas.common import constants as taas_consts from neutron_taas.common import topics 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.taas_plugin import TaasPlugin from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import excutils 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): """Taas Rpc Service Driver class""" def __init__(self, service_plugin): LOG.debug("Loading TaasRpcDriver.") 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.create_consumer(topics.TAAS_PLUGIN, self.endpoints, fanout=False) @@ -125,12 +190,19 @@ class TaasRpcDriver(service_drivers.TaasBaseDriver): tf['source_port']) host = port['binding:host_id'] 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 # tap service(destination) port host rpc_msg = {'tap_flow': tf, 'port_mac': port_mac, '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) return @@ -147,12 +219,59 @@ class TaasRpcDriver(service_drivers.TaasBaseDriver): tf['source_port']) host = port['binding:host_id'] 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 # tap service(destination) port host rpc_msg = {'tap_flow': tf, 'port_mac': port_mac, '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) return diff --git a/neutron_taas/services/taas/taas_plugin.py b/neutron_taas/services/taas/taas_plugin.py index 95fb18f5..2052b66c 100644 --- a/neutron_taas/services/taas/taas_plugin.py +++ b/neutron_taas/services/taas/taas_plugin.py @@ -22,7 +22,7 @@ from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources 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.extensions import taas as taas_ex 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 class TaasPlugin(taas_db.Taas_db_Mixin): - supported_extension_aliases = ["taas"] + supported_extension_aliases = ["taas", + "taas-vlan-filter"] path_prefix = "/taas" def __init__(self): LOG.debug("TAAS PLUGIN INITIALIZED") 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.driver = self._get_driver_for_provider(self.default_provider) @@ -59,7 +61,7 @@ class TaasPlugin(taas_db.Taas_db_Mixin): def _load_drivers(self): """Loads plugin-drivers specified in configuration.""" self.drivers, self.default_provider = service_base.load_drivers( - 'TAAS', self) + taas_consts.TAAS, self) def _get_driver_for_provider(self, provider): if provider in self.drivers: diff --git a/neutron_taas/taas_client/tapflow.py b/neutron_taas/taas_client/tapflow.py index b395a205..974cb1e0 100644 --- a/neutron_taas/taas_client/tapflow.py +++ b/neutron_taas/taas_client/tapflow.py @@ -1,3 +1,4 @@ +# Copyright (C) 2018 AT&T # Copyright 2015 NEC Corporation # All Rights Reserved # @@ -47,7 +48,8 @@ class ListTapFlow(extension.ClientExtensionList, TapFlow): """List tap flows.""" 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 sorting_support = True @@ -77,6 +79,11 @@ class CreateTapFlow(extension.ClientExtensionCreate, TapFlow): choices=['IN', 'OUT', 'BOTH'], type=utils.convert_to_uppercase, 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): client = self.get_client() @@ -88,7 +95,8 @@ class CreateTapFlow(extension.ClientExtensionCreate, TapFlow): parsed_args.tap_service) body = {'source_port': source_port, '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) return {self.resource: body} diff --git a/neutron_taas/tests/base.py b/neutron_taas/tests/base.py new file mode 100644 index 00000000..bae95bca --- /dev/null +++ b/neutron_taas/tests/base.py @@ -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.""" diff --git a/neutron_taas/tests/unit/db/test_taas_db.py b/neutron_taas/tests/unit/db/test_taas_db.py index 3b7d5f0f..cba2ceb1 100644 --- a/neutron_taas/tests/unit/db/test_taas_db.py +++ b/neutron_taas/tests/unit/db/test_taas_db.py @@ -46,14 +46,16 @@ class TaaSDbTestCase(testlib_api.SqlTestCase): "port_id": port_id}} 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() return {"tap_flow": {"name": name, "tenant_id": self.tenant_id, "description": "test tap flow", "tap_service_id": tap_service_id, "source_port": source_port, - "direction": direction}} + "direction": direction, + "vlan_filter": vlan_filter}} def _get_tap_service(self, tap_service_id): """Helper method to retrieve tap service.""" @@ -180,6 +182,25 @@ class TaaSDbTestCase(testlib_api.SqlTestCase): self.assertEqual(tf_direction, tf['direction']) 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): """Test to retrieve all tap flows from the database.""" ts_data = self._get_tap_service_data() diff --git a/neutron_taas/tests/unit/extensions/test_taas.py b/neutron_taas/tests/unit/extensions/test_taas.py index e162f238..ad2c6f93 100644 --- a/neutron_taas/tests/unit/extensions/test_taas.py +++ b/neutron_taas/tests/unit/extensions/test_taas.py @@ -72,6 +72,9 @@ class TaasExtensionTestCase(test_api_v2_extension.ExtensionTestCase): def test_delete_tap_service(self): self._test_entity_delete('tap_service') + def _get_expected_tap_flow(self, data): + return data + def test_create_tap_flow(self): tenant_id = _uuid() tap_flow_data = { @@ -84,7 +87,8 @@ class TaasExtensionTestCase(test_api_v2_extension.ExtensionTestCase): 'project_id': tenant_id, } 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()}) instance = self.plugin.return_value 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) instance.create_tap_flow.assert_called_with( mock.ANY, - tap_flow=data) + tap_flow=expected_data) self.assertEqual(exc.HTTPCreated.code, res.status_int) res = self.deserialize(res) self.assertIn('tap_flow', res) diff --git a/neutron_taas/tests/unit/extensions/test_vlan_filter.py b/neutron_taas/tests/unit/extensions/test_vlan_filter.py new file mode 100644 index 00000000..d2402028 --- /dev/null +++ b/neutron_taas/tests/unit/extensions/test_vlan_filter.py @@ -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) diff --git a/neutron_taas/tests/unit/services/drivers/__init__.py b/neutron_taas/tests/unit/services/drivers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/neutron_taas/tests/unit/services/drivers/test_linux_sriov_nic_driver.py b/neutron_taas/tests/unit/services/drivers/test_linux_sriov_nic_driver.py new file mode 100644 index 00000000..79adc4b1 --- /dev/null +++ b/neutron_taas/tests/unit/services/drivers/test_linux_sriov_nic_driver.py @@ -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) diff --git a/neutron_taas/tests/unit/services/drivers/test_linux_sriov_utils.py b/neutron_taas/tests/unit/services/drivers/test_linux_sriov_utils.py new file mode 100644 index 00000000..9495dc2d --- /dev/null +++ b/neutron_taas/tests/unit/services/drivers/test_linux_sriov_utils.py @@ -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) diff --git a/neutron_taas/tests/unit/services/taas/test_taas_plugin.py b/neutron_taas/tests/unit/services/taas/test_taas_plugin.py index 1dec6d2c..5ad385c4 100644 --- a/neutron_taas/tests/unit/services/taas/test_taas_plugin.py +++ b/neutron_taas/tests/unit/services/taas/test_taas_plugin.py @@ -40,8 +40,6 @@ class TestTaasPlugin(testlib_api.SqlTestCase): def setUp(self): super(TestTaasPlugin, self).setUp() 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, 'TaasAgentApi', auto_spec=True).start() self.driver = mock.MagicMock() @@ -70,6 +68,7 @@ class TestTaasPlugin(testlib_api.SqlTestCase): 'port_id': self._port_id, 'project_id': self._project_id, } + self.vlan_filter = "1-5,9,18,27-30,99-108,4000-4095" self._tap_flow = { 'description': 'This is my tap flow', 'direction': 'BOTH', @@ -77,6 +76,7 @@ class TestTaasPlugin(testlib_api.SqlTestCase): 'source_port': self._port_id, 'tenant_id': self._tenant_id, 'project_id': self._project_id, + 'vlan_filter': self.vlan_filter, } @contextlib.contextmanager @@ -115,6 +115,7 @@ class TestTaasPlugin(testlib_api.SqlTestCase): self._tap_flow['id'] = mock.ANY self._tap_flow['status'] = 'ACTIVE' self._tap_service['id'] = mock.ANY + self._tap_flow['vlan_filter'] = mock.ANY self.driver.assert_has_calls([ mock.call.create_tap_flow_precommit(mock.ANY), diff --git a/playbooks/legacy/tempest-dsvm-tap-as-a-service/run.yaml b/playbooks/legacy/tempest-dsvm-tap-as-a-service/run.yaml index 1a109b9f..9bc79804 100644 --- a/playbooks/legacy/tempest-dsvm-tap-as-a-service/run.yaml +++ b/playbooks/legacy/tempest-dsvm-tap-as-a-service/run.yaml @@ -51,7 +51,7 @@ # Enable tap-as-a-service 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 diff --git a/releasenotes/notes/bp-port-mirroring-sriov-vf-879bc2aa53c2c8d4.yaml b/releasenotes/notes/bp-port-mirroring-sriov-vf-879bc2aa53c2c8d4.yaml new file mode 100644 index 00000000..5964f086 --- /dev/null +++ b/releasenotes/notes/bp-port-mirroring-sriov-vf-879bc2aa53c2c8d4.yaml @@ -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 `_] + * 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. diff --git a/requirements.txt b/requirements.txt index 7f1a4516..e71504d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 Babel!=2.4.0,>=2.3.4 # BSD 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 # http://lists.openstack.org/pipermail/openstack-dev/2018-September/135063.html diff --git a/setup.cfg b/setup.cfg index 1333cca7..0349cb61 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,13 @@ classifier = packages = neutron_taas +data_files = + etc/neutron/rootwrap.d = + etc/neutron/rootwrap.d/taas-i40e-sysfs.filters + +scripts = + bin/i40e_sysfs_command + [build_sphinx] source-dir = doc/source build-dir = doc/build @@ -54,6 +61,7 @@ neutron.agent.l2.extensions = taas = neutron_taas.services.taas.agents.extensions.taas:TaasAgentExtension neutron_taas.taas.agent_drivers = ovs = neutron_taas.services.taas.drivers.linux.ovs_taas:OvsTaasDriver + sriov = neutron_taas.services.taas.drivers.linux.sriov_nic_taas:SriovNicTaasDriver neutron.service_plugins = taas = neutron_taas.services.taas.taas_plugin:TaasPlugin neutron.db.alembic_migrations =