From 3ebdfe612a931a044877b2041a4ab68489bde5b7 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Sun, 2 Oct 2022 17:29:32 +0200 Subject: [PATCH] Support for minimum bandwidth rules in tunnelled networks This patch adds support for QoS minimum bandwidth rules in tunnelled networks. Now the ML2/OVS and ML2/OVN mechanism drivers can represent in the Placement API the available bandwidth of the tunnelled networks in each compute host. Both mechanism drivers represent the compute VTEP (VXLAN) or TEP (Geneve) interface as an IP address. This new resource provider (by default called "rp_tunnelled") represents the available bandwidth of this interface. Any new port created in a compute node that belongs to a tunnelled network, will request to the Placement API the corresponding bandwidth from the resource provider inventory. This patch does not provide backend enforcement support for minimum bandwidth rules. RFE spec: https://review.opendev.org/c/openstack/neutron-specs/+/860859 What is missing and will be added in next patches: * Tempest tests, that will be pushed to the corresponding repository. Depends-On: https://review.opendev.org/c/openstack/neutron-tempest-plugin/+/863880 Partial-Bug: #1991965 Related-Bug: #1578989 Change-Id: I3bfc2c0f9566bcc6861ca91339e32257ea92c7e9 --- doc/source/admin/config-qos-min-bw.rst | 33 +++++++-- neutron/agent/common/placement_report.py | 17 ++++- neutron/agent/common/utils.py | 20 ++--- neutron/common/_constants.py | 5 ++ neutron/common/utils.py | 9 ++- neutron/conf/plugins/ml2/config.py | 13 +++- .../agent/extension_drivers/qos_driver.py | 5 ++ .../openvswitch/agent/ovs_neutron_agent.py | 10 ++- .../mech_driver/ovsdb/extensions/placement.py | 20 ++++- .../qos/drivers/openvswitch/driver.py | 13 ---- neutron/services/qos/qos_plugin.py | 14 +++- neutron/tests/fullstack/test_qos.py | 36 ++++++--- neutron/tests/functional/agent/l2/base.py | 2 + .../agent/common/test_placement_report.py | 20 ++++- neutron/tests/unit/agent/common/test_utils.py | 30 ++++++++ .../extension_drivers/test_qos_driver.py | 29 +++++++- .../qos/drivers/openvswitch/test_driver.py | 18 +++-- .../unit/services/qos/test_qos_plugin.py | 74 ++++++++++++------- ...w-tunnelled-networks-8064d6f21f7d9267.yaml | 10 +++ 19 files changed, 281 insertions(+), 97 deletions(-) create mode 100644 releasenotes/notes/qos-minimum-bw-tunnelled-networks-8064d6f21f7d9267.yaml diff --git a/doc/source/admin/config-qos-min-bw.rst b/doc/source/admin/config-qos-min-bw.rst index fe0507a75a7..be3682f59cf 100644 --- a/doc/source/admin/config-qos-min-bw.rst +++ b/doc/source/admin/config-qos-min-bw.rst @@ -40,9 +40,6 @@ Limitations technical reasons (in this case the port is created too late for Neutron to affect scheduling). -* Bandwidth guarantees for ports can only be requested on networks - backed by a physical network (physnet). - * In Stein there is no support for networks with multiple physnets. However some simpler multi-segment networks are still supported: @@ -185,6 +182,13 @@ supported: by a ``direct-physical`` port. +Since 2023.1 (Antelope), Open vSwitch and OVN mechanism drivers can specify +the available bandwidth for tunnelled networks (SR-IOV does not support these +network types yet). The key "rp_tunnelled" is used to model those networks +that are not backed by a physical network. This bandwidth models the limits +of the VTEP/TEP interface used to send the tunnelled traffic (VXLAN, Geneve). + + neutron-server config ~~~~~~~~~~~~~~~~~~~~~ @@ -260,9 +264,20 @@ Valid values are all the [ovs] bridge_mappings = physnet0:br-physnet0,... - resource_provider_bandwidths = br-physnet0:10000000:10000000,... + resource_provider_bandwidths = br-physnet0:10000000:10000000,rp_tunnelled:20000000:20000000,... #resource_provider_inventory_defaults = step_size:1000,... + +.. note:: + + "rp_tunnelled" is not a bridge nor an interface present in the host. + The ML2/OVS agent will read the host local "resource_provider_bandwidths" + and will assign, by default, the "rp_tunnelled" resource provider to + the local host where is running. In other words, it is not needed to + populate "resource_provider_hypervisors" with the host assigned to this + specific resource provider. + + neutron-sriov-agent config ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -296,9 +311,9 @@ SR-IOV and OVS agents. This is how the values are registered: $ root@dev20:~# ovs-vsctl list Open_vSwitch ... external_ids : {hostname=dev20.fistro.com, \ - ovn-cms-options="resource_provider_bandwidths=br-ex:1001:2000;br-ex2:3000:4000, \ + ovn-cms-options="resource_provider_bandwidths=br-ex:1001:2000;br-ex2:3000:4000;rp_tunnelled:5000:6000, \ resource_provider_inventory_defaults=allocation_ratio:1.0;min_unit:10, \ - resource_provider_hypervisors=br-ex:dev20.fistro.com;br-ex2:dev20.fistro.com", \ + resource_provider_hypervisors=br-ex:dev20.fistro.com;br-ex2:dev20.fistro.com;rp_tunnelled:dev20.fistro.com", \ rundir="/var/run/openvswitch", \ system-id="029e7d3d-d2ab-4f2c-bc92-ec58c94a8fc1"} ... @@ -354,7 +369,9 @@ queue periodically. $ openstack network agent show -f value -c configuration 5e57b85f-b017-419a-8745-9c406e149f9e {'bridge_mappings': {'physnet0': 'br-physnet0'}, 'resource_provider_bandwidths': {'br-physnet0': {'egress': 10000000, - 'ingress': 10000000}}, + 'ingress': 10000000} + 'rp_tunnelled': {'egress': 20000000, + 'ingress': 20000000}}, 'resource_provider_inventory_defaults': {'allocation_ratio': 1.0, 'min_unit': 1, 'reserved': 0, @@ -578,6 +595,7 @@ Please find an example in section `Propagation of resource information`_. | 1c7e83f0-108d-5c35-ada7-7ebebbe43aad | devstack0:NIC Switch agent:ens5 | 2 | 3b36d91e-bf60-460f-b1f8-3322dee5cdfd | 4a8a819d-61f9-5822-8c5c-3e9c7cb942d6 | | 89ca1421-5117-5348-acab-6d0e2054239c | devstack0:Open vSwitch agent | 0 | 3b36d91e-bf60-460f-b1f8-3322dee5cdfd | 3b36d91e-bf60-460f-b1f8-3322dee5cdfd | | f9c9ce07-679d-5d72-ac5f-31720811629a | devstack0:Open vSwitch agent:br-physnet0 | 2 | 3b36d91e-bf60-460f-b1f8-3322dee5cdfd | 89ca1421-5117-5348-acab-6d0e2054239c | + | 521f53a6-c8c0-583c-98da-7a47f39ff887 | devstack0:Open vSwitch agent:rp_tunnelled| 2 | 3b36d91e-bf60-460f-b1f8-3322dee5cdfd | 89ca1421-5117-5348-acab-6d0e2054239c | +--------------------------------------+------------------------------------------+------------+--------------------------------------+--------------------------------------+ * Does Placement have the expected traits? @@ -587,6 +605,7 @@ Please find an example in section `Propagation of resource information`_. # as admin $ openstack --os-placement-api-version 1.17 trait list | awk '/CUSTOM_/ { print $2 }' | sort CUSTOM_PHYSNET_PHYSNET0 + CUSTOM_TUNNELLED_NETWORKS CUSTOM_VNIC_TYPE_DIRECT CUSTOM_VNIC_TYPE_DIRECT_PHYSICAL CUSTOM_VNIC_TYPE_MACVTAP diff --git a/neutron/agent/common/placement_report.py b/neutron/agent/common/placement_report.py index 30d4b812af8..103a23b949b 100644 --- a/neutron/agent/common/placement_report.py +++ b/neutron/agent/common/placement_report.py @@ -15,8 +15,12 @@ from neutron_lib import constants as nlib_const from neutron_lib.placement import utils as place_utils import os_resource_classes as orc +from oslo_config import cfg from oslo_log import log as logging +from neutron.common import _constants as n_const + + LOG = logging.getLogger(__name__) @@ -99,6 +103,7 @@ class PlacementState(object): self._device_mappings = device_mappings self._supported_vnic_types = supported_vnic_types self._client = client + self._rp_tun_name = cfg.CONF.ml2.tunnelled_network_rp_name def _deferred_update_physnet_traits(self): traits = [] @@ -111,6 +116,10 @@ class PlacementState(object): name=place_utils.physnet_trait(physnet))) return traits + def _deferred_update_tunnelled_traits(self): + return [DeferredCall(self._client.update_trait, + name=n_const.TRAIT_NETWORK_TUNNEL)] + def _deferred_update_vnic_type_traits(self): traits = [] for vnic_type in self._supported_vnic_types: @@ -123,6 +132,7 @@ class PlacementState(object): def deferred_update_traits(self): traits = [] traits += self._deferred_update_physnet_traits() + traits += self._deferred_update_tunnelled_traits() traits += self._deferred_update_vnic_type_traits() return traits @@ -196,7 +206,8 @@ class PlacementState(object): def deferred_update_resource_provider_traits(self): rp_traits = [] - + tunnelled_trait_mappings = { + self._rp_tun_name: n_const.TRAIT_NETWORK_TUNNEL} physnet_trait_mappings = {} for physnet, devices in self._device_mappings.items(): for device in devices: @@ -210,8 +221,8 @@ class PlacementState(object): self._driver_uuid_namespace, self._hypervisor_rps[device]['name'], device) - traits = [] - traits.append(physnet_trait_mappings[device]) + traits = [physnet_trait_mappings.get(device) or + tunnelled_trait_mappings[device]] traits.extend(vnic_type_traits) rp_traits.append( DeferredCall( diff --git a/neutron/agent/common/utils.py b/neutron/agent/common/utils.py index f2cc9fb32f4..d2181d245e4 100644 --- a/neutron/agent/common/utils.py +++ b/neutron/agent/common/utils.py @@ -135,7 +135,8 @@ def get_hypervisor_hostname(): # TODO(bence romsics): rehome this to neutron_lib.placement.utils def default_rp_hypervisors(hypervisors, device_mappings, - default_hypervisor=None): + default_hypervisor=None, + tunnelled_network_rp_name=None): """Fill config option 'resource_provider_hypervisors' with defaults. :param hypervisors: Config option 'resource_provider_hypervisors' @@ -145,14 +146,13 @@ def default_rp_hypervisors(hypervisors, device_mappings, format. :param default_hypervisor: Default hypervisor hostname. If not set, it tries to default to fully qualified domain name (fqdn) + :param tunnelled_network_rp_name: the resource provider name for tunnelled + networks; if present, it will be added to the devices list. """ _default_hypervisor = default_hypervisor or get_hypervisor_hostname() - - rv = {} - for _physnet, devices in device_mappings.items(): - for device in devices: - if device in hypervisors: - rv[device] = hypervisors[device] - else: - rv[device] = _default_hypervisor - return rv + # device_mappings = {'physnet1': ['br-phy1'], 'physnet2': ['br-phy2'], ...} + devices = {dev for devs in device_mappings.values() for dev in devs} + if tunnelled_network_rp_name: + devices.add(tunnelled_network_rp_name) + return {device: hypervisors.get(device) or _default_hypervisor + for device in devices} diff --git a/neutron/common/_constants.py b/neutron/common/_constants.py index 5b8c1a46be9..97aea68a12f 100644 --- a/neutron/common/_constants.py +++ b/neutron/common/_constants.py @@ -78,3 +78,8 @@ IDPOOL_SELECT_SIZE = 100 AUTO_DELETE_PORT_OWNERS = [constants.DEVICE_OWNER_DHCP, constants.DEVICE_OWNER_DISTRIBUTED, constants.DEVICE_OWNER_AGENT_GW] + +# TODO(ralonsoh): move this constant to neutron_lib.placement.constants +# Tunnelled networks resource provider default name. +RP_TUNNELLED = 'rp_tunnelled' +TRAIT_NETWORK_TUNNEL = 'CUSTOM_NETWORK_TUNNEL_PROVIDER' diff --git a/neutron/common/utils.py b/neutron/common/utils.py index 078a47d8f26..27802ff7cab 100644 --- a/neutron/common/utils.py +++ b/neutron/common/utils.py @@ -864,7 +864,8 @@ def port_ip_changed(new_port, original_port): return False -def validate_rp_bandwidth(rp_bandwidths, device_names): +def validate_rp_bandwidth(rp_bandwidths, device_names, + tunnelled_network_rp_name=None): """Validate resource provider bandwidths against device names. :param rp_bandwidths: Dict containing resource provider bandwidths, @@ -873,10 +874,14 @@ def validate_rp_bandwidth(rp_bandwidths, device_names): :param device_names: A set of the device names given in bridge_mappings in case of ovs-agent or in physical_device_mappings in case of sriov-agent + :param tunnelled_network_rp_name: the resource provider name for tunnelled + networks; if present, it will be added + to the devices list. :raises ValueError: In case of the devices (keys) in the rp_bandwidths dict are not in the device_names set. """ - + if tunnelled_network_rp_name: + device_names.add(tunnelled_network_rp_name) for dev_name in rp_bandwidths: if dev_name not in device_names: raise ValueError(_( diff --git a/neutron/conf/plugins/ml2/config.py b/neutron/conf/plugins/ml2/config.py index b82083acf29..78a4feb58d2 100644 --- a/neutron/conf/plugins/ml2/config.py +++ b/neutron/conf/plugins/ml2/config.py @@ -16,6 +16,8 @@ from oslo_config import cfg from neutron._i18n import _ +from neutron.common import _constants as common_const + ml2_opts = [ cfg.ListOpt('type_drivers', @@ -65,7 +67,16 @@ ml2_opts = [ cfg.IntOpt('overlay_ip_version', default=4, help=_("IP version of all overlay (tunnel) network endpoints. " - "Use a value of 4 for IPv4 or 6 for IPv6.")) + "Use a value of 4 for IPv4 or 6 for IPv6.")), + cfg.StrOpt('tunnelled_network_rp_name', + default=common_const.RP_TUNNELLED, + help=_("Resource provider name for the host with tunnelled " + "networks. This resource provider represents the " + "available bandwidth for all tunnelled networks in a " + "compute node. NOTE: this parameter is used both by the " + "Neutron server and the mechanism driver agents; it is " + "recommended not to change it once any resource " + "provider register has been created.")), ] diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/extension_drivers/qos_driver.py b/neutron/plugins/ml2/drivers/openvswitch/agent/extension_drivers/qos_driver.py index bc7815e74c0..68c03139bf5 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/extension_drivers/qos_driver.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/extension_drivers/qos_driver.py @@ -432,6 +432,11 @@ class QosOVSAgentDriver(qos.QosLinuxAgentDriver, 'vif_port was not found. It seems that port is already ' 'deleted', port.get('port_id')) return + elif not port.get('physical_network'): + LOG.debug('update_minimum_bandwidth was received for port %s but ' + 'has no physical network associated', + port.get('port_id')) + return self.ports[port['port_id']][(qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH, rule.direction)] = port diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py index 95943c6be27..6e264f0e58a 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py @@ -62,6 +62,7 @@ from neutron.api.rpc.handlers import securitygroups_rpc as sg_rpc from neutron.common import config from neutron.common import utils as n_utils from neutron.conf.agent import common as agent_config +from neutron.conf.plugins.ml2 import config as ml2_config from neutron.conf import service as service_conf from neutron.plugins.ml2.drivers.agent import capabilities from neutron.plugins.ml2.drivers.l2pop.rpc_manager import l2population_rpc @@ -233,8 +234,9 @@ class OVSNeutronAgent(l2population_rpc.L2populationRpcCallBackTunnelMixin, self._validate_rp_pkt_processing_cfg() br_set = set(self.bridge_mappings.values()) - n_utils.validate_rp_bandwidth(self.rp_bandwidths, - br_set) + n_utils.validate_rp_bandwidth( + self.rp_bandwidths, br_set, + tunnelled_network_rp_name=self.conf.ml2.tunnelled_network_rp_name) self.rp_inventory_defaults = place_utils.parse_rp_inventory_defaults( ovs_conf.resource_provider_inventory_defaults) # At the moment the format of @@ -250,7 +252,8 @@ class OVSNeutronAgent(l2population_rpc.L2populationRpcCallBackTunnelMixin, self.rp_hypervisors = utils.default_rp_hypervisors( ovs_conf.resource_provider_hypervisors, {k: [v] for k, v in self.bridge_mappings.items()}, - ovs_conf.resource_provider_default_hypervisor + default_hypervisor=ovs_conf.resource_provider_default_hypervisor, + tunnelled_network_rp_name=self.conf.ml2.tunnelled_network_rp_name, ) self.phys_brs = {} @@ -2925,6 +2928,7 @@ def main(bridge_classes): l2_agent_extensions_manager.register_opts(cfg.CONF) agent_config.setup_privsep() service_conf.register_service_opts(service_conf.RPC_EXTRA_OPTS, cfg.CONF) + ml2_config.register_ml2_plugin_opts(cfg=cfg.CONF) ext_mgr = l2_agent_extensions_manager.L2AgentExtensionsManager(cfg.CONF) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py index ddf48b4808b..4b835a45de5 100644 --- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/extensions/placement.py @@ -21,6 +21,7 @@ from neutron_lib.placement import utils as placement_utils from neutron_lib.plugins import constants as plugins_constants from neutron_lib.plugins import directory from neutron_lib.utils import helpers +from oslo_config import cfg from oslo_log import log as logging from ovsdbapp.backend.ovs_idl import event as row_event @@ -174,6 +175,7 @@ class OVNClientPlacementExtension(object): self._plugin = None self.uuid_ns = ovn_const.OVN_RP_UUID self.supported_vnic_types = ovn_const.OVN_SUPPORTED_VNIC_TYPES + self._rp_tun_name = cfg.CONF.ml2.tunnelled_network_rp_name @property def placement_plugin(self): @@ -247,6 +249,13 @@ class OVNClientPlacementExtension(object): LOG.debug('Building placement options for chassis %s: %s', chassis.name, cms_options) hypervisor_rps = {} + + # ML2/OVN can also track tunnelled networks bandwidth. The key + # RP_TUNNELLED must be defined in "resource_provider_bandwidths" and + # "resource_provider_hypervisors". E.g.: + # ovn-cms-options = + # resource_provider_bandwidths=br-ex:100:200;rp_tunnelled:300:400 + # resource_provider_hypervisors=br-ex:host1,rp_tunnelled:host1 for device, hyperv in cms_options[ovn_const.RP_HYPERVISORS].items(): try: hypervisor_rps[device] = {'name': hyperv, @@ -254,7 +263,12 @@ class OVNClientPlacementExtension(object): except (KeyError, AttributeError): continue - bridges = set(itertools.chain(*bridge_mappings.values())) + rp_devices = set(itertools.chain(*bridge_mappings.values())) + # If "ml2.tunnelled_network_rp_name" is present in configured resource + # providers, that means this ML2/OVN host will track the tunnelled + # networks available bandwidth. + if self._rp_tun_name in hypervisor_rps: + rp_devices.add(self._rp_tun_name) # Remove "cms_options[RP_BANDWIDTHS]" not present in "hypervisor_rps" # and "bridge_mappings". If we don't have a way to match the RP bridge # with a host ("hypervisor_rps") or a way to match the RP bridge with @@ -262,8 +276,8 @@ class OVNClientPlacementExtension(object): rp_bw = cms_options[n_const.RP_BANDWIDTHS] if rp_bw: cms_options[n_const.RP_BANDWIDTHS] = { - device: bw for device, bw in rp_bw.items() if - device in hypervisor_rps and device in bridges} + rp_device: bw for rp_device, bw in rp_bw.items() if + rp_device in hypervisor_rps and rp_device in rp_devices} # NOTE(ralonsoh): OVN only reports min BW RPs; packet processing RPs # will be added in a future implementation. If no RP_BANDWIDTHS values diff --git a/neutron/services/qos/drivers/openvswitch/driver.py b/neutron/services/qos/drivers/openvswitch/driver.py index 18d2db835c4..b3214b9fdaa 100644 --- a/neutron/services/qos/drivers/openvswitch/driver.py +++ b/neutron/services/qos/drivers/openvswitch/driver.py @@ -20,8 +20,6 @@ from neutron_lib.services.qos import base from neutron_lib.services.qos import constants as qos_consts from oslo_log import log as logging -from neutron.objects import network as network_object - LOG = logging.getLogger(__name__) @@ -76,17 +74,6 @@ class OVSDriver(base.DriverBase): def validate_rule_for_port(self, context, rule, port): return self.validate_rule_for_network(context, rule, port.network_id) - def validate_rule_for_network(self, context, rule, network_id): - # Minimum-bandwidth rule is only supported on networks whose - # first segment is backed by a physnet. - if rule.rule_type == qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH: - net = network_object.Network.get_object( - context, id=network_id) - physnet = net.segments[0].physical_network - if physnet is None: - return False - return True - def register(): """Register the driver.""" diff --git a/neutron/services/qos/qos_plugin.py b/neutron/services/qos/qos_plugin.py index 28331ee4f07..776dae6da23 100644 --- a/neutron/services/qos/qos_plugin.py +++ b/neutron/services/qos/qos_plugin.py @@ -49,6 +49,7 @@ from oslo_config import cfg from oslo_log import log as logging from neutron._i18n import _ +from neutron.common import _constants as n_const from neutron.db import db_base_plugin_common from neutron.exceptions import qos as neutron_qos_exc from neutron.extensions import qos @@ -254,17 +255,22 @@ class QoSPlugin(qos.QoSPluginBase): # support will be available. See Placement spec: # https://review.opendev.org/565730 first_segment = segments[0] - if not first_segment or not first_segment.physical_network: + if not first_segment: return [] - physnet_trait = pl_utils.physnet_trait( - first_segment.physical_network) + elif not first_segment.physical_network: + # If there is no physical network this is because this is an + # overlay network (tunnelled network). + net_trait = n_const.TRAIT_NETWORK_TUNNEL + else: + net_trait = pl_utils.physnet_trait(first_segment.physical_network) + # NOTE(ralonsoh): we should not rely on the current execution order of # the port extending functions. Although here we have # port_res[VNIC_TYPE], we should retrieve this value from the port DB # object instead. vnic_trait = pl_utils.vnic_type_trait(vnic_type) - return [physnet_trait, vnic_trait] + return [net_trait, vnic_trait] @staticmethod @resource_extend.extends([port_def.COLLECTION_NAME_BULK]) diff --git a/neutron/tests/fullstack/test_qos.py b/neutron/tests/fullstack/test_qos.py index 0a09f605a46..52fc0fb4e30 100644 --- a/neutron/tests/fullstack/test_qos.py +++ b/neutron/tests/fullstack/test_qos.py @@ -825,30 +825,42 @@ class TestMinBwQoSOvs(_TestMinBwQoS, base.BaseFullStackTestCase): qoses, queues = self._qos_info(vm.bridge) self.fail(queuenum + qoses + queues) - def test_min_bw_qos_create_network_vxlan_not_supported(self): + def test_min_bw_qos_create_network_vxlan_supported(self): qos_policy = self._create_qos_policy() qos_policy_id = qos_policy['id'] self.safe_client.create_minimum_bandwidth_rule( self.tenant_id, qos_policy_id, MIN_BANDWIDTH, self.direction) network_args = {'network_type': 'vxlan', 'qos_policy_id': qos_policy_id} - self.assertRaises( - exceptions.Conflict, - self.safe_client.create_network, + net = self.safe_client.create_network( self.tenant_id, name='network-test', **network_args) + self.assertEqual(qos_policy_id, net['qos_policy_id']) - def test_min_bw_qos_update_network_vxlan_not_supported(self): - network_args = {'network_type': 'vxlan'} - network = self.safe_client.create_network( - self.tenant_id, name='network-test', **network_args) + def test_min_bw_qos_create_and_update_network_vxlan_supported(self): qos_policy = self._create_qos_policy() qos_policy_id = qos_policy['id'] self.safe_client.create_minimum_bandwidth_rule( self.tenant_id, qos_policy_id, MIN_BANDWIDTH, self.direction) - self.assertRaises( - exceptions.Conflict, - self.client.update_network, network['id'], - body={'network': {'qos_policy_id': qos_policy_id}}) + network_args = {'network_type': 'vxlan', + 'qos_policy_id': qos_policy_id} + network = self.safe_client.create_network( + self.tenant_id, name='network-test', **network_args) + self.assertEqual(qos_policy_id, network['qos_policy_id']) + + qos_policy2 = self._create_qos_policy() + qos_policy2_id = qos_policy2['id'] + self.client.update_network( + network['id'], body={'network': {'qos_policy_id': qos_policy2_id}}) + _net = self.client.show_network(network['id']) + self.assertEqual(qos_policy2_id, _net['network']['qos_policy_id']) + + # This action will remove the QoS policy from the network. This is also + # necessary before the cleanUp call, that will delete the QoS policy + # before the network. + self.client.update_network( + network['id'], body={'network': {'qos_policy_id': None}}) + _net = self.client.show_network(network['id']) + self.assertIsNone(_net['network']['qos_policy_id']) def test_min_bw_qos_port_removed(self): """Test if min BW limit config is properly removed when port removed. diff --git a/neutron/tests/functional/agent/l2/base.py b/neutron/tests/functional/agent/l2/base.py index a17c9bb167e..d62ce22e972 100644 --- a/neutron/tests/functional/agent/l2/base.py +++ b/neutron/tests/functional/agent/l2/base.py @@ -33,6 +33,7 @@ from neutron.common import utils from neutron.conf.agent import common as agent_config from neutron.conf.agent import ovs_conf as ovs_agent_config from neutron.conf import common as common_config +from neutron.conf.plugins.ml2 import config as ml2_config from neutron.conf.plugins.ml2.drivers import agent from neutron.conf.plugins.ml2.drivers import ovs_conf from neutron.plugins.ml2.drivers.openvswitch.agent.extension_drivers \ @@ -140,6 +141,7 @@ class OVSAgentTestFramework(base.BaseOVSLinuxTestCase, OVSOFControllerHelper): agent_config.register_agent_state_opts_helper(config) ovs_agent_config.register_ovs_agent_opts(config) ext_manager.register_opts(config) + ml2_config.register_ml2_plugin_opts(cfg=config) return config def _configure_agent(self): diff --git a/neutron/tests/unit/agent/common/test_placement_report.py b/neutron/tests/unit/agent/common/test_placement_report.py index 858a68759dc..26c41de20b5 100644 --- a/neutron/tests/unit/agent/common/test_placement_report.py +++ b/neutron/tests/unit/agent/common/test_placement_report.py @@ -16,6 +16,8 @@ from unittest import mock import uuid from neutron.agent.common import placement_report +from neutron.common import _constants as n_const +from neutron.conf.plugins.ml2 import config as ml2_config from neutron.tests import base @@ -43,6 +45,7 @@ class DeferredCallTestCase(base.BaseTestCase): class PlacementStateTestCase(base.BaseTestCase): def setUp(self): + ml2_config.register_ml2_plugin_opts() super(PlacementStateTestCase, self).setUp() self.client_mock = mock.Mock() self.driver_uuid_namespace = uuid.UUID( @@ -61,6 +64,10 @@ class PlacementStateTestCase(base.BaseTestCase): 'hypervisor_rps': { 'eth0': {'name': 'fakehost', 'uuid': self.hypervisor1_rp_uuid}, 'eth1': {'name': 'fakehost', 'uuid': self.hypervisor1_rp_uuid}, + # NOTE(ralonsoh): use the 'rp_tunnelled' n-lib constant once + # merged. + 'rp_tunnelled': {'name': 'fakehost', + 'uuid': self.hypervisor1_rp_uuid}, }, 'device_mappings': {}, 'supported_vnic_types': [], @@ -195,6 +202,7 @@ class PlacementStateTestCase(base.BaseTestCase): }, 'rp_bandwidths': { 'eth0': {'egress': 1, 'ingress': 1}, + 'rp_tunnelled': {'egress': 2, 'ingress': 3}, }, 'supported_vnic_types': ['normal'], }) @@ -211,6 +219,13 @@ class PlacementStateTestCase(base.BaseTestCase): '1ea6f823-bcf2-5dc5-9bee-4ee6177a6451'), traits=mock.ANY), + # uuid -v5 '00000000-0000-0000-0000-000000000001' \ + # 'fakehost:rp_tunnelled' + mock.call( + resource_provider_uuid=uuid.UUID( + '357001cb-88b4-5e1d-ae6e-85b238a7a83e'), + traits=mock.ANY), + # uuid -v5 '00000000-0000-0000-0000-000000000001' 'fakehost' mock.call( resource_provider_uuid=uuid.UUID( @@ -223,8 +238,9 @@ class PlacementStateTestCase(base.BaseTestCase): actual_traits = [set(args[1]['traits']) for args in self.client_mock.update_resource_provider_traits.call_args_list] self.assertEqual( - [set(['CUSTOM_PHYSNET_PHYSNET0', 'CUSTOM_VNIC_TYPE_NORMAL']), - set(['CUSTOM_VNIC_TYPE_NORMAL'])], + [{'CUSTOM_PHYSNET_PHYSNET0', 'CUSTOM_VNIC_TYPE_NORMAL'}, + {n_const.TRAIT_NETWORK_TUNNEL, 'CUSTOM_VNIC_TYPE_NORMAL'}, + {'CUSTOM_VNIC_TYPE_NORMAL'}], actual_traits) def test_deferred_update_resource_provider_inventories_bw(self): diff --git a/neutron/tests/unit/agent/common/test_utils.py b/neutron/tests/unit/agent/common/test_utils.py index dc6db966f11..51dd5a9d785 100644 --- a/neutron/tests/unit/agent/common/test_utils.py +++ b/neutron/tests/unit/agent/common/test_utils.py @@ -16,9 +16,12 @@ import socket from unittest import mock +from oslo_config import cfg + from neutron.agent.common import utils from neutron.agent.linux import interface from neutron.conf.agent import common as config +from neutron.conf.plugins.ml2 import config as ml2_config from neutron.tests import base from neutron.tests.unit import testlib_api @@ -158,6 +161,10 @@ class TestGetHypervisorHostname(base.BaseTestCase): # TODO(bence romsics): rehome this to neutron_lib class TestDefaultRpHypervisors(base.BaseTestCase): + def setUp(self): + super().setUp() + ml2_config.register_ml2_plugin_opts() + @mock.patch.object(utils, 'get_hypervisor_hostname', return_value='thishost') def test_defaults(self, hostname_mock): @@ -197,3 +204,26 @@ class TestDefaultRpHypervisors(base.BaseTestCase): default_hypervisor='defaulthost', ) ) + + rp_tunnelled = cfg.CONF.ml2.tunnelled_network_rp_name + self.assertEqual( + {'eth0': 'thathost', 'eth1': 'defaulthost', + rp_tunnelled: 'defaulthost'}, + utils.default_rp_hypervisors( + hypervisors={'eth0': 'thathost'}, + device_mappings={'physnet0': ['eth0', 'eth1']}, + default_hypervisor='defaulthost', + tunnelled_network_rp_name=rp_tunnelled + ) + ) + + self.assertEqual( + {'eth0': 'thathost', 'eth1': 'defaulthost', + rp_tunnelled: 'thathost'}, + utils.default_rp_hypervisors( + hypervisors={'eth0': 'thathost', rp_tunnelled: 'thathost'}, + device_mappings={'physnet0': ['eth0', 'eth1']}, + default_hypervisor='defaulthost', + tunnelled_network_rp_name=rp_tunnelled + ) + ) diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/extension_drivers/test_qos_driver.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/extension_drivers/test_qos_driver.py index e5a1a8cbfd0..dca207633da 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/extension_drivers/test_qos_driver.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/extension_drivers/test_qos_driver.py @@ -386,19 +386,39 @@ class QosOVSAgentDriverTestCase(ovs_test_base.OVSAgentConfigTestBase): self.qos_driver.delete_minimum_bandwidth({'port_id': 'p_id'}) mock_delete_minimum_bandwidth_queue.assert_called_once_with('p_id') - def test_update_minimum_bandwidth_no_vif_port(self): + @mock.patch.object(qos_driver, 'LOG') + def test_update_minimum_bandwidth_no_vif_port(self, mock_log): with mock.patch.object(self.qos_driver.br_int, 'update_minimum_bandwidth_queue') \ as mock_delete_minimum_bandwidth_queue: - self.qos_driver.update_minimum_bandwidth({}, mock.ANY) + self.qos_driver.update_minimum_bandwidth( + {'port_id': 'portid'}, mock.ANY) mock_delete_minimum_bandwidth_queue.assert_not_called() + mock_log.debug.assert_called_once_with( + 'update_minimum_bandwidth was received for port %s but ' + 'vif_port was not found. It seems that port is already ' + 'deleted', 'portid') + + @mock.patch.object(qos_driver, 'LOG') + def test_update_minimum_bandwidth_no_physical_network(self, mock_log): + with mock.patch.object(self.qos_driver.br_int, + 'update_minimum_bandwidth_queue') \ + as mock_delete_minimum_bandwidth_queue: + port = {'vif_port': mock.ANY, 'port_id': 'portid', + 'physical_network': None} + self.qos_driver.update_minimum_bandwidth(port, mock.ANY) + mock_delete_minimum_bandwidth_queue.assert_not_called() + mock_log.debug.assert_called_once_with( + 'update_minimum_bandwidth was received for port %s but ' + 'has no physical network associated', 'portid') def test_update_minimum_bandwidth_no_phy_brs(self): vif_port = mock.Mock() vif_port.ofport = 'ofport' rule = mock.Mock() rule.min_kbps = 1500 - port = {'port_id': 'port_id', 'vif_port': vif_port} + port = {'port_id': 'port_id', 'vif_port': vif_port, + 'physical_network': mock.ANY} with mock.patch.object(self.qos_driver.br_int, 'update_minimum_bandwidth_queue') \ as mock_delete_minimum_bandwidth_queue, \ @@ -413,7 +433,8 @@ class QosOVSAgentDriverTestCase(ovs_test_base.OVSAgentConfigTestBase): vif_port.ofport = 'ofport' rule = mock.Mock() rule.min_kbps = 1500 - port = {'port_id': 'port_id', 'vif_port': vif_port} + port = {'port_id': 'port_id', 'vif_port': vif_port, + 'physical_network': mock.ANY} with mock.patch.object(self.qos_driver.br_int, 'update_minimum_bandwidth_queue') \ as mock_delete_minimum_bandwidth_queue, \ diff --git a/neutron/tests/unit/services/qos/drivers/openvswitch/test_driver.py b/neutron/tests/unit/services/qos/drivers/openvswitch/test_driver.py index 6345cb82608..09ac0a060fa 100644 --- a/neutron/tests/unit/services/qos/drivers/openvswitch/test_driver.py +++ b/neutron/tests/unit/services/qos/drivers/openvswitch/test_driver.py @@ -27,12 +27,14 @@ class TestOVSDriver(base.BaseQosTestCase): super(TestOVSDriver, self).setUp() self.driver = driver.OVSDriver.create() - def test_validate_min_bw_rule_vs_physnet_non_physnet(self): - scenarios = [ - ({'physical_network': 'fake physnet'}, self.assertTrue), - ({}, self.assertFalse), - ] - for segment_kwargs, test_method in scenarios: + def test_validate_min_bw_rule(self): + # Minimum bandwidth rules are now allowed for tunnelled networks since + # LP#1991965. The ML2/OVS backend cannot enforce them but Placement can + # schedule a VM using this information. + scenarios = [{'physical_network': 'fake physnet'}, + {}, + ] + for segment_kwargs in scenarios: segment = network_object.NetworkSegment(**segment_kwargs) net = network_object.Network(mock.Mock(), segments=[segment]) rule = mock.Mock() @@ -41,7 +43,7 @@ class TestOVSDriver(base.BaseQosTestCase): with mock.patch( 'neutron.objects.network.Network.get_object', return_value=net): - test_method(self.driver.validate_rule_for_port( + self.assertTrue(self.driver.validate_rule_for_port( mock.Mock(), rule, port)) - test_method(self.driver.validate_rule_for_network( + self.assertTrue(self.driver.validate_rule_for_network( mock.Mock(), rule, network_id=mock.Mock())) diff --git a/neutron/tests/unit/services/qos/test_qos_plugin.py b/neutron/tests/unit/services/qos/test_qos_plugin.py index b9ef242e4a5..875712f172c 100644 --- a/neutron/tests/unit/services/qos/test_qos_plugin.py +++ b/neutron/tests/unit/services/qos/test_qos_plugin.py @@ -15,6 +15,7 @@ from unittest import mock from keystoneauth1 import exceptions as ks_exc import netaddr +from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import qos from neutron_lib.callbacks import events from neutron_lib import constants as lib_constants @@ -23,6 +24,7 @@ from neutron_lib import exceptions as lib_exc from neutron_lib.exceptions import placement as pl_exc from neutron_lib.exceptions import qos as qos_exc from neutron_lib.objects import utils as obj_utils +from neutron_lib.placement import utils as pl_utils from neutron_lib.plugins import constants as plugins_constants from neutron_lib.plugins import directory from neutron_lib.services.qos import constants as qos_consts @@ -32,6 +34,7 @@ from oslo_config import cfg from oslo_utils import uuidutils import webob.exc +from neutron.common import _constants as n_const from neutron.exceptions import qos as neutron_qos_exc from neutron.extensions import qos_pps_minimum_rule_alias from neutron.extensions import qos_rules_alias @@ -83,6 +86,7 @@ class TestQosPlugin(base.BaseQosTestCase): self.ctxt = context.Context('fake_user', 'fake_tenant') self.admin_ctxt = context.get_admin_context() + self.default_uuid = 'fake_uuid' self.policy_data = { 'policy': {'id': uuidutils.generate_uuid(), @@ -129,6 +133,9 @@ class TestQosPlugin(base.BaseQosTestCase): self.min_pps_rule = rule_object.QosMinimumPacketRateRule( self.ctxt, **self.rule_data['minimum_packet_rate_rule']) + self._rp_tun_name = cfg.CONF.ml2.tunnelled_network_rp_name + self._rp_tun_trait = n_const.TRAIT_NETWORK_TUNNEL + def _validate_driver_params(self, method_name, ctxt): call_args = self.qos_plugin.driver_manager.call.call_args[0] self.assertTrue(self.qos_plugin.driver_manager.call.called) @@ -172,7 +179,7 @@ class TestQosPlugin(base.BaseQosTestCase): return_value=min_pps_rules), \ mock.patch( 'uuid.uuid5', - return_value='fake_uuid', + return_value=self.default_uuid, side_effect=request_groups_uuids): return qos_plugin.QoSPlugin._extend_port_resource_request( port_res, self.port) @@ -333,34 +340,33 @@ class TestQosPlugin(base.BaseQosTestCase): port = self._create_and_extend_port([self.min_bw_rule], physical_network=None) - self.assertIsNone(port.get('resource_request')) + expected = { + 'request_groups': [{'id': self.default_uuid, + 'required': [self._rp_tun_trait, + 'CUSTOM_VNIC_TYPE_NORMAL'], + 'resources': { + orc.NET_BW_EGR_KILOBIT_PER_SEC: 10}}], + 'same_subtree': [self.default_uuid]} + self.assertEqual(expected, port['resource_request']) def test__extend_port_resource_request_mix_rules_non_provider_net(self): self.min_bw_rule.direction = lib_constants.EGRESS_DIRECTION - port = self._create_and_extend_port([self.min_bw_rule], - [self.min_pps_rule], - physical_network=None) - self.assertEqual( - 1, - len(port['resource_request']['request_groups']) - ) - self.assertEqual( - 'fake_uuid', - port['resource_request']['request_groups'][0]['id'] - ) - self.assertEqual( - ['CUSTOM_VNIC_TYPE_NORMAL'], - port['resource_request']['request_groups'][0]['required'] - ) - self.assertEqual( - {orc.NET_PACKET_RATE_KILOPACKET_PER_SEC: 10}, - port['resource_request']['request_groups'][0]['resources'], - ) - self.assertEqual( - ['fake_uuid'], - port['resource_request']['same_subtree'], - ) + port = self._create_and_extend_port( + [self.min_bw_rule], [self.min_pps_rule], physical_network=None, + request_groups_uuids=['fake_uuid0', 'fake_uuid1']) + request_groups = [ + {'id': 'fake_uuid0', + 'required': [self._rp_tun_trait, + 'CUSTOM_VNIC_TYPE_NORMAL'], + 'resources': {orc.NET_BW_EGR_KILOBIT_PER_SEC: 10}}, + {'id': 'fake_uuid1', + 'required': ['CUSTOM_VNIC_TYPE_NORMAL'], + 'resources': {orc.NET_PACKET_RATE_KILOPACKET_PER_SEC: 10}}] + expected = { + 'request_groups': request_groups, + 'same_subtree': ['fake_uuid0', 'fake_uuid1']} + self.assertEqual(expected, port['resource_request']) def test__extend_port_resource_request_bulk_min_bw_rule(self): self.min_bw_rule.direction = lib_constants.EGRESS_DIRECTION @@ -1844,6 +1850,24 @@ class TestQosPlugin(base.BaseQosTestCase): self.qos_plugin.get_rule_type, self.ctxt, qos_consts.RULE_TYPE_MINIMUM_PACKET_RATE) + def test__get_min_bw_traits(self): + vnic_type = portbindings.VNIC_NORMAL + segments = [None] + ret = self.qos_plugin._get_min_bw_traits(vnic_type, segments) + self.assertEqual([], ret) + + segments = [mock.Mock(physical_network=None)] + ret = self.qos_plugin._get_min_bw_traits(vnic_type, segments) + # NOTE(ralonsoh): once implemented, use the neutron-lib method to + # generate the tunnelled networks trait. + self.assertEqual([self._rp_tun_trait, + pl_utils.vnic_type_trait(vnic_type)], ret) + + segments = [mock.Mock(physical_network='physnet_1')] + ret = self.qos_plugin._get_min_bw_traits(vnic_type, segments) + self.assertEqual([pl_utils.physnet_trait('physnet_1'), + pl_utils.vnic_type_trait(vnic_type)], ret) + class QoSRuleAliasTestExtensionManager(object): diff --git a/releasenotes/notes/qos-minimum-bw-tunnelled-networks-8064d6f21f7d9267.yaml b/releasenotes/notes/qos-minimum-bw-tunnelled-networks-8064d6f21f7d9267.yaml new file mode 100644 index 00000000000..8aba619c247 --- /dev/null +++ b/releasenotes/notes/qos-minimum-bw-tunnelled-networks-8064d6f21f7d9267.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + ML2/OVS and ML2/OVN now support modelling tunnelled networks in the + Placement API. The "tunnelled_network_rp_name" configuration option + defines the resource provider name used to represent all tunnelled + networks in a compute node (by default "rp_tunnelled"). If this string + is present in the "resource_provider_bandwidths" dictionary, the + corresponding mechanism driver will create a resource provider for + the overlay traffic.