# Copyright (c) 2015 Clinton Knight.  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.
"""
NetApp Data ONTAP cDOT multi-SVM storage driver library.

This library extends the abstract base library and completes the multi-SVM
functionality needed by the cDOT multi-SVM Manila driver.  This library
variant creates Data ONTAP storage virtual machines (i.e. 'vservers')
as needed to provision shares.
"""

import re

from oslo_log import log
from oslo_utils import excutils

from manila import exception
from manila.i18n import _, _LE, _LW, _LI
from manila.share.drivers.netapp.dataontap.client import client_cmode
from manila.share.drivers.netapp.dataontap.cluster_mode import lib_base
from manila.share.drivers.netapp import utils as na_utils
from manila import utils


LOG = log.getLogger(__name__)
SUPPORTED_NETWORK_TYPES = (None, 'flat', 'vlan')
SEGMENTED_NETWORK_TYPES = ('vlan',)
DEFAULT_MTU = 1500


class NetAppCmodeMultiSVMFileStorageLibrary(
        lib_base.NetAppCmodeFileStorageLibrary):

    @na_utils.trace
    def check_for_setup_error(self):

        if self._have_cluster_creds:
            if self.configuration.netapp_vserver:
                msg = _LW('Vserver is specified in the configuration. This is '
                          'ignored when the driver is managing share servers.')
                LOG.warning(msg)

        else:  # only have vserver creds, which is an error in multi_svm mode
            msg = _('Cluster credentials must be specified in the '
                    'configuration when the driver is managing share servers.')
            raise exception.InvalidInput(reason=msg)

        # Ensure one or more aggregates are available.
        if not self._find_matching_aggregates():
            msg = _('No aggregates are available for provisioning shares. '
                    'Ensure that the configuration option '
                    'netapp_aggregate_name_search_pattern is set correctly.')
            raise exception.NetAppException(msg)

        super(NetAppCmodeMultiSVMFileStorageLibrary, self).\
            check_for_setup_error()

    @na_utils.trace
    def _get_vserver(self, share_server=None):

        if not share_server:
            msg = _('Share server not provided')
            raise exception.InvalidInput(reason=msg)

        backend_details = share_server.get('backend_details')
        vserver = backend_details.get(
            'vserver_name') if backend_details else None

        if not vserver:
            msg = _('Vserver name is absent in backend details. Please '
                    'check whether Vserver was created properly.')
            raise exception.VserverNotSpecified(msg)

        if not self._client.vserver_exists(vserver):
            raise exception.VserverNotFound(vserver=vserver)

        vserver_client = self._get_api_client(vserver)
        return vserver, vserver_client

    @na_utils.trace
    def _handle_housekeeping_tasks(self):
        """Handle various cleanup activities."""
        self._client.prune_deleted_nfs_export_policies()
        self._client.prune_deleted_snapshots()

        super(NetAppCmodeMultiSVMFileStorageLibrary, self).\
            _handle_housekeeping_tasks()

    @na_utils.trace
    def _find_matching_aggregates(self):
        """Find all aggregates match pattern."""
        aggregate_names = self._client.list_aggregates()
        pattern = self.configuration.netapp_aggregate_name_search_pattern
        return [aggr_name for aggr_name in aggregate_names
                if re.match(pattern, aggr_name)]

    @na_utils.trace
    def setup_server(self, network_info, metadata=None):
        """Creates and configures new Vserver."""

        vlan = network_info['segmentation_id']

        @utils.synchronized('netapp-VLAN-%s' % vlan, external=True)
        def setup_server_with_lock():
            LOG.debug('Creating server %s', network_info['server_id'])
            self._validate_network_type(network_info)

            vserver_name = self._get_vserver_name(network_info['server_id'])
            server_details = {'vserver_name': vserver_name}

            try:
                self._create_vserver(vserver_name, network_info)
            except Exception as e:
                e.detail_data = {'server_details': server_details}
                raise

            return server_details

        return setup_server_with_lock()

    @na_utils.trace
    def _validate_network_type(self, network_info):
        """Raises exception if the segmentation type is incorrect."""
        if network_info['network_type'] not in SUPPORTED_NETWORK_TYPES:
            msg = _('The specified network type %s is unsupported by the '
                    'NetApp clustered Data ONTAP driver')
            raise exception.NetworkBadConfigurationException(
                reason=msg % network_info['network_type'])

    @na_utils.trace
    def _get_vserver_name(self, server_id):
        return self.configuration.netapp_vserver_name_template % server_id

    @na_utils.trace
    def _create_vserver(self, vserver_name, network_info):
        """Creates Vserver with given parameters if it doesn't exist."""

        if self._client.vserver_exists(vserver_name):
            msg = _('Vserver %s already exists.')
            raise exception.NetAppException(msg % vserver_name)

        ipspace_name = self._create_ipspace(network_info)

        LOG.debug('Vserver %s does not exist, creating.', vserver_name)
        self._client.create_vserver(
            vserver_name,
            self.configuration.netapp_root_volume_aggregate,
            self.configuration.netapp_root_volume,
            self._find_matching_aggregates(),
            ipspace_name)

        vserver_client = self._get_api_client(vserver=vserver_name)
        security_services = None
        try:
            self._create_vserver_lifs(vserver_name,
                                      vserver_client,
                                      network_info,
                                      ipspace_name)

            self._create_vserver_admin_lif(vserver_name,
                                           vserver_client,
                                           network_info,
                                           ipspace_name)

            vserver_client.enable_nfs(
                self.configuration.netapp_enabled_share_protocols)

            security_services = network_info.get('security_services')
            if security_services:
                self._client.setup_security_services(security_services,
                                                     vserver_client,
                                                     vserver_name)
        except Exception:
            with excutils.save_and_reraise_exception():
                LOG.error(_LE("Failed to configure Vserver."))
                self._delete_vserver(vserver_name,
                                     security_services=security_services)

    def _get_valid_ipspace_name(self, network_id):
        """Get IPspace name according to network id."""
        return 'ipspace_' + network_id.replace('-', '_')

    @na_utils.trace
    def _create_ipspace(self, network_info):
        """If supported, create an IPspace for a new Vserver."""

        if not self._client.features.IPSPACES:
            return None

        if (network_info['network_allocations'][0]['network_type']
                not in SEGMENTED_NETWORK_TYPES):
            return client_cmode.DEFAULT_IPSPACE

        # NOTE(cknight): Neutron needs cDOT IP spaces because it can provide
        # overlapping IP address ranges for different subnets.  That is not
        # believed to be an issue for any of Manila's other network plugins.
        ipspace_id = network_info.get('neutron_subnet_id')
        if not ipspace_id:
            return client_cmode.DEFAULT_IPSPACE

        ipspace_name = self._get_valid_ipspace_name(ipspace_id)
        if not self._client.ipspace_exists(ipspace_name):
            self._client.create_ipspace(ipspace_name)

        return ipspace_name

    @na_utils.trace
    def _create_vserver_lifs(self, vserver_name, vserver_client, network_info,
                             ipspace_name):
        """Create Vserver data logical interfaces (LIFs)."""

        nodes = self._client.list_cluster_nodes()
        node_network_info = zip(nodes, network_info['network_allocations'])

        for node_name, network_allocation in node_network_info:
            lif_name = self._get_lif_name(node_name, network_allocation)
            self._create_lif(vserver_client, vserver_name, ipspace_name,
                             node_name, lif_name, network_allocation)

    @na_utils.trace
    def _create_vserver_admin_lif(self, vserver_name, vserver_client,
                                  network_info, ipspace_name):
        """Create Vserver admin LIF, if defined."""

        network_allocations = network_info.get('admin_network_allocations')
        if not network_allocations:
            LOG.info(_LI('No admin network defined for Vserver %s.') %
                     vserver_name)
            return

        node_name = self._client.list_cluster_nodes()[0]
        network_allocation = network_allocations[0]
        lif_name = self._get_lif_name(node_name, network_allocation)

        self._create_lif(vserver_client, vserver_name, ipspace_name,
                         node_name, lif_name, network_allocation)

    @na_utils.trace
    def _get_node_data_port(self, node):
        port_names = self._client.list_node_data_ports(node)
        pattern = self.configuration.netapp_port_name_search_pattern
        matched_port_names = [port_name for port_name in port_names
                              if re.match(pattern, port_name)]
        if not matched_port_names:
            raise exception.NetAppException(
                _('Could not find eligible network ports on node %s on which '
                  'to create Vserver LIFs.') % node)
        return matched_port_names[0]

    def _get_lif_name(self, node_name, network_allocation):
        """Get LIF name based on template from manila.conf file."""
        lif_name_args = {
            'node': node_name,
            'net_allocation_id': network_allocation['id'],
        }
        return self.configuration.netapp_lif_name_template % lif_name_args

    @na_utils.trace
    def _create_lif(self, vserver_client, vserver_name, ipspace_name,
                    node_name, lif_name, network_allocation):
        """Creates LIF for Vserver."""

        port = self._get_node_data_port(node_name)
        ip_address = network_allocation['ip_address']
        netmask = utils.cidr_to_netmask(network_allocation['cidr'])
        vlan = network_allocation['segmentation_id']
        network_mtu = network_allocation.get('mtu')
        mtu = network_mtu or DEFAULT_MTU

        if not vserver_client.network_interface_exists(
                vserver_name, node_name, port, ip_address, netmask, vlan):

            self._client.create_network_interface(
                ip_address, netmask, vlan, node_name, port, vserver_name,
                lif_name, ipspace_name, mtu)

    @na_utils.trace
    def get_network_allocations_number(self):
        """Get number of network interfaces to be created."""
        return len(self._client.list_cluster_nodes())

    @na_utils.trace
    def get_admin_network_allocations_number(self, admin_network_api):
        """Get number of network allocations for creating admin LIFs."""
        return 1 if admin_network_api else 0

    @na_utils.trace
    def teardown_server(self, server_details, security_services=None):
        """Teardown share server."""
        vserver = server_details.get(
            'vserver_name') if server_details else None

        if not vserver:
            LOG.warning(_LW("Vserver not specified for share server being "
                            "deleted. Deletion of share server record will "
                            "proceed anyway."))
            return

        elif not self._client.vserver_exists(vserver):
            LOG.warning(_LW("Could not find Vserver for share server being "
                            "deleted: %s. Deletion of share server "
                            "record will proceed anyway."), vserver)
            return

        self._delete_vserver(vserver, security_services=security_services)

    @na_utils.trace
    def _delete_vserver(self, vserver, security_services=None):
        """Delete a Vserver plus IPspace and security services as needed."""

        ipspace_name = self._client.get_vserver_ipspace(vserver)

        vserver_client = self._get_api_client(vserver=vserver)
        network_interfaces = vserver_client.get_network_interfaces()

        vlan = None
        if network_interfaces:
            home_port = network_interfaces[0]['home-port']
            vlan = home_port.split('-')[1]

        @utils.synchronized('netapp-VLAN-%s' % vlan, external=True)
        def _delete_vserver_with_lock():
            self._client.delete_vserver(vserver,
                                        vserver_client,
                                        security_services=security_services)

            if ipspace_name and not self._client.ipspace_has_data_vservers(
                    ipspace_name):
                self._client.delete_ipspace(ipspace_name)

            self._delete_vserver_vlan(network_interfaces)

        return _delete_vserver_with_lock()

    @na_utils.trace
    def _delete_vserver_vlan(self, vserver_network_interfaces):
        """Delete Vserver's VLAN configuration from ports"""

        for interface in vserver_network_interfaces:
            try:
                home_port = interface['home-port']
                port, vlan = home_port.split('-')
                node = interface['home-node']
                self._client.delete_vlan(node, port, vlan)
            except exception.NetAppException:
                LOG.exception(_LE("Deleting Vserver VLAN failed."))
