# Copyright 2016 OVH SAS
# All Rights Reserved.
#
#    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.exceptions import qos as qos_exc
from neutron_lib.services.qos import constants as qos_consts
from pyroute2.netlink import rtnl

from neutron.agent.linux import tc_lib
from neutron.common import utils
from neutron.privileged.agent.linux import tc_lib as priv_tc_lib
from neutron.tests import base

DEVICE_NAME = "tap_device"
KERNEL_HZ_VALUE = 1000
BW_LIMIT = 2000  # [kbps]
BURST = 100  # [kbit]
LATENCY = 50  # [ms]

TC_FILTERS_OUTPUT = (
    'filter protocol all pref 49152 u32 \nfilter protocol all pref '
    '49152 u32 fh 800: ht divisor 1 \nfilter protocol all pref 49152 u32 fh '
    '800::800 order 2048 key ht 800 \n  match 00000000/00000000 at 0\n  '
    'police 0x1e rate %(bw)skbit burst %(burst)skbit mtu 2Kb action \n'
    'drop overhead 0b \n  ref 1 bind 1'
) % {'bw': BW_LIMIT, 'burst': BURST}


class BaseUnitConversionTest(object):

    def test_convert_to_kilobits_bare_value(self):
        value = "1000"
        expected_value = 8  # kbit
        self.assertEqual(
            expected_value,
            tc_lib.convert_to_kilobits(value, self.base_unit)
        )

    def test_convert_to_kilobits_bytes_value(self):
        value = "1000b"
        expected_value = 8  # kbit
        self.assertEqual(
            expected_value,
            tc_lib.convert_to_kilobits(value, self.base_unit)
        )

    def test_convert_to_kilobits_bits_value(self):
        value = "1000bit"
        expected_value = utils.bits_to_kilobits(1000, self.base_unit)
        self.assertEqual(
            expected_value,
            tc_lib.convert_to_kilobits(value, self.base_unit)
        )

    def test_convert_to_kilobits_megabytes_value(self):
        value = "1m"
        expected_value = utils.bits_to_kilobits(
            self.base_unit ** 2 * 8, self.base_unit)
        self.assertEqual(
            expected_value,
            tc_lib.convert_to_kilobits(value, self.base_unit)
        )

    def test_convert_to_kilobits_megabits_value(self):
        value = "1mbit"
        expected_value = utils.bits_to_kilobits(
            self.base_unit ** 2, self.base_unit)
        self.assertEqual(
            expected_value,
            tc_lib.convert_to_kilobits(value, self.base_unit)
        )

    def test_convert_to_bytes_wrong_unit(self):
        value = "1Zbit"
        self.assertRaises(
            tc_lib.InvalidUnit,
            tc_lib.convert_to_kilobits, value, self.base_unit
        )


class TestSIUnitConversions(BaseUnitConversionTest, base.BaseTestCase):

    base_unit = constants.SI_BASE


class TestIECUnitConversions(BaseUnitConversionTest, base.BaseTestCase):

    base_unit = constants.IEC_BASE


class TestHandleFromHexToString(base.BaseTestCase):

    def test_run(self):
        test_cases = [(0x1, '0:1'),
                      (0x2a003f, '2a:3f'),
                      (0xf0000, 'f:0'),
                      (0xffffffff, 'ffff:ffff'),
                      (0x12345678, '1234:5678')]
        for _in, expected in test_cases:
            self.assertEqual(expected, tc_lib._handle_from_hex_to_string(_in))


