From 45410d76bea6a37b750b1b197f1b04edaf32c5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Kap=C5=82o=C5=84ski?= Date: Sat, 17 Oct 2015 12:54:32 +0200 Subject: [PATCH] Add support for QoS for LinuxBridge agent There is a new QoS extension driver for the linuxbridge agent being added. This driver provides support for QoS configuring on the linuxbridge agent. This patch introduces two new config options for Linuxbridge agent: kernel_hz - HZ value of host kernel, tbf_latency - value of latency in tbf qdisc to calculate size of queue. Co-Authored-By: vikram.choudhary Change-Id: I457ca2569b5d4a916ba09e71040505cd0ad3257b Closes-Bug: #1500012 Closes-Bug: #1550514 DocImpact Update agent configuration to show settings related to QoS and bandwidth limiting --- doc/source/devref/quality_of_service.rst | 27 ++- .../rootwrap.d/linuxbridge-plugin.filters | 5 + neutron/agent/linux/tc_lib.py | 155 +++++++++++++ .../ml2/drivers/agent/_common_agent.py | 7 +- .../linuxbridge/agent/common/config.py | 16 +- .../agent/extension_drivers/__init__.py | 0 .../agent/extension_drivers/qos_driver.py | 60 +++++ .../mech_driver/mech_linuxbridge.py | 3 + neutron/tests/fullstack/resources/config.py | 22 +- neutron/tests/fullstack/test_qos.py | 46 +++- .../functional/agent/linux/test_tc_lib.py | 69 ++++++ neutron/tests/unit/agent/linux/test_tc_lib.py | 217 ++++++++++++++++++ .../agent/extension_drivers/__init__.py | 0 .../extension_drivers/test_qos_driver.py | 79 +++++++ ...or-linuxbridge-agent-bdb13515aac4e555.yaml | 13 ++ setup.cfg | 1 + 16 files changed, 701 insertions(+), 19 deletions(-) create mode 100644 neutron/agent/linux/tc_lib.py create mode 100644 neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/__init__.py create mode 100644 neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/qos_driver.py create mode 100644 neutron/tests/functional/agent/linux/test_tc_lib.py create mode 100644 neutron/tests/unit/agent/linux/test_tc_lib.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/__init__.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/test_qos_driver.py create mode 100644 releasenotes/notes/QoS-for-linuxbridge-agent-bdb13515aac4e555.yaml diff --git a/doc/source/devref/quality_of_service.rst b/doc/source/devref/quality_of_service.rst index 8e3e6d81d25..81e0c76f92c 100644 --- a/doc/source/devref/quality_of_service.rst +++ b/doc/source/devref/quality_of_service.rst @@ -283,13 +283,15 @@ with them. Agent backends ~~~~~~~~~~~~~~ -At the moment, QoS is supported by Open vSwitch and SR-IOV ml2 drivers. +At the moment, QoS is supported by Open vSwitch, SR-IOV and Linux bridge +ml2 drivers. Each agent backend defines a QoS driver that implements the QosAgentDriver interface: * Open vSwitch (QosOVSAgentDriver); -* SR-IOV (QosSRIOVAgentDriver). +* SR-IOV (QosSRIOVAgentDriver); +* Linux bridge (QosLinuxbridgeAgentDriver). Open vSwitch @@ -326,6 +328,22 @@ to 1 Mbps only. If the limit is set to something that does not divide to 1000 kbps chunks, then the effective limit is rounded to the nearest integer Mbps value. +Linux bridge +~~~~~~~~~~~~ + +The Linux bridge implementation relies on the new tc_lib functions: + +* set_bw_limit +* update_bw_limit +* delete_bw_limit + +The ingress bandwidth limit is configured on the tap port by setting a simple +`tc-tbf `_ queueing discipline (qdisc) on the +port. It requires a value of HZ parameter configured in kernel on the host. +This value is neccessary to calculate the minimal burst value which is set in +tc. Details about how it is calculated can be found in +`http://unix.stackexchange.com/a/100797`_. This solution is similar to Open +vSwitch implementation. Configuration ------------- @@ -379,6 +397,11 @@ Additions to ovs_lib to set bandwidth limits on ports are covered in: * neutron.tests.functional.agent.test_ovs_lib +New functional tests for tc_lib to set bandwidth limits on ports are in: + +* neutron.tests.functional.agent.linux.test_tc_lib + + API tests ~~~~~~~~~ diff --git a/etc/neutron/rootwrap.d/linuxbridge-plugin.filters b/etc/neutron/rootwrap.d/linuxbridge-plugin.filters index 1e0b891b973..eab3cbae87b 100644 --- a/etc/neutron/rootwrap.d/linuxbridge-plugin.filters +++ b/etc/neutron/rootwrap.d/linuxbridge-plugin.filters @@ -18,3 +18,8 @@ bridge: CommandFilter, bridge, root ip: IpFilter, ip, root find: RegExpFilter, find, root, find, /sys/class/net, -maxdepth, 1, -type, l, -printf, %.* ip_exec: IpNetnsExecFilter, ip, root + +# tc commands needed for QoS support +tc_replace_tbf: RegExpFilter, tc, root, tc, qdisc, replace, dev, .+, root, tbf, rate, .+, latency, .+, burst, .+ +tc_delete: RegExpFilter, tc, root, tc, qdisc, del, dev, .+, root +tc_show: RegExpFilter, tc, root, tc, qdisc, show, dev, .+ diff --git a/neutron/agent/linux/tc_lib.py b/neutron/agent/linux/tc_lib.py new file mode 100644 index 00000000000..7ac2c46fb9a --- /dev/null +++ b/neutron/agent/linux/tc_lib.py @@ -0,0 +1,155 @@ +# Copyright 2016 OVH SAS +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re + +from neutron._i18n import _ +from neutron.agent.linux import ip_lib +from neutron.common import exceptions + + +SI_BASE = 1000 +IEC_BASE = 1024 + +LATENCY_UNIT = "ms" +BW_LIMIT_UNIT = "kbit" # kilobits per second in tc's notation +BURST_UNIT = "kbit" # kilobits in tc's notation + +# Those are RATES (bits per second) and SIZE (bytes) unit names from tc manual +UNITS = { + "k": 1, + "m": 2, + "g": 3, + "t": 4 +} + + +class InvalidKernelHzValue(exceptions.NeutronException): + message = _("Kernel HZ value %(value)s is not valid. This value must be " + "greater than 0.") + + +class InvalidUnit(exceptions.NeutronException): + message = _("Unit name '%(unit)s' is not valid.") + + +def convert_to_kilobits(value, base): + value = value.lower() + if "bit" in value: + input_in_bits = True + value = value.replace("bit", "") + else: + input_in_bits = False + value = value.replace("b", "") + # if it is now bare number then it is in bits, so we return it simply + if value.isdigit(): + value = int(value) + if input_in_bits: + return bits_to_kilobits(value, base) + else: + bits_value = bytes_to_bits(value) + return bits_to_kilobits(bits_value, base) + unit = value[-1:] + if unit not in UNITS.keys(): + raise InvalidUnit(unit=unit) + val = int(value[:-1]) + if input_in_bits: + bits_value = val * (base ** UNITS[unit]) + else: + bits_value = bytes_to_bits(val * (base ** UNITS[unit])) + return bits_to_kilobits(bits_value, base) + + +def bytes_to_bits(value): + return value * 8 + + +def bits_to_kilobits(value, base): + #NOTE(slaweq): round up that even 1 bit will give 1 kbit as a result + return int((value + (base - 1)) / base) + + +class TcCommand(ip_lib.IPDevice): + + def __init__(self, name, kernel_hz, namespace=None): + if kernel_hz <= 0: + raise InvalidKernelHzValue(value=kernel_hz) + super(TcCommand, self).__init__(name, namespace=namespace) + self.kernel_hz = kernel_hz + + def _execute_tc_cmd(self, cmd, **kwargs): + cmd = ['tc'] + cmd + ip_wrapper = ip_lib.IPWrapper(self.namespace) + return ip_wrapper.netns.execute(cmd, run_as_root=True, **kwargs) + + def get_bw_limits(self): + return self._get_tbf_limits() + + def set_bw_limit(self, bw_limit, burst_limit, latency_value): + return self._replace_tbf_qdisc(bw_limit, burst_limit, latency_value) + + def update_bw_limit(self, bw_limit, burst_limit, latency_value): + return self._replace_tbf_qdisc(bw_limit, burst_limit, latency_value) + + def delete_bw_limit(self): + cmd = ['qdisc', 'del', 'dev', self.name, 'root'] + # Return_code=2 is fine because it means + # "RTNETLINK answers: No such file or directory" what is fine when we + # are trying to delete qdisc + return self._execute_tc_cmd(cmd, extra_ok_codes=[2]) + + def get_burst_value(self, bw_limit, burst_limit): + min_burst_value = self._get_min_burst_value(bw_limit) + return max(min_burst_value, burst_limit) + + def _get_min_burst_value(self, bw_limit): + # bw_limit [kbit] / HZ [1/s] = burst [kbit] + return float(bw_limit) / float(self.kernel_hz) + + def _get_tbf_limits(self): + cmd = ['qdisc', 'show', 'dev', self.name] + cmd_result = self._execute_tc_cmd(cmd) + if not cmd_result: + return None, None + pattern = re.compile( + r"qdisc (\w+) \w+: \w+ refcnt \d rate (\w+) burst (\w+) \w*" + ) + m = pattern.match(cmd_result) + if not m: + return None, None + qdisc_name = m.group(1) + if qdisc_name != "tbf": + return None, None + #NOTE(slaweq): because tc is giving bw limit in SI units + # we need to calculate it as 1000bit = 1kbit: + bw_limit = convert_to_kilobits(m.group(2), SI_BASE) + #NOTE(slaweq): because tc is giving burst limit in IEC units + # we need to calculate it as 1024bit = 1kbit: + burst_limit = convert_to_kilobits(m.group(3), IEC_BASE) + return bw_limit, burst_limit + + def _replace_tbf_qdisc(self, bw_limit, burst_limit, latency_value): + burst = "%s%s" % ( + self.get_burst_value(bw_limit, burst_limit), BURST_UNIT) + latency = "%s%s" % (latency_value, LATENCY_UNIT) + rate_limit = "%s%s" % (bw_limit, BW_LIMIT_UNIT) + cmd = [ + 'qdisc', 'replace', 'dev', self.name, + 'root', 'tbf', + 'rate', rate_limit, + 'latency', latency, + 'burst', burst + ] + return self._execute_tc_cmd(cmd) diff --git a/neutron/plugins/ml2/drivers/agent/_common_agent.py b/neutron/plugins/ml2/drivers/agent/_common_agent.py index ece86468ba1..394c7840aac 100644 --- a/neutron/plugins/ml2/drivers/agent/_common_agent.py +++ b/neutron/plugins/ml2/drivers/agent/_common_agent.py @@ -97,6 +97,10 @@ class CommonAgentLoop(service.Service): heartbeat = loopingcall.FixedIntervalLoopingCall( self._report_state) heartbeat.start(interval=report_interval) + + # The initialization is complete; we can start receiving messages + self.connection.consume_in_threads() + self.daemon_loop() def stop(self, graceful=True): @@ -152,7 +156,8 @@ class CommonAgentLoop(service.Service): consumers = self.mgr.get_rpc_consumers() self.connection = agent_rpc.create_consumers(self.endpoints, self.topic, - consumers) + consumers, + start_listening=False) def init_extension_manager(self, connection): ext_manager.register_opts(cfg.CONF) diff --git a/neutron/plugins/ml2/drivers/linuxbridge/agent/common/config.py b/neutron/plugins/ml2/drivers/linuxbridge/agent/common/config.py index c8fa665eb57..02075f15336 100644 --- a/neutron/plugins/ml2/drivers/linuxbridge/agent/common/config.py +++ b/neutron/plugins/ml2/drivers/linuxbridge/agent/common/config.py @@ -19,7 +19,8 @@ from neutron._i18n import _ DEFAULT_BRIDGE_MAPPINGS = [] DEFAULT_INTERFACE_MAPPINGS = [] DEFAULT_VXLAN_GROUP = '224.0.0.1' - +DEFAULT_KERNEL_HZ_VALUE = 250 # [Hz] +DEFAULT_TC_TBF_LATENCY = 50 # [ms] vxlan_opts = [ cfg.BoolOpt('enable_vxlan', default=True, @@ -62,6 +63,19 @@ bridge_opts = [ help=_("List of :")), ] +qos_options = [ + cfg.IntOpt('kernel_hz', default=DEFAULT_KERNEL_HZ_VALUE, + help=_("Value of host kernel tick rate (hz) for calculating " + "minimum burst value in bandwidth limit rules for " + "a port with QoS. See kernel configuration file for " + "HZ value and tc-tbf manual for more information.")), + cfg.IntOpt('tbf_latency', default=DEFAULT_TC_TBF_LATENCY, + help=_("Value of latency (ms) for calculating size of queue " + "for a port with QoS. See tc-tbf manual for more " + "information.")) +] + cfg.CONF.register_opts(vxlan_opts, "VXLAN") cfg.CONF.register_opts(bridge_opts, "LINUX_BRIDGE") +cfg.CONF.register_opts(qos_options, "QOS") diff --git a/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/__init__.py b/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/qos_driver.py b/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/qos_driver.py new file mode 100644 index 00000000000..6b9499f83f9 --- /dev/null +++ b/neutron/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/qos_driver.py @@ -0,0 +1,60 @@ +# Copyright 2016 OVH SAS +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import helpers as log_helpers +from oslo_log import log + +from neutron._i18n import _LI +from neutron.agent.l2.extensions import qos +from neutron.agent.linux import tc_lib +from neutron.plugins.ml2.drivers.linuxbridge.mech_driver import ( + mech_linuxbridge) + +LOG = log.getLogger(__name__) + + +class QosLinuxbridgeAgentDriver(qos.QosAgentDriver): + + SUPPORTED_RULES = ( + mech_linuxbridge.LinuxbridgeMechanismDriver.supported_qos_rule_types + ) + + def initialize(self): + LOG.info(_LI("Initializing Linux bridge QoS extension")) + + @log_helpers.log_method_call + def create_bandwidth_limit(self, port, rule): + tc_wrapper = self._get_tc_wrapper(port) + tc_wrapper.set_bw_limit( + rule.max_kbps, rule.max_burst_kbps, cfg.CONF.QOS.tbf_latency + ) + + @log_helpers.log_method_call + def update_bandwidth_limit(self, port, rule): + tc_wrapper = self._get_tc_wrapper(port) + tc_wrapper.update_bw_limit( + rule.max_kbps, rule.max_burst_kbps, cfg.CONF.QOS.tbf_latency + ) + + @log_helpers.log_method_call + def delete_bandwidth_limit(self, port): + tc_wrapper = self._get_tc_wrapper(port) + tc_wrapper.delete_bw_limit() + + def _get_tc_wrapper(self, port): + return tc_lib.TcCommand( + port['device'], + cfg.CONF.QOS.kernel_hz, + ) diff --git a/neutron/plugins/ml2/drivers/linuxbridge/mech_driver/mech_linuxbridge.py b/neutron/plugins/ml2/drivers/linuxbridge/mech_driver/mech_linuxbridge.py index 44c842c226e..6bf19c672db 100644 --- a/neutron/plugins/ml2/drivers/linuxbridge/mech_driver/mech_linuxbridge.py +++ b/neutron/plugins/ml2/drivers/linuxbridge/mech_driver/mech_linuxbridge.py @@ -18,6 +18,7 @@ from neutron.common import constants from neutron.extensions import portbindings from neutron.plugins.common import constants as p_constants from neutron.plugins.ml2.drivers import mech_agent +from neutron.services.qos import qos_consts class LinuxbridgeMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase): @@ -30,6 +31,8 @@ class LinuxbridgeMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase): network. """ + supported_qos_rule_types = [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT] + def __init__(self): sg_enabled = securitygroups_rpc.is_firewall_enabled() super(LinuxbridgeMechanismDriver, self).__init__( diff --git a/neutron/tests/fullstack/resources/config.py b/neutron/tests/fullstack/resources/config.py index 472f173f560..b67fccf72bb 100644 --- a/neutron/tests/fullstack/resources/config.py +++ b/neutron/tests/fullstack/resources/config.py @@ -114,10 +114,14 @@ class ML2ConfigFixture(ConfigFixture): super(ML2ConfigFixture, self).__init__( env_desc, host_desc, temp_dir, base_filename='ml2_conf.ini') + mechanism_drivers = 'openvswitch,linuxbridge' + if self.env_desc.l2_pop: + mechanism_drivers += ',l2population' + self.config.update({ 'ml2': { 'tenant_network_types': tenant_network_types, - 'mechanism_drivers': self.mechanism_drivers, + 'mechanism_drivers': mechanism_drivers, }, 'ml2_type_vlan': { 'network_vlan_ranges': 'physnet1:1000:2999', @@ -134,16 +138,6 @@ class ML2ConfigFixture(ConfigFixture): self.config['ml2']['extension_drivers'] =\ qos_ext.QOS_EXT_DRIVER_ALIAS - @property - def mechanism_drivers(self): - mechanism_drivers = set(['openvswitch']) - for host in self.host_desc: - if host.l2_agent_type == constants.AGENT_TYPE_LINUXBRIDGE: - mechanism_drivers.add('linuxbridge') - if self.env_desc.l2_pop: - mechanism_drivers.add('l2population') - return ','.join(mechanism_drivers) - class OVSConfigFixture(ConfigFixture): @@ -226,6 +220,12 @@ class LinuxBridgeConfigFixture(ConfigFixture): 'l2_population': str(self.env_desc.l2_pop), } }) + if env_desc.qos: + self.config.update({ + 'AGENT': { + 'extensions': 'qos' + } + }) if self.env_desc.tunneling_enabled: self.config.update({ 'LINUX_BRIDGE': { diff --git a/neutron/tests/fullstack/test_qos.py b/neutron/tests/fullstack/test_qos.py index 9e10336c614..10d5c1b5f45 100644 --- a/neutron/tests/fullstack/test_qos.py +++ b/neutron/tests/fullstack/test_qos.py @@ -13,39 +13,77 @@ # under the License. from oslo_utils import uuidutils +import testscenarios +from neutron.agent.common import ovs_lib +from neutron.agent.linux import bridge_lib +from neutron.agent.linux import tc_lib from neutron.agent.linux import utils +from neutron.common import constants from neutron.services.qos import qos_consts from neutron.tests.fullstack import base from neutron.tests.fullstack.resources import environment from neutron.tests.fullstack.resources import machine +from neutron.plugins.ml2.drivers.linuxbridge.agent.common import \ + config as linuxbridge_agent_config +from neutron.plugins.ml2.drivers.linuxbridge.agent import \ + linuxbridge_neutron_agent as linuxbridge_agent from neutron.plugins.ml2.drivers.openvswitch.mech_driver import \ mech_openvswitch as mech_ovs +load_tests = testscenarios.load_tests_apply_scenarios + + BANDWIDTH_LIMIT = 500 BANDWIDTH_BURST = 100 -def _wait_for_rule_applied(vm, limit, burst): +def _wait_for_rule_applied_ovs_agent(vm, limit, burst): utils.wait_until_true( lambda: vm.bridge.get_egress_bw_limit_for_port( vm.port.name) == (limit, burst)) +def _wait_for_rule_applied_linuxbridge_agent(vm, limit, burst): + port_name = linuxbridge_agent.LinuxBridgeManager.get_tap_device_name( + vm.neutron_port['id']) + tc = tc_lib.TcCommand( + port_name, + linuxbridge_agent_config.DEFAULT_KERNEL_HZ_VALUE, + namespace=vm.host.host_namespace + ) + utils.wait_until_true( + lambda: tc.get_bw_limits() == (limit, burst)) + + +def _wait_for_rule_applied(vm, limit, burst): + if isinstance(vm.bridge, ovs_lib.OVSBridge): + _wait_for_rule_applied_ovs_agent(vm, limit, burst) + if isinstance(vm.bridge, bridge_lib.BridgeDevice): + _wait_for_rule_applied_linuxbridge_agent(vm, limit, burst) + + def _wait_for_rule_removed(vm): # No values are provided when port doesn't have qos policy _wait_for_rule_applied(vm, None, None) -class TestQoSWithOvsAgent(base.BaseFullStackTestCase): +class TestQoSWithL2Agent(base.BaseFullStackTestCase): + + scenarios = [ + ("ovs", {'l2_agent_type': constants.AGENT_TYPE_OVS}), + ("linuxbridge", {'l2_agent_type': constants.AGENT_TYPE_LINUXBRIDGE}) + ] def setUp(self): - host_desc = [environment.HostDescription(l3_agent=False)] + host_desc = [environment.HostDescription( + l3_agent=False, + l2_agent_type=self.l2_agent_type)] env_desc = environment.EnvironmentDescription(qos=True) env = environment.Environment(env_desc, host_desc) - super(TestQoSWithOvsAgent, self).setUp(env) + super(TestQoSWithL2Agent, self).setUp(env) def _create_qos_policy(self): return self.safe_client.create_qos_policy( diff --git a/neutron/tests/functional/agent/linux/test_tc_lib.py b/neutron/tests/functional/agent/linux/test_tc_lib.py new file mode 100644 index 00000000000..402b9d39ea2 --- /dev/null +++ b/neutron/tests/functional/agent/linux/test_tc_lib.py @@ -0,0 +1,69 @@ +# Copyright (c) 2016 OVH SAS +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging + +from neutron.agent.linux import ip_lib +from neutron.agent.linux import tc_lib +from neutron.tests.functional import base as functional_base + +LOG = logging.getLogger(__name__) + +TEST_HZ_VALUE = 250 +LATENCY = 50 +BW_LIMIT = 1024 +BURST = 512 + +DEV_NAME = "test_tap" +MAC_ADDRESS = "fa:16:3e:01:01:01" + + +class TcLibTestCase(functional_base.BaseSudoTestCase): + + def setUp(self): + super(TcLibTestCase, self).setUp() + self.create_device() + self.tc = tc_lib.TcCommand(DEV_NAME, TEST_HZ_VALUE) + + def create_device(self): + """Create a tuntap with the specified attributes. + + The device is cleaned up at the end of the test. + """ + + ip = ip_lib.IPWrapper() + tap_device = ip.add_tuntap(DEV_NAME) + self.addCleanup(tap_device.link.delete) + tap_device.link.set_address(MAC_ADDRESS) + tap_device.link.set_up() + + def test_bandwidth_limit(self): + self.tc.set_bw_limit(BW_LIMIT, BURST, LATENCY) + bw_limit, burst = self.tc.get_bw_limits() + self.assertEqual(BW_LIMIT, bw_limit) + self.assertEqual(BURST, burst) + + new_bw_limit = BW_LIMIT + 500 + new_burst = BURST + 50 + + self.tc.update_bw_limit(new_bw_limit, new_burst, LATENCY) + bw_limit, burst = self.tc.get_bw_limits() + self.assertEqual(new_bw_limit, bw_limit) + self.assertEqual(new_burst, burst) + + self.tc.delete_bw_limit() + bw_limit, burst = self.tc.get_bw_limits() + self.assertIsNone(bw_limit) + self.assertIsNone(burst) diff --git a/neutron/tests/unit/agent/linux/test_tc_lib.py b/neutron/tests/unit/agent/linux/test_tc_lib.py new file mode 100644 index 00000000000..3ba9bf33639 --- /dev/null +++ b/neutron/tests/unit/agent/linux/test_tc_lib.py @@ -0,0 +1,217 @@ +# Copyright 2016 OVH SAS +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from neutron.agent.linux import tc_lib +from neutron.tests import base + +DEVICE_NAME = "tap_device" +KERNEL_HZ_VALUE = 1000 +BW_LIMIT = 2000 # [kbps] +BURST = 100 # [kbit] +LATENCY = 50 # [ms] + +TC_OUTPUT = ( + 'qdisc tbf 8011: root refcnt 2 rate %(bw)skbit burst %(burst)skbit ' + 'lat 50.0ms \n') % {'bw': BW_LIMIT, 'burst': BURST} + + +class BaseUnitConversionTest(object): + + def test_convert_to_kilobits_bare_value(self): + value = "1000" + expected_value = 8 # kbit + self.assertEqual( + expected_value, + tc_lib.convert_to_kilobits(value, self.base_unit) + ) + + def test_convert_to_kilobits_bytes_value(self): + value = "1000b" + expected_value = 8 # kbit + self.assertEqual( + expected_value, + tc_lib.convert_to_kilobits(value, self.base_unit) + ) + + def test_convert_to_kilobits_bits_value(self): + value = "1000bit" + expected_value = tc_lib.bits_to_kilobits(1000, self.base_unit) + self.assertEqual( + expected_value, + tc_lib.convert_to_kilobits(value, self.base_unit) + ) + + def test_convert_to_kilobits_megabytes_value(self): + value = "1m" + expected_value = tc_lib.bits_to_kilobits( + self.base_unit ** 2 * 8, self.base_unit) + self.assertEqual( + expected_value, + tc_lib.convert_to_kilobits(value, self.base_unit) + ) + + def test_convert_to_kilobits_megabits_value(self): + value = "1mbit" + expected_value = tc_lib.bits_to_kilobits( + self.base_unit ** 2, self.base_unit) + self.assertEqual( + expected_value, + tc_lib.convert_to_kilobits(value, self.base_unit) + ) + + def test_convert_to_bytes_wrong_unit(self): + value = "1Zbit" + self.assertRaises( + tc_lib.InvalidUnit, + tc_lib.convert_to_kilobits, value, self.base_unit + ) + + def test_bytes_to_bits(self): + test_values = [ + (0, 0), # 0 bytes should be 0 bits + (1, 8) # 1 byte should be 8 bits + ] + for input_bytes, expected_bits in test_values: + self.assertEqual( + expected_bits, tc_lib.bytes_to_bits(input_bytes) + ) + + +class TestSIUnitConversions(BaseUnitConversionTest, base.BaseTestCase): + + base_unit = tc_lib.SI_BASE + + def test_bits_to_kilobits(self): + test_values = [ + (0, 0), # 0 bites should be 0 kilobites + (1, 1), # 1 bit should be 1 kilobit + (999, 1), # 999 bits should be 1 kilobit + (1000, 1), # 1000 bits should be 1 kilobit + (1001, 2) # 1001 bits should be 2 kilobits + ] + for input_bits, expected_kilobits in test_values: + self.assertEqual( + expected_kilobits, + tc_lib.bits_to_kilobits(input_bits, self.base_unit) + ) + + +class TestIECUnitConversions(BaseUnitConversionTest, base.BaseTestCase): + + base_unit = tc_lib.IEC_BASE + + def test_bits_to_kilobits(self): + test_values = [ + (0, 0), # 0 bites should be 0 kilobites + (1, 1), # 1 bit should be 1 kilobit + (1023, 1), # 1023 bits should be 1 kilobit + (1024, 1), # 1024 bits should be 1 kilobit + (1025, 2) # 1025 bits should be 2 kilobits + ] + for input_bits, expected_kilobits in test_values: + self.assertEqual( + expected_kilobits, + tc_lib.bits_to_kilobits(input_bits, self.base_unit) + ) + + +class TestTcCommand(base.BaseTestCase): + def setUp(self): + super(TestTcCommand, self).setUp() + self.tc = tc_lib.TcCommand(DEVICE_NAME, KERNEL_HZ_VALUE) + self.bw_limit = "%s%s" % (BW_LIMIT, tc_lib.BW_LIMIT_UNIT) + self.burst = "%s%s" % (BURST, tc_lib.BURST_UNIT) + self.latency = "%s%s" % (LATENCY, tc_lib.LATENCY_UNIT) + self.execute = mock.patch('neutron.agent.common.utils.execute').start() + + def test_check_kernel_hz_lower_then_zero(self): + self.assertRaises( + tc_lib.InvalidKernelHzValue, + tc_lib.TcCommand, DEVICE_NAME, 0 + ) + self.assertRaises( + tc_lib.InvalidKernelHzValue, + tc_lib.TcCommand, DEVICE_NAME, -100 + ) + + def test_get_bw_limits(self): + self.execute.return_value = TC_OUTPUT + bw_limit, burst_limit = self.tc.get_bw_limits() + self.assertEqual(BW_LIMIT, bw_limit) + self.assertEqual(BURST, burst_limit) + + def test_get_bw_limits_when_wrong_qdisc(self): + output = TC_OUTPUT.replace("tbf", "different_qdisc") + self.execute.return_value = output + bw_limit, burst_limit = self.tc.get_bw_limits() + self.assertIsNone(bw_limit) + self.assertIsNone(burst_limit) + + def test_get_bw_limits_when_wrong_units(self): + output = TC_OUTPUT.replace("kbit", "Xbit") + self.execute.return_value = output + self.assertRaises(tc_lib.InvalidUnit, self.tc.get_bw_limits) + + def test_set_bw_limit(self): + self.tc.set_bw_limit(BW_LIMIT, BURST, LATENCY) + self.execute.assert_called_once_with( + ["tc", "qdisc", "replace", "dev", DEVICE_NAME, + "root", "tbf", "rate", self.bw_limit, + "latency", self.latency, + "burst", self.burst], + run_as_root=True, + check_exit_code=True, + log_fail_as_error=True, + extra_ok_codes=None + ) + + def test_update_bw_limit(self): + self.tc.update_bw_limit(BW_LIMIT, BURST, LATENCY) + self.execute.assert_called_once_with( + ["tc", "qdisc", "replace", "dev", DEVICE_NAME, + "root", "tbf", "rate", self.bw_limit, + "latency", self.latency, + "burst", self.burst], + run_as_root=True, + check_exit_code=True, + log_fail_as_error=True, + extra_ok_codes=None + ) + + def test_delete_bw_limit(self): + self.tc.delete_bw_limit() + self.execute.assert_called_once_with( + ["tc", "qdisc", "del", "dev", DEVICE_NAME, "root"], + run_as_root=True, + check_exit_code=True, + log_fail_as_error=True, + extra_ok_codes=[2] + ) + + def test_burst_value_when_burst_bigger_then_minimal(self): + result = self.tc.get_burst_value(BW_LIMIT, BURST) + self.assertEqual(BURST, result) + + def test_burst_value_when_burst_smaller_then_minimal(self): + result = self.tc.get_burst_value(BW_LIMIT, 0) + self.assertEqual(2, result) + + def test__get_min_burst_value_in_bits(self): + result = self.tc._get_min_burst_value(BW_LIMIT) + #if input is 2000kbit and kernel_hz is configured to 1000 then + # min_burst should be 2 kbit + self.assertEqual(2, result) diff --git a/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/__init__.py b/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/test_qos_driver.py b/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/test_qos_driver.py new file mode 100644 index 00000000000..7bb553dd003 --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/drivers/linuxbridge/agent/extension_drivers/test_qos_driver.py @@ -0,0 +1,79 @@ +# Copyright 2016 OVH SAS +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from oslo_config import cfg +from oslo_utils import uuidutils + +from neutron.agent.linux import tc_lib +from neutron.objects.qos import rule +from neutron.plugins.ml2.drivers.linuxbridge.agent.common import config # noqa +from neutron.plugins.ml2.drivers.linuxbridge.agent.extension_drivers import ( + qos_driver) +from neutron.tests import base + + +TEST_LATENCY_VALUE = 100 + + +class QosLinuxbridgeAgentDriverTestCase(base.BaseTestCase): + + def setUp(self): + super(QosLinuxbridgeAgentDriverTestCase, self).setUp() + cfg.CONF.set_override("tbf_latency", TEST_LATENCY_VALUE, "QOS") + self.qos_driver = qos_driver.QosLinuxbridgeAgentDriver() + self.qos_driver.initialize() + self.rule = self._create_bw_limit_rule_obj() + self.port = self._create_fake_port(uuidutils.generate_uuid()) + + def _create_bw_limit_rule_obj(self): + rule_obj = rule.QosBandwidthLimitRule() + rule_obj.id = uuidutils.generate_uuid() + rule_obj.max_kbps = 2 + rule_obj.max_burst_kbps = 200 + rule_obj.obj_reset_changes() + return rule_obj + + def _create_fake_port(self, policy_id): + return {'qos_policy_id': policy_id, + 'network_qos_policy_id': None, + 'device': 'fake_tap'} + + def test_create_rule(self): + with mock.patch.object( + tc_lib.TcCommand, "set_bw_limit" + ) as set_bw_limit: + self.qos_driver.create_bandwidth_limit(self.port, self.rule) + set_bw_limit.assert_called_once_with( + self.rule.max_kbps, self.rule.max_burst_kbps, + TEST_LATENCY_VALUE + ) + + def test_update_rule(self): + with mock.patch.object( + tc_lib.TcCommand, "update_bw_limit" + ) as update_bw_limit: + self.qos_driver.update_bandwidth_limit(self.port, self.rule) + update_bw_limit.assert_called_once_with( + self.rule.max_kbps, self.rule.max_burst_kbps, + TEST_LATENCY_VALUE + ) + + def test_delete_rule(self): + with mock.patch.object( + tc_lib.TcCommand, "delete_bw_limit" + ) as delete_bw_limit: + self.qos_driver.delete_bandwidth_limit(self.port) + delete_bw_limit.assert_called_once_with() diff --git a/releasenotes/notes/QoS-for-linuxbridge-agent-bdb13515aac4e555.yaml b/releasenotes/notes/QoS-for-linuxbridge-agent-bdb13515aac4e555.yaml new file mode 100644 index 00000000000..c8423970532 --- /dev/null +++ b/releasenotes/notes/QoS-for-linuxbridge-agent-bdb13515aac4e555.yaml @@ -0,0 +1,13 @@ +--- +prelude: > + The LinuxBridge agent now supports QoS bandwidth limiting. +features: + - The LinuxBridge agent can now configure basic bandwidth limiting + QoS rules set for ports and networks. + It introduces two new config options for LinuxBridge agent. + First is 'kernel_hz' option which is value of host kernel HZ + setting. It is necessary for proper calculation of minimum burst + value in tbf qdisc setting. + Second is 'tbf_latency' which is value of latency to be configured + in tc-tbf setting. Details about this option can be found in + `tc-tbf manual `_. diff --git a/setup.cfg b/setup.cfg index 57f95d2cae5..24b223f5c70 100644 --- a/setup.cfg +++ b/setup.cfg @@ -113,6 +113,7 @@ neutron.agent.l2.extensions = neutron.qos.agent_drivers = ovs = neutron.plugins.ml2.drivers.openvswitch.agent.extension_drivers.qos_driver:QosOVSAgentDriver sriov = neutron.plugins.ml2.drivers.mech_sriov.agent.extension_drivers.qos_driver:QosSRIOVAgentDriver + linuxbridge = neutron.plugins.ml2.drivers.linuxbridge.agent.extension_drivers.qos_driver:QosLinuxbridgeAgentDriver neutron.agent.linux.pd_drivers = dibbler = neutron.agent.linux.dibbler:PDDibbler neutron.services.external_dns_drivers =