# Copyright 2014 Hewlett-Packard Development Company, L.P.
# 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 keystoneauth1 import discover as ks_disc
from keystoneauth1 import loading as ks_loading
from oslo_log import log as logging
from oslo_utils import importutils

import nova.conf
from nova import exception
from nova.i18n import _
from nova import utils


LOG = logging.getLogger(__name__)
CONF = nova.conf.CONF

ironic = None

IRONIC_GROUP = nova.conf.ironic.ironic_group

# The API version required by the Ironic driver
IRONIC_API_VERSION = (1, 46)
# NOTE(TheJulia): This version should ALWAYS be the _last_ release
# supported version of the API version used by nova. If a feature
# needs a higher version to be negotiated to operate properly, then the version
# above should be updated, and this version should only be changed
# once a cycle to the API version desired for features merging in
# that cycle.
PRIOR_IRONIC_API_VERSION = (1, 38)


class IronicClientWrapper(object):
    """Ironic client wrapper class that encapsulates authentication logic."""

    def __init__(self):
        """Initialise the IronicClientWrapper for use.

        Initialise IronicClientWrapper by loading ironicclient
        dynamically so that ironicclient is not a dependency for
        Nova.
        """
        global ironic
        if ironic is None:
            ironic = importutils.import_module('ironicclient')
            # NOTE(deva): work around a lack of symbols in the current version.
            if not hasattr(ironic, 'exc'):
                ironic.exc = importutils.import_module('ironicclient.exc')
            if not hasattr(ironic, 'client'):
                ironic.client = importutils.import_module(
                                                    'ironicclient.client')
        self._cached_client = None

    def _get_auth_plugin(self):
        """Load an auth plugin from CONF options."""
        # If an auth plugin name is defined in `auth_type` option of [ironic]
        # group, register its options and load it.
        auth_plugin = ks_loading.load_auth_from_conf_options(CONF,
                                                             IRONIC_GROUP.name)

        return auth_plugin

    def _get_client(self, retry_on_conflict=True):
        max_retries = CONF.ironic.api_max_retries if retry_on_conflict else 1
        retry_interval = (CONF.ironic.api_retry_interval
                          if retry_on_conflict else 0)

        # If we've already constructed a valid, authed client, just return
        # that.
        if retry_on_conflict and self._cached_client is not None:
            return self._cached_client

        auth_plugin = self._get_auth_plugin()

        sess = ks_loading.load_session_from_conf_options(CONF,
                                                         IRONIC_GROUP.name,
                                                         auth=auth_plugin)

        # Retries for Conflict exception
        kwargs = {}
        kwargs['max_retries'] = max_retries
        kwargs['retry_interval'] = retry_interval
        # NOTE(TheJulia): The ability for a list of available versions to be
        # accepted was added in python-ironicclient 2.2.0. The highest
        # available version will be utilized by the client for the lifetime
        # of the client.
        kwargs['os_ironic_api_version'] = [
            '%d.%d' % IRONIC_API_VERSION, '%d.%d' % PRIOR_IRONIC_API_VERSION]

        ironic_conf = CONF[IRONIC_GROUP.name]
        # valid_interfaces is a list. ironicclient passes this kwarg through to
        # ksa, which is set up to handle 'interface' as either a list or a
        # single value.
        kwargs['interface'] = ironic_conf.valid_interfaces

        # NOTE(clenimar/efried): by default, the endpoint is taken from the
        # service catalog. Use `endpoint_override` if you want to override it.
        try:
            ksa_adap = utils.get_ksa_adapter(
                nova.conf.ironic.DEFAULT_SERVICE_TYPE,
                ksa_auth=auth_plugin, ksa_session=sess,
                min_version=(IRONIC_API_VERSION[0], 0),
                max_version=(IRONIC_API_VERSION[0], ks_disc.LATEST))
            ironic_url = ksa_adap.get_endpoint()
            ironic_url_none_reason = 'returned None'
        except exception.ServiceNotFound:
            # NOTE(efried): No reason to believe service catalog lookup
            # won't also fail in ironic client init, but this way will
            # yield the expected exception/behavior.
            ironic_url = None
            ironic_url_none_reason = 'raised ServiceNotFound'

        if ironic_url is None:
            LOG.warning("Could not discover ironic_url via keystoneauth1: "
                        "Adapter.get_endpoint %s", ironic_url_none_reason)
            # NOTE(eandersson): We pass in region here to make sure
            # that the Ironic client can make an educated decision when
            # we don't have a valid endpoint to pass on.
            kwargs['region_name'] = ironic_conf.region_name

        try:
            cli = ironic.client.get_client(IRONIC_API_VERSION[0],
                                           endpoint=ironic_url,
                                           session=sess, **kwargs)
            # Cache the client so we don't have to reconstruct and
            # reauthenticate it every time we need it.
            if retry_on_conflict:
                self._cached_client = cli

        except ironic.exc.Unauthorized:
            msg = _("Unable to authenticate Ironic client.")
            LOG.error(msg)
            raise exception.NovaException(msg)

        return cli

    def _multi_getattr(self, obj, attr):
        """Support nested attribute path for getattr().

        :param obj: Root object.
        :param attr: Path of final attribute to get. E.g., "a.b.c.d"

        :returns: The value of the final named attribute.
        :raises: AttributeError will be raised if the path is invalid.
        """
        for attribute in attr.split("."):
            obj = getattr(obj, attribute)
        return obj

    def call(self, method, *args, **kwargs):
        """Call an Ironic client method and retry on stale token.

        :param method: Name of the client method to call as a string.
        :param args: Client method arguments.
        :param kwargs: Client method keyword arguments.
        :param retry_on_conflict: Boolean value. Whether the request should be
                                  retried in case of a conflict error
                                  (HTTP 409) or not. If retry_on_conflict is
                                  False the cached instance of the client
                                  won't be used. Defaults to True.
        """
        retry_on_conflict = kwargs.pop('retry_on_conflict', True)

        # authentication retry for token expiration is handled in keystone
        # session, other retries are handled by ironicclient starting with
        # 0.8.0
        client = self._get_client(retry_on_conflict=retry_on_conflict)
        return self._multi_getattr(client, method)(*args, **kwargs)

    @property
    def current_api_version(self):
        """Value representing the negotiated API client version.

        This value represents the current negotiated API version that
        is being utilized by the client to permit the caller to make
        decisions based upon that version.

        :returns: The highest available negotiatable version or None
                  if a version has not yet been negotiated by the underlying
                  client library.
        """
        return self._get_client().current_api_version

    @property
    def is_api_version_negotiated(self):
        """Boolean to indicate if the client version has been negotiated.

        :returns: True if the underlying client library has completed API
                  version negotiation. Otherwise the value returned is
                  False.
        """
        return self._get_client().is_api_version_negotiated
