#!/usr/bin/python
#  -*- coding: utf-8 -*-

# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Diane Wang <dianew@vmware.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function

__metaclass__ = type

DOCUMENTATION = r"""
---
module: vmware_guest_vgpu
short_description: Modify vGPU video card profile of the specified virtual machine in the given vCenter infrastructure
description:
    - This module is used to reconfigure vGPU card profile of the given virtual machine.
    - VM must be power off M(community.vmware.vmware_guest_powerstate) module can perform that task.
author:
    - Mohamed Alibi (@Medalibi)
    - Unknown (@matancarmeli7)
options:
   name:
     description:
       - Name of the virtual machine.
       - This is a required parameter, if parameter O(uuid) or O(moid) is not supplied.
     type: str
   uuid:
     description:
       - UUID of the instance to gather facts if known, this is VMware's unique identifier.
       - This is a required parameter, if parameter O(name) or O(moid) is not supplied.
     type: str
   moid:
     description:
       - Managed Object ID of the instance to manage if known, this is a unique identifier only within a single vCenter instance.
       - This is required if O(name) or O(uuid) is not supplied.
     type: str
   folder:
     description:
       - Destination folder, absolute or relative path to find an existing guest.
       - This is a required parameter, only if multiple VMs are found with same name.
       - The folder should include the datacenter. ESXi server's datacenter is ha-datacenter.
       - 'Examples:'
       - '   folder: /ha-datacenter/vm'
       - '   folder: ha-datacenter/vm'
       - '   folder: /datacenter1/vm'
       - '   folder: datacenter1/vm'
       - '   folder: /datacenter1/vm/folder1'
       - '   folder: datacenter1/vm/folder1'
       - '   folder: /folder1/datacenter1/vm'
       - '   folder: folder1/datacenter1/vm'
       - '   folder: /folder1/datacenter1/vm/folder2'
     type: str
   datacenter:
     default: ha-datacenter
     description:
       - The datacenter name to which virtual machine belongs to.
     type: str
   state:
     default: present
     choices: [ 'present', 'absent' ]
     description:
       - vGPU profile state.
       - When V(present), the selected vGPU profile will be added if the VM hosted ESXi host NVIDIA GPU offer it.
       - When V(absent), the selected vGPU profile gets removed from the VM.
     type: str
   vgpu:
     description:
       - A supported vGPU profile depending on the GPU model. Required for any operation.
     type: str
   force:
     description:
       - Force operation.
     default: false
     type: bool
   use_instance_uuid:
     description:
       - Whether to use the VMware instance UUID rather than the BIOS UUID.
     default: false
     type: bool
   cluster:
     description:
       - The cluster name where the virtual machine is running.
     type: str
   esxi_hostname:
     description:
       - The ESXi hostname where the virtual machine is running.
     type: str
extends_documentation_fragment:
- community.vmware.vmware.documentation
"""

EXAMPLES = r"""
- name: Add vGPU profile to VM
  community.vmware.vmware_guest_vgpu:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    datacenter: "{{ datacenter_name }}"
    validate_certs: false
    name: UbuntuTest
    vgpu: 'grid_m10-8q'
    state: present
  delegate_to: localhost
  register: vgpu_info

- name: Remove vGPU profile to VM
  community.vmware.vmware_guest_vgpu:
    hostname: "{{ vcenter_hostname }}"
    username: "{{ vcenter_username }}"
    password: "{{ vcenter_password }}"
    datacenter: "{{ datacenter_name }}"
    validate_certs: false
    name: UbuntuTest
    vgpu: 'grid_m10-8q'
    state: absent
  delegate_to: localhost
  register: vgpu_info
"""

RETURN = r"""
vgpu_info:
    description: metadata about the virtual machine's vGPU profile
    returned: always
    type: dict
    sample: {
        "vgpu": {
            "Controller_Key": 100,
            "Key": 13000,
            "Label": "PCI device 0",
            "Summary": "NVIDIA GRID vGPU grid_m10-8q",
            "Unit_Number": 18,
            "Vgpu": "grid_m10-8q"
        }
    }
"""

try:
    from pyVmomi import vim
except ImportError:
    pass

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.vmware.plugins.module_utils.vmware import (
    PyVmomi,
    vmware_argument_spec,
    wait_for_task,
)


