# Copyright 2012 Cloudbase Solutions Srl
# All Rights Reserved.
#
#    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.

"""
Management class for host operations.
"""
import datetime
import platform
import time

import os_resource_classes as orc
from os_win import constants as os_win_const
from os_win import utilsfactory
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import units

from nova.compute import utils as compute_utils
import nova.conf
from nova.i18n import _
from nova import objects
from nova.objects import fields as obj_fields
from nova.virt.hyperv import constants
from nova.virt.hyperv import pathutils

CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)


class HostOps(object):
    def __init__(self):
        self._diskutils = utilsfactory.get_diskutils()
        self._hostutils = utilsfactory.get_hostutils()
        self._pathutils = pathutils.PathUtils()

    def _get_cpu_info(self):
        """Get the CPU information.
        :returns: A dictionary containing the main properties
        of the central processor in the hypervisor.
        """
        cpu_info = dict()

        processors = self._hostutils.get_cpus_info()

        w32_arch_dict = constants.WMI_WIN32_PROCESSOR_ARCHITECTURE
        cpu_info['arch'] = w32_arch_dict.get(processors[0]['Architecture'],
                                             'Unknown')
        cpu_info['model'] = processors[0]['Name']
        cpu_info['vendor'] = processors[0]['Manufacturer']

        topology = dict()
        topology['sockets'] = len(processors)
        topology['cores'] = processors[0]['NumberOfCores']
        topology['threads'] = (processors[0]['NumberOfLogicalProcessors'] //
                               processors[0]['NumberOfCores'])
        cpu_info['topology'] = topology

        features = list()
        for fkey, fname in os_win_const.PROCESSOR_FEATURE.items():
            if self._hostutils.is_cpu_feature_present(fkey):
                features.append(fname)
        cpu_info['features'] = features

        return cpu_info

    def _get_memory_info(self):
        (total_mem_kb, free_mem_kb) = self._hostutils.get_memory_info()
        total_mem_mb = total_mem_kb // 1024
        free_mem_mb = free_mem_kb // 1024
        return (total_mem_mb, free_mem_mb, total_mem_mb - free_mem_mb)

    def _get_storage_info_gb(self):
        instances_dir = self._pathutils.get_instances_dir()
        (size, free_space) = self._diskutils.get_disk_capacity(
            instances_dir)

        total_gb = size // units.Gi
        free_gb = free_space // units.Gi
        used_gb = total_gb - free_gb
        return (total_gb, free_gb, used_gb)

    def _get_hypervisor_version(self):
        """Get hypervisor version.
        :returns: hypervisor version (ex. 6003)
        """

        # NOTE(claudiub): The hypervisor_version will be stored in the database
        # as an Integer and it will be used by the scheduler, if required by
        # the image property 'hypervisor_version_requires'.
        # The hypervisor_version will then be converted back to a version
        # by splitting the int in groups of 3 digits.
        # E.g.: hypervisor_version 6003 is converted to '6.3'.
        version = self._hostutils.get_windows_version().split('.')
        version = int(version[0]) * 1000 + int(version[1])
        LOG.debug('Windows version: %s ', version)
        return version

    def _get_remotefx_gpu_info(self):
        total_video_ram = 0
        available_video_ram = 0

        if CONF.hyperv.enable_remotefx:
            gpus = self._hostutils.get_remotefx_gpu_info()
            for gpu in gpus:
                total_video_ram += int(gpu['total_video_ram'])
                available_video_ram += int(gpu['available_video_ram'])
        else:
            gpus = []

        return {'total_video_ram': total_video_ram,
                'used_video_ram': total_video_ram - available_video_ram,
                'gpu_info': jsonutils.dumps(gpus)}

    def _get_host_numa_topology(self):
        numa_nodes = self._hostutils.get_numa_nodes()
        cells = []
        for numa_node in numa_nodes:
            # Hyper-V does not support CPU pinning / mempages.
            # initializing the rest of the fields.
            numa_node.update(pinned_cpus=set(), mempages=[], siblings=[])
            cell = objects.NUMACell(**numa_node)
            cells.append(cell)

        return objects.NUMATopology(cells=cells)

    def get_available_resource(self):
        """Retrieve resource info.

        This method is called when nova-compute launches, and
        as part of a periodic task.

        :returns: dictionary describing resources

        """
        LOG.debug('get_available_resource called')

        (total_mem_mb,
         free_mem_mb,
         used_mem_mb) = self._get_memory_info()

        (total_hdd_gb,
         free_hdd_gb,
         used_hdd_gb) = self._get_storage_info_gb()

        cpu_info = self._get_cpu_info()
        cpu_topology = cpu_info['topology']
        vcpus = (cpu_topology['sockets'] *
                 cpu_topology['cores'] *
                 cpu_topology['threads'])

        # NOTE(claudiub): free_hdd_gb only refers to the currently free
        # physical storage, it doesn't take into consideration the virtual
        # sizes of the VMs' dynamic disks. This means that the VMs' disks can
        # expand beyond the free_hdd_gb's value, and instances will still be
        # scheduled to this compute node.
        dic = {'vcpus': vcpus,
               'memory_mb': total_mem_mb,
               'memory_mb_used': used_mem_mb,
               'local_gb': total_hdd_gb,
               'local_gb_used': used_hdd_gb,
               'disk_available_least': free_hdd_gb,
               'hypervisor_type': "hyperv",
               'hypervisor_version': self._get_hypervisor_version(),
               'hypervisor_hostname': platform.node(),
               'vcpus_used': 0,
               'cpu_info': jsonutils.dumps(cpu_info),
               'supported_instances': [
                   (obj_fields.Architecture.I686,
                    obj_fields.HVType.HYPERV,
                    obj_fields.VMMode.HVM),
                   (obj_fields.Architecture.X86_64,
                    obj_fields.HVType.HYPERV,
                    obj_fields.VMMode.HVM)],
               'numa_topology': self._get_host_numa_topology()._to_json(),
               'pci_passthrough_devices': self._get_pci_passthrough_devices(),
               }

        gpu_info = self._get_remotefx_gpu_info()
        dic.update(gpu_info)
        return dic

    def _get_pci_passthrough_devices(self):
        """Get host PCI devices information.

        Obtains PCI devices information and returns it as a JSON string.

        :returns: a JSON string containing a list of the assignable PCI
                  devices information.
        """

        pci_devices = self._hostutils.get_pci_passthrough_devices()

        for pci_dev in pci_devices:
            # NOTE(claudiub): These fields are required by the PCI tracker.
            dev_label = 'label_%(vendor_id)s_%(product_id)s' % {
                'vendor_id': pci_dev['vendor_id'],
                'product_id': pci_dev['product_id']}

            # TODO(claudiub): Find a way to associate the PCI devices with
            # the NUMA nodes they are in.
            pci_dev.update(dev_type=obj_fields.PciDeviceType.STANDARD,
                           label=dev_label,
                           numa_node=None)

        return jsonutils.dumps(pci_devices)

    def host_power_action(self, action):
        """Reboots, shuts down or powers up the host."""
        if action in [constants.HOST_POWER_ACTION_SHUTDOWN,
                      constants.HOST_POWER_ACTION_REBOOT]:
            self._hostutils.host_power_action(action)
        else:
            if action == constants.HOST_POWER_ACTION_STARTUP:
                raise NotImplementedError(
                    _("Host PowerOn is not supported by the Hyper-V driver"))

    def get_host_ip_addr(self):
        host_ip = CONF.my_ip
        if not host_ip:
            # Return the first available address
            host_ip = self._hostutils.get_local_ips()[0]
        LOG.debug("Host IP address is: %s", host_ip)
        return host_ip

    def get_host_uptime(self):
        """Returns the host uptime."""

        tick_count64 = self._hostutils.get_host_tick_count64()

        # format the string to match libvirt driver uptime
        # Libvirt uptime returns a combination of the following
        # - current host time
        # - time since host is up
        # - number of logged in users
        # - cpu load
        # Since the Windows function GetTickCount64 returns only
        # the time since the host is up, returning 0s for cpu load
        # and number of logged in users.
        # This is done to ensure the format of the returned
        # value is same as in libvirt
        return "%s up %s,  0 users,  load average: 0, 0, 0" % (
                   str(time.strftime("%H:%M:%S")),
                   str(datetime.timedelta(milliseconds=int(tick_count64))))

    def update_provider_tree(self, provider_tree, nodename,
                             allocation_ratios, allocations=None):
        resources = self.get_available_resource()

        inventory = {
            orc.VCPU: {
                'total': resources['vcpus'],
                'min_unit': 1,
                'max_unit': resources['vcpus'],
                'step_size': 1,
                'allocation_ratio': allocation_ratios[orc.VCPU],
                'reserved': CONF.reserved_host_cpus,
            },
            orc.MEMORY_MB: {
                'total': resources['memory_mb'],
                'min_unit': 1,
                'max_unit': resources['memory_mb'],
                'step_size': 1,
                'allocation_ratio': allocation_ratios[orc.MEMORY_MB],
                'reserved': CONF.reserved_host_memory_mb,
            },
            # TODO(lpetrut): once #1784020 is fixed, we can skip reporting
            # shared storage capacity
            orc.DISK_GB: {
                'total': resources['local_gb'],
                'min_unit': 1,
                'max_unit': resources['local_gb'],
                'step_size': 1,
                'allocation_ratio': allocation_ratios[orc.DISK_GB],
                'reserved': compute_utils.convert_mb_to_ceil_gb(
                    CONF.reserved_host_disk_mb),
            },
        }

        provider_tree.update_inventory(nodename, inventory)
