#
#    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 os
import pkgutil
import string
import warnings

from novaclient import exceptions as nova_exceptions
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
import six
from six.moves.urllib import parse as urlparse

from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LW
from heat.engine import scheduler

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((jsonutils.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 jsonutils.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)])
