# Copyright 2013 IBM Corp.
#
# 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.

"""
Handles all requests to Nova.
"""

from keystoneauth1 import exceptions as ks_exc
from keystoneauth1 import identity
from keystoneauth1 import loading as ks_loading
from novaclient import api_versions
from novaclient import client as nova_client
from novaclient import exceptions as nova_exceptions
from oslo_config import cfg
from oslo_log import log as logging
from requests import exceptions as request_exceptions

from cinder.db import base
from cinder import exception
from cinder.message import api as message_api
from cinder.message import message_field
from cinder import service_auth

nova_opts = [
    cfg.StrOpt('region_name',
               help='Name of nova region to use. Useful if keystone manages '
                    'more than one region.'),
    cfg.StrOpt('interface',
               default='public',
               choices=['public', 'admin', 'internal'],
               help='Type of the nova endpoint to use.  This endpoint will '
                    'be looked up in the keystone catalog and should be '
                    'one of public, internal or admin.'),
    cfg.StrOpt('token_auth_url',
               help='The authentication URL for the nova connection when '
                    'using the current user''s token'),
]


NOVA_GROUP = 'nova'
CONF = cfg.CONF

nova_session_opts = ks_loading.get_session_conf_options()
nova_auth_opts = ks_loading.get_auth_common_conf_options()

CONF.register_opts(nova_opts, group=NOVA_GROUP)
CONF.register_opts(nova_session_opts, group=NOVA_GROUP)
CONF.register_opts(nova_auth_opts, group=NOVA_GROUP)

LOG = logging.getLogger(__name__)

NOVA_API_VERSION = "2.1"

nova_extensions = [ext for ext in
                   nova_client.discover_extensions(NOVA_API_VERSION)
                   if ext.name in ("assisted_volume_snapshots",
                                   "list_extensions",
                                   "server_external_events")]


def _get_identity_endpoint_from_sc(context):
    # Search for the identity endpoint in the service catalog
    for service in context.service_catalog:
        if service.get('type') != 'identity':
            continue
        for endpoint in service['endpoints']:
            if (not CONF[NOVA_GROUP].region_name or
                    endpoint.get('region') == CONF[NOVA_GROUP].region_name):
                return endpoint.get(CONF[NOVA_GROUP].interface + 'URL')
    raise ks_exc.EndpointNotFound()


def novaclient(context, privileged_user=False, timeout=None, api_version=None):
    """Returns a Nova client

    @param privileged_user:
        If True, use the account from configuration
        (requires 'auth_type' and the other usual Keystone authentication
        options to be set in the [nova] section)
    @param timeout:
        Number of seconds to wait for an answer before raising a
        Timeout exception (None to disable)
    @param api_version:
        api version of nova
    """

    if privileged_user and CONF[NOVA_GROUP].auth_type:
        LOG.debug('Creating Keystone auth plugin from conf')
        n_auth = ks_loading.load_auth_from_conf_options(CONF, NOVA_GROUP)
    else:
        if CONF[NOVA_GROUP].token_auth_url:
            url = CONF[NOVA_GROUP].token_auth_url
        else:
            url = _get_identity_endpoint_from_sc(context)
        LOG.debug('Creating Keystone token plugin using URL: %s', url)
        n_auth = identity.Token(auth_url=url,
                                token=context.auth_token,
                                project_name=context.project_name,
                                project_domain_id=context.project_domain_id)

    if CONF.auth_strategy == 'keystone':
        n_auth = service_auth.get_auth_plugin(context, auth=n_auth)

    keystone_session = ks_loading.load_session_from_conf_options(
        CONF,
        NOVA_GROUP,
        auth=n_auth)

    c = nova_client.Client(
        api_versions.APIVersion(api_version or NOVA_API_VERSION),
        session=keystone_session,
        insecure=CONF[NOVA_GROUP].insecure,
        timeout=timeout,
        region_name=CONF[NOVA_GROUP].region_name,
        endpoint_type=CONF[NOVA_GROUP].interface,
        cacert=CONF[NOVA_GROUP].cafile,
        global_request_id=context.global_id,
        extensions=nova_extensions)

    return c


