#
#    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.
"""Utilities for Resources that use the OpenStack Nova API."""

import email
from email.mime import multipart
from email.mime import text
import json
from novaclient import exceptions as nova_exceptions
import os
import pkgutil
import string

from oslo.config import cfg
import six
from six.moves.urllib import parse as urlparse
import warnings

from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LW
from heat.engine import scheduler
from heat.openstack.common import log as logging

LOG = logging.getLogger(__name__)


deferred_server_statuses = ['BUILD',
                            'HARD_REBOOT',
                            'PASSWORD',
                            'REBOOT',
                            'RESCUE',
                            'RESIZE',
                            'REVERT_RESIZE',
                            'SHUTOFF',
                            'SUSPENDED',
                            'VERIFY_RESIZE']


def refresh_server(server):
    '''
    Refresh server's attributes and log warnings for non-critical API errors.
    '''
    warnings.warn('nova_utils.refresh_server is deprecated. '
                  'Use self.client_plugin("nova").refresh_server')
    try:
        server.get()
    except nova_exceptions.OverLimit as exc:
        LOG.warn(_LW("Server %(name)s (%(id)s) received an OverLimit "
                     "response during server.get(): %(exception)s"),
                 {'name': server.name,
                  'id': server.id,
                  'exception': exc})
    except nova_exceptions.ClientException as exc:
        http_status = (getattr(exc, 'http_status', None) or
                       getattr(exc, 'code', None))
        if http_status in (500, 503):
            LOG.warn(_LW('Server "%(name)s" (%(id)s) received the following '
                         'exception during server.get(): %(exception)s'),
                     {'name': server.name,
                      'id': server.id,
                      'exception': exc})
        else:
            raise


def get_ip(server, net_type, ip_version):
    """Return the server's IP of the given type and version."""
    warnings.warn('nova_utils.get_ip is deprecated. '
                  'Use self.client_plugin("nova").get_ip')
    if net_type in server.addresses:
        for ip in server.addresses[net_type]:
            if ip['version'] == ip_version:
                return ip['addr']


def get_flavor_id(nova_client, flavor):
    warnings.warn('nova_utils.get_flavor_id is deprecated. '
                  'Use self.client_plugin("nova").get_flavor_id')
    '''
    Get the id for the specified flavor name.
    If the specified value is flavor id, just return it.

    :param nova_client: the nova client to use
    :param flavor: the name of the flavor to find
    :returns: the id of :flavor:
    :raises: exception.FlavorMissing
    '''
    flavor_id = None
    flavor_list = nova_client.flavors.list()
    for o in flavor_list:
        if o.name == flavor:
            flavor_id = o.id
            break
        if o.id == flavor:
            flavor_id = o.id
            break
    if flavor_id is None:
        raise exception.FlavorMissing(flavor_id=flavor)
    return flavor_id


def get_keypair(nova_client, key_name):
    warnings.warn('nova_utils.get_keypair is deprecated. '
                  'Use self.client_plugin("nova").get_keypair')
    '''
    Get the public key specified by :key_name:

    :param nova_client: the nova client to use
    :param key_name: the name of the key to look for
    :returns: the keypair (name, public_key) for :key_name:
    :raises: exception.UserKeyPairMissing
    '''
    try:
        return nova_client.keypairs.get(key_name)
    except nova_exceptions.NotFound:
        raise exception.UserKeyPairMissing(key_name=key_name)


