Implement add_tc_qdisc and list_tc_qdiscs using pyroute2
TcCommand.set_tbf_bw_limit() is used now to set and replace a TC TBF filter. Related-Bug: #1560963 Change-Id: I162dea499d16db76692dd3d6d99b6be45f44ae59
This commit is contained in:
parent
8914f8247f
commit
d7cefa56e4
|
@ -13,17 +13,25 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import math
|
||||
import re
|
||||
|
||||
from neutron_lib import exceptions
|
||||
from neutron_lib.exceptions import qos as qos_exc
|
||||
from neutron_lib.services.qos import constants as qos_consts
|
||||
from oslo_log import log as logging
|
||||
from pyroute2.netlink import rtnl
|
||||
from pyroute2.netlink.rtnl.tcmsg import common as rtnl_common
|
||||
|
||||
from neutron._i18n import _
|
||||
from neutron.agent.linux import ip_lib
|
||||
from neutron.common import constants
|
||||
from neutron.common import utils
|
||||
from neutron.privileged.agent.linux import tc_lib as priv_tc_lib
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
INGRESS_QDISC_ID = "ffff:"
|
||||
MAX_MTU_VALUE = 65535
|
||||
|
||||
|
@ -43,6 +51,12 @@ 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*")
|
||||
|
||||
TC_QDISC_TYPES = ['htb', 'tbf', 'ingress']
|
||||
|
||||
TC_QDISC_PARENT = {'root': rtnl.TC_H_ROOT,
|
||||
'ingress': rtnl.TC_H_INGRESS}
|
||||
TC_QDISC_PARENT_NAME = {v: k for k, v in TC_QDISC_PARENT.items()}
|
||||
|
||||
|
||||
class InvalidKernelHzValue(exceptions.NeutronException):
|
||||
message = _("Kernel HZ value %(value)s is not valid. This value must be "
|
||||
|
@ -80,6 +94,56 @@ def convert_to_kilobits(value, base):
|
|||
return utils.bits_to_kilobits(bits_value, base)
|
||||
|
||||
|
||||
def _get_attr(pyroute2_obj, attr_name):
|
||||
rule_attrs = pyroute2_obj.get('attrs', [])
|
||||
for attr in (attr for attr in rule_attrs if attr[0] == attr_name):
|
||||
return attr[1]
|
||||
return
|
||||
|
||||
|
||||
def _get_tbf_burst_value(rate, burst_limit, kernel_hz):
|
||||
min_burst_value = float(rate) / float(kernel_hz)
|
||||
return max(min_burst_value, burst_limit)
|
||||
|
||||
|
||||
def _calc_burst(rate, buffer):
|
||||
"""Calculate burst rate
|
||||
|
||||
:param rate: (int) rate in bytes per second.
|
||||
:param buffer: (int) buffer size in bytes.
|
||||
:return: (int) burst in bytes
|
||||
"""
|
||||
# NOTE(ralonsoh): this function is based in
|
||||
# pyroute2.netlink.rtnl.tcmsg.common.calc_xmittime
|
||||
return int(math.ceil(
|
||||
float(buffer * rate) /
|
||||
(rtnl_common.TIME_UNITS_PER_SEC * rtnl_common.tick_in_usec)))
|
||||
|
||||
|
||||
def _calc_latency_ms(limit, burst, rate):
|
||||
"""Calculate latency value, in ms
|
||||
|
||||
:param limit: (int) pyroute2 limit value
|
||||
:param burst: (int) burst in bytes
|
||||
:param rate: (int) maximum bandwidth in kbytes per second
|
||||
:return: (int) latency, in ms
|
||||
"""
|
||||
return int(math.ceil(
|
||||
float((limit - burst) * rtnl_common.TIME_UNITS_PER_SEC) /
|
||||
(rate * 1000)))
|
||||
|
||||
|
||||
def _handle_from_hex_to_string(handle):
|
||||
"""Convert TC handle from hex to string
|
||||
|
||||
:param handle: (int) TC handle
|
||||
:return: (string) handle formatted to string: 0xMMMMmmmm -> "M:m"
|
||||
"""
|
||||
minor = str(handle & 0xFFFF)
|
||||
major = str((handle & 0xFFFF0000) >> 16)
|
||||
return ':'.join([major, minor])
|
||||
|
||||
|
||||
class TcCommand(ip_lib.IPDevice):
|
||||
|
||||
def __init__(self, name, kernel_hz, namespace=None):
|
||||
|
@ -123,23 +187,15 @@ class TcCommand(ip_lib.IPDevice):
|
|||
return None, None
|
||||
|
||||
def get_tbf_bw_limits(self):
|
||||
cmd = ['qdisc', 'show', 'dev', self.name]
|
||||
cmd_result = self._execute_tc_cmd(cmd)
|
||||
if not cmd_result:
|
||||
qdiscs = list_tc_qdiscs(self.name, namespace=self.namespace)
|
||||
if not qdiscs:
|
||||
return None, None
|
||||
m = tbf_pattern.match(cmd_result)
|
||||
if not m:
|
||||
|
||||
qdisc = qdiscs[0]
|
||||
if qdisc['qdisc_type'] != 'tbf':
|
||||
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), constants.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), constants.IEC_BASE)
|
||||
return bw_limit, burst_limit
|
||||
|
||||
return qdisc['max_kbps'], qdisc['burst_kb']
|
||||
|
||||
def set_filters_bw_limit(self, bw_limit, burst_limit):
|
||||
"""Set ingress qdisc and filter for police ingress traffic on device
|
||||
|
@ -155,21 +211,21 @@ class TcCommand(ip_lib.IPDevice):
|
|||
return self.update_filters_bw_limit(bw_limit, burst_limit)
|
||||
|
||||
def set_tbf_bw_limit(self, bw_limit, burst_limit, latency_value):
|
||||
"""Set token bucket filter qdisc on device
|
||||
"""Set/update token bucket filter qdisc on device
|
||||
|
||||
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)
|
||||
return add_tc_qdisc(self.name, 'tbf', parent='root',
|
||||
max_kbps=bw_limit, burst_kb=burst_limit,
|
||||
latency_ms=latency_value, kernel_hz=self.kernel_hz,
|
||||
namespace=self.namespace)
|
||||
|
||||
def update_filters_bw_limit(self, bw_limit, burst_limit,
|
||||
qdisc_id=INGRESS_QDISC_ID):
|
||||
def update_filters_bw_limit(self, bw_limit, burst_limit):
|
||||
self.delete_filters_bw_limit()
|
||||
return self._set_filters_bw_limit(bw_limit, burst_limit, qdisc_id)
|
||||
|
||||
def update_tbf_bw_limit(self, bw_limit, burst_limit, latency_value):
|
||||
return self._replace_tbf_qdisc(bw_limit, burst_limit, latency_value)
|
||||
add_tc_qdisc(self.name, 'ingress', namespace=self.namespace)
|
||||
return self._add_policy_filter(bw_limit, burst_limit)
|
||||
|
||||
def delete_filters_bw_limit(self):
|
||||
# NOTE(slaweq): For limit traffic egress from instance we need to use
|
||||
|
@ -179,13 +235,6 @@ class TcCommand(ip_lib.IPDevice):
|
|||
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]
|
||||
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
|
||||
|
@ -195,24 +244,6 @@ class TcCommand(ip_lib.IPDevice):
|
|||
# If the device doesn't exist, the qdisc is already deleted.
|
||||
return self._execute_tc_cmd(cmd, extra_ok_codes=[1, 2])
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
]
|
||||
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)
|
||||
|
@ -232,3 +263,79 @@ class TcCommand(ip_lib.IPDevice):
|
|||
'mtu', MAX_MTU_VALUE,
|
||||
'drop']
|
||||
return self._execute_tc_cmd(cmd)
|
||||
|
||||
|
||||
def add_tc_qdisc(device, qdisc_type, parent=None, handle=None, latency_ms=None,
|
||||
max_kbps=None, burst_kb=None, kernel_hz=None,
|
||||
namespace=None):
|
||||
"""Add/replace a TC qdisc on a device
|
||||
|
||||
pyroute2 input parameters:
|
||||
- rate (min bw): bytes/second
|
||||
- burst: bytes
|
||||
- latency: us
|
||||
|
||||
:param device: (string) device name
|
||||
:param qdisc_type: (string) qdisc type (TC_QDISC_TYPES)
|
||||
:param parent: (string) qdisc parent class ('root', '2:10')
|
||||
:param handle: (string, int) (required for HTB) major handler identifier
|
||||
(0xffff0000, '1', '1:', '1:0') [1]
|
||||
:param latency_ms: (string, int) (required for TBF) latency time in ms
|
||||
:param max_kbps: (string, int) (required for TBF) maximum bandwidth in
|
||||
kbits per second.
|
||||
:param burst_kb: (string, int) (required for TBF) maximum bandwidth in
|
||||
kbits.
|
||||
:param kernel_hz: (string, int) (required for TBF) kernel HZ.
|
||||
:param namespace: (string) (optional) namespace name
|
||||
|
||||
[1] https://lartc.org/howto/lartc.qdisc.classful.html
|
||||
"""
|
||||
if qdisc_type and qdisc_type not in TC_QDISC_TYPES:
|
||||
raise qos_exc.TcLibQdiscTypeError(
|
||||
qdisc_type=qdisc_type, supported_qdisc_types=TC_QDISC_TYPES)
|
||||
|
||||
args = {'kind': qdisc_type}
|
||||
if qdisc_type in ['htb', 'ingress']:
|
||||
if handle:
|
||||
args['handle'] = str(handle).split(':')[0] + ':0'
|
||||
elif qdisc_type == 'tbf':
|
||||
if not latency_ms or not max_kbps or not kernel_hz:
|
||||
raise qos_exc.TcLibQdiscNeededArguments(
|
||||
qdisc_type=qdisc_type,
|
||||
needed_arguments=['latency_ms', 'max_kbps', 'kernel_hz'])
|
||||
args['burst'] = int(
|
||||
_get_tbf_burst_value(max_kbps, burst_kb, kernel_hz) * 1024 / 8)
|
||||
args['rate'] = int(max_kbps * 1024 / 8)
|
||||
args['latency'] = latency_ms * 1000
|
||||
if parent:
|
||||
args['parent'] = rtnl.TC_H_ROOT if parent == 'root' else parent
|
||||
priv_tc_lib.add_tc_qdisc(device, namespace=namespace, **args)
|
||||
|
||||
|
||||
def list_tc_qdiscs(device, namespace=None):
|
||||
"""List all TC qdiscs of a device
|
||||
|
||||
:param device: (string) device name
|
||||
:param namespace: (string) (optional) namespace name
|
||||
:return: (list) TC qdiscs
|
||||
"""
|
||||
qdiscs = priv_tc_lib.list_tc_qdiscs(device, namespace=namespace)
|
||||
retval = []
|
||||
for qdisc in qdiscs:
|
||||
qdisc_attrs = {
|
||||
'qdisc_type': _get_attr(qdisc, 'TCA_KIND'),
|
||||
'parent': TC_QDISC_PARENT_NAME.get(
|
||||
qdisc['parent'], _handle_from_hex_to_string(qdisc['parent'])),
|
||||
'handle': _handle_from_hex_to_string(qdisc['handle'])}
|
||||
if qdisc_attrs['qdisc_type'] == 'tbf':
|
||||
tca_options = _get_attr(qdisc, 'TCA_OPTIONS')
|
||||
tca_tbf_parms = _get_attr(tca_options, 'TCA_TBF_PARMS')
|
||||
qdisc_attrs['max_kbps'] = int(tca_tbf_parms['rate'] * 8 / 1024)
|
||||
burst_bytes = _calc_burst(tca_tbf_parms['rate'],
|
||||
tca_tbf_parms['buffer'])
|
||||
qdisc_attrs['burst_kb'] = int(burst_bytes * 8 / 1024)
|
||||
qdisc_attrs['latency_ms'] = _calc_latency_ms(
|
||||
tca_tbf_parms['limit'], burst_bytes, tca_tbf_parms['rate'])
|
||||
retval.append(qdisc_attrs)
|
||||
|
||||
return retval
|
||||
|
|
|
@ -90,7 +90,7 @@ class QosLinuxbridgeAgentDriver(qos.QosLinuxAgentDriver):
|
|||
def update_bandwidth_limit(self, port, rule):
|
||||
tc_wrapper = self._get_tc_wrapper(port)
|
||||
if rule.direction == const.INGRESS_DIRECTION:
|
||||
tc_wrapper.update_tbf_bw_limit(
|
||||
tc_wrapper.set_tbf_bw_limit(
|
||||
rule.max_kbps, rule.max_burst_kbps, self.tbf_latency)
|
||||
else:
|
||||
tc_wrapper.update_filters_bw_limit(
|
||||
|
|
|
@ -165,7 +165,7 @@ def get_routing_table(ip_version, namespace=None):
|
|||
return routes
|
||||
|
||||
|
||||
def _get_iproute(namespace):
|
||||
def get_iproute(namespace):
|
||||
# From iproute.py:
|
||||
# `IPRoute` -- RTNL API to the current network namespace
|
||||
# `NetNS` -- RTNL API to another network namespace
|
||||
|
@ -184,9 +184,9 @@ def _translate_ip_device_exception(e, device=None, namespace=None):
|
|||
namespace=namespace)
|
||||
|
||||
|
||||
def _get_link_id(device, namespace):
|
||||
def get_link_id(device, namespace):
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
with get_iproute(namespace) as ip:
|
||||
return ip.link_lookup(ifname=device)[0]
|
||||
except IndexError:
|
||||
raise NetworkInterfaceNotFound(device=device, namespace=namespace)
|
||||
|
@ -194,8 +194,8 @@ def _get_link_id(device, namespace):
|
|||
|
||||
def _run_iproute_link(command, device, namespace=None, **kwargs):
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
idx = _get_link_id(device, namespace)
|
||||
with get_iproute(namespace) as ip:
|
||||
idx = get_link_id(device, namespace)
|
||||
return ip.link(command, index=idx, **kwargs)
|
||||
except NetlinkError as e:
|
||||
_translate_ip_device_exception(e, device, namespace)
|
||||
|
@ -208,8 +208,8 @@ def _run_iproute_link(command, device, namespace=None, **kwargs):
|
|||
|
||||
def _run_iproute_neigh(command, device, namespace, **kwargs):
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
idx = _get_link_id(device, namespace)
|
||||
with get_iproute(namespace) as ip:
|
||||
idx = get_link_id(device, namespace)
|
||||
return ip.neigh(command, ifindex=idx, **kwargs)
|
||||
except NetlinkError as e:
|
||||
_translate_ip_device_exception(e, device, namespace)
|
||||
|
@ -222,8 +222,8 @@ def _run_iproute_neigh(command, device, namespace, **kwargs):
|
|||
|
||||
def _run_iproute_addr(command, device, namespace, **kwargs):
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
idx = _get_link_id(device, namespace)
|
||||
with get_iproute(namespace) as ip:
|
||||
idx = get_link_id(device, namespace)
|
||||
return ip.addr(command, index=idx, **kwargs)
|
||||
except NetlinkError as e:
|
||||
_translate_ip_device_exception(e, device, namespace)
|
||||
|
@ -289,8 +289,8 @@ def delete_ip_address(ip_version, ip, prefixlen, device, namespace):
|
|||
def flush_ip_addresses(ip_version, device, namespace):
|
||||
family = _IP_VERSION_FAMILY_MAP[ip_version]
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
idx = _get_link_id(device, namespace)
|
||||
with get_iproute(namespace) as ip:
|
||||
idx = get_link_id(device, namespace)
|
||||
ip.flush_addr(index=idx, family=family)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
|
@ -306,11 +306,11 @@ def flush_ip_addresses(ip_version, device, namespace):
|
|||
def create_interface(ifname, namespace, kind, **kwargs):
|
||||
ifname = ifname[:constants.DEVICE_NAME_MAX_LEN]
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
with get_iproute(namespace) as ip:
|
||||
physical_interface = kwargs.pop("physical_interface", None)
|
||||
if physical_interface:
|
||||
link_key = "vxlan_link" if kind == "vxlan" else "link"
|
||||
kwargs[link_key] = _get_link_id(physical_interface, namespace)
|
||||
kwargs[link_key] = get_link_id(physical_interface, namespace)
|
||||
return ip.link("add", ifname=ifname, kind=kind, **kwargs)
|
||||
except NetlinkError as e:
|
||||
if e.code == errno.EEXIST:
|
||||
|
@ -338,7 +338,7 @@ def delete_interface(ifname, namespace, **kwargs):
|
|||
@lockutils.synchronized("privileged-ip-lib")
|
||||
def interface_exists(ifname, namespace):
|
||||
try:
|
||||
idx = _get_link_id(ifname, namespace)
|
||||
idx = get_link_id(ifname, namespace)
|
||||
return bool(idx)
|
||||
except NetworkInterfaceNotFound:
|
||||
return False
|
||||
|
@ -522,20 +522,20 @@ def list_netns(**kwargs):
|
|||
return netns.listnetns(**kwargs)
|
||||
|
||||
|
||||
def _make_serializable(value):
|
||||
def make_serializable(value):
|
||||
"""Make a pyroute2 object serializable
|
||||
|
||||
This function converts 'netlink.nla_slot' object (key, value) in a list
|
||||
of two elements.
|
||||
"""
|
||||
if isinstance(value, list):
|
||||
return [_make_serializable(item) for item in value]
|
||||
return [make_serializable(item) for item in value]
|
||||
elif isinstance(value, dict):
|
||||
return {key: _make_serializable(data) for key, data in value.items()}
|
||||
return {key: make_serializable(data) for key, data in value.items()}
|
||||
elif isinstance(value, netlink.nla_slot):
|
||||
return [value[0], _make_serializable(value[1])]
|
||||
return [value[0], make_serializable(value[1])]
|
||||
elif isinstance(value, tuple):
|
||||
return tuple(_make_serializable(item) for item in value)
|
||||
return tuple(make_serializable(item) for item in value)
|
||||
return value
|
||||
|
||||
|
||||
|
@ -550,8 +550,8 @@ def get_link_devices(namespace, **kwargs):
|
|||
:return: (list) interfaces in a namespace
|
||||
"""
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
return _make_serializable(ip.get_links(**kwargs))
|
||||
with get_iproute(namespace) as ip:
|
||||
return make_serializable(ip.get_links(**kwargs))
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise NetworkNamespaceNotFound(netns_name=namespace)
|
||||
|
@ -584,8 +584,8 @@ def get_ip_addresses(namespace, **kwargs):
|
|||
:return: (tuple) IP addresses in a namespace
|
||||
"""
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
return _make_serializable(ip.get_addr(**kwargs))
|
||||
with get_iproute(namespace) as ip:
|
||||
return make_serializable(ip.get_addr(**kwargs))
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise NetworkNamespaceNotFound(netns_name=namespace)
|
||||
|
@ -600,7 +600,7 @@ def get_ip_addresses(namespace, **kwargs):
|
|||
def list_ip_rules(namespace, ip_version, match=None, **kwargs):
|
||||
"""List all IP rules"""
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
with get_iproute(namespace) as ip:
|
||||
rules = ip.get_rules(family=_IP_VERSION_FAMILY_MAP[ip_version],
|
||||
match=match, **kwargs)
|
||||
for rule in rules:
|
||||
|
@ -623,7 +623,7 @@ def list_ip_rules(namespace, ip_version, match=None, **kwargs):
|
|||
def add_ip_rule(namespace, **kwargs):
|
||||
"""Add a new IP rule"""
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
with get_iproute(namespace) as ip:
|
||||
ip.rule('add', **kwargs)
|
||||
except netlink_exceptions.NetlinkError as e:
|
||||
if e.code == errno.EEXIST:
|
||||
|
@ -643,7 +643,7 @@ def add_ip_rule(namespace, **kwargs):
|
|||
def delete_ip_rule(namespace, **kwargs):
|
||||
"""Delete an IP rule"""
|
||||
try:
|
||||
with _get_iproute(namespace) as ip:
|
||||
with get_iproute(namespace) as ip:
|
||||
ip.rule('del', **kwargs)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# 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 errno
|
||||
import socket
|
||||
|
||||
from neutron_lib import constants as n_constants
|
||||
|
||||
from neutron import privileged
|
||||
from neutron.privileged.agent.linux import ip_lib
|
||||
|
||||
|
||||
_IP_VERSION_FAMILY_MAP = {n_constants.IP_VERSION_4: socket.AF_INET,
|
||||
n_constants.IP_VERSION_6: socket.AF_INET6}
|
||||
|
||||
|
||||
@privileged.default.entrypoint
|
||||
def add_tc_qdisc(device, namespace=None, **kwargs):
|
||||
"""Add TC qdisc"""
|
||||
index = ip_lib.get_link_id(device, namespace)
|
||||
try:
|
||||
with ip_lib.get_iproute(namespace) as ip:
|
||||
ip.tc('replace', index=index, **kwargs)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace)
|
||||
raise
|
||||
|
||||
|
||||
@privileged.default.entrypoint
|
||||
def list_tc_qdiscs(device, namespace=None):
|
||||
"""List all TC qdiscs of a device"""
|
||||
index = ip_lib.get_link_id(device, namespace)
|
||||
try:
|
||||
with ip_lib.get_iproute(namespace) as ip:
|
||||
return ip_lib.make_serializable(ip.get_qdiscs(index=index))
|
||||
except OSError as e:
|
||||
if e.errno == errno.ENOENT:
|
||||
raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace)
|
||||
raise
|
|
@ -302,15 +302,9 @@ class TestBwLimitQoSLinuxbridge(_TestBwLimitQoS, base.BaseFullStackTestCase):
|
|||
|
||||
@staticmethod
|
||||
def _get_expected_ingress_burst_value(limit):
|
||||
# calculate expected burst in same way as it's done in tc_lib but
|
||||
# burst value = 0 so it's always value calculated from kernel's hz
|
||||
# value
|
||||
# as in tc_lib.bits_to_kilobits result is rounded up that even
|
||||
# 1 bit gives 1 kbit same should be added here to expected burst
|
||||
# value
|
||||
return int(
|
||||
float(limit) /
|
||||
float(linuxbridge_agent_config.DEFAULT_KERNEL_HZ_VALUE) + 1)
|
||||
float(linuxbridge_agent_config.DEFAULT_KERNEL_HZ_VALUE))
|
||||
|
||||
def _wait_for_bw_rule_applied(self, vm, limit, burst, direction):
|
||||
port_name = linuxbridge_agent.LinuxBridgeManager.get_tap_device_name(
|
||||
|
|
|
@ -74,7 +74,7 @@ class TcLibTestCase(functional_base.BaseSudoTestCase):
|
|||
new_bw_limit = BW_LIMIT + 500
|
||||
new_burst = BURST + 50
|
||||
|
||||
tc.update_tbf_bw_limit(new_bw_limit, new_burst, LATENCY)
|
||||
tc.set_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)
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
# Copyright 2018 Red Hat, Inc.
|
||||
#
|
||||
# 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_utils import uuidutils
|
||||
from pyroute2.netlink import rtnl
|
||||
|
||||
from neutron.agent.linux import tc_lib
|
||||
from neutron.privileged.agent.linux import ip_lib as priv_ip_lib
|
||||
from neutron.privileged.agent.linux import tc_lib as priv_tc_lib
|
||||
from neutron.tests.functional import base as functional_base
|
||||
|
||||
|
||||
class TcQdiscTestCase(functional_base.BaseSudoTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TcQdiscTestCase, self).setUp()
|
||||
self.namespace = 'ns_test-' + uuidutils.generate_uuid()
|
||||
priv_ip_lib.create_netns(self.namespace)
|
||||
self.addCleanup(self._remove_ns, self.namespace)
|
||||
self.device = 'int_dummy'
|
||||
priv_ip_lib.create_interface(self.device, self.namespace, 'dummy')
|
||||
|
||||
def _remove_ns(self, namespace):
|
||||
priv_ip_lib.remove_netns(namespace)
|
||||
|
||||
def test_add_tc_qdisc_htb(self):
|
||||
priv_tc_lib.add_tc_qdisc(
|
||||
self.device, parent=rtnl.TC_H_ROOT, kind='htb', handle='5:',
|
||||
namespace=self.namespace)
|
||||
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
|
||||
namespace=self.namespace)
|
||||
self.assertEqual(1, len(qdiscs))
|
||||
self.assertEqual(rtnl.TC_H_ROOT, qdiscs[0]['parent'])
|
||||
self.assertEqual(0x50000, qdiscs[0]['handle'])
|
||||
self.assertEqual('htb', tc_lib._get_attr(qdiscs[0], 'TCA_KIND'))
|
||||
|
||||
def test_add_tc_qdisc_htb_no_handle(self):
|
||||
priv_tc_lib.add_tc_qdisc(
|
||||
self.device, parent=rtnl.TC_H_ROOT, kind='htb',
|
||||
namespace=self.namespace)
|
||||
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
|
||||
namespace=self.namespace)
|
||||
self.assertEqual(1, len(qdiscs))
|
||||
self.assertEqual(rtnl.TC_H_ROOT, qdiscs[0]['parent'])
|
||||
self.assertEqual(0, qdiscs[0]['handle'] & 0xFFFF)
|
||||
self.assertEqual('htb', tc_lib._get_attr(qdiscs[0], 'TCA_KIND'))
|
||||
|
||||
def test_add_tc_qdisc_tbf(self):
|
||||
burst = 192000
|
||||
rate = 320000
|
||||
latency = 50000
|
||||
priv_tc_lib.add_tc_qdisc(
|
||||
self.device, parent=rtnl.TC_H_ROOT, kind='tbf', burst=burst,
|
||||
rate=rate, latency=latency, namespace=self.namespace)
|
||||
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
|
||||
namespace=self.namespace)
|
||||
self.assertEqual(1, len(qdiscs))
|
||||
self.assertEqual(rtnl.TC_H_ROOT, qdiscs[0]['parent'])
|
||||
self.assertEqual('tbf', tc_lib._get_attr(qdiscs[0], 'TCA_KIND'))
|
||||
tca_options = tc_lib._get_attr(qdiscs[0], 'TCA_OPTIONS')
|
||||
tca_tbf_parms = tc_lib._get_attr(tca_options, 'TCA_TBF_PARMS')
|
||||
self.assertEqual(rate, tca_tbf_parms['rate'])
|
||||
self.assertEqual(burst, tc_lib._calc_burst(tca_tbf_parms['rate'],
|
||||
tca_tbf_parms['buffer']))
|
||||
self.assertEqual(latency, tc_lib._calc_latency_ms(
|
||||
tca_tbf_parms['limit'], burst, tca_tbf_parms['rate']) * 1000)
|
||||
|
||||
def test_add_tc_qdisc_ingress(self):
|
||||
priv_tc_lib.add_tc_qdisc(self.device, kind='ingress',
|
||||
namespace=self.namespace)
|
||||
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
|
||||
namespace=self.namespace)
|
||||
self.assertEqual(1, len(qdiscs))
|
||||
self.assertEqual('ingress', tc_lib._get_attr(qdiscs[0], 'TCA_KIND'))
|
||||
self.assertEqual(rtnl.TC_H_INGRESS, qdiscs[0]['parent'])
|
||||
self.assertEqual(0xffff0000, qdiscs[0]['handle'])
|
|
@ -14,11 +14,14 @@
|
|||
# under the License.
|
||||
|
||||
import mock
|
||||
from neutron_lib.exceptions import qos as qos_exc
|
||||
from neutron_lib.services.qos import constants as qos_consts
|
||||
from pyroute2.netlink import rtnl
|
||||
|
||||
from neutron.agent.linux import tc_lib
|
||||
from neutron.common import constants
|
||||
from neutron.common import utils
|
||||
from neutron.privileged.agent.linux import tc_lib as priv_tc_lib
|
||||
from neutron.tests import base
|
||||
|
||||
DEVICE_NAME = "tap_device"
|
||||
|
@ -27,10 +30,6 @@ 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 '
|
||||
|
@ -110,6 +109,10 @@ class TestTcCommand(base.BaseTestCase):
|
|||
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()
|
||||
self.mock_list_tc_qdiscs = mock.patch.object(tc_lib,
|
||||
'list_tc_qdiscs').start()
|
||||
self.mock_add_tc_qdisc = mock.patch.object(tc_lib,
|
||||
'add_tc_qdisc').start()
|
||||
|
||||
def test_check_kernel_hz_lower_then_zero(self):
|
||||
self.assertRaises(
|
||||
|
@ -144,35 +147,20 @@ class TestTcCommand(base.BaseTestCase):
|
|||
self.assertRaises(tc_lib.InvalidUnit, self.tc.get_filters_bw_limits)
|
||||
|
||||
def test_get_tbf_bw_limits(self):
|
||||
self.execute.return_value = TC_QDISC_OUTPUT
|
||||
bw_limit, burst_limit = self.tc.get_tbf_bw_limits()
|
||||
self.assertEqual(BW_LIMIT, bw_limit)
|
||||
self.assertEqual(BURST, burst_limit)
|
||||
self.mock_list_tc_qdiscs.return_value = [
|
||||
{'qdisc_type': 'tbf', 'max_kbps': BW_LIMIT, 'burst_kb': BURST}]
|
||||
self.assertEqual((BW_LIMIT, BURST), self.tc.get_tbf_bw_limits())
|
||||
|
||||
def test_get_tbf_bw_limits_when_wrong_qdisc(self):
|
||||
output = TC_QDISC_OUTPUT.replace("tbf", "different_qdisc")
|
||||
self.execute.return_value = output
|
||||
bw_limit, burst_limit = self.tc.get_tbf_bw_limits()
|
||||
self.assertIsNone(bw_limit)
|
||||
self.assertIsNone(burst_limit)
|
||||
|
||||
def test_get_tbf_bw_limits_when_wrong_units(self):
|
||||
output = TC_QDISC_OUTPUT.replace("kbit", "Xbit")
|
||||
self.execute.return_value = output
|
||||
self.assertRaises(tc_lib.InvalidUnit, self.tc.get_tbf_bw_limits)
|
||||
self.mock_list_tc_qdiscs.return_value = [{'qdisc_type': 'other_type'}]
|
||||
self.assertEqual((None, None), self.tc.get_tbf_bw_limits())
|
||||
|
||||
def test_set_tbf_bw_limit(self):
|
||||
self.tc.set_tbf_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
|
||||
)
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
DEVICE_NAME, 'tbf', parent='root', max_kbps=BW_LIMIT,
|
||||
burst_kb=BURST, latency_ms=LATENCY, kernel_hz=self.tc.kernel_hz,
|
||||
namespace=self.tc.namespace)
|
||||
|
||||
def test_update_filters_bw_limit(self):
|
||||
self.tc.update_filters_bw_limit(BW_LIMIT, BURST)
|
||||
|
@ -184,14 +172,6 @@ class TestTcCommand(base.BaseTestCase):
|
|||
log_fail_as_error=True,
|
||||
extra_ok_codes=[1, 2]
|
||||
),
|
||||
mock.call(
|
||||
['tc', 'qdisc', 'add', 'dev', DEVICE_NAME, "ingress",
|
||||
"handle", tc_lib.INGRESS_QDISC_ID],
|
||||
run_as_root=True,
|
||||
check_exit_code=True,
|
||||
log_fail_as_error=True,
|
||||
extra_ok_codes=None
|
||||
),
|
||||
mock.call(
|
||||
['tc', 'filter', 'add', 'dev', DEVICE_NAME,
|
||||
'parent', tc_lib.INGRESS_QDISC_ID, 'protocol', 'all',
|
||||
|
@ -206,19 +186,8 @@ class TestTcCommand(base.BaseTestCase):
|
|||
extra_ok_codes=None
|
||||
)]
|
||||
)
|
||||
|
||||
def test_update_tbf_bw_limit(self):
|
||||
self.tc.update_tbf_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
|
||||
)
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
self.tc.name, 'ingress', namespace=self.tc.namespace)
|
||||
|
||||
def test_delete_filters_bw_limit(self):
|
||||
self.tc.delete_filters_bw_limit()
|
||||
|
@ -259,10 +228,115 @@ class TestTcCommand(base.BaseTestCase):
|
|||
self.tc.get_ingress_qdisc_burst_value(BW_LIMIT, 0)
|
||||
)
|
||||
|
||||
|
||||
class TcTestCase(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TcTestCase, self).setUp()
|
||||
self.mock_add_tc_qdisc = mock.patch.object(
|
||||
priv_tc_lib, 'add_tc_qdisc').start()
|
||||
self.namespace = 'namespace'
|
||||
|
||||
def test_add_tc_qdisc_htb(self):
|
||||
tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle='1:',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='1:0',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.reset_mock()
|
||||
|
||||
tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle='2',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='2:0',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.reset_mock()
|
||||
|
||||
tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle='3:12',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='3:0',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.reset_mock()
|
||||
|
||||
tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle=4,
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='4:0',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.reset_mock()
|
||||
|
||||
tc_lib.add_tc_qdisc('device', 'htb', parent='root',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
'device', parent=rtnl.TC_H_ROOT, kind='htb',
|
||||
namespace=self.namespace)
|
||||
self.mock_add_tc_qdisc.reset_mock()
|
||||
|
||||
tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle=5)
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='5:0',
|
||||
namespace=None)
|
||||
self.mock_add_tc_qdisc.reset_mock()
|
||||
|
||||
def test_add_tc_qdisc_tbf(self):
|
||||
tc_lib.add_tc_qdisc('device', 'tbf', parent='root', max_kbps=10000,
|
||||
burst_kb=1500, latency_ms=70, kernel_hz=250,
|
||||
namespace=self.namespace)
|
||||
burst = tc_lib._get_tbf_burst_value(10000, 1500, 70) * 1024 / 8
|
||||
self.mock_add_tc_qdisc.assert_called_once_with(
|
||||
'device', parent=rtnl.TC_H_ROOT, kind='tbf', rate=10000 * 128,
|
||||
burst=burst, latency=70000, namespace=self.namespace)
|
||||
|
||||
def test_add_tc_qdisc_tbf_missing_arguments(self):
|
||||
self.assertRaises(
|
||||
qos_exc.TcLibQdiscNeededArguments, tc_lib.add_tc_qdisc,
|
||||
'device', 'tbf', parent='root')
|
||||
|
||||
def test_add_tc_qdisc_wrong_qdisc_type(self):
|
||||
self.assertRaises(qos_exc.TcLibQdiscTypeError, tc_lib.add_tc_qdisc,
|
||||
mock.ANY, 'wrong_qdic_type_name')
|
||||
|
||||
def test_list_tc_qdiscs_htb(self):
|
||||
qdisc = {'index': 2, 'handle': 327680, 'parent': 4294967295,
|
||||
'attrs': (('TCA_KIND', 'htb'), )}
|
||||
with mock.patch.object(priv_tc_lib, 'list_tc_qdiscs') as \
|
||||
mock_list_tc_qdiscs:
|
||||
mock_list_tc_qdiscs.return_value = tuple([qdisc])
|
||||
qdiscs = tc_lib.list_tc_qdiscs('device',
|
||||
namespace=self.namespace)
|
||||
self.assertEqual(1, len(qdiscs))
|
||||
self.assertEqual('root', qdiscs[0]['parent'])
|
||||
self.assertEqual('5:0', qdiscs[0]['handle'])
|
||||
self.assertEqual('htb', qdiscs[0]['qdisc_type'])
|
||||
|
||||
def test_list_tc_qdiscs_tbf(self):
|
||||
tca_tbf_params = {'buffer': 9375000,
|
||||
'rate': 320000,
|
||||
'limit': 208000}
|
||||
qdisc = {'index': 2, 'handle': 327681, 'parent': 4294967295,
|
||||
'attrs': (
|
||||
('TCA_KIND', 'tbf'),
|
||||
('TCA_OPTIONS', {'attrs': (
|
||||
('TCA_TBF_PARMS', tca_tbf_params), )}))
|
||||
}
|
||||
with mock.patch.object(priv_tc_lib, 'list_tc_qdiscs') as \
|
||||
mock_list_tc_qdiscs:
|
||||
mock_list_tc_qdiscs.return_value = tuple([qdisc])
|
||||
qdiscs = tc_lib.list_tc_qdiscs('device',
|
||||
namespace=self.namespace)
|
||||
self.assertEqual(1, len(qdiscs))
|
||||
self.assertEqual('root', qdiscs[0]['parent'])
|
||||
self.assertEqual('5:1', qdiscs[0]['handle'])
|
||||
self.assertEqual('tbf', qdiscs[0]['qdisc_type'])
|
||||
self.assertEqual(2500, qdiscs[0]['max_kbps'])
|
||||
self.assertEqual(1500, qdiscs[0]['burst_kb'])
|
||||
self.assertEqual(50, qdiscs[0]['latency_ms'])
|
||||
|
||||
def test__get_tbf_burst_value_when_burst_bigger_then_minimal(self):
|
||||
result = self.tc._get_tbf_burst_value(BW_LIMIT, BURST)
|
||||
result = tc_lib._get_tbf_burst_value(BW_LIMIT, BURST, KERNEL_HZ_VALUE)
|
||||
self.assertEqual(BURST, result)
|
||||
|
||||
def test__get_tbf_burst_value_when_burst_smaller_then_minimal(self):
|
||||
result = self.tc._get_tbf_burst_value(BW_LIMIT, 0)
|
||||
result = tc_lib._get_tbf_burst_value(BW_LIMIT, 0, KERNEL_HZ_VALUE)
|
||||
self.assertEqual(2, result)
|
||||
|
|
|
@ -148,26 +148,26 @@ class QosLinuxbridgeAgentDriverTestCase(base.BaseTestCase):
|
|||
with mock.patch.object(
|
||||
tc_lib.TcCommand, "update_filters_bw_limit"
|
||||
) as update_filters_bw_limit, mock.patch.object(
|
||||
tc_lib.TcCommand, "update_tbf_bw_limit"
|
||||
) as update_tbf_bw_limit:
|
||||
tc_lib.TcCommand, "set_tbf_bw_limit"
|
||||
) as set_tbf_bw_limit:
|
||||
self.qos_driver.update_bandwidth_limit(self.port,
|
||||
self.rule_egress_bw_limit)
|
||||
update_filters_bw_limit.assert_called_once_with(
|
||||
self.rule_egress_bw_limit.max_kbps,
|
||||
self.rule_egress_bw_limit.max_burst_kbps,
|
||||
)
|
||||
update_tbf_bw_limit.assert_not_called()
|
||||
set_tbf_bw_limit.assert_not_called()
|
||||
|
||||
def test_update_ingress_bandwidth_limit(self):
|
||||
with mock.patch.object(
|
||||
tc_lib.TcCommand, "update_filters_bw_limit"
|
||||
) as update_filters_bw_limit, mock.patch.object(
|
||||
tc_lib.TcCommand, "update_tbf_bw_limit"
|
||||
) as update_tbf_bw_limit:
|
||||
tc_lib.TcCommand, "set_tbf_bw_limit"
|
||||
) as set_tbf_bw_limit:
|
||||
self.qos_driver.update_bandwidth_limit(self.port,
|
||||
self.rule_ingress_bw_limit)
|
||||
update_filters_bw_limit.assert_not_called()
|
||||
update_tbf_bw_limit.assert_called_once_with(
|
||||
set_tbf_bw_limit.assert_called_once_with(
|
||||
self.rule_egress_bw_limit.max_kbps,
|
||||
self.rule_egress_bw_limit.max_burst_kbps,
|
||||
TEST_LATENCY_VALUE
|
||||
|
|
Loading…
Reference in New Issue