class TestTcCommand(base.BaseTestCase):
    def setUp(self):
        super(TestTcCommand, self).setUp()
        self.tc = tc_lib.TcCommand(DEVICE_NAME, KERNEL_HZ_VALUE)
        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(
            tc_lib.InvalidKernelHzValue,
            tc_lib.TcCommand, DEVICE_NAME, 0
        )
        self.assertRaises(
            tc_lib.InvalidKernelHzValue,
            tc_lib.TcCommand, DEVICE_NAME, -100
        )

    def test_get_filters_bw_limits(self):
        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_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_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 = [
            {'qdisc_type': 'tbf', 'max_kbps': BW_LIMIT, 'burst_kb': BURST}]
        self.assertEqual((BW_LIMIT, BURST), self.tc.get_tbf_bw_limits())

    def test_get_tbf_bw_limits_when_wrong_qdisc(self):
        self.mock_list_tc_qdiscs.return_value = [{'qdisc_type': 'other_type'}]
        self.assertEqual((None, None), self.tc.get_tbf_bw_limits())

    def test_set_tbf_bw_limit(self):
        self.tc.set_tbf_bw_limit(BW_LIMIT, BURST, LATENCY)
        self.mock_add_tc_qdisc.assert_called_once_with(
            DEVICE_NAME, 'tbf', parent='root', max_kbps=BW_LIMIT,
            burst_kb=BURST, latency_ms=LATENCY, kernel_hz=self.tc.kernel_hz,
            namespace=self.tc.namespace)

    def test_update_filters_bw_limit(self):
        self.tc.update_filters_bw_limit(BW_LIMIT, BURST)
        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()
        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.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(
            BURST, self.tc.get_ingress_qdisc_burst_value(BW_LIMIT, BURST)
        )

    def test_get_ingress_qdisc_burst_no_burst_value_given(self):
        expected_burst = BW_LIMIT * qos_consts.DEFAULT_BURST_RATE
        self.assertEqual(
            expected_burst,
            self.tc.get_ingress_qdisc_burst_value(BW_LIMIT, None)
        )

    def test_get_ingress_qdisc_burst_burst_value_zero(self):
        expected_burst = BW_LIMIT * qos_consts.DEFAULT_BURST_RATE
        self.assertEqual(
            expected_burst,
            self.tc.get_ingress_qdisc_burst_value(BW_LIMIT, 0)
        )


class TcTestCase(base.BaseTestCase):

    def setUp(self):
        super(TcTestCase, self).setUp()
        self.mock_add_tc_qdisc = mock.patch.object(
            priv_tc_lib, 'add_tc_qdisc').start()
        self.namespace = 'namespace'

    def test_add_tc_qdisc_htb(self):
        tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle='1:',
                            namespace=self.namespace)
        self.mock_add_tc_qdisc.assert_called_once_with(
            'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='1:0',
            namespace=self.namespace)
        self.mock_add_tc_qdisc.reset_mock()

        tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle='2',
                            namespace=self.namespace)
        self.mock_add_tc_qdisc.assert_called_once_with(
            'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='2:0',
            namespace=self.namespace)
        self.mock_add_tc_qdisc.reset_mock()

        tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle='3:12',
                            namespace=self.namespace)
        self.mock_add_tc_qdisc.assert_called_once_with(
            'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='3:0',
            namespace=self.namespace)
        self.mock_add_tc_qdisc.reset_mock()

        tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle=4,
                            namespace=self.namespace)
        self.mock_add_tc_qdisc.assert_called_once_with(
            'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='4:0',
            namespace=self.namespace)
        self.mock_add_tc_qdisc.reset_mock()

        tc_lib.add_tc_qdisc('device', 'htb', parent='root',
                            namespace=self.namespace)
        self.mock_add_tc_qdisc.assert_called_once_with(
            'device', parent=rtnl.TC_H_ROOT, kind='htb',
            namespace=self.namespace)
        self.mock_add_tc_qdisc.reset_mock()

        tc_lib.add_tc_qdisc('device', 'htb', parent='root', handle=5)
        self.mock_add_tc_qdisc.assert_called_once_with(
            'device', parent=rtnl.TC_H_ROOT, kind='htb', handle='5:0',
            namespace=None)
        self.mock_add_tc_qdisc.reset_mock()

    def test_add_tc_qdisc_tbf(self):
        tc_lib.add_tc_qdisc('device', 'tbf', parent='root', max_kbps=10000,
                            burst_kb=1500, latency_ms=70, kernel_hz=250,
                            namespace=self.namespace)
        burst = tc_lib._get_tbf_burst_value(10000, 1500, 70) * 1000 / 8
        self.mock_add_tc_qdisc.assert_called_once_with(
            'device', parent=rtnl.TC_H_ROOT, kind='tbf', rate=10000 * 125,
            burst=burst, latency=70000, namespace=self.namespace)

    def test_add_tc_qdisc_tbf_missing_arguments(self):
        self.assertRaises(
            qos_exc.TcLibQdiscNeededArguments, tc_lib.add_tc_qdisc,
            'device', 'tbf', parent='root')

    def test_add_tc_qdisc_wrong_qdisc_type(self):
        self.assertRaises(qos_exc.TcLibQdiscTypeError, tc_lib.add_tc_qdisc,
                          mock.ANY, 'wrong_qdic_type_name')

    def test_list_tc_qdiscs_htb(self):
        qdisc = {'index': 2, 'handle': 327680, 'parent': 4294967295,
                 'attrs': (('TCA_KIND', 'htb'), )}
        with mock.patch.object(priv_tc_lib, 'list_tc_qdiscs') as \
                mock_list_tc_qdiscs:
            mock_list_tc_qdiscs.return_value = tuple([qdisc])
            qdiscs = tc_lib.list_tc_qdiscs('device',
                                           namespace=self.namespace)
        self.assertEqual(1, len(qdiscs))
        self.assertEqual('root', qdiscs[0]['parent'])
        self.assertEqual('5:0', qdiscs[0]['handle'])
        self.assertEqual('htb', qdiscs[0]['qdisc_type'])

    @mock.patch('pyroute2.netlink.rtnl.tcmsg.common.tick_in_usec', 15.625)
    def test_list_tc_qdiscs_tbf(self):
        tca_tbf_params = {'buffer': 9375000,
                          'rate': 320000,
                          'limit': 208000}
        qdisc = {'index': 2, 'handle': 327681, 'parent': 4294967295,
                 'attrs': (
                     ('TCA_KIND', 'tbf'),
                     ('TCA_OPTIONS', {'attrs': (
                         ('TCA_TBF_PARMS', tca_tbf_params), )}))
                 }
        with mock.patch.object(priv_tc_lib, 'list_tc_qdiscs') as \
                mock_list_tc_qdiscs:
            mock_list_tc_qdiscs.return_value = tuple([qdisc])
            qdiscs = tc_lib.list_tc_qdiscs('device',
                                           namespace=self.namespace)
        self.assertEqual(1, len(qdiscs))
        self.assertEqual('root', qdiscs[0]['parent'])
        self.assertEqual('5:1', qdiscs[0]['handle'])
        self.assertEqual('tbf', qdiscs[0]['qdisc_type'])
        self.assertEqual(2560, qdiscs[0]['max_kbps'])
        self.assertEqual(1536, qdiscs[0]['burst_kb'])
        self.assertEqual(50, qdiscs[0]['latency_ms'])

    def test__get_tbf_burst_value_when_burst_bigger_then_minimal(self):
        result = tc_lib._get_tbf_burst_value(BW_LIMIT, BURST, KERNEL_HZ_VALUE)
        self.assertEqual(BURST, result)

    def test__get_tbf_burst_value_when_burst_smaller_then_minimal(self):
        result = tc_lib._get_tbf_burst_value(BW_LIMIT, 0, KERNEL_HZ_VALUE)
        self.assertEqual(2, result)