def build_userdata(resource, userdata=None, instance_user=None,
                   user_data_format='HEAT_CFNTOOLS'):
    warnings.warn('nova_utils.build_userdata is deprecated. '
                  'Use self.client_plugin("nova").build_userdata')
    '''
    Build multipart data blob for CloudInit which includes user-supplied
    Metadata, user data, and the required Heat in-instance configuration.

    :param resource: the resource implementation
    :type resource: heat.engine.Resource
    :param userdata: user data string
    :type userdata: str or None
    :param instance_user: the user to create on the server
    :type instance_user: string
    :param user_data_format: Format of user data to return
    :type user_data_format: string
    :returns: multipart mime as a string
    '''

    if user_data_format == 'RAW':
        return userdata

    is_cfntools = user_data_format == 'HEAT_CFNTOOLS'
    is_software_config = user_data_format == 'SOFTWARE_CONFIG'

    def make_subpart(content, filename, subtype=None):
        if subtype is None:
            subtype = os.path.splitext(filename)[0]
        msg = text.MIMEText(content, _subtype=subtype)
        msg.add_header('Content-Disposition', 'attachment',
                       filename=filename)
        return msg

    def read_cloudinit_file(fn):
        return pkgutil.get_data('heat', 'cloudinit/%s' % fn)

    if instance_user:
        config_custom_user = 'user: %s' % instance_user
        # FIXME(shadower): compatibility workaround for cloud-init 0.6.3. We
        # can drop this once we stop supporting 0.6.3 (which ships with Ubuntu
        # 12.04 LTS).
        #
        # See bug https://bugs.launchpad.net/heat/+bug/1257410
        boothook_custom_user = r"""useradd -m %s
echo -e '%s\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
""" % (instance_user, instance_user)
    else:
        config_custom_user = ''
        boothook_custom_user = ''

    cloudinit_config = string.Template(
        read_cloudinit_file('config')).safe_substitute(
            add_custom_user=config_custom_user)
    cloudinit_boothook = string.Template(
        read_cloudinit_file('boothook.sh')).safe_substitute(
            add_custom_user=boothook_custom_user)

    attachments = [(cloudinit_config, 'cloud-config'),
                   (cloudinit_boothook, 'boothook.sh', 'cloud-boothook'),
                   (read_cloudinit_file('part_handler.py'),
                    'part-handler.py')]

    if is_cfntools:
        attachments.append((userdata, 'cfn-userdata', 'x-cfninitdata'))
    elif is_software_config:
        # attempt to parse userdata as a multipart message, and if it
        # is, add each part as an attachment
        userdata_parts = None
        try:
            userdata_parts = email.message_from_string(userdata)
        except Exception:
            pass
        if userdata_parts and userdata_parts.is_multipart():
            for part in userdata_parts.get_payload():
                attachments.append((part.get_payload(),
                                    part.get_filename(),
                                    part.get_content_subtype()))
        else:
            attachments.append((userdata, 'userdata', 'x-shellscript'))

    if is_cfntools:
        attachments.append((read_cloudinit_file('loguserdata.py'),
                           'loguserdata.py', 'x-shellscript'))

    metadata = resource.metadata_get()
    if metadata:
        attachments.append((json.dumps(metadata),
                            'cfn-init-data', 'x-cfninitdata'))

    attachments.append((cfg.CONF.heat_watch_server_url,
                        'cfn-watch-server', 'x-cfninitdata'))

    if is_cfntools:
        attachments.append((cfg.CONF.heat_metadata_server_url,
                            'cfn-metadata-server', 'x-cfninitdata'))

        # Create a boto config which the cfntools on the host use to know
        # where the cfn and cw API's are to be accessed
        cfn_url = urlparse.urlparse(cfg.CONF.heat_metadata_server_url)
        cw_url = urlparse.urlparse(cfg.CONF.heat_watch_server_url)
        is_secure = cfg.CONF.instance_connection_is_secure
        vcerts = cfg.CONF.instance_connection_https_validate_certificates
        boto_cfg = "\n".join(["[Boto]",
                              "debug = 0",
                              "is_secure = %s" % is_secure,
                              "https_validate_certificates = %s" % vcerts,
                              "cfn_region_name = heat",
                              "cfn_region_endpoint = %s" %
                              cfn_url.hostname,
                              "cloudwatch_region_name = heat",
                              "cloudwatch_region_endpoint = %s" %
                              cw_url.hostname])
        attachments.append((boto_cfg,
                            'cfn-boto-cfg', 'x-cfninitdata'))

    subparts = [make_subpart(*args) for args in attachments]
    mime_blob = multipart.MIMEMultipart(_subparts=subparts)

    return mime_blob.as_string()


