Add add_tc_policy_class and list_tc_policy_classes using pyroute2

Related-Bug: #1560963

Change-Id: I01333077d4d47cf7f0d7ede4ccc5c2e2c63aa67a
This commit is contained in:
Rodolfo Alonso Hernandez 2018-12-13 10:14:51 +00:00
parent 17a7a62e84
commit 4a92b0b142
4 changed files with 185 additions and 1 deletions

View File

@ -54,7 +54,7 @@ tbf_pattern = re.compile(
TC_QDISC_TYPES = ['htb', 'tbf', 'ingress']
TC_QDISC_PARENT = {'root': rtnl.TC_H_ROOT,
'ingress': rtnl.TC_H_INGRESS}
'ingress': rtnl.TC_H_INGRESS}
TC_QDISC_PARENT_NAME = {v: k for k, v in TC_QDISC_PARENT.items()}
@ -369,3 +369,78 @@ def delete_tc_qdisc(device, parent=None, is_ingress=False,
device, parent=parent, kind=qdisc_type,
raise_interface_not_found=raise_interface_not_found,
raise_qdisc_not_found=raise_qdisc_not_found, namespace=namespace)
def add_tc_policy_class(device, parent, classid, qdisc_type,
min_kbps=None, max_kbps=None, burst_kb=None,
namespace=None):
"""Add a TC policy class
:param device: (string) device name
:param parent: (string) qdisc parent class ('root', 'ingress', '2:10')
:param classid: (string) major:minor handler identifier ('10:20')
:param qdisc_type: (string) qdisc type ("sfq", "htb", "u32", etc)
:param min_kbps: (int) (optional) minimum bandwidth in kbps
:param max_kbps: (int) (optional) maximum bandwidth in kbps
:param burst_kb: (int) (optional) burst size in kb
:param namespace: (string) (optional) namespace name
:return:
"""
parent = TC_QDISC_PARENT.get(parent, parent)
args = {}
# NOTE(ralonsoh): pyroute2 input parameters and units [1]:
# - rate (min bw): bytes/second
# - ceil (max bw): bytes/second
# - burst: bytes
# [1] https://www.systutorials.com/docs/linux/man/8-tc/
if min_kbps:
args['rate'] = int(min_kbps * 1024 / 8)
if max_kbps:
args['ceil'] = int(max_kbps * 1024 / 8)
if burst_kb:
args['burst'] = int(burst_kb * 1024 / 8)
priv_tc_lib.add_tc_policy_class(device, parent, classid, qdisc_type,
namespace=namespace, **args)
def list_tc_policy_class(device, namespace=None):
"""List all TC policy classes of a device
:param device: (string) device name
:param namespace: (string) (optional) namespace name
:return: (list) TC policy classes
"""
def get_params(tca_options, qdisc_type):
if qdisc_type not in TC_QDISC_TYPES:
return None, None, None
tca_params = _get_attr(tca_options,
'TCA_' + qdisc_type.upper() + '_PARMS')
burst_kb = int(
_calc_burst(tca_params['rate'], tca_params['buffer']) * 8 / 1024)
max_kbps = int(tca_params['ceil'] * 8 / 1024)
min_kbps = int(tca_params['rate'] * 8 / 1024)
return max_kbps, min_kbps, burst_kb
tc_classes = priv_tc_lib.list_tc_policy_classes(device,
namespace=namespace)
classes = []
for tc_class in tc_classes:
index = tc_class['index']
parent = TC_QDISC_PARENT_NAME.get(
tc_class['parent'], _handle_from_hex_to_string(tc_class['parent']))
classid = _handle_from_hex_to_string(tc_class['handle'])
qdisc_type = _get_attr(tc_class, 'TCA_KIND')
tca_options = _get_attr(tc_class, 'TCA_OPTIONS')
max_kbps, min_kbps, burst_kb = get_params(tca_options, qdisc_type)
classes.append({'device': device,
'index': index,
'namespace': namespace,
'parent': parent,
'classid': classid,
'qdisc_type': qdisc_type,
'min_kbps': min_kbps,
'max_kbps': max_kbps,
'burst_kb': burst_kb})
return classes

View File

