# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 OpenStack Foundation
# Copyright 2012 Nebula, Inc.
# Copyright (c) 2012 X.commerce, a business unit of eBay Inc.
#
#    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.

import collections
import logging
from operator import attrgetter

from django.conf import settings
from django.utils.translation import ugettext_lazy as _

from novaclient import api_versions
from novaclient import exceptions as nova_exceptions
from novaclient.v2 import instance_action as nova_instance_action
from novaclient.v2 import servers as nova_servers

from horizon import exceptions as horizon_exceptions
from horizon.utils import memoized

from openstack_dashboard.api import _nova
from openstack_dashboard.api import base
from openstack_dashboard.api import cinder
from openstack_dashboard.contrib.developer.profiler import api as profiler
from openstack_dashboard.utils import settings as utils

LOG = logging.getLogger(__name__)

# API static values
INSTANCE_ACTIVE_STATE = 'ACTIVE'
VOLUME_STATE_AVAILABLE = "available"
DEFAULT_QUOTA_NAME = 'default'

# python-novaclient 16.0.0 removed the list_extensions module and the
# GET /extensions API is deprecated since Newton. Furthermore, the ability
# to enable/disable compute API extensions was also removed in Newton.
# Therefore we hard-code the list of extensions here until the
# OPENSTACK_NOVA_EXTENSIONS_BLACKLIST setting is no longer used.
EXTENSIONS = (
    'AccessIPs',
    'AdminActions',
    'AdminPassword',
    'Agents',
    'Aggregates',
    'AssistedVolumeSnapshots',
    'AttachInterfaces',
    'AvailabilityZone',
    'BareMetalExtStatus',
    'BareMetalNodes',
    'BlockDeviceMapping',
    'BlockDeviceMappingV2Boot',
    'CellCapacities',
    'Cells',
    'Certificates',
    'Cloudpipe',
    'CloudpipeUpdate',
    'ConfigDrive',
    'ConsoleAuthTokens',
    'ConsoleOutput',
    'Consoles',
    'CreateBackup',
    'Createserverext',
    'DeferredDelete',
    'DiskConfig',
    'Evacuate',
    'ExtendedAvailabilityZone',
    'ExtendedEvacuateFindHost',
    'ExtendedFloatingIps',
    'ExtendedHypervisors',
    'ExtendedIps',
    'ExtendedIpsMac',
    'ExtendedNetworks',
    'ExtendedQuotas',
    'ExtendedRescueWithImage',
    'ExtendedServerAttributes',
    'ExtendedServices',
    'ExtendedServicesDelete',
    'ExtendedStatus',
    'ExtendedStatus',
    'ExtendedVolumes',
    'FixedIPs',
    'FlavorAccess',
    'FlavorDisabled',
    'FlavorExtraData',
    'FlavorExtraSpecs',
    'FlavorManage',
    'FlavorRxtx',
    'FlavorSwap',
    'FloatingIpDns',
    'FloatingIpPools',
    'FloatingIps',
    'FloatingIpsBulk',
    'Fping',
    'HideServerAddresses',
    'Hosts',
    'HypervisorStatus',
    'Hypervisors',
    'ImageSize',
    'InstanceActions',
    'Keypairs',
    'LockServer',
    'MigrateServer',
    'Migrations',
    'Multinic',
    'MultipleCreate',
    'NetworkAssociationSupport',
    'Networks',
    'OSInstanceUsageAuditLog',
    'OSTenantNetworks',
    'PauseServer',
    'Personality',
    'PreserveEphemeralOnRebuild',
    'QuotaClasses',
    'Quotas',
    'Rescue',
    'SchedulerHints',
    'SecurityGroupDefaultRules',
    'SecurityGroups',
    'ServerDiagnostics',
    'ServerExternalEvents',
    'ServerGroupQuotas',
    'ServerGroups',
    'ServerListMultiStatus',
    'ServerPassword',
    'ServerSortKeys',
    'ServerStartStop',
    'ServerUsage',
    'Services',
    'Shelve',
    'SimpleTenantUsage',
    'SuspendServer',
    'UsedLimits',
    'UsedLimitsForAdmin',
    'UserData',
    'UserQuotas',
    'VirtualInterfaces',
    'VolumeAttachmentUpdate',
    'Volumes'
)


get_microversion = _nova.get_microversion
server_get = _nova.server_get
Server = _nova.Server


def is_feature_available(request, features):
    return bool(get_microversion(request, features))


class VolumeMultiattachNotSupported(horizon_exceptions.HorizonException):
    status_code = 400


