# Copyright 2014 VMware, 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.
#

from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_utils import excutils

from vmware_nsx._i18n import _
from vmware_nsx.api_client import exception as api_exc
from vmware_nsx.common import exceptions as p_exc
from vmware_nsx.db import db as nsx_db
from vmware_nsx.db import lsn_db
from vmware_nsx.dhcp_meta import constants as const
from vmware_nsx.nsxlib.mh import lsn as lsn_api
from vmware_nsx.nsxlib.mh import switch as switch_api

LOG = logging.getLogger(__name__)

META_CONF = 'metadata-proxy'
DHCP_CONF = 'dhcp'


lsn_opts = [
    cfg.BoolOpt('sync_on_missing_data', default=False,
                help=_('Pull LSN information from NSX in case it is missing '
                       'from the local data store. This is useful to rebuild '
                       'the local store in case of server recovery.'))
]


def register_lsn_opts(config):
    config.CONF.register_opts(lsn_opts, "NSX_LSN")


def get_nsx_switch_ids(session, cluster, neutron_network_id):
    """Return the NSX switch id for a given neutron network.

    First lookup for mappings in Neutron database. If no mapping is
    found, query the NSX backend and add the mappings.
    """
    nsx_switch_ids = nsx_db.get_nsx_switch_ids(
        session, neutron_network_id)
    if not nsx_switch_ids:
        # Find logical switches from backend.
        # This is a rather expensive query, but it won't be executed
        # more than once for each network in Neutron's lifetime
        nsx_switches = switch_api.get_lswitches(cluster, neutron_network_id)
        if not nsx_switches:
            LOG.warning("Unable to find NSX switches for Neutron network "
                        "%s", neutron_network_id)
            return []
        nsx_switch_ids = []
        with session.begin(subtransactions=True):
            for nsx_switch in nsx_switches:
                nsx_switch_id = nsx_switch['uuid']
                nsx_switch_ids.append(nsx_switch_id)
                # Create DB mapping
                nsx_db.add_neutron_nsx_network_mapping(
                    session,
                    neutron_network_id,
                    nsx_switch_id)
    return nsx_switch_ids


