Add propagate_uplink_status to port

Introduce an attribute 'propagate_uplink_status' to port.
This attribute can be implemented for VF port to indicate if the VF
link state should follow the state of the PF.

Note: ML2 extension driver loaded on request via configuration:

  [ml2]
  extension_drivers = uplink_status_propagation

Other related patches:
* neutron-lib: https://review.openstack.org/#/c/571821/
* tempest test: https://review.openstack.org/#/c/586719/
* OSC: https://review.openstack.org/#/c/586684/
* neutronclient: https://review.openstack.org/#/c/586712/

APIImpact Add 'propagate_uplink_status' attribute to 'port' resource

Change-Id: Ie8260c332e24c1880f9f82e6b6dacca8415be842
Close-Bug: #1722720
This commit is contained in:
Hongbin Lu 2018-06-01 22:46:52 +00:00
parent 4b7a070b3f
commit f0678b9b09
26 changed files with 486 additions and 48 deletions

View File

@ -0,0 +1,3 @@
function configure_uplink_status_propagation_extension {
neutron_ml2_extension_driver_add "uplink_status_propagation"
}

View File

@ -12,6 +12,7 @@ source $LIBDIR/segments
source $LIBDIR/trunk
source $LIBDIR/log
source $LIBDIR/fip_port_forwarding
source $LIBDIR/uplink_status_propagation
Q_BUILD_OVS_FROM_GIT=$(trueorfalse False Q_BUILD_OVS_FROM_GIT)
@ -30,6 +31,9 @@ if [[ "$1" == "stack" ]]; then
fi
;;
post-config)
if is_service_enabled neutron-uplink-status-propagation; then
configure_uplink_status_propagation_extension
fi
if is_service_enabled q-flavors neutron-flavors; then
configure_flavors
fi

View File

@ -1 +1 @@
d72db3e25539
cada2437bf41

View File

@ -0,0 +1,41 @@
# Copyright 2018 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from alembic import op
import sqlalchemy as sa
"""add propagate_uplink_status to port
Revision ID: cada2437bf41
Revises: d72db3e25539
Create Date: 2018-11-29 19:25:12.197590
"""
# revision identifiers, used by Alembic.
revision = 'cada2437bf41'
down_revision = 'd72db3e25539'
def upgrade():
op.create_table('portuplinkstatuspropagation',
sa.Column('port_id', sa.String(36),
sa.ForeignKey('ports.id',
ondelete="CASCADE"),
primary_key=True, index=True),
sa.Column('propagate_uplink_status', sa.Boolean(),
nullable=False,
server_default=sa.sql.false()))

View File

@ -0,0 +1,33 @@
# 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.db import model_base
import sqlalchemy as sa
from sqlalchemy import orm
from neutron.db import models_v2
class PortUplinkStatusPropagation(model_base.BASEV2):
__tablename__ = 'portuplinkstatuspropagation'
port_id = sa.Column(sa.String(36),
sa.ForeignKey('ports.id', ondelete="CASCADE"),
primary_key=True, index=True)
propagate_uplink_status = sa.Column(sa.Boolean(), nullable=False,
server_default=sa.sql.false())
port = orm.relationship(
models_v2.Port, load_on_pending=True,
backref=orm.backref("propagate_uplink_status",
lazy='joined', uselist=False,
cascade='delete'))
revises_on_change = ('port', )

View File

@ -0,0 +1,32 @@
# 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.definitions import uplink_status_propagation as usp
from neutron.objects.port.extensions import uplink_status_propagation as \
usp_obj
class UplinkStatusPropagationMixin(object):
"""Mixin class to add uplink propagation to a port"""
def _process_create_port(self, context, data, res):
obj = usp_obj.PortUplinkStatusPropagation(context, port_id=res['id'],
propagate_uplink_status=data[usp.PROPAGATE_UPLINK_STATUS])
obj.create()
res[usp.PROPAGATE_UPLINK_STATUS] = data[usp.PROPAGATE_UPLINK_STATUS]
@staticmethod
def _extend_port_dict(port_res, port_db):
usp_db = port_db.get(usp.PROPAGATE_UPLINK_STATUS)
port_res[usp.PROPAGATE_UPLINK_STATUS] = (
usp_db.propagate_uplink_status if usp_db else False)