class VNCConsole(base.APIDictWrapper):
    """Wrapper for the "console" dictionary.

    Returned by the novaclient.servers.get_vnc_console method.
    """
    _attrs = ['url', 'type']


class SPICEConsole(base.APIDictWrapper):
    """Wrapper for the "console" dictionary.

    Returned by the novaclient.servers.get_spice_console method.
    """
    _attrs = ['url', 'type']


class RDPConsole(base.APIDictWrapper):
    """Wrapper for the "console" dictionary.

    Returned by the novaclient.servers.get_rdp_console method.
    """
    _attrs = ['url', 'type']


class SerialConsole(base.APIDictWrapper):
    """Wrapper for the "console" dictionary.

    Returned by the novaclient.servers.get_serial_console method.
    """
    _attrs = ['url', 'type']


class MKSConsole(base.APIDictWrapper):
    """Wrapper for the "console" dictionary.

    Returned by the novaclient.servers.get_mks_console method.
    """
    _attrs = ['url', 'type']


class Hypervisor(base.APIDictWrapper):
    """Simple wrapper around novaclient.hypervisors.Hypervisor."""

    _attrs = ['manager', '_loaded', '_info', 'hypervisor_hostname', 'id',
              'servers']

    @property
    def servers(self):
        # if hypervisor doesn't have servers, the attribute is not present
        servers = []
        try:
            servers = self._apidict.servers
        except Exception:
            pass

        return servers


class NovaUsage(base.APIResourceWrapper):
    """Simple wrapper around contrib/simple_usage.py."""

    _attrs = ['start', 'server_usages', 'stop', 'tenant_id',
              'total_local_gb_usage', 'total_memory_mb_usage',
              'total_vcpus_usage', 'total_hours']

    def get_summary(self):
        return {'instances': self.total_active_instances,
                'memory_mb': self.memory_mb,
                'vcpus': self.vcpus,
                'vcpu_hours': self.vcpu_hours,
                'local_gb': self.local_gb,
                'disk_gb_hours': self.disk_gb_hours,
                'memory_mb_hours': self.memory_mb_hours}

    @property
    def total_active_instances(self):
        return sum(1 for s in self.server_usages if s['ended_at'] is None)

    @property
    def vcpus(self):
        return sum(s['vcpus'] for s in self.server_usages
                   if s['ended_at'] is None)

    @property
    def vcpu_hours(self):
        return getattr(self, "total_vcpus_usage", 0)

    @property
    def local_gb(self):
        return sum(s['local_gb'] for s in self.server_usages
                   if s['ended_at'] is None)

    @property
    def memory_mb(self):
        return sum(s['memory_mb'] for s in self.server_usages
                   if s['ended_at'] is None)

    @property
    def disk_gb_hours(self):
        return getattr(self, "total_local_gb_usage", 0)

    @property
    def memory_mb_hours(self):
        return getattr(self, "total_memory_mb_usage", 0)


class FlavorExtraSpec(object):
    def __init__(self, flavor_id, key, val):
        self.flavor_id = flavor_id
        self.id = key
        self.key = key
        self.value = val


class QuotaSet(base.QuotaSet):

    # We don't support nova-network, so we exclude nova-network relatd
    # quota fields from the response.
    ignore_quotas = {
        "floating_ips",
        "fixed_ips",
        "security_groups",
        "security_group_rules",
    }


def upgrade_api(request, client, version):
    """Ugrade the nova API to the specified version if possible."""

    min_ver, max_ver = api_versions._get_server_version_range(client)
    if min_ver <= api_versions.APIVersion(version) <= max_ver:
        client = _nova.novaclient(request, version)
    return client


@profiler.trace
def server_vnc_console(request, instance_id, console_type='novnc'):
    nc = _nova.novaclient(request)
    console = nc.servers.get_vnc_console(instance_id, console_type)
    return VNCConsole(console['console'])


@profiler.trace
def server_spice_console(request, instance_id, console_type='spice-html5'):
    nc = _nova.novaclient(request)
    console = nc.servers.get_spice_console(instance_id, console_type)
    return SPICEConsole(console['console'])


@profiler.trace
def server_rdp_console(request, instance_id, console_type='rdp-html5'):
    nc = _nova.novaclient(request)
    console = nc.servers.get_rdp_console(instance_id, console_type)
    return RDPConsole(console['console'])


@profiler.trace
def server_serial_console(request, instance_id, console_type='serial'):
    nc = _nova.novaclient(request)
    console = nc.servers.get_serial_console(instance_id, console_type)
    return SerialConsole(console['console'])


