# -*- coding: utf-8 -*-
"""
Module for handling openstack glance calls.

:optdepends:    - glanceclient Python adapter
:configuration: This module is not usable until the following are specified
    either in a pillar or in the minion's config file::

        keystone.user: admin
        keystone.password: verybadpass
        keystone.tenant: admin
        keystone.insecure: False   #(optional)
        keystone.auth_url: 'http://127.0.0.1:5000/v2.0/'

    If configuration for multiple openstack accounts is required, they can be
    set up as different configuration profiles:
    For example::

        openstack1:
          keystone.user: admin
          keystone.password: verybadpass
          keystone.tenant: admin
          keystone.auth_url: 'http://127.0.0.1:5000/v2.0/'

        openstack2:
          keystone.user: admin
          keystone.password: verybadpass
          keystone.tenant: admin
          keystone.auth_url: 'http://127.0.0.2:5000/v2.0/'

    With this configuration in place, any of the glance functions can
    make use of a configuration profile by declaring it explicitly.
    For example::

        salt '*' glance.image_list profile=openstack1
"""

# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals

import logging
import pprint
import re

# Import salt libs
from salt.exceptions import SaltInvocationError
from salt.ext import six

# pylint: disable=import-error
HAS_GLANCE = False
try:
    from glanceclient import client
    from glanceclient import exc

    HAS_GLANCE = True
except ImportError:
    pass

# Workaround, as the Glance API v2 requires you to
# already have a keystone session token
HAS_KEYSTONE = False
try:
    from keystoneclient.v2_0 import client as kstone

    # import keystoneclient.apiclient.exceptions as kstone_exc
    HAS_KEYSTONE = True
except ImportError:
    pass


logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)


def __virtual__():
    """
    Only load this module if glance
    is installed on this minion.
    """
    if HAS_GLANCE:
        return "glance"
    return (
        False,
        "The glance execution module cannot be loaded: the glanceclient python library is not available.",
    )


__opts__ = {}


def _auth(profile=None, api_version=2, **connection_args):
    """
    Set up glance credentials, returns
    `glanceclient.client.Client`. Optional parameter
    "api_version" defaults to 2.

    Only intended to be used within glance-enabled modules
    """
    __utils__["versions.warn_until"](
        "Aluminium",
        (
            "The glance module has been deprecated and will be removed in {version}.  "
            "Please update to using the glanceng module"
        ),
    )

    if profile:
        prefix = profile + ":keystone."
    else:
        prefix = "keystone."

    def get(key, default=None):
        """
        Checks connection_args, then salt-minion config,
        falls back to specified default value.
        """
        return connection_args.get(
            "connection_" + key, __salt__["config.get"](prefix + key, default)
        )

    user = get("user", "admin")
    password = get("password", None)
    tenant = get("tenant", "admin")
    tenant_id = get("tenant_id")
    auth_url = get("auth_url", "http://127.0.0.1:35357/v2.0")
    insecure = get("insecure", False)
    admin_token = get("token")
    region = get("region")
    ks_endpoint = get("endpoint", "http://127.0.0.1:9292/")
    g_endpoint_url = __salt__["keystone.endpoint_get"]("glance", profile)
    # The trailing 'v2' causes URLs like thise one:
    # http://127.0.0.1:9292/v2/v1/images
    g_endpoint_url = re.sub("/v2", "", g_endpoint_url["internalurl"])

    if admin_token and api_version != 1 and not password:
        # If we had a password we could just
        # ignore the admin-token and move on...
        raise SaltInvocationError(
            "Only can use keystone admin token " + "with Glance API v1"
        )
    elif password:
        # Can't use the admin-token anyway
        kwargs = {
            "username": user,
            "password": password,
            "tenant_id": tenant_id,
            "auth_url": auth_url,
            "endpoint_url": g_endpoint_url,
            "region_name": region,
            "tenant_name": tenant,
        }
        # 'insecure' keyword not supported by all v2.0 keystone clients
        #   this ensures it's only passed in when defined
        if insecure:
            kwargs["insecure"] = True
    elif api_version == 1 and admin_token:
        kwargs = {
            "token": admin_token,
            "auth_url": auth_url,
            "endpoint_url": g_endpoint_url,
        }
    else:
        raise SaltInvocationError("No credentials to authenticate with.")

    if HAS_KEYSTONE:
        log.debug(
            "Calling keystoneclient.v2_0.client.Client(%s, **%s)", ks_endpoint, kwargs
        )
        keystone = kstone.Client(**kwargs)
        kwargs["token"] = keystone.get_token(keystone.session)
        # This doesn't realy prevent the password to show up
        # in the minion log as keystoneclient.session is
        # logging it anyway when in debug-mode
        kwargs.pop("password")
        log.debug(
            "Calling glanceclient.client.Client(%s, %s, **%s)",
            api_version,
            g_endpoint_url,
            kwargs,
        )
        # may raise exc.HTTPUnauthorized, exc.HTTPNotFound
        # but we deal with those elsewhere
        return client.Client(api_version, g_endpoint_url, **kwargs)
    else:
        raise NotImplementedError("Can't retrieve a auth_token without keystone")


