# Copyright (c) 2016 Cisco Systems, Inc.
# 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.

import collections
import mock
from oslo_config import cfg

from networking_cisco.backwards_compatibility import constants as p_const
from networking_cisco.backwards_compatibility import ml2_api as api

from networking_cisco.ml2_drivers.nexus import (
    constants as const)
from networking_cisco.ml2_drivers.nexus import exceptions
from networking_cisco.ml2_drivers.nexus import nexus_db_v2

from networking_cisco.ml2_drivers.nexus import (
    nexus_restapi_snippets as snipp)
from networking_cisco.tests.unit.ml2_drivers.nexus import (
    test_cisco_nexus_base as base)


class TestCiscoNexusVxlanDeviceConfig(object):
    """Config Data for Cisco ML2 VXLAN Nexus device driver."""

    test_configs = {
        'test_vxlan_config1':
            base.TestCiscoNexusBase.TestConfigObj(
                base.NEXUS_IP_ADDRESS_1,
                base.HOST_NAME_1,
                base.NEXUS_PORT_1,
                base.INSTANCE_1,
                base.VLAN_ID_1,
                base.VXLAN_ID,
                base.MCAST_GROUP,
                base.DEVICE_OWNER_COMPUTE,
                {},
                None,
                base.NORMAL_VNIC),
        'test_vxlan_config2':
            base.TestCiscoNexusBase.TestConfigObj(
                base.NEXUS_IP_ADDRESS_1,
                base.HOST_NAME_2,
                base.NEXUS_PORT_2,
                base.INSTANCE_2,
                base.VLAN_ID_1,
                base.VXLAN_ID,
                base.MCAST_GROUP,
                base.DEVICE_OWNER_COMPUTE,
                {},
                None,
                base.NORMAL_VNIC),
        'test_vxlan_config3':
            base.TestCiscoNexusBase.TestConfigObj(
                base.NEXUS_IP_ADDRESS_6,
                base.HOST_NAME_3,
                base.NEXUS_PORT_1,
                base.INSTANCE_1,
                base.VLAN_ID_1,
                base.VXLAN_ID,
                base.MCAST_GROUP,
                base.DEVICE_OWNER_COMPUTE,
                {},
                None,
                base.NORMAL_VNIC),
        'test_vxlan_config4':
            base.TestCiscoNexusBase.TestConfigObj(
                base.NEXUS_IP_ADDRESS_7,
                base.HOST_NAME_3,
                base.NEXUS_DUAL_2,
                base.INSTANCE_1,
                base.VLAN_ID_1,
                base.VXLAN_ID,
                base.MCAST_GROUP,
                base.DEVICE_OWNER_COMPUTE,
                {},
                None,
                base.NORMAL_VNIC),
        'test_vxlan_config5':
            base.TestCiscoNexusBase.TestConfigObj(
                base.NEXUS_IP_ADDRESS_8,
                base.HOST_NAME_4,
                base.NEXUS_PORT_1,
                base.INSTANCE_1,
                base.VLAN_ID_1,
                base.VXLAN_ID,
                base.MCAST_GROUP,
                base.DEVICE_OWNER_COMPUTE,
                {},
                None,
                base.NORMAL_VNIC),
        'test_vxlan_config6':
            base.TestCiscoNexusBase.TestConfigObj(
                base.NEXUS_IP_ADDRESS_8,
                base.HOST_NAME_5,
                base.NEXUS_PORT_2,
                base.INSTANCE_1,
                base.VLAN_ID_2,
                base.VXLAN_ID + 1,
                base.MCAST_GROUP,
                base.DEVICE_OWNER_COMPUTE,
                {},
                None,
                base.NORMAL_VNIC),
    }

    test_configs = collections.OrderedDict(sorted(test_configs.items()))