@profiler.trace
def server_mks_console(request, instance_id, console_type='mks'):
    microver = get_microversion(request, "remote_console_mks")
    nc = _nova.novaclient(request, microver)
    console = nc.servers.get_mks_console(instance_id, console_type)
    return MKSConsole(console['remote_console'])


@profiler.trace
def flavor_create(request, name, memory, vcpu, disk, flavorid='auto',
                  ephemeral=0, swap=0, metadata=None, is_public=True,
                  rxtx_factor=1):
    flavor = _nova.novaclient(request).flavors.create(name, memory, vcpu, disk,
                                                      flavorid=flavorid,
                                                      ephemeral=ephemeral,
                                                      swap=swap,
                                                      is_public=is_public,
                                                      rxtx_factor=rxtx_factor)
    if (metadata):
        flavor_extra_set(request, flavor.id, metadata)
    return flavor


@profiler.trace
def flavor_delete(request, flavor_id):
    _nova.novaclient(request).flavors.delete(flavor_id)


@profiler.trace
def flavor_get(request, flavor_id, get_extras=False):
    flavor = _nova.novaclient(request).flavors.get(flavor_id)
    if get_extras:
        flavor.extras = flavor_get_extras(request, flavor.id, True, flavor)
    return flavor


@profiler.trace
@memoized.memoized
def flavor_list(request, is_public=True, get_extras=False):
    """Get the list of available instance sizes (flavors)."""
    flavors = _nova.novaclient(request).flavors.list(is_public=is_public)
    if get_extras:
        for flavor in flavors:
            flavor.extras = flavor_get_extras(request, flavor.id, True, flavor)
    return flavors


@profiler.trace
def update_pagination(entities, page_size, marker, reversed_order=False):
    has_more_data = has_prev_data = False
    if len(entities) > page_size:
        has_more_data = True
        entities.pop()
        if marker is not None:
            has_prev_data = True
    # first page condition when reached via prev back
    elif reversed_order and marker is not None:
        has_more_data = True
    # last page condition
    elif marker is not None:
        has_prev_data = True

    # restore the original ordering here
    if reversed_order:
        entities.reverse()

    return entities, has_more_data, has_prev_data


@profiler.trace
@memoized.memoized
def flavor_list_paged(request, is_public=True, get_extras=False, marker=None,
                      paginate=False, sort_key="name", sort_dir="desc",
                      reversed_order=False):
    """Get the list of available instance sizes (flavors)."""
    has_more_data = False
    has_prev_data = False

    if paginate:
        if reversed_order:
            sort_dir = 'desc' if sort_dir == 'asc' else 'asc'
        page_size = utils.get_page_size(request)
        flavors = _nova.novaclient(request).flavors.list(is_public=is_public,
                                                         marker=marker,
                                                         limit=page_size + 1,
                                                         sort_key=sort_key,
                                                         sort_dir=sort_dir)
        flavors, has_more_data, has_prev_data = update_pagination(
            flavors, page_size, marker, reversed_order)
    else:
        flavors = _nova.novaclient(request).flavors.list(is_public=is_public)

    if get_extras:
        for flavor in flavors:
            flavor.extras = flavor_get_extras(request, flavor.id, True, flavor)

    return (flavors, has_more_data, has_prev_data)


@profiler.trace
@memoized.memoized
def flavor_access_list(request, flavor=None):
    """Get the list of access instance sizes (flavors)."""
    return _nova.novaclient(request).flavor_access.list(flavor=flavor)


@profiler.trace
def add_tenant_to_flavor(request, flavor, tenant):
    """Add a tenant to the given flavor access list."""
    return _nova.novaclient(request).flavor_access.add_tenant_access(
        flavor=flavor, tenant=tenant)


@profiler.trace
def remove_tenant_from_flavor(request, flavor, tenant):
    """Remove a tenant from the given flavor access list."""
    return _nova.novaclient(request).flavor_access.remove_tenant_access(
        flavor=flavor, tenant=tenant)


@profiler.trace
def flavor_get_extras(request, flavor_id, raw=False, flavor=None):
    """Get flavor extra specs."""
    if flavor is None:
        flavor = _nova.novaclient(request).flavors.get(flavor_id)
    extras = flavor.get_keys()
    if raw:
        return extras
    return [FlavorExtraSpec(flavor_id, key, value) for
            key, value in extras.items()]