class PyVmomiHelper(PyVmomi):
    def __init__(self, module):
        super(PyVmomiHelper, self).__init__(module)

    def _gather_vgpu_profile_facts(self, vm_obj):
        """
        Gather facts about VM's vGPU profile settings
        Args:
            vm_obj: Managed object of virtual machine
        Returns: vGPU profile and its facts
        """
        vgpu_info = dict()
        for vgpu_VirtualDevice_obj in vm_obj.config.hardware.device:
            if hasattr(vgpu_VirtualDevice_obj.backing, "vgpu"):
                vgpu_info = dict(
                    Vgpu=vgpu_VirtualDevice_obj.backing.vgpu,
                    Key=vgpu_VirtualDevice_obj.key,
                    Summary=vgpu_VirtualDevice_obj.deviceInfo.summary,
                    Label=vgpu_VirtualDevice_obj.deviceInfo.label,
                    Unit_Number=vgpu_VirtualDevice_obj.unitNumber,
                    Controller_Key=vgpu_VirtualDevice_obj.controllerKey,
                )
                break
        return vgpu_info

    def _vgpu_absent(self, vm_obj):
        """
        Remove vGPU profile of virtual machine.
        Args:
            vm_obj: Managed object of virtual machine
        Returns: Operation results and vGPU facts
        """
        result = {}
        vgpu_prfl = self.params["vgpu"]
        vgpu_VirtualDevice_obj = self._get_vgpu_VirtualDevice_object(vm_obj, vgpu_prfl)
        if vgpu_VirtualDevice_obj is None:
            changed = False
            failed = False
        else:
            vgpu_fact = self._gather_vgpu_profile_facts(vm_obj)
            changed, failed = self._remove_vgpu_profile_from_vm(
                vm_obj, vgpu_VirtualDevice_obj, vgpu_prfl
            )
        result = {"changed": changed, "failed": failed, "vgpu": vgpu_fact}
        return result

    def _remove_vgpu_profile_from_vm(self, vm_obj, vgpu_VirtualDevice_obj, vgpu_prfl):
        """
        Remove vGPU profile of virtual machine
        Args:
            vm_obj: Managed object of virtual machine
            vgpu_VirtualDevice_obj: vGPU profile object holding its facts
            vgpu_prfl: vGPU profile name
        Returns: Operation results
        """
        changed = False
        failed = False
        vm_current_vgpu_profile = self._get_vgpu_profile_in_the_vm(vm_obj)
        if vgpu_prfl in vm_current_vgpu_profile:
            vdspec = vim.vm.device.VirtualDeviceSpec()
            vmConfigSpec = vim.vm.ConfigSpec()
            vdspec.operation = "remove"
            vdspec.device = vgpu_VirtualDevice_obj
            vmConfigSpec.deviceChange.append(vdspec)

            try:
                task = vm_obj.ReconfigVM_Task(spec=vmConfigSpec)
                wait_for_task(task)
                changed = True
                return changed, failed
            except Exception as exc:
                failed = True
                self.module.fail_json(
                    msg="Failed to delete vGPU profile"
                    " '%s' from vm %s." % (vgpu_prfl, vm_obj.name),
                    detail=exc.msg,
                )
        return changed, failed

    def _vgpu_present(self, vm_obj):
        """
        Add vGPU profile to virtual machine.
        Args:
            vm_obj: Managed object of virtual machine
        Returns: Operation results and vGPU facts
        """
        result = {}
        vgpu_prfl = self.params["vgpu"]
        vgpu_profile_name = self._get_vgpu_profiles_name(vm_obj, vgpu_prfl)
        if vgpu_profile_name is None:
            self.module.fail_json(
                msg="vGPU Profile '%s'" " does not exist." % vgpu_prfl
            )

        changed, failed = self._add_vgpu_profile_to_vm(
            vm_obj, vgpu_profile_name, vgpu_prfl
        )
        vgpu_fact = self._gather_vgpu_profile_facts(vm_obj)
        result = {"changed": changed, "failed": failed, "vgpu": vgpu_fact}
        return result

    def _add_vgpu_profile_to_vm(self, vm_obj, vgpu_profile_name, vgpu_prfl):
        """
        Add vGPU profile of virtual machine
        Args:
            vm_obj: Managed object of virtual machine
            vgpu_profile_name: vGPU profile object name from ESXi server list
            vgpu_prfl: vGPU profile name
        Returns: Operation results
        """
        changed = False
        failed = False
        vm_current_vgpu_profile = self._get_vgpu_profile_in_the_vm(vm_obj)
        if self.params["force"] or vgpu_prfl not in vm_current_vgpu_profile:
            vgpu_p = vgpu_profile_name.vgpu
            backing = vim.VirtualPCIPassthroughVmiopBackingInfo(vgpu=vgpu_p)
            summary = "NVIDIA GRID vGPU " + vgpu_prfl
            deviceInfo = vim.Description(summary=summary, label="PCI device 0")
            hba_object = vim.VirtualPCIPassthrough(
                backing=backing, deviceInfo=deviceInfo
            )
            new_device_config = vim.VirtualDeviceConfigSpec(device=hba_object)
            new_device_config.operation = "add"
            vmConfigSpec = vim.vm.ConfigSpec()
            vmConfigSpec.deviceChange = [new_device_config]
            vmConfigSpec.memoryReservationLockedToMax = True

            try:
                task = vm_obj.ReconfigVM_Task(spec=vmConfigSpec)
                wait_for_task(task)
                changed = True
            except Exception as exc:
                failed = True
                self.module.fail_json(
                    msg="Failed to add vGPU Profile"
                    " '%s' to vm %s." % (vgpu_prfl, vm_obj.name),
                    detail=exc.msg,
                )
        else:
            return changed, failed
        return changed, failed

    def _get_vgpu_profile_in_the_vm(self, vm_obj):
        """
        Get vGPU profile object of virtual machine
        Args:
            vm_obj: Managed object of virtual machine
        Returns: vGPU profile name
        """
        vm_current_vgpu_profile = []
        for vgpu_VirtualDevice_obj in vm_obj.config.hardware.device:
            if hasattr(vgpu_VirtualDevice_obj.backing, "vgpu"):
                vm_current_vgpu_profile.append(vgpu_VirtualDevice_obj.backing.vgpu)
        return vm_current_vgpu_profile

    def _get_vgpu_VirtualDevice_object(self, vm_obj, vgpu_prfl):
        """
        Get current vGPU profile object of virtual machine
        Args:
            vm_obj: Managed object of virtual machine
            vgpu_prfl: vGPU profile name
        Returns: vGPU profile name of virtual machine
        """
        for vgpu_VirtualDevice_obj in vm_obj.config.hardware.device:
            if hasattr(vgpu_VirtualDevice_obj.backing, "vgpu"):
                if vgpu_VirtualDevice_obj.backing.vgpu == vgpu_prfl:
                    return vgpu_VirtualDevice_obj
        return None

    def _get_vgpu_profiles_name(self, vm_obj, vgpu_prfl):
        """
        Get matched vGPU profile object of ESXi host
        Args:
            vm_obj: Managed object of virtual machine
            vgpu_prfl: vGPU profile name
        Returns: vGPU profile object
        """
        vm_host = vm_obj.runtime.host
        vgpu_profiles = vm_host.config.sharedGpuCapabilities
        for vgpu_profile_name in vgpu_profiles:
            if vgpu_profile_name.vgpu == vgpu_prfl:
                return vgpu_profile_name
        return None


