Merge "Support for minimum bandwidth rules in tunnelled networks"

This commit is contained in:
Zuul 2023-01-16 18:59:59 +00:00 committed by Gerrit Code Review
commit c078c6569f
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
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

View File

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

View File

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

View File

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

View File

@ -883,7 +883,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,
@ -892,10 +893,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(_(

View File

@ -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.")),
]

View File

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

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 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 = {}
@ -2930,6 +2933,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)

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

View File

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

View File

@ -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])

View File

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

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 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):

View File

@ -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):

View File

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

View File

@ -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, \

View File

@ -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()))

View File

@ -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):

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.