Implement delete_tc_qdisc using pyroute2

Related-Bug: #1560963

Change-Id: I79932dfc7464f9d904047df12d0f3f9cd5508a50
This commit is contained in:
Rodolfo Alonso Hernandez 2018-12-12 16:22:04 +00:00
parent d3a131d0b2
commit 17a7a62e84
4 changed files with 158 additions and 47 deletions

View File

@ -95,6 +95,18 @@ def convert_to_kilobits(value, base):
def _get_attr(pyroute2_obj, attr_name):
"""Get an attribute in a pyroute object
pyroute2 object attributes are stored under a key called 'attrs'. This key
contains a tuple of tuples. E.g.:
pyroute2_obj = {'attrs': (('TCA_KIND': 'htb'),
('TCA_OPTIONS': {...}))}
:param pyroute2_obj: (dict) pyroute2 object
:param attr_name: (string) first value of the tuple we are looking for
:return: (object) second value of the tuple, None if the tuple doesn't
exist
"""
rule_attrs = pyroute2_obj.get('attrs', [])
for attr in (attr for attr in rule_attrs if attr[0] == attr_name):
return attr[1]
@ -230,19 +242,14 @@ class TcCommand(ip_lib.IPDevice):
def delete_filters_bw_limit(self):
# NOTE(slaweq): For limit traffic egress from instance we need to use
# qdisc "ingress" because it is ingress traffic from interface POV:
self._delete_qdisc("ingress")
delete_tc_qdisc(self.name, is_ingress=True,
raise_interface_not_found=False,
raise_qdisc_not_found=False, namespace=self.namespace)
def delete_tbf_bw_limit(self):
self._delete_qdisc("root")
def _delete_qdisc(self, qdisc_name):
cmd = ['qdisc', 'del', 'dev', self.name, qdisc_name]
# Return_code=2 is fine because it means
# "RTNETLINK answers: No such file or directory" what is fine when we
# are trying to delete qdisc
# Return_code=1 means "RTNETLINK answers: Cannot find device <device>".
# If the device doesn't exist, the qdisc is already deleted.
return self._execute_tc_cmd(cmd, extra_ok_codes=[1, 2])
delete_tc_qdisc(self.name, parent='root',
raise_interface_not_found=False,
raise_qdisc_not_found=False, namespace=self.namespace)
def _add_policy_filter(self, bw_limit, burst_limit,
qdisc_id=INGRESS_QDISC_ID):
@ -339,3 +346,26 @@ def list_tc_qdiscs(device, namespace=None):
retval.append(qdisc_attrs)
return retval
def delete_tc_qdisc(device, parent=None, is_ingress=False,
raise_interface_not_found=True, raise_qdisc_not_found=True,
namespace=None):
"""Delete a TC qdisc of a device
:param device: (string) device name
:param parent: (string) (optional) qdisc parent class ('root', '2:10')
:param is_ingress: (bool) (optional) if qdisc type is 'ingress'
:param raise_interface_not_found: (bool) (optional) raise exception if the
interface doesn't exist
:param raise_qdisc_not_found: (bool) (optional) raise exception if the
qdisc doesn't exist
:param namespace: (string) (optional) namespace name
"""
qdisc_type = 'ingress' if is_ingress else None
if parent:
parent = rtnl.TC_H_ROOT if parent == 'root' else parent
priv_tc_lib.delete_tc_qdisc(
device, parent=parent, kind=qdisc_type,
raise_interface_not_found=raise_interface_not_found,
raise_qdisc_not_found=raise_qdisc_not_found, namespace=namespace)

View File

@ -16,6 +16,7 @@ import errno
import socket
from neutron_lib import constants as n_constants
import pyroute2
from neutron import privileged
from neutron.privileged.agent.linux import ip_lib
@ -49,3 +50,36 @@ def list_tc_qdiscs(device, namespace=None):
if e.errno == errno.ENOENT:
raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace)
raise
@privileged.default.entrypoint
def delete_tc_qdisc(device, parent=None, kind=None, namespace=None,
raise_interface_not_found=True,
raise_qdisc_not_found=True):
"""Delete a TC qdisc of a device"""
try:
index = ip_lib.get_link_id(device, namespace)
args = {}
if parent:
args['parent'] = parent
if kind:
args['kind'] = kind
with ip_lib.get_iproute(namespace) as ip:
ip.tc('del', index=index, **args)
except ip_lib.NetworkInterfaceNotFound:
if raise_interface_not_found:
raise
except pyroute2.NetlinkError as e:
# NOTE(ralonsoh): tc delete will raise a NetlinkError exception with
# code (22, 'Invalid argument') if kind='ingress' and the qdisc does
# not exist. This behaviour must be refactored in pyroute2.
if ((e.code == errno.ENOENT or
(e.code == errno.EINVAL and kind == 'ingress')) and
raise_qdisc_not_found is False):
# NOTE(ralonsoh): return error code for testing purposes
return e.code
raise
except OSError as e:
if e.errno == errno.ENOENT:
raise ip_lib.NetworkNamespaceNotFound(netns_name=namespace)
raise

View File

