# Copyright 2016 Platform9 Systems Inc.(http://www.platform9.com)
#
#    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 neutron.common import constants as n_const
from neutron.db import common_db_mixin
from neutron.db import extraroute_db
from neutron.db import l3_db
from neutron.db import l3_dvrscheduler_db
from neutron.db import l3_gwmode_db
from neutron.db import l3_hamode_db
from neutron.db import l3_hascheduler_db
from neutron.plugins.common import constants
from neutron.quota import resource_registry
from neutron.services import service_base
from oslo_log import log as logging
from neutron.common import exceptions
from neutron.db import securitygroups_db

from neutron.common.aws_utils import AwsUtils

LOG = logging.getLogger(__name__)

class AwsRouterPlugin(service_base.ServicePluginBase,
                     common_db_mixin.CommonDbMixin,
                     extraroute_db.ExtraRoute_db_mixin,
                     l3_hamode_db.L3_HA_NAT_db_mixin,
                     l3_gwmode_db.L3_NAT_db_mixin,
                     l3_dvrscheduler_db.L3_DVRsch_db_mixin,
                     l3_hascheduler_db.L3_HA_scheduler_db_mixin):
    """Implementation of the Neutron L3 Router Service Plugin.

    This class implements a L3 service plugin that provides
    router and floatingip resources and manages associated
    request/response.
    All DB related work is implemented in classes
    l3_db.L3_NAT_db_mixin, l3_hamode_db.L3_HA_NAT_db_mixin,
    l3_dvr_db.L3_NAT_with_dvr_db_mixin, and extraroute_db.ExtraRoute_db_mixin.
    """
    supported_extension_aliases = ["dvr", "router", "ext-gw-mode",
                                   "extraroute", "l3_agent_scheduler",
                                   "l3-ha", "security-group"]

    @resource_registry.tracked_resources(router=l3_db.Router,
                                         floatingip=l3_db.FloatingIP,
                                         security_group=securitygroups_db.SecurityGroup)
    def __init__(self):
        self.aws_utils = AwsUtils()
        super(AwsRouterPlugin, self).__init__()
        l3_db.subscribe()

    def get_plugin_type(self):
        return constants.L3_ROUTER_NAT

    def get_plugin_description(self):
        """returns string description of the plugin."""
        return ("AWS L3 Router Service Plugin for basic L3 forwarding"
                " between (L2) Neutron networks and access to external"
                " networks via a NAT gateway.")

    ########## FLOATING IP FEATURES ###############

    def create_floatingip(self, context, floatingip):
        try:
            response = self.aws_utils.allocate_elastic_ip()
            public_ip_allocated = response['PublicIp']
            LOG.info("Created elastic IP %s" % public_ip_allocated)
            if 'floatingip' in floatingip:
                floatingip['floatingip']['floating_ip_address'] = public_ip_allocated

            if 'port_id' in floatingip['floatingip'] and floatingip['floatingip']['port_id'] is not None:
                # Associate to a Port
                port_id = floatingip['floatingip']['port_id']
                self._associate_floatingip_to_port(context, public_ip_allocated, port_id)
        except Exception as e:
            LOG.error("Error in Creation/Allocating EIP")
            if public_ip_allocated:
                LOG.error("Deleting Elastic IP: %s" % public_ip_allocated)
                self.aws_utils.delete_elastic_ip(public_ip_allocated)
            raise e

        try:
            res = super(AwsRouterPlugin, self).create_floatingip(
                context, floatingip,
                initial_status=n_const.FLOATINGIP_STATUS_DOWN)
        except Exception as e:
            LOG.error("Error when adding floating ip in openstack. Deleting Elastic IP: %s" % public_ip_allocated)
            self.aws_utils.delete_elastic_ip(public_ip_allocated)
            raise e
        return res

    def _associate_floatingip_to_port(self, context, floating_ip_address, port_id):
        port = self._core_plugin.get_port(context, port_id)
        ec2_id = None
        fixed_ip_address = None
        # TODO: Assuming that there is only one fixed IP
        if len(port['fixed_ips']) > 0:
            fixed_ip = port['fixed_ips'][0]
            if 'ip_address' in fixed_ip:
                fixed_ip_address = fixed_ip['ip_address']
                search_opts = {'ip': fixed_ip_address, 'tenant_id': context.tenant_id}
                server_list = self.aws_utils.get_nova_client().servers.list(search_opts=search_opts)
                if len(server_list) > 0:
                    server = server_list[0]
                    if 'ec2_id' in server.metadata:
                        ec2_id = server.metadata['ec2_id']
        if floating_ip_address is not None and ec2_id is not None:
            self.aws_utils.associate_elastic_ip_to_ec2_instance(floating_ip_address, ec2_id)
            LOG.info("EC2 ID found for IP %s : %s" % (fixed_ip_address, ec2_id))
        else:
            LOG.warning("EC2 ID not found to associate the floating IP")
            raise exceptions.AwsException(error_code="No Server Found",
                message="No server found with the Required IP")

    def update_floatingip(self, context, id, floatingip):
        floating_ip_dict = super(AwsRouterPlugin, self).get_floatingip(context, id)
        if 'floatingip' in floatingip and 'port_id' in floatingip['floatingip']:
            port_id = floatingip['floatingip']['port_id']
            if port_id is not None:
                # Associate Floating IP
                LOG.info("Associating elastic IP %s with port %s" %
                    (floating_ip_dict['floating_ip_address'], port_id))
                self._associate_floatingip_to_port(context,
                    floating_ip_dict['floating_ip_address'], port_id)
            else:
                try:
                    # Port Disassociate
                    self.aws_utils.disassociate_elastic_ip_from_ec2_instance(floating_ip_dict['floating_ip_address'])
                except exceptions.AwsException as e:
                    if 'Association ID not found' in e.msg:
                        # Since its already disassociated on EC2, we continue and remove the association here.
                        LOG.warn("Association for Elastic IP not found. Probable out of band change on EC2.")
                    elif 'InvalidAddress.NotFound' in e.msg:
                        LOG.warn("Elastic IP cannot be found in EC2. Probably removed out of band on EC2.")
                    else:
                        raise e
        return super(AwsRouterPlugin, self).update_floatingip(context, id, floatingip)

    def delete_floatingip(self, context, id):
        floating_ip = super(AwsRouterPlugin, self).get_floatingip(context, id)
        floating_ip_address = floating_ip['floating_ip_address']
        LOG.info("Deleting elastic IP %s" % floating_ip_address)
        try:
            self.aws_utils.delete_elastic_ip(floating_ip_address)
        except exceptions.AwsException as e:
            if 'InvalidAddress.NotFound' in e.msg:
                LOG.warn("Elastic IP not found on AWS. Cleaning up neutron db")
            else:
                raise e
        return super(AwsRouterPlugin, self).delete_floatingip(context, id)

    ##### ROUTERS #####

    def create_router(self, context, router):
        try:
            router_name = router['router']['name']
            internet_gw_res = self.aws_utils.create_internet_gateway_resource()
            ret_obj = super(AwsRouterPlugin, self).create_router(context, router)
            internet_gw_res.create_tags(Tags=[
                {'Key': 'Name', 'Value': router_name},
                {'Key': 'openstack_router_id', 'Value': ret_obj['id']}
            ])
            LOG.info("Created AWS router %s with openstack id %s" %
                (router_name, ret_obj['id']))
            return ret_obj
        except Exception as e:
            LOG.error("Error while creating router %s" % e)
            raise e

    def delete_router(self, context, id):
        try:
            LOG.info("Deleting router %s" % id)
            self.aws_utils.detach_internet_gateway_by_router_id(id)
            self.aws_utils.delete_internet_gateway_by_router_id(id)
        except Exception as e:
            LOG.error("Error in Deleting Router: %s " % e)
            raise e
        return super(AwsRouterPlugin, self).delete_router(context, id)

    def update_router(self, context, id, router):
        ## get internet gateway resource by openstack router id and update the tags
        try:
            if 'router' in router and 'name' in router['router']:
                router_name = router['router']['name']
                tags_list = [
                    {'Key': 'Name', 'Value': router_name},
                    {'Key': 'openstack_router_id', 'Value': id}
                ]
                LOG.info("Updated router %s" % id)
                self.aws_utils.create_tags_internet_gw_from_router_id(id, tags_list)
        except Exception as e:
            LOG.error("Error in Updating Router: %s " % e)
            raise e
        return super(AwsRouterPlugin, self).update_router(context, id, router)