@profiler.trace
def flavor_extra_delete(request, flavor_id, keys):
    """Unset the flavor extra spec keys."""
    flavor = _nova.novaclient(request).flavors.get(flavor_id)
    return flavor.unset_keys(keys)


@profiler.trace
def flavor_extra_set(request, flavor_id, metadata):
    """Set the flavor extra spec keys."""
    flavor = _nova.novaclient(request).flavors.get(flavor_id)
    if (not metadata):  # not a way to delete keys
        return None
    return flavor.set_keys(metadata)


@profiler.trace
def snapshot_create(request, instance_id, name):
    return _nova.novaclient(request).servers.create_image(instance_id, name)


@profiler.trace
def keypair_create(request, name, key_type='ssh'):
    microversion = get_microversion(request, 'key_types')
    return _nova.novaclient(request, microversion).\
        keypairs.create(name, key_type=key_type)


@profiler.trace
def keypair_import(request, name, public_key, key_type='ssh'):
    microversion = get_microversion(request, 'key_types')
    return _nova.novaclient(request, microversion).\
        keypairs.create(name, public_key, key_type)


@profiler.trace
def keypair_delete(request, name):
    _nova.novaclient(request).keypairs.delete(name)


@profiler.trace
def keypair_list(request):
    microversion = get_microversion(request, 'key_type_list')
    return _nova.novaclient(request, microversion).keypairs.list()


@profiler.trace
def keypair_get(request, name):
    return _nova.novaclient(request).keypairs.get(name)


@profiler.trace
def server_create(request, name, image, flavor, key_name, user_data,
                  security_groups, block_device_mapping=None,
                  block_device_mapping_v2=None, nics=None,
                  availability_zone=None, instance_count=1, admin_pass=None,
                  disk_config=None, config_drive=None, meta=None,
                  scheduler_hints=None, description=None):
    microversion = get_microversion(request, ("multiattach",
                                              "instance_description",
                                              "auto_allocated_network"))
    nova_client = _nova.novaclient(request, version=microversion)

    # NOTE(amotoki): Handling auto allocated network
    # Nova API 2.37 or later, it accepts a special string 'auto' for nics
    # which means nova uses a network that is available for a current project
    # if one exists and otherwise it creates a network automatically.
    # This special handling is processed here as JS side assumes 'nics'
    # is a list and it is easiest to handle it here.
    if nics:
        is_auto_allocate = any(nic.get('net-id') == '__auto_allocate__'
                               for nic in nics)
        if is_auto_allocate:
            nics = 'auto'

    kwargs = {}
    if description is not None:
        kwargs['description'] = description

    return Server(nova_client.servers.create(
        name.strip(), image, flavor, userdata=user_data,
        security_groups=security_groups,
        key_name=key_name, block_device_mapping=block_device_mapping,
        block_device_mapping_v2=block_device_mapping_v2,
        nics=nics, availability_zone=availability_zone,
        min_count=instance_count, admin_pass=admin_pass,
        disk_config=disk_config, config_drive=config_drive,
        meta=meta, scheduler_hints=scheduler_hints, **kwargs), request)


@profiler.trace
def server_delete(request, instance_id):
    _nova.novaclient(request).servers.delete(instance_id)
    # Session is available and consistent for the current view
    # among Horizon django servers even in load-balancing setup,
    # so only the view listing the servers will recognize it as
    # own DeleteInstance action performed. Note that dict is passed
    # by reference in python. Quote from django's developer manual:
    # " You can read it and write to request.session at any point
    #   in your view. You can edit it multiple times."
    request.session['server_deleted'] = instance_id


def get_novaclient_with_locked_status(request):
    microversion = get_microversion(request, "locked_attribute")
    return _nova.novaclient(request, version=microversion)


