# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2010 Cloud.com, 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.

"""
A connection to Hyper-V .
Uses Windows Management Instrumentation (WMI) calls to interact with Hyper-V
Hyper-V WMI usage:
    http://msdn.microsoft.com/en-us/library/cc723875%28v=VS.85%29.aspx
The Hyper-V object model briefly:
    The physical computer and its hosted virtual machines are each represented
    by the Msvm_ComputerSystem class.

    Each virtual machine is associated with a
    Msvm_VirtualSystemGlobalSettingData (vs_gs_data) instance and one or more
    Msvm_VirtualSystemSettingData (vmsetting) instances. For each vmsetting
    there is a series of Msvm_ResourceAllocationSettingData (rasd) objects.
    The rasd objects describe the settings for each device in a VM.
    Together, the vs_gs_data, vmsettings and rasds describe the configuration
    of the virtual machine.

    Creating new resources such as disks and nics involves cloning a default
    rasd object and appropriately modifying the clone and calling the
    AddVirtualSystemResources WMI method
    Changing resources such as memory uses the ModifyVirtualSystemResources
    WMI method

Using the Python WMI library:
    Tutorial:
        http://timgolden.me.uk/python/wmi/tutorial.html
    Hyper-V WMI objects can be retrieved simply by using the class name
    of the WMI object and optionally specifying a column to filter the
    result set. More complex filters can be formed using WQL (sql-like)
    queries.
    The parameters and return tuples of WMI method calls can gleaned by
    examining the doc string. For example:
    >>> vs_man_svc.ModifyVirtualSystemResources.__doc__
    ModifyVirtualSystemResources (ComputerSystem, ResourceSettingData[])
                 => (Job, ReturnValue)'
    When passing setting data (ResourceSettingData) to the WMI method,
    an XML representation of the data is passed in using GetText_(1).
    Available methods on a service can be determined using method.keys():
    >>> vs_man_svc.methods.keys()
    vmsettings and rasds for a vm can be retrieved using the 'associators'
    method with the appropriate return class.
    Long running WMI commands generally return a Job (an instance of
    Msvm_ConcreteJob) whose state can be polled to determine when it finishes

"""

import os
import time

from nova.compute import power_state
from nova import exception
from nova import flags
from nova import log as logging
from nova.virt import driver
from nova.virt import images

wmi = None


FLAGS = flags.FLAGS


LOG = logging.getLogger('nova.virt.hyperv')


HYPERV_POWER_STATE = {
    3: power_state.SHUTDOWN,
    2: power_state.RUNNING,
    32768: power_state.PAUSED,
}


REQ_POWER_STATE = {
    'Enabled': 2,
    'Disabled': 3,
    'Reboot': 10,
    'Reset': 11,
    'Paused': 32768,
    'Suspended': 32769,
}


WMI_JOB_STATUS_STARTED = 4096
WMI_JOB_STATE_RUNNING = 4
WMI_JOB_STATE_COMPLETED = 7


def get_connection(_):
    global wmi
    if wmi is None:
        wmi = __import__('wmi')
    return HyperVConnection()