class LsnManager(object):
    """Manage LSN entities associated with networks."""

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

    @property
    def cluster(self):
        return self.plugin.cluster

    def lsn_exists(self, context, network_id):
        """Return True if a Logical Service Node exists for the network."""
        return self.lsn_get(
            context, network_id, raise_on_err=False) is not None

    def lsn_get(self, context, network_id, raise_on_err=True):
        """Retrieve the LSN id associated to the network."""
        try:
            return lsn_api.lsn_for_network_get(self.cluster, network_id)
        except (n_exc.NotFound, api_exc.NsxApiException):
            if raise_on_err:
                LOG.error('Unable to find Logical Service Node for '
                          'network %s.',
                          network_id)
                raise p_exc.LsnNotFound(entity='network',
                                        entity_id=network_id)
            LOG.warning('Unable to find Logical Service Node for '
                        'the requested network %s.',
                        network_id)

    def lsn_create(self, context, network_id):
        """Create a LSN associated to the network."""
        try:
            return lsn_api.lsn_for_network_create(self.cluster, network_id)
        except api_exc.NsxApiException:
            err_msg = _('Unable to create LSN for network %s') % network_id
            raise p_exc.NsxPluginException(err_msg=err_msg)

    def lsn_delete(self, context, lsn_id):
        """Delete a LSN given its id."""
        try:
            lsn_api.lsn_delete(self.cluster, lsn_id)
        except (n_exc.NotFound, api_exc.NsxApiException):
            LOG.warning('Unable to delete Logical Service Node %s',
                        lsn_id)

    def lsn_delete_by_network(self, context, network_id):
        """Delete a LSN associated to the network."""
        lsn_id = self.lsn_get(context, network_id, raise_on_err=False)
        if lsn_id:
            self.lsn_delete(context, lsn_id)

    def lsn_port_get(self, context, network_id, subnet_id, raise_on_err=True):
        """Retrieve LSN and LSN port for the network and the subnet."""
        lsn_id = self.lsn_get(context, network_id, raise_on_err=raise_on_err)
        if lsn_id:
            try:
                lsn_port_id = lsn_api.lsn_port_by_subnet_get(
                    self.cluster, lsn_id, subnet_id)
            except (n_exc.NotFound, api_exc.NsxApiException):
                if raise_on_err:
                    LOG.error('Unable to find Logical Service Node Port '
                              'for LSN %(lsn_id)s and subnet '
                              '%(subnet_id)s',
                              {'lsn_id': lsn_id, 'subnet_id': subnet_id})
                    raise p_exc.LsnPortNotFound(lsn_id=lsn_id,
                                                entity='subnet',
                                                entity_id=subnet_id)
                LOG.warning('Unable to find Logical Service Node Port '
                            'for LSN %(lsn_id)s and subnet '
                            '%(subnet_id)s',
                            {'lsn_id': lsn_id, 'subnet_id': subnet_id})
                return (lsn_id, None)
            else:
                return (lsn_id, lsn_port_id)
        else:
            return (None, None)

    def lsn_port_get_by_mac(self, context, network_id, mac, raise_on_err=True):
        """Retrieve LSN and LSN port given network and mac address."""
        lsn_id = self.lsn_get(context, network_id, raise_on_err=raise_on_err)
        if lsn_id:
            try:
                lsn_port_id = lsn_api.lsn_port_by_mac_get(
                    self.cluster, lsn_id, mac)
            except (n_exc.NotFound, api_exc.NsxApiException):
                if raise_on_err:
                    LOG.error('Unable to find Logical Service Node Port '
                              'for LSN %(lsn_id)s and mac address '
                              '%(mac)s',
                              {'lsn_id': lsn_id, 'mac': mac})
                    raise p_exc.LsnPortNotFound(lsn_id=lsn_id,
                                                entity='MAC',
                                                entity_id=mac)
                LOG.warning('Unable to find Logical Service Node '
                            'Port for LSN %(lsn_id)s and mac address '
                            '%(mac)s',
                            {'lsn_id': lsn_id, 'mac': mac})
                return (lsn_id, None)
            else:
                return (lsn_id, lsn_port_id)
        else:
            return (None, None)

    def lsn_port_create(self, context, lsn_id, subnet_info):
        """Create and return LSN port for associated subnet."""
        try:
            return lsn_api.lsn_port_create(self.cluster, lsn_id, subnet_info)
        except n_exc.NotFound:
            raise p_exc.LsnNotFound(entity='', entity_id=lsn_id)
        except api_exc.NsxApiException:
            err_msg = _('Unable to create port for LSN  %s') % lsn_id
            raise p_exc.NsxPluginException(err_msg=err_msg)

    def lsn_port_delete(self, context, lsn_id, lsn_port_id):
        """Delete a LSN port from the Logical Service Node."""
        try:
            lsn_api.lsn_port_delete(self.cluster, lsn_id, lsn_port_id)
        except (n_exc.NotFound, api_exc.NsxApiException):
            LOG.warning('Unable to delete LSN Port %s', lsn_port_id)

    def lsn_port_dispose(self, context, network_id, mac_address):
        """Delete a LSN port given the network and the mac address."""
        lsn_id, lsn_port_id = self.lsn_port_get_by_mac(
            context, network_id, mac_address, raise_on_err=False)
        if lsn_port_id:
            self.lsn_port_delete(context, lsn_id, lsn_port_id)
            if mac_address == const.METADATA_MAC:
                try:
                    lswitch_port_id = switch_api.get_port_by_neutron_tag(
                        self.cluster, network_id,
                        const.METADATA_PORT_ID)['uuid']
                    switch_api.delete_port(
                        self.cluster, network_id, lswitch_port_id)
                except (n_exc.PortNotFoundOnNetwork,
                        api_exc.NsxApiException):
                    LOG.warning("Metadata port not found while attempting "
                                "to delete it from network %s",
                                network_id)
        else:
            LOG.warning("Unable to find Logical Services Node "
                        "Port with MAC %s", mac_address)

    def lsn_port_dhcp_setup(
        self, context, network_id, port_id, port_data, subnet_config=None):
        """Connect network to LSN via specified port and port_data."""
        try:
            lsn_id = None
            switch_id = get_nsx_switch_ids(
                context.session, self.cluster, network_id)[0]
            lswitch_port_id = switch_api.get_port_by_neutron_tag(
                self.cluster, switch_id, port_id)['uuid']
            lsn_id = self.lsn_get(context, network_id)
            lsn_port_id = self.lsn_port_create(context, lsn_id, port_data)
        except (n_exc.NotFound, p_exc.NsxPluginException):
            raise p_exc.PortConfigurationError(
                net_id=network_id, lsn_id=lsn_id, port_id=port_id)
        else:
            try:
                lsn_api.lsn_port_plug_network(
                    self.cluster, lsn_id, lsn_port_id, lswitch_port_id)
            except p_exc.LsnConfigurationConflict:
                self.lsn_port_delete(context, lsn_id, lsn_port_id)
                raise p_exc.PortConfigurationError(
                    net_id=network_id, lsn_id=lsn_id, port_id=port_id)
            if subnet_config:
                self.lsn_port_dhcp_configure(
                    context, lsn_id, lsn_port_id, subnet_config)
            else:
                return (lsn_id, lsn_port_id)

    def lsn_port_metadata_setup(self, context, lsn_id, subnet):
        """Connect subnet to specified LSN."""
        data = {
            "mac_address": const.METADATA_MAC,
            "ip_address": subnet['cidr'],
            "subnet_id": subnet['id']
        }
        network_id = subnet['network_id']
        tenant_id = subnet['tenant_id']
        lswitch_port_id = None
        try:
            switch_id = get_nsx_switch_ids(
                context.session, self.cluster, network_id)[0]
            lswitch_port_id = switch_api.create_lport(
                self.cluster, switch_id, tenant_id,
                const.METADATA_PORT_ID, const.METADATA_PORT_NAME,
                const.METADATA_DEVICE_ID, True)['uuid']
            lsn_port_id = self.lsn_port_create(context, lsn_id, data)
        except (n_exc.NotFound, p_exc.NsxPluginException,
                api_exc.NsxApiException):
            raise p_exc.PortConfigurationError(
                net_id=network_id, lsn_id=lsn_id, port_id=lswitch_port_id)
        else:
            try:
                lsn_api.lsn_port_plug_network(
                    self.cluster, lsn_id, lsn_port_id, lswitch_port_id)
            except p_exc.LsnConfigurationConflict:
                self.lsn_port_delete(self.cluster, lsn_id, lsn_port_id)
                switch_api.delete_port(
                    self.cluster, network_id, lswitch_port_id)
                raise p_exc.PortConfigurationError(
                    net_id=network_id, lsn_id=lsn_id, port_id=lsn_port_id)

    def lsn_port_dhcp_configure(self, context, lsn_id, lsn_port_id, subnet):
        """Enable/disable dhcp services with the given config options."""
        is_enabled = subnet["enable_dhcp"]
        dhcp_options = {
            "domain_name": cfg.CONF.NSX_DHCP.domain_name,
            "default_lease_time": cfg.CONF.NSX_DHCP.default_lease_time,
        }
        dns_servers = cfg.CONF.NSX_DHCP.extra_domain_name_servers or []
        dns_servers.extend(subnet["dns_nameservers"])
        if subnet['gateway_ip']:
            dhcp_options["routers"] = subnet["gateway_ip"]
        if dns_servers:
            dhcp_options["domain_name_servers"] = ",".join(dns_servers)
        if subnet["host_routes"]:
            dhcp_options["classless_static_routes"] = (
                ",".join(subnet["host_routes"])
            )
        try:
            lsn_api.lsn_port_dhcp_configure(
                self.cluster, lsn_id, lsn_port_id, is_enabled, dhcp_options)
        except (n_exc.NotFound, api_exc.NsxApiException):
            err_msg = (_('Unable to configure dhcp for Logical Service '
                         'Node %(lsn_id)s and port %(lsn_port_id)s')
                       % {'lsn_id': lsn_id, 'lsn_port_id': lsn_port_id})
            LOG.error(err_msg)
            raise p_exc.NsxPluginException(err_msg=err_msg)

    def lsn_metadata_configure(self, context, subnet_id, is_enabled):
        """Configure metadata service for the specified subnet."""
        subnet = self.plugin.get_subnet(context, subnet_id)
        network_id = subnet['network_id']
        meta_conf = cfg.CONF.NSX_METADATA
        metadata_options = {
            'metadata_server_ip': meta_conf.metadata_server_address,
            'metadata_server_port': meta_conf.metadata_server_port,
            'metadata_proxy_shared_secret': meta_conf.metadata_shared_secret
        }
        try:
            lsn_id = self.lsn_get(context, network_id)
            lsn_api.lsn_metadata_configure(
                self.cluster, lsn_id, is_enabled, metadata_options)
        except (p_exc.LsnNotFound, api_exc.NsxApiException):
            err_msg = (_('Unable to configure metadata '
                         'for subnet %s') % subnet_id)
            LOG.error(err_msg)
            raise p_exc.NsxPluginException(err_msg=err_msg)
        if is_enabled:
            try:
                # test that the lsn port exists
                self.lsn_port_get(context, network_id, subnet_id)
            except p_exc.LsnPortNotFound:
                # this might happen if subnet had dhcp off when created
                # so create one, and wire it
                self.lsn_port_metadata_setup(context, lsn_id, subnet)
        else:
            self.lsn_port_dispose(context, network_id, const.METADATA_MAC)

    def _lsn_port_host_conf(self, context, network_id, subnet_id, data, hdlr):
        lsn_id, lsn_port_id = self.lsn_port_get(
            context, network_id, subnet_id, raise_on_err=False)
        try:
            if lsn_id and lsn_port_id:
                hdlr(self.cluster, lsn_id, lsn_port_id, data)
        except (n_exc.NotFound, api_exc.NsxApiException):
            LOG.error('Error while configuring LSN '
                      'port %s', lsn_port_id)
            raise p_exc.PortConfigurationError(
                net_id=network_id, lsn_id=lsn_id, port_id=lsn_port_id)

    def lsn_port_dhcp_host_add(self, context, network_id, subnet_id, host):
        """Add dhcp host entry to LSN port configuration."""
        self._lsn_port_host_conf(context, network_id, subnet_id, host,
                                 lsn_api.lsn_port_dhcp_host_add)

    def lsn_port_dhcp_host_remove(self, context, network_id, subnet_id, host):
        """Remove dhcp host entry from LSN port configuration."""
        self._lsn_port_host_conf(context, network_id, subnet_id, host,
                                 lsn_api.lsn_port_dhcp_host_remove)

    def lsn_port_meta_host_add(self, context, network_id, subnet_id, host):
        """Add dhcp host entry to LSN port configuration."""
        self._lsn_port_host_conf(context, network_id, subnet_id, host,
                                 lsn_api.lsn_port_metadata_host_add)

    def lsn_port_meta_host_remove(self, context, network_id, subnet_id, host):
        """Remove dhcp host entry from LSN port configuration."""
        self._lsn_port_host_conf(context, network_id, subnet_id, host,
                                 lsn_api.lsn_port_metadata_host_remove)

    def lsn_port_update(
        self, context, network_id, subnet_id, dhcp=None, meta=None):
        """Update the specified configuration for the LSN port."""
        if not dhcp and not meta:
            return
        try:
            lsn_id, lsn_port_id = self.lsn_port_get(
                context, network_id, subnet_id, raise_on_err=False)
            if dhcp and lsn_id and lsn_port_id:
                lsn_api.lsn_port_host_entries_update(
                    self.cluster, lsn_id, lsn_port_id, DHCP_CONF, dhcp)
            if meta and lsn_id and lsn_port_id:
                lsn_api.lsn_port_host_entries_update(
                    self.cluster, lsn_id, lsn_port_id, META_CONF, meta)
        except api_exc.NsxApiException:
            raise p_exc.PortConfigurationError(
                net_id=network_id, lsn_id=lsn_id, port_id=lsn_port_id)