@profiler.trace
def server_list_paged(request,
                      search_opts=None,
                      detailed=True,
                      sort_dir="desc"):
    has_more_data = False
    has_prev_data = False
    nova_client = get_novaclient_with_locked_status(request)
    page_size = utils.get_page_size(request)
    search_opts = {} if search_opts is None else search_opts
    marker = search_opts.get('marker', None)

    if not search_opts.get('all_tenants', False):
        search_opts['project_id'] = request.user.tenant_id

    if search_opts.pop('paginate', False):
        reversed_order = sort_dir == "asc"
        LOG.debug("Notify received on deleted server: %r",
                  ('server_deleted' in request.session))
        deleted = request.session.pop('server_deleted',
                                      None)
        view_marker = 'possibly_deleted' if deleted and marker else 'ok'
        search_opts['marker'] = deleted if deleted else marker
        search_opts['limit'] = page_size + 1
        # NOTE(amotoki): It looks like the 'sort_keys' must be unique to make
        # the pagination in the nova API works as expected. Multiple servers
        # can have a same 'created_at' as its resolution is a second.
        # To ensure the uniqueness we add 'uuid' to the sort keys.
        # 'display_name' is added before 'uuid' to list servers in the
        # alphabetical order.
        sort_keys = ['created_at', 'display_name', 'uuid']

        servers = [Server(s, request)
                   for s in nova_client.servers.list(detailed, search_opts,
                                                     sort_keys=sort_keys,
                                                     sort_dirs=[sort_dir] * 3)]

        if view_marker == 'possibly_deleted':
            if not servers:
                view_marker = 'head_deleted'
                reversed_order = False
                servers = [Server(s, request)
                           for s in
                           nova_client.servers.list(detailed,
                                                    search_opts,
                                                    sort_keys=sort_keys,
                                                    sort_dirs=['desc'] * 3)]
            if not servers:
                view_marker = 'tail_deleted'
                reversed_order = True
                servers = [Server(s, request)
                           for s in
                           nova_client.servers.list(detailed,
                                                    search_opts,
                                                    sort_keys=sort_keys,
                                                    sort_dirs=['asc'] * 3)]
        (servers, has_more_data, has_prev_data) = update_pagination(
            servers, page_size, marker, reversed_order)
        has_prev_data = (False
                         if view_marker == 'head_deleted'
                         else has_prev_data)
        has_more_data = (False
                         if view_marker == 'tail_deleted'
                         else has_more_data)
    else:
        servers = [Server(s, request)
                   for s in nova_client.servers.list(detailed, search_opts)]
    return (servers, has_more_data, has_prev_data)


@profiler.trace
def server_list(request, search_opts=None, detailed=True):
    (servers, has_more_data, _) = server_list_paged(request,
                                                    search_opts,
                                                    detailed)
    return (servers, has_more_data)


@profiler.trace
def server_console_output(request, instance_id, tail_length=None):
    """Gets console output of an instance."""
    nc = _nova.novaclient(request)
    return nc.servers.get_console_output(instance_id, length=tail_length)


@profiler.trace
def server_pause(request, instance_id):
    _nova.novaclient(request).servers.pause(instance_id)


@profiler.trace
def server_unpause(request, instance_id):
    _nova.novaclient(request).servers.unpause(instance_id)


@profiler.trace
def server_suspend(request, instance_id):
    _nova.novaclient(request).servers.suspend(instance_id)


@profiler.trace
def server_resume(request, instance_id):
    _nova.novaclient(request).servers.resume(instance_id)


@profiler.trace
def server_shelve(request, instance_id):
    _nova.novaclient(request).servers.shelve(instance_id)


@profiler.trace
def server_unshelve(request, instance_id):
    _nova.novaclient(request).servers.unshelve(instance_id)


@profiler.trace
def server_reboot(request, instance_id, soft_reboot=False):
    hardness = nova_servers.REBOOT_HARD
    if soft_reboot:
        hardness = nova_servers.REBOOT_SOFT
    _nova.novaclient(request).servers.reboot(instance_id, hardness)


@profiler.trace
def server_rebuild(request, instance_id, image_id, password=None,
                   disk_config=None, description=None):
    kwargs = {}
    if description:
        kwargs['description'] = description
    nc = _nova.get_novaclient_with_instance_desc(request)
    return nc.servers.rebuild(instance_id, image_id, password, disk_config,
                              **kwargs)


@profiler.trace
def server_update(request, instance_id, name, description=None):
    nc = _nova.get_novaclient_with_instance_desc(request)
    return nc.servers.update(instance_id, name=name.strip(),
                             description=description)


@profiler.trace
def server_migrate(request, instance_id):
    _nova.novaclient(request).servers.migrate(instance_id)


@profiler.trace
def server_live_migrate(request, instance_id, host, block_migration=False,
                        disk_over_commit=False):
    _nova.novaclient(request).servers.live_migrate(instance_id, host,
                                                   block_migration,
                                                   disk_over_commit)


@profiler.trace
def server_resize(request, instance_id, flavor, disk_config=None, **kwargs):
    _nova.novaclient(request).servers.resize(instance_id, flavor,
                                             disk_config, **kwargs)


@profiler.trace
def server_confirm_resize(request, instance_id):
    _nova.novaclient(request).servers.confirm_resize(instance_id)