@ -12,7 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import errno
from oslo_utils import uuidutils
import pyroute2
from pyroute2.netlink import rtnl
from neutron.agent.linux import tc_lib
@ -45,6 +48,12 @@ class TcQdiscTestCase(functional_base.BaseSudoTestCase):
self.assertEqual(0x50000, qdiscs[0]['handle'])
self.assertEqual('htb', tc_lib._get_attr(qdiscs[0], 'TCA_KIND'))
priv_tc_lib.delete_tc_qdisc(self.device, rtnl.TC_H_ROOT,
namespace=self.namespace)
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
namespace=self.namespace)
self.assertEqual(0, len(qdiscs))
def test_add_tc_qdisc_htb_no_handle(self):
priv_tc_lib.add_tc_qdisc(
self.device, parent=rtnl.TC_H_ROOT, kind='htb',
@ -56,6 +65,12 @@ class TcQdiscTestCase(functional_base.BaseSudoTestCase):
self.assertEqual(0, qdiscs[0]['handle'] & 0xFFFF)
self.assertEqual('htb', tc_lib._get_attr(qdiscs[0], 'TCA_KIND'))
priv_tc_lib.delete_tc_qdisc(self.device, parent=rtnl.TC_H_ROOT,
namespace=self.namespace)
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
namespace=self.namespace)
self.assertEqual(0, len(qdiscs))
def test_add_tc_qdisc_tbf(self):
burst = 192000
rate = 320000
@ -76,6 +91,12 @@ class TcQdiscTestCase(functional_base.BaseSudoTestCase):
self.assertEqual(latency, tc_lib._calc_latency_ms(
tca_tbf_parms['limit'], burst, tca_tbf_parms['rate']) * 1000)
priv_tc_lib.delete_tc_qdisc(self.device, parent=rtnl.TC_H_ROOT,
namespace=self.namespace)
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
namespace=self.namespace)
self.assertEqual(0, len(qdiscs))
def test_add_tc_qdisc_ingress(self):
priv_tc_lib.add_tc_qdisc(self.device, kind='ingress',
namespace=self.namespace)
@ -85,3 +106,48 @@ class TcQdiscTestCase(functional_base.BaseSudoTestCase):
self.assertEqual('ingress', tc_lib._get_attr(qdiscs[0], 'TCA_KIND'))
self.assertEqual(rtnl.TC_H_INGRESS, qdiscs[0]['parent'])
self.assertEqual(0xffff0000, qdiscs[0]['handle'])
priv_tc_lib.delete_tc_qdisc(self.device, kind='ingress',
namespace=self.namespace)
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
namespace=self.namespace)
self.assertEqual(0, len(qdiscs))
def test_delete_tc_qdisc_no_device(self):
self.assertRaises(
priv_ip_lib.NetworkInterfaceNotFound, priv_tc_lib.delete_tc_qdisc,
'other_device', rtnl.TC_H_ROOT, namespace=self.namespace)
def test_delete_tc_qdisc_no_device_no_exception(self):
self.assertIsNone(priv_tc_lib.delete_tc_qdisc(
'other_device', rtnl.TC_H_ROOT, namespace=self.namespace,
raise_interface_not_found=False))
def test_delete_tc_qdisc_no_qdisc(self):
self.assertRaises(
pyroute2.NetlinkError, priv_tc_lib.delete_tc_qdisc,
self.device, rtnl.TC_H_ROOT, namespace=self.namespace)
def test_delete_tc_qdisc_no_qdisc_no_exception(self):
self.assertEqual(2, priv_tc_lib.delete_tc_qdisc(
self.device, rtnl.TC_H_ROOT, namespace=self.namespace,
raise_qdisc_not_found=False))
def test_delete_tc_qdisc_ingress_twice(self):
priv_tc_lib.add_tc_qdisc(self.device, kind='ingress',
namespace=self.namespace)
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
namespace=self.namespace)
self.assertEqual(1, len(qdiscs))
self.assertEqual('ingress', tc_lib._get_attr(qdiscs[0], 'TCA_KIND'))
self.assertIsNone(
priv_tc_lib.delete_tc_qdisc(self.device, kind='ingress',
namespace=self.namespace))
qdiscs = priv_tc_lib.list_tc_qdiscs(self.device,
namespace=self.namespace)
self.assertEqual(0, len(qdiscs))
self.assertEqual(
errno.EINVAL,
priv_tc_lib.delete_tc_qdisc(self.device, kind='ingress',
namespace=self.namespace,
raise_qdisc_not_found=False))

View File

@ -113,6 +113,8 @@ class TestTcCommand(base.BaseTestCase):
'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()
def test_check_kernel_hz_lower_then_zero(self):
self.assertRaises(
@ -164,50 +166,29 @@ class TestTcCommand(base.BaseTestCase):
def test_update_filters_bw_limit(self):
self.tc.update_filters_bw_limit(BW_LIMIT, BURST)
self.execute.assert_has_calls([
mock.call(
["tc", "qdisc", "del", "dev", DEVICE_NAME, "ingress"],
run_as_root=True,
check_exit_code=True,
log_fail_as_error=True,
extra_ok_codes=[1, 2]
),
mock.call(
['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.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)
def test_delete_filters_bw_limit(self):
self.tc.delete_filters_bw_limit()
self.execute.assert_called_once_with(
["tc", "qdisc", "del", "dev", DEVICE_NAME, "ingress"],
run_as_root=True,
check_exit_code=True,
log_fail_as_error=True,
extra_ok_codes=[1, 2]
)
self.mock_delete_tc_qdisc.assert_called_once_with(
DEVICE_NAME, is_ingress=True, raise_interface_not_found=False,
raise_qdisc_not_found=False, namespace=self.tc.namespace)
def test_delete_tbf_bw_limit(self):
self.tc.delete_tbf_bw_limit()
self.execute.assert_called_once_with(
["tc", "qdisc", "del", "dev", DEVICE_NAME, "root"],
run_as_root=True,
check_exit_code=True,
log_fail_as_error=True,
extra_ok_codes=[1, 2]
)
self.mock_delete_tc_qdisc.assert_called_once_with(
DEVICE_NAME, parent='root', raise_interface_not_found=False,
raise_qdisc_not_found=False, namespace=self.tc.namespace)
def test_get_ingress_qdisc_burst_value_burst_not_none(self):
self.assertEqual(