Merge "Linux Bridge: driver support for QoS egress minimum bandwidth"
This commit is contained in:
commit
4157c2888e
@ -362,17 +362,44 @@ Linux bridge
|
||||
|
||||
The Linux bridge implementation relies on the new tc_lib functions:
|
||||
|
||||
* set_bw_limit
|
||||
* update_bw_limit
|
||||
* delete_bw_limit
|
||||
* set_bw
|
||||
* delete_bw
|
||||
|
||||
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 necessary to calculate the minimal burst value which is set in
|
||||
tc. Details about how it is calculated can be found in
|
||||
`here <http://unix.stackexchange.com/a/100797>`_. This solution is similar to Open
|
||||
vSwitch implementation.
|
||||
Only egress direction traffic shaping, from the instance point of view, is
|
||||
implemented. Traffic shaping is done by a classful Traffic Control qdisq
|
||||
called Class Based Queueing
|
||||
(`Classful Queueing Disciplines <http://lartc.org/howto/lartc.qdisc.classful.html>`_).
|
||||
This shaping algorithm is implemented by
|
||||
`tc-htb <https://linux.die.net/man/8/tc-htb>`_, replacing the former one used
|
||||
`tc-tbf <http://linux.die.net/man/8/tc-tbf>`_.
|
||||
|
||||
Traffic shaping is executed only for interface egress traffic. Because the
|
||||
traffic coming from the instance is considered as ingress traffic by the
|
||||
interface, shaping is not possible. To solve this, a new element is introduced:
|
||||
`Intermediate Functional Block <http://linux-ip.net/gl/tc-filters/tc-filters-node3.html>`_,
|
||||
a pseudo network interface used to redirect all the ingress traffic from the TAP
|
||||
port to the egress IFB queue. The QoS rules are applied on this IFB
|
||||
interface associated to the TAP port::
|
||||
|
||||
+-----------------+
|
||||
| [Instance veth] |
|
||||
+-----------------+
|
||||
| (egress traffic, instance point of view)
|
||||
|
|
||||
| +--------------------------+
|
||||
\ / [tap port] | [IFB] \ /
|
||||
+-------------------------------+ | +-------------------------------+
|
||||
| ingress queue | egress queue | | | ingress queue | egress queue |
|
||||
| (filter: | | | | | (QoS rules, |
|
||||
| redir IFB) | | | | | tc-htb) |
|
||||
+-------------------------------+ | +-------------------------------+
|
||||
| | | shaped
|
||||
+--------------------------------+ | traffic
|
||||
\ /
|
||||
|
||||
Traffic in the tap port is redirected (mirrored) to the IFB using a Traffic
|
||||
Control filter
|
||||
(`Filter Actions <http://linux-ip.net/gl/tc-filters/tc-filters-node2.html>`_).
|
||||
|
||||
Notification driver design
|
||||
--------------------------
|
||||
|
@ -20,9 +20,12 @@ find: RegExpFilter, find, root, find, /sys/class/net, -maxdepth, 1, -type, l, -p
|
||||
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_add_ingress: RegExpFilter, tc, root, tc, qdisc, add, dev, .+, ingress, handle, .+
|
||||
tc_delete: RegExpFilter, tc, root, tc, qdisc, del, dev, .+, .+
|
||||
tc_add_qdisc: RegExpFilter, tc, root, tc, qdisc, add, dev, .+, (root|parent .+), handle, .+, htb
|
||||
tc_add_qdisc_ingress: RegExpFilter, tc, root, tc, qdisc, add, dev, .+, ingress, handle, .+
|
||||
tc_show_qdisc: RegExpFilter, tc, root, tc, qdisc, show, dev, .+
|
||||
tc_show_filters: RegExpFilter, tc, root, tc, filter, show, dev, .+, parent, .+
|
||||
tc_add_filter: RegExpFilter, tc, root, tc, filter, add, dev, .+, parent, .+, protocol, all, prio, .+, basic, police, rate, .+, burst, .+, mtu, .+, drop
|
||||
tc_del_qdisc: RegExpFilter, tc, root, tc, qdisc, del, dev, .+, (root|ingress|parent .+)
|
||||
tc_add_class: RegExpFilter, tc, root, tc, class, replace, dev, .+, parent, .+, classid, .+, .+, rate, .+
|
||||
tc_add_class_max: RegExpFilter, tc, root, tc, class, replace, dev, .+, parent, .+, classid, .+, .+, rate, .+, ceil, .+, burst, .+
|
||||
tc_show_class: RegExpFilter, tc, root, tc, class, show, dev, .+
|
||||
tc_show_filter: RegExpFilter, tc, root, tc, filter, show, dev, .+, parent, .+
|
||||
tc_add_filter_ifb: RegExpFilter, tc, root, tc, filter, add, dev, .+, parent, .+, protocol, all, u32, match, u32, 0, 0, action, mirred, egress, redirect, dev, .+
|
||||
|
@ -204,6 +204,15 @@ class IPWrapper(SubProcessBase):
|
||||
self._as_root([], 'link', ('add', name, 'type', 'dummy'))
|
||||
return IPDevice(name, namespace=self.namespace)
|
||||
|
||||
def add_ifb(self, name):
|
||||
"""Create a Linux IFB type interface with the given name."""
|
||||
self._as_root([], 'link', ('add', name, 'type', 'ifb'))
|
||||
return IPDevice(name, namespace=self.namespace)
|
||||
|
||||
def del_ifb(self, name):
|
||||
"""Delete a Linux IFB type interface with the given name."""
|
||||
self._as_root([], 'link', ('del', name))
|
||||
|
||||
def ensure_namespace(self, name):
|
||||
if not self.netns.exists(name):
|
||||
ip = self.netns.add(name)
|
||||
|
@ -13,22 +13,31 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import math
|
||||
import re
|
||||
|
||||
from neutron_lib import exceptions
|
||||
from oslo_log import log as logging
|
||||
|
||||
from neutron._i18n import _
|
||||
from neutron.agent.linux import ip_lib
|
||||
from neutron.common import constants
|
||||
from neutron.services.qos import qos_consts
|
||||
|
||||
|
||||
INGRESS_QDISC_ID = "ffff:"
|
||||
MAX_MTU_VALUE = 65535
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
ROOT_QDISC = "root"
|
||||
INGRESS_QDISC = "ingress"
|
||||
INGRESS_QDISC_HEX = "ffff:fff1"
|
||||
INGRESS_QDISC_HANDLE = "ffff:"
|
||||
QDISC_TYPE_HTB = "htb"
|
||||
QDISC_TYPE_DEFAULT = "pfifo_fast"
|
||||
|
||||
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
|
||||
|
||||
@ -40,21 +49,32 @@ UNITS = {
|
||||
"t": 4
|
||||
}
|
||||
|
||||
filters_pattern = re.compile(r"police \w+ rate (\w+) burst (\w+)")
|
||||
tbf_pattern = re.compile(
|
||||
r"qdisc (\w+) \w+: \w+ refcnt \d rate (\w+) burst (\w+) \w*")
|
||||
|
||||
|
||||
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):
|
||||
class InvalidPolicyClassParameters(exceptions.NeutronException):
|
||||
message = _("'rate' or 'ceil' parameters must be defined")
|
||||
|
||||
|
||||
def kilobits_to_bits(value, base):
|
||||
return value * base
|
||||
|
||||
|
||||
def bits_to_kilobits(value, base):
|
||||
return int(math.ceil(float(value) / base))
|
||||
|
||||
|
||||
def bytes_to_bits(value):
|
||||
return value * 8
|
||||
|
||||
|
||||
def bits_to_bytes(value):
|
||||
return int(value / 8)
|
||||
|
||||
|
||||
def convert_to_kilo(value, base):
|
||||
value = value.lower()
|
||||
if "bit" in value:
|
||||
input_in_bits = True
|
||||
@ -81,23 +101,8 @@ def convert_to_kilobits(value, base):
|
||||
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)
|
||||
@ -111,131 +116,248 @@ class TcCommand(ip_lib.IPDevice):
|
||||
rate to ensure that limit for TCP traffic will work well
|
||||
"""
|
||||
if not burst_limit:
|
||||
return float(bw_limit) * qos_consts.DEFAULT_BURST_RATE
|
||||
return int(float(bw_limit) * qos_consts.DEFAULT_BURST_RATE)
|
||||
return burst_limit
|
||||
|
||||
def get_filters_bw_limits(self, qdisc_id=INGRESS_QDISC_ID):
|
||||
cmd = ['filter', 'show', 'dev', self.name, 'parent', qdisc_id]
|
||||
cmd_result = self._execute_tc_cmd(cmd)
|
||||
if not cmd_result:
|
||||
return None, None
|
||||
for line in cmd_result.split("\n"):
|
||||
m = filters_pattern.match(line.strip())
|
||||
if m:
|
||||
#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(1), 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(2), IEC_BASE)
|
||||
return bw_limit, burst_limit
|
||||
return None, None
|
||||
def set_bw(self, max, burst, min, direction):
|
||||
max = kilobits_to_bits(max, SI_BASE) if max else max
|
||||
burst = (bits_to_bytes(kilobits_to_bits(burst, IEC_BASE)) if burst
|
||||
else burst)
|
||||
min = kilobits_to_bits(min, SI_BASE) if min else min
|
||||
if direction == constants.EGRESS_DIRECTION:
|
||||
return self._set_ingress_bw(max, burst, min)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_tbf_bw_limits(self):
|
||||
cmd = ['qdisc', 'show', 'dev', self.name]
|
||||
cmd_result = self._execute_tc_cmd(cmd)
|
||||
if not cmd_result:
|
||||
return None, None
|
||||
m = tbf_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 delete_bw(self, direction):
|
||||
if direction == constants.EGRESS_DIRECTION:
|
||||
return self._delete_ingress()
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_filters_bw_limit(self, bw_limit, burst_limit):
|
||||
"""Set ingress qdisc and filter for police ingress traffic on device
|
||||
def get_limits(self, direction):
|
||||
if direction == constants.EGRESS_DIRECTION:
|
||||
return self._get_ingress_limits()
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
This will allow to police traffic incoming to interface. It
|
||||
means that it is fine to limit egress traffic from instance point of
|
||||
view.
|
||||
"""
|
||||
#because replace of tc filters is not working properly and it's adding
|
||||
# new filters each time instead of replacing existing one first old
|
||||
# ingress qdisc should be deleted and then added new one so update will
|
||||
# be called to do that:
|
||||
return self.update_filters_bw_limit(bw_limit, burst_limit)
|
||||
def _set_ingress_bw(self, max, burst, min):
|
||||
self._add_policy_qdisc(INGRESS_QDISC, INGRESS_QDISC_HANDLE)
|
||||
self._configure_ifb(max=max, burst=burst, min=min)
|
||||
|
||||
def set_tbf_bw_limit(self, bw_limit, burst_limit, latency_value):
|
||||
"""Set token bucket filter qdisc on device
|
||||
def _delete_ingress(self):
|
||||
ifb = self._find_mirrored_ifb()
|
||||
if ifb:
|
||||
self._del_ifb(ifb)
|
||||
self._del_policy_qdisc(INGRESS_QDISC)
|
||||
|
||||
This will allow to limit speed of packets going out from interface. It
|
||||
means that it is fine to limit ingress traffic from instance point of
|
||||
view.
|
||||
"""
|
||||
return self._replace_tbf_qdisc(bw_limit, burst_limit, latency_value)
|
||||
def _add_policy_qdisc(self, parent, handle, qdisc_type=None, dev=None):
|
||||
def check_qdisc(qdisc, qdisc_type, handle, parent, device):
|
||||
if not qdisc or qdisc.get('type') == QDISC_TYPE_DEFAULT:
|
||||
return False
|
||||
elif ((qdisc_type and (qdisc.get('type') != qdisc_type or
|
||||
qdisc.get('handle') != handle)) or
|
||||
(not qdisc_type and qdisc.get('handle') != handle)):
|
||||
self._del_policy_qdisc(parent, dev=device)
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_filters_bw_limit(self, bw_limit, burst_limit,
|
||||
qdisc_id=INGRESS_QDISC_ID):
|
||||
self.delete_filters_bw_limit()
|
||||
return self._set_filters_bw_limit(bw_limit, burst_limit, qdisc_id)
|
||||
device = str(dev) if dev else self.name
|
||||
qdisc = self._show_policy_qdisc(parent, dev=device)
|
||||
if check_qdisc(qdisc, qdisc_type, handle, parent, device):
|
||||
return
|
||||
cmd = ['qdisc', 'add', 'dev', device]
|
||||
if parent in [ROOT_QDISC, INGRESS_QDISC]:
|
||||
cmd += [parent]
|
||||
else:
|
||||
cmd += ['parent', parent]
|
||||
cmd += ['handle', handle]
|
||||
if qdisc_type:
|
||||
cmd += [qdisc_type]
|
||||
|
||||
def update_tbf_bw_limit(self, bw_limit, burst_limit, latency_value):
|
||||
return self._replace_tbf_qdisc(bw_limit, burst_limit, latency_value)
|
||||
LOG.debug("Add policy qdisc cmd: %s", cmd)
|
||||
return self._execute_tc_cmd(cmd)
|
||||
|
||||
def delete_filters_bw_limit(self):
|
||||
#NOTE(slaweq): For limit traffic egress from instance we need to use
|
||||
# qdisc "ingress" because it is ingress traffic from interface POV:
|
||||
self._delete_qdisc("ingress")
|
||||
def _del_policy_qdisc(self, parent, dev=None):
|
||||
device = str(dev) if dev else self.name
|
||||
if not self._show_policy_qdisc(parent, dev=device):
|
||||
return
|
||||
cmd = ['qdisc', 'del', 'dev', device]
|
||||
if parent in [ROOT_QDISC, INGRESS_QDISC]:
|
||||
cmd += [parent]
|
||||
else:
|
||||
cmd += ['parent', parent]
|
||||
|
||||
def delete_tbf_bw_limit(self):
|
||||
self._delete_qdisc("root")
|
||||
|
||||
def _set_filters_bw_limit(self, bw_limit, burst_limit,
|
||||
qdisc_id=INGRESS_QDISC_ID):
|
||||
cmd = ['qdisc', 'add', 'dev', self.name, 'ingress',
|
||||
'handle', qdisc_id]
|
||||
LOG.debug("Delete policy qdisc cmd: %s", cmd)
|
||||
self._execute_tc_cmd(cmd)
|
||||
return self._add_policy_filter(bw_limit, burst_limit)
|
||||
|
||||
def _delete_qdisc(self, qdisc_name):
|
||||
cmd = ['qdisc', 'del', 'dev', self.name, qdisc_name]
|
||||
# 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 _list_policy_qdisc(self, dev=None):
|
||||
device = str(dev) if dev else self.name
|
||||
cmd = ['qdisc', 'show', 'dev', device]
|
||||
LOG.debug("List policy qdisc cmd: %s", cmd)
|
||||
result = self._execute_tc_cmd(cmd)
|
||||
pat = re.compile(r'qdisc (\w+) (\w+\:) (root|parent (\w*\:\w+))')
|
||||
qdiscs = collections.defaultdict(dict)
|
||||
for match in (pat.match(line) for line in result.splitlines()
|
||||
if pat.match(line)):
|
||||
qdisc = {}
|
||||
qdisc['type'] = match.groups()[0]
|
||||
qdisc['handle'] = match.groups()[1]
|
||||
if match.groups()[2] == ROOT_QDISC:
|
||||
qdisc['parentid'] = ROOT_QDISC
|
||||
else:
|
||||
qdisc['parentid'] = match.groups()[3]
|
||||
qdisc_ref = INGRESS_QDISC if qdisc['parentid'] == \
|
||||
INGRESS_QDISC_HEX else qdisc['parentid']
|
||||
qdiscs[qdisc_ref] = qdisc
|
||||
|
||||
def _get_tbf_burst_value(self, bw_limit, burst_limit):
|
||||
min_burst_value = float(bw_limit) / float(self.kernel_hz)
|
||||
return max(min_burst_value, burst_limit)
|
||||
LOG.debug("List of policy qdiscs: %s", qdiscs)
|
||||
return qdiscs
|
||||
|
||||
def _replace_tbf_qdisc(self, bw_limit, burst_limit, latency_value):
|
||||
burst = "%s%s" % (
|
||||
self._get_tbf_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
|
||||
]
|
||||
def _show_policy_qdisc(self, parent, dev=None):
|
||||
device = str(dev) if dev else self.name
|
||||
return self._list_policy_qdisc(device).get(parent)
|
||||
|
||||
def _add_policy_class(self, parent, classid, qdisc_type, rate=None,
|
||||
ceil=None, burst=None, dev=None):
|
||||
"""Add new TC class"""
|
||||
device = str(dev) if dev else self.name
|
||||
policy = self._show_policy_class(classid, dev=device)
|
||||
if policy:
|
||||
rate = (kilobits_to_bits(policy['rate'], SI_BASE) if not rate
|
||||
else rate)
|
||||
ceil = (kilobits_to_bits(policy['ceil'], SI_BASE) if not ceil
|
||||
else ceil)
|
||||
burst = (bits_to_bytes(kilobits_to_bits(policy['burst'], IEC_BASE))
|
||||
if not burst else burst)
|
||||
|
||||
if not rate and not ceil:
|
||||
raise InvalidPolicyClassParameters
|
||||
if not rate:
|
||||
rate = ceil
|
||||
|
||||
cmd = self._cmd_policy_class(classid, qdisc_type, rate, device, parent,
|
||||
ceil, burst)
|
||||
LOG.debug("Add/replace policy class cmd: %s", cmd)
|
||||
return self._execute_tc_cmd(cmd)
|
||||
|
||||
def _add_policy_filter(self, bw_limit, burst_limit,
|
||||
qdisc_id=INGRESS_QDISC_ID):
|
||||
rate_limit = "%s%s" % (bw_limit, BW_LIMIT_UNIT)
|
||||
burst = "%s%s" % (
|
||||
self.get_ingress_qdisc_burst_value(bw_limit, burst_limit),
|
||||
BURST_UNIT
|
||||
)
|
||||
#NOTE(slaweq): it is made in exactly same way how openvswitch is doing
|
||||
# it when configuing ingress traffic limit on port. It can be found in
|
||||
# lib/netdev-linux.c#L4698 in openvswitch sources:
|
||||
cmd = [
|
||||
'filter', 'add', 'dev', self.name,
|
||||
'parent', qdisc_id, 'protocol', 'all',
|
||||
'prio', '49', 'basic', 'police',
|
||||
'rate', rate_limit,
|
||||
'burst', burst,
|
||||
'mtu', MAX_MTU_VALUE,
|
||||
'drop']
|
||||
def _cmd_policy_class(self, classid, qdisc_type, rate, device, parent,
|
||||
ceil, burst):
|
||||
cmd = ['class', 'replace', 'dev', device]
|
||||
if parent:
|
||||
cmd += ['parent', parent]
|
||||
rate = 8 if rate < 8 else rate
|
||||
cmd += ['classid', classid, qdisc_type, 'rate', rate]
|
||||
if ceil:
|
||||
ceil = rate if ceil < rate else ceil
|
||||
cmd += ['ceil', ceil]
|
||||
if burst:
|
||||
cmd += ['burst', burst]
|
||||
return cmd
|
||||
|
||||
def _list_policy_class(self, dev=None):
|
||||
device = str(dev) if dev else self.name
|
||||
cmd = ['class', 'show', 'dev', device]
|
||||
result = self._execute_tc_cmd(cmd, check_exit_code=False)
|
||||
if not result:
|
||||
return {}
|
||||
classes = collections.defaultdict(dict)
|
||||
pat = re.compile(r'class (\S+) ([0-9a-fA-F]+\:[0-9a-fA-F]+) '
|
||||
r'(root|parent ([0-9a-fA-F]+\:[0-9a-fA-F]+))'
|
||||
r'( prio ([0-9]+))* rate (\w+) ceil (\w+) burst (\w+)'
|
||||
r' cburst (\w+)')
|
||||
for match in (pat.match(line) for line in result.splitlines()
|
||||
if pat.match(line)):
|
||||
_class = {}
|
||||
_class['type'] = match.groups()[0]
|
||||
classid = match.groups()[1]
|
||||
if match.groups()[2] == ROOT_QDISC:
|
||||
_class['parentid'] = None
|
||||
else:
|
||||
_class['parentid'] = match.groups()[3]
|
||||
_class['prio'] = match.groups()[5]
|
||||
_class['rate'] = convert_to_kilo(match.groups()[6], SI_BASE)
|
||||
_class['ceil'] = convert_to_kilo(match.groups()[7], SI_BASE)
|
||||
_class['burst'] = convert_to_kilo(match.groups()[8], IEC_BASE)
|
||||
_class['cburst'] = convert_to_kilo(match.groups()[9], IEC_BASE)
|
||||
classes[classid] = _class
|
||||
LOG.debug("Policy classes: %s", classes)
|
||||
return classes
|
||||
|
||||
def _show_policy_class(self, classid, dev=None):
|
||||
device = str(dev) if dev else self.name
|
||||
return self._list_policy_class(device).get(classid)
|
||||
|
||||
def _add_policy_filter(self, parent, protocol, filter, dev=None,
|
||||
action=None):
|
||||
"""Add a new filter"""
|
||||
device = str(dev) if dev else self.name
|
||||
cmd = ['filter', 'add', 'dev', device, 'parent', parent]
|
||||
cmd += ['protocol'] + protocol
|
||||
cmd += filter
|
||||
if action:
|
||||
cmd += ['action'] + action
|
||||
|
||||
LOG.debug("Add policy filter cmd: %s", cmd)
|
||||
return self._execute_tc_cmd(cmd)
|
||||
|
||||
def _list_policy_filters(self, parent, dev=None):
|
||||
"""Returns the output of showing the filters in a device"""
|
||||
device = dev if dev else self.name
|
||||
cmd = ['filter', 'show', 'dev', device, 'parent', parent]
|
||||
LOG.debug("List policy filter cmd: %s", cmd)
|
||||
return self._execute_tc_cmd(cmd)
|
||||
|
||||
def _add_ifb(self, dev_name):
|
||||
"""Create a new IFB device"""
|
||||
ns_ip = ip_lib.IPWrapper(namespace=self.namespace)
|
||||
if self._find_mirrored_ifb():
|
||||
ifb = ip_lib.IPDevice(dev_name, namespace=self.namespace)
|
||||
if not ifb.exists():
|
||||
self._del_ifb(dev_name=dev_name)
|
||||
ifb = ns_ip.add_ifb(dev_name)
|
||||
else:
|
||||
self._del_ifb(dev_name=dev_name)
|
||||
ifb = ns_ip.add_ifb(dev_name)
|
||||
|
||||
ifb.disable_ipv6()
|
||||
ifb.link.set_up()
|
||||
return ifb
|
||||
|
||||
def _del_ifb(self, dev_name):
|
||||
"""Delete a IFB device"""
|
||||
ns_ip = ip_lib.IPWrapper(namespace=self.namespace)
|
||||
devices = ns_ip.get_devices(exclude_loopback=True)
|
||||
for device in (dev for dev in devices if dev.name == dev_name):
|
||||
ns_ip.del_ifb(device.name)
|
||||
|
||||
def _find_mirrored_ifb(self):
|
||||
"""Return the name of the IFB device where the traffic is mirrored"""
|
||||
ifb_name = self.name.replace("tap", "ifb")
|
||||
ifb = ip_lib.IPDevice(ifb_name, namespace=self.namespace)
|
||||
if not ifb.exists():
|
||||
return None
|
||||
return ifb_name
|
||||
|
||||
def _configure_ifb(self, max=None, burst=None, min=None):
|
||||
ifb = self._find_mirrored_ifb()
|
||||
if not ifb:
|
||||
ifb = self.name.replace("tap", "ifb")
|
||||
self._add_ifb(ifb)
|
||||
protocol = ['all', 'u32']
|
||||
filter = ['match', 'u32', '0', '0']
|
||||
action = ['mirred', 'egress', 'redirect', 'dev', '%s' % ifb]
|
||||
self._add_policy_filter(INGRESS_QDISC_HANDLE, protocol, filter,
|
||||
dev=self.name, action=action)
|
||||
self._add_policy_qdisc(ROOT_QDISC, "1:", qdisc_type=QDISC_TYPE_HTB,
|
||||
dev=ifb)
|
||||
self._add_policy_class("1:", "1:1", QDISC_TYPE_HTB, rate=min,
|
||||
ceil=max, burst=burst, dev=ifb)
|
||||
|
||||
def _get_ingress_limits(self):
|
||||
ifb = self._find_mirrored_ifb()
|
||||
if ifb:
|
||||
policy = self._show_policy_class("1:1", dev=ifb)
|
||||
if policy:
|
||||
return policy['ceil'], policy['burst'], policy['rate']
|
||||
return None, None, None
|
||||
|
@ -718,7 +718,8 @@ def transaction_guard(f):
|
||||
return inner
|
||||
|
||||
|
||||
def wait_until_true(predicate, timeout=60, sleep=1, exception=None):
|
||||
def wait_until_true(predicate, timeout=60, sleep=1, exception=None,
|
||||
initial_sleep=0):
|
||||
"""
|
||||
Wait until callable predicate is evaluated as True
|
||||
|
||||
@ -730,6 +731,7 @@ def wait_until_true(predicate, timeout=60, sleep=1, exception=None):
|
||||
(default) then WaitTimeout exception is raised.
|
||||
"""
|
||||
try:
|
||||
eventlet.sleep(initial_sleep)
|
||||
with eventlet.timeout.Timeout(timeout):
|
||||
while not predicate():
|
||||
eventlet.sleep(sleep)
|
||||
|
@ -19,8 +19,6 @@ 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,
|
||||
@ -76,20 +74,7 @@ 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."))
|
||||
]
|
||||
|
||||
|
||||
def register_linuxbridge_opts(cfg=cfg.CONF):
|
||||
cfg.register_opts(vxlan_opts, "VXLAN")
|
||||
cfg.register_opts(bridge_opts, "LINUX_BRIDGE")
|
||||
cfg.register_opts(qos_options, "QOS")
|
||||
|
@ -12,7 +12,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_config import cfg
|
||||
import collections
|
||||
|
||||
from oslo_log import helpers as log_helpers
|
||||
from oslo_log import log
|
||||
|
||||
@ -23,12 +24,18 @@ from neutron.agent.linux import tc_lib
|
||||
import neutron.common.constants as const
|
||||
from neutron.plugins.ml2.drivers.linuxbridge.mech_driver import (
|
||||
mech_linuxbridge)
|
||||
from neutron.services.qos import qos_consts
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class QosLinuxbridgeAgentDriver(qos.QosLinuxAgentDriver):
|
||||
|
||||
# TODO(ralonsoh):
|
||||
# - All driver calls should include the rule parameter, including
|
||||
# the delete function, to have the 'direction' parameter. This QoS
|
||||
# extension modification is going to be implemented in
|
||||
# https://review.openstack.org/#/c/341186/
|
||||
SUPPORTED_RULES = (
|
||||
mech_linuxbridge.LinuxbridgeMechanismDriver.supported_qos_rule_types
|
||||
)
|
||||
@ -38,6 +45,10 @@ class QosLinuxbridgeAgentDriver(qos.QosLinuxAgentDriver):
|
||||
IPTABLES_DIRECTION_PREFIX = {const.INGRESS_DIRECTION: "i",
|
||||
const.EGRESS_DIRECTION: "o"}
|
||||
|
||||
def __init__(self):
|
||||
super(QosLinuxbridgeAgentDriver, self).__init__()
|
||||
self._port_rules = collections.defaultdict(dict)
|
||||
|
||||
def initialize(self):
|
||||
LOG.info(_LI("Initializing Linux bridge QoS extension"))
|
||||
self.iptables_manager = iptables_manager.IptablesManager(use_ipv6=True)
|
||||
@ -58,22 +69,41 @@ class QosLinuxbridgeAgentDriver(qos.QosLinuxAgentDriver):
|
||||
|
||||
@log_helpers.log_method_call
|
||||
def create_bandwidth_limit(self, port, rule):
|
||||
tc_wrapper = self._get_tc_wrapper(port)
|
||||
tc_wrapper.set_filters_bw_limit(
|
||||
rule.max_kbps, self._get_egress_burst_value(rule)
|
||||
)
|
||||
self.update_bandwidth_limit(port, rule)
|
||||
|
||||
@log_helpers.log_method_call
|
||||
def update_bandwidth_limit(self, port, rule):
|
||||
tc_wrapper = self._get_tc_wrapper(port)
|
||||
tc_wrapper.update_filters_bw_limit(
|
||||
rule.max_kbps, self._get_egress_burst_value(rule)
|
||||
)
|
||||
device = port.get('device')
|
||||
port_id = port.get('port_id')
|
||||
if not device:
|
||||
LOG.debug("update_bandwidth_limit was received for port %s but "
|
||||
"device was not found. It seems that port is already "
|
||||
"deleted", port_id)
|
||||
return
|
||||
|
||||
self._port_rules[port_id][qos_consts.RULE_TYPE_BANDWIDTH_LIMIT] = rule
|
||||
max, burst, min = self._get_port_bw_parameters(port_id)
|
||||
tc_wrapper = tc_lib.TcCommand(device)
|
||||
tc_wrapper.set_bw(max, burst, min, const.EGRESS_DIRECTION)
|
||||
|
||||
@log_helpers.log_method_call
|
||||
def delete_bandwidth_limit(self, port):
|
||||
tc_wrapper = self._get_tc_wrapper(port)
|
||||
tc_wrapper.delete_filters_bw_limit()
|
||||
device = port.get('device')
|
||||
port_id = port.get('port_id')
|
||||
if not device:
|
||||
LOG.debug("delete_bandwidth_limit was received for port %s but "
|
||||
"device was not found. It seems that port is already "
|
||||
"deleted", port_id)
|
||||
return
|
||||
|
||||
self._port_rules[port_id].pop(qos_consts.RULE_TYPE_BANDWIDTH_LIMIT,
|
||||
None)
|
||||
max, burst, min = self._get_port_bw_parameters(port_id)
|
||||
tc_wrapper = tc_lib.TcCommand(device)
|
||||
if not min:
|
||||
tc_wrapper.delete_bw(const.EGRESS_DIRECTION)
|
||||
else:
|
||||
tc_wrapper.set_bw(max, burst, min, const.EGRESS_DIRECTION)
|
||||
|
||||
@log_helpers.log_method_call
|
||||
def create_dscp_marking(self, port, rule):
|
||||
@ -143,8 +173,53 @@ class QosLinuxbridgeAgentDriver(qos.QosLinuxAgentDriver):
|
||||
"mangle", chain_name, ip_version=ip_version)
|
||||
return len(rules_in_chain) == 0
|
||||
|
||||
def _get_tc_wrapper(self, port):
|
||||
return tc_lib.TcCommand(
|
||||
port['device'],
|
||||
cfg.CONF.QOS.kernel_hz,
|
||||
)
|
||||
@log_helpers.log_method_call
|
||||
def create_minimum_bandwidth(self, port, rule):
|
||||
self.update_minimum_bandwidth(port, rule)
|
||||
|
||||
@log_helpers.log_method_call
|
||||
def update_minimum_bandwidth(self, port, rule):
|
||||
device = port.get('device')
|
||||
port_id = port.get('port_id')
|
||||
if not device:
|
||||
LOG.debug("update_minimum_bandwidth was received for port %s but "
|
||||
"device was not found. It seems that port is already "
|
||||
"deleted", port_id)
|
||||
return
|
||||
|
||||
self._port_rules[port_id][
|
||||
qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH] = rule
|
||||
max, burst, min = self._get_port_bw_parameters(port_id)
|
||||
tc_wrapper = tc_lib.TcCommand(device)
|
||||
tc_wrapper.set_bw(max, burst, min, const.EGRESS_DIRECTION)
|
||||
|
||||
@log_helpers.log_method_call
|
||||
def delete_minimum_bandwidth(self, port):
|
||||
device = port.get('device')
|
||||
port_id = port.get('port_id')
|
||||
if not device:
|
||||
LOG.debug("delete_minimum_bandwidth was received for port %s but "
|
||||
"device was not found. It seems that port is already "
|
||||
"deleted", port_id)
|
||||
return
|
||||
|
||||
self._port_rules[port_id].pop(qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH,
|
||||
None)
|
||||
max, burst, min = self._get_port_bw_parameters(port_id)
|
||||
tc_wrapper = tc_lib.TcCommand(device)
|
||||
if not max and not burst:
|
||||
tc_wrapper.delete_bw(const.EGRESS_DIRECTION)
|
||||
else:
|
||||
tc_wrapper.set_bw(max, burst, min, const.EGRESS_DIRECTION)
|
||||
|
||||
def _get_port_bw_parameters(self, port_id):
|
||||
rules = self._port_rules[port_id]
|
||||
if not rules:
|
||||
return None, None, None
|
||||
rule_min = rules.get(qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH)
|
||||
rule_limit = rules.get(qos_consts.RULE_TYPE_BANDWIDTH_LIMIT)
|
||||
min = rule_min.min_kbps if rule_min else None
|
||||
max = rule_limit.max_kbps if rule_limit else None
|
||||
burst = (self._get_egress_burst_value(rule_limit) if rule_limit else
|
||||
None)
|
||||
return max, burst, min
|
||||
|
@ -33,7 +33,8 @@ class LinuxbridgeMechanismDriver(mech_agent.SimpleAgentMechanismDriverBase):
|
||||
"""
|
||||
|
||||
supported_qos_rule_types = [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT,
|
||||
qos_consts.RULE_TYPE_DSCP_MARKING]
|
||||
qos_consts.RULE_TYPE_DSCP_MARKING,
|
||||
qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH]
|
||||
|
||||
def __init__(self):
|
||||
sg_enabled = securitygroups_rpc.is_firewall_enabled()
|
||||
|
@ -176,6 +176,22 @@ class ClientFixture(fixtures.Fixture):
|
||||
|
||||
return rule['dscp_marking_rule']
|
||||
|
||||
def create_minimum_bandwidth_rule(self, tenant_id, qos_policy_id,
|
||||
min_bw=None):
|
||||
rule = {'tenant_id': tenant_id}
|
||||
if min_bw:
|
||||
rule['min_kbps'] = min_bw
|
||||
rule = self.client.create_minimum_bandwidth_rule(
|
||||
policy=qos_policy_id,
|
||||
body={'minimum_bandwidth_rule': rule})
|
||||
|
||||
self.addCleanup(
|
||||
_safe_method(self.client.delete_minimum_bandwidth_rule),
|
||||
rule['minimum_bandwidth_rule']['id'],
|
||||
qos_policy_id)
|
||||
|
||||
return rule['minimum_bandwidth_rule']
|
||||
|
||||
def create_trunk(self, tenant_id, port_id, name=None,
|
||||
admin_state_up=None, sub_ports=None):
|
||||
"""Create a trunk via API.
|
||||
|
@ -18,6 +18,7 @@ from neutron_lib import constants
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from neutron.agent.linux import tc_lib
|
||||
from neutron.common import constants as common_consts
|
||||
from neutron.common import utils
|
||||
from neutron.services.qos import qos_consts
|
||||
from neutron.tests.common.agents import l2_extensions
|
||||
@ -27,8 +28,6 @@ from neutron.tests.fullstack.resources import machine
|
||||
from neutron.tests.fullstack import utils as fullstack_utils
|
||||
from neutron.tests.unit import testlib_api
|
||||
|
||||
from neutron.conf.plugins.ml2.drivers import linuxbridge 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 \
|
||||
@ -39,9 +38,22 @@ load_tests = testlib_api.module_load_tests
|
||||
|
||||
BANDWIDTH_BURST = 100
|
||||
BANDWIDTH_LIMIT = 500
|
||||
MINIMUM_BANDWIDTH = 200
|
||||
DSCP_MARK = 16
|
||||
|
||||
|
||||
def _check_bw_limits(tc, limit, burst, min):
|
||||
# NOTE(ralonsoh): once QoS bw limit rule has 'direction' parameter, this
|
||||
# should be included in the function call. Now EGRESS_DIRECTION is forced.
|
||||
observed = tc.get_limits(common_consts.EGRESS_DIRECTION)
|
||||
if not (limit or burst or min):
|
||||
return observed == (limit, burst, min)
|
||||
elif not min and (limit or burst):
|
||||
return observed == (limit, burst, limit)
|
||||
elif not (limit or burst) and min:
|
||||
return observed[2] == min
|
||||
|
||||
|
||||
class BaseQoSRuleTestCase(object):
|
||||
of_interface = None
|
||||
ovsdb_interface = None
|
||||
@ -104,7 +116,7 @@ class _TestBwLimitQoS(BaseQoSRuleTestCase):
|
||||
|
||||
def _wait_for_bw_rule_removed(self, vm):
|
||||
# No values are provided when port doesn't have qos policy
|
||||
self._wait_for_bw_rule_applied(vm, None, None)
|
||||
self._wait_for_bw_rule_applied(vm)
|
||||
|
||||
def _add_bw_limit_rule(self, limit, burst, qos_policy):
|
||||
qos_policy_id = qos_policy['id']
|
||||
@ -124,7 +136,8 @@ class _TestBwLimitQoS(BaseQoSRuleTestCase):
|
||||
BANDWIDTH_LIMIT, BANDWIDTH_BURST)])
|
||||
bw_rule = qos_policy['rules'][0]
|
||||
|
||||
self._wait_for_bw_rule_applied(vm, BANDWIDTH_LIMIT, BANDWIDTH_BURST)
|
||||
self._wait_for_bw_rule_applied(vm, limit=BANDWIDTH_LIMIT,
|
||||
burst=BANDWIDTH_BURST)
|
||||
qos_policy_id = qos_policy['id']
|
||||
|
||||
self.client.delete_bandwidth_limit_rule(bw_rule['id'], qos_policy_id)
|
||||
@ -138,14 +151,64 @@ class _TestBwLimitQoS(BaseQoSRuleTestCase):
|
||||
)
|
||||
new_rule = self.safe_client.create_bandwidth_limit_rule(
|
||||
self.tenant_id, qos_policy_id, new_limit)
|
||||
self._wait_for_bw_rule_applied(vm, new_limit, new_expected_burst)
|
||||
self._wait_for_bw_rule_applied(vm, limit=new_limit,
|
||||
burst=new_expected_burst)
|
||||
|
||||
# Update qos policy rule id
|
||||
self.client.update_bandwidth_limit_rule(
|
||||
new_rule['id'], qos_policy_id,
|
||||
body={'bandwidth_limit_rule': {'max_kbps': BANDWIDTH_LIMIT,
|
||||
'max_burst_kbps': BANDWIDTH_BURST}})
|
||||
self._wait_for_bw_rule_applied(vm, BANDWIDTH_LIMIT, BANDWIDTH_BURST)
|
||||
self._wait_for_bw_rule_applied(vm, limit=BANDWIDTH_LIMIT,
|
||||
burst=BANDWIDTH_BURST)
|
||||
|
||||
# Remove qos policy from port
|
||||
self.client.update_port(
|
||||
vm.neutron_port['id'],
|
||||
body={'port': {'qos_policy_id': None}})
|
||||
self._wait_for_bw_rule_removed(vm)
|
||||
|
||||
|
||||
class _TestMinimumBwQoS(BaseQoSRuleTestCase):
|
||||
|
||||
number_of_hosts = 1
|
||||
|
||||
def _wait_for_bw_rule_removed(self, vm):
|
||||
# No values are provided when port doesn't have qos policy
|
||||
self._wait_for_bw_rule_applied(vm)
|
||||
|
||||
def _add_min_bw_rule(self, min, qos_policy):
|
||||
qos_policy_id = qos_policy['id']
|
||||
rule = self.safe_client.create_minimum_bandwidth_rule(
|
||||
self.tenant_id, qos_policy_id, min)
|
||||
# Make it consistent with GET reply
|
||||
rule['type'] = qos_consts.RULE_TYPE_MINIMUM_BANDWIDTH
|
||||
rule['qos_policy_id'] = qos_policy_id
|
||||
qos_policy['rules'].append(rule)
|
||||
|
||||
def test_min_bw_qos_policy_rule_lifecycle(self):
|
||||
new_min = MINIMUM_BANDWIDTH + 100
|
||||
|
||||
# Create port with qos policy attached
|
||||
vm, qos_policy = self._prepare_vm_with_qos_policy(
|
||||
[functools.partial(self._add_min_bw_rule, MINIMUM_BANDWIDTH)])
|
||||
bw_rule = qos_policy['rules'][0]
|
||||
|
||||
self._wait_for_bw_rule_applied(vm, min=MINIMUM_BANDWIDTH)
|
||||
qos_policy_id = qos_policy['id']
|
||||
|
||||
self.client.delete_minimum_bandwidth_rule(bw_rule['id'], qos_policy_id)
|
||||
self._wait_for_bw_rule_removed(vm)
|
||||
|
||||
new_rule = self.safe_client.create_minimum_bandwidth_rule(
|
||||
self.tenant_id, qos_policy_id, new_min)
|
||||
self._wait_for_bw_rule_applied(vm, min=new_min)
|
||||
|
||||
# Update qos policy rule id
|
||||
self.client.update_minimum_bandwidth_rule(
|
||||
new_rule['id'], qos_policy_id,
|
||||
body={'minimum_bandwidth_rule': {'min_kbps': MINIMUM_BANDWIDTH}})
|
||||
self._wait_for_bw_rule_applied(vm, min=MINIMUM_BANDWIDTH)
|
||||
|
||||
# Remove qos policy from port
|
||||
self.client.update_port(
|
||||
@ -158,25 +221,32 @@ class TestBwLimitQoSOvs(_TestBwLimitQoS, base.BaseFullStackTestCase):
|
||||
l2_agent_type = constants.AGENT_TYPE_OVS
|
||||
scenarios = fullstack_utils.get_ovs_interface_scenarios()
|
||||
|
||||
def _wait_for_bw_rule_applied(self, vm, limit, burst):
|
||||
def _wait_for_bw_rule_applied(self, vm, limit=None, burst=None):
|
||||
utils.wait_until_true(
|
||||
lambda: vm.bridge.get_egress_bw_limit_for_port(
|
||||
vm.port.name) == (limit, burst))
|
||||
vm.port.name) == (limit, burst),
|
||||
initial_sleep=2)
|
||||
|
||||
|
||||
class TestBwLimitQoSLinuxbridge(_TestBwLimitQoS, base.BaseFullStackTestCase):
|
||||
l2_agent_type = constants.AGENT_TYPE_LINUXBRIDGE
|
||||
|
||||
def _wait_for_bw_rule_applied(self, vm, limit, burst):
|
||||
def _wait_for_bw_rule_applied(self, vm, limit=None, burst=None, min=None):
|
||||
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_filters_bw_limits() == (limit, burst))
|
||||
tc = tc_lib.TcCommand(port_name, namespace=vm.host.host_namespace)
|
||||
utils.wait_until_true(lambda: _check_bw_limits(tc, limit, burst, min))
|
||||
|
||||
|
||||
class TestMinimumBwQoSLinuxbridge(_TestMinimumBwQoS,
|
||||
base.BaseFullStackTestCase):
|
||||
l2_agent_type = constants.AGENT_TYPE_LINUXBRIDGE
|
||||
|
||||
def _wait_for_bw_rule_applied(self, vm, limit=None, burst=None, min=None):
|
||||
port_name = linuxbridge_agent.LinuxBridgeManager.get_tap_device_name(
|
||||
vm.neutron_port['id'])
|
||||
tc = tc_lib.TcCommand(port_name, namespace=vm.host.host_namespace)
|
||||
utils.wait_until_true(lambda: _check_bw_limits(tc, limit, burst, min))
|
||||
|
||||
|
||||
class _TestDscpMarkingQoS(BaseQoSRuleTestCase):
|
||||
|
@ -17,12 +17,10 @@ from neutron.agent.linux import ip_lib
|
||||
from neutron.agent.linux import tc_lib
|
||||
from neutron.tests.functional import base as functional_base
|
||||
|
||||
TEST_HZ_VALUE = 250
|
||||
LATENCY = 50
|
||||
BW_LIMIT = 1024
|
||||
BURST = 512
|
||||
|
||||
BASE_DEV_NAME = "test_tap"
|
||||
BW_LIMIT = 100
|
||||
BURST = 50
|
||||
BW_MIN = 25
|
||||
DIRECTION_EGRESS = 'egress'
|
||||
|
||||
|
||||
class TcLibTestCase(functional_base.BaseSudoTestCase):
|
||||
@ -38,48 +36,44 @@ class TcLibTestCase(functional_base.BaseSudoTestCase):
|
||||
self.addCleanup(tap_device.link.delete)
|
||||
tap_device.link.set_up()
|
||||
|
||||
def test_filters_bandwidth_limit(self):
|
||||
device_name = "%s_filters" % BASE_DEV_NAME
|
||||
def test_bandwidth_limit(self):
|
||||
device_name = "tap_testmax"
|
||||
self.create_device(device_name)
|
||||
tc = tc_lib.TcCommand(device_name, TEST_HZ_VALUE)
|
||||
tc = tc_lib.TcCommand(device_name)
|
||||
|
||||
tc.set_filters_bw_limit(BW_LIMIT, BURST)
|
||||
bw_limit, burst = tc.get_filters_bw_limits()
|
||||
tc.set_bw(BW_LIMIT, BURST, None, DIRECTION_EGRESS)
|
||||
bw_limit, burst, _ = tc.get_limits(DIRECTION_EGRESS)
|
||||
self.assertEqual(BW_LIMIT, bw_limit)
|
||||
self.assertEqual(BURST, burst)
|
||||
|
||||
new_bw_limit = BW_LIMIT + 500
|
||||
new_bw_limit = BW_LIMIT + 100
|
||||
new_burst = BURST + 50
|
||||
|
||||
tc.update_filters_bw_limit(new_bw_limit, new_burst)
|
||||
bw_limit, burst = tc.get_filters_bw_limits()
|
||||
tc.set_bw(new_bw_limit, new_burst, None, DIRECTION_EGRESS)
|
||||
bw_limit, burst, _ = tc.get_limits(DIRECTION_EGRESS)
|
||||
self.assertEqual(new_bw_limit, bw_limit)
|
||||
self.assertEqual(new_burst, burst)
|
||||
|
||||
tc.delete_filters_bw_limit()
|
||||
bw_limit, burst = tc.get_filters_bw_limits()
|
||||
tc.delete_bw(DIRECTION_EGRESS)
|
||||
bw_limit, burst, _ = tc.get_limits(DIRECTION_EGRESS)
|
||||
self.assertIsNone(bw_limit)
|
||||
self.assertIsNone(burst)
|
||||
|
||||
def test_tbf_bandwidth_limit(self):
|
||||
device_name = "%s_tbf" % BASE_DEV_NAME
|
||||
def test_minimum_bandwidth(self):
|
||||
device_name = "tap_testmin"
|
||||
self.create_device(device_name)
|
||||
tc = tc_lib.TcCommand(device_name, TEST_HZ_VALUE)
|
||||
tc = tc_lib.TcCommand(device_name)
|
||||
|
||||
tc.set_tbf_bw_limit(BW_LIMIT, BURST, LATENCY)
|
||||
bw_limit, burst = tc.get_tbf_bw_limits()
|
||||
self.assertEqual(BW_LIMIT, bw_limit)
|
||||
self.assertEqual(BURST, burst)
|
||||
tc.set_bw(None, None, BW_MIN, DIRECTION_EGRESS)
|
||||
_, _, bw_min = tc.get_limits(DIRECTION_EGRESS)
|
||||
self.assertEqual(BW_MIN, bw_min)
|
||||
|
||||
new_bw_limit = BW_LIMIT + 500
|
||||
new_burst = BURST + 50
|
||||
new_bw_min = BW_MIN + 50
|
||||
|
||||
tc.update_tbf_bw_limit(new_bw_limit, new_burst, LATENCY)
|
||||
bw_limit, burst = tc.get_tbf_bw_limits()
|
||||
self.assertEqual(new_bw_limit, bw_limit)
|
||||
self.assertEqual(new_burst, burst)
|
||||
tc.set_bw(None, None, new_bw_min, DIRECTION_EGRESS)
|
||||
_, _, bw_min = tc.get_limits(DIRECTION_EGRESS)
|
||||
self.assertEqual(new_bw_min, bw_min)
|
||||
|
||||
tc.delete_tbf_bw_limit()
|
||||
bw_limit, burst = tc.get_tbf_bw_limits()
|
||||
self.assertIsNone(bw_limit)
|
||||
self.assertIsNone(burst)
|
||||
tc.delete_bw(DIRECTION_EGRESS)
|
||||
_, _, bw_min = tc.get_limits(DIRECTION_EGRESS)
|
||||
self.assertIsNone(bw_min)
|
||||
|
@ -390,6 +390,21 @@ class TestIpWrapper(base.BaseTestCase):
|
||||
run_as_root=True, namespace=None,
|
||||
log_fail_as_error=True)
|
||||
|
||||
def test_add_ifb(self):
|
||||
ip_lib.IPWrapper().add_ifb('ifb-dummy0')
|
||||
self.execute.assert_called_once_with([], 'link',
|
||||
('add', 'ifb-dummy0',
|
||||
'type', 'ifb'),
|
||||
run_as_root=True, namespace=None,
|
||||
log_fail_as_error=True)
|
||||
|
||||
def test_del_ifb(self):
|
||||
ip_lib.IPWrapper().del_ifb('ifb-dummy0')
|
||||
self.execute.assert_called_once_with([], 'link',
|
||||
('del', 'ifb-dummy0'),
|
||||
run_as_root=True, namespace=None,
|
||||
log_fail_as_error=True)
|
||||
|
||||
def test_get_device(self):
|
||||
dev = ip_lib.IPWrapper(namespace='ns').device('eth0')
|
||||
self.assertEqual(dev.namespace, 'ns')
|
||||
|
@ -13,80 +13,69 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import math
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from neutron.agent.linux import ip_lib
|
||||
from neutron.agent.linux import tc_lib
|
||||
from neutron.services.qos import qos_consts
|
||||
from neutron.tests import base
|
||||
|
||||
DEVICE_NAME = "tap_device"
|
||||
KERNEL_HZ_VALUE = 1000
|
||||
BW_LIMIT = 2000 # [kbps]
|
||||
BURST = 100 # [kbit]
|
||||
LATENCY = 50 # [ms]
|
||||
|
||||
TC_QDISC_OUTPUT = (
|
||||
'qdisc tbf 8011: root refcnt 2 rate %(bw)skbit burst %(burst)skbit '
|
||||
'lat 50.0ms \n') % {'bw': BW_LIMIT, 'burst': BURST}
|
||||
|
||||
TC_FILTERS_OUTPUT = (
|
||||
'filter protocol all pref 49152 u32 \nfilter protocol all pref '
|
||||
'49152 u32 fh 800: ht divisor 1 \nfilter protocol all pref 49152 u32 fh '
|
||||
'800::800 order 2048 key ht 800 \n match 00000000/00000000 at 0\n '
|
||||
'police 0x1e rate %(bw)skbit burst %(burst)skbit mtu 2Kb action \n'
|
||||
'drop overhead 0b \n ref 1 bind 1'
|
||||
) % {'bw': BW_LIMIT, 'burst': BURST}
|
||||
|
||||
|
||||
class BaseUnitConversionTest(object):
|
||||
|
||||
def test_convert_to_kilobits_bare_value(self):
|
||||
value = "1000"
|
||||
expected_value = 8 # kbit
|
||||
def test_convert_to_kilo_bare_value(self):
|
||||
value = "10000"
|
||||
expected_value = int(math.ceil(float(80000) / self.base_unit)) # kbit
|
||||
self.assertEqual(
|
||||
expected_value,
|
||||
tc_lib.convert_to_kilobits(value, self.base_unit)
|
||||
tc_lib.convert_to_kilo(value, self.base_unit)
|
||||
)
|
||||
|
||||
def test_convert_to_kilobits_bytes_value(self):
|
||||
value = "1000b"
|
||||
expected_value = 8 # kbit
|
||||
def test_convert_to_kilo_bytes_value(self):
|
||||
value = "10000b"
|
||||
expected_value = int(math.ceil(float(80000) / self.base_unit)) # kbit
|
||||
self.assertEqual(
|
||||
expected_value,
|
||||
tc_lib.convert_to_kilobits(value, self.base_unit)
|
||||
tc_lib.convert_to_kilo(value, self.base_unit)
|
||||
)
|
||||
|
||||
def test_convert_to_kilobits_bits_value(self):
|
||||
def test_convert_to_kilo_bits_value(self):
|
||||
value = "1000bit"
|
||||
expected_value = tc_lib.bits_to_kilobits(1000, self.base_unit)
|
||||
expected_value = int(math.ceil(float(1000) / self.base_unit))
|
||||
self.assertEqual(
|
||||
expected_value,
|
||||
tc_lib.convert_to_kilobits(value, self.base_unit)
|
||||
tc_lib.convert_to_kilo(value, self.base_unit)
|
||||
)
|
||||
|
||||
def test_convert_to_kilobits_megabytes_value(self):
|
||||
def test_convert_to_kilo_megabytes_value(self):
|
||||
value = "1m"
|
||||
expected_value = tc_lib.bits_to_kilobits(
|
||||
self.base_unit ** 2 * 8, self.base_unit)
|
||||
expected_value = int(math.ceil(float(self.base_unit ** 2 * 8) /
|
||||
self.base_unit))
|
||||
self.assertEqual(
|
||||
expected_value,
|
||||
tc_lib.convert_to_kilobits(value, self.base_unit)
|
||||
tc_lib.convert_to_kilo(value, self.base_unit)
|
||||
)
|
||||
|
||||
def test_convert_to_kilobits_megabits_value(self):
|
||||
def test_convert_to_kilo_megabits_value(self):
|
||||
value = "1mbit"
|
||||
expected_value = tc_lib.bits_to_kilobits(
|
||||
self.base_unit ** 2, self.base_unit)
|
||||
expected_value = int(math.ceil(float(self.base_unit ** 2) /
|
||||
self.base_unit))
|
||||
self.assertEqual(
|
||||
expected_value,
|
||||
tc_lib.convert_to_kilobits(value, self.base_unit)
|
||||
tc_lib.convert_to_kilo(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
|
||||
tc_lib.convert_to_kilo, value, self.base_unit
|
||||
)
|
||||
|
||||
def test_bytes_to_bits(self):
|
||||
@ -139,166 +128,659 @@ class TestIECUnitConversions(BaseUnitConversionTest, base.BaseTestCase):
|
||||
|
||||
|
||||
class TestTcCommand(base.BaseTestCase):
|
||||
MAX_RATE = 10000
|
||||
BURST_RATE = 8000
|
||||
CBURST_RATE = 1500
|
||||
MIN_RATE = 1500
|
||||
RATE_LIMIT = 8
|
||||
DIRECTION_EGRESS = 'egress'
|
||||
DIRECTION_INGRESS = 'ingress'
|
||||
DEVICE_NAME = 'tap-test-dev'
|
||||
IFB_NAME = 'ifb-test-dev'
|
||||
CLASS_PARENT = '10:'
|
||||
CLASSID = '10:1'
|
||||
QDISC_PARENT = '20:2'
|
||||
QDISC_HANDLE = '30:'
|
||||
QDISC_ROOT = 'root'
|
||||
QDISC_INGRESS = 'ingress'
|
||||
QDISC_INGRESS_HANDLE = 'ffff:'
|
||||
FILTER_PARENT = CLASS_PARENT
|
||||
FILTER_PROTOCOL = ['all', 'u32']
|
||||
FILTER_FILTER = ['match', 'u32', '0', '0']
|
||||
FILTER_ACTION = ['mirred', 'egress', 'redirect', 'dev', IFB_NAME]
|
||||
TYPE_HTB = 'htb'
|
||||
|
||||
def _call_qdisc_add(self, device, parent, handle, qdisc_type):
|
||||
cmd = ['tc', 'qdisc', 'add', 'dev', device]
|
||||
if parent in [self.QDISC_ROOT, self.QDISC_INGRESS]:
|
||||
cmd += [parent]
|
||||
else:
|
||||
cmd += ['parent', parent]
|
||||
qdisc_type = '' if qdisc_type is None else qdisc_type
|
||||
cmd += ['handle', handle, qdisc_type]
|
||||
return cmd
|
||||
|
||||
def _call_qdisc_del(self, device, parent):
|
||||
cmd = ['tc', 'qdisc', 'del', 'dev', device]
|
||||
if parent in [self.QDISC_ROOT, self.QDISC_INGRESS]:
|
||||
cmd += [parent]
|
||||
else:
|
||||
cmd += ['parent', parent]
|
||||
return cmd
|
||||
|
||||
@staticmethod
|
||||
def _call_qdisc_show(device):
|
||||
return ['tc', 'qdisc', 'show', 'dev', device]
|
||||
|
||||
def _call_class_replace(self, device, parent, classid, type, rate, ceil,
|
||||
burst):
|
||||
cmd = ['class', 'replace', 'dev', device]
|
||||
if parent:
|
||||
cmd += ['parent', parent]
|
||||
rate = self.RATE_LIMIT if rate < self.RATE_LIMIT else rate
|
||||
cmd += ['classid', classid, type, 'rate', rate]
|
||||
if ceil:
|
||||
ceil = rate if ceil < rate else ceil
|
||||
cmd += ['ceil', ceil]
|
||||
if burst:
|
||||
cmd += ['burst', burst]
|
||||
return cmd
|
||||
|
||||
@staticmethod
|
||||
def _call_class_show(device):
|
||||
return ['tc', 'class', 'show', 'dev', device]
|
||||
|
||||
@staticmethod
|
||||
def _call_filter_add(device, parent, protocol, filter, action):
|
||||
cmd = ['tc', 'filter', 'add', 'dev', device, 'parent', parent,
|
||||
'protocol'] + protocol + filter
|
||||
if action:
|
||||
cmd += ['action'] + action
|
||||
return cmd
|
||||
|
||||
@staticmethod
|
||||
def _call_filter_show(device, parent):
|
||||
return ['tc', 'filter', 'show', 'dev', device, 'parent', parent]
|
||||
|
||||
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.tc = tc_lib.TcCommand(self.DEVICE_NAME)
|
||||
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_set_bw_egress(self):
|
||||
with mock.patch.object(self.tc, '_set_ingress_bw') as \
|
||||
mock_set_ingress_bw:
|
||||
self.tc.set_bw(self.MAX_RATE,
|
||||
self.BURST_RATE,
|
||||
self.MIN_RATE,
|
||||
self.DIRECTION_EGRESS)
|
||||
mock_set_ingress_bw.assert_called_once_with(
|
||||
self.MAX_RATE * tc_lib.SI_BASE,
|
||||
(self.BURST_RATE * tc_lib.IEC_BASE) / 8,
|
||||
self.MIN_RATE * tc_lib.SI_BASE)
|
||||
|
||||
def test_get_filters_bw_limits(self):
|
||||
self.execute.return_value = TC_FILTERS_OUTPUT
|
||||
bw_limit, burst_limit = self.tc.get_filters_bw_limits()
|
||||
self.assertEqual(BW_LIMIT, bw_limit)
|
||||
self.assertEqual(BURST, burst_limit)
|
||||
def test_set_bw_ingress(self):
|
||||
with testtools.ExpectedException(NotImplementedError):
|
||||
self.tc.set_bw(self.MAX_RATE, self.BURST_RATE, self.MIN_RATE,
|
||||
self.DIRECTION_INGRESS)
|
||||
|
||||
def test_get_filters_bw_limits_when_output_not_match(self):
|
||||
output = (
|
||||
"Some different "
|
||||
"output from command:"
|
||||
"tc filters show dev XXX parent ffff:"
|
||||
)
|
||||
self.execute.return_value = output
|
||||
bw_limit, burst_limit = self.tc.get_filters_bw_limits()
|
||||
self.assertIsNone(bw_limit)
|
||||
self.assertIsNone(burst_limit)
|
||||
def test_delete_bw_egress(self):
|
||||
with mock.patch.object(self.tc, '_delete_ingress') as \
|
||||
mock_delete_ingress:
|
||||
self.tc.delete_bw(self.DIRECTION_EGRESS)
|
||||
mock_delete_ingress.assert_called_once_with()
|
||||
|
||||
def test_get_filters_bw_limits_when_wrong_units(self):
|
||||
output = TC_FILTERS_OUTPUT.replace("kbit", "Xbit")
|
||||
self.execute.return_value = output
|
||||
self.assertRaises(tc_lib.InvalidUnit, self.tc.get_filters_bw_limits)
|
||||
def test_delete_bw_ingress(self):
|
||||
with testtools.ExpectedException(NotImplementedError):
|
||||
self.tc.delete_bw(self.DIRECTION_INGRESS)
|
||||
|
||||
def test_get_tbf_bw_limits(self):
|
||||
self.execute.return_value = TC_QDISC_OUTPUT
|
||||
bw_limit, burst_limit = self.tc.get_tbf_bw_limits()
|
||||