# Copyright 2014 VMware, Inc
#
#    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 oslo_config import cfg
from oslo_log import log as logging

from neutron_lib import constants as n_consts
from neutron_lib.db import api as db_api

from vmware_nsx._i18n import _
from vmware_nsx.common import exceptions as nsxv_exc
from vmware_nsx.common import locking
from vmware_nsx.db import nsxv_db
from vmware_nsx.plugins.nsx_v.drivers import (
    abstract_router_driver as router_driver)
from vmware_nsx.plugins.nsx_v import plugin as nsx_v
from vmware_nsx.plugins.nsx_v.vshield import edge_utils
from vmware_nsx.services.lbaas.octavia import constants as oct_const

LOG = logging.getLogger(__name__)


class RouterExclusiveDriver(router_driver.RouterBaseDriver):

    def get_type(self):
        return "exclusive"

    def create_router(self, context, lrouter, appliance_size=None,
                      allow_metadata=True):
        availability_zone = self.get_router_az(lrouter)
        self.edge_manager.create_lrouter(
            context, lrouter, dist=False, appliance_size=appliance_size,
            availability_zone=availability_zone)
        if allow_metadata:
            self.plugin.get_metadata_proxy_handler(
                availability_zone.name).configure_router_edge(
                    context, lrouter['id'])

    def update_router(self, context, router_id, router):
        r = router['router']
        is_routes_update = bool('routes' in r)

        gw_info = self.plugin._extract_external_gw(context, router,
                                                   is_extract=True)
        super(nsx_v.NsxVPluginV2, self.plugin).update_router(
            context, router_id, router)
        if gw_info != n_consts.ATTR_NOT_SPECIFIED:
            self.plugin._update_router_gw_info(context, router_id, gw_info,
                                               is_routes_update)
        elif is_routes_update:
            # here is used to handle routes which tenant updates.
            router_db = self.plugin._get_router(context, router_id)
            nexthop = self.plugin._get_external_attachment_info(
                context, router_db)[2]
            with locking.LockManager.get_lock(
                    self._get_router_edge_id(context, router_id)):
                self.plugin._update_subnets_and_dnat_firewall(context,
                                                              router_db)
            self.update_routes(context, router_id, nexthop)
        if 'admin_state_up' in r:
            self.plugin._update_router_admin_state(
                context, router_id, self.get_type(), r['admin_state_up'])
        if 'name' in r:
            self.edge_manager.rename_lrouter(context, router_id, r['name'])
        if r.get('router_size'):
            self.edge_manager.resize_lrouter(context, router_id,
                                             r['router_size'])
        return self.plugin.get_router(context, router_id)

    def detach_router(self, context, router_id, router):
        LOG.debug("Detach exclusive router id %s", router_id)
        router_db = self.plugin._get_router(context, router_id)
        self._notify_before_router_edge_association(context, router_db)
        self.edge_manager.unbind_router_on_edge(context, router_id)
        if self.plugin.metadata_proxy_handler:
            az = self.get_router_az_by_id(context, router_id)
            metadata_proxy_handler = self.plugin.get_metadata_proxy_handler(
                az.name)
            if metadata_proxy_handler:
                metadata_proxy_handler.cleanup_router_edge(context, router_id)

    def _build_router_data_from_db(self, router_db, router):
        """Return a new dictionary with all DB & requested router attributes
        """
        router_attr = router['router'].copy()
        fields = ['status', 'name', 'admin_state_up', 'tenant_id', 'id']
        for field in fields:
            if field not in router['router']:
                router_attr[field] = getattr(router_db, field)
        return router_attr

    def attach_router(self, context, router_id, router, appliance_size=None):
        router_db = self.plugin._get_router(context, router_id)

        # Add DB attributes to the router data structure
        # before creating it as an exclusive router
        router_attr = self._build_router_data_from_db(router_db, router)
        allow_metadata = bool(self.plugin.metadata_proxy_handler)
        self.create_router(context,
                           router_attr,
                           allow_metadata=allow_metadata,
                           appliance_size=appliance_size)

        edge_id = edge_utils.get_router_edge_id(context, router_id)
        LOG.debug("Exclusive router %s attached to edge %s",
                  router_id, edge_id)

        # add all internal interfaces of the router on edge
        intf_net_ids = (
            self.plugin._get_internal_network_ids_by_router(context,
                                                            router_id))
        with locking.LockManager.get_lock(edge_id):
            for network_id in intf_net_ids:
                address_groups = self.plugin._get_address_groups(
                    context, router_id, network_id)
                edge_utils.update_internal_interface(
                    self.nsx_v, context, router_id, network_id,
                    address_groups, router_db.admin_state_up)

        # Update external interface (which also update nat rules, routes, etc)
        external_net_id = self._get_external_network_id_by_router(context,
                                                                  router_id)
        gw_info = None
        if (external_net_id):
            gw_info = {'network_id': external_net_id,
                       'enable_snat': router_db.enable_snat}
        self.plugin._update_router_gw_info(
            context, router_id, gw_info, force_update=True)

    def delete_router(self, context, router_id):
        edge_id, az_name = self.plugin._get_edge_id_and_az_by_rtr_id(
            context, router_id)
        if self.plugin.metadata_proxy_handler:
            # The neutron router was already deleted, so we cannot get the AZ
            # from it. Get it from the router-bindings DB
            md_proxy = self.plugin.get_metadata_proxy_handler(az_name)
            if md_proxy:
                md_proxy.cleanup_router_edge(context, router_id)

        # When LBs are hosted on router edges, there are cases where LB binding
        # remains even though the LB is gone. While the router is protected
        # from deletion when an LB exists, there's no proper cleanup for this
        # binding entry so we clean it up here
        if cfg.CONF.nsxv.use_routers_as_lbaas_platform:
            lb_bindings = nsxv_db.get_nsxv_lbaas_loadbalancer_binding_by_edge(
                context.session, edge_id)

            for lb_binding in lb_bindings:
                LOG.warning("Deleting stale LB binding for LB %s on edge %s",
                            lb_binding['loadbalancer_id'],
                            edge_id)
                nsxv_db.del_nsxv_lbaas_loadbalancer_binding(
                    context.session, lb_binding['loadbalancer_id'])

        self.edge_manager.delete_lrouter(context, router_id, dist=False)

    def update_routes(self, context, router_id, nexthop):
        with locking.LockManager.get_lock(
                self._get_router_edge_id(context, router_id)):
            self.plugin._update_routes(context, router_id, nexthop)

    @db_api.retry_db_errors
    def _update_router_gw_info(self, context, router_id, info,
                               is_routes_update=False, force_update=False):
        router = self.plugin._get_router(context, router_id)
        org_ext_net_id = router.gw_port_id and router.gw_port.network_id
        org_enable_snat = router.enable_snat
        orgaddr, orgmask, orgnexthop = (
            self.plugin._get_external_attachment_info(
                context, router))

        super(nsx_v.NsxVPluginV2, self.plugin)._update_router_gw_info(
            context, router_id, info, router=router)
        router = self.plugin._get_router(context, router_id)

        new_ext_net_id = router.gw_port_id and router.gw_port.network_id
        new_enable_snat = router.enable_snat
        newaddr, newmask, newnexthop = (
            self.plugin._get_external_attachment_info(
                context, router))

        edge_id = self._get_router_edge_id(context, router_id)
        with locking.LockManager.get_lock(edge_id):
            if ((new_ext_net_id != org_ext_net_id or force_update) and
                orgnexthop):
                # network changed, so need to remove default gateway before
                # vnic can be configured
                LOG.debug("Delete default gateway %s", orgnexthop)
                edge_utils.clear_gateway(self.nsx_v, context, router_id)

            secondary = self.plugin._get_floatingips_by_router(
                context, router_id)

            # Update external vnic if addr or mask is changed
            if orgaddr != newaddr or orgmask != newmask or force_update:
                self.edge_manager.update_external_interface(
                    self.nsx_v, context, router_id,
                    new_ext_net_id, newaddr, newmask, secondary=secondary)

            # Update SNAT rules if ext net changed
            # or ext net not changed but snat is changed.
            if (new_ext_net_id != org_ext_net_id or
                (new_ext_net_id == org_ext_net_id and
                 new_enable_snat != org_enable_snat) or
                force_update):
                self.plugin._update_nat_rules(context, router)

            if (new_ext_net_id != org_ext_net_id or
                new_enable_snat != org_enable_snat or
                is_routes_update or force_update):
                self.plugin._update_subnets_and_dnat_firewall(context, router)

            # Update static routes in all.
            self.plugin._update_routes(context, router_id, newnexthop)
        if new_ext_net_id or force_update:
            self._notify_after_router_edge_association(context, router)

    def add_router_interface(self, context, router_id, interface_info):
        self.plugin._check_intf_number_of_router(context, router_id)
        info = super(nsx_v.NsxVPluginV2, self.plugin).add_router_interface(
            context, router_id, interface_info)

        router_db = self.plugin._get_router(context, router_id)
        subnet = self.plugin.get_subnet(context, info['subnet_id'])
        network_id = subnet['network_id']
        address_groups = self.plugin._get_address_groups(
            context, router_id, network_id)
        with locking.LockManager.get_lock(
                self._get_router_edge_id(context, router_id)):
            edge_utils.update_internal_interface(
                self.nsx_v, context, router_id, network_id, address_groups,
                router_db['admin_state_up'])
            # Update edge's firewall rules to accept subnets flows.
            self.plugin._update_subnets_and_dnat_firewall(context, router_db)

            if router_db.gw_port and router_db.enable_snat:
                # Update Nat rules on external edge vnic
                self.plugin._update_nat_rules(context, router_db)
        return info

    def remove_router_interface(self, context, router_id, interface_info):

        # If a loadbalancer is attached to this Edge appliance, we cannot
        # detach the subnet from the exclusive router.
        subnet = interface_info.get('subnet_id')
        if not subnet and interface_info.get('port_id'):
            port = self.plugin.get_port(context, interface_info['port_id'])
            port_subnets = [
                fixed_ip['subnet_id'] for fixed_ip in port.get(
                    'fixed_ips', [])]
            subnet = port_subnets[0]

        if subnet and self._check_lb_on_subnet(context, subnet, router_id):
            error = _('Cannot delete router %(rtr)s interface while '
                      'loadbalancers are provisioned on attached '
                      'subnet %(subnet)s') % {'rtr': router_id,
                                              'subnet': subnet}
            raise nsxv_exc.NsxPluginException(err_msg=error)

        info = super(nsx_v.NsxVPluginV2, self.plugin).remove_router_interface(
            context, router_id, interface_info)
        router_db = self.plugin._get_router(context, router_id)
        subnet = self.plugin.get_subnet(context, info['subnet_id'])
        network_id = subnet['network_id']

        with locking.LockManager.get_lock(
                self._get_router_edge_id(context, router_id)):
            if router_db.gw_port and router_db.enable_snat:
                # First update nat rules
                self.plugin._update_nat_rules(context, router_db)
            ports = self.plugin._get_router_interface_ports_by_network(
                context, router_id, network_id)
            self.plugin._update_subnets_and_dnat_firewall(context, router_db)
            # No subnet on the network connects to the edge vnic
            if not ports:
                edge_utils.delete_interface(self.nsx_v, context,
                                            router_id, network_id,
                                            dist=False)
            else:
                address_groups = self.plugin._get_address_groups(
                    context, router_id, network_id)
                edge_utils.update_internal_interface(self.nsx_v, context,
                                                     router_id, network_id,
                                                     address_groups)
        return info

    def _check_lb_on_subnet(self, context, subnet_id, router_id):
        # Check lbaas
        dev_owner_v1 = n_consts.DEVICE_OWNER_LOADBALANCER
        dev_owner_v2 = n_consts.DEVICE_OWNER_LOADBALANCERV2
        dev_owner_oct = oct_const.DEVICE_OWNER_OCTAVIA
        filters = {'device_owner': [dev_owner_v1, dev_owner_v2, dev_owner_oct],
                   'fixed_ips': {'subnet_id': [subnet_id]}}
        ports = super(nsx_v.NsxVPluginV2, self.plugin).get_ports(
            context, filters=filters)

        edge_id = self._get_router_edge_id(context, router_id)
        lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding_by_edge(
            context.session, edge_id)
        return (len(ports) >= 1) and lb_binding

    def _update_edge_router(self, context, router_id):
        router = self.plugin._get_router(context.elevated(), router_id)
        with locking.LockManager.get_lock(
                self._get_router_edge_id(context, router_id)):
            self.plugin._update_external_interface(context, router)
            self.plugin._update_nat_rules(context, router)
            self.plugin._update_subnets_and_dnat_firewall(context, router)

    def _get_router_edge_id(self, context, router_id):
        binding = nsxv_db.get_nsxv_router_binding(context.session, router_id)
        return binding['edge_id']