def main():
    argument_spec = vmware_argument_spec()
    argument_spec.update(
        name=dict(type="str"),
        uuid=dict(type="str"),
        use_instance_uuid=dict(type="bool", default=False),
        moid=dict(type="str"),
        folder=dict(type="str"),
        datacenter=dict(type="str", default="ha-datacenter"),
        esxi_hostname=dict(type="str"),
        cluster=dict(type="str"),
        vgpu=dict(type="str"),
        force=dict(type="bool", default=False),
        state=dict(type="str", default="present", choices=["absent", "present"]),
    )

    module = AnsibleModule(
        argument_spec=argument_spec,
        mutually_exclusive=[["cluster", "esxi_hostname"]],
        required_one_of=[["name", "uuid", "moid"]],
    )

    pyv = PyVmomiHelper(module)
    vm = pyv.get_vm()

    if not vm:
        vm_id = (
            module.params.get("uuid")
            or module.params.get("name")
            or module.params.get("moid")
        )
        module.fail_json(
            msg="Unable to manage vGPU profile for non-existing VM %s" % vm_id
        )

    if module.params["state"] == "present":
        result = pyv._vgpu_present(vm)
    elif module.params["state"] == "absent":
        result = pyv._vgpu_absent(vm)

    if "failed" not in result:
        result["failed"] = False

    if result["failed"]:
        module.fail_json(**result)
    else:
        module.exit_json(**result)


if __name__ == "__main__":
    main()
