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
This commit is contained in:
Rodolfo Alonso Hernandez 2022-10-02 17:29:32 +02:00 committed by Rodolfo Alonso
parent e2d14b4209
commit 3ebdfe612a
19 changed files with 281 additions and 97 deletions

View File

@ -40,9 +40,6 @@ Limitations
technical reasons (in this case the port is created too late for technical reasons (in this case the port is created too late for
Neutron to affect scheduling). 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. * In Stein there is no support for networks with multiple physnets.
However some simpler multi-segment networks are still supported: However some simpler multi-segment networks are still supported:
@ -185,6 +182,13 @@ supported:
by a ``direct-physical`` port. 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 neutron-server config
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
@ -260,9 +264,20 @@ Valid values are all the
[ovs] [ovs]
bridge_mappings = physnet0:br-physnet0,... 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,... #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 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 $ root@dev20:~# ovs-vsctl list Open_vSwitch
... ...
external_ids : {hostname=dev20.fistro.com, \ 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_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", \ rundir="/var/run/openvswitch", \
system-id="029e7d3d-d2ab-4f2c-bc92-ec58c94a8fc1"} 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 $ openstack network agent show -f value -c configuration 5e57b85f-b017-419a-8745-9c406e149f9e
{'bridge_mappings': {'physnet0': 'br-physnet0'}, {'bridge_mappings': {'physnet0': 'br-physnet0'},
'resource_provider_bandwidths': {'br-physnet0': {'egress': 10000000, '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, 'resource_provider_inventory_defaults': {'allocation_ratio': 1.0,
'min_unit': 1, 'min_unit': 1,
'reserved': 0, '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 | | 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 | | 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 | | 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? * Does Placement have the expected traits?
@ -587,6 +605,7 @@ Please find an example in section `Propagation of resource information`_.
# as admin # as admin
$ openstack --os-placement-api-version 1.17 trait list | awk '/CUSTOM_/ { print $2 }' | sort $ openstack --os-placement-api-version 1.17 trait list | awk '/CUSTOM_/ { print $2 }' | sort
CUSTOM_PHYSNET_PHYSNET0 CUSTOM_PHYSNET_PHYSNET0
CUSTOM_TUNNELLED_NETWORKS
CUSTOM_VNIC_TYPE_DIRECT CUSTOM_VNIC_TYPE_DIRECT
CUSTOM_VNIC_TYPE_DIRECT_PHYSICAL CUSTOM_VNIC_TYPE_DIRECT_PHYSICAL
CUSTOM_VNIC_TYPE_MACVTAP CUSTOM_VNIC_TYPE_MACVTAP

View File

@ -15,8 +15,12 @@
from neutron_lib import constants as nlib_const from neutron_lib import constants as nlib_const
from neutron_lib.placement import utils as place_utils from neutron_lib.placement import utils as place_utils
import os_resource_classes as orc import os_resource_classes as orc
from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from neutron.common import _constants as n_const
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -99,6 +103,7 @@ class PlacementState(object):
self._device_mappings = device_mappings self._device_mappings = device_mappings
self._supported_vnic_types = supported_vnic_types self._supported_vnic_types = supported_vnic_types
self._client = client self._client = client
self._rp_tun_name = cfg.CONF.ml2.tunnelled_network_rp_name
def _deferred_update_physnet_traits(self): def _deferred_update_physnet_traits(self):
traits = [] traits = []
@ -111,6 +116,10 @@ class PlacementState(object):
name=place_utils.physnet_trait(physnet))) name=place_utils.physnet_trait(physnet)))
return traits 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): def _deferred_update_vnic_type_traits(self):
traits = [] traits = []
for vnic_type in self._supported_vnic_types: for vnic_type in self._supported_vnic_types:
@ -123,6 +132,7 @@ class PlacementState(object):
def deferred_update_traits(self): def deferred_update_traits(self):
traits = [] traits = []
traits += self._deferred_update_physnet_traits() traits += self._deferred_update_physnet_traits()
traits += self._deferred_update_tunnelled_traits()
traits += self._deferred_update_vnic_type_traits() traits += self._deferred_update_vnic_type_traits()
return traits return traits
@ -196,7 +206,8 @@ class PlacementState(object):
def deferred_update_resource_provider_traits(self): def deferred_update_resource_provider_traits(self):
rp_traits = [] rp_traits = []
tunnelled_trait_mappings = {
self._rp_tun_name: n_const.TRAIT_NETWORK_TUNNEL}
physnet_trait_mappings = {} physnet_trait_mappings = {}
for physnet, devices in self._device_mappings.items(): for physnet, devices in self._device_mappings.items():
for device in devices: for device in devices:
@ -210,8 +221,8 @@ class PlacementState(object):
self._driver_uuid_namespace, self._driver_uuid_namespace,
self._hypervisor_rps[device]['name'], self._hypervisor_rps[device]['name'],
device) device)
traits = [] traits = [physnet_trait_mappings.get(device) or
traits.append(physnet_trait_mappings[device]) tunnelled_trait_mappings[device]]
traits.extend(vnic_type_traits) traits.extend(vnic_type_traits)
rp_traits.append( rp_traits.append(
DeferredCall( DeferredCall(

View File

@ -135,7 +135,8 @@ def get_hypervisor_hostname():
# TODO(bence romsics): rehome this to neutron_lib.placement.utils # TODO(bence romsics): rehome this to neutron_lib.placement.utils
def default_rp_hypervisors(hypervisors, device_mappings, 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. """Fill config option 'resource_provider_hypervisors' with defaults.
:param hypervisors: Config option 'resource_provider_hypervisors' :param hypervisors: Config option 'resource_provider_hypervisors'
@ -145,14 +146,13 @@ def default_rp_hypervisors(hypervisors, device_mappings,
format. format.
:param default_hypervisor: Default hypervisor hostname. If not set, :param default_hypervisor: Default hypervisor hostname. If not set,
it tries to default to fully qualified domain name (fqdn) 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() _default_hypervisor = default_hypervisor or get_hypervisor_hostname()
# device_mappings = {'physnet1': ['br-phy1'], 'physnet2': ['br-phy2'], ...}
rv = {} devices = {dev for devs in device_mappings.values() for dev in devs}
for _physnet, devices in device_mappings.items(): if tunnelled_network_rp_name:
for device in devices: devices.add(tunnelled_network_rp_name)
if device in hypervisors: return {device: hypervisors.get(device) or _default_hypervisor
rv[device] = hypervisors[device] for device in devices}
else:
rv[device] = _default_hypervisor
return rv

View File

@ -78,3 +78,8 @@ IDPOOL_SELECT_SIZE = 100
AUTO_DELETE_PORT_OWNERS = [constants.DEVICE_OWNER_DHCP, AUTO_DELETE_PORT_OWNERS = [constants.DEVICE_OWNER_DHCP,
constants.DEVICE_OWNER_DISTRIBUTED, constants.DEVICE_OWNER_DISTRIBUTED,
constants.DEVICE_OWNER_AGENT_GW] 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'

View File

@ -864,7 +864,8 @@ def port_ip_changed(new_port, original_port):
return False 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. """Validate resource provider bandwidths against device names.
:param rp_bandwidths: Dict containing resource provider bandwidths, :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 :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 ovs-agent or in physical_device_mappings
in case of sriov-agent 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 :raises ValueError: In case of the devices (keys) in the rp_bandwidths dict
are not in the device_names set. 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: for dev_name in rp_bandwidths:
if dev_name not in device_names: if dev_name not in device_names:
raise ValueError(_( raise ValueError(_(

View File

@ -16,6 +16,8 @@
from oslo_config import cfg from oslo_config import cfg
from neutron._i18n import _ from neutron._i18n import _
from neutron.common import _constants as common_const
ml2_opts = [ ml2_opts = [
cfg.ListOpt('type_drivers', cfg.ListOpt('type_drivers',
@ -65,7 +67,16 @@ ml2_opts = [
cfg.IntOpt('overlay_ip_version', cfg.IntOpt('overlay_ip_version',
default=4, default=4,
help=_("IP version of all overlay (tunnel) network endpoints. " 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.")),
] ]

View File

@ -432,6 +432,11 @@ class QosOVSAgentDriver(qos.QosLinuxAgentDriver,
'vif_port was not found. It seems that port is already ' 'vif_port was not found. It seems that port is already '
'deleted', port.get('port_id')) 'deleted', port.get('port_id'))
return 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, self.ports[port['port_id']][(qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH,
rule.direction)] = port rule.direction)] = port

View File

@ -62,6 +62,7 @@ from neutron.api.rpc.handlers import securitygroups_rpc as sg_rpc
from neutron.common import config from neutron.common import config
from neutron.common import utils as n_utils from neutron.common import utils as n_utils
from neutron.conf.agent import common as agent_config 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.conf import service as service_conf
from neutron.plugins.ml2.drivers.agent import capabilities from neutron.plugins.ml2.drivers.agent import capabilities
from neutron.plugins.ml2.drivers.l2pop.rpc_manager import l2population_rpc 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() self._validate_rp_pkt_processing_cfg()
br_set = set(self.bridge_mappings.values()) br_set = set(self.bridge_mappings.values())
n_utils.validate_rp_bandwidth(self.rp_bandwidths, n_utils.validate_rp_bandwidth(
br_set) 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( self.rp_inventory_defaults = place_utils.parse_rp_inventory_defaults(
ovs_conf.resource_provider_inventory_defaults) ovs_conf.resource_provider_inventory_defaults)
# At the moment the format of # At the moment the format of
@ -250,7 +252,8 @@ class OVSNeutronAgent(l2population_rpc.L2populationRpcCallBackTunnelMixin,
self.rp_hypervisors = utils.default_rp_hypervisors( self.rp_hypervisors = utils.default_rp_hypervisors(
ovs_conf.resource_provider_hypervisors, ovs_conf.resource_provider_hypervisors,
{k: [v] for k, v in self.bridge_mappings.items()}, {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 = {} self.phys_brs = {}
@ -2925,6 +2928,7 @@ def main(bridge_classes):
l2_agent_extensions_manager.register_opts(cfg.CONF) l2_agent_extensions_manager.register_opts(cfg.CONF)
agent_config.setup_privsep() agent_config.setup_privsep()
service_conf.register_service_opts(service_conf.RPC_EXTRA_OPTS, cfg.CONF) 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) ext_mgr = l2_agent_extensions_manager.L2AgentExtensionsManager(cfg.CONF)

View File

@ -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 constants as plugins_constants
from neutron_lib.plugins import directory from neutron_lib.plugins import directory
from neutron_lib.utils import helpers from neutron_lib.utils import helpers
from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from ovsdbapp.backend.ovs_idl import event as row_event from ovsdbapp.backend.ovs_idl import event as row_event
@ -174,6 +175,7 @@ class OVNClientPlacementExtension(object):
self._plugin = None self._plugin = None
self.uuid_ns = ovn_const.OVN_RP_UUID self.uuid_ns = ovn_const.OVN_RP_UUID
self.supported_vnic_types = ovn_const.OVN_SUPPORTED_VNIC_TYPES self.supported_vnic_types = ovn_const.OVN_SUPPORTED_VNIC_TYPES
self._rp_tun_name = cfg.CONF.ml2.tunnelled_network_rp_name
@property @property
def placement_plugin(self): def placement_plugin(self):
@ -247,6 +249,13 @@ class OVNClientPlacementExtension(object):
LOG.debug('Building placement options for chassis %s: %s', LOG.debug('Building placement options for chassis %s: %s',
chassis.name, cms_options) chassis.name, cms_options)
hypervisor_rps = {} 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(): for device, hyperv in cms_options[ovn_const.RP_HYPERVISORS].items():
try: try:
hypervisor_rps[device] = {'name': hyperv, hypervisor_rps[device] = {'name': hyperv,
@ -254,7 +263,12 @@ class OVNClientPlacementExtension(object):
except (KeyError, AttributeError): except (KeyError, AttributeError):
continue 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" # 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 # 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 # 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] rp_bw = cms_options[n_const.RP_BANDWIDTHS]
if rp_bw: if rp_bw:
cms_options[n_const.RP_BANDWIDTHS] = { cms_options[n_const.RP_BANDWIDTHS] = {
device: bw for device, bw in rp_bw.items() if rp_device: bw for rp_device, bw in rp_bw.items() if
device in hypervisor_rps and device in bridges} rp_device in hypervisor_rps and rp_device in rp_devices}
# NOTE(ralonsoh): OVN only reports min BW RPs; packet processing RPs # NOTE(ralonsoh): OVN only reports min BW RPs; packet processing RPs
# will be added in a future implementation. If no RP_BANDWIDTHS values # will be added in a future implementation. If no RP_BANDWIDTHS values

View File

@ -20,8 +20,6 @@ from neutron_lib.services.qos import base
from neutron_lib.services.qos import constants as qos_consts from neutron_lib.services.qos import constants as qos_consts
from oslo_log import log as logging from oslo_log import log as logging
from neutron.objects import network as network_object
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -76,17 +74,6 @@ class OVSDriver(base.DriverBase):
def validate_rule_for_port(self, context, rule, port): def validate_rule_for_port(self, context, rule, port):
return self.validate_rule_for_network(context, rule, port.network_id) 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(): def register():
"""Register the driver.""" """Register the driver."""

View File

@ -49,6 +49,7 @@ from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from neutron._i18n import _ from neutron._i18n import _
from neutron.common import _constants as n_const
from neutron.db import db_base_plugin_common from neutron.db import db_base_plugin_common
from neutron.exceptions import qos as neutron_qos_exc from neutron.exceptions import qos as neutron_qos_exc
from neutron.extensions import qos from neutron.extensions import qos
@ -254,17 +255,22 @@ class QoSPlugin(qos.QoSPluginBase):
# support will be available. See Placement spec: # support will be available. See Placement spec:
# https://review.opendev.org/565730 # https://review.opendev.org/565730
first_segment = segments[0] first_segment = segments[0]
if not first_segment or not first_segment.physical_network: if not first_segment:
return [] return []
physnet_trait = pl_utils.physnet_trait( elif not first_segment.physical_network:
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 # NOTE(ralonsoh): we should not rely on the current execution order of
# the port extending functions. Although here we have # the port extending functions. Although here we have
# port_res[VNIC_TYPE], we should retrieve this value from the port DB # port_res[VNIC_TYPE], we should retrieve this value from the port DB
# object instead. # object instead.
vnic_trait = pl_utils.vnic_type_trait(vnic_type) vnic_trait = pl_utils.vnic_type_trait(vnic_type)
return [physnet_trait, vnic_trait] return [net_trait, vnic_trait]
@staticmethod @staticmethod
@resource_extend.extends([port_def.COLLECTION_NAME_BULK]) @resource_extend.extends([port_def.COLLECTION_NAME_BULK])

View File

@ -825,30 +825,42 @@ class TestMinBwQoSOvs(_TestMinBwQoS, base.BaseFullStackTestCase):
qoses, queues = self._qos_info(vm.bridge) qoses, queues = self._qos_info(vm.bridge)
self.fail(queuenum + qoses + queues) 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 = self._create_qos_policy()
qos_policy_id = qos_policy['id'] qos_policy_id = qos_policy['id']
self.safe_client.create_minimum_bandwidth_rule( self.safe_client.create_minimum_bandwidth_rule(
self.tenant_id, qos_policy_id, MIN_BANDWIDTH, self.direction) self.tenant_id, qos_policy_id, MIN_BANDWIDTH, self.direction)
network_args = {'network_type': 'vxlan', network_args = {'network_type': 'vxlan',
'qos_policy_id': qos_policy_id} 'qos_policy_id': qos_policy_id}
self.assertRaises( net = self.safe_client.create_network(
exceptions.Conflict,
self.safe_client.create_network,
self.tenant_id, name='network-test', **network_args) 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): def test_min_bw_qos_create_and_update_network_vxlan_supported(self):
network_args = {'network_type': 'vxlan'}
network = self.safe_client.create_network(
self.tenant_id, name='network-test', **network_args)
qos_policy = self._create_qos_policy() qos_policy = self._create_qos_policy()
qos_policy_id = qos_policy['id'] qos_policy_id = qos_policy['id']
self.safe_client.create_minimum_bandwidth_rule( self.safe_client.create_minimum_bandwidth_rule(
self.tenant_id, qos_policy_id, MIN_BANDWIDTH, self.direction) self.tenant_id, qos_policy_id, MIN_BANDWIDTH, self.direction)
self.assertRaises( network_args = {'network_type': 'vxlan',
exceptions.Conflict, 'qos_policy_id': qos_policy_id}
self.client.update_network, network['id'], network = self.safe_client.create_network(
body={'network': {'qos_policy_id': qos_policy_id}}) 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): def test_min_bw_qos_port_removed(self):
"""Test if min BW limit config is properly removed when port removed. """Test if min BW limit config is properly removed when port removed.

View File

@ -33,6 +33,7 @@ from neutron.common import utils
from neutron.conf.agent import common as agent_config from neutron.conf.agent import common as agent_config
from neutron.conf.agent import ovs_conf as ovs_agent_config from neutron.conf.agent import ovs_conf as ovs_agent_config
from neutron.conf import common as common_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 agent
from neutron.conf.plugins.ml2.drivers import ovs_conf from neutron.conf.plugins.ml2.drivers import ovs_conf
from neutron.plugins.ml2.drivers.openvswitch.agent.extension_drivers \ 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) agent_config.register_agent_state_opts_helper(config)
ovs_agent_config.register_ovs_agent_opts(config) ovs_agent_config.register_ovs_agent_opts(config)
ext_manager.register_opts(config) ext_manager.register_opts(config)
ml2_config.register_ml2_plugin_opts(cfg=config)
return config return config
def _configure_agent(self): def _configure_agent(self):

View File

@ -16,6 +16,8 @@ from unittest import mock
import uuid import uuid
from neutron.agent.common import placement_report 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 from neutron.tests import base
@ -43,6 +45,7 @@ class DeferredCallTestCase(base.BaseTestCase):
class PlacementStateTestCase(base.BaseTestCase): class PlacementStateTestCase(base.BaseTestCase):
def setUp(self): def setUp(self):
ml2_config.register_ml2_plugin_opts()
super(PlacementStateTestCase, self).setUp() super(PlacementStateTestCase, self).setUp()
self.client_mock = mock.Mock() self.client_mock = mock.Mock()
self.driver_uuid_namespace = uuid.UUID( self.driver_uuid_namespace = uuid.UUID(
@ -61,6 +64,10 @@ class PlacementStateTestCase(base.BaseTestCase):
'hypervisor_rps': { 'hypervisor_rps': {
'eth0': {'name': 'fakehost', 'uuid': self.hypervisor1_rp_uuid}, 'eth0': {'name': 'fakehost', 'uuid': self.hypervisor1_rp_uuid},
'eth1': {'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': {}, 'device_mappings': {},
'supported_vnic_types': [], 'supported_vnic_types': [],
@ -195,6 +202,7 @@ class PlacementStateTestCase(base.BaseTestCase):
}, },
'rp_bandwidths': { 'rp_bandwidths': {
'eth0': {'egress': 1, 'ingress': 1}, 'eth0': {'egress': 1, 'ingress': 1},
'rp_tunnelled': {'egress': 2, 'ingress': 3},
}, },
'supported_vnic_types': ['normal'], 'supported_vnic_types': ['normal'],
}) })
@ -211,6 +219,13 @@ class PlacementStateTestCase(base.BaseTestCase):
'1ea6f823-bcf2-5dc5-9bee-4ee6177a6451'), '1ea6f823-bcf2-5dc5-9bee-4ee6177a6451'),
traits=mock.ANY), 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' # uuid -v5 '00000000-0000-0000-0000-000000000001' 'fakehost'
mock.call( mock.call(
resource_provider_uuid=uuid.UUID( resource_provider_uuid=uuid.UUID(
@ -223,8 +238,9 @@ class PlacementStateTestCase(base.BaseTestCase):
actual_traits = [set(args[1]['traits']) for args in actual_traits = [set(args[1]['traits']) for args in
self.client_mock.update_resource_provider_traits.call_args_list] self.client_mock.update_resource_provider_traits.call_args_list]
self.assertEqual( self.assertEqual(
[set(['CUSTOM_PHYSNET_PHYSNET0', 'CUSTOM_VNIC_TYPE_NORMAL']), [{'CUSTOM_PHYSNET_PHYSNET0', 'CUSTOM_VNIC_TYPE_NORMAL'},
set(['CUSTOM_VNIC_TYPE_NORMAL'])], {n_const.TRAIT_NETWORK_TUNNEL, 'CUSTOM_VNIC_TYPE_NORMAL'},
{'CUSTOM_VNIC_TYPE_NORMAL'}],
actual_traits) actual_traits)
def test_deferred_update_resource_provider_inventories_bw(self): def test_deferred_update_resource_provider_inventories_bw(self):

View File

@ -16,9 +16,12 @@
import socket import socket
from unittest import mock from unittest import mock
from oslo_config import cfg
from neutron.agent.common import utils from neutron.agent.common import utils
from neutron.agent.linux import interface from neutron.agent.linux import interface
from neutron.conf.agent import common as config 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 import base
from neutron.tests.unit import testlib_api from neutron.tests.unit import testlib_api
@ -158,6 +161,10 @@ class TestGetHypervisorHostname(base.BaseTestCase):
# TODO(bence romsics): rehome this to neutron_lib # TODO(bence romsics): rehome this to neutron_lib
class TestDefaultRpHypervisors(base.BaseTestCase): class TestDefaultRpHypervisors(base.BaseTestCase):
def setUp(self):
super().setUp()
ml2_config.register_ml2_plugin_opts()
@mock.patch.object(utils, 'get_hypervisor_hostname', @mock.patch.object(utils, 'get_hypervisor_hostname',
return_value='thishost') return_value='thishost')
def test_defaults(self, hostname_mock): def test_defaults(self, hostname_mock):
@ -197,3 +204,26 @@ class TestDefaultRpHypervisors(base.BaseTestCase):
default_hypervisor='defaulthost', 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
)
)

View File

@ -386,19 +386,39 @@ class QosOVSAgentDriverTestCase(ovs_test_base.OVSAgentConfigTestBase):
self.qos_driver.delete_minimum_bandwidth({'port_id': 'p_id'}) self.qos_driver.delete_minimum_bandwidth({'port_id': 'p_id'})
mock_delete_minimum_bandwidth_queue.assert_called_once_with('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, with mock.patch.object(self.qos_driver.br_int,
'update_minimum_bandwidth_queue') \ 'update_minimum_bandwidth_queue') \
as mock_delete_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_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): def test_update_minimum_bandwidth_no_phy_brs(self):
vif_port = mock.Mock() vif_port = mock.Mock()
vif_port.ofport = 'ofport' vif_port.ofport = 'ofport'
rule = mock.Mock() rule = mock.Mock()
rule.min_kbps = 1500 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, with mock.patch.object(self.qos_driver.br_int,
'update_minimum_bandwidth_queue') \ 'update_minimum_bandwidth_queue') \
as mock_delete_minimum_bandwidth_queue, \ as mock_delete_minimum_bandwidth_queue, \
@ -413,7 +433,8 @@ class QosOVSAgentDriverTestCase(ovs_test_base.OVSAgentConfigTestBase):
vif_port.ofport = 'ofport' vif_port.ofport = 'ofport'
rule = mock.Mock() rule = mock.Mock()
rule.min_kbps = 1500 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, with mock.patch.object(self.qos_driver.br_int,
'update_minimum_bandwidth_queue') \ 'update_minimum_bandwidth_queue') \
as mock_delete_minimum_bandwidth_queue, \ as mock_delete_minimum_bandwidth_queue, \

View File

@ -27,12 +27,14 @@ class TestOVSDriver(base.BaseQosTestCase):
super(TestOVSDriver, self).setUp() super(TestOVSDriver, self).setUp()
self.driver = driver.OVSDriver.create() self.driver = driver.OVSDriver.create()
def test_validate_min_bw_rule_vs_physnet_non_physnet(self): def test_validate_min_bw_rule(self):
scenarios = [ # Minimum bandwidth rules are now allowed for tunnelled networks since
({'physical_network': 'fake physnet'}, self.assertTrue), # LP#1991965. The ML2/OVS backend cannot enforce them but Placement can
({}, self.assertFalse), # schedule a VM using this information.
] scenarios = [{'physical_network': 'fake physnet'},
for segment_kwargs, test_method in scenarios: {},
]
for segment_kwargs in scenarios:
segment = network_object.NetworkSegment(**segment_kwargs) segment = network_object.NetworkSegment(**segment_kwargs)
net = network_object.Network(mock.Mock(), segments=[segment]) net = network_object.Network(mock.Mock(), segments=[segment])
rule = mock.Mock() rule = mock.Mock()
@ -41,7 +43,7 @@ class TestOVSDriver(base.BaseQosTestCase):
with mock.patch( with mock.patch(
'neutron.objects.network.Network.get_object', 'neutron.objects.network.Network.get_object',
return_value=net): return_value=net):
test_method(self.driver.validate_rule_for_port( self.assertTrue(self.driver.validate_rule_for_port(
mock.Mock(), rule, 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())) mock.Mock(), rule, network_id=mock.Mock()))

View File

@ -15,6 +15,7 @@ from unittest import mock
from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import exceptions as ks_exc
import netaddr import netaddr
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import qos from neutron_lib.api.definitions import qos
from neutron_lib.callbacks import events from neutron_lib.callbacks import events
from neutron_lib import constants as lib_constants 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 placement as pl_exc
from neutron_lib.exceptions import qos as qos_exc from neutron_lib.exceptions import qos as qos_exc
from neutron_lib.objects import utils as obj_utils 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 constants as plugins_constants
from neutron_lib.plugins import directory from neutron_lib.plugins import directory
from neutron_lib.services.qos import constants as qos_consts from neutron_lib.services.qos import constants as qos_consts
@ -32,6 +34,7 @@ from oslo_config import cfg
from oslo_utils import uuidutils from oslo_utils import uuidutils
import webob.exc import webob.exc
from neutron.common import _constants as n_const
from neutron.exceptions import qos as neutron_qos_exc from neutron.exceptions import qos as neutron_qos_exc
from neutron.extensions import qos_pps_minimum_rule_alias from neutron.extensions import qos_pps_minimum_rule_alias
from neutron.extensions import qos_rules_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.ctxt = context.Context('fake_user', 'fake_tenant')
self.admin_ctxt = context.get_admin_context() self.admin_ctxt = context.get_admin_context()
self.default_uuid = 'fake_uuid'
self.policy_data = { self.policy_data = {
'policy': {'id': uuidutils.generate_uuid(), 'policy': {'id': uuidutils.generate_uuid(),
@ -129,6 +133,9 @@ class TestQosPlugin(base.BaseQosTestCase):
self.min_pps_rule = rule_object.QosMinimumPacketRateRule( self.min_pps_rule = rule_object.QosMinimumPacketRateRule(
self.ctxt, **self.rule_data['minimum_packet_rate_rule']) 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): def _validate_driver_params(self, method_name, ctxt):
call_args = self.qos_plugin.driver_manager.call.call_args[0] call_args = self.qos_plugin.driver_manager.call.call_args[0]
self.assertTrue(self.qos_plugin.driver_manager.call.called) self.assertTrue(self.qos_plugin.driver_manager.call.called)
@ -172,7 +179,7 @@ class TestQosPlugin(base.BaseQosTestCase):
return_value=min_pps_rules), \ return_value=min_pps_rules), \
mock.patch( mock.patch(
'uuid.uuid5', 'uuid.uuid5',
return_value='fake_uuid', return_value=self.default_uuid,
side_effect=request_groups_uuids): side_effect=request_groups_uuids):
return qos_plugin.QoSPlugin._extend_port_resource_request( return qos_plugin.QoSPlugin._extend_port_resource_request(
port_res, self.port) port_res, self.port)
@ -333,34 +340,33 @@ class TestQosPlugin(base.BaseQosTestCase):
port = self._create_and_extend_port([self.min_bw_rule], port = self._create_and_extend_port([self.min_bw_rule],
physical_network=None) 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): def test__extend_port_resource_request_mix_rules_non_provider_net(self):
self.min_bw_rule.direction = lib_constants.EGRESS_DIRECTION self.min_bw_rule.direction = lib_constants.EGRESS_DIRECTION
port = self._create_and_extend_port([self.min_bw_rule], port = self._create_and_extend_port(
[self.min_pps_rule], [self.min_bw_rule], [self.min_pps_rule], physical_network=None,
physical_network=None) request_groups_uuids=['fake_uuid0', 'fake_uuid1'])
self.assertEqual( request_groups = [
1, {'id': 'fake_uuid0',
len(port['resource_request']['request_groups']) 'required': [self._rp_tun_trait,
) 'CUSTOM_VNIC_TYPE_NORMAL'],
self.assertEqual( 'resources': {orc.NET_BW_EGR_KILOBIT_PER_SEC: 10}},
'fake_uuid', {'id': 'fake_uuid1',
port['resource_request']['request_groups'][0]['id'] 'required': ['CUSTOM_VNIC_TYPE_NORMAL'],
) 'resources': {orc.NET_PACKET_RATE_KILOPACKET_PER_SEC: 10}}]
self.assertEqual( expected = {
['CUSTOM_VNIC_TYPE_NORMAL'], 'request_groups': request_groups,
port['resource_request']['request_groups'][0]['required'] 'same_subtree': ['fake_uuid0', 'fake_uuid1']}
) self.assertEqual(expected, port['resource_request'])
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'],
)
def test__extend_port_resource_request_bulk_min_bw_rule(self): def test__extend_port_resource_request_bulk_min_bw_rule(self):
self.min_bw_rule.direction = lib_constants.EGRESS_DIRECTION self.min_bw_rule.direction = lib_constants.EGRESS_DIRECTION
@ -1844,6 +1850,24 @@ class TestQosPlugin(base.BaseQosTestCase):
self.qos_plugin.get_rule_type, self.qos_plugin.get_rule_type,
self.ctxt, qos_consts.RULE_TYPE_MINIMUM_PACKET_RATE) 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): class QoSRuleAliasTestExtensionManager(object):

View File

@ -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.