from mock import MagicMock, call, patch
import collections
import charmhelpers.contrib.openstack.templating as templating

templating.OSConfigRenderer = MagicMock()

import quantum_utils


try:
    import neutronclient
except ImportError:
    neutronclient = None

from test_utils import (
    CharmTestCase
)

import charmhelpers.core.hookenv as hookenv


TO_PATCH = [
    'config',
    'get_os_codename_install_source',
    'get_os_codename_package',
    'apt_update',
    'apt_upgrade',
    'apt_install',
    'configure_installation_source',
    'log',
    'add_bridge',
    'add_bridge_port',
    'networking_name',
    'headers_package',
    'full_restart',
    'service_running',
    'NetworkServiceContext',
    'ExternalPortContext',
    'DataPortContext',
    'unit_private_ip',
    'relations_of_type',
    'service_stop',
    'determine_dkms_package',
    'service_restart',
    'remap_plugin',
    'is_relation_made',
    'lsb_release'
]


class TestQuantumUtils(CharmTestCase):

    def assertDictEqual(self, d1, d2, msg=None):  # assertEqual uses for dicts
        for k, v1 in d1.iteritems():
            self.assertIn(k, d2, msg)
            v2 = d2[k]
            if(isinstance(v1, collections.Iterable) and
               not isinstance(v1, basestring)):
                self.assertItemsEqual(v1, v2, msg)
            else:
                self.assertEqual(v1, v2, msg)

    def setUp(self):
        super(TestQuantumUtils, self).setUp(quantum_utils, TO_PATCH)
        self.networking_name.return_value = 'neutron'
        self.headers_package.return_value = 'linux-headers-2.6.18'
        self._set_distrib_codename('trusty')

        def noop(value):
            return value
        self.remap_plugin.side_effect = noop

    def tearDown(self):
        # Reset cached cache
        hookenv.cache = {}

    def _set_distrib_codename(self, newcodename):
        self.lsb_release.return_value = {'DISTRIB_CODENAME': newcodename}

    def test_valid_plugin(self):
        self.config.return_value = 'ovs'
        self.assertTrue(quantum_utils.valid_plugin())
        self.config.return_value = 'nvp'
        self.assertTrue(quantum_utils.valid_plugin())
        self.config.return_value = 'nsx'
        self.assertTrue(quantum_utils.valid_plugin())

    def test_invalid_plugin(self):
        self.config.return_value = 'invalid'
        self.assertFalse(quantum_utils.valid_plugin())

    def test_get_early_packages_ovs(self):
        self.config.return_value = 'ovs'
        self.determine_dkms_package.return_value = [
            'openvswitch-datapath-dkms']
        self.assertEquals(
            quantum_utils.get_early_packages(),
            ['openvswitch-datapath-dkms', 'linux-headers-2.6.18'])

    def test_get_early_packages_nvp(self):
        self.config.return_value = 'nvp'
        self.assertEquals(
            quantum_utils.get_early_packages(),
            [])

    def test_get_early_packages_empty(self):
        self.config.return_value = 'noop'
        self.assertEquals(quantum_utils.get_early_packages(),
                          [])

    def test_get_packages_ovs(self):
        self.config.return_value = 'ovs'
        self.get_os_codename_install_source.return_value = 'havana'
        self.assertNotEqual(quantum_utils.get_packages(), [])

    def test_get_packages_ovs_icehouse(self):
        self.config.return_value = 'ovs'
        self.get_os_codename_install_source.return_value = 'icehouse'
        self.assertTrue('neutron-vpn-agent' in quantum_utils.get_packages())
        self.assertFalse('neutron-l3-agent' in quantum_utils.get_packages())

    def test_get_packages_ovs_juno_utopic(self):
        self.config.return_value = 'ovs'
        self.get_os_codename_install_source.return_value = 'juno'
        self._set_distrib_codename('utopic')
        self.assertFalse('neutron-vpn-agent' in quantum_utils.get_packages())
        self.assertTrue('neutron-l3-agent' in quantum_utils.get_packages())

    def test_get_packages_ovs_juno_trusty(self):
        self.config.return_value = 'ovs'
        self.get_os_codename_install_source.return_value = 'juno'
        self.assertTrue('neutron-vpn-agent' in quantum_utils.get_packages())
        self.assertFalse('neutron-l3-agent' in quantum_utils.get_packages())

    def test_configure_ovs_starts_service_if_required(self):
        self.config.return_value = 'ovs'
        self.service_running.return_value = False
        quantum_utils.configure_ovs()
        self.assertTrue(self.full_restart.called)

    def test_configure_ovs_doesnt_restart_service(self):
        self.service_running.return_value = True
        quantum_utils.configure_ovs()
        self.assertFalse(self.full_restart.called)

    def test_configure_ovs_ovs_ext_port(self):
        self.config.side_effect = self.test_config.get
        self.test_config.set('plugin', 'ovs')
        self.test_config.set('ext-port', 'eth0')
        self.ExternalPortContext.return_value = \
            DummyExternalPortContext(return_value={'ext_port': 'eth0'})
        self.DataPortContext.return_value = \
            DummyExternalPortContext(return_value=None)
        quantum_utils.configure_ovs()
        self.add_bridge.assert_has_calls([
            call('br-int'),
            call('br-ex'),
            call('br-data')
        ])
        self.add_bridge_port.assert_called_with('br-ex', 'eth0')

    def test_configure_ovs_ovs_data_port(self):
        self.config.side_effect = self.test_config.get
        self.test_config.set('plugin', 'ovs')
        self.test_config.set('data-port', 'eth0')
        self.ExternalPortContext.return_value = \
            DummyExternalPortContext(return_value=None)
        self.DataPortContext.return_value = \
            DummyExternalPortContext(return_value={'data_port': 'eth0'})
        quantum_utils.configure_ovs()
        self.add_bridge.assert_has_calls([
            call('br-int'),
            call('br-ex'),
            call('br-data')
        ])
        self.add_bridge_port.assert_called_with('br-data', 'eth0',
                                                promisc=True)

    def test_do_openstack_upgrade(self):
        self.config.side_effect = self.test_config.get
        self.is_relation_made.return_value = False
        self.test_config.set('openstack-origin', 'cloud:precise-havana')
        self.test_config.set('plugin', 'ovs')
        self.get_os_codename_install_source.return_value = 'havana'
        quantum_utils.do_openstack_upgrade()
        self.assertTrue(self.log.called)
        self.apt_update.assert_called_with(fatal=True)
        dpkg_opts = [
            '--option', 'Dpkg::Options::=--force-confnew',
            '--option', 'Dpkg::Options::=--force-confdef',
        ]
        self.apt_upgrade.assert_called_with(
            options=dpkg_opts, fatal=True, dist=True
        )
        self.configure_installation_source.assert_called_with(
            'cloud:precise-havana'
        )

    def test_register_configs_ovs(self):
        self.config.return_value = 'ovs'
        self.is_relation_made.return_value = False
        configs = quantum_utils.register_configs()
        confs = [quantum_utils.NEUTRON_DHCP_AGENT_CONF,
                 quantum_utils.NEUTRON_METADATA_AGENT_CONF,
                 quantum_utils.NOVA_CONF,
                 quantum_utils.NEUTRON_CONF,
                 quantum_utils.NEUTRON_L3_AGENT_CONF,
                 quantum_utils.NEUTRON_OVS_PLUGIN_CONF,
                 quantum_utils.EXT_PORT_CONF]
        for conf in confs:
            configs.register.assert_any_call(
                conf,
                quantum_utils.CONFIG_FILES['neutron'][quantum_utils.OVS][conf]
                                          ['hook_contexts']
            )

    def test_register_configs_amqp_nova(self):
        self.config.return_value = 'ovs'
        self.is_relation_made.return_value = True
        configs = quantum_utils.register_configs()
        confs = [quantum_utils.NEUTRON_DHCP_AGENT_CONF,
                 quantum_utils.NEUTRON_METADATA_AGENT_CONF,
                 quantum_utils.NOVA_CONF,
                 quantum_utils.NEUTRON_CONF,
                 quantum_utils.NEUTRON_L3_AGENT_CONF,
                 quantum_utils.NEUTRON_OVS_PLUGIN_CONF,
                 quantum_utils.EXT_PORT_CONF]
        for conf in confs:
            configs.register.assert_any_call(
                conf,
                quantum_utils.CONFIG_FILES['neutron'][quantum_utils.OVS][conf]
                                          ['hook_contexts']
            )

    def test_restart_map_ovs(self):
        self.config.return_value = 'ovs'
        ex_map = {
            quantum_utils.NEUTRON_CONF: ['neutron-l3-agent',
                                         'neutron-dhcp-agent',
                                         'neutron-metadata-agent',
                                         'neutron-plugin-openvswitch-agent',
                                         'neutron-plugin-metering-agent',
                                         'neutron-metering-agent',
                                         'neutron-lbaas-agent',
                                         'neutron-plugin-vpn-agent',
                                         'neutron-vpn-agent'],
            quantum_utils.NEUTRON_DNSMASQ_CONF: ['neutron-dhcp-agent'],
            quantum_utils.NEUTRON_LBAAS_AGENT_CONF:
            ['neutron-lbaas-agent'],
            quantum_utils.NEUTRON_OVS_PLUGIN_CONF:
            ['neutron-plugin-openvswitch-agent'],
            quantum_utils.NEUTRON_METADATA_AGENT_CONF:
            ['neutron-metadata-agent'],
            quantum_utils.NEUTRON_VPNAAS_AGENT_CONF: [
                'neutron-plugin-vpn-agent',
                'neutron-vpn-agent'],
            quantum_utils.NEUTRON_L3_AGENT_CONF: ['neutron-l3-agent'],
            quantum_utils.NEUTRON_DHCP_AGENT_CONF: ['neutron-dhcp-agent'],
            quantum_utils.NEUTRON_FWAAS_CONF: ['neutron-l3-agent'],
            quantum_utils.NEUTRON_METERING_AGENT_CONF:
            ['neutron-metering-agent', 'neutron-plugin-metering-agent'],
            quantum_utils.NOVA_CONF: ['nova-api-metadata'],
        }

        self.assertDictEqual(quantum_utils.restart_map(), ex_map)

    def test_register_configs_nvp(self):
        self.config.return_value = 'nvp'
        self.is_relation_made.return_value = False
        configs = quantum_utils.register_configs()
        confs = [quantum_utils.NEUTRON_DHCP_AGENT_CONF,
                 quantum_utils.NEUTRON_METADATA_AGENT_CONF,
                 quantum_utils.NOVA_CONF,
                 quantum_utils.NEUTRON_CONF]
        for conf in confs:
            configs.register.assert_any_call(
                conf,
                quantum_utils.CONFIG_FILES['neutron'][quantum_utils.NVP][conf]
                                          ['hook_contexts']
            )

    def test_register_configs_nsx(self):
        self.config.return_value = 'nsx'
        configs = quantum_utils.register_configs()
        confs = [quantum_utils.NEUTRON_DHCP_AGENT_CONF,
                 quantum_utils.NEUTRON_METADATA_AGENT_CONF,
                 quantum_utils.NOVA_CONF,
                 quantum_utils.NEUTRON_CONF]
        for conf in confs:
            configs.register.assert_any_call(
                conf,
                quantum_utils.CONFIG_FILES['neutron'][quantum_utils.NSX][conf]
                                          ['hook_contexts']
            )

    def test_stop_services_nvp(self):
        self.config.return_value = 'nvp'
        quantum_utils.stop_services()
        calls = [
            call('neutron-dhcp-agent'),
            call('nova-api-metadata'),
            call('neutron-metadata-agent')
        ]
        self.service_stop.assert_has_calls(
            calls,
            any_order=True,
        )

    def test_stop_services_ovs(self):
        self.config.return_value = 'ovs'
        quantum_utils.stop_services()
        calls = [call('neutron-dhcp-agent'),
                 call('neutron-plugin-openvswitch-agent'),
                 call('nova-api-metadata'),
                 call('neutron-l3-agent'),
                 call('neutron-metadata-agent')]
        self.service_stop.assert_has_calls(
            calls,
            any_order=True,
        )

    def test_restart_map_nvp(self):
        self.config.return_value = 'nvp'
        ex_map = {
            quantum_utils.NEUTRON_DHCP_AGENT_CONF: ['neutron-dhcp-agent'],
            quantum_utils.NEUTRON_DNSMASQ_CONF: ['neutron-dhcp-agent'],
            quantum_utils.NOVA_CONF: ['nova-api-metadata'],
            quantum_utils.NEUTRON_CONF: ['neutron-dhcp-agent',
                                         'neutron-metadata-agent'],
            quantum_utils.NEUTRON_METADATA_AGENT_CONF:
            ['neutron-metadata-agent'],
        }
        self.assertEquals(quantum_utils.restart_map(), ex_map)

    def test_register_configs_pre_install(self):
        self.config.return_value = 'ovs'
        self.is_relation_made.return_value = False
        self.networking_name.return_value = 'quantum'
        configs = quantum_utils.register_configs()
        confs = [quantum_utils.QUANTUM_DHCP_AGENT_CONF,
                 quantum_utils.QUANTUM_METADATA_AGENT_CONF,
                 quantum_utils.NOVA_CONF,
                 quantum_utils.QUANTUM_CONF,
                 quantum_utils.QUANTUM_L3_AGENT_CONF,
                 quantum_utils.QUANTUM_OVS_PLUGIN_CONF,
                 quantum_utils.EXT_PORT_CONF]
        print configs.register.mock_calls
        for conf in confs:
            configs.register.assert_any_call(
                conf,
                quantum_utils.CONFIG_FILES['quantum'][quantum_utils.OVS][conf]
                                          ['hook_contexts']
            )

    def test_get_common_package_quantum(self):
        self.get_os_codename_package.return_value = 'folsom'
        self.assertEquals(quantum_utils.get_common_package(), 'quantum-common')

    def test_get_common_package_neutron(self):
        self.get_os_codename_package.return_value = None
        self.assertEquals(quantum_utils.get_common_package(), 'neutron-common')


