# Copyright (c) 2015 Infoblox 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 mock
import netaddr
from neutron_lib import constants
from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_utils import uuidutils
import webob.exc

from neutron.common import constants as n_const
from neutron.common import ipv6_utils
from neutron.db import ipam_backend_mixin
from neutron.db import ipam_pluggable_backend
from neutron.db import models_v2
from neutron.ipam import requests as ipam_req
from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db_base


class UseIpamMixin(object):

    def setUp(self):
        cfg.CONF.set_override("ipam_driver", 'internal')
        super(UseIpamMixin, self).setUp()


class TestIpamHTTPResponse(UseIpamMixin, test_db_base.TestV2HTTPResponse):
    pass


class TestIpamPorts(UseIpamMixin, test_db_base.TestPortsV2):
    pass


class TestIpamNetworks(UseIpamMixin, test_db_base.TestNetworksV2):
    pass


class TestIpamSubnets(UseIpamMixin, test_db_base.TestSubnetsV2):
    pass


class TestIpamSubnetPool(UseIpamMixin, test_db_base.TestSubnetPoolsV2):
    pass


class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
    def setUp(self):
        cfg.CONF.set_override("ipam_driver", 'internal')
        super(TestDbBasePluginIpam, self).setUp()
        self.tenant_id = uuidutils.generate_uuid()
        self.subnet_id = uuidutils.generate_uuid()

    def _prepare_mocks(self, address_factory=None, subnet_factory=None):
        if address_factory is None:
            address_factory = ipam_req.AddressRequestFactory
        if subnet_factory is None:
            subnet_factory = ipam_req.SubnetRequestFactory

        mocks = {
            'driver': mock.Mock(),
            'subnet': mock.Mock(),
            'subnets': mock.Mock(),
            'subnet_request': ipam_req.SpecificSubnetRequest(
                self.tenant_id,
                self.subnet_id,
                '10.0.0.0/24',
                '10.0.0.1',
                [netaddr.IPRange('10.0.0.2', '10.0.0.254')]),
        }
        mocks['driver'].get_subnet.return_value = mocks['subnet']
        mocks['driver'].allocate_subnet.return_value = mocks['subnet']
        mocks['driver'].get_allocator.return_value = mocks['subnets']
        mocks['subnets'].allocate.return_value = (
            mock.sentinel.address, mock.sentinel.subnet_id)
        mocks['driver'].get_subnet_request_factory.return_value = (
            subnet_factory)
        mocks['driver'].get_address_request_factory.return_value = (
            address_factory)
        mocks['subnet'].get_details.return_value = mocks['subnet_request']
        return mocks

    def _prepare_ipam(self):
        mocks = self._prepare_mocks()
        mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
        return mocks

    def _prepare_mocks_with_pool_mock(self, pool_mock, address_factory=None,
                                      subnet_factory=None):
        mocks = self._prepare_mocks(address_factory=address_factory,
                                    subnet_factory=subnet_factory)
        pool_mock.get_instance.return_value = mocks['driver']
        return mocks

    def _get_allocate_mock(self, subnet_id, auto_ip='10.0.0.2',
                           fail_ip='127.0.0.1',
                           exception=None):
        if exception is None:
            exception = n_exc.InvalidInput(error_message='SomeError')

        def allocate_mock(request):
            if type(request) == ipam_req.SpecificAddressRequest:
                if request.address == netaddr.IPAddress(fail_ip):
                    raise exception
                else:
                    return str(request.address), subnet_id
            else:
                return auto_ip, subnet_id

        return allocate_mock

    def _get_deallocate_mock(self, fail_ip='127.0.0.1', exception=None):
            if exception is None:
                exception = n_exc.InvalidInput(error_message='SomeError')

            def deallocate_mock(ip):
                if str(ip) == fail_ip:
                    raise exception

            return deallocate_mock

    def _validate_allocate_calls(self, expected_calls, mocks):
        self.assertTrue(mocks['subnets'].allocate.called)

        actual_calls = mocks['subnets'].allocate.call_args_list
        self.assertEqual(len(expected_calls), len(actual_calls))

        i = 0
        for call in expected_calls:
            if call['ip_address']:
                self.assertIsInstance(actual_calls[i][0][0],
                                      ipam_req.SpecificAddressRequest)
                self.assertEqual(netaddr.IPAddress(call['ip_address']),
                                 actual_calls[i][0][0].address)
            else:
                self.assertIsInstance(actual_calls[i][0][0],
                                      ipam_req.AnyAddressRequest)
            i += 1

    def _convert_to_ips(self, data):
        ips = [{'ip_address': ip,
                'subnet_id': data[ip][1],
                'subnet_cidr': data[ip][0]} for ip in data]
        return sorted(ips, key=lambda t: t['subnet_cidr'])

    def _gen_subnet_id(self):
        return uuidutils.generate_uuid()

    def test_deallocate_single_ip(self):
        mocks = self._prepare_ipam()
        ip = '192.168.12.45'
        data = {ip: ['192.168.12.0/24', self._gen_subnet_id()]}
        ips = self._convert_to_ips(data)

        mocks['ipam']._ipam_deallocate_ips(mock.ANY, mocks['driver'],
                                           mock.ANY, ips)

        mocks['driver'].get_subnet.assert_called_once_with(data[ip][1])
        mocks['subnet'].deallocate.assert_called_once_with(ip)

    def test_deallocate_multiple_ips(self):
        mocks = self._prepare_ipam()
        data = {'192.168.43.15': ['192.168.43.0/24', self._gen_subnet_id()],
                '172.23.158.84': ['172.23.128.0/17', self._gen_subnet_id()],
                '8.8.8.8': ['8.0.0.0/8', self._gen_subnet_id()]}
        ips = self._convert_to_ips(data)

        mocks['ipam']._ipam_deallocate_ips(mock.ANY, mocks['driver'],
                                           mock.ANY, ips)

        get_calls = [mock.call(data[ip][1]) for ip in data]
        mocks['driver'].get_subnet.assert_has_calls(get_calls, any_order=True)

        ip_calls = [mock.call(ip) for ip in data]
        mocks['subnet'].deallocate.assert_has_calls(ip_calls, any_order=True)

    def _single_ip_allocate_helper(self, mocks, ip, network, subnet):
        ips = [{'subnet_cidr': network,
                'subnet_id': subnet}]
        if ip:
            ips[0]['ip_address'] = ip

        allocated_ips = mocks['ipam']._ipam_allocate_ips(
            mock.ANY, mocks['driver'], mock.ANY, ips)

        mocks['driver'].get_allocator.assert_called_once_with([subnet])

        self.assertTrue(mocks['subnets'].allocate.called)
        request = mocks['subnets'].allocate.call_args[0][0]

        return {'ips': allocated_ips,
                'request': request}

    def test_allocate_single_fixed_ip(self):
        mocks = self._prepare_ipam()
        ip = '192.168.15.123'
        subnet_id = self._gen_subnet_id()
        mocks['subnets'].allocate.return_value = ip, subnet_id

        results = self._single_ip_allocate_helper(mocks,
                                                  ip,
                                                  '192.168.15.0/24',
                                                  subnet_id)

        self.assertIsInstance(results['request'],
                              ipam_req.SpecificAddressRequest)
        self.assertEqual(netaddr.IPAddress(ip), results['request'].address)

        self.assertEqual(ip, results['ips'][0]['ip_address'],
                         'Should allocate the same ip as passed')

    def test_allocate_single_any_ip(self):
        mocks = self._prepare_ipam()
        network = '192.168.15.0/24'
        ip = '192.168.15.83'
        subnet_id = self._gen_subnet_id()
        mocks['subnets'].allocate.return_value = ip, subnet_id

        results = self._single_ip_allocate_helper(mocks, '', network,
                                                  subnet_id)

        self.assertIsInstance(results['request'], ipam_req.AnyAddressRequest)
        self.assertEqual(ip, results['ips'][0]['ip_address'])

    def test_allocate_eui64_ip(self):
        mocks = self._prepare_ipam()
        ip = {'subnet_id': self._gen_subnet_id(),
              'subnet_cidr': '2001:470:abcd::/64',
              'mac': '6c:62:6d:de:cf:49',
              'eui64_address': True}
        eui64_ip = ipv6_utils.get_ipv6_addr_by_EUI64(ip['subnet_cidr'],
                                                     ip['mac'])
        mocks['ipam']._ipam_allocate_ips(mock.ANY, mocks['driver'],
                                         mock.ANY, [ip])

        request = mocks['subnets'].allocate.call_args[0][0]
        self.assertIsInstance(request, ipam_req.AutomaticAddressRequest)
        self.assertEqual(eui64_ip, request.address)

    def test_allocate_multiple_ips(self):
        mocks = self._prepare_ipam()
        subnet_id = self._gen_subnet_id()
        data = {'': ['172.23.128.0/17', subnet_id],
                '192.168.43.15': ['192.168.43.0/24', self._gen_subnet_id()],
                '8.8.8.8': ['8.0.0.0/8', self._gen_subnet_id()]}
        ips = self._convert_to_ips(data)
        mocks['subnets'].allocate.side_effect = self._get_allocate_mock(
            subnet_id, auto_ip='172.23.128.94')

        mocks['ipam']._ipam_allocate_ips(
            mock.ANY, mocks['driver'], mock.ANY, ips)
        get_calls = [mock.call([data[ip][1]]) for ip in data]
        mocks['driver'].get_allocator.assert_has_calls(
            get_calls, any_order=True)

        self._validate_allocate_calls(ips, mocks)

    def _test_allocate_multiple_ips_with_exception(self,
                                                   exc_on_deallocate=False):
        mocks = self._prepare_ipam()
        fail_ip = '192.168.43.15'
        auto_ip = '172.23.128.94'
        subnet_id = self._gen_subnet_id()
        data = {'': ['172.23.128.0/17', subnet_id],
                fail_ip: ['192.168.43.0/24', self._gen_subnet_id()],
                '8.8.8.8': ['8.0.0.0/8', self._gen_subnet_id()]}
        ips = self._convert_to_ips(data)

        mocks['subnets'].allocate.side_effect = self._get_allocate_mock(
            subnet_id, auto_ip=auto_ip, fail_ip=fail_ip,
            exception=db_exc.DBDeadlock())

        # Exception should be raised on attempt to allocate second ip.
        # Revert action should be performed for the already allocated ips,
        # In this test case only one ip should be deallocated
        # and original error should be reraised
        self.assertRaises(db_exc.DBDeadlock,
                          mocks['ipam']._ipam_allocate_ips,
                          mock.ANY,
                          mocks['driver'],
                          mock.ANY,
                          ips)

        # get_subnet should be called only for the first two networks
        get_calls = [mock.call([data[ip][1]]) for ip in ['', fail_ip]]
        mocks['driver'].get_allocator.assert_has_calls(
            get_calls, any_order=True)

        # Allocate should be called for the first two ips only
        self._validate_allocate_calls(ips[:-1], mocks)
        # Deallocate should be called for the first ip only
        mocks['subnet'].deallocate.assert_called_once_with(auto_ip)

    def test_allocate_multiple_ips_with_exception(self):
        self._test_allocate_multiple_ips_with_exception()

    def test_allocate_multiple_ips_with_exception_on_rollback(self):
        # Validate that original exception is not replaced with one raised on
        # rollback (during deallocate)
        self._test_allocate_multiple_ips_with_exception(exc_on_deallocate=True)

    def test_deallocate_multiple_ips_with_exception(self):
        mocks = self._prepare_ipam()
        fail_ip = '192.168.43.15'
        data = {fail_ip: ['192.168.43.0/24', self._gen_subnet_id()],
                '0.10.8.8': ['0.10.0.0/8', self._gen_subnet_id()]}
        ips = self._convert_to_ips(data)

        mocks['subnet'].deallocate.side_effect = self._get_deallocate_mock(
            fail_ip=fail_ip, exception=db_exc.DBDeadlock())
        mocks['subnet'].allocate.side_effect = ValueError('Some-error')
        # Validate that exception from deallocate (DBDeadlock) is not replaced
        # by exception from allocate (ValueError) in rollback block,
        # so original exception is not changed
        self.assertRaises(db_exc.DBDeadlock,
                          mocks['ipam']._ipam_deallocate_ips,
                          mock.ANY,
                          mocks['driver'],
                          mock.ANY,
                          ips)
        mocks['subnets'].allocate.assert_called_once_with(mock.ANY)

    @mock.patch('neutron.ipam.driver.Pool')
    def test_create_subnet_over_ipam(self, pool_mock):
        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
        cidr = '192.168.0.0/24'
        allocation_pools = [{'start': '192.168.0.2', 'end': '192.168.0.254'}]
        with self.subnet(allocation_pools=allocation_pools,
                         cidr=cidr):
            pool_mock.get_instance.assert_called_once_with(None, mock.ANY)
            self.assertTrue(mocks['driver'].allocate_subnet.called)
            request = mocks['driver'].allocate_subnet.call_args[0][0]
            self.assertIsInstance(request, ipam_req.SpecificSubnetRequest)
            self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)

    @mock.patch('neutron.ipam.driver.Pool')
    def test_create_ipv6_pd_subnet_over_ipam(self, pool_mock):
        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
        cfg.CONF.set_override('ipv6_pd_enabled', True)
        cidr = n_const.PROVISIONAL_IPV6_PD_PREFIX
        allocation_pools = [netaddr.IPRange('::2', '::ffff:ffff:ffff:ffff')]
        with self.subnet(cidr=None, ip_version=6,
                         subnetpool_id=constants.IPV6_PD_POOL_ID,
                         ipv6_ra_mode=n_const.IPV6_SLAAC,
                         ipv6_address_mode=n_const.IPV6_SLAAC):
            pool_mock.get_instance.assert_called_once_with(None, mock.ANY)
            self.assertTrue(mocks['driver'].allocate_subnet.called)
            request = mocks['driver'].allocate_subnet.call_args[0][0]
            self.assertIsInstance(request, ipam_req.SpecificSubnetRequest)
            self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
            self.assertEqual(allocation_pools, request.allocation_pools)

    @mock.patch('neutron.ipam.driver.Pool')
    def test_create_subnet_over_ipam_with_rollback(self, pool_mock):
        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
        mocks['driver'].allocate_subnet.side_effect = ValueError
        cidr = '10.0.2.0/24'
        with self.network() as network:
            self._create_subnet(self.fmt, network['network']['id'],
                                cidr, expected_res_status=500)

            pool_mock.get_instance.assert_called_once_with(None, mock.ANY)
            self.assertTrue(mocks['driver'].allocate_subnet.called)
            request = mocks['driver'].allocate_subnet.call_args[0][0]
            self.assertIsInstance(request, ipam_req.SpecificSubnetRequest)
            self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
            # Verify no subnet was created for network
            req = self.new_show_request('networks', network['network']['id'])
            res = req.get_response(self.api)
            net = self.deserialize(self.fmt, res)
            self.assertEqual(0, len(net['network']['subnets']))

    @mock.patch('neutron.ipam.driver.Pool')
    def test_ipam_subnet_deallocated_if_create_fails(self, pool_mock):
        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
        cidr = '10.0.2.0/24'
        with mock.patch.object(
                ipam_backend_mixin.IpamBackendMixin, '_save_subnet',
                side_effect=ValueError), self.network() as network:
            self._create_subnet(self.fmt, network['network']['id'],
                                cidr, expected_res_status=500)
            pool_mock.get_instance.assert_any_call(None, mock.ANY)
            self.assertEqual(2, pool_mock.get_instance.call_count)
            self.assertTrue(mocks['driver'].allocate_subnet.called)
            request = mocks['driver'].allocate_subnet.call_args[0][0]
            self.assertIsInstance(request, ipam_req.SpecificSubnetRequest)
            self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
            # Verify remove ipam subnet was called
            mocks['driver'].remove_subnet.assert_called_once_with(
                self.subnet_id)

    @mock.patch('neutron.ipam.driver.Pool')
    def test_update_subnet_over_ipam(self, pool_mock):
        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
        cidr = '10.0.0.0/24'
        allocation_pools = [{'start': '10.0.0.2', 'end': '10.0.0.254'}]
        with self.subnet(allocation_pools=allocation_pools,
                         cidr=cidr) as subnet:
            data = {'subnet': {'allocation_pools': [
                    {'start': '10.0.0.10', 'end': '10.0.0.20'},