# Copyright (c) 2015-2016 OpenStack Foundation
# 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 os import path

import mock
from oslo_log import log
from oslo_serialization import jsonutils
import requests

from neutron.extensions import portbindings
from neutron.plugins.common import constants
from neutron.plugins.ml2 import driver_api
from neutron.plugins.ml2 import driver_context
from neutron_lib import constants as n_constants

from networking_odl.common import cache
from networking_odl.ml2 import mech_driver
from networking_odl.ml2 import mech_driver_v2
from networking_odl.ml2 import network_topology
from networking_odl.tests import base


LOG = log.getLogger(__name__)


class TestNetworkTopologyManager(base.DietTestCase):

    # pylint: disable=protected-access

    # given valid  and invalid segments
    valid_segment = {
        driver_api.ID: 'API_ID',
        driver_api.NETWORK_TYPE: constants.TYPE_LOCAL,
        driver_api.SEGMENTATION_ID: 'API_SEGMENTATION_ID',
        driver_api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'}

    invalid_segment = {
        driver_api.ID: 'API_ID',
        driver_api.NETWORK_TYPE: constants.TYPE_NONE,
        driver_api.SEGMENTATION_ID: 'API_SEGMENTATION_ID',
        driver_api.PHYSICAL_NETWORK: 'API_PHYSICAL_NETWORK'}

    segments_to_bind = [valid_segment, invalid_segment]

    def setUp(self):
        super(TestNetworkTopologyManager, self).setUp()
        self.patch(network_topology.LOG, 'isEnabledFor', lambda level: True)
        # patch given configuration
        self.cfg = mocked_cfg = self.patch(network_topology.client, 'cfg')
        mocked_cfg.CONF.ml2_odl.url =\
            'http://localhost:8181/controller/nb/v2/neutron'
        mocked_cfg.CONF.ml2_odl.username = 'admin'
        mocked_cfg.CONF.ml2_odl.password = 'admin'
        mocked_cfg.CONF.ml2_odl.timeout = 5

    @mock.patch.object(cache, 'LOG')
    @mock.patch.object(network_topology, 'LOG')
    def test_fetch_elements_by_host_with_no_entry(
            self, network_topology_logger, cache_logger):
        given_client = self.mock_client('ovs_topology.json')
        self.mock_get_addresses_by_name(['127.0.0.1', '192.168.0.1'])
        given_network_topology = network_topology.NetworkTopologyManager(
            client=given_client)

        try:
            next(given_network_topology._fetch_elements_by_host(
                 'some_host_name'))
        except ValueError as error:
            cache_logger.warning.assert_called_once_with(
                'Error fetching values for keys: %r',
                "'some_host_name', '127.0.0.1', '192.168.0.1'",
                exc_info=(ValueError, error, mock.ANY))
            network_topology_logger.exception.assert_called_once_with(
                'No such network topology elements for given host '
                '%(host_name)r and given IPs: %(ip_addresses)s.',
                {'ip_addresses': '127.0.0.1, 192.168.0.1',
                 'host_name': 'some_host_name'})
        else:
            self.fail('Expected ValueError being raised.')

    def test_fetch_element_with_ovs_entry(self):
        given_client = self.mock_client('ovs_topology.json')
        self.mock_get_addresses_by_name(['127.0.0.1', '10.237.214.247'])
        given_network_topology = network_topology.NetworkTopologyManager(
            client=given_client)

        elements = given_network_topology._fetch_elements_by_host(
            'some_host_name.')

        self.assertEqual([
            {'class':
             'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyElement',
             'has_datapath_type_netdev': False,
             'host_addresses': ['10.237.214.247'],
             'support_vhost_user': False,
             'uuid': 'c4ad780f-8f91-4fa4-804e-dd16beb191e2',
             'valid_vif_types': [portbindings.VIF_TYPE_OVS]}],
            [e.to_dict() for e in elements])

    def test_fetch_elements_with_vhost_user_entry(self):
        given_client = self.mock_client('vhostuser_topology.json')
        self.mock_get_addresses_by_name(['127.0.0.1', '192.168.66.1'])
        given_network_topology = network_topology.NetworkTopologyManager(
            client=given_client)

        elements = given_network_topology._fetch_elements_by_host(
            'some_host_name.')

        self.assertEqual([
            {'class':
             'networking_odl.ml2.ovsdb_topology.OvsdbNetworkTopologyElement',
             'has_datapath_type_netdev': True,
             'host_addresses': ['192.168.66.1'],
             'support_vhost_user': True,
             'uuid': 'c805d82d-a5d8-419d-bc89-6e3713ff9f6c',
             'valid_vif_types': [portbindings.VIF_TYPE_VHOST_USER,
                                 portbindings.VIF_TYPE_OVS],
             'port_prefix': 'vhu',
             'vhostuser_socket_dir': '/var/run/openvswitch'}],
            [e.to_dict() for e in elements])

    def mock_get_addresses_by_name(self, ips):
        utils = self.patch(
            network_topology, 'utils',
            mock.Mock(
                get_addresses_by_name=mock.Mock(return_value=tuple(ips))))
        return utils.get_addresses_by_name

    def mock_client(self, topology_name=None):

        mocked_client = mock.NonCallableMock(
            specs=network_topology.NetworkTopologyClient)

        if topology_name:
            cached_file_path = path.join(path.dirname(__file__), topology_name)

            with open(cached_file_path, 'rt') as fd:
                topology = jsonutils.loads(str(fd.read()), encoding='utf-8')

            mocked_client.get().json.return_value = topology

        return mocked_client

    def test_bind_port_from_mech_driver_with_ovs(self):

        given_client = self.mock_client('ovs_topology.json')
        self.mock_get_addresses_by_name(['127.0.0.1', '10.237.214.247'])
        given_network_topology = network_topology.NetworkTopologyManager(
            vif_details={'some': 'detail'},
            client=given_client)
        self.patch(
            network_topology, 'NetworkTopologyManager',
            return_value=given_network_topology)

        given_driver = mech_driver.OpenDaylightMechanismDriver()
        given_driver.odl_drv = mech_driver.OpenDaylightDriver()
        given_port_context = self.given_port_context()

        # when port is bound
        given_driver.bind_port(given_port_context)

        # then context binding is setup with returned vif_type and valid
        # segment api ID
        given_port_context.set_binding.assert_called_once_with(
            self.valid_segment[driver_api.ID], portbindings.VIF_TYPE_OVS,
            {'some': 'detail'}, status=n_constants.PORT_STATUS_ACTIVE)

    def test_bind_port_from_mech_driver_with_vhostuser(self):

        given_client = self.mock_client('vhostuser_topology.json')
        self.mock_get_addresses_by_name(['127.0.0.1', '192.168.66.1'])
        given_network_topology = network_topology.NetworkTopologyManager(
            vif_details={'some': 'detail'},
            client=given_client)
        self.patch(
            network_topology, 'NetworkTopologyManager',
            return_value=given_network_topology)

        given_driver = mech_driver.OpenDaylightMechanismDriver()
        given_driver.odl_drv = mech_driver.OpenDaylightDriver()
        given_port_context = self.given_port_context()

        # when port is bound
        given_driver.bind_port(given_port_context)

        expected_vif_details = {
            'vhostuser_socket': '/var/run/openvswitch/vhuCURRENT_CON',
            'vhostuser_ovs_plug': True,
            'some': 'detail',
            'vhostuser_mode': 'client'}

        # then context binding is setup with returned vif_type and valid
        # segment api ID
        given_port_context.set_binding.assert_called_once_with(
            self.valid_segment[driver_api.ID],
            portbindings.VIF_TYPE_VHOST_USER,
            expected_vif_details, status=n_constants.PORT_STATUS_ACTIVE)

    def test_bind_port_from_mech_driver_v2_with_ovs(self):
        given_client = self.mock_client('ovs_topology.json')
        self.mock_get_addresses_by_name(['127.0.0.1', '10.237.214.247'])
        given_network_topology = network_topology.NetworkTopologyManager(
            vif_details={'some': 'detail'},
            client=given_client)
        self.patch(
            network_topology, 'NetworkTopologyManager',
            return_value=given_network_topology)

        given_driver = mech_driver_v2.OpenDaylightMechanismDriver()
        given_port_context = self.given_port_context()

        given_driver.initialize()
        # when port is bound
        given_driver.bind_port(given_port_context)

        # then context binding is setup with returned vif_type and valid
        # segment api ID
        given_port_context.set_binding.assert_called_once_with(
            self.valid_segment[driver_api.ID], portbindings.VIF_TYPE_OVS,
            {'some': 'detail'}, status=n_constants.PORT_STATUS_ACTIVE)

    def test_bind_port_from_mech_driver_v2_with_vhostuser(self):
        given_client = self.mock_client('vhostuser_topology.json')
        self.mock_get_addresses_by_name(['127.0.0.1', '192.168.66.1'])
        given_network_topology = network_topology.NetworkTopologyManager(
            vif_details={'some': 'detail'},
            client=given_client)
        self.patch(
            network_topology, 'NetworkTopologyManager',
            return_value=given_network_topology)

        given_driver = mech_driver_v2.OpenDaylightMechanismDriver()
        given_driver._network_topology = given_network_topology
        given_port_context = self.given_port_context()

        given_driver.initialize()
        # when port is bound
        given_driver.bind_port(given_port_context)

        expected_vif_details = {
            'vhostuser_socket': '/var/run/openvswitch/vhuCURRENT_CON',
            'vhostuser_ovs_plug': True,
            'some': 'detail',
            'vhostuser_mode': 'client'}

        # then context binding is setup with returned vif_type and valid
        # segment api ID
        given_port_context.set_binding.assert_called_once_with(
            self.valid_segment[driver_api.ID],
            portbindings.VIF_TYPE_VHOST_USER,
            expected_vif_details, status=n_constants.PORT_STATUS_ACTIVE)

    def test_bind_port_with_vif_type_ovs(self):
        given_topology = self._mock_network_topology(
            'ovs_topology.json', vif_details={'much': 'details'})
        given_port_context = self.given_port_context()

        # when port is bound
        given_topology.bind_port(given_port_context)

        # then context binding is setup wit returned vif_type and valid
        # segment api ID
        given_port_context.set_binding.assert_called_once_with(
            self.valid_segment[driver_api.ID], portbindings.VIF_TYPE_OVS,
            {'much': 'details'}, status=n_constants.PORT_STATUS_ACTIVE)

    def test_bind_port_with_vif_type_vhost_user(self):
        given_topology = self._mock_network_topology(
            'vhostuser_topology.json', vif_details={'much': 'details'})
        given_port_context = self.given_port_context()

        # when port is bound
        given_topology.bind_port(given_port_context)

        # then context binding is setup wit returned vif_type and valid
        # segment api ID
        given_port_context.set_binding.assert_called_once_with(
            self.valid_segment[driver_api.ID],
            portbindings.VIF_TYPE_VHOST_USER,
            {'vhostuser_socket': '/var/run/openvswitch/vhuCURRENT_CON',
             'vhostuser_ovs_plug': True, 'vhostuser_mode': 'client',
             'much': 'details'},
            status=n_constants.PORT_STATUS_ACTIVE)

    @mock.patch.object(network_topology, 'LOG')
    def test_bind_port_without_valid_segment(self, logger):
        given_topology = self._mock_network_topology('ovs_topology.json')
        given_port_context = self.given_port_context(
            given_segments=[self.invalid_segment])

        # when port is bound
        given_topology.bind_port(given_port_context)

        self.assertFalse(given_port_context.set_binding.called)
        logger.exception.assert_called_once_with(
            'Network topology element has failed binding port:\n%(element)s',
            {'element': mock.ANY})
        logger.error.assert_called_once_with(
            'Unable to bind port element for given host and valid VIF types:\n'
            '\thostname: %(host_name)s\n'
            '\tvalid VIF types: %(valid_vif_types)s',
            {'host_name': 'some_host', 'valid_vif_types': 'vhostuser, ovs'})

    def _mock_network_topology(self, given_topology, vif_details=None):
        self.mock_get_addresses_by_name(
            ['127.0.0.1', '10.237.214.247', '192.168.66.1'])
        return network_topology.NetworkTopologyManager(
            client=self.mock_client(given_topology),
            vif_details=vif_details)

    def given_port_context(self, given_segments=None):
        # given NetworkContext
        network = mock.MagicMock(spec=driver_api.NetworkContext)

        if given_segments is None:
            given_segments = self.segments_to_bind

        # given port context
        return mock.MagicMock(
            spec=driver_context.PortContext,
            current={'id': 'CURRENT_CONTEXT_ID'},
            host='some_host',
            segments_to_bind=given_segments,
            network=network,
            _new_bound_segment=self.valid_segment)

    NETOWORK_TOPOLOGY_URL =\
        'http://localhost:8181/'\
        'restconf/operational/network-topology:network-topology/'

    def mock_request_network_topology(self, file_name):
        cached_file_path = path.join(
            path.dirname(__file__), file_name + '.json')

        if path.isfile(cached_file_path):
            LOG.debug('Loading topology from file: %r', cached_file_path)
            with open(cached_file_path, 'rt') as fd:
                topology = jsonutils.loads(str(fd.read()), encoding='utf-8')
        else:
            LOG.debug(
                'Getting topology from ODL: %r', self.NETOWORK_TOPOLOGY_URL)
            request = requests.get(
                self.NETOWORK_TOPOLOGY_URL, auth=('admin', 'admin'),
                headers={'Content-Type': 'application/json'})
            request.raise_for_status()

            with open(cached_file_path, 'wt') as fd:
                LOG.debug('Saving topology to file: %r', cached_file_path)
                topology = request.json()
                jsonutils.dump(
                    topology, fd, sort_keys=True, indent=4,
                    separators=(',', ': '))

        mocked_request = self.patch(
            mech_driver.odl_client.requests, 'request',
            return_value=mock.MagicMock(
                spec=requests.Response,
                json=mock.MagicMock(return_value=topology)))

        return mocked_request


class TestNetworkTopologyClient(base.DietTestCase):

    given_host = 'given.host'
    given_port = 1234
    given_url_with_port = 'http://{}:{}/'.format(
        given_host, given_port)
    given_url_without_port = 'http://{}/'.format(given_host)
    given_username = 'GIVEN_USERNAME'
    given_password = 'GIVEN_PASSWORD'
    given_timeout = 20

    def given_client(
            self, url=None, username=None, password=None, timeout=None):
        return network_topology.NetworkTopologyClient(
            url=url or self.given_url_with_port,
            username=username or self.given_username,
            password=password or self.given_password,
            timeout=timeout or self.given_timeout)

    def test_constructor(self):
        # When client is created
        rest_client = network_topology.NetworkTopologyClient(
            url=self.given_url_with_port,
            username=self.given_username,
            password=self.given_password,
            timeout=self.given_timeout)

        self.assertEqual(
            self.given_url_with_port +
            'restconf/operational/network-topology:network-topology',
            rest_client.url)
        self.assertEqual(
            (self.given_username, self.given_password), rest_client.auth)
        self.assertEqual(self.given_timeout, rest_client.timeout)

    def test_request_with_port(self):
        # Given rest client and used 'requests' module
        given_client = self.given_client()
        mocked_requests_module = self.mocked_requests()

        # When a request is performed
        result = given_client.request(
            'GIVEN_METHOD', 'given/path', 'GIVEN_DATA')

        # Then request method is called
        mocked_requests_module.request.assert_called_once_with(
            'GIVEN_METHOD',
            url='http://given.host:1234/restconf/operational/' +
            'network-topology:network-topology/given/path',
            auth=(self.given_username, self.given_password),
            data='GIVEN_DATA', headers={'Content-Type': 'application/json'},
            timeout=self.given_timeout)

        # Then request method result is returned
        self.assertIs(mocked_requests_module.request.return_value, result)

    def test_request_without_port(self):
        # Given rest client and used 'requests' module
        given_client = self.given_client(url=self.given_url_without_port)
        mocked_requests_module = self.mocked_requests()

        # When a request is performed
        result = given_client.request(
            'GIVEN_METHOD', 'given/path', 'GIVEN_DATA')

        # Then request method is called
        mocked_requests_module.request.assert_called_once_with(
            'GIVEN_METHOD',
            url='http://given.host/restconf/operational/' +
            'network-topology:network-topology/given/path',
            auth=(self.given_username, self.given_password),
            data='GIVEN_DATA', headers={'Content-Type': 'application/json'},
            timeout=self.given_timeout)

        # Then request method result is returned
        self.assertIs(mocked_requests_module.request.return_value, result)

    def test_get(self):
        # Given rest client and used 'requests' module
        given_client = self.given_client()
        mocked_requests_module = self.mocked_requests()

        # When a request is performed
        result = given_client.get('given/path', 'GIVEN_DATA')

        # Then request method is called
        mocked_requests_module.request.assert_called_once_with(
            'get',
            url='http://given.host:1234/restconf/operational/' +
            'network-topology:network-topology/given/path',
            auth=(self.given_username, self.given_password),
            data='GIVEN_DATA', headers={'Content-Type': 'application/json'},
            timeout=self.given_timeout)

        # Then request method result is returned
        self.assertIs(mocked_requests_module.request.return_value, result)

    def mocked_requests(self):
        return self.patch(network_topology.client, 'requests')