def _add_image(collection, image):
    """
    Add image to given dictionary
    """
    image_prep = {
        "id": image.id,
        "name": image.name,
        "created_at": image.created_at,
        "file": image.file,
        "min_disk": image.min_disk,
        "min_ram": image.min_ram,
        "owner": image.owner,
        "protected": image.protected,
        "status": image.status,
        "tags": image.tags,
        "updated_at": image.updated_at,
        "visibility": image.visibility,
    }
    # Those cause AttributeErrors in Icehouse' glanceclient
    for attr in ["container_format", "disk_format", "size"]:
        if attr in image:
            image_prep[attr] = image[attr]
    if type(collection) is dict:
        collection[image.name] = image_prep
    elif type(collection) is list:
        collection.append(image_prep)
    else:
        msg = (
            '"collection" is {0}'.format(type(collection)) + "instead of dict or list."
        )
        log.error(msg)
        raise TypeError(msg)
    return collection


def image_create(
    name,
    location=None,
    profile=None,
    visibility=None,
    container_format="bare",
    disk_format="raw",
    protected=None,
):
    """
    Create an image (glance image-create)

    CLI Example, old format:

    .. code-block:: bash

        salt '*' glance.image_create name=f16-jeos \\
                 disk_format=qcow2 container_format=ovf

    CLI Example, new format resembling Glance API v2:

    .. code-block:: bash

        salt '*' glance.image_create name=f16-jeos visibility=public \\
                 disk_format=qcow2 container_format=ovf

    The parameter 'visibility' defaults to 'public' if not specified.
    """
    kwargs = {}
    # valid options for "visibility":
    v_list = ["public", "private"]
    # valid options for "container_format":
    cf_list = ["ami", "ari", "aki", "bare", "ovf"]
    # valid options for "disk_format":
    df_list = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vdi", "iso"]

    kwargs["copy_from"] = location
    if visibility is not None:
        if visibility not in v_list:
            raise SaltInvocationError(
                '"visibility" needs to be one '
                + "of the following: {0}".format(", ".join(v_list))
            )
        elif visibility == "public":
            kwargs["is_public"] = True
        else:
            kwargs["is_public"] = False
    else:
        kwargs["is_public"] = True
    if container_format not in cf_list:
        raise SaltInvocationError(
            '"container_format" needs to be '
            + "one of the following: {0}".format(", ".join(cf_list))
        )
    else:
        kwargs["container_format"] = container_format
    if disk_format not in df_list:
        raise SaltInvocationError(
            '"disk_format" needs to be one '
            + "of the following: {0}".format(", ".join(df_list))
        )
    else:
        kwargs["disk_format"] = disk_format
    if protected is not None:
        kwargs["protected"] = protected
    # Icehouse's glanceclient doesn't have add_location() and
    # glanceclient.v2 doesn't implement Client.images.create()
    # in a usable fashion. Thus we have to use v1 for now.
    g_client = _auth(profile, api_version=1)
    image = g_client.images.create(name=name, **kwargs)
    return image_show(image.id, profile=profile)


def image_delete(id=None, name=None, profile=None):  # pylint: disable=C0103
    """
    Delete an image (glance image-delete)

    CLI Examples:

    .. code-block:: bash

        salt '*' glance.image_delete c2eb2eb0-53e1-4a80-b990-8ec887eae7df
        salt '*' glance.image_delete id=c2eb2eb0-53e1-4a80-b990-8ec887eae7df
        salt '*' glance.image_delete name=f16-jeos
    """
    g_client = _auth(profile)
    image = {"id": False, "name": None}
    if name:
        for image in g_client.images.list():
            if image.name == name:
                id = image.id  # pylint: disable=C0103
                continue
    if not id:
        return {
            "result": False,
            "comment": "Unable to resolve image id " "for name {0}".format(name),
        }
    elif not name:
        name = image["name"]
    try:
        g_client.images.delete(id)
    except exc.HTTPNotFound:
        return {"result": False, "comment": "No image with ID {0}".format(id)}
    except exc.HTTPForbidden as forbidden:
        log.error(six.text_type(forbidden))
        return {"result": False, "comment": six.text_type(forbidden)}
    return {
        "result": True,
        "comment": "Deleted image '{0}' ({1}).".format(name, id),
    }


