diff --git a/devstack/lib/uplink_status_propagation b/devstack/lib/uplink_status_propagation new file mode 100644 index 00000000000..28a77a85547 --- /dev/null +++ b/devstack/lib/uplink_status_propagation @@ -0,0 +1,3 @@ +function configure_uplink_status_propagation_extension { + neutron_ml2_extension_driver_add "uplink_status_propagation" +} diff --git a/devstack/plugin.sh b/devstack/plugin.sh index b7d114fca45..ef19c51fef6 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -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 diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 1d4f823f759..2b8029940a1 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -d72db3e25539 +cada2437bf41 diff --git a/neutron/db/migration/alembic_migrations/versions/stein/expand/cada2437bf41_add_propagate_uplink_status_to_port.py b/neutron/db/migration/alembic_migrations/versions/stein/expand/cada2437bf41_add_propagate_uplink_status_to_port.py new file mode 100644 index 00000000000..9aa40662726 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/stein/expand/cada2437bf41_add_propagate_uplink_status_to_port.py @@ -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())) diff --git a/neutron/db/models/uplink_status_propagation.py b/neutron/db/models/uplink_status_propagation.py new file mode 100644 index 00000000000..2825ffcec82 --- /dev/null +++ b/neutron/db/models/uplink_status_propagation.py @@ -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', ) diff --git a/neutron/db/uplink_status_propagation_db.py b/neutron/db/uplink_status_propagation_db.py new file mode 100644 index 00000000000..4f7f9c9a6dc --- /dev/null +++ b/neutron/db/uplink_status_propagation_db.py @@ -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) diff --git a/neutron/extensions/uplink_status_propagation.py b/neutron/extensions/uplink_status_propagation.py new file mode 100644 index 00000000000..22be67142af --- /dev/null +++ b/neutron/extensions/uplink_status_propagation.py @@ -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 diff --git a/neutron/objects/port/extensions/uplink_status_propagation.py b/neutron/objects/port/extensions/uplink_status_propagation.py new file mode 100644 index 00000000000..95010b0a409 --- /dev/null +++ b/neutron/objects/port/extensions/uplink_status_propagation.py @@ -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'}} diff --git a/neutron/plugins/ml2/drivers/mech_sriov/agent/eswitch_manager.py b/neutron/plugins/ml2/drivers/mech_sriov/agent/eswitch_manager.py index 02579bd0043..51866e4b00c 100644 --- a/neutron/plugins/ml2/drivers/mech_sriov/agent/eswitch_manager.py +++ b/neutron/plugins/ml2/drivers/mech_sriov/agent/eswitch_manager.py @@ -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 diff --git a/neutron/plugins/ml2/drivers/mech_sriov/agent/pci_lib.py b/neutron/plugins/ml2/drivers/mech_sriov/agent/pci_lib.py index 20a182abc6a..f84a4583194 100644 --- a/neutron/plugins/ml2/drivers/mech_sriov/agent/pci_lib.py +++ b/neutron/plugins/ml2/drivers/mech_sriov/agent/pci_lib.py @@ -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): diff --git a/neutron/plugins/ml2/drivers/mech_sriov/agent/sriov_nic_agent.py b/neutron/plugins/ml2/drivers/mech_sriov/agent/sriov_nic_agent.py index 2e5dea368be..6aa37194070 100644 --- a/neutron/plugins/ml2/drivers/mech_sriov/agent/sriov_nic_agent.py +++ b/neutron/plugins/ml2/drivers/mech_sriov/agent/sriov_nic_agent.py @@ -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: diff --git a/neutron/plugins/ml2/extensions/uplink_status_propagation.py b/neutron/plugins/ml2/extensions/uplink_status_propagation.py new file mode 100644 index 00000000000..e77335e248e --- /dev/null +++ b/neutron/plugins/ml2/extensions/uplink_status_propagation.py @@ -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) diff --git a/neutron/plugins/ml2/rpc.py b/neutron/plugins/ml2/rpc.py index 88c9fc9ac2b..64fd530d492 100644 --- a/neutron/plugins/ml2/rpc.py +++ b/neutron/plugins/ml2/rpc.py @@ -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 diff --git a/neutron/tests/contrib/gate_hook.sh b/neutron/tests/contrib/gate_hook.sh index ce0c5831b84..3742342c760 100644 --- a/neutron/tests/contrib/gate_hook.sh +++ b/neutron/tests/contrib/gate_hook.sh @@ -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 diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index 43a7efa1640..02f8bfe8515 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -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" diff --git a/neutron/tests/contrib/hooks/uplink_status_propagation b/neutron/tests/contrib/hooks/uplink_status_propagation new file mode 100644 index 00000000000..f7320a0ce52 --- /dev/null +++ b/neutron/tests/contrib/hooks/uplink_status_propagation @@ -0,0 +1 @@ +enable_service neutron-uplink-status-propagation diff --git a/neutron/tests/unit/db/test_db_base_plugin_v2.py b/neutron/tests/unit/db/test_db_base_plugin_v2.py index c8c0bf8f53f..e8f6802e259 100644 --- a/neutron/tests/unit/db/test_db_base_plugin_v2.py +++ b/neutron/tests/unit/db/test_db_base_plugin_v2.py @@ -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: diff --git a/neutron/tests/unit/extensions/test_uplink_status_propagation.py b/neutron/tests/unit/extensions/test_uplink_status_propagation.py new file mode 100644 index 00000000000..825b4f5f646 --- /dev/null +++ b/neutron/tests/unit/extensions/test_uplink_status_propagation.py @@ -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]) diff --git a/neutron/tests/unit/objects/port/extensions/test_uplink_status_propagation.py b/neutron/tests/unit/objects/port/extensions/test_uplink_status_propagation.py new file mode 100644 index 00000000000..87d078c2567 --- /dev/null +++ b/neutron/tests/unit/objects/port/extensions/test_uplink_status_propagation.py @@ -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()}) diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 4c9b8d66ecb..744cc002575 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -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', diff --git a/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_eswitch_manager.py b/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_eswitch_manager.py index 3e30e94a334..108d20adef3 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_eswitch_manager.py +++ b/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_eswitch_manager.py @@ -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." diff --git a/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_pci_lib.py b/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_pci_lib.py index feab1c8a37d..ad958d54822 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_pci_lib.py +++ b/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_pci_lib.py @@ -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, diff --git a/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_sriov_nic_agent.py b/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_sriov_nic_agent.py index 2b65f0823b3..48906f404f5 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_sriov_nic_agent.py +++ b/neutron/tests/unit/plugins/ml2/drivers/mech_sriov/agent/test_sriov_nic_agent.py @@ -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] diff --git a/neutron/tests/unit/plugins/ml2/extensions/test_uplink_status_propagation.py b/neutron/tests/unit/plugins/ml2/extensions/test_uplink_status_propagation.py new file mode 100644 index 00000000000..b010f922caa --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/extensions/test_uplink_status_propagation.py @@ -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']) diff --git a/releasenotes/notes/add-propagate_uplink_status-to-port-f4e53395e86eb3cb.yaml b/releasenotes/notes/add-propagate_uplink_status-to-port-f4e53395e86eb3cb.yaml new file mode 100644 index 00000000000..eb95bc0c077 --- /dev/null +++ b/releasenotes/notes/add-propagate_uplink_status-to-port-f4e53395e86eb3cb.yaml @@ -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. diff --git a/setup.cfg b/setup.cfg index 9a4380793c3..4bb20545573 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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