class TestCiscoNexusVxlanResults(
    base.TestCiscoNexusBaseResults):

    """Unit tests driver results for Cisco ML2 Nexus."""

    test_results = {

        # The following contains desired Nexus output for
        # some basic vxlan config.
        'add_port_driver_result': [
            [(snipp.PATH_VNI_UPDATE % ('1', '70000')),
             base.NEXUS_IP_ADDRESS_1,
             (snipp.BODY_VNI_UPDATE % (
                 '70000', '70000', '70000',
                 base.MCAST_GROUP)),
             base.POST],
            [snipp.PATH_ALL,
             base.NEXUS_IP_ADDRESS_1,
             (snipp.BODY_VXLAN_ADD % (267, 70000)),
             base.POST],
            [(snipp.PATH_IF % 'phys-[eth1/10]'),
             base.NEXUS_IP_ADDRESS_1,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '+267')),
             base.POST]
        ],

        'delete_port_driver_result': [
            [(snipp.PATH_VNI_UPDATE % ('1', '70000')),
             base.NEXUS_IP_ADDRESS_1,
             '',
             base.DELETE],
            [(snipp.PATH_IF % 'phys-[eth1/10]'),
             base.NEXUS_IP_ADDRESS_1,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '-267')),
             base.POST],
            [(snipp.PATH_VLAN % '267'),
             base.NEXUS_IP_ADDRESS_1,
             '',
             base.DELETE]
        ],


        'add_port2_driver_result': [
            [snipp.PATH_ALL,
             base.NEXUS_IP_ADDRESS_1,
             (snipp.BODY_VXLAN_ADD % (267, 70000)),
             base.POST],
            [(snipp.PATH_IF % 'phys-[eth1/20]'),
             base.NEXUS_IP_ADDRESS_1,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '+267')),
             base.POST]
        ],

        'delete_port2_driver_result': [
            [(snipp.PATH_IF % 'phys-[eth1/20]'),
             base.NEXUS_IP_ADDRESS_1,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '-267')),
             base.POST],
        ],

        'add_port_driver_result3': [
            [(snipp.PATH_VNI_UPDATE % ('1', '70000')),
             base.NEXUS_IP_ADDRESS_6,
             (snipp.BODY_VNI_UPDATE % (
                 '70000', '70000', '70000',
                 base.MCAST_GROUP)),
             base.POST],
            [(snipp.PATH_VNI_UPDATE % ('1', '70000')),
             base.NEXUS_IP_ADDRESS_7,
             (snipp.BODY_VNI_UPDATE % (
                 '70000', '70000', '70000',
                 base.MCAST_GROUP)),
             base.POST],
            [snipp.PATH_ALL,
             base.NEXUS_IP_ADDRESS_6,
             (snipp.BODY_VXLAN_ADD % (267, 70000)),
             base.POST],
            [(snipp.PATH_IF % 'phys-[eth1/10]'),
             base.NEXUS_IP_ADDRESS_6,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '+267')),
             base.POST],
            [snipp.PATH_ALL,
             base.NEXUS_IP_ADDRESS_7,
             (snipp.BODY_VXLAN_ADD % (267, 70000)),
             base.POST],
            [(snipp.PATH_IF % 'phys-[eth1/2]'),
             base.NEXUS_IP_ADDRESS_7,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '+267')),
             base.POST],
            [snipp.PATH_ALL,
             base.NEXUS_IP_ADDRESS_7,
             (snipp.BODY_VXLAN_ADD % (267, 70000)),
             base.POST],
            [(snipp.PATH_IF % 'phys-[eth1/3]'),
             base.NEXUS_IP_ADDRESS_7,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '+267')),
             base.POST]
        ],

        'delete_port_driver_result3': [
            [(snipp.PATH_VNI_UPDATE % ('1', '70000')),
             base.NEXUS_IP_ADDRESS_6,
             '',
             base.DELETE],
            [(snipp.PATH_VNI_UPDATE % ('1', '70000')),
             base.NEXUS_IP_ADDRESS_7,
             '',
             base.DELETE],
            [(snipp.PATH_IF % 'phys-[eth1/10]'),
             base.NEXUS_IP_ADDRESS_6,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '-267')),
             base.POST],
            [(snipp.PATH_VLAN % '267'),
             base.NEXUS_IP_ADDRESS_6,
             '',
             base.DELETE],
            [(snipp.PATH_IF % 'phys-[eth1/2]'),
             base.NEXUS_IP_ADDRESS_7,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '-267')),
             base.POST],
            [(snipp.PATH_VLAN % '267'),
             base.NEXUS_IP_ADDRESS_7,
             '',
             base.DELETE],
            [(snipp.PATH_IF % 'phys-[eth1/3]'),
             base.NEXUS_IP_ADDRESS_7,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '-267')),
             base.POST],
        ],

        'add_port_driver_result2': [
            [(snipp.PATH_VNI_UPDATE % ('1', '70001')),
             base.NEXUS_IP_ADDRESS_8,
             (snipp.BODY_VNI_UPDATE % (
                 '70001', '70001', '70001',
                 base.MCAST_GROUP)),
             base.POST],
            [snipp.PATH_ALL,
             base.NEXUS_IP_ADDRESS_8,
             (snipp.BODY_VXLAN_ADD % (265, 70001)),
             base.POST],
            [(snipp.PATH_IF % 'phys-[eth1/20]'),
             base.NEXUS_IP_ADDRESS_8,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '+265')),
             base.POST]
        ],

        'delete_port_driver_result2': [
            [(snipp.PATH_VNI_UPDATE % ('1', '70001')),
             base.NEXUS_IP_ADDRESS_8,
             '',
             base.DELETE],
            [(snipp.PATH_IF % 'phys-[eth1/20]'),
             base.NEXUS_IP_ADDRESS_8,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '-265')),
             base.POST],
            [(snipp.PATH_VLAN % '265'),
             base.NEXUS_IP_ADDRESS_8,
             '',
             base.DELETE]
        ],
        'add_port_driver_result4': [
            [(snipp.PATH_VNI_UPDATE % ('1', '70000')),
             base.NEXUS_IP_ADDRESS_8,
             (snipp.BODY_VNI_UPDATE % (
                 '70000', '70000', '70000',
                 base.MCAST_GROUP)),
             base.POST],
            [snipp.PATH_ALL,
             base.NEXUS_IP_ADDRESS_8,
             (snipp.BODY_VXLAN_ADD % (267, 70000)),
             base.POST],
            [(snipp.PATH_IF % 'phys-[eth1/10]'),
             base.NEXUS_IP_ADDRESS_8,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '+267')),
             base.POST]
        ],

        'delete_port_driver_result4': [
            [(snipp.PATH_VNI_UPDATE % ('1', '70000')),
             base.NEXUS_IP_ADDRESS_8,
             '',
             base.DELETE],
            [(snipp.PATH_IF % 'phys-[eth1/10]'),
             base.NEXUS_IP_ADDRESS_8,
             (snipp.BODY_TRUNKVLAN % ('l1PhysIf', '', '-267')),
             base.POST],
            [(snipp.PATH_VLAN % '267'),
             base.NEXUS_IP_ADDRESS_8,
             '',
             base.DELETE]
        ],

    }