def image_show(id=None, name=None, profile=None):  # pylint: disable=C0103
    """
    Return details about a specific image (glance image-show)

    CLI Example:

    .. code-block:: bash

        salt '*' glance.image_show
    """
    g_client = _auth(profile)
    ret = {}
    if name:
        for image in g_client.images.list():
            if image.name == name:
                id = image.id  # pylint: disable=C0103
                continue
    if not id:
        return {
            "result": False,
            "comment": "Unable to resolve image ID " "for name '{0}'".format(name),
        }
    try:
        image = g_client.images.get(id)
    except exc.HTTPNotFound:
        return {"result": False, "comment": "No image with ID {0}".format(id)}
    pformat = pprint.PrettyPrinter(indent=4).pformat
    log.debug(
        "Properties of image %s:\n%s",
        image.name,
        pprint.PrettyPrinter(indent=4).pformat(image),
    )

    schema = image_schema(profile=profile)
    if len(schema.keys()) == 1:
        schema = schema["image"]
    for key in schema:
        if key in image:
            ret[key] = image[key]
    return ret


def image_list(id=None, profile=None, name=None):  # pylint: disable=C0103
    """
    Return a list of available images (glance image-list)

    CLI Example:

    .. code-block:: bash

        salt '*' glance.image_list
    """
    g_client = _auth(profile)
    ret = []
    for image in g_client.images.list():
        if id is None and name is None:
            _add_image(ret, image)
        else:
            if id is not None and id == image.id:
                _add_image(ret, image)
                return ret
            if name == image.name:
                if name in ret and __salt__["salt_version.less_than"]("Boron"):
                    # Not really worth an exception
                    return {
                        "result": False,
                        "comment": "More than one image with "
                        'name "{0}"'.format(name),
                    }
                _add_image(ret, image)
    log.debug("Returning images: %s", ret)
    return ret


def image_schema(profile=None):
    """
    Returns names and descriptions of the schema "image"'s
    properties for this profile's instance of glance

    CLI Example:

    .. code-block:: bash

        salt '*' glance.image_schema
    """
    return schema_get("image", profile)


def image_update(id=None, name=None, profile=None, **kwargs):  # pylint: disable=C0103
    """
    Update properties of given image.
    Known to work for:
    - min_ram (in MB)
    - protected (bool)
    - visibility ('public' or 'private')

    CLI Example:

    .. code-block:: bash

        salt '*' glance.image_update id=c2eb2eb0-53e1-4a80-b990-8ec887eae7df
        salt '*' glance.image_update name=f16-jeos
    """
    if id:
        image = image_show(id=id, profile=profile)
        if "result" in image and not image["result"]:
            return image
        elif len(image) == 1:
            image = next(iter(image.values()))
    elif name:
        img_list = image_list(name=name, profile=profile)
        if img_list is dict and "result" in img_list:
            return img_list
        elif not img_list:
            return {
                "result": False,
                "comment": "No image with name '{0}' " "found.".format(name),
            }
        elif len(img_list) == 1:
            try:
                image = img_list[0]
            except KeyError:
                image = img_list[name]
    else:
        raise SaltInvocationError
    log.debug("Found image:\n%s", image)
    to_update = {}
    for key, value in kwargs.items():
        if key.startswith("_"):
            continue
        if key not in image or image[key] != value:
            log.debug("add <%s=%s> to to_update", key, value)
            to_update[key] = value
    g_client = _auth(profile)
    updated = g_client.images.update(image["id"], **to_update)
    return updated


def schema_get(name, profile=None):
    """
    Known valid names of schemas are:
      - image
      - images
      - member
      - members

    CLI Example:

    .. code-block:: bash

        salt '*' glance.schema_get name=f16-jeos
    """
    g_client = _auth(profile)
    schema_props = {}
    for prop in g_client.schemas.get(name).properties:
        schema_props[prop.name] = prop.description
    log.debug(
        "Properties of schema %s:\n%s",
        name,
        pprint.PrettyPrinter(indent=4).pformat(schema_props),
    )
    return {name: schema_props}


def _item_list(profile=None):
    """
    Template for writing list functions
    Return a list of available items (glance items-list)

    CLI Example:

    .. code-block:: bash

        salt '*' glance.item_list
    """
    g_client = _auth(profile)
    ret = []
    for item in g_client.items.list():
        ret.append(item.__dict__)
        # ret[item.name] = {
        #        'name': item.name,
        #    }
    return ret


# The following is a list of functions that need to be incorporated in the
# glance module. This list should be updated as functions are added.

# image-download      Download a specific image.
# member-create       Share a specific image with a tenant.
# member-delete       Remove a shared image from a tenant.
# member-list         Describe sharing permissions by image or tenant.
