# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
# Copyright (c) 2010 Citrix Systems, Inc.
# Copyright 2011 Ken Pepple
#
#    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.

"""Built-in instance properties."""

import re
import uuid

from oslo.config import cfg
import six

from nova import context
from nova import db
from nova import exception
from nova.openstack.common.db import exception as db_exc
from nova.openstack.common.gettextutils import _
from nova.openstack.common import log as logging
from nova.openstack.common import strutils
from nova.pci import pci_request
from nova import utils

flavor_opts = [
    cfg.StrOpt('default_flavor',
               default='m1.small',
               help='Default flavor to use for the EC2 API only. The Nova API '
               'does not support a default flavor.'),
]

CONF = cfg.CONF
CONF.register_opts(flavor_opts)

LOG = logging.getLogger(__name__)

# NOTE(luisg): Flavor names can include non-ascii characters so that users can
# create flavor names in locales that use them, however flavor IDs are limited
# to ascii characters.
VALID_ID_REGEX = re.compile("^[\w\.\- ]*$")
VALID_NAME_REGEX = re.compile("^[\w\.\- ]*$", re.UNICODE)
# NOTE(dosaboy): This is supposed to represent the maximum value that we can
# place into a SQL single precision float so that we can check whether values
# are oversize. Postgres and MySQL both define this as their max whereas Sqlite
# uses dynamic typing so this would not apply. Different dbs react in different
# ways to oversize values e.g. postgres will raise an exception while mysql
# will round off the value. Nevertheless we may still want to know prior to
# insert whether the value is oversize.
SQL_SP_FLOAT_MAX = 3.40282e+38

# Validate extra specs key names.
VALID_EXTRASPEC_NAME_REGEX = re.compile(r"[\w\.\- :]+$", re.UNICODE)


def _int_or_none(val):
    if val is not None:
        return int(val)


system_metadata_flavor_props = {
    'id': int,
    'name': str,
    'memory_mb': int,
    'vcpus': int,
    'root_gb': int,
    'ephemeral_gb': int,
    'flavorid': str,
    'swap': int,
    'rxtx_factor': float,
    'vcpu_weight': _int_or_none,
    }


def create(name, memory, vcpus, root_gb, ephemeral_gb=0, flavorid=None,
           swap=0, rxtx_factor=1.0, is_public=True):
    """Creates flavors."""
    if not flavorid:
        flavorid = uuid.uuid4()

    kwargs = {
        'memory_mb': memory,
        'vcpus': vcpus,
        'root_gb': root_gb,
        'ephemeral_gb': ephemeral_gb,
        'swap': swap,
        'rxtx_factor': rxtx_factor,
    }

    if isinstance(name, six.string_types):
        name = name.strip()
    # ensure name do not exceed 255 characters
    utils.check_string_length(name, 'name', min_length=1, max_length=255)

    # ensure name does not contain any special characters
    valid_name = VALID_NAME_REGEX.search(name)
    if not valid_name:
        msg = _("Flavor names can only contain alphanumeric characters, "
                "periods, dashes, underscores and spaces.")
        raise exception.InvalidInput(reason=msg)

    # NOTE(vish): Internally, flavorid is stored as a string but it comes
    #             in through json as an integer, so we convert it here.
    flavorid = unicode(flavorid)

    # ensure leading/trailing whitespaces not present.
    if flavorid.strip() != flavorid:
        msg = _("id cannot contain leading and/or trailing whitespace(s)")
        raise exception.InvalidInput(reason=msg)

    # ensure flavor id does not exceed 255 characters
    utils.check_string_length(flavorid, 'id', min_length=1,
                              max_length=255)

    # ensure flavor id does not contain any special characters
    valid_flavor_id = VALID_ID_REGEX.search(flavorid)
    if not valid_flavor_id:
        msg = _("Flavor id can only contain letters from A-Z (both cases), "
                "periods, dashes, underscores and spaces.")
        raise exception.InvalidInput(reason=msg)

    # Some attributes are positive ( > 0) integers
    for option in ['memory_mb', 'vcpus']:
        kwargs[option] = utils.validate_integer(kwargs[option], option, 1,
                                                db.MAX_INT)

    # Some attributes are non-negative ( >= 0) integers
    for option in ['root_gb', 'ephemeral_gb', 'swap']:
        kwargs[option] = utils.validate_integer(kwargs[option], option, 0,
                                                db.MAX_INT)

    # rxtx_factor should be a positive float
    try:
        kwargs['rxtx_factor'] = float(kwargs['rxtx_factor'])
        if (kwargs['rxtx_factor'] <= 0 or
                kwargs['rxtx_factor'] > SQL_SP_FLOAT_MAX):
            raise ValueError()
    except ValueError:
        msg = (_("'rxtx_factor' argument must be a float between 0 and %g") %
               SQL_SP_FLOAT_MAX)
        raise exception.InvalidInput(reason=msg)

    kwargs['name'] = name
    kwargs['flavorid'] = flavorid
    # ensure is_public attribute is boolean
    try:
        kwargs['is_public'] = strutils.bool_from_string(
            is_public, strict=True)
    except ValueError:
        raise exception.InvalidInput(reason=_("is_public must be a boolean"))

    try:
        return db.flavor_create(context.get_admin_context(), kwargs)
    except db_exc.DBError as e:
        LOG.exception(_('DB error: %s') % e)
        raise exception.FlavorCreateFailed()