@profiler.trace
def server_revert_resize(request, instance_id):
    _nova.novaclient(request).servers.revert_resize(instance_id)


@profiler.trace
def server_start(request, instance_id):
    _nova.novaclient(request).servers.start(instance_id)


@profiler.trace
def server_stop(request, instance_id):
    _nova.novaclient(request).servers.stop(instance_id)


@profiler.trace
def server_lock(request, instance_id):
    microversion = get_microversion(request, "locked_attribute")
    _nova.novaclient(request, version=microversion).servers.lock(instance_id)


@profiler.trace
def server_unlock(request, instance_id):
    microversion = get_microversion(request, "locked_attribute")
    _nova.novaclient(request, version=microversion).servers.unlock(instance_id)


@profiler.trace
def server_metadata_update(request, instance_id, metadata):
    _nova.novaclient(request).servers.set_meta(instance_id, metadata)


@profiler.trace
def server_metadata_delete(request, instance_id, keys):
    _nova.novaclient(request).servers.delete_meta(instance_id, keys)


@profiler.trace
def server_rescue(request, instance_id, password=None, image=None):
    _nova.novaclient(request).servers.rescue(instance_id,
                                             password=password,
                                             image=image)


@profiler.trace
def server_unrescue(request, instance_id):
    _nova.novaclient(request).servers.unrescue(instance_id)


@profiler.trace
def tenant_quota_get(request, tenant_id):
    return QuotaSet(_nova.novaclient(request).quotas.get(tenant_id))


@profiler.trace
def tenant_quota_update(request, tenant_id, **kwargs):
    if kwargs:
        _nova.novaclient(request).quotas.update(tenant_id, **kwargs)


@profiler.trace
def default_quota_get(request, tenant_id):
    return QuotaSet(_nova.novaclient(request).quotas.defaults(tenant_id))


@profiler.trace
def default_quota_update(request, **kwargs):
    _nova.novaclient(request).quota_classes.update(DEFAULT_QUOTA_NAME,
                                                   **kwargs)


def _get_usage_marker(usage):
    marker = None
    if hasattr(usage, 'server_usages') and usage.server_usages:
        marker = usage.server_usages[-1].get('instance_id')
    return marker


def _get_usage_list_marker(usage_list):
    marker = None
    if usage_list:
        marker = _get_usage_marker(usage_list[-1])
    return marker


def _merge_usage(usage, next_usage):
    usage.server_usages.extend(next_usage.server_usages)
    usage.total_hours += next_usage.total_hours
    usage.total_memory_mb_usage += next_usage.total_memory_mb_usage
    usage.total_vcpus_usage += next_usage.total_vcpus_usage
    usage.total_local_gb_usage += next_usage.total_local_gb_usage


def _merge_usage_list(usages, next_usage_list):
    for next_usage in next_usage_list:
        if next_usage.tenant_id in usages:
            _merge_usage(usages[next_usage.tenant_id], next_usage)
        else:
            usages[next_usage.tenant_id] = next_usage


@profiler.trace
def usage_get(request, tenant_id, start, end):
    client = upgrade_api(request, _nova.novaclient(request), '2.40')
    usage = client.usage.get(tenant_id, start, end)
    if client.api_version >= api_versions.APIVersion('2.40'):
        # If the number of instances used to calculate the usage is greater
        # than max_limit, the usage will be split across multiple requests
        # and the responses will need to be merged back together.
        marker = _get_usage_marker(usage)
        while marker:
            next_usage = client.usage.get(tenant_id, start, end, marker=marker)
            marker = _get_usage_marker(next_usage)
            if marker:
                _merge_usage(usage, next_usage)
    return NovaUsage(usage)


@profiler.trace
def usage_list(request, start, end):
    client = upgrade_api(request, _nova.novaclient(request), '2.40')
    usage_list = client.usage.list(start, end, True)
    if client.api_version >= api_versions.APIVersion('2.40'):
        # If the number of instances used to calculate the usage is greater
        # than max_limit, the usage will be split across multiple requests
        # and the responses will need to be merged back together.
        usages = collections.OrderedDict()
        _merge_usage_list(usages, usage_list)
        marker = _get_usage_list_marker(usage_list)
        while marker:
            next_usage_list = client.usage.list(start, end, True,
                                                marker=marker)
            marker = _get_usage_list_marker(next_usage_list)
            if marker:
                _merge_usage_list(usages, next_usage_list)
        usage_list = usages.values()
    return [NovaUsage(u) for u in usage_list]


