# Copyright 2018 Ericsson
#
#    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 functools

from neutron_lib import constants

from neutron.common import utils
from neutron.tests.common import net_helpers
from neutron.tests.fullstack import base
from neutron.tests.fullstack.resources import config as f_const
from neutron.tests.fullstack.resources import environment
from neutron.tests.unit import testlib_api

load_tests = testlib_api.module_load_tests

BR_MAPPINGS = 'bridge_mappings'
DEV_MAPPINGS = 'device_mappings'


def _get_physnet_names_from_mapping(mapping):
    physnets = []
    for pair in mapping.split(','):
        physnets.append(pair.split(':')[0])
    return physnets


def _add_new_device_to_agent_config(l2_agent_config, mapping_key_name,
                                    new_dev):
    old_bw = l2_agent_config[constants.RP_BANDWIDTHS]
    old_mappings = l2_agent_config[mapping_key_name]
    if new_dev in old_bw or new_dev in old_mappings:
        return

    new_mappings = 'physnetnew:%s' % new_dev
    new_bw = '{}:{}:{}'.format(new_dev,
                               f_const.MINIMUM_BANDWIDTH_EGRESS_KBPS,
                               f_const.MINIMUM_BANDWIDTH_INGRESS_KBPS)
    l2_agent_config[mapping_key_name] = '{},{}'.format(
        old_mappings, new_mappings)
    l2_agent_config[constants.RP_BANDWIDTHS] = '{},{}'.format(
        old_bw, new_bw)


def _change_agent_conf(l2_agent_config, l2_agent,
                       mapping_key_name, new_dev):
    _add_new_device_to_agent_config(l2_agent_config, mapping_key_name, new_dev)
    l2_agent.agent_cfg_fixture.write_config_to_configfile()


def _add_new_bridge_and_restart_agent(host):
    l2_agent = host.l2_agent
    l2_agent_config = l2_agent.agent_cfg_fixture.config

    if 'ovs' in host.agents:
        new_dev = utils.get_rand_device_name(prefix='br-new')
        _change_agent_conf(
            l2_agent_config['ovs'], l2_agent, BR_MAPPINGS, new_dev)
        physnets = _get_physnet_names_from_mapping(
            l2_agent_config['ovs'][BR_MAPPINGS])
        br_phys_new = host.useFixture(
            net_helpers.OVSBridgeFixture(new_dev)).bridge
        host.connect_to_central_network_via_vlans(br_phys_new)
    elif 'sriov' in host.agents:
        new_dev = utils.get_rand_device_name(prefix='ens7')
        _change_agent_conf(
            l2_agent_config['sriov_nic'], l2_agent,
            'physical_device_mappings', new_dev)
        physnets = _get_physnet_names_from_mapping(
            l2_agent_config['sriov_nic']['physical_device_mappings'])

    l2_agent.restart()
    return physnets