def destroy(name):
    """Marks flavor as deleted."""
    try:
        if not name:
            raise ValueError()
        db.flavor_destroy(context.get_admin_context(), name)
    except (ValueError, exception.NotFound):
        LOG.exception(_('Instance type %s not found for deletion') % name)
        raise exception.FlavorNotFoundByName(flavor_name=name)


def get_all_flavors(ctxt=None, inactive=False, filters=None):
    """Get all non-deleted flavors as a dict.

    Pass true as argument if you want deleted flavors returned also.
    """
    if ctxt is None:
        ctxt = context.get_admin_context()

    inst_types = db.flavor_get_all(
            ctxt, inactive=inactive, filters=filters)

    inst_type_dict = {}
    for inst_type in inst_types:
        inst_type_dict[inst_type['id']] = inst_type
    return inst_type_dict


def get_all_flavors_sorted_list(ctxt=None, inactive=False, filters=None,
                                sort_key='flavorid', sort_dir='asc',
                                limit=None, marker=None):
    """Get all non-deleted flavors as a sorted list.

    Pass true as argument if you want deleted flavors returned also.
    """
    if ctxt is None:
        ctxt = context.get_admin_context()

    return db.flavor_get_all(ctxt, filters=filters, sort_key=sort_key,
                             sort_dir=sort_dir, limit=limit, marker=marker)


def get_default_flavor():
    """Get the default flavor."""
    name = CONF.default_flavor
    return get_flavor_by_name(name)


def get_flavor(instance_type_id, ctxt=None, inactive=False):
    """Retrieves single flavor by id."""
    if instance_type_id is None:
        return get_default_flavor()

    if ctxt is None:
        ctxt = context.get_admin_context()

    if inactive:
        ctxt = ctxt.elevated(read_deleted="yes")

    return db.flavor_get(ctxt, instance_type_id)


def get_flavor_by_name(name, ctxt=None):
    """Retrieves single flavor by name."""
    if name is None:
        return get_default_flavor()

    if ctxt is None:
        ctxt = context.get_admin_context()

    return db.flavor_get_by_name(ctxt, name)


# TODO(termie): flavor-specific code should probably be in the API that uses
#               flavors.
def get_flavor_by_flavor_id(flavorid, ctxt=None, read_deleted="yes"):
    """Retrieve flavor by flavorid.

    :raises: FlavorNotFound
    """
    if ctxt is None:
        ctxt = context.get_admin_context(read_deleted=read_deleted)

    return db.flavor_get_by_flavor_id(ctxt, flavorid, read_deleted)


def get_flavor_access_by_flavor_id(flavorid, ctxt=None):
    """Retrieve flavor access list by flavor id."""
    if ctxt is None:
        ctxt = context.get_admin_context()

    return db.flavor_access_get_by_flavor_id(ctxt, flavorid)


def add_flavor_access(flavorid, projectid, ctxt=None):
    """Add flavor access for project."""
    if ctxt is None:
        ctxt = context.get_admin_context()

    return db.flavor_access_add(ctxt, flavorid, projectid)


def remove_flavor_access(flavorid, projectid, ctxt=None):
    """Remove flavor access for project."""
    if ctxt is None:
        ctxt = context.get_admin_context()

    return db.flavor_access_remove(ctxt, flavorid, projectid)


def extract_flavor(instance, prefix=''):
    """Create an InstanceType-like object from instance's system_metadata
    information.
    """

    instance_type = {}
    sys_meta = utils.instance_sys_meta(instance)
    for key, type_fn in system_metadata_flavor_props.items():
        type_key = '%sinstance_type_%s' % (prefix, key)
        instance_type[key] = type_fn(sys_meta[type_key])
    return instance_type


def save_flavor_info(metadata, instance_type, prefix=''):
    """Save properties from instance_type into instance's system_metadata,
    in the format of:

      [prefix]instance_type_[key]

    This can be used to update system_metadata in place from a type, as well
    as stash information about another instance_type for later use (such as
    during resize).
    """

    for key in system_metadata_flavor_props.keys():
        to_key = '%sinstance_type_%s' % (prefix, key)
        metadata[to_key] = instance_type[key]
    pci_request.save_flavor_pci_info(metadata, instance_type, prefix)
    return metadata


def delete_flavor_info(metadata, *prefixes):
    """Delete flavor instance_type information from instance's system_metadata
    by prefix.
    """

    for key in system_metadata_flavor_props.keys():
        for prefix in prefixes:
            to_key = '%sinstance_type_%s' % (prefix, key)
            del metadata[to_key]
    pci_request.delete_flavor_pci_info(metadata, *prefixes)
    return metadata


def validate_extra_spec_keys(key_names_list):
    for key_name in key_names_list:
        if not VALID_EXTRASPEC_NAME_REGEX.match(key_name):
            expl = _('Key Names can only contain alphanumeric characters, '
                     'periods, dashes, underscores, colons and spaces.')
            raise exception.InvalidInput(message=expl)