@ -83,3 +83,31 @@ def delete_tc_qdisc(device, parent=None, kind=None, namespace=None,
if e.errno == errno.ENOENT:
raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def add_tc_policy_class(device, parent, classid, qdisc_type, namespace=None,
**kwargs):
"""Add/replace TC policy class"""
try:
index = ip_lib.get_link_id(device, namespace)
with ip_lib.get_iproute(namespace) as ip:
ip.tc('replace-class', kind=qdisc_type, index=index,
handle=classid, parent=parent, **kwargs)
except OSError as e:
if e.errno == errno.ENOENT:
raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def list_tc_policy_classes(device, namespace=None):
"""List all TC policy classes of a device"""
try:
index = ip_lib.get_link_id(device, namespace)
with ip_lib.get_iproute(namespace) as ip:
return ip_lib.make_serializable(ip.get_classes(index=index))
except OSError as e:
if e.errno == errno.ENOENT:
raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace)
raise

View File

@ -151,3 +151,43 @@ class TcQdiscTestCase(functional_base.BaseSudoTestCase):
priv_tc_lib.delete_tc_qdisc(self.device, kind='ingress',
namespace=self.namespace,
raise_qdisc_not_found=False))
class TcPolicyClassTestCase(functional_base.BaseSudoTestCase):
def setUp(self):
super(TcPolicyClassTestCase, 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_policy_class_htb(self):
priv_tc_lib.add_tc_qdisc(
self.device, kind='htb', parent=rtnl.TC_H_ROOT, handle='1:',
namespace=self.namespace)
classes = {'1:1': {'rate': 10000, 'ceil': 20000, 'burst': 1500},
'1:3': {'rate': 20000, 'ceil': 50000, 'burst': 1600},
'1:5': {'rate': 30000, 'ceil': 90000, 'burst': 1700},
'1:7': {'rate': 35001, 'ceil': 90000, 'burst': 1701}}
for classid, rates in classes.items():
priv_tc_lib.add_tc_policy_class(
self.device, '1:', classid, 'htb', namespace=self.namespace,
**rates)
tc_classes = priv_tc_lib.list_tc_policy_classes(
self.device, namespace=self.namespace)
self.assertEqual(len(classes), len(tc_classes))
for tc_class in tc_classes:
handle = tc_lib._handle_from_hex_to_string(tc_class['handle'])
tca_options = tc_lib._get_attr(tc_class, 'TCA_OPTIONS')
tca_htb_params = tc_lib._get_attr(tca_options, 'TCA_HTB_PARMS')
self.assertEqual(classes[handle]['rate'], tca_htb_params['rate'])
self.assertEqual(classes[handle]['ceil'], tca_htb_params['ceil'])
burst = tc_lib._calc_burst(classes[handle]['rate'],
tca_htb_params['buffer'])
self.assertEqual(classes[handle]['burst'], burst)

View File

@ -321,3 +321,44 @@ class TcTestCase(base.BaseTestCase):
def test__get_tbf_burst_value_when_burst_smaller_then_minimal(self):
result = tc_lib._get_tbf_burst_value(BW_LIMIT, 0, KERNEL_HZ_VALUE)
self.assertEqual(2, result)
class TcPolicyClassTestCase(base.BaseTestCase):
def setUp(self):
super(TcPolicyClassTestCase, self).setUp()
self.mock_add_tc_policy_class = mock.patch.object(
priv_tc_lib, 'add_tc_policy_class').start()
self.mock_list_tc_policy_classes = mock.patch.object(
priv_tc_lib, 'list_tc_policy_classes').start()
self.namespace = 'namespace'
def test_add_tc_policy_class(self):
tc_lib.add_tc_policy_class(
'device', 'root', '1:10', 'qdisc_type', min_kbps=1000,
max_kbps=2000, burst_kb=1600, namespace=self.namespace)
self.mock_add_tc_policy_class.assert_called_once_with(
'device', rtnl.TC_H_ROOT, '1:10', 'qdisc_type', rate=1000 * 128,
ceil=2000 * 128, burst=1600 * 128, namespace=self.namespace)
def test_list_tc_policy_classes(self):
htb_params = {'buffer': 12500000, 'ceil': 256000, 'rate': 192000}
self.mock_list_tc_policy_classes.return_value = tuple([
{'index': 3, 'handle': 65537, 'parent': 4294967295,
'attrs': (
('TCA_KIND', 'htb'),
('TCA_OPTIONS', {
'attrs': tuple([('TCA_HTB_PARMS', htb_params)])}))
}])
_class = tc_lib.list_tc_policy_class('device',
namespace=self.namespace)[0]
reference = {'device': 'device',
'index': 3,
'namespace': self.namespace,
'parent': 'root',
'classid': '1:1',
'qdisc_type': 'htb',
'min_kbps': 1500,
'max_kbps': 2000,
'burst_kb': 1200}
self.assertEqual(reference, _class)