@profiler.trace
def get_password(request, instance_id, private_key=None):
    return _nova.novaclient(request).servers.get_password(instance_id,
                                                          private_key)


@profiler.trace
def instance_volume_attach(request, volume_id, instance_id, device):
    # If we have a multiattach volume, we need to use microversion>=2.60.
    volume = cinder.volume_get(request, volume_id)
    if volume.multiattach:
        version = get_microversion(request, 'multiattach')
        if version:
            client = _nova.novaclient(request, version)
        else:
            raise VolumeMultiattachNotSupported(
                _('Multiattach volumes are not yet supported.'))
    else:
        client = _nova.novaclient(request)
    return client.volumes.create_server_volume(
        instance_id, volume_id, device)


@profiler.trace
def instance_volume_detach(request, instance_id, att_id):
    return _nova.novaclient(request).volumes.delete_server_volume(instance_id,
                                                                  att_id)


@profiler.trace
def instance_volumes_list(request, instance_id):
    volumes = _nova.novaclient(request).volumes.get_server_volumes(instance_id)

    for volume in volumes:
        volume_data = cinder.cinderclient(request).volumes.get(volume.id)
        volume.name = cinder.Volume(volume_data).name

    return volumes


@profiler.trace
def hypervisor_list(request):
    return _nova.novaclient(request).hypervisors.list()


@profiler.trace
def hypervisor_stats(request):
    return _nova.novaclient(request).hypervisors.statistics()


@profiler.trace
def hypervisor_search(request, query, servers=True):
    return _nova.novaclient(request).hypervisors.search(query, servers)


@profiler.trace
def evacuate_host(request, host, target=None, on_shared_storage=False):
    # TODO(jmolle) This should be change for nova atomic api host_evacuate
    hypervisors = _nova.novaclient(request).hypervisors.search(host, True)
    response = []
    err_code = None
    for hypervisor in hypervisors:
        hyper = Hypervisor(hypervisor)
        # if hypervisor doesn't have servers, the attribute is not present
        for server in hyper.servers:
            try:
                _nova.novaclient(request).servers.evacuate(server['uuid'],
                                                           target,
                                                           on_shared_storage)
            except nova_exceptions.ClientException as err:
                err_code = err.code
                msg = _("Name: %(name)s ID: %(uuid)s")
                msg = msg % {'name': server['name'], 'uuid': server['uuid']}
                response.append(msg)

    if err_code:
        msg = _('Failed to evacuate instances: %s') % ', '.join(response)
        raise nova_exceptions.ClientException(err_code, msg)

    return True


@profiler.trace
def migrate_host(request, host, live_migrate=False, disk_over_commit=False,
                 block_migration=False):
    nc = _nova.novaclient(request)
    hypervisors = nc.hypervisors.search(host, True)
    response = []
    err_code = None
    for hyper in hypervisors:
        for server in getattr(hyper, "servers", []):
            try:
                if live_migrate:
                    instance = server_get(request, server['uuid'])

                    # Checking that instance can be live-migrated
                    if instance.status in ["ACTIVE", "PAUSED"]:
                        nc.servers.live_migrate(
                            server['uuid'],
                            None,
                            block_migration,
                            disk_over_commit
                        )
                    else:
                        nc.servers.migrate(server['uuid'])
                else:
                    nc.servers.migrate(server['uuid'])
            except nova_exceptions.ClientException as err:
                err_code = err.code
                msg = _("Name: %(name)s ID: %(uuid)s")
                msg = msg % {'name': server['name'], 'uuid': server['uuid']}
                response.append(msg)

    if err_code:
        msg = _('Failed to migrate instances: %s') % ', '.join(response)
        raise nova_exceptions.ClientException(err_code, msg)

    return True


@profiler.trace
def tenant_absolute_limits(request, reserved=False, tenant_id=None):
    # Nova does not allow to specify tenant_id for non-admin users
    # even if tenant_id matches a tenant_id of the user.
    if tenant_id == request.user.tenant_id:
        tenant_id = None
    limits = _nova.novaclient(request).limits.get(reserved=reserved,
                                                  tenant_id=tenant_id).absolute
    limits_dict = {}
    for limit in limits:
        if limit.value < 0:
            # Workaround for nova bug 1370867 that absolute_limits
            # returns negative value for total.*Used instead of 0.
            # For such case, replace negative values with 0.
            if limit.name.startswith('total') and limit.name.endswith('Used'):
                limits_dict[limit.name] = 0
            else:
                # -1 is used to represent unlimited quotas
                limits_dict[limit.name] = float("inf")
        else:
            limits_dict[limit.name] = limit.value
    return limits_dict


