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 =