###### ROUTER INTERFACE ######

    def add_router_interface(self, context, router_id, interface_info):
        subnet_id = interface_info['subnet_id']
        subnet_obj = self._core_plugin.get_subnet(context, subnet_id)
        LOG.info("Adding subnet %s to router %s" % (subnet_id, router_id))
        neutron_network_id = subnet_obj['network_id']
        try:
            # Get Internet Gateway ID
            ig_id = self.aws_utils.get_internet_gw_from_router_id(router_id)
            # Get VPC ID
            vpc_id = self.aws_utils.get_vpc_from_neutron_network_id(neutron_network_id)
            self.aws_utils.attach_internet_gateway(ig_id, vpc_id)
            # Search for a Route table tagged with Router-id
            route_tables = self.aws_utils.get_route_table_by_router_id(router_id)
            if len(route_tables) == 0:
                # If not tagged, Fetch all the Route Tables Select one and tag it
                route_tables = self.aws_utils.describe_route_tables_by_vpc_id(vpc_id)
                if len(route_tables) > 0:
                    route_table = route_tables[0]
                    route_table_res = self.aws_utils._get_ec2_resource().RouteTable(route_table['RouteTableId'])
                    route_table_res.create_tags(Tags=[
                        {'Key': 'openstack_router_id', 'Value': router_id}
                    ])
            if len(route_tables) > 0:
                route_table = route_tables[0]
                self.aws_utils.create_default_route_to_ig(route_table['RouteTableId'], ig_id, ignore_errors=True)
        except Exception as e:
            LOG.error("Error in Creating Interface: %s " % e)
            raise e
        return super(AwsRouterPlugin, self).add_router_interface(context, router_id, interface_info)

    def remove_router_interface(self, context, router_id, interface_info):
        LOG.info("Deleting port %s from router %s" % (interface_info['port_id'], router_id))
        self.aws_utils.detach_internet_gateway_by_router_id(router_id)
        route_tables = self.aws_utils.get_route_table_by_router_id(router_id)
        if route_tables:
            route_table_id = route_tables[0]['RouteTableId']
            self.aws_utils.delete_default_route_to_ig(route_table_id)
        return super(AwsRouterPlugin, self).remove_router_interface(context, router_id, interface_info)
