From 4a92b0b1429cc2f823fbb32964d0519abfd1c6a1 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 13 Dec 2018 10:14:51 +0000 Subject: [PATCH] Add add_tc_policy_class and list_tc_policy_classes using pyroute2 Related-Bug: #1560963 Change-Id: I01333077d4d47cf7f0d7ede4ccc5c2e2c63aa67a --- neutron/agent/linux/tc_lib.py | 77 ++++++++++++++++++- neutron/privileged/agent/linux/tc_lib.py | 28 +++++++ .../privileged/agent/linux/test_tc_lib.py | 40 ++++++++++ neutron/tests/unit/agent/linux/test_tc_lib.py | 41 ++++++++++ 4 files changed, 185 insertions(+), 1 deletion(-) diff --git a/neutron/agent/linux/tc_lib.py b/neutron/agent/linux/tc_lib.py index 902c2db1e11..b72a94acf60 100644 --- a/neutron/agent/linux/tc_lib.py +++ b/neutron/agent/linux/tc_lib.py @@ -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 diff --git a/neutron/privileged/agent/linux/tc_lib.py b/neutron/privileged/agent/linux/tc_lib.py index ac97493e550..ffb8b4f6994 100644 --- a/neutron/privileged/agent/linux/tc_lib.py +++ b/neutron/privileged/agent/linux/tc_lib.py @@ -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 diff --git a/neutron/tests/functional/privileged/agent/linux/test_tc_lib.py b/neutron/tests/functional/privileged/agent/linux/test_tc_lib.py index 165134d1b72..5464b372447 100644 --- a/neutron/tests/functional/privileged/agent/linux/test_tc_lib.py +++ b/neutron/tests/functional/privileged/agent/linux/test_tc_lib.py @@ -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) diff --git a/neutron/tests/unit/agent/linux/test_tc_lib.py b/neutron/tests/unit/agent/linux/test_tc_lib.py index 69db37f3863..200259c30b4 100644 --- a/neutron/tests/unit/agent/linux/test_tc_lib.py +++ b/neutron/tests/unit/agent/linux/test_tc_lib.py @@ -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)