class API(base.Base):
    """API for interacting with novaclient."""
    NotFound = nova_exceptions.NotFound

    def __init__(self):
        self.message_api = message_api.API()
        super().__init__()

    def _get_volume_extended_event(self, server_id, volume_id):
        return {'name': 'volume-extended',
                'server_uuid': server_id,
                'tag': volume_id}

    def _get_volume_reimaged_event(self, server_id, volume_id):
        return {'name': 'volume-reimaged',
                'server_uuid': server_id,
                'tag': volume_id}

    def _send_events(self, context, events, api_version=None):
        nova = novaclient(context, privileged_user=True,
                          api_version=api_version)
        try:
            response = nova.server_external_events.create(events)
        except nova_exceptions.NotFound:
            LOG.warning('Nova returned NotFound for events: %s.', events)
            return False
        except Exception:
            LOG.exception('Failed to notify nova on events: %s.', events)
            return False
        else:
            if not isinstance(response, list):
                LOG.error('Error response returned from nova: %s.', response)
                return False
            response_error = False
            for event in response:
                code = event.get('code')
                if code is None:
                    response_error = True
                    continue
                if code != 200:
                    LOG.warning(
                        'Nova event: %s returned with failed status.', event)
                else:
                    LOG.info('Nova event response: %s.', event)
            if response_error:
                LOG.error('Error response returned from nova: %s.', response)
                return False
        return True

    def update_server_volume(self, context, server_id, src_volid,
                             new_volume_id):
        nova = novaclient(context, privileged_user=True)
        nova.volumes.update_server_volume(server_id,
                                          src_volid,
                                          new_volume_id)

    def create_volume_snapshot(self, context, volume_id, create_info):
        nova = novaclient(context, privileged_user=True)

        # pylint: disable=E1101
        nova.assisted_volume_snapshots.create(
            volume_id,
            create_info=create_info)

    def delete_volume_snapshot(self, context, snapshot_id, delete_info):
        nova = novaclient(context, privileged_user=True)

        # pylint: disable=E1101
        nova.assisted_volume_snapshots.delete(
            snapshot_id,
            delete_info=delete_info)

    def get_server(self, context, server_id, privileged_user=False,
                   timeout=None):
        try:
            return novaclient(context, privileged_user=privileged_user,
                              timeout=timeout).servers.get(server_id)
        except nova_exceptions.NotFound:
            raise exception.ServerNotFound(uuid=server_id)
        except request_exceptions.Timeout:
            raise exception.APITimeout(service='Nova')

    def extend_volume(self, context, server_ids, volume_id):
        api_version = '2.51'
        events = [self._get_volume_extended_event(server_id, volume_id)
                  for server_id in server_ids]
        result = self._send_events(context, events, api_version=api_version)
        if not result:
            self.message_api.create(
                context,
                message_field.Action.EXTEND_VOLUME,
                resource_uuid=volume_id,
                detail=message_field.Detail.NOTIFY_COMPUTE_SERVICE_FAILED)
        return result

    def reimage_volume(self, context, server_ids, volume_id):
        api_version = '2.93'
        events = [self._get_volume_reimaged_event(server_id, volume_id)
                  for server_id in server_ids]
        result = self._send_events(context, events, api_version=api_version)
        if not result:
            self.message_api.create(
                context,
                message_field.Action.REIMAGE_VOLUME,
                resource_uuid=volume_id,
                detail=message_field.Detail.REIMAGE_VOLUME_FAILED)
        return result

    @staticmethod
    def get_server_volume(context, server_id, volume_id):
        # Use microversion that includes attachment_id
        nova = novaclient(context, api_version='2.89')
        return nova.volumes.get_server_volume(server_id, volume_id)