class TestAgentBandwidthReport(base.BaseFullStackTestCase):

    scenarios = [
        (constants.AGENT_TYPE_OVS,
         {'l2_agent_type': constants.AGENT_TYPE_OVS}),
        (constants.AGENT_TYPE_NIC_SWITCH,
         {'l2_agent_type': constants.AGENT_TYPE_NIC_SWITCH})
    ]

    def setUp(self, env=None):
        if not env:
            host_desc = [environment.HostDescription(
                l3_agent=False,
                l2_agent_type=self.l2_agent_type)]
            env_desc = environment.EnvironmentDescription(
                network_type='vlan',
                l2_pop=False,
                report_bandwidths=True,
            )
            env = environment.Environment(env_desc, host_desc)

        super().setUp(env)

    def _check_agent_configurations(self, agent_id, expected_physnets):
        agent = self.client.show_agent(agent_id)['agent']
        agent_configurations = agent['configurations']
        if 'Open vSwitch' in agent['agent_type']:
            mapping_key = BR_MAPPINGS
        elif 'NIC Switch' in agent['agent_type']:
            mapping_key = DEV_MAPPINGS
        else:
            return False

        for physnet in expected_physnets:
            if physnet not in agent_configurations[mapping_key]:
                return False
            bridge_or_devices = agent_configurations[mapping_key][physnet]

            if (constants.RP_BANDWIDTHS not in agent_configurations or
                    constants.RP_INVENTORY_DEFAULTS not in
                    agent_configurations):
                return False

            if mapping_key == BR_MAPPINGS:
                if (bridge_or_devices not in
                        agent_configurations[constants.RP_BANDWIDTHS]):
                    return False
            else:
                for device in bridge_or_devices:
                    if (device not in
                            agent_configurations[constants.RP_BANDWIDTHS]):
                        return False

        for device in agent_configurations[constants.RP_BANDWIDTHS]:
            conf_device = agent_configurations[constants.RP_BANDWIDTHS][device]
            if (f_const.MINIMUM_BANDWIDTH_INGRESS_KBPS !=
                    conf_device['ingress'] and
                    f_const.MINIMUM_BANDWIDTH_EGRESS_KBPS !=
                    conf_device[device]['egress']):
                return False
        return True

    def test_agent_configurations(self):
        agents = self.client.list_agents()

        self.assertEqual(1, len(agents['agents']))
        self.assertTrue(agents['agents'][0]['alive'])

        agent_config = self.environment.hosts[0].l2_agent.agent_config
        if 'ovs' in self.environment.hosts[0].agents:
            physnets = _get_physnet_names_from_mapping(
                agent_config['ovs'][BR_MAPPINGS])
        elif 'sriov' in self.environment.hosts[0].agents:
            physnets = _get_physnet_names_from_mapping(
                agent_config['sriov_nic']['physical_device_mappings'])

        self.assertTrue(
            self._check_agent_configurations(agents['agents'][0]['id'],
                                             physnets))

        # Add new physnet with bandwidth value to agent config and check
        # if after agent restart and report_interval wait it is visible in
        # the configurations field.
        physnets = _add_new_bridge_and_restart_agent(self.environment.hosts[0])

        agents = self.client.list_agents()
        l2_agent = agents['agents'][0]
        neutron_config = self.environment.hosts[0].l2_agent.neutron_config
        report_interval = neutron_config['agent']['report_interval']

        check_agent_alive = functools.partial(self._check_agent_configurations,
                                              l2_agent['id'],
                                              physnets)
        base.wait_until_true(
            predicate=check_agent_alive,
            timeout=float(report_interval) + 10,
            sleep=5)


class TestPlacementBandwidthReport(base.BaseFullStackTestCase):

    scenarios = [
        (constants.AGENT_TYPE_OVS,
         {'l2_agent_type': constants.AGENT_TYPE_OVS,
          'mech_drivers': 'openvswitch',
          'placement_port': '8080'}),
        (constants.AGENT_TYPE_NIC_SWITCH,
         {'l2_agent_type': constants.AGENT_TYPE_NIC_SWITCH,
          'mech_drivers': 'sriovnicswitch',
          'placement_port': '8081'})
    ]

    def setUp(self):
        host_desc = [environment.HostDescription(
            l3_agent=False,
            l2_agent_type=self.l2_agent_type)]
        env_desc = environment.EnvironmentDescription(
            network_type='vlan',
            l2_pop=False,
            mech_drivers=self.mech_drivers,
            report_bandwidths=True,
            has_placement=True,
            placement_port=self.placement_port
        )
        env = environment.Environment(env_desc, host_desc)
        super().setUp(env)

    def _check_agent_not_synced(self):
        return not self._check_agent_synced()

    def _check_agent_synced(self):
        agents = self.client.list_agents(agent_type=self.l2_agent_type)
        for agent in agents['agents']:
            if (agent['id'] == self.original_agent_id and
                    agent['resources_synced']):
                return True
        return False

    def test_configurations_are_synced_towards_placement(self):
        neutron_config = self.environment.hosts[0].l2_agent.neutron_config
        report_interval = int(neutron_config['agent']['report_interval'])

        agents = self.client.list_agents(agent_type=self.l2_agent_type)
        self.assertEqual(1, len(agents['agents']))
        self.original_agent_id = agents['agents'][0]['id']

        check_agent_synced = functools.partial(self._check_agent_synced)
        base.wait_until_true(
            predicate=check_agent_synced,
            timeout=report_interval + 10,
            sleep=1)

        self.environment.placement.process_fixture.stop()

        placement_fixture = self.environment.placement.process_fixture
        base.wait_until_true(
            predicate=functools.partial(
                placement_fixture.process_is_not_running),
            timeout=report_interval, sleep=1
        )

        _add_new_bridge_and_restart_agent(self.environment.hosts[0])

        check_agent_not_synced = functools.partial(
            self._check_agent_not_synced)
        base.wait_until_true(
            predicate=check_agent_not_synced,
            timeout=report_interval + 10,
            sleep=1)

        self.environment.placement.process_fixture.start()
        check_agent_synced = functools.partial(self._check_agent_synced)
        base.wait_until_true(
            predicate=check_agent_synced,
            timeout=report_interval + 10,
            sleep=1)
