From 93d556e4344ee9ba25c51e2b72460c8ed69d9943 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Thu, 13 Dec 2018 12:32:06 +0000 Subject: [PATCH] Add TC filter functions implemented with pyroute2 Added add_tc_filter_match_mac, add_tc_filter_policy and list_tc_filters Related-Bug: #1560963 Change-Id: I360e68b98465706aef66e00590e1063345ead6b3 --- neutron/agent/linux/tc_lib.py | 167 ++++++++++++++---- neutron/privileged/agent/linux/tc_lib.py | 59 +++++++ .../privileged/agent/linux/test_tc_lib.py | 59 +++++++ neutron/tests/unit/agent/linux/test_tc_lib.py | 56 +++--- 4 files changed, 284 insertions(+), 57 deletions(-) diff --git a/neutron/agent/linux/tc_lib.py b/neutron/agent/linux/tc_lib.py index ccf87936ada..54876903393 100644 --- a/neutron/agent/linux/tc_lib.py +++ b/neutron/agent/linux/tc_lib.py @@ -16,16 +16,17 @@ import math import re +import netaddr 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.iproute import linux as iproute_linux 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 @@ -156,6 +157,37 @@ def _handle_from_hex_to_string(handle): return ':'.join([major, minor]) +def _mac_to_pyroute2_keys(mac, offset): + """Convert a MAC address to a list of filter keys + + For example: + MAC: '01:23:45:67:89:0a', offset: 8 + keys: ['0x01234567/0xffffffff+8', '0x890a0000/0xffff0000+12'] + + :param mac: (string) MAC address + :param offset: (int) natural number, offset bytes number from the IP header + """ + int_mac = int(netaddr.EUI(mac)) + high_value = int_mac >> 16 + high_mask = 0xffffffff + high_offset = offset + high = {'value': high_value, + 'mask': high_mask, + 'offset': high_offset, + 'key': (hex(high_value) + '/' + hex(high_mask) + '+' + + str(high_offset))} + + low_value = (int_mac & 0xffff) << 16 + low_mask = 0xffff0000 + low_offset = offset + 4 + low = {'value': low_value, + 'mask': low_mask, + 'offset': low_offset, + 'key': hex(low_value) + '/' + hex(low_mask) + '+' + str(low_offset)} + + return [high, low] + + class TcCommand(ip_lib.IPDevice): def __init__(self, name, kernel_hz, namespace=None): @@ -164,11 +196,6 @@ class TcCommand(ip_lib.IPDevice): super(TcCommand, self).__init__(name, namespace=namespace) self.kernel_hz = kernel_hz - def _execute_tc_cmd(self, cmd, **kwargs): - cmd = ['tc'] + cmd - ip_wrapper = ip_lib.IPWrapper(self.namespace) - return ip_wrapper.netns.execute(cmd, run_as_root=True, **kwargs) - @staticmethod def get_ingress_qdisc_burst_value(bw_limit, burst_limit): """Return burst value used in ingress qdisc. @@ -181,21 +208,11 @@ class TcCommand(ip_lib.IPDevice): 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), 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(2), constants.IEC_BASE) - return bw_limit, burst_limit + filters = list_tc_filters(self.name, qdisc_id, + namespace=self.namespace) + if filters: + return filters[0].get('rate_kbps'), filters[0].get('burst_kb') + return None, None def get_tbf_bw_limits(self): @@ -253,23 +270,11 @@ class TcCommand(ip_lib.IPDevice): 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'] - return self._execute_tc_cmd(cmd) + add_tc_filter_policy(self.name, qdisc_id, bw_limit, burst_limit, + MAX_MTU_VALUE, 'drop', priority=49) def add_tc_qdisc(device, qdisc_type, parent=None, handle=None, latency_ms=None, @@ -456,3 +461,95 @@ def delete_tc_policy_class(device, parent, classid, namespace=None): """ priv_tc_lib.delete_tc_policy_class(device, parent, classid, namespace=namespace) + + +def add_tc_filter_match_mac(device, parent, classid, mac, offset=0, priority=0, + protocol=None, namespace=None): + """Add a TC filter in a device to match a MAC address. + + :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 mac: (string) MAC address to match + :param offset: (int) (optional) match offset, starting from the outer + packet IP header + :param priority: (int) (optional) filter priority (lower priority, higher + preference) + :param protocol: (int) (optional) traffic filter protocol; if None, all + will be matched. + :param namespace: (string) (optional) namespace name + + """ + keys = [key['key'] for key in _mac_to_pyroute2_keys(mac, offset)] + priv_tc_lib.add_tc_filter_match32(device, parent, priority, classid, keys, + protocol=protocol, namespace=namespace) + + +def add_tc_filter_policy(device, parent, rate_kbps, burst_kb, mtu, action, + priority=0, protocol=None, namespace=None): + """Add a TC filter in a device to set a policy. + + :param device: (string) device name + :param parent: (string) qdisc parent class ('root', 'ingress', '2:10') + :param rate_kbps: (int) rate in kbits/second + :param burst_kb: (int) burst in kbits + :param mtu: (int) MTU size (bytes) + :param action: (string) filter policy action + :param priority: (int) (optional) filter priority (lower priority, higher + preference) + :param protocol: (int) (optional) traffic filter protocol; if None, all + will be matched. + :param namespace: (string) (optional) namespace name + + """ + rate = int(rate_kbps * 1024 / 8) + burst = int(burst_kb * 1024 / 8) + priv_tc_lib.add_tc_filter_policy(device, parent, priority, rate, burst, + mtu, action, protocol=protocol, + namespace=namespace) + + +def list_tc_filters(device, parent, namespace=None): + """List TC filter in a device + + :param device: (string) device name + :param parent: (string) qdisc parent class ('root', 'ingress', '2:10') + :param namespace: (string) (optional) namespace name + + """ + parent = iproute_linux.transform_handle(parent) + filters = priv_tc_lib.list_tc_filters(device, parent, namespace=namespace) + retval = [] + for filter in filters: + tca_options = _get_attr(filter, 'TCA_OPTIONS') + if not tca_options: + continue + tca_u32_sel = _get_attr(tca_options, 'TCA_U32_SEL') + if not tca_u32_sel: + continue + keys = [] + for key in tca_u32_sel['keys']: + key_off = key['key_off'] + value = 0 + for i in range(4): + value = (value << 8) + (key_off & 0xff) + key_off = key_off >> 8 + keys.append({'value': value, + 'mask': key['key_val'], + 'offset': key['key_offmask']}) + + value = {'keys': keys} + + tca_u32_police = _get_attr(tca_options, 'TCA_U32_POLICE') + if tca_u32_police: + tca_police_tbf = _get_attr(tca_u32_police, 'TCA_POLICE_TBF') + if tca_police_tbf: + value['rate_kbps'] = int(tca_police_tbf['rate'] * 8 / 1024) + value['burst_kb'] = int( + _calc_burst(tca_police_tbf['rate'], + tca_police_tbf['burst']) * 8 / 1024) + value['mtu'] = tca_police_tbf['mtu'] + + retval.append(value) + + return retval diff --git a/neutron/privileged/agent/linux/tc_lib.py b/neutron/privileged/agent/linux/tc_lib.py index c11a0fca01e..b36fde389f0 100644 --- a/neutron/privileged/agent/linux/tc_lib.py +++ b/neutron/privileged/agent/linux/tc_lib.py @@ -17,6 +17,7 @@ import socket from neutron_lib import constants as n_constants import pyroute2 +from pyroute2 import protocols as pyroute2_protocols from neutron._i18n import _ from neutron import privileged @@ -141,4 +142,62 @@ def delete_tc_policy_class(device, parent, classid, namespace=None, if e.code == errno.ENOENT: raise TrafficControlClassNotFound(classid=classid, namespace=namespace) + + +@privileged.default.entrypoint +def add_tc_filter_match32(device, parent, priority, class_id, keys, + protocol=None, namespace=None, **kwargs): + """Add TC filter, type: match u32""" + # NOTE(ralonsoh): by default (protocol=None), every packet is filtered. + protocol = protocol or pyroute2_protocols.ETH_P_ALL + try: + index = ip_lib.get_link_id(device, namespace) + with ip_lib.get_iproute(namespace) as ip: + ip.tc('add-filter', kind='u32', index=index, + parent=parent, priority=priority, target=class_id, + protocol=protocol, keys=keys, **kwargs) + except OSError as e: + if e.errno == errno.ENOENT: + raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace) + raise + + +@privileged.default.entrypoint +def add_tc_filter_policy(device, parent, priority, rate, burst, mtu, action, + protocol=None, keys=None, flowid=1, namespace=None, + **kwargs): + """Add TC filter, type: policy filter + + By default (protocol=None), that means every packet is shaped. "keys" + and "target" (flowid) parameters are mandatory. If the filter is + applied on a classless qdisc, "target" is irrelevant and a default value + can be passed. If all packets must be shaped, an empty filter ("keys") + can be passed. + """ + keys = keys if keys else ['0x0/0x0'] + protocol = protocol or pyroute2_protocols.ETH_P_ALL + try: + index = ip_lib.get_link_id(device, namespace) + with ip_lib.get_iproute(namespace) as ip: + ip.tc('add-filter', kind='u32', index=index, + parent=parent, priority=priority, protocol=protocol, + rate=rate, burst=burst, mtu=mtu, action=action, + keys=keys, target=flowid, **kwargs) + except OSError as e: + if e.errno == errno.ENOENT: + raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace) + raise + + +@privileged.default.entrypoint +def list_tc_filters(device, parent, namespace=None, **kwargs): + """List TC filters""" + try: + index = ip_lib.get_link_id(device, namespace) + with ip_lib.get_iproute(namespace) as ip: + return ip_lib.make_serializable( + ip.get_filters(index=index, parent=parent, **kwargs)) + 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 d88e9c16071..a81128172a5 100644 --- a/neutron/tests/functional/privileged/agent/linux/test_tc_lib.py +++ b/neutron/tests/functional/privileged/agent/linux/test_tc_lib.py @@ -232,3 +232,62 @@ class TcPolicyClassTestCase(functional_base.BaseSudoTestCase): priv_tc_lib.TrafficControlClassNotFound, priv_tc_lib.delete_tc_policy_class, self.device, '1:', '1:1000', namespace=self.namespace) + + +class TcFilterClassTestCase(functional_base.BaseSudoTestCase): + + 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}} + + def setUp(self): + super(TcFilterClassTestCase, 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('int_dummy', self.namespace, 'dummy') + + def _remove_ns(self, namespace): + priv_ip_lib.remove_netns(namespace) + + def test_add_tc_filter_match32(self): + priv_tc_lib.add_tc_qdisc( + self.device, parent=rtnl.TC_H_ROOT, kind='htb', handle='1:', + namespace=self.namespace) + priv_tc_lib.add_tc_policy_class( + self.device, '1:', '1:10', 'htb', namespace=self.namespace, + rate=10000) + keys = tc_lib._mac_to_pyroute2_keys('7a:8c:f9:1f:e5:cb', 41) + priv_tc_lib.add_tc_filter_match32( + self.device, '1:0', 10, '1:10', [keys[0]['key'], keys[1]['key']], + namespace=self.namespace) + + filters = tc_lib.list_tc_filters( + self.device, '1:0', namespace=self.namespace) + self.assertEqual(1, len(filters)) + filter_keys = filters[0]['keys'] + self.assertEqual(len(keys), len(filter_keys)) + for index, value in enumerate(keys): + value.pop('key') + self.assertEqual(value, filter_keys[index]) + + def test_add_tc_filter_policy(self): + priv_tc_lib.add_tc_qdisc( + self.device, parent=rtnl.TC_H_ROOT, kind='ingress', + namespace=self.namespace) + + # NOTE(ralonsoh): + # - rate: 320000 bytes/sec (pyroute2 units) = 2500 kbits/sec (OS units) + # - burst: 192000 bytes/sec = 1500 kbits/sec + priv_tc_lib.add_tc_filter_policy( + self.device, 'ffff:', 49, 320000, 192000, 1200, 'drop', + namespace=self.namespace) + + filters = tc_lib.list_tc_filters( + self.device, 'ffff:', namespace=self.namespace) + self.assertEqual(1, len(filters)) + self.assertEqual(2500, filters[0]['rate_kbps']) + self.assertEqual(1500, filters[0]['burst_kb']) + self.assertEqual(1200, filters[0]['mtu']) diff --git a/neutron/tests/unit/agent/linux/test_tc_lib.py b/neutron/tests/unit/agent/linux/test_tc_lib.py index 200259c30b4..2028064a7a5 100644 --- a/neutron/tests/unit/agent/linux/test_tc_lib.py +++ b/neutron/tests/unit/agent/linux/test_tc_lib.py @@ -105,16 +105,16 @@ class TestTcCommand(base.BaseTestCase): def setUp(self): super(TestTcCommand, self).setUp() self.tc = tc_lib.TcCommand(DEVICE_NAME, KERNEL_HZ_VALUE) - self.bw_limit = "%s%s" % (BW_LIMIT, tc_lib.BW_LIMIT_UNIT) - self.burst = "%s%s" % (BURST, tc_lib.BURST_UNIT) - self.latency = "%s%s" % (LATENCY, tc_lib.LATENCY_UNIT) - self.execute = mock.patch('neutron.agent.common.utils.execute').start() 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() self.mock_delete_tc_qdisc = mock.patch.object( tc_lib, 'delete_tc_qdisc').start() + self.mock_list_tc_filters = mock.patch.object( + tc_lib, 'list_tc_filters').start() + self.mock_add_tc_filter_policy = mock.patch.object( + tc_lib, 'add_tc_filter_policy').start() def test_check_kernel_hz_lower_then_zero(self): self.assertRaises( @@ -127,26 +127,23 @@ class TestTcCommand(base.BaseTestCase): ) def test_get_filters_bw_limits(self): - self.execute.return_value = TC_FILTERS_OUTPUT + self.mock_list_tc_filters.return_value = [{'rate_kbps': BW_LIMIT, + 'burst_kb': BURST}] bw_limit, burst_limit = self.tc.get_filters_bw_limits() self.assertEqual(BW_LIMIT, bw_limit) self.assertEqual(BURST, burst_limit) - 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 + def test_get_filters_bw_limits_no_filters(self): + self.mock_list_tc_filters.return_value = [] bw_limit, burst_limit = self.tc.get_filters_bw_limits() self.assertIsNone(bw_limit) self.assertIsNone(burst_limit) - 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_get_filters_bw_limits_no_rate_info(self): + self.mock_list_tc_filters.return_value = [{'other_values': 1}] + bw_limit, burst_limit = self.tc.get_filters_bw_limits() + self.assertIsNone(bw_limit) + self.assertIsNone(burst_limit) def test_get_tbf_bw_limits(self): self.mock_list_tc_qdiscs.return_value = [ @@ -166,17 +163,14 @@ class TestTcCommand(base.BaseTestCase): def test_update_filters_bw_limit(self): self.tc.update_filters_bw_limit(BW_LIMIT, BURST) - self.execute.assert_called_once_with( - ['tc', 'filter', 'add', 'dev', DEVICE_NAME, 'parent', - tc_lib.INGRESS_QDISC_ID, 'protocol', 'all', 'prio', '49', - 'basic', 'police', 'rate', self.bw_limit, 'burst', self.burst, - 'mtu', tc_lib.MAX_MTU_VALUE, 'drop'], 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) self.mock_delete_tc_qdisc.assert_called_once_with( self.tc.name, is_ingress=True, raise_interface_not_found=False, raise_qdisc_not_found=False, namespace=self.tc.namespace) + self.mock_add_tc_filter_policy.assert_called_once_with( + self.tc.name, tc_lib.INGRESS_QDISC_ID, BW_LIMIT, BURST, + tc_lib.MAX_MTU_VALUE, 'drop', priority=49) def test_delete_filters_bw_limit(self): self.tc.delete_filters_bw_limit() @@ -362,3 +356,21 @@ class TcPolicyClassTestCase(base.BaseTestCase): 'max_kbps': 2000, 'burst_kb': 1200} self.assertEqual(reference, _class) + + +class TcFilterTestCase(base.BaseTestCase): + + def test__mac_to_pyroute2_keys(self): + mac = '01:23:45:67:89:ab' + offset = 10 + keys = tc_lib._mac_to_pyroute2_keys(mac, offset) + high = {'value': 0x1234567, + 'mask': 0xffffffff, + 'offset': 10, + 'key': '0x1234567/0xffffffff+10'} + low = {'value': 0x89ab0000, + 'mask': 0xffff0000, + 'offset': 14, + 'key': '0x89ab0000/0xffff0000+14'} + self.assertEqual(high, keys[0]) + self.assertEqual(low, keys[1])