Browse Source

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
tags/14.0.0.0b3^2
Rodolfo Alonso Hernandez 1 year ago
parent
commit
93d556e434
4 changed files with 284 additions and 57 deletions
  1. +132
    -35
      neutron/agent/linux/tc_lib.py
  2. +59
    -0
      neutron/privileged/agent/linux/tc_lib.py
  3. +59
    -0
      neutron/tests/functional/privileged/agent/linux/test_tc_lib.py
  4. +34
    -22
      neutron/tests/unit/agent/linux/test_tc_lib.py

+ 132
- 35
neutron/agent/linux/tc_lib.py View File

@@ -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

+ 59
- 0
neutron/privileged/agent/linux/tc_lib.py View File

@@ -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

+ 59
- 0
neutron/tests/functional/privileged/agent/linux/test_tc_lib.py View File

@@ -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'])

+ 34
- 22
neutron/tests/unit/agent/linux/test_tc_lib.py View File

@@ -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])

Loading…
Cancel
Save