# Copyright 2015 OpenStack LLC.
# 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 netaddr
from oslo_log import log
from oslo_utils import uuidutils

from neutron.common import exceptions as n_exc
from neutron.common import ipv6_utils
from neutron.db import api as db_api
from neutron.i18n import _LE
from neutron import ipam
from neutron.ipam import driver as ipam_base
from neutron.ipam.drivers.neutrondb_ipam import db_api as ipam_db_api
from neutron.ipam import exceptions as ipam_exc
from neutron.ipam import subnet_alloc
from neutron.ipam import utils as ipam_utils
from neutron import manager


LOG = log.getLogger(__name__)


class NeutronDbSubnet(ipam_base.Subnet):
    """Manage IP addresses for Neutron DB IPAM driver.

    This class implements the strategy for IP address allocation and
    deallocation for the Neutron DB IPAM driver.
    Allocation for IP addresses is based on the concept of availability
    ranges, which were already used in Neutron's DB base class for handling
    IPAM operations.
    """

    @classmethod
    def create_allocation_pools(cls, subnet_manager, session, pools):
        for pool in pools:
            subnet_manager.create_pool(
                session,
                netaddr.IPAddress(pool.first).format(),
                netaddr.IPAddress(pool.last).format())

    @classmethod
    def create_from_subnet_request(cls, subnet_request, ctx):
        ipam_subnet_id = uuidutils.generate_uuid()
        subnet_manager = ipam_db_api.IpamSubnetManager(
            ipam_subnet_id,
            None)
        # Create subnet resource
        session = ctx.session
        subnet_manager.create(session)
        # If allocation pools are not specified, define them around
        # the subnet's gateway IP
        if not subnet_request.allocation_pools:
            pools = ipam_utils.generate_pools(subnet_request.subnet_cidr,
                                              subnet_request.gateway_ip)
        else:
            pools = subnet_request.allocation_pools
        # Create IPAM allocation pools and availability ranges
        cls.create_allocation_pools(subnet_manager, session, pools)

        return cls(ipam_subnet_id,
                   ctx,
                   cidr=subnet_request.subnet_cidr,
                   allocation_pools=pools,
                   gateway_ip=subnet_request.gateway_ip,
                   tenant_id=subnet_request.tenant_id,
                   subnet_id=subnet_request.subnet_id,
                   subnet_id_not_set=True)

    @classmethod
    def load(cls, neutron_subnet_id, ctx):
        """Load an IPAM subnet from the database given its neutron ID.

        :param neutron_subnet_id: neutron subnet identifier.
        """
        ipam_subnet = ipam_db_api.IpamSubnetManager.load_by_neutron_subnet_id(
            ctx.session, neutron_subnet_id)
        if not ipam_subnet:
            LOG.error(_LE("Unable to retrieve IPAM subnet as the referenced "
                          "Neutron subnet %s does not exist"),
                      neutron_subnet_id)
            raise n_exc.SubnetNotFound(subnet_id=neutron_subnet_id)
        pools = []
        for pool in ipam_subnet.allocation_pools:
            pools.append(netaddr.IPRange(pool['first_ip'], pool['last_ip']))

        neutron_subnet = cls._fetch_subnet(ctx, neutron_subnet_id)

        return cls(ipam_subnet['id'],
                   ctx,
                   cidr=neutron_subnet['cidr'],
                   allocation_pools=pools,
                   gateway_ip=neutron_subnet['gateway_ip'],
                   tenant_id=neutron_subnet['tenant_id'],
                   subnet_id=neutron_subnet_id)

    @classmethod
    def _fetch_subnet(cls, context, id):
        plugin = manager.NeutronManager.get_plugin()
        return plugin._get_subnet(context, id)

    def __init__(self, internal_id, ctx, cidr=None,
                 allocation_pools=None, gateway_ip=None, tenant_id=None,
                 subnet_id=None, subnet_id_not_set=False):
        # NOTE: In theory it could have been possible to grant the IPAM
        # driver direct access to the database. While this is possible,
        # it would have led to duplicate code and/or non-trivial
        # refactorings in neutron.db.db_base_plugin_v2.
        # This is because in the Neutron V2 plugin logic DB management is
        # encapsulated within the plugin.
        self._cidr = cidr
        self._pools = allocation_pools
        self._gateway_ip = gateway_ip
        self._tenant_id = tenant_id
        self._subnet_id = None if subnet_id_not_set else subnet_id
        self.subnet_manager = ipam_db_api.IpamSubnetManager(internal_id,
                                                            self._subnet_id)
        self._context = ctx

    def _verify_ip(self, session, ip_address):
        """Verify whether IP address can be allocated on subnet.

        :param session: database session
        :param ip_address: String representing the IP address to verify
        :raises: InvalidInput, IpAddressAlreadyAllocated
        """
        # Ensure that the IP's are unique
        if not self.subnet_manager.check_unique_allocation(session,
                                                           ip_address):
            raise ipam_exc.IpAddressAlreadyAllocated(
                subnet_id=self.subnet_manager.neutron_id,
                ip=ip_address)

        # Ensure that the IP is valid on the subnet
        if not ipam_utils.check_subnet_ip(self._cidr, ip_address):
            raise ipam_exc.InvalidIpForSubnet(
                subnet_id=self.subnet_manager.neutron_id,
                ip=ip_address)

    def _allocate_specific_ip(self, session, ip_address,
                              allocation_pool_id=None):
        """Remove an IP address from subnet's availability ranges.

        This method is supposed to be called from within a database
        transaction, otherwise atomicity and integrity might not be
        enforced and the operation might result in incosistent availability
        ranges for the subnet.

        :param session: database session
        :param ip_address: ip address to mark as allocated
        :param allocation_pool_id: identifier of the allocation pool from
             which the ip address has been extracted. If not specified this
             routine will scan all allocation pools.
        :returns: list of IP ranges as instances of IPAvailabilityRange
        """
        # Return immediately for EUI-64 addresses. For this
        # class of subnets availability ranges do not apply
        if ipv6_utils.is_eui64_address(ip_address):
            return

        LOG.debug("Removing %(ip_address)s from availability ranges for "
                  "subnet id:%(subnet_id)s",