class TcPolicyClassTestCase(base.BaseTestCase):

    def setUp(self):
        super(TcPolicyClassTestCase, self).setUp()
        self.mock_add_tc_policy_class = mock.patch.object(
            priv_tc_lib, 'add_tc_policy_class').start()
        self.mock_list_tc_policy_classes = mock.patch.object(
            priv_tc_lib, 'list_tc_policy_classes').start()
        self.namespace = 'namespace'

    def test_add_tc_policy_class(self):
        tc_lib.add_tc_policy_class(
            'device', 'root', '1:10', min_kbps=1000, max_kbps=2000,
            burst_kb=1600, namespace=self.namespace)
        self.mock_add_tc_policy_class.assert_called_once_with(
            'device', rtnl.TC_H_ROOT, '1:10', 'htb', rate=1000 * 125,
            ceil=2000 * 125, burst=1600 * 125, namespace=self.namespace)

    @mock.patch('pyroute2.netlink.rtnl.tcmsg.common.tick_in_usec', 15.625)
    def test_list_tc_policy_classes(self):
        htb_params = {'buffer': 12500000, 'ceil': 256000, 'rate': 192000}
        self.mock_list_tc_policy_classes.return_value = tuple([
            {'index': 3, 'handle': 65537, 'parent': 4294967295,
             'attrs': (
                 ('TCA_KIND', 'htb'),
                 ('TCA_OPTIONS', {
                     'attrs': tuple([('TCA_HTB_PARMS', htb_params)])}))
             }])
        _class = tc_lib.list_tc_policy_class('device',
                                             namespace=self.namespace)[0]
        reference = {'device': 'device',
                     'index': 3,
                     'namespace': self.namespace,
                     'parent': 'root',
                     'classid': '1:1',
                     'qdisc_type': 'htb',
                     'min_kbps': 1536,
                     'max_kbps': 2048,
                     'burst_kb': 1228}
        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])

    @mock.patch.object(priv_tc_lib, 'add_tc_filter_match32')
    def test_add_tc_filter_vxlan(self, mock_add_filter):
        tc_lib.add_tc_filter_vxlan('device', 'parent', 'classid',
                                   '12:34:56:78:90:ab', 52, namespace='ns')
        keys = ['0x3400/0xffffff00+32', '0x12345678/0xffffffff+42',
                '0x90ab0000/0xffff0000+46']
        mock_add_filter.assert_called_once_with(
            'device', 'parent', 1, 'classid', keys, namespace='ns')