network_context = {
    'service_username': 'foo',
    'service_password': 'bar',
    'service_tenant': 'baz',
    'region': 'foo-bar',
    'keystone_host': 'keystone',
    'auth_port': 5000,
    'auth_protocol': 'https'
}


class DummyNetworkServiceContext():

    def __init__(self, return_value):
        self.return_value = return_value

    def __call__(self):
        return self.return_value


class DummyExternalPortContext():

    def __init__(self, return_value):
        self.return_value = return_value

    def __call__(self):
        return self.return_value

agents_all_alive = {
    'DHCP Agent': {
        'agents': [
            {'alive': True,
             'host': 'cluster1-machine1.internal',
             'id': '3e3550f2-38cc-11e3-9617-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster1-machine2.internal',
             'id': '53d6eefc-38cc-11e3-b3c8-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine1.internal',
             'id': '92b8b6bc-38ce-11e3-8537-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine3.internal',
             'id': 'ebdcc950-51c8-11e3-a804-1c6f65b044df'},
        ]
    },
    'L3 Agent': {
        'agents': [
            {'alive': True,
             'host': 'cluster1-machine1.internal',
             'id': '7128198e-38ce-11e3-ba78-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster1-machine2.internal',
             'id': '72453824-38ce-11e3-938e-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine1.internal',
             'id': '84a04126-38ce-11e3-9449-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine3.internal',
             'id': '00f4268a-51c9-11e3-9177-1c6f65b044df'},
        ]
    }
}

