summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2019-03-11 04:27:46 +0000
committerGerrit Code Review <review@openstack.org>2019-03-11 04:27:46 +0000
commit6920727fe1fec152c8be0ce5b42fcbc3b2cbdabd (patch)
tree30787cc14fe9667d17dca20a7a3d6ba4761d4eb6
parent4b52b123d3c3e40bd91e647dfd198ce8a058b2d3 (diff)
parent93d556e4344ee9ba25c51e2b72460c8ed69d9943 (diff)
downloadneutron-14.0.0.0b3.tar.gz
Merge "Add TC filter functions implemented with pyroute2"14.0.0.0b3
-rw-r--r--neutron/agent/linux/tc_lib.py167
-rw-r--r--neutron/privileged/agent/linux/tc_lib.py59
-rw-r--r--neutron/tests/functional/privileged/agent/linux/test_tc_lib.py59
-rw-r--r--neutron/tests/unit/agent/linux/test_tc_lib.py56
4 files changed, 284 insertions, 57 deletions
diff --git a/neutron/agent/linux/tc_lib.py b/neutron/agent/linux/tc_lib.py
index ccf87936ad..5487690339 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 c11a0fca01..b36fde389f 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 d88e9c1607..a81128172a 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 200259c30b..2028064a7a 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])