class PersistentLsnManager(LsnManager):
    """Add local persistent state to LSN Manager."""

    def __init__(self, plugin):
        super(PersistentLsnManager, self).__init__(plugin)
        self.sync_on_missing = cfg.CONF.NSX_LSN.sync_on_missing_data

    def lsn_get(self, context, network_id, raise_on_err=True):
        try:
            obj = lsn_db.lsn_get_for_network(
                context, network_id, raise_on_err=raise_on_err)
            return obj.lsn_id if obj else None
        except p_exc.LsnNotFound:
            with excutils.save_and_reraise_exception() as ctxt:
                ctxt.reraise = False
                if self.sync_on_missing:
                    lsn_id = super(PersistentLsnManager, self).lsn_get(
                        context, network_id, raise_on_err=raise_on_err)
                    self.lsn_save(context, network_id, lsn_id)
                    return lsn_id
                if raise_on_err:
                    ctxt.reraise = True

    def lsn_save(self, context, network_id, lsn_id):
        """Save LSN-Network mapping to the DB."""
        try:
            lsn_db.lsn_add(context, network_id, lsn_id)
        except db_exc.DBError:
            err_msg = _('Unable to save LSN for network %s') % network_id
            LOG.exception(err_msg)
            raise p_exc.NsxPluginException(err_msg=err_msg)

    def lsn_create(self, context, network_id):
        lsn_id = super(PersistentLsnManager,
                       self).lsn_create(context, network_id)
        try:
            self.lsn_save(context, network_id, lsn_id)
        except p_exc.NsxPluginException:
            with excutils.save_and_reraise_exception():
                super(PersistentLsnManager, self).lsn_delete(context, lsn_id)
        return lsn_id

    def lsn_delete(self, context, lsn_id):
        lsn_db.lsn_remove(context, lsn_id)
        super(PersistentLsnManager, self).lsn_delete(context, lsn_id)

    def lsn_port_get(self, context, network_id, subnet_id, raise_on_err=True):
        try:
            obj = lsn_db.lsn_port_get_for_subnet(
                context, subnet_id, raise_on_err=raise_on_err)
            return (obj.lsn_id, obj.lsn_port_id) if obj else (None, None)
        except p_exc.LsnPortNotFound:
            with excutils.save_and_reraise_exception() as ctxt:
                ctxt.reraise = False
                if self.sync_on_missing:
                    lsn_id, lsn_port_id = (
                        super(PersistentLsnManager, self).lsn_port_get(
                            context, network_id, subnet_id,
                            raise_on_err=raise_on_err))
                    mac_addr = lsn_api.lsn_port_info_get(
                        self.cluster, lsn_id, lsn_port_id)['mac_address']
                    self.lsn_port_save(
                        context, lsn_port_id, subnet_id, mac_addr, lsn_id)
                    return (lsn_id, lsn_port_id)
                if raise_on_err:
                    ctxt.reraise = True

    def lsn_port_get_by_mac(self, context, network_id, mac, raise_on_err=True):
        try:
            obj = lsn_db.lsn_port_get_for_mac(
                context, mac, raise_on_err=raise_on_err)
            return (obj.lsn_id, obj.lsn_port_id) if obj else (None, None)
        except p_exc.LsnPortNotFound:
            with excutils.save_and_reraise_exception() as ctxt:
                ctxt.reraise = False
                if self.sync_on_missing:
                    lsn_id, lsn_port_id = (
                        super(PersistentLsnManager, self).lsn_port_get_by_mac(
                            context, network_id, mac,
                            raise_on_err=raise_on_err))
                    subnet_id = lsn_api.lsn_port_info_get(
                        self.cluster, lsn_id, lsn_port_id).get('subnet_id')
                    self.lsn_port_save(
                        context, lsn_port_id, subnet_id, mac, lsn_id)
                    return (lsn_id, lsn_port_id)
                if raise_on_err:
                    ctxt.reraise = True

    def lsn_port_save(self, context, lsn_port_id, subnet_id, mac_addr, lsn_id):
        """Save LSN Port information to the DB."""
        try:
            lsn_db.lsn_port_add_for_lsn(
                context, lsn_port_id, subnet_id, mac_addr, lsn_id)
        except db_exc.DBError:
            err_msg = _('Unable to save LSN port for subnet %s') % subnet_id
            LOG.exception(err_msg)
            raise p_exc.NsxPluginException(err_msg=err_msg)

    def lsn_port_create(self, context, lsn_id, subnet_info):
        lsn_port_id = super(PersistentLsnManager,
                            self).lsn_port_create(context, lsn_id, subnet_info)
        try:
            self.lsn_port_save(context, lsn_port_id, subnet_info['subnet_id'],
                               subnet_info['mac_address'], lsn_id)
        except p_exc.NsxPluginException:
            with excutils.save_and_reraise_exception():
                super(PersistentLsnManager, self).lsn_port_delete(
                    context, lsn_id, lsn_port_id)
        return lsn_port_id

    def lsn_port_delete(self, context, lsn_id, lsn_port_id):
        lsn_db.lsn_port_remove(context, lsn_port_id)
        super(PersistentLsnManager, self).lsn_port_delete(
            context, lsn_id, lsn_port_id)