agents_some_dead_cl1 = {
    'DHCP Agent': {
        'agents': [
            {'alive': False,
             'host': 'cluster1-machine1.internal',
             'id': '3e3550f2-38cc-11e3-9617-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine1.internal',
             'id': '53d6eefc-38cc-11e3-b3c8-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine2.internal',
             'id': '92b8b6bc-38ce-11e3-8537-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine3.internal',
             'id': 'ebdcc950-51c8-11e3-a804-1c6f65b044df'},
        ]
    },
    'L3 Agent': {
        'agents': [
            {'alive': False,
             'host': 'cluster1-machine1.internal',
             'id': '7128198e-38ce-11e3-ba78-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine1.internal',
             'id': '72453824-38ce-11e3-938e-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine2.internal',
             'id': '84a04126-38ce-11e3-9449-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine3.internal',
             'id': '00f4268a-51c9-11e3-9177-1c6f65b044df'},
        ]
    }
}

agents_some_dead_cl2 = {
    'DHCP Agent': {
        'agents': [
            {'alive': True,
             'host': 'cluster1-machine1.internal',
             'id': '3e3550f2-38cc-11e3-9617-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine1.internal',
             'id': '53d6eefc-38cc-11e3-b3c8-3c970e8b1cf7'},
            {'alive': False,
             'host': 'cluster2-machine2.internal',
             'id': '92b8b6bc-38ce-11e3-8537-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine3.internal',
             'id': 'ebdcc950-51c8-11e3-a804-1c6f65b044df'},
        ]
    },
    'L3 Agent': {
        'agents': [
            {'alive': True,
             'host': 'cluster1-machine1.internal',
             'id': '7128198e-38ce-11e3-ba78-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine1.internal',
             'id': '72453824-38ce-11e3-938e-3c970e8b1cf7'},
            {'alive': False,
             'host': 'cluster2-machine2.internal',
             'id': '84a04126-38ce-11e3-9449-3c970e8b1cf7'},
            {'alive': True,
             'host': 'cluster2-machine3.internal',
             'id': '00f4268a-51c9-11e3-9177-1c6f65b044df'},
        ]
    }
}