class TestCiscoNexusVxlanDevice(base.TestCiscoNexusBase,
                           TestCiscoNexusVxlanDeviceConfig):

    """Unit tests for Cisco ML2 VXLAN Nexus device driver."""

    def setUp(self):
        """Sets up mock driver, and switch and credentials dictionaries."""

        cfg.CONF.set_override('switch_heartbeat_time', 0, 'ml2_cisco')
        super(TestCiscoNexusVxlanDevice, self).setUp()
        self.mock_driver.reset_mock()
        self.addCleanup(self._clear_nve_db)
        self.results = TestCiscoNexusVxlanResults()

    def _clear_nve_db(self):
        nexus_db_v2.remove_all_nexusnve_bindings()

    def test_enable_vxlan_feature_failure(self):
        """Verifies exception during enable VXLAN driver. """

        # Set configuration variable to add/delete the VXLAN global nexus
        # switch values.
        cfg.CONF.set_override('vxlan_global_config', True, 'ml2_cisco')
        self._create_port_failure(
            'rest_post.side_effect',
            '{"fmVnSegment": {"attributes": {"adminSt": "enabled"}}}',
            'test_vxlan_config1',
            __name__)

    def test_disable_vxlan_feature_failure(self):
        """Verifies exception during disable VXLAN driver. """

        # Set configuration variable to add/delete the VXLAN global nexus
        # switch values.
        cfg.CONF.set_override('vxlan_global_config', True, 'ml2_cisco')
        self._delete_port_failure(
            'rest_post.side_effect',
            '{"fmVnSegment": {"attributes": {"adminSt": "disabled"}}}',
            'test_vxlan_config1',
            __name__)

    def test_create_nve_member_failure(self):
        """Verifies exception during create nve member driver. """

        self._create_port_failure(
            'rest_post.side_effect',
            'api/mo/sys/epId-1/nws/vni-70000.json',
            'test_vxlan_config1',
            __name__)

    def test_delete_nve_member_failure(self):
        """Verifies exception during delete nve member driver. """

        self._delete_port_failure(
            'rest_delete.side_effect',
            'api/mo/sys/epId-1/nws/vni-70000.json',
            'test_vxlan_config1',
            __name__)

    def test_nexus_vxlan_one_network_two_hosts(self):
        """Tests creation and deletion of two new virtual Ports."""

        self._basic_create_verify_port_vlan(
            'test_vxlan_config1',
            self.results.get_test_results(
                'add_port_driver_result'))

        self._create_port(
            self.test_configs['test_vxlan_config2'])
        self._verify_results(
            self.results.get_test_results(
                'add_port2_driver_result'))

        bindings = nexus_db_v2.get_nexusvlan_binding(
                       base.VLAN_ID_1,
                       base.NEXUS_IP_ADDRESS_1)
        self.assertEqual(2, len(bindings))

        # Clean all the driver mock_calls so we can evaluate
        # results of delete operations.
        self.mock_driver.reset_mock()

        self._basic_delete_verify_port_vlan(
            'test_vxlan_config2',
            self.results.get_test_results(
                'delete_port2_driver_result'),
            nbr_of_bindings=1)

        self._basic_delete_verify_port_vlan(
            'test_vxlan_config1',
            self.results.get_test_results(
                'delete_port_driver_result'))

    def test_nexus_missing_vxlan_fields(self):
        """Test handling of a VXLAN NexusMissingRequiredFields exception.

        Test the Cisco NexusMissingRequiredFields exception by using
        empty VNI and mcast address values during port update event.
        """
        local_test_configs = {
            'test_vxlan_config_no_vni':
                base.TestCiscoNexusBase.TestConfigObj(
                    base.NEXUS_IP_ADDRESS_1,
                    base.HOST_NAME_1,
                    base.NEXUS_PORT_1,
                    base.INSTANCE_1,
                    base.VLAN_ID_1,
                    None,
                    base.MCAST_GROUP,
                    base.DEVICE_OWNER_COMPUTE,
                    {},
                    None,
                    base.NORMAL_VNIC),
            'test_vxlan_config_no_mcast':
                base.TestCiscoNexusBase.TestConfigObj(
                    base.NEXUS_IP_ADDRESS_1,
                    base.HOST_NAME_1,
                    base.NEXUS_PORT_1,
                    base.INSTANCE_1,
                    base.VLAN_ID_1,
                    base.VXLAN_ID,
                    None,
                    base.DEVICE_OWNER_COMPUTE,
                    {},
                    None,
                    base.NORMAL_VNIC),
        }
        test_list = ('test_vxlan_config_no_vni',
                     'test_vxlan_config_no_mcast')
        for test_name in test_list:
            self.assertRaises(
                exceptions.NexusMissingRequiredFields,
                self._create_port,
                local_test_configs[test_name])

    def test_nexus_vxlan_bind_port(self):
        """Test VXLAN bind_port method processing.

        Verify the bind_port method allocates the VLAN segment correctly.
        """
        expected_dynamic_segment = {
            api.SEGMENTATION_ID: mock.ANY,
            api.PHYSICAL_NETWORK: base.PHYSNET,
            api.ID: mock.ANY,
            api.NETWORK_TYPE: p_const.TYPE_VLAN}

        self.mock_get_dynamic_segment.return_value = expected_dynamic_segment

        self._bind_port(self.test_configs['test_vxlan_config1'])
        self.mock_continue_binding.assert_called_once_with(
            base.NETID,
            [expected_dynamic_segment])

    def test_nexus_vxlan_bind_port_no_physnet(self):
        """Test VXLAN bind_port error processing.

        Verify that continue_binding() method is not called when no 'physnet'
        key is present in the nexus switch dictionary.
        """

        cfg.CONF.set_override(
            const.PHYSNET, None,
            cfg.CONF.ml2_cisco.nexus_switches.get(
                base.NEXUS_IP_ADDRESS_1)._group)

        try:
            self._bind_port(self.test_configs['test_vxlan_config1'])
        except exceptions.PhysnetNotConfigured:
            assert not self.mock_continue_binding.called

    def test_nexus_vxlan_bind_port_no_dynamic_segment(self):
        """Test VXLAN bind_port processing.

        Verify that the continue_binding() method is not called when the vlan
        dynamic segment wasn't allocated.
        """

        self.mock_get_dynamic_segment.return_value = None

        try:
            self._bind_port(self.test_configs['test_vxlan_config1'])
        except exceptions.NoDynamicSegmentAllocated:
            assert not self.mock_continue_binding.called

    def test_nexus_vxlan_one_network(self):
        """Test processing for creating one VXLAN segment."""

        # Since test_vxlan_config3 & test_vxlan_config4 share
        # the same host name they both get processed in the
        # next call.
        self._basic_create_verify_port_vlan(
            'test_vxlan_config3',
            self.results.get_test_results(
                'add_port_driver_result3'))

        for switch_ip, nbr_bind in [
            (base.NEXUS_IP_ADDRESS_6, 1),
            (base.NEXUS_IP_ADDRESS_7, 2)]:
            bindings = nexus_db_v2.get_nexusvlan_binding(
                           base.VLAN_ID_1,
                           switch_ip)
            self.assertEqual(nbr_bind, len(bindings))
            binding = nexus_db_v2.get_nve_switch_bindings(switch_ip)
            self.assertEqual(1, len(binding))

        # Since test_vxlan_config3 & test_vxlan_config4 share
        # the same host name they both get processed in the
        # next call.
        self._basic_delete_verify_port_vlan(
            'test_vxlan_config3',
            self.results.get_test_results(
                'delete_port_driver_result3'))

        for switch_ip in [
            base.NEXUS_IP_ADDRESS_6,
            base.NEXUS_IP_ADDRESS_7]:
            try:
                bindings = nexus_db_v2.get_nexusvlan_binding(
                               base.VLAN_ID_1,
                               switch_ip)
            except exceptions.NexusPortBindingNotFound:
                bindings = []
            self.assertEqual(0, len(bindings))
            try:
                binding = nexus_db_v2.get_nve_switch_bindings(switch_ip)
            except exceptions.NexusPortBindingNotFound:
                binding = []
            self.assertEqual(0, len(binding))

    def test_nexus_vxlan_two_network(self):
        """Test processing for creating one VXLAN segment."""

        self._basic_create_verify_port_vlan(
            'test_vxlan_config5',
            self.results.get_test_results(
                'add_port_driver_result4'))

        self._create_port(
            self.test_configs['test_vxlan_config6'],
            override_netid=888)
        self._verify_results(
            self.results.get_test_results(
                'add_port_driver_result2'))

        binding = nexus_db_v2.get_nve_switch_bindings(
            base.NEXUS_IP_ADDRESS_8)
        self.assertEqual(2, len(binding))

        # Clean all the driver mock_calls so we can evaluate
        # results of delete operations.
        self.mock_driver.reset_mock()

        self._basic_delete_verify_port_vlan(
            'test_vxlan_config6',
            self.results.get_test_results(
                'delete_port_driver_result2'),
            nbr_of_bindings=1)

        self._basic_delete_verify_port_vlan(
            'test_vxlan_config5',
            self.results.get_test_results(
                'delete_port_driver_result4'))

        try:
            binding = nexus_db_v2.get_nve_switch_bindings(
                base.NEXUS_IP_ADDRESS_8)
        except exceptions.NexusPortBindingNotFound:
            binding = []
        self.assertEqual(0, len(binding))
