Merge "Add support for QoS for LinuxBridge agent"
This commit is contained in:
commit
be922997e7
@ -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 <http://linux.die.net/man/8/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
|
||||
~~~~~~~~~
|
||||
|
||||
|
@ -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, .+
|
||||
|
155
neutron/agent/linux/tc_lib.py
Normal file
155
neutron/agent/linux/tc_lib.py
Normal file
@ -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)
|
@ -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)
|
||||
|
@ -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,
|
||||
@ -68,6 +69,19 @@ bridge_opts = [
|
||||
help=_("List of <physical_network>:<physical_bridge>")),
|
||||
]
|
||||
|
||||
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")
|
||||
|
@ -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,
|
||||
)
|
@ -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__(
|
||||
|
@ -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': {
|
||||
|
@ -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(
|
||||
|
69
neutron/tests/functional/agent/linux/test_tc_lib.py
Normal file
69
neutron/tests/functional/agent/linux/test_tc_lib.py
Normal file
@ -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)
|
217
neutron/tests/unit/agent/linux/test_tc_lib.py
Normal file
217
neutron/tests/unit/agent/linux/test_tc_lib.py
Normal file
@ -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)
|
@ -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()
|
@ -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 <http://linux.die.net/man/8/tc-tbf>`_.
|
@ -114,6 +114,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 =
|
||||
|
Loading…
Reference in New Issue
Block a user