def delete_server(server):
    '''
    A co-routine that deletes the server and waits for it to
    disappear from Nova.
    '''
    warnings.warn('nova_utils.delete_server is deprecated. '
                  'Use self.client_plugin("nova").delete_server')
    if not server:
        return
    try:
        server.delete()
    except nova_exceptions.NotFound:
        return

    while True:
        yield

        try:
            refresh_server(server)
        except nova_exceptions.NotFound:
            break
        else:
            # Some clouds append extra (STATUS) strings to the status
            short_server_status = server.status.split('(')[0]
            if short_server_status == "DELETED":
                break
            if short_server_status == "ERROR":
                fault = getattr(server, 'fault', {})
                message = fault.get('message', 'Unknown')
                code = fault.get('code')
                errmsg = (_("Server %(name)s delete failed: (%(code)s) "
                            "%(message)s"))
                raise exception.Error(errmsg % {"name": server.name,
                                                "code": code,
                                                "message": message})


@scheduler.wrappertask
def resize(server, flavor, flavor_id):
    """Resize the server and then call check_resize task to verify."""
    warnings.warn('nova_utils.resize is deprecated. '
                  'Use self.client_plugin("nova").resize')
    server.resize(flavor_id)
    yield check_resize(server, flavor, flavor_id)


def rename(server, name):
    """Update the name for a server."""
    warnings.warn('nova_utils.rename is deprecated. '
                  'Use self.client_plugin("nova").rename')
    server.update(name)


def check_resize(server, flavor, flavor_id):
    """
    Verify that a resizing server is properly resized.
    If that's the case, confirm the resize, if not raise an error.
    """
    warnings.warn('nova_utils.check_resize is deprecated. '
                  'Use self.client_plugin("nova").check_resize')
    refresh_server(server)
    while server.status == 'RESIZE':
        yield
        refresh_server(server)
    if server.status == 'VERIFY_RESIZE':
        server.confirm_resize()
    else:
        raise exception.Error(
            _("Resizing to '%(flavor)s' failed, status '%(status)s'") %
            dict(flavor=flavor, status=server.status))


@scheduler.wrappertask
def rebuild(server, image_id, preserve_ephemeral=False):
    """Rebuild the server and call check_rebuild to verify."""
    warnings.warn('nova_utils.rebuild is deprecated. '
                  'Use self.client_plugin("nova").rebuild')
    server.rebuild(image_id, preserve_ephemeral=preserve_ephemeral)
    yield check_rebuild(server, image_id)


def check_rebuild(server, image_id):
    """
    Verify that a rebuilding server is rebuilt.
    Raise error if it ends up in an ERROR state.
    """
    warnings.warn('nova_utils.check_rebuild is deprecated. '
                  'Use self.client_plugin("nova").check_rebuild')
    refresh_server(server)
    while server.status == 'REBUILD':
        yield
        refresh_server(server)
    if server.status == 'ERROR':
        raise exception.Error(
            _("Rebuilding server failed, status '%s'") % server.status)


def meta_serialize(metadata):
    """
    Serialize non-string metadata values before sending them to
    Nova.
    """
    warnings.warn('nova_utils.meta_serialize is deprecated. '
                  'Use self.client_plugin("nova").meta_serialize')
    return dict((key, (value if isinstance(value,
                                           six.string_types)
                       else json.dumps(value))
                 ) for (key, value) in metadata.items())


def meta_update(client, server, metadata):
    """Delete/Add the metadata in nova as needed."""
    warnings.warn('nova_utils.meta_update is deprecated. '
                  'Use self.client_plugin("nova").meta_update')
    metadata = meta_serialize(metadata)
    current_md = server.metadata
    to_del = [key for key in current_md.keys() if key not in metadata]
    if len(to_del) > 0:
        client.servers.delete_meta(server, to_del)

    client.servers.set_meta(server, metadata)


def server_to_ipaddress(client, server):
    '''
    Return the server's IP address, fetching it from Nova.
    '''
    warnings.warn('nova_utils.server_to_ipaddress is deprecated. '
                  'Use self.client_plugin("nova").server_to_ipaddress')
    try:
        server = client.servers.get(server)
    except nova_exceptions.NotFound as ex:
        LOG.warn(_LW('Instance (%(server)s) not found: %(ex)s'),
                 {'server': server, 'ex': ex})
    else:
        for n in server.networks:
            if len(server.networks[n]) > 0:
                return server.networks[n][0]


def absolute_limits(nova_client):
    """Return the absolute limits as a dictionary."""
    warnings.warn('nova_utils.absolute_limits is deprecated. '
                  'Use self.client_plugin("nova").absolute_limits')
    limits = nova_client.limits.get()
    return dict([(limit.name, limit.value) for limit in list(limits.absolute)])