dhcp_agent_networks = {
    'networks': [
        {'id': 'foo'},
        {'id': 'bar'}
    ]
}

l3_agent_routers = {
    'routers': [
        {'id': 'baz'},
        {'id': 'bong'}
    ]
}

cluster1 = ['cluster1-machine1.internal']
cluster2 = ['cluster2-machine1.internal', 'cluster2-machine2.internal'
            'cluster2-machine3.internal']


class TestQuantumAgentReallocation(CharmTestCase):

    def setUp(self):
        if not neutronclient:
            raise self.skipTest('Skipping, no neutronclient installed')
        super(TestQuantumAgentReallocation, self).setUp(quantum_utils,
                                                        TO_PATCH)

    def tearDown(self):
        # Reset cached cache
        hookenv.cache = {}

    def test_no_network_context(self):
        self.NetworkServiceContext.return_value = \
            DummyNetworkServiceContext(return_value=None)
        quantum_utils.reassign_agent_resources()
        self.assertTrue(self.log.called)

    @patch('neutronclient.v2_0.client.Client')
    def test_no_down_agents(self, _client):
        self.NetworkServiceContext.return_value = \
            DummyNetworkServiceContext(return_value=network_context)
        dummy_client = MagicMock()
        dummy_client.list_agents.side_effect = agents_all_alive.itervalues()
        _client.return_value = dummy_client
        quantum_utils.reassign_agent_resources()
        dummy_client.add_router_to_l3_agent.assert_not_called()
        dummy_client.remove_router_from_l3_agent.assert_not_called()
        dummy_client.add_network_to_dhcp_agent.assert_not_called()
        dummy_client.remove_network_from_dhcp_agent.assert_not_called()

    @patch('neutronclient.v2_0.client.Client')
    def test_agents_down_relocation_required(self, _client):
        self.NetworkServiceContext.return_value = \
            DummyNetworkServiceContext(return_value=network_context)
        dummy_client = MagicMock()
        dummy_client.list_agents.side_effect = \
            agents_some_dead_cl2.itervalues()
        dummy_client.list_networks_on_dhcp_agent.return_value = \
            dhcp_agent_networks
        dummy_client.list_routers_on_l3_agent.return_value = \
            l3_agent_routers
        _client.return_value = dummy_client
        self.unit_private_ip.return_value = 'cluster2-machine1.internal'
        self.relations_of_type.return_value = \
            [{'private-address': 'cluster2-machine3.internal'}]
        quantum_utils.reassign_agent_resources()

        # Ensure routers removed from dead l3 agent
        dummy_client.remove_router_from_l3_agent.assert_has_calls(
            [call(l3_agent='84a04126-38ce-11e3-9449-3c970e8b1cf7',
                  router_id='bong'),
             call(l3_agent='84a04126-38ce-11e3-9449-3c970e8b1cf7',
                  router_id='baz')], any_order=True)
        # and re-assigned across the remaining two live agents
        dummy_client.add_router_to_l3_agent.assert_has_calls(
            [call(l3_agent='00f4268a-51c9-11e3-9177-1c6f65b044df',
                  body={'router_id': 'baz'}),
             call(l3_agent='72453824-38ce-11e3-938e-3c970e8b1cf7',
                  body={'router_id': 'bong'})], any_order=True)
        # Ensure networks removed from dead dhcp agent
        dummy_client.remove_network_from_dhcp_agent.assert_has_calls(
            [call(dhcp_agent='92b8b6bc-38ce-11e3-8537-3c970e8b1cf7',
                  network_id='foo'),
             call(dhcp_agent='92b8b6bc-38ce-11e3-8537-3c970e8b1cf7',
                  network_id='bar')], any_order=True)
        # and re-assigned across the remaining two live agents
        dummy_client.add_network_to_dhcp_agent.assert_has_calls(
            [call(dhcp_agent='53d6eefc-38cc-11e3-b3c8-3c970e8b1cf7',
                  body={'network_id': 'foo'}),
             call(dhcp_agent='ebdcc950-51c8-11e3-a804-1c6f65b044df',
                  body={'network_id': 'bar'})], any_order=True)

    @patch('neutronclient.v2_0.client.Client')
    def test_agents_down_relocation_impossible(self, _client):
        self.NetworkServiceContext.return_value = \
            DummyNetworkServiceContext(return_value=network_context)
        dummy_client = MagicMock()
        dummy_client.list_agents.side_effect = \
            agents_some_dead_cl1.itervalues()
        dummy_client.list_networks_on_dhcp_agent.return_value = \
            dhcp_agent_networks
        dummy_client.list_routers_on_l3_agent.return_value = \
            l3_agent_routers
        _client.return_value = dummy_client
        self.unit_private_ip.return_value = 'cluster1-machine1.internal'
        self.relations_of_type.return_value = []
        quantum_utils.reassign_agent_resources()
        self.assertTrue(self.log.called)
        assert not dummy_client.remove_router_from_l3_agent.called
        assert not dummy_client.remove_network_from_dhcp_agent.called