neutron/neutron/tests/unit/agent/linux/test_l3_tc_lib.py

451 lines
20 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from neutron_lib import constants
from neutron_lib import exceptions
from neutron.agent.linux import l3_tc_lib as tc_lib
from neutron.agent.linux import tc_lib as base_tc_lib
from neutron.tests import base
FLOATING_IP_DEVICE_NAME = "qg-device_rfp"
FLOATING_IP_ROUTER_NAMESPACE = "qrouter-namespace_snat-namespace"
FLOATING_IP_1 = "172.16.5.146"
FLOATING_IP_2 = "172.16.10.105"
FILETER_ID_1 = "800::800"
FILETER_ID_2 = "800::801"
TC_INGRESS_FILTERS_BASE = (
'filter protocol ip u32 \n'
'filter protocol ip u32 %(chain_value)sfh 800: ht divisor 1 \n'
'filter protocol ip u32 %(chain_value)sfh %(filter_id1)s order 2048 key '
'ht 800 bkt 0 '
'flowid :1 (rule hit 0 success 0)\n'
' match IP dst %(fip1)s/32 (success 0 ) \n'
' police 0x3 rate 3000Kbit burst 3Mb mtu 64Kb action drop overhead 0b \n'
'ref 1 bind 1\n'
'\n'
' Sent 111 bytes 222 pkts (dropped 0, overlimits 0) \n'
'filter protocol ip u32 %(chain_value)sfh %(filter_id2)s order 2049 key '
'ht 800 bkt 0 '
'flowid :1 (rule hit 0 success 0)\n'
' match IP dst %(fip2)s/32 (success 0 ) \n'
' police 0x1b rate 22000Kbit burst 22Mb mtu 64Kb action drop '
'overhead 0b \n'
'ref 1 bind 1\n'
'\n'
' Sent 111 bytes 222 pkts (dropped 0, overlimits 0)\n')
TC_INGRESS_FILTERS_WITHOUT_CHAIN = TC_INGRESS_FILTERS_BASE % {
"chain_value": "",
"filter_id1": FILETER_ID_1,
"fip1": FLOATING_IP_1,
"filter_id2": FILETER_ID_2,
"fip2": FLOATING_IP_2}
# NOTE(slaweq): in iproute 4.15 chain value was added to filter output
TC_INGRESS_FILTERS_WITH_CHAIN = TC_INGRESS_FILTERS_BASE % {
"chain_value": "chain 1 ",
"filter_id1": FILETER_ID_1,
"fip1": FLOATING_IP_1,
"filter_id2": FILETER_ID_2,
"fip2": FLOATING_IP_2}
TC_INGRESS_FILTERS_DUP_WITHOUT_CHAIN = TC_INGRESS_FILTERS_WITHOUT_CHAIN + (
'filter protocol ip u32 %(chain_value)sfh %(filter_id2)s order 2049 key '
'ht 800 bkt 0 '
'flowid :1 (rule hit 0 success 0)\n'
' match IP dst %(fip2)s/32 (success 0 ) \n'
' police 0x1b rate 22000Kbit burst 22Mb mtu 64Kb action drop '
'overhead 0b \n'
'ref 1 bind 1\n'
'\n'
' Sent 111 bytes 222 pkts (dropped 0, overlimits 0)\n') % {
"chain_value": "",
"filter_id2": FILETER_ID_2,
"fip2": FLOATING_IP_2}
TC_INGRESS_FILTERS_DUP_WITH_CHAIN = TC_INGRESS_FILTERS_WITH_CHAIN + (
'filter protocol ip u32 %(chain_value)sfh %(filter_id2)s order 2049 key '
'ht 800 bkt 0 '
'flowid :1 (rule hit 0 success 0)\n'
' match IP dst %(fip2)s/32 (success 0 ) \n'
' police 0x1b rate 22000Kbit burst 22Mb mtu 64Kb action drop '
'overhead 0b \n'
'ref 1 bind 1\n'
'\n'
' Sent 111 bytes 222 pkts (dropped 0, overlimits 0)\n') % {
"chain_value": "chain 1 ",
"filter_id2": FILETER_ID_2,
"fip2": FLOATING_IP_2}
TC_EGRESS_FILTERS_BASE = (
'filter protocol ip u32 \n'
'filter protocol ip u32 %(chain_name)sfh 800: ht divisor 1 \n'
'filter protocol ip u32 %(chain_name)sfh %(filter_id1)s order 2048 key '
'ht 800 bkt 0 '
'flowid :1 (rule hit 0 success 0)\n'
' match IP src %(fip1)s/32 (success 0 ) \n'
' police 0x4 rate 3000Kbit burst 3Mb mtu 64Kb action drop overhead 0b \n'
'ref 1 bind 1\n'
'\n'
' Sent 111 bytes 222 pkts (dropped 0, overlimits 0) \n'
'filter protocol ip u32 %(chain_name)sfh %(filter_id2)s order 2049 key '
'ht 800 bkt 0 '
'flowid :1 (rule hit 0 success 0)\n'
' match IP src %(fip2)s/32 (success 0 ) \n'
' police 0x1c rate 22000Kbit burst 22Mb mtu 64Kb action drop '
'overhead 0b \n'
'ref 1 bind 1\n'
'\n'
' Sent 111 bytes 222 pkts (dropped 0, overlimits 0)\n')
TC_EGRESS_FILTERS_WITHOUT_CHAIN = TC_EGRESS_FILTERS_BASE % {
"chain_name": "",
"filter_id1": FILETER_ID_1,
"fip1": FLOATING_IP_1,
"filter_id2": FILETER_ID_2,
"fip2": FLOATING_IP_2}
TC_EGRESS_FILTERS_WITH_CHAIN = TC_EGRESS_FILTERS_BASE % {
"chain_name": "chain 1 ",
"filter_id1": FILETER_ID_1,
"fip1": FLOATING_IP_1,
"filter_id2": FILETER_ID_2,
"fip2": FLOATING_IP_2}
INGRESS_QSIC_ID = "ffff:"
EGRESS_QDISC_ID = "1:"
QDISC_IDS = {constants.INGRESS_DIRECTION: INGRESS_QSIC_ID,
constants.EGRESS_DIRECTION: EGRESS_QDISC_ID}
TC_QDISCS = [{'handle': '1:', 'qdisc_type': 'htb', 'parent': 'root'},
{'handle': 'ffff:', 'qdisc_type': 'ingress', 'parent': 'ingress'}]
class TestFloatingIPTcCommandBase(base.BaseTestCase):
def setUp(self):
super(TestFloatingIPTcCommandBase, self).setUp()
self.tc = tc_lib.FloatingIPTcCommandBase(
FLOATING_IP_DEVICE_NAME,
namespace=FLOATING_IP_ROUTER_NAMESPACE)
self.execute = mock.patch('neutron.agent.common.utils.execute').start()
def test__get_qdisc_id_for_filter(self):
with mock.patch.object(base_tc_lib, 'list_tc_qdiscs',
return_value=TC_QDISCS):
q1 = self.tc._get_qdisc_id_for_filter(constants.INGRESS_DIRECTION)
self.assertEqual(INGRESS_QSIC_ID, q1)
q2 = self.tc._get_qdisc_id_for_filter(constants.EGRESS_DIRECTION)
self.assertEqual(EGRESS_QDISC_ID, q2)
@mock.patch.object(base_tc_lib, 'add_tc_qdisc')
def test__add_qdisc(self, mock_add_tc_qdisc):
self.tc._add_qdisc(constants.INGRESS_DIRECTION)
mock_add_tc_qdisc.assert_called_once_with(
self.tc.name, 'ingress', namespace=self.tc.namespace)
mock_add_tc_qdisc.reset_mock()
self.tc._add_qdisc(constants.EGRESS_DIRECTION)
mock_add_tc_qdisc.assert_called_once_with(
self.tc.name, 'htb', parent='root', namespace=self.tc.namespace)
def test__get_filters(self):
self.tc._get_filters(INGRESS_QSIC_ID)
self.execute.assert_called_with(
['ip', 'netns', 'exec', FLOATING_IP_ROUTER_NAMESPACE,
'tc', '-p', '-s', '-d', 'filter', 'show', 'dev',
FLOATING_IP_DEVICE_NAME,
'parent', INGRESS_QSIC_ID, 'prio', 1],
run_as_root=True,
check_exit_code=True,
log_fail_as_error=True,
extra_ok_codes=None,
privsep_exec=True
)
def _test__get_filterid_for_ip(self, filters):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filters') as get_filters:
get_filters.return_value = filters
f_id = self.tc._get_filterid_for_ip(INGRESS_QSIC_ID, FLOATING_IP_1)
self.assertEqual(FILETER_ID_1, f_id)
def test__get_filterid_for_ip_without_chain(self):
self._test__get_filterid_for_ip(TC_EGRESS_FILTERS_WITHOUT_CHAIN)
def test__get_filterid_for_ip_with_chain(self):
self._test__get_filterid_for_ip(TC_EGRESS_FILTERS_WITH_CHAIN)
def test__get_filterid_for_ip_no_output(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filters') as get_filters:
get_filters.return_value = ""
self.assertRaises(exceptions.FilterIDForIPNotFound,
self.tc._get_filterid_for_ip,
INGRESS_QSIC_ID, FLOATING_IP_1)
def _test__get_filterid_for_ip_duplicated(self, filters):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filters') as get_filters:
get_filters.return_value = filters
self.assertRaises(exceptions.MultipleFilterIDForIPFound,
self.tc._get_filterid_for_ip,
INGRESS_QSIC_ID, FLOATING_IP_2)
def test__get_filterid_for_ip_duplicated_without_chain(self):
self._test__get_filterid_for_ip_duplicated(
TC_INGRESS_FILTERS_DUP_WITHOUT_CHAIN)
def test__get_filterid_for_ip_duplicated_with_chain(self):
self._test__get_filterid_for_ip_duplicated(
TC_INGRESS_FILTERS_DUP_WITH_CHAIN)
def _test__get_filterid_for_ip_not_found(self, filters):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filters') as get_filters:
get_filters.return_value = filters
self.assertRaises(exceptions.FilterIDForIPNotFound,
self.tc._get_filterid_for_ip,
INGRESS_QSIC_ID, "1.1.1.1")
def test__get_filterid_for_ip_not_found_without_chain(self):
self._test__get_filterid_for_ip_not_found(
TC_EGRESS_FILTERS_WITHOUT_CHAIN)
def test__get_filterid_for_ip_not_found_with_chain(self):
self._test__get_filterid_for_ip_not_found(TC_EGRESS_FILTERS_WITH_CHAIN)
def test__del_filter_by_id(self):
self.tc._del_filter_by_id(INGRESS_QSIC_ID, FLOATING_IP_1)
self.execute.assert_called_once_with(
['ip', 'netns', 'exec', FLOATING_IP_ROUTER_NAMESPACE,
'tc', 'filter', 'del', 'dev', FLOATING_IP_DEVICE_NAME,
'parent', INGRESS_QSIC_ID,
'prio', 1, 'handle', FLOATING_IP_1, 'u32'],
run_as_root=True,
check_exit_code=True,
log_fail_as_error=True,
extra_ok_codes=None,
privsep_exec=True
)
def _test__get_qdisc_filters(self, filters):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filters') as get_filters:
get_filters.return_value = filters
f_ids = self.tc._get_qdisc_filters(INGRESS_QSIC_ID)
self.assertEqual([FILETER_ID_1, FILETER_ID_2], f_ids)
def test__get_qdisc_filters_without_chain(self):
self._test__get_qdisc_filters(TC_EGRESS_FILTERS_WITHOUT_CHAIN)
def test__get_qdisc_filters_with_chain(self):
self._test__get_qdisc_filters(TC_EGRESS_FILTERS_WITH_CHAIN)
def test__get_qdisc_filters_no_output(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filters') as get_filters:
get_filters.return_value = ""
f_ids = self.tc._get_qdisc_filters(INGRESS_QSIC_ID)
self.assertEqual(0, len(f_ids))
def test__add_filter(self):
protocol = ['protocol', 'ip']
prio = ['prio', 1]
match = ['u32', 'match', 'ip', 'dst', FLOATING_IP_1]
police = ['police', 'rate', '1kbit', 'burst', '1kbit',
'mtu', '64kb', 'drop', 'flowid', ':1']
args = protocol + prio + match + police
cmd = ['tc', 'filter', 'add', 'dev', FLOATING_IP_DEVICE_NAME,
'parent', INGRESS_QSIC_ID] + args
self.tc._add_filter(INGRESS_QSIC_ID,
constants.INGRESS_DIRECTION,
FLOATING_IP_1, 1, 1)
self.execute.assert_called_once_with(
['ip', 'netns', 'exec', FLOATING_IP_ROUTER_NAMESPACE] + cmd,
run_as_root=True,
check_exit_code=True,
log_fail_as_error=True,
extra_ok_codes=None,
privsep_exec=True
)
def test__get_or_create_qdisc(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc1:
get_disc1.return_value = None
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_add_qdisc'):
with mock.patch.object(
tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc2:
get_disc2.return_value = INGRESS_QSIC_ID
qdisc_id = self.tc._get_or_create_qdisc(
constants.INGRESS_DIRECTION)
self.assertEqual(INGRESS_QSIC_ID, qdisc_id)
def test__get_or_create_qdisc_failed(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc1:
get_disc1.return_value = None
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_add_qdisc'):
with mock.patch.object(
tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc2:
get_disc2.return_value = None
self.assertRaises(exceptions.FailedToAddQdiscToDevice,
self.tc._get_or_create_qdisc,
constants.INGRESS_DIRECTION)
class TestFloatingIPTcCommand(base.BaseTestCase):
def setUp(self):
super(TestFloatingIPTcCommand, self).setUp()
self.tc = tc_lib.FloatingIPTcCommand(
FLOATING_IP_DEVICE_NAME,
namespace=FLOATING_IP_ROUTER_NAMESPACE)
self.execute = mock.patch('neutron.agent.common.utils.execute').start()
def _test_clear_all_filters(self, filters):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc:
get_disc.return_value = EGRESS_QDISC_ID
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filters') as get_filters:
get_filters.return_value = filters
self.tc.clear_all_filters(constants.EGRESS_DIRECTION)
self.assertEqual(2, self.execute.call_count)
def test_clear_all_filters_without_chain(self):
self._test_clear_all_filters(TC_EGRESS_FILTERS_WITHOUT_CHAIN)
def test_clear_all_filters_with_chain(self):
self._test_clear_all_filters(TC_EGRESS_FILTERS_WITH_CHAIN)
def test_set_ip_rate_limit_filter_existed(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc:
get_disc.return_value = EGRESS_QDISC_ID
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filterid_for_ip') as get_filter:
get_filter.return_value = FILETER_ID_1
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_del_filter_by_id') as del_filter:
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_add_filter') as add_filter:
ip = "111.111.111.111"
self.tc.set_ip_rate_limit(constants.EGRESS_DIRECTION,
ip, 1, 1)
del_filter.assert_called_once_with(
EGRESS_QDISC_ID, FILETER_ID_1)
add_filter.assert_called_once_with(
EGRESS_QDISC_ID, constants.EGRESS_DIRECTION,
ip, 1, 1)
def _test_set_ip_rate_limit_no_qdisc(self, filters):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc:
get_disc.return_value = None
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_add_qdisc'):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filters') as get_filters:
get_filters.return_value = filters
get_disc.return_value = INGRESS_QSIC_ID
ip = "111.111.111.111"
self.tc.set_ip_rate_limit(constants.INGRESS_DIRECTION,
ip, 1, 1)
protocol = ['protocol', 'ip']
prio = ['prio', 1]
_match = 'dst'
match = ['u32', 'match', 'ip', _match, ip]
police = ['police', 'rate', '1kbit', 'burst', '1kbit',
'mtu', '64kb', 'drop', 'flowid', ':1']
args = protocol + prio + match + police
self.execute.assert_called_once_with(
['ip', 'netns', 'exec', FLOATING_IP_ROUTER_NAMESPACE,
'tc', 'filter', 'add', 'dev', FLOATING_IP_DEVICE_NAME,
'parent', INGRESS_QSIC_ID] + args,
run_as_root=True,
check_exit_code=True,
log_fail_as_error=True,
extra_ok_codes=None,
privsep_exec=True
)
def test_set_ip_rate_limit_no_qdisc_without_chain(self):
self._test_set_ip_rate_limit_no_qdisc(TC_INGRESS_FILTERS_WITHOUT_CHAIN)
def test_set_ip_rate_limit_no_qdisc_with_chain(self):
self._test_set_ip_rate_limit_no_qdisc(TC_INGRESS_FILTERS_WITH_CHAIN)
def test_clear_ip_rate_limit(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc:
get_disc.return_value = EGRESS_QDISC_ID
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filterid_for_ip') as get_filter_id:
get_filter_id.return_value = FILETER_ID_1
self.tc.clear_ip_rate_limit(constants.EGRESS_DIRECTION,
FLOATING_IP_1)
self.execute.assert_called_once_with(
['ip', 'netns', 'exec', FLOATING_IP_ROUTER_NAMESPACE,
'tc', 'filter', 'del', 'dev', FLOATING_IP_DEVICE_NAME,
'parent', EGRESS_QDISC_ID,
'prio', 1, 'handle', FILETER_ID_1, 'u32'],
run_as_root=True,
check_exit_code=True,
log_fail_as_error=True,
extra_ok_codes=None,
privsep_exec=True
)
def test_get_filter_id_for_ip(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc:
get_disc.return_value = EGRESS_QDISC_ID
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_filterid_for_ip') as get_filter_id:
self.tc.get_filter_id_for_ip(constants.EGRESS_DIRECTION,
'8.8.8.8')
get_filter_id.assert_called_once_with(EGRESS_QDISC_ID,
'8.8.8.8')
def test_get_existing_filter_ids(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc:
get_disc.return_value = EGRESS_QDISC_ID
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_filters') as get_filter_ids:
self.tc.get_existing_filter_ids(constants.EGRESS_DIRECTION)
get_filter_ids.assert_called_once_with(EGRESS_QDISC_ID)
def test_delete_filter_ids(self):
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_get_qdisc_id_for_filter') as get_disc:
get_disc.return_value = EGRESS_QDISC_ID
with mock.patch.object(tc_lib.FloatingIPTcCommandBase,
'_del_filter_by_id') as del_filter_id:
self.tc.delete_filter_ids(constants.EGRESS_DIRECTION,
[FILETER_ID_1, FILETER_ID_2])
del_filter_id.assert_has_calls(
[mock.call(EGRESS_QDISC_ID, FILETER_ID_1),
mock.call(EGRESS_QDISC_ID, FILETER_ID_2)])