@profiler.trace
def availability_zone_list(request, detailed=False):
    nc = _nova.novaclient(request)
    zones = nc.availability_zones.list(detailed=detailed)
    zones.sort(key=attrgetter('zoneName'))
    return zones


@profiler.trace
def server_group_list(request):
    return _nova.novaclient(request).server_groups.list()


@profiler.trace
def server_group_create(request, **kwargs):
    microversion = get_microversion(request, "servergroup_soft_policies")
    nc = _nova.novaclient(request, version=microversion)
    return nc.server_groups.create(**kwargs)


@profiler.trace
def server_group_delete(request, servergroup_id):
    _nova.novaclient(request).server_groups.delete(servergroup_id)


@profiler.trace
def server_group_get(request, servergroup_id):
    microversion = get_microversion(request, "servergroup_user_info")
    return _nova.novaclient(request, version=microversion).server_groups.get(
        servergroup_id)


@profiler.trace
def service_list(request, binary=None):
    return _nova.novaclient(request).services.list(binary=binary)


@profiler.trace
def service_enable(request, host, binary):
    return _nova.novaclient(request).services.enable(host, binary)


@profiler.trace
def service_disable(request, host, binary, reason=None):
    if reason:
        return _nova.novaclient(request).services.disable_log_reason(
            host, binary, reason)
    else:
        return _nova.novaclient(request).services.disable(host, binary)


@profiler.trace
def aggregate_details_list(request):
    result = []
    c = _nova.novaclient(request)
    for aggregate in c.aggregates.list():
        result.append(c.aggregates.get_details(aggregate.id))
    return result


@profiler.trace
def aggregate_create(request, name, availability_zone=None):
    return _nova.novaclient(request).aggregates.create(name, availability_zone)


@profiler.trace
def aggregate_delete(request, aggregate_id):
    return _nova.novaclient(request).aggregates.delete(aggregate_id)


@profiler.trace
def aggregate_get(request, aggregate_id):
    return _nova.novaclient(request).aggregates.get(aggregate_id)


@profiler.trace
def aggregate_update(request, aggregate_id, values):
    _nova.novaclient(request).aggregates.update(aggregate_id, values)


@profiler.trace
def aggregate_set_metadata(request, aggregate_id, metadata):
    return _nova.novaclient(request).aggregates.set_metadata(aggregate_id,
                                                             metadata)


@profiler.trace
def add_host_to_aggregate(request, aggregate_id, host):
    _nova.novaclient(request).aggregates.add_host(aggregate_id, host)


@profiler.trace
def remove_host_from_aggregate(request, aggregate_id, host):
    _nova.novaclient(request).aggregates.remove_host(aggregate_id, host)


@profiler.trace
def interface_attach(request,
                     server, port_id=None, net_id=None, fixed_ip=None):
    return _nova.novaclient(request).servers.interface_attach(
        server, port_id, net_id, fixed_ip)


@profiler.trace
def interface_detach(request, server, port_id):
    return _nova.novaclient(request).servers.interface_detach(server, port_id)


@profiler.trace
@memoized.memoized
def list_extensions(request):
    """List all nova extension names, except the ones in the blacklist."""
    blacklist = set(settings.OPENSTACK_NOVA_EXTENSIONS_BLACKLIST)
    return tuple(
        extension for extension in EXTENSIONS if extension not in blacklist
    )


@profiler.trace
@memoized.memoized
def extension_supported(extension_name, request):
    """Determine if nova supports a given extension name.

    Example values for the extension_name include AdminActions, ConsoleOutput,
    etc.
    """
    return extension_name in list_extensions(request)


@profiler.trace
def can_set_server_password():
    return utils.get_dict_config('OPENSTACK_HYPERVISOR_FEATURES',
                                 'can_set_password')


@profiler.trace
def instance_action_list(request, instance_id):
    return nova_instance_action.InstanceActionManager(
        _nova.novaclient(request)).list(instance_id)


@profiler.trace
def can_set_mount_point():
    """Return the Hypervisor's capability of setting mount points."""
    return utils.get_dict_config('OPENSTACK_HYPERVISOR_FEATURES',
                                 'can_set_mount_point')


@profiler.trace
def requires_keypair():
    return utils.get_dict_config('OPENSTACK_HYPERVISOR_FEATURES',
                                 'requires_keypair')


def can_set_quotas():
    return utils.get_dict_config('OPENSTACK_HYPERVISOR_FEATURES',
                                 'enable_quotas')