View File

@ -0,0 +1,18 @@
# 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.definitions import uplink_status_propagation as apidef
from neutron_lib.api import extensions
class Uplink_status_propagation(extensions.APIExtensionDescriptor):
api_definition = apidef

View File

@ -0,0 +1,34 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_versionedobjects import fields as obj_fields
from neutron.db.models import uplink_status_propagation as db_models
from neutron.objects import base
from neutron.objects import common_types
@base.NeutronObjectRegistry.register
class PortUplinkStatusPropagation(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = "1.0"
db_model = db_models.PortUplinkStatusPropagation
primary_keys = ['port_id']
fields = {
'port_id': common_types.UUIDField(),
'propagate_uplink_status': obj_fields.BooleanField(default=False),
}
foreign_keys = {'Port': {'port_id': 'id'}}

View File

@ -169,14 +169,15 @@ class EmbSwitch(object):
vf_index = self._get_vf_index(pci_slot)
return self.pci_dev_wrapper.get_vf_state(vf_index)
def set_device_state(self, pci_slot, state):
def set_device_state(self, pci_slot, state, propagate_uplink_state):
"""Set device state.
@param pci_slot: Virtual Function address
@param state: link state
"""
vf_index = self._get_vf_index(pci_slot)
return self.pci_dev_wrapper.set_vf_state(vf_index, state)
return self.pci_dev_wrapper.set_vf_state(vf_index, state,
auto=propagate_uplink_state)
def set_device_rate(self, pci_slot, rate_type, rate_kbps):
"""Set device rate: rate (max_tx_rate), min_tx_rate
@ -293,15 +294,15 @@ class ESwitchManager(object):
def get_device_state(self, device_mac, pci_slot):
"""Get device state.
Get the device state (up/True or down/False)
Get the device state (up/enable, down/disable, or auto)
@param device_mac: device mac
@param pci_slot: VF PCI slot
@return: device state (True/False) None if failed
@return: device state (enable/disable/auto) None if failed
"""
embedded_switch = self._get_emb_eswitch(device_mac, pci_slot)
if embedded_switch:
return embedded_switch.get_device_state(pci_slot)
return False
return pci_lib.LinkState.DISABLE
def set_device_max_rate(self, device_mac, pci_slot, max_kbps):
"""Set device max rate
@ -333,18 +334,21 @@ class ESwitchManager(object):
ip_link_support.IpLinkConstants.IP_LINK_CAPABILITY_MIN_TX_RATE,
min_kbps)
def set_device_state(self, device_mac, pci_slot, admin_state_up):
def set_device_state(self, device_mac, pci_slot, admin_state_up,
propagate_uplink_state):
"""Set device state
Sets the device state (up or down)
@param device_mac: device mac
@param pci_slot: pci slot
@param admin_state_up: device admin state True/False
@param propagate_uplink_state: follow uplink state True/False
"""
embedded_switch = self._get_emb_eswitch(device_mac, pci_slot)
if embedded_switch:
embedded_switch.set_device_state(pci_slot,
admin_state_up)
admin_state_up,
propagate_uplink_state)
def set_device_spoofcheck(self, device_mac, pci_slot, enabled):
"""Set device spoofcheck

View File

@ -24,6 +24,12 @@ from neutron.plugins.ml2.drivers.mech_sriov.agent.common \
LOG = logging.getLogger(__name__)
class LinkState(object):
ENABLE = "enable"
DISABLE = "disable"
AUTO = "auto"
class PciDeviceIPWrapper(ip_lib.IPWrapper):
"""Wrapper class for ip link commands.
@ -41,10 +47,6 @@ class PciDeviceIPWrapper(ip_lib.IPWrapper):
IP_LINK_OP_NOT_SUPPORTED = 'RTNETLINK answers: Operation not supported'
class LinkState(object):
ENABLE = "enable"
DISABLE = "disable"
def __init__(self, dev_name):
super(PciDeviceIPWrapper, self).__init__()
self.dev_name = dev_name
@ -96,10 +98,9 @@ class PciDeviceIPWrapper(ip_lib.IPWrapper):
return vf_to_mac_mapping
def get_vf_state(self, vf_index):
"""Get vf state {True/False}
"""Get vf state {enable/disable/auto}
@param vf_index: vf index
@todo: Handle "auto" state
"""
try:
out = self._as_root([], "link", ("show", self.dev_name))
@ -112,19 +113,22 @@ class PciDeviceIPWrapper(ip_lib.IPWrapper):
vf_details = self._parse_vf_link_show(vf_lines[0])
if vf_details:
state = vf_details.get("link-state",
self.LinkState.DISABLE)
if state != self.LinkState.DISABLE:
return True
return False
LinkState.DISABLE)
if state in (LinkState.AUTO, LinkState.ENABLE):
return state
return LinkState.DISABLE
def set_vf_state(self, vf_index, state):
def set_vf_state(self, vf_index, state, auto=False):
"""sets vf state.
@param vf_index: vf index
@param state: required state {True/False}
"""
status_str = self.LinkState.ENABLE if state else \
self.LinkState.DISABLE
if auto:
status_str = LinkState.AUTO
else:
status_str = LinkState.ENABLE if state else \
LinkState.DISABLE
self._set_feature(vf_index, "state", status_str)
def set_vf_spoofcheck(self, vf_index, enabled):

View File

@ -240,7 +240,8 @@ class SriovNicSwitchAgent(object):
# If one of the above operations fails => resync with plugin
return (resync_a | resync_b)
def treat_device(self, device, pci_slot, admin_state_up, spoofcheck=True):
def treat_device(self, device, pci_slot, admin_state_up, spoofcheck=True,
propagate_uplink_state=False):
if self.eswitch_mgr.device_exists(device, pci_slot):
try:
self.eswitch_mgr.set_device_spoofcheck(device, pci_slot,
@ -253,7 +254,8 @@ class SriovNicSwitchAgent(object):
try:
self.eswitch_mgr.set_device_state(device, pci_slot,
admin_state_up)
admin_state_up,
propagate_uplink_state)
except exc.IpCommandOperationNotSupportedError:
LOG.warning("Device %s does not support state change",
device)
@ -305,10 +307,12 @@ class SriovNicSwitchAgent(object):
port_id = device_details['port_id']
profile = device_details['profile']
spoofcheck = device_details.get('port_security_enabled', True)
if self.treat_device(device,
profile.get('pci_slot'),
device_details['admin_state_up'],
spoofcheck):
if self.treat_device(
device,
profile.get('pci_slot'),
device_details['admin_state_up'],
spoofcheck,
device_details['propagate_uplink_status']):
if device_details['admin_state_up']:
devices_up.add(device)
else:

View File

@ -0,0 +1,42 @@
# 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.definitions import uplink_status_propagation as usp
from neutron_lib.plugins.ml2 import api
from oslo_log import log as logging
from neutron.db import uplink_status_propagation_db as usp_db
LOG = logging.getLogger(__name__)
class UplinkStatusPropagationExtensionDriver(
api.ExtensionDriver, usp_db.UplinkStatusPropagationMixin):
_supported_extension_alias = 'uplink-status-propagation'
def initialize(self):
LOG.info("UplinkStatusPropagationExtensionDriver initialization "
"complete")
@property
def extension_alias(self):
return self._supported_extension_alias
def process_create_port(self, context, data, result):
# Create the port extension attributes.
if usp.PROPAGATE_UPLINK_STATUS in data:
self._process_create_port(context, data, result)
def extend_port_dict(self, session, db_data, result):
self._extend_port_dict(result, db_data)

View File

@ -16,6 +16,7 @@
from neutron_lib.agent import topics
from neutron_lib.api.definitions import port_security as psec
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import uplink_status_propagation as usp
from neutron_lib.callbacks import resources
from neutron_lib import constants as n_const
from neutron_lib.plugins import directory
@ -156,7 +157,9 @@ class RpcCallbacks(type_tunnel.TunnelRpcCallbackMixin):
'port_security_enabled': port.get(psec.PORTSECURITY, True),
'qos_policy_id': port.get(qos_consts.QOS_POLICY_ID),
'network_qos_policy_id': network_qos_policy_id,
'profile': port[portbindings.PROFILE]}
'profile': port[portbindings.PROFILE],
'propagate_uplink_status': port.get(
usp.PROPAGATE_UPLINK_STATUS, False)}
LOG.debug("Returning: %s", entry)
return entry

View File

@ -98,6 +98,7 @@ case $VENV in
load_rc_hook disable_dvr_tests
fi
load_conf_hook quotas
load_rc_hook uplink_status_propagation
load_rc_hook dns
load_rc_hook qos
load_rc_hook segments

View File

@ -58,3 +58,4 @@ NETWORK_API_EXTENSIONS+=",standard-attr-tag"
NETWORK_API_EXTENSIONS+=",subnet_allocation"
NETWORK_API_EXTENSIONS+=",trunk"
NETWORK_API_EXTENSIONS+=",trunk-details"
NETWORK_API_EXTENSIONS+=",uplink-status-propagation"

View File

@ -0,0 +1 @@
enable_service neutron-uplink-status-propagation

View File

@ -400,7 +400,8 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
for arg in (('admin_state_up', 'device_id',
'mac_address', 'name', 'fixed_ips',
'tenant_id', 'device_owner', 'security_groups') +
'tenant_id', 'device_owner', 'security_groups',
'propagate_uplink_status') +
(arg_list or ())):
# Arg must be present
if arg in kwargs:

View File

@ -0,0 +1,71 @@
# 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 ddt
from neutron_lib.api.definitions import uplink_status_propagation as apidef
from neutron.db import _resource_extend as resource_extend
from neutron.db import db_base_plugin_v2
from neutron.db import uplink_status_propagation_db as usp_db
from neutron.tests.unit.db import test_db_base_plugin_v2
class UplinkStatusPropagationExtensionTestPlugin(
db_base_plugin_v2.NeutronDbPluginV2,
usp_db.UplinkStatusPropagationMixin):
"""Test plugin to mixin the uplink status propagation extension.
"""
supported_extension_aliases = [apidef.ALIAS]
@staticmethod
@resource_extend.extends([apidef.COLLECTION_NAME])
def _extend_network_project_default(port_res, port_db):
return usp_db.UplinkStatusPropagationMixin._extend_port_dict(
port_res, port_db)
def create_port(self, context, port):
with context.session.begin(subtransactions=True):
new_port = super(UplinkStatusPropagationExtensionTestPlugin,
self).create_port(context, port)
# Update the propagate_uplink_status in the database
p = port['port']
if 'propagate_uplink_status' not in p:
p['propagate_uplink_status'] = False
self._process_create_port(context, p, new_port)
return new_port
@ddt.ddt
class UplinkStatusPropagationExtensionTestCase(
test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
"""Test API extension propagate_uplink_status attributes.
"""
def setUp(self):
plugin = ('neutron.tests.unit.extensions.test_uplink_status_'
'propagation.UplinkStatusPropagationExtensionTestPlugin')
super(UplinkStatusPropagationExtensionTestCase,
self).setUp(plugin=plugin)
@ddt.data(True, False)
def test_create_port_propagate_uplink_status(
self, propagate_uplink_status):
name = 'propagate_uplink_status'
keys = [('name', name), ('admin_state_up', True),
('status', self.port_create_status),
('propagate_uplink_status', propagate_uplink_status)]
with self.port(name=name,
propagate_uplink_status=propagate_uplink_status
) as port:
for k, v in keys:
self.assertEqual(v, port['port'][k])

View File

@ -0,0 +1,33 @@
# 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.objects.port.extensions import uplink_status_propagation
from neutron.tests.unit.objects import test_base as obj_test_base
from neutron.tests.unit import testlib_api
class UplinkStatusPropagationIfaceObjectTestCase(
obj_test_base.BaseObjectIfaceTestCase):
_test_class = uplink_status_propagation.PortUplinkStatusPropagation
class UplinkStatusPropagationDbObjectTestCase(
obj_test_base.BaseDbObjectTestCase,
testlib_api.SqlTestCase):
_test_class = uplink_status_propagation.PortUplinkStatusPropagation
def setUp(self):
super(UplinkStatusPropagationDbObjectTestCase, self).setUp()
self.update_obj_fields(
{'port_id': lambda: self._create_test_port_id()})

View File

@ -69,6 +69,7 @@ object_data = {
'PortDNS': '1.1-c5ca2dc172bdd5fafee3fc986d1d7023',
'PortForwarding': '1.1-db61273978c497239be5389a8aeb1c61',
'PortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3',
'PortUplinkStatusPropagation': '1.0-3cfb3f7da716ca9687e4f04ca72b081d',
'ProviderResourceAssociation': '1.0-05ab2d5a3017e5ce9dd381328f285f34',
'ProvisioningBlock': '1.0-c19d6d05bfa8143533471c1296066125',
'QosBandwidthLimitRule': '1.3-51b662b12a8d1dfa89288d826c6d26d3',

View File

@ -158,27 +158,38 @@ class TestESwitchManagerApi(base.BaseTestCase):
self.assertIn(devices_info['p6p1'][0], list(result))
self.assertIn(devices_info['p6p2'][0], list(result))
def test_get_device_status_true(self):
def test_get_device_status_enable(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.EmbSwitch.get_pci_device",
return_value=self.ASSIGNED_MAC),\
mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.EmbSwitch.get_device_state",
return_value=True):
return_value='enable'):
result = self.eswitch_mgr.get_device_state(self.ASSIGNED_MAC,
self.PCI_SLOT)
self.assertTrue(result)
self.assertEqual('enable', result)
def test_get_device_status_false(self):
def test_get_device_status_disable(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.EmbSwitch.get_pci_device",
return_value=self.ASSIGNED_MAC),\
mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.EmbSwitch.get_device_state",
return_value=False):
return_value='disable'):
result = self.eswitch_mgr.get_device_state(self.ASSIGNED_MAC,
self.PCI_SLOT)
self.assertFalse(result)
self.assertEqual('disable', result)
def test_get_device_status_auto(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.EmbSwitch.get_pci_device",
return_value=self.ASSIGNED_MAC),\
mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.EmbSwitch.get_device_state",
return_value='auto'):
result = self.eswitch_mgr.get_device_state(self.ASSIGNED_MAC,
self.PCI_SLOT)
self.assertEqual('auto', result)
def test_get_device_status_mismatch(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
@ -186,7 +197,7 @@ class TestESwitchManagerApi(base.BaseTestCase):
return_value=self.ASSIGNED_MAC),\
mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.EmbSwitch.get_device_state",
return_value=True):
return_value='enable'):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.LOG.warning") as log_mock:
result = self.eswitch_mgr.get_device_state(self.WRONG_MAC,
@ -195,7 +206,7 @@ class TestESwitchManagerApi(base.BaseTestCase):
'%(device_mac)s - %(pci_slot)s',
{'pci_slot': self.PCI_SLOT,
'device_mac': self.WRONG_MAC})
self.assertFalse(result)
self.assertEqual('disable', result)
def test_set_device_status(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
@ -204,7 +215,7 @@ class TestESwitchManagerApi(base.BaseTestCase):
mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.EmbSwitch.set_device_state"):
self.eswitch_mgr.set_device_state(self.ASSIGNED_MAC,
self.PCI_SLOT, True)
self.PCI_SLOT, True, False)
def test_set_device_max_rate(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
@ -241,7 +252,7 @@ class TestESwitchManagerApi(base.BaseTestCase):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"eswitch_manager.LOG.warning") as log_mock:
self.eswitch_mgr.set_device_state(self.WRONG_MAC,
self.PCI_SLOT, True)
self.PCI_SLOT, True, False)
log_mock.assert_called_with('device pci mismatch: '
'%(device_mac)s - %(pci_slot)s',
{'pci_slot': self.PCI_SLOT,
@ -463,7 +474,7 @@ class TestEmbSwitch(base.BaseTestCase):
"PciDeviceIPWrapper.set_vf_state"):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent."
"pci_lib.LOG.warning") as log_mock:
self.emb_switch.set_device_state(self.PCI_SLOT, True)
self.emb_switch.set_device_state(self.PCI_SLOT, True, False)
self.assertEqual(0, log_mock.call_count)
def test_set_device_state_fail(self):
@ -471,7 +482,7 @@ class TestEmbSwitch(base.BaseTestCase):
"PciDeviceIPWrapper.set_vf_state"):
self.assertRaises(exc.InvalidPciSlotError,
self.emb_switch.set_device_state,
self.WRONG_PCI_SLOT, True)
self.WRONG_PCI_SLOT, True, False)
def test_set_device_spoofcheck_ok(self):
with mock.patch("neutron.plugins.ml2.drivers.mech_sriov.agent.pci_lib."

View File

@ -81,14 +81,14 @@ class TestPciLib(base.BaseTestCase):
"_as_root") as mock_as_root:
mock_as_root.return_value = self.VF_LINK_SHOW
result = self.pci_wrapper.get_vf_state(self.VF_INDEX)
self.assertTrue(result)
self.assertEqual('enable', result)
def test_get_vf_state_disable(self):
with mock.patch.object(self.pci_wrapper,
"_as_root") as mock_as_root:
mock_as_root.return_value = self.VF_LINK_SHOW
result = self.pci_wrapper.get_vf_state(self.VF_INDEX_DISABLE)
self.assertFalse(result)
self.assertEqual('disable', result)
def test_get_vf_state_fail(self):
with mock.patch.object(self.pci_wrapper,

View File

@ -232,6 +232,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port123',
'network_id': 'net123',
'admin_state_up': True,
'propagate_uplink_status': True,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': '1:2:3.0'},
@ -255,6 +256,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port123',
'network_id': 'net123',
'admin_state_up': True,
'propagate_uplink_status': False,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': SLOT1},
@ -264,6 +266,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port124',
'network_id': 'net123',
'admin_state_up': True,
'propagate_uplink_status': False,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': SLOT2},
@ -299,6 +302,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port123',
'network_id': 'net123',
'admin_state_up': True,
'propagate_uplink_status': False,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': '1:2:3.0'},
@ -319,7 +323,7 @@ class TestSriovAgent(base.BaseTestCase):
agent.eswitch_mgr.set_device_state.assert_called_with(
'aa:bb:cc:dd:ee:ff',
'1:2:3.0',
True)
True, False)
agent.eswitch_mgr.set_device_spoofcheck.assert_called_with(
'aa:bb:cc:dd:ee:ff',
'1:2:3.0',
@ -337,6 +341,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port123',
'network_id': 'net123',
'admin_state_up': True,
'propagate_uplink_status': False,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': '1:2:3.0'},
@ -346,6 +351,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port321',
'network_id': 'net123',
'admin_state_up': True,
'propagate_uplink_status': False,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': '1:2:3.0'},
@ -364,8 +370,8 @@ class TestSriovAgent(base.BaseTestCase):
calls = [mock.call('aa:bb:cc:dd:ee:ff', '1:2:3.0'),
mock.call('11:22:33:44:55:66', '1:2:3.0')]
agent.eswitch_mgr.device_exists.assert_has_calls(calls, any_order=True)
calls = [mock.call('aa:bb:cc:dd:ee:ff', '1:2:3.0', True),
mock.call('11:22:33:44:55:66', '1:2:3.0', True)]
calls = [mock.call('aa:bb:cc:dd:ee:ff', '1:2:3.0', True, False),
mock.call('11:22:33:44:55:66', '1:2:3.0', True, False)]
agent.eswitch_mgr.set_device_state.assert_has_calls(calls,
any_order=True)
calls = [mock.call('aa:bb:cc:dd:ee:ff', '1:2:3.0', False),
@ -385,6 +391,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port123',
'network_id': 'net123',
'admin_state_up': True,
'propagate_uplink_status': False,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': '1:2:3.0'},
@ -394,6 +401,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port321',
'network_id': 'net123',
'admin_state_up': False,
'propagate_uplink_status': False,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': '1:2:3.0'},
@ -412,8 +420,8 @@ class TestSriovAgent(base.BaseTestCase):
calls = [mock.call('aa:bb:cc:dd:ee:ff', '1:2:3.0'),
mock.call('11:22:33:44:55:66', '1:2:3.0')]
agent.eswitch_mgr.device_exists.assert_has_calls(calls, any_order=True)
calls = [mock.call('aa:bb:cc:dd:ee:ff', '1:2:3.0', True),
mock.call('11:22:33:44:55:66', '1:2:3.0', False)]
calls = [mock.call('aa:bb:cc:dd:ee:ff', '1:2:3.0', True, False),
mock.call('11:22:33:44:55:66', '1:2:3.0', False, False)]
agent.eswitch_mgr.set_device_state.assert_has_calls(calls,
any_order=True)
calls = [mock.call('aa:bb:cc:dd:ee:ff', '1:2:3.0', False),
@ -465,6 +473,7 @@ class TestSriovAgent(base.BaseTestCase):
'port_id': 'port123',
'network_id': 'net123',
'admin_state_up': False,
'propagate_uplink_status': False,
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': '1:2:3.0'},
@ -493,6 +502,7 @@ class TestSriovAgent(base.BaseTestCase):
'network_type': 'vlan',
'segmentation_id': 100,
'profile': {'pci_slot': '1:2:3.0'},
'propagate_uplink_status': False,
'physical_network': 'physnet1'}
agent.plugin_rpc = mock.Mock()
agent.plugin_rpc.get_devices_details_list.return_value = [mock_details]

View File

@ -0,0 +1,70 @@
# 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 mock
from neutron_lib.api.definitions import port as port_def
from neutron_lib.plugins import directory
from oslo_config import cfg
from neutron.plugins.ml2.extensions import uplink_status_propagation as usp
from neutron.tests.unit.plugins.ml2 import test_plugin
class UplinkStatusPropagationML2ExtDriverTestCase(
test_plugin.Ml2PluginV2TestCase):
_extension_drivers = ['uplink_status_propagation']
def setUp(self):
cfg.CONF.set_override('extension_drivers',
self._extension_drivers,
group='ml2')
super(UplinkStatusPropagationML2ExtDriverTestCase, self).setUp()
self.plugin = directory.get_plugin()
def test_extend_port_dict_no_project_default(self):
for db_data in ({'propagate_uplink_status': None}, {}):
response_data = {}
session = mock.Mock()
driver = usp.UplinkStatusPropagationExtensionDriver()
driver.extend_port_dict(session, db_data, response_data)
self.assertFalse(response_data['propagate_uplink_status'])
def test_show_port_has_propagate_uplink_status(self):
with self.port(propagate_uplink_status=True) as port:
req = self.new_show_request(port_def.COLLECTION_NAME,
port['port']['id'],
self.fmt)
n = self.deserialize(self.fmt, req.get_response(self.api))
self.assertTrue(n['port']['propagate_uplink_status'])
def test_port_create_propagate_uplink_status(self):
with self.network() as n:
args = {'port':
{'name': 'test',
'network_id': n['network']['id'],
'tenant_id': n['network']['id'],
'device_id': '',
'device_owner': '',
'fixed_ips': '',
'propagate_uplink_status': True,
'admin_state_up': True,
'status': 'ACTIVE'}}
port = None
try:
port = self.plugin.create_port(self.context, args)
finally:
if port:
self.plugin.delete_port(self.context, port['id'])
self.assertTrue(port['propagate_uplink_status'])

View File

@ -0,0 +1,15 @@
---
features:
- |
Introduce the attribute ``propagate_uplink_status`` to ports.
Right now, the SRIOV mechanism driver leverages this attribute to decide
if the VF link should follow the state of the PF.
For example, if the PF is down, the VF link state is automatically
set to down as well.
Operators can turn on this feature via the configuration option::
[ml2]
extension_drivers = uplink_status_propagation
The API extension ``uplink_status_propagation`` is introduced to indicate
if this feature is turned on.

View File

@ -100,6 +100,7 @@ neutron.ml2.extension_drivers =
dns = neutron.plugins.ml2.extensions.dns_integration:DNSExtensionDriverML2
data_plane_status = neutron.plugins.ml2.extensions.data_plane_status:DataPlaneStatusExtensionDriver
dns_domain_ports = neutron.plugins.ml2.extensions.dns_integration:DNSDomainPortsExtensionDriver
uplink_status_propagation = neutron.plugins.ml2.extensions.uplink_status_propagation:UplinkStatusPropagationExtensionDriver
neutron.ipam_drivers =
fake = neutron.tests.unit.ipam.fake_driver:FakeDriver
internal = neutron.ipam.drivers.neutrondb_ipam.driver:NeutronDbPool