class HyperVConnection(driver.ComputeDriver):
    def __init__(self):
        super(HyperVConnection, self).__init__()
        self._conn = wmi.WMI(moniker='//./root/virtualization')
        self._cim_conn = wmi.WMI(moniker='//./root/cimv2')

    def init_host(self, host):
        #FIXME(chiradeep): implement this
        LOG.debug(_('In init host'))
        pass

    def list_instances(self):
        """ Return the names of all the instances known to Hyper-V. """
        vms = [v.ElementName \
                for v in self._conn.Msvm_ComputerSystem(['ElementName'])]
        return vms

    def list_instances_detail(self):
        # TODO(justinsb): This is a terrible implementation (1+N)
        instance_infos = []
        for instance_name in self.list_instances():
            info = self.get_info(instance_name)

            state = info['state']

            instance_info = driver.InstanceInfo(instance_name, state)
            instance_infos.append(instance_info)

        return instance_infos

    def spawn(self, context, instance, image_meta,
              network_info=None, block_device_info=None):
        """ Create a new VM and start it."""
        vm = self._lookup(instance.name)
        if vm is not None:
            raise exception.InstanceExists(name=instance.name)

        #Fetch the file, assume it is a VHD file.
        base_vhd_filename = os.path.join(FLAGS.instances_path,
                                         instance.name)
        vhdfile = "%s.vhd" % (base_vhd_filename)
        images.fetch(instance['image_ref'], vhdfile,
                     instance['user_id'], instance['project_id'])

        try:
            self._create_vm(instance)

            self._create_disk(instance['name'], vhdfile)

            mac_address = None
            if instance['mac_addresses']:
                mac_address = instance['mac_addresses'][0]['address']

            self._create_nic(instance['name'], mac_address)

            LOG.debug(_('Starting VM %s '), instance.name)
            self._set_vm_state(instance['name'], 'Enabled')
            LOG.info(_('Started VM %s '), instance.name)
        except Exception as exn:
            LOG.exception(_('spawn vm failed: %s'), exn)
            self.destroy(instance)

    def _create_vm(self, instance):
        """Create a VM but don't start it.  """
        vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0]

        vs_gs_data = self._conn.Msvm_VirtualSystemGlobalSettingData.new()
        vs_gs_data.ElementName = instance['name']
        (job, ret_val) = vs_man_svc.DefineVirtualSystem(
                [], None, vs_gs_data.GetText_(1))[1:]
        if ret_val == WMI_JOB_STATUS_STARTED:
            success = self._check_job_status(job)
        else:
            success = (ret_val == 0)

        if not success:
            raise Exception(_('Failed to create VM %s'), instance.name)

        LOG.debug(_('Created VM %s...'), instance.name)
        vm = self._conn.Msvm_ComputerSystem(ElementName=instance.name)[0]

        vmsettings = vm.associators(
                          wmi_result_class='Msvm_VirtualSystemSettingData')
        vmsetting = [s for s in vmsettings
                        if s.SettingType == 3][0]  # avoid snapshots
        memsetting = vmsetting.associators(
                           wmi_result_class='Msvm_MemorySettingData')[0]
        #No Dynamic Memory, so reservation, limit and quantity are identical.
        mem = long(str(instance['memory_mb']))
        memsetting.VirtualQuantity = mem
        memsetting.Reservation = mem
        memsetting.Limit = mem

        (job, ret_val) = vs_man_svc.ModifyVirtualSystemResources(
                vm.path_(), [memsetting.GetText_(1)])
        LOG.debug(_('Set memory for vm %s...'), instance.name)
        procsetting = vmsetting.associators(
                wmi_result_class='Msvm_ProcessorSettingData')[0]
        vcpus = long(instance['vcpus'])
        procsetting.VirtualQuantity = vcpus
        procsetting.Reservation = vcpus
        procsetting.Limit = 100000  # static assignment to 100%

        (job, ret_val) = vs_man_svc.ModifyVirtualSystemResources(
                vm.path_(), [procsetting.GetText_(1)])
        LOG.debug(_('Set vcpus for vm %s...'), instance.name)

    def _create_disk(self, vm_name, vhdfile):
        """Create a disk and attach it to the vm"""
        LOG.debug(_('Creating disk for %(vm_name)s by attaching'
                ' disk file %(vhdfile)s') % locals())
        #Find the IDE controller for the vm.
        vms = self._conn.MSVM_ComputerSystem(ElementName=vm_name)
        vm = vms[0]
        vmsettings = vm.associators(
                wmi_result_class='Msvm_VirtualSystemSettingData')
        rasds = vmsettings[0].associators(
                wmi_result_class='MSVM_ResourceAllocationSettingData')
        ctrller = [r for r in rasds
                   if r.ResourceSubType == 'Microsoft Emulated IDE Controller'\
                   and r.Address == "0"]
        #Find the default disk drive object for the vm and clone it.
        diskdflt = self._conn.query(
                "SELECT * FROM Msvm_ResourceAllocationSettingData \
                WHERE ResourceSubType LIKE 'Microsoft Synthetic Disk Drive'\
                AND InstanceID LIKE '%Default%'")[0]
        diskdrive = self._clone_wmi_obj(
                'Msvm_ResourceAllocationSettingData', diskdflt)
        #Set the IDE ctrller as parent.
        diskdrive.Parent = ctrller[0].path_()
        diskdrive.Address = 0
        #Add the cloned disk drive object to the vm.
        new_resources = self._add_virt_resource(diskdrive, vm)
        if new_resources is None:
            raise Exception(_('Failed to add diskdrive to VM %s'),
                                             vm_name)
        diskdrive_path = new_resources[0]
        LOG.debug(_('New disk drive path is %s'), diskdrive_path)
        #Find the default VHD disk object.
        vhddefault = self._conn.query(
                "SELECT * FROM Msvm_ResourceAllocationSettingData \
                 WHERE ResourceSubType LIKE 'Microsoft Virtual Hard Disk' AND \
                 InstanceID LIKE '%Default%' ")[0]

        #Clone the default and point it to the image file.
        vhddisk = self._clone_wmi_obj(
                'Msvm_ResourceAllocationSettingData', vhddefault)
        #Set the new drive as the parent.
        vhddisk.Parent = diskdrive_path
        vhddisk.Connection = [vhdfile]

        #Add the new vhd object as a virtual hard disk to the vm.
        new_resources = self._add_virt_resource(vhddisk, vm)
        if new_resources is None:
            raise Exception(_('Failed to add vhd file to VM %s'),
                                             vm_name)
        LOG.info(_('Created disk for %s'), vm_name)

    def _create_nic(self, vm_name, mac):
        """Create a (emulated) nic and attach it to the vm"""
        LOG.debug(_('Creating nic for %s '), vm_name)
        #Find the vswitch that is connected to the physical nic.
        vms = self._conn.Msvm_ComputerSystem(ElementName=vm_name)
        extswitch = self._find_external_network()
        vm = vms[0]
        switch_svc = self._conn.Msvm_VirtualSwitchManagementService()[0]
        #Find the default nic and clone it to create a new nic for the vm.
        #Use Msvm_SyntheticEthernetPortSettingData for Windows or Linux with
        #Linux Integration Components installed.
        emulatednics_data = self._conn.Msvm_EmulatedEthernetPortSettingData()
        default_nic_data = [n for n in emulatednics_data
                            if n.InstanceID.rfind('Default') > 0]
        new_nic_data = self._clone_wmi_obj(
                'Msvm_EmulatedEthernetPortSettingData',
                default_nic_data[0])
        #Create a port on the vswitch.
        (new_port, ret_val) = switch_svc.CreateSwitchPort(vm_name, vm_name,
                                            "", extswitch.path_())
        if ret_val != 0:
            LOG.error(_('Failed creating a port on the external vswitch'))
            raise Exception(_('Failed creating port for %s'),
                    vm_name)
        ext_path = extswitch.path_()
        LOG.debug(_("Created switch port %(vm_name)s on switch %(ext_path)s")
                % locals())
        #Connect the new nic to the new port.
        new_nic_data.Connection = [new_port]
        new_nic_data.ElementName = vm_name + ' nic'
        new_nic_data.Address = ''.join(mac.split(':'))
        new_nic_data.StaticMacAddress = 'TRUE'
        #Add the new nic to the vm.
        new_resources = self._add_virt_resource(new_nic_data, vm)
        if new_resources is None:
            raise Exception(_('Failed to add nic to VM %s'),
                    vm_name)
        LOG.info(_("Created nic for %s "), vm_name)

    def _add_virt_resource(self, res_setting_data, target_vm):
        """Add a new resource (disk/nic) to the VM"""
        vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0]
        (job, new_resources, ret_val) = vs_man_svc.\
                    AddVirtualSystemResources([res_setting_data.GetText_(1)],
                                                target_vm.path_())
        success = True
        if ret_val == WMI_JOB_STATUS_STARTED:
            success = self._check_job_status(job)
        else:
            success = (ret_val == 0)
        if success:
            return new_resources
        else:
            return None

    #TODO: use the reactor to poll instead of sleep
    def _check_job_status(self, jobpath):
        """Poll WMI job state for completion"""
        #Jobs have a path of the form:
        #\\WIN-P5IG7367DAG\root\virtualization:Msvm_ConcreteJob.InstanceID=
        #"8A496B9C-AF4D-4E98-BD3C-1128CD85320D"
        inst_id = jobpath.split('=')[1].strip('"')
        jobs = self._conn.Msvm_ConcreteJob(InstanceID=inst_id)
        if len(jobs) == 0:
            return False
        job = jobs[0]
        while job.JobState == WMI_JOB_STATE_RUNNING:
            time.sleep(0.1)
            job = self._conn.Msvm_ConcreteJob(InstanceID=inst_id)[0]
        if job.JobState != WMI_JOB_STATE_COMPLETED:
            LOG.debug(_("WMI job failed: %s"), job.ErrorSummaryDescription)
            return False
        desc = job.Description
        elap = job.ElapsedTime
        LOG.debug(_("WMI job succeeded: %(desc)s, Elapsed=%(elap)s ")
                % locals())
        return True

    def _find_external_network(self):
        """Find the vswitch that is connected to the physical nic.
           Assumes only one physical nic on the host
        """
        #If there are no physical nics connected to networks, return.
        bound = self._conn.Msvm_ExternalEthernetPort(IsBound='TRUE')
        if len(bound) == 0:
            return None
        return self._conn.Msvm_ExternalEthernetPort(IsBound='TRUE')[0]\
            .associators(wmi_result_class='Msvm_SwitchLANEndpoint')[0]\
            .associators(wmi_result_class='Msvm_SwitchPort')[0]\
            .associators(wmi_result_class='Msvm_VirtualSwitch')[0]

    def _clone_wmi_obj(self, wmi_class, wmi_obj):
        """Clone a WMI object"""
        cl = self._conn.__getattr__(wmi_class)  # get the class
        newinst = cl.new()
        #Copy the properties from the original.
        for prop in wmi_obj._properties:
            newinst.Properties_.Item(prop).Value = \
                    wmi_obj.Properties_.Item(prop).Value
        return newinst

    def reboot(self, instance, network_info, reboot_type):
        """Reboot the specified instance."""
        vm = self._lookup(instance.name)
        if vm is None:
            raise exception.InstanceNotFound(instance_id=instance.id)
        self._set_vm_state(instance.name, 'Reboot')

    def destroy(self, instance, network_info, block_device_info=None):
        """Destroy the VM. Also destroy the associated VHD disk files"""
        LOG.debug(_("Got request to destroy vm %s"), instance.name)
        vm = self._lookup(instance.name)
        if vm is None:
            return
        vm = self._conn.Msvm_ComputerSystem(ElementName=instance.name)[0]
        vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0]
        #Stop the VM first.
        self._set_vm_state(instance.name, 'Disabled')
        vmsettings = vm.associators(
                         wmi_result_class='Msvm_VirtualSystemSettingData')
        rasds = vmsettings[0].associators(
                         wmi_result_class='MSVM_ResourceAllocationSettingData')
        disks = [r for r in rasds \
                    if r.ResourceSubType == 'Microsoft Virtual Hard Disk']
        diskfiles = []
        #Collect disk file information before destroying the VM.
        for disk in disks:
            diskfiles.extend([c for c in disk.Connection])
        #Nuke the VM. Does not destroy disks.
        (job, ret_val) = vs_man_svc.DestroyVirtualSystem(vm.path_())
        if ret_val == WMI_JOB_STATUS_STARTED:
            success = self._check_job_status(job)
        elif ret_val == 0:
            success = True
        if not success:
            raise Exception(_('Failed to destroy vm %s') % instance.name)
        #Delete associated vhd disk files.
        for disk in diskfiles:
            vhdfile = self._cim_conn.CIM_DataFile(Name=disk)
            for vf in vhdfile:
                vf.Delete()
                instance_name = instance.name
                LOG.debug(_("Del: disk %(vhdfile)s vm %(instance_name)s")
                        % locals())

    def get_info(self, instance_name):
        """Get information about the VM"""
        vm = self._lookup(instance_name)
        if vm is None:
            raise exception.InstanceNotFound(instance_id=instance_name)
        vm = self._conn.Msvm_ComputerSystem(ElementName=instance_name)[0]
        vs_man_svc = self._conn.Msvm_VirtualSystemManagementService()[0]
        vmsettings = vm.associators(
                       wmi_result_class='Msvm_VirtualSystemSettingData')
        settings_paths = [v.path_() for v in vmsettings]
        #See http://msdn.microsoft.com/en-us/library/cc160706%28VS.85%29.aspx
        summary_info = vs_man_svc.GetSummaryInformation(
                                       [4, 100, 103, 105], settings_paths)[1]
        info = summary_info[0]
        state = str(HYPERV_POWER_STATE[info.EnabledState])
        memusage = str(info.MemoryUsage)
        numprocs = str(info.NumberOfProcessors)
        uptime = str(info.UpTime)

        LOG.debug(_("Got Info for vm %(instance_name)s: state=%(state)s,"
                " mem=%(memusage)s, num_cpu=%(numprocs)s,"
                " cpu_time=%(uptime)s") % locals())

        return {'state': HYPERV_POWER_STATE[info.EnabledState],
                'max_mem': info.MemoryUsage,
                'mem': info.MemoryUsage,
                'num_cpu': info.NumberOfProcessors,
                'cpu_time': info.UpTime}

    def _lookup(self, i):
        vms = self._conn.Msvm_ComputerSystem(ElementName=i)
        n = len(vms)
        if n == 0:
            return None
        elif n > 1:
            raise Exception(_('duplicate name found: %s') % i)
        else:
            return vms[0].ElementName

    def _set_vm_state(self, vm_name, req_state):
        """Set the desired state of the VM"""
        vms = self._conn.Msvm_ComputerSystem(ElementName=vm_name)
        if len(vms) == 0:
            return False
        (job, ret_val) = vms[0].RequestStateChange(REQ_POWER_STATE[req_state])
        success = False
        if ret_val == WMI_JOB_STATUS_STARTED:
            success = self._check_job_status(job)
        elif ret_val == 0:
            success = True
        elif ret_val == 32775:
            #Invalid state for current operation. Typically means it is
            #already in the state requested
            success = True
        if success:
            LOG.info(_("Successfully changed vm state of %(vm_name)s"
                    " to %(req_state)s") % locals())
        else:
            msg = _("Failed to change vm state of %(vm_name)s"
                    " to %(req_state)s") % locals()
            LOG.error(msg)
            raise Exception(msg)

    def attach_volume(self, connection_info, instance_name, mountpoint):
        vm = self._lookup(instance_name)
        if vm is None:
            raise exception.InstanceNotFound(instance_id=instance_name)

    def detach_volume(self, connection_info, instance_name, mountpoint):
        vm = self._lookup(instance_name)
        if vm is None:
            raise exception.InstanceNotFound(instance_id=instance_name)

    def poll_rebooting_instances(self, timeout):
        """See xenapi_conn.py implementation."""
        pass

    def poll_rescued_instances(self, timeout):
        """See xenapi_conn.py implementation."""
        pass

    def poll_unconfirmed_resizes(self, resize_confirm_window):
        """See xenapi_conn.py implementation."""
        pass

    def update_available_resource(self, ctxt, host):
        """This method is supported only by libvirt."""
        return

    def update_host_status(self):
        """See xenapi_conn.py implementation."""
        pass

    def get_host_stats(self, refresh=False):
        """See xenapi_conn.py implementation."""
        pass

    def host_power_action(self, host, action):
        """Reboots, shuts down or powers up the host."""
        pass

    def set_host_enabled(self, host, enabled):
        """Sets the specified host's ability to accept new instances."""
        pass

    def plug_vifs(self, instance, network_info):
        """Plug VIFs into networks."""
        pass

    def unplug_vifs(self, instance, network_info):
        """Unplug VIFs from networks."""
        pass
