#!/usr/bin/python
#
# Copyright (c) 2018, Bojan Vitnik <bvitnik@mainstream.rs>
# 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 annotations

DOCUMENTATION = r"""
module: xenserver_guest
short_description: Manages virtual machines running on Citrix Hypervisor/XenServer host or pool
description: >-
  This module can be used to create new virtual machines from templates or other virtual machines, modify various virtual
  machine components like network and disk, rename a virtual machine and remove a virtual machine with associated components.
author:
  - Bojan Vitnik (@bvitnik) <bvitnik@mainstream.rs>
notes:
  - Minimal supported version of XenServer is 5.6.
  - Module was tested with XenServer 6.5, 7.1, 7.2, 7.6, Citrix Hypervisor 8.0, XCP-ng 7.6 and 8.0.
  - 'To acquire XenAPI Python library, just run C(pip install XenAPI) on your Ansible Control Node. The library can also be
    found inside Citrix Hypervisor/XenServer SDK (downloadable from Citrix website). Copy the C(XenAPI.py) file from the SDK
    to your Python site-packages on your Ansible Control Node to use it. Latest version of the library can also be acquired
    from GitHub: U(https://raw.githubusercontent.com/xapi-project/xen-api/master/scripts/examples/python/XenAPI/XenAPI.py).'
  - If no scheme is specified in O(hostname), module defaults to C(http://) because C(https://) is problematic in most setups.
    Make sure you are accessing XenServer host in trusted environment or use C(https://) scheme explicitly.
  - To use C(https://) scheme for O(hostname) you have to either import host certificate to your OS certificate store or use
    O(validate_certs=false) which requires XenAPI library from XenServer 7.2 SDK or newer and Python 2.7.9 or newer.
  - 'Network configuration inside a guest OS, by using parameters O(networks[].type), O(networks[].ip), O(networks[].gateway)
    and so on, is supported on XenServer 7.0 or newer for Windows guests by using official XenServer Guest agent support for
    network configuration. The module tries to detect if such support is available and utilize it, else it uses a custom method
    of configuration using xenstore. Since XenServer Guest agent only support None and Static types of network configuration,
    where None means DHCP configured interface, O(networks[].type) and O(networks[].type6) values V(none) and V(dhcp) have
    same effect. More info here:
    U(https://web.archive.org/web/20180218110151/https://xenserver.org/blog/entry/set-windows-guest-vm-static-ip-address-in-xenserver.html).'
  - 'On platforms without official support for network configuration inside a guest OS, network parameters are written to
    xenstore C(vm-data/networks/<vif_device>) key. Parameters can be inspected by using C(xenstore ls) and C(xenstore read)
    tools on \*nix guests or through WMI interface on Windows guests. They can also be found in VM facts C(instance.xenstore_data)
    key as returned by the module. It is up to the user to implement a boot time scripts or custom agent that reads the parameters
    from xenstore and configure network with given parameters. Take note that for xenstore data to become available inside
    a guest, a VM restart is needed hence module requires VM restart if any parameter is changed. This is a limitation of
    XenAPI and xenstore. Considering these limitations, network configuration through xenstore is most useful for bootstrapping
    newly deployed VMs, much less for reconfiguring existing ones. More info here: U(https://support.citrix.com/article/CTX226713).'
requirements:
  - XenAPI
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  state:
    description:
      - Specify the state VM should be in.
      - If O(state) is set to V(present) and VM exists, ensure the VM configuration conforms to given parameters.
      - If O(state) is set to V(present) and VM does not exist, then VM is deployed with given parameters.
      - If O(state) is set to V(absent) and VM exists, then VM is removed with its associated components.
      - If O(state) is set to V(poweredon) and VM does not exist, then VM is deployed with given parameters and powered on
        automatically.
    type: str
    default: present
    choices: [present, absent, poweredon]
  name:
    description:
      - Name of the VM to work with.
      - VMs running on XenServer do not necessarily have unique names. The module fails if multiple VMs with same name are
        found.
      - In case of multiple VMs with same name, use O(uuid) to uniquely specify VM to manage.
      - This parameter is case sensitive.
    type: str
    aliases: [name_label]
  name_desc:
    description:
      - VM description.
    type: str
  uuid:
    description:
      - UUID of the VM to manage if known. This is XenServer's unique identifier.
      - It is required if name is not unique.
      - Please note that a supplied UUID is ignored on VM creation, as XenServer creates the UUID internally.
    type: str
  template:
    description:
      - Name of a template, an existing VM (must be shut down) or a snapshot that should be used to create VM.
      - Templates/VMs/snapshots on XenServer do not necessarily have unique names. The module fails if multiple templates
        with same name are found.
      - In case of multiple templates/VMs/snapshots with same name, use O(template_uuid) to uniquely specify source template.
      - If VM already exists, this setting is ignored.
      - This parameter is case sensitive.
    type: str
    aliases: [template_src]
  template_uuid:
    description:
      - UUID of a template, an existing VM or a snapshot that should be used to create VM.
      - It is required if template name is not unique.
    type: str
  is_template:
    description:
      - Convert VM to template.
    type: bool
    default: false
  folder:
    description:
      - Destination folder for VM.
      - This parameter is case sensitive.
      - 'Example: O(folder=/folder1/folder2).'
    type: str
  hardware:
    description:
      - Manage VM's hardware parameters. VM needs to be shut down to reconfigure these parameters.
    type: dict
    suboptions:
      num_cpus:
        description:
          - Number of CPUs.
        type: int
      num_cpu_cores_per_socket:
        description:
          - Number of Cores Per Socket. O(hardware.num_cpus) has to be a multiple of O(hardware.num_cpu_cores_per_socket).
        type: int
      memory_mb:
        description:
          - Amount of memory in MB.
        type: int
  disks:
    description:
      - A list of disks to add to VM.
      - All parameters are case sensitive.
      - Removing or detaching existing disks of VM is not supported.
      - New disks are required to have either a O(disks[].size) or one of O(ignore:disks[].size_[tb,gb,mb,kb,b]) parameters
        specified.
      - VM needs to be shut down to reconfigure disk size.
    type: list
    elements: dict
    aliases: [disk]
    suboptions:
      size:
        description:
          - 'Disk size with unit. Unit must be: V(b), V(kb), V(mb), V(gb), V(tb). VM needs to be shut down to reconfigure
            this parameter.'
          - If no unit is specified, size is assumed to be in bytes.
        type: str
      size_b:
        description:
          - Disk size in bytes.
        type: str
      size_kb:
        description:
          - Disk size in kilobytes.
        type: str
      size_mb:
        description:
          - Disk size in megabytes.
        type: str
      size_gb:
        description:
          - Disk size in gigabytes.
        type: str
      size_tb:
        description:
          - Disk size in terabytes.
        type: str
      name:
        description:
          - Disk name.
        type: str
        aliases: [name_label]
      name_desc:
        description:
          - Disk description.
        type: str
      sr:
        description:
          - Storage Repository to create disk on. If not specified, it uses default SR. Cannot be used for moving disk to
            other SR.
        type: str
      sr_uuid:
        description:
          - UUID of a SR to create disk on. Use if SR name is not unique.
        type: str
  cdrom:
    description:
      - A CD-ROM configuration for the VM.
      - All parameters are case sensitive.
    type: dict
    suboptions:
      type:
        description:
          - The type of CD-ROM. When V(none) the CD-ROM device is present but empty.
        type: str
        choices: [none, iso]
      iso_name:
        description:
          - The file name of an ISO image from one of the XenServer ISO Libraries (implies O(cdrom.type=iso)).
          - Required if O(cdrom.type) is set to V(iso).
        type: str
  networks:
    description:
      - A list of networks (in the order of the NICs).
      - All parameters are case sensitive.
      - Name is required for new NICs. Other parameters are optional in all cases.
    type: list
    elements: dict
    aliases: [network]
    suboptions:
      name:
        description:
          - Name of a XenServer network to attach the network interface to.
        type: str
        aliases: [name_label]
      mac:
        description:
          - Customize MAC address of the interface.
        type: str
      type:
        description:
          - Type of IPv4 assignment. Value V(none) means whatever is default for OS.
          - On some operating systems it could be DHCP configured (for example Windows) or unconfigured interface (for example
            Linux).
        type: str
        choices: [none, dhcp, static]
      ip:
        description:
          - Static IPv4 address (implies O(networks[].type=static)). Can include prefix in format C(<IPv4 address>/<prefix>)
            instead of using C(netmask).
        type: str
      netmask:
        description:
          - Static IPv4 netmask required for O(networks[].ip) if prefix is not specified.
        type: str
      gateway:
        description:
          - Static IPv4 gateway.
        type: str
      type6:
        description:
          - Type of IPv6 assignment. Value V(none) means whatever is default for OS.
        type: str
        choices: [none, dhcp, static]
      ip6:
        description:
          - Static IPv6 address (implies O(networks[].type6=static)) with prefix in format C(<IPv6 address>/<prefix>).
        type: str
      gateway6:
        description:
          - Static IPv6 gateway.
        type: str
  home_server:
    description:
      - Name of a XenServer host that is a Home Server for the VM.
      - This parameter is case sensitive.
    type: str
  custom_params:
    description:
      - Define a list of custom VM params to set on VM.
      - Useful for advanced users familiar with managing VM params through C(xe) CLI.
      - A custom value object takes two fields O(custom_params[].key) and O(custom_params[].value) (see example below).
    type: list
    elements: dict
    suboptions:
      key:
        description:
          - VM param name.
        type: str
        required: true
      value:
        description:
          - VM param value.
        type: raw
        required: true
  wait_for_ip_address:
    description:
      - Wait until XenServer detects an IP address for the VM. If O(state) is set to V(absent), this parameter is ignored.
      - This requires XenServer Tools to be preinstalled on the VM to work properly.
    type: bool
    default: false
  state_change_timeout:
    description:
      - By default, the module waits indefinitely for VM to acquire an IP address if O(wait_for_ip_address=true).
      - If this parameter is set to a positive value, the module instead waits the specified number of seconds for the state
        change.
      - In case of timeout, module generates an error message.
    type: int
    default: 0
  linked_clone:
    description:
      - Whether to create a Linked Clone from the template, existing VM or snapshot. If V(false), it creates a full copy.
      - This is equivalent to C(Use storage-level fast disk clone) option in XenCenter.
    type: bool
    default: false
  force:
    description:
      - Ignore warnings and complete the actions.
      - This parameter is useful for removing VM in running state or reconfiguring VM params that require VM to be shut down.
    type: bool
    default: false
extends_documentation_fragment:
  - community.general._xenserver.documentation
  - community.general._attributes
"""

EXAMPLES = r"""
- name: Create a VM from a template
  community.general.xenserver_guest:
    hostname: "{{ xenserver_hostname }}"
    username: "{{ xenserver_username }}"
    password: "{{ xenserver_password }}"
    folder: /testvms
    name: testvm_2
    state: poweredon
    template: CentOS 7
    disks:
      - size_gb: 10
        sr: my_sr
    hardware:
      num_cpus: 6
      num_cpu_cores_per_socket: 3
      memory_mb: 512
    cdrom:
      type: iso
      iso_name: guest-tools.iso
    networks:
      - name: VM Network
        mac: aa:bb:dd:aa:00:14
    wait_for_ip_address: true
  delegate_to: localhost
  register: deploy

- name: Create a VM template
  community.general.xenserver_guest:
    hostname: "{{ xenserver_hostname }}"
    username: "{{ xenserver_username }}"
    password: "{{ xenserver_password }}"
    folder: /testvms
    name: testvm_6
    is_template: true
    disk:
      - size_gb: 10
        sr: my_sr
    hardware:
      memory_mb: 512
      num_cpus: 1
  delegate_to: localhost
  register: deploy

- name: Rename a VM (requires the VM's UUID)
  community.general.xenserver_guest:
    hostname: "{{ xenserver_hostname }}"
    username: "{{ xenserver_username }}"
    password: "{{ xenserver_password }}"
    uuid: 421e4592-c069-924d-ce20-7e7533fab926
    name: new_name
    state: present
  delegate_to: localhost

- name: Remove a VM by UUID
  community.general.xenserver_guest:
    hostname: "{{ xenserver_hostname }}"
    username: "{{ xenserver_username }}"
    password: "{{ xenserver_password }}"
    uuid: 421e4592-c069-924d-ce20-7e7533fab926
    state: absent
  delegate_to: localhost

- name: Modify custom params (boot order)
  community.general.xenserver_guest:
    hostname: "{{ xenserver_hostname }}"
    username: "{{ xenserver_username }}"
    password: "{{ xenserver_password }}"
    name: testvm_8
    state: present
    custom_params:
      - key: HVM_boot_params
        value: {"order": "ndc"}
  delegate_to: localhost

- name: Customize network parameters
  community.general.xenserver_guest:
    hostname: "{{ xenserver_hostname }}"
    username: "{{ xenserver_username }}"
    password: "{{ xenserver_password }}"
    name: testvm_10
    networks:
      - name: VM Network
        ip: 192.168.1.100/24
        gateway: 192.168.1.1
      - type: dhcp
  delegate_to: localhost
"""

RETURN = r"""
instance:
  description: Metadata about the VM.
  returned: always
  type: dict
  sample:
    {
      "cdrom": {
        "type": "none"
      },
      "customization_agent": "native",
      "disks": [
        {
          "name": "testvm_11-0",
          "name_desc": "",
          "os_device": "xvda",
          "size": 42949672960,
          "sr": "Local storage",
          "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075",
          "vbd_userdevice": "0"
        },
        {
          "name": "testvm_11-1",
          "name_desc": "",
          "os_device": "xvdb",
          "size": 42949672960,
          "sr": "Local storage",
          "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075",
          "vbd_userdevice": "1"
        }
      ],
      "domid": "56",
      "folder": "",
      "hardware": {
        "memory_mb": 8192,
        "num_cpu_cores_per_socket": 2,
        "num_cpus": 4
      },
      "home_server": "",
      "is_template": false,
      "name": "testvm_11",
      "name_desc": "",
      "networks": [
        {
          "gateway": "192.168.0.254",
          "gateway6": "fc00::fffe",
          "ip": "192.168.0.200",
          "ip6": [
            "fe80:0000:0000:0000:e9cb:625a:32c5:c291",
            "fc00:0000:0000:0000:0000:0000:0000:0001"
          ],
          "mac": "ba:91:3a:48:20:76",
          "mtu": "1500",
          "name": "Pool-wide network associated with eth1",
          "netmask": "255.255.255.128",
          "prefix": "25",
          "prefix6": "64",
          "vif_device": "0"
        }
      ],
      "other_config": {
        "base_template_name": "Windows Server 2016 (64-bit)",
        "import_task": "OpaqueRef:e43eb71c-45d6-5351-09ff-96e4fb7d0fa5",
        "install-methods": "cdrom",
        "instant": "true",
        "mac_seed": "f83e8d8a-cfdc-b105-b054-ef5cb416b77e"
      },
      "platform": {
        "acpi": "1",
        "apic": "true",
        "cores-per-socket": "2",
        "device_id": "0002",
        "hpet": "true",
        "nx": "true",
        "pae": "true",
        "timeoffset": "-25200",
        "vga": "std",
        "videoram": "8",
        "viridian": "true",
        "viridian_reference_tsc": "true",
        "viridian_time_ref_count": "true"
      },
      "state": "poweredon",
      "uuid": "e3c0b2d5-5f05-424e-479c-d3df8b3e7cda",
      "xenstore_data": {
        "vm-data": ""
      }
    }
changes:
  description: Detected or made changes to VM.
  returned: always
  type: list
  sample:
    [
      {
        "hardware": [
          "num_cpus"
        ]
      },
      {
        "disks_changed": [
          [],
          [
            "size"
          ]
        ]
      },
      {
        "disks_new": [
          {
            "name": "new-disk",
            "name_desc": "",
            "position": 2,
            "size_gb": "4",
            "vbd_userdevice": "2"
          }
        ]
      },
      {
        "cdrom": [
          "type",
          "iso_name"
        ]
      },
      {
        "networks_changed": [
          [
            "mac"
          ]
        ]
      },
      {
        "networks_new": [
          {
            "name": "Pool-wide network associated with eth2",
            "position": 1,
            "vif_device": "1"
          }
        ]
      },
      "need_poweredoff"
    ]
"""

import re

HAS_XENAPI = False
try:
    import XenAPI

    HAS_XENAPI = True
except ImportError:
    pass

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.network import is_mac

from ansible_collections.community.general.plugins.module_utils._xenserver import (
    XenServerObject,
    gather_vm_facts,
    gather_vm_params,
    get_object_ref,
    ip_netmask_to_prefix,
    ip_prefix_to_netmask,
    is_valid_ip6_addr,
    is_valid_ip6_prefix,
    is_valid_ip_addr,
    is_valid_ip_netmask,
    is_valid_ip_prefix,
    set_vm_power_state,
    wait_for_vm_ip_address,
    xenserver_common_argument_spec,
)


class XenServerVM(XenServerObject):
    """Class for managing XenServer VM.

    Attributes:
        vm_ref (str): XAPI reference to VM.
        vm_params (dict): A dictionary with VM parameters as returned
            by gather_vm_params() function.
    """

    def __init__(self, module):
        """Inits XenServerVM using module parameters.

        Args:
            module: Reference to Ansible module object.
        """
        super().__init__(module)

        self.vm_ref = get_object_ref(
            self.module,
            self.module.params["name"],
            self.module.params["uuid"],
            obj_type="VM",
            fail=False,
            msg_prefix="VM search: ",
        )
        self.gather_params()

    def exists(self):
        """Returns True if VM exists, else False."""
        return True if self.vm_ref is not None else False

    def gather_params(self):
        """Gathers all VM parameters available in XAPI database."""
        self.vm_params = gather_vm_params(self.module, self.vm_ref)

    def gather_facts(self):
        """Gathers and returns VM facts."""
        return gather_vm_facts(self.module, self.vm_params)

    def set_power_state(self, power_state):
        """Controls VM power state."""
        state_changed, current_state = set_vm_power_state(
            self.module, self.vm_ref, power_state, self.module.params["state_change_timeout"]
        )

        # If state has changed, update vm_params.
        if state_changed:
            self.vm_params["power_state"] = current_state.capitalize()

        return state_changed

    def wait_for_ip_address(self):
        """Waits for VM to acquire an IP address."""
        self.vm_params["guest_metrics"] = wait_for_vm_ip_address(
            self.module, self.vm_ref, self.module.params["state_change_timeout"]
        )

    def deploy(self):
        """Deploys new VM from template."""
        # Safety check.
        if self.exists():
            self.module.fail_json(msg="Called deploy on existing VM!")

        try:
            templ_ref = get_object_ref(
                self.module,
                self.module.params["template"],
                self.module.params["template_uuid"],
                obj_type="template",
                fail=True,
                msg_prefix="VM deploy: ",
            )

            # Is this an existing running VM?
            if self.xapi_session.xenapi.VM.get_power_state(templ_ref).lower() != "halted":
                self.module.fail_json(msg="VM deploy: running VM cannot be used as a template!")

            # Find a SR we can use for VM.copy(). We use SR of the first disk
            # if specified or default SR if not specified.
            disk_params_list = self.module.params["disks"]

            sr_ref = None

            if disk_params_list:
                disk_params = disk_params_list[0]

                disk_sr_uuid = disk_params.get("sr_uuid")
                disk_sr = disk_params.get("sr")

                if disk_sr_uuid is not None or disk_sr is not None:
                    sr_ref = get_object_ref(
                        self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True, msg_prefix="VM deploy disks[0]: "
                    )

            if not sr_ref:
                if self.default_sr_ref != "OpaqueRef:NULL":
                    sr_ref = self.default_sr_ref
                else:
                    self.module.fail_json(
                        msg="VM deploy disks[0]: no default SR found! You must specify SR explicitly."
                    )

            # VM name could be an empty string which is bad.
            if self.module.params["name"] is not None and not self.module.params["name"]:
                self.module.fail_json(msg="VM deploy: VM name must not be an empty string!")

            # Support for Ansible check mode.
            if self.module.check_mode:
                return

            # Now we can instantiate VM. We use VM.clone for linked_clone and
            # VM.copy for non linked_clone.
            if self.module.params["linked_clone"]:
                self.vm_ref = self.xapi_session.xenapi.VM.clone(templ_ref, self.module.params["name"])
            else:
                self.vm_ref = self.xapi_session.xenapi.VM.copy(templ_ref, self.module.params["name"], sr_ref)

            # Description is copied over from template so we reset it.
            self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, "")

            # If template is one of built-in XenServer templates, we have to
            # do some additional steps.
            # Note: VM.get_is_default_template() is supported from XenServer 7.2
            #       onward so we use an alternative way.
            templ_other_config = self.xapi_session.xenapi.VM.get_other_config(templ_ref)

            if "default_template" in templ_other_config and templ_other_config["default_template"]:
                # other_config of built-in XenServer templates have a key called
                # 'disks' with the following content:
                #   disks: <provision><disk bootable="true" device="0" size="10737418240" sr="" type="system"/></provision>
                # This value of other_data is copied to cloned or copied VM and
                # it prevents provisioning of VM because sr is not specified and
                # XAPI returns an error. To get around this, we remove the
                # 'disks' key and add disks to VM later ourselves.
                vm_other_config = self.xapi_session.xenapi.VM.get_other_config(self.vm_ref)

                if "disks" in vm_other_config:
                    del vm_other_config["disks"]

                self.xapi_session.xenapi.VM.set_other_config(self.vm_ref, vm_other_config)

            # At this point we have VM ready for provisioning.
            self.xapi_session.xenapi.VM.provision(self.vm_ref)

            # After provisioning we can prepare vm_params for reconfigure().
            self.gather_params()

            # VM is almost ready. We just need to reconfigure it...
            self.reconfigure()

            # Power on VM if needed.
            if self.module.params["state"] == "poweredon":
                self.set_power_state("poweredon")

        except XenAPI.Failure as f:
            self.module.fail_json(msg=f"XAPI ERROR: {f.details}")

    def reconfigure(self):
        """Reconfigures an existing VM.

        Returns:
            list: parameters that were reconfigured.
        """
        # Safety check.
        if not self.exists():
            self.module.fail_json(msg="Called reconfigure on non existing VM!")

        config_changes = self.get_changes()

        vm_power_state_save = self.vm_params["power_state"].lower()

        if "need_poweredoff" in config_changes and vm_power_state_save != "halted" and not self.module.params["force"]:
            self.module.fail_json(
                msg="VM reconfigure: VM has to be in powered off state to reconfigure but force was not specified!"
            )

        # Support for Ansible check mode.
        if self.module.check_mode:
            return config_changes

        if "need_poweredoff" in config_changes and vm_power_state_save != "halted" and self.module.params["force"]:
            self.set_power_state("shutdownguest")

        try:
            for change in config_changes:
                if isinstance(change, str):
                    if change == "name":
                        self.xapi_session.xenapi.VM.set_name_label(self.vm_ref, self.module.params["name"])
                    elif change == "name_desc":
                        self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, self.module.params["name_desc"])
                    elif change == "folder":
                        self.xapi_session.xenapi.VM.remove_from_other_config(self.vm_ref, "folder")

                        if self.module.params["folder"]:
                            self.xapi_session.xenapi.VM.add_to_other_config(
                                self.vm_ref, "folder", self.module.params["folder"]
                            )
                    elif change == "home_server":
                        if self.module.params["home_server"]:
                            host_ref = self.xapi_session.xenapi.host.get_by_name_label(
                                self.module.params["home_server"]
                            )[0]
                        else:
                            host_ref = "OpaqueRef:NULL"

                        self.xapi_session.xenapi.VM.set_affinity(self.vm_ref, host_ref)
                elif isinstance(change, dict):
                    if change.get("hardware"):
                        for hardware_change in change["hardware"]:
                            if hardware_change == "num_cpus":
                                num_cpus = int(self.module.params["hardware"]["num_cpus"])

                                if num_cpus < int(self.vm_params["VCPUs_at_startup"]):
                                    self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus))
                                    self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus))
                                else:
                                    self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus))
                                    self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus))
                            elif hardware_change == "num_cpu_cores_per_socket":
                                self.xapi_session.xenapi.VM.remove_from_platform(self.vm_ref, "cores-per-socket")
                                num_cpu_cores_per_socket = int(
                                    self.module.params["hardware"]["num_cpu_cores_per_socket"]
                                )

                                if num_cpu_cores_per_socket > 1:
                                    self.xapi_session.xenapi.VM.add_to_platform(
                                        self.vm_ref, "cores-per-socket", str(num_cpu_cores_per_socket)
                                    )
                            elif hardware_change == "memory_mb":
                                memory_b = str(int(self.module.params["hardware"]["memory_mb"]) * 1048576)
                                vm_memory_static_min_b = str(
                                    min(int(memory_b), int(self.vm_params["memory_static_min"]))
                                )

                                self.xapi_session.xenapi.VM.set_memory_limits(
                                    self.vm_ref, vm_memory_static_min_b, memory_b, memory_b, memory_b
                                )

                    elif change.get("cdrom"):
                        vm_cdrom_params_list = [
                            cdrom_params for cdrom_params in self.vm_params["VBDs"] if cdrom_params["type"] == "CD"
                        ]

                        # If there is no CD present, we have to create one.
                        if not vm_cdrom_params_list:
                            # We will try to place cdrom at userdevice position
                            # 3 (which is default) if it is not already occupied
                            # else we will place it at first allowed position.
                            cdrom_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref)

                            if "3" in cdrom_userdevices_allowed:
                                cdrom_userdevice = "3"
                            else:
                                cdrom_userdevice = cdrom_userdevices_allowed[0]

                            cdrom_vbd = {
                                "VM": self.vm_ref,
                                "VDI": "OpaqueRef:NULL",
                                "userdevice": cdrom_userdevice,
                                "bootable": False,
                                "mode": "RO",
                                "type": "CD",
                                "empty": True,
                                "other_config": {},
                                "qos_algorithm_type": "",
                                "qos_algorithm_params": {},
                            }

                            cdrom_vbd_ref = self.xapi_session.xenapi.VBD.create(cdrom_vbd)
                        else:
                            cdrom_vbd_ref = self.xapi_session.xenapi.VBD.get_by_uuid(vm_cdrom_params_list[0]["uuid"])

                        cdrom_is_empty = self.xapi_session.xenapi.VBD.get_empty(cdrom_vbd_ref)

                        for cdrom_change in change["cdrom"]:
                            if cdrom_change == "type":
                                cdrom_type = self.module.params["cdrom"]["type"]

                                if cdrom_type == "none" and not cdrom_is_empty:
                                    self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref)
                                elif cdrom_type == "host":
                                    # Unimplemented!
                                    pass

                            elif cdrom_change == "iso_name":
                                if not cdrom_is_empty:
                                    self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref)

                                cdrom_vdi_ref = self.xapi_session.xenapi.VDI.get_by_name_label(
                                    self.module.params["cdrom"]["iso_name"]
                                )[0]
                                self.xapi_session.xenapi.VBD.insert(cdrom_vbd_ref, cdrom_vdi_ref)

                    elif change.get("disks_changed"):
                        vm_disk_params_list = [
                            disk_params for disk_params in self.vm_params["VBDs"] if disk_params["type"] == "Disk"
                        ]

                        for position, disk_change_list in enumerate(change["disks_changed"]):
                            for disk_change in disk_change_list:
                                vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(
                                    vm_disk_params_list[position]["VDI"]["uuid"]
                                )

                                if disk_change == "name":
                                    self.xapi_session.xenapi.VDI.set_name_label(
                                        vdi_ref, self.module.params["disks"][position]["name"]
                                    )
                                elif disk_change == "name_desc":
                                    self.xapi_session.xenapi.VDI.set_name_description(
                                        vdi_ref, self.module.params["disks"][position]["name_desc"]
                                    )
                                elif disk_change == "size":
                                    self.xapi_session.xenapi.VDI.resize(
                                        vdi_ref,
                                        str(
                                            self.get_normalized_disk_size(
                                                self.module.params["disks"][position],
                                                f"VM reconfigure disks[{position}]: ",
                                            )
                                        ),
                                    )

                    elif change.get("disks_new"):
                        for position, disk_userdevice in change["disks_new"]:
                            disk_params = self.module.params["disks"][position]

                            disk_name = (
                                disk_params["name"]
                                if disk_params.get("name")
                                else f"{self.vm_params['name_label']}-{position}"
                            )
                            disk_name_desc = disk_params["name_desc"] if disk_params.get("name_desc") else ""

                            if disk_params.get("sr_uuid"):
                                sr_ref = self.xapi_session.xenapi.SR.get_by_uuid(disk_params["sr_uuid"])
                            elif disk_params.get("sr"):
                                sr_ref = self.xapi_session.xenapi.SR.get_by_name_label(disk_params["sr"])[0]
                            else:
                                sr_ref = self.default_sr_ref

                            disk_size = str(
                                self.get_normalized_disk_size(
                                    self.module.params["disks"][position], f"VM reconfigure disks[{position}]: "
                                )
                            )

                            new_disk_vdi = {
                                "name_label": disk_name,
                                "name_description": disk_name_desc,
                                "SR": sr_ref,
                                "virtual_size": disk_size,
                                "type": "user",
                                "sharable": False,
                                "read_only": False,
                                "other_config": {},
                            }

                            new_disk_vbd = {
                                "VM": self.vm_ref,
                                "VDI": None,
                                "userdevice": disk_userdevice,
                                "bootable": False,
                                "mode": "RW",
                                "type": "Disk",
                                "empty": False,
                                "other_config": {},
                                "qos_algorithm_type": "",
                                "qos_algorithm_params": {},
                            }

                            new_disk_vbd["VDI"] = self.xapi_session.xenapi.VDI.create(new_disk_vdi)
                            vbd_ref_new = self.xapi_session.xenapi.VBD.create(new_disk_vbd)

                            if self.vm_params["power_state"].lower() == "running":
                                self.xapi_session.xenapi.VBD.plug(vbd_ref_new)

                    elif change.get("networks_changed"):
                        for position, network_change_list in enumerate(change["networks_changed"]):
                            if network_change_list:
                                vm_vif_params = self.vm_params["VIFs"][position]
                                network_params = self.module.params["networks"][position]

                                vif_ref = self.xapi_session.xenapi.VIF.get_by_uuid(vm_vif_params["uuid"])
                                network_ref = self.xapi_session.xenapi.network.get_by_uuid(
                                    vm_vif_params["network"]["uuid"]
                                )

                                vif_recreated = False

                                if "name" in network_change_list or "mac" in network_change_list:
                                    # To change network or MAC, we destroy old
                                    # VIF and then create a new one with changed
                                    # parameters. That's how XenCenter does it.

                                    # Copy all old parameters to new VIF record.
                                    vif = {
                                        "device": vm_vif_params["device"],
                                        "network": network_ref,
                                        "VM": vm_vif_params["VM"],
                                        "MAC": vm_vif_params["MAC"],
                                        "MTU": vm_vif_params["MTU"],
                                        "other_config": vm_vif_params["other_config"],
                                        "qos_algorithm_type": vm_vif_params["qos_algorithm_type"],
                                        "qos_algorithm_params": vm_vif_params["qos_algorithm_params"],
                                        "locking_mode": vm_vif_params["locking_mode"],
                                        "ipv4_allowed": vm_vif_params["ipv4_allowed"],
                                        "ipv6_allowed": vm_vif_params["ipv6_allowed"],
                                    }

                                    if "name" in network_change_list:
                                        network_ref_new = self.xapi_session.xenapi.network.get_by_name_label(
                                            network_params["name"]
                                        )[0]
                                        vif["network"] = network_ref_new
                                        vif["MTU"] = self.xapi_session.xenapi.network.get_MTU(network_ref_new)

                                    if "mac" in network_change_list:
                                        vif["MAC"] = network_params["mac"].lower()

                                    if self.vm_params["power_state"].lower() == "running":
                                        self.xapi_session.xenapi.VIF.unplug(vif_ref)

                                    self.xapi_session.xenapi.VIF.destroy(vif_ref)
                                    vif_ref_new = self.xapi_session.xenapi.VIF.create(vif)

                                    if self.vm_params["power_state"].lower() == "running":
                                        self.xapi_session.xenapi.VIF.plug(vif_ref_new)

                                    vif_ref = vif_ref_new
                                    vif_recreated = True

                                if self.vm_params["customization_agent"] == "native":
                                    vif_reconfigure_needed = False

                                    if "type" in network_change_list:
                                        network_type = network_params["type"].capitalize()
                                        vif_reconfigure_needed = True
                                    else:
                                        network_type = vm_vif_params["ipv4_configuration_mode"]

                                    if "ip" in network_change_list:
                                        network_ip = network_params["ip"]
                                        vif_reconfigure_needed = True
                                    elif vm_vif_params["ipv4_addresses"]:
                                        network_ip = vm_vif_params["ipv4_addresses"][0].split("/")[0]
                                    else:
                                        network_ip = ""

                                    if "prefix" in network_change_list:
                                        network_prefix = f"/{network_params['prefix']}"
                                        vif_reconfigure_needed = True
                                    elif vm_vif_params["ipv4_addresses"] and vm_vif_params["ipv4_addresses"][0]:
                                        network_prefix = f"/{vm_vif_params['ipv4_addresses'][0].split('/')[1]}"
                                    else:
                                        network_prefix = ""

                                    if "gateway" in network_change_list:
                                        network_gateway = network_params["gateway"]
                                        vif_reconfigure_needed = True
                                    else:
                                        network_gateway = vm_vif_params["ipv4_gateway"]

                                    if vif_recreated or vif_reconfigure_needed:
                                        self.xapi_session.xenapi.VIF.configure_ipv4(
                                            vif_ref, network_type, f"{network_ip}{network_prefix}", network_gateway
                                        )

                                    vif_reconfigure_needed = False

                                    if "type6" in network_change_list:
                                        network_type6 = network_params["type6"].capitalize()
                                        vif_reconfigure_needed = True
                                    else:
                                        network_type6 = vm_vif_params["ipv6_configuration_mode"]

                                    if "ip6" in network_change_list:
                                        network_ip6 = network_params["ip6"]
                                        vif_reconfigure_needed = True
                                    elif vm_vif_params["ipv6_addresses"]:
                                        network_ip6 = vm_vif_params["ipv6_addresses"][0].split("/")[0]
                                    else:
                                        network_ip6 = ""

                                    if "prefix6" in network_change_list:
                                        network_prefix6 = f"/{network_params['prefix6']}"
                                        vif_reconfigure_needed = True
                                    elif vm_vif_params["ipv6_addresses"] and vm_vif_params["ipv6_addresses"][0]:
                                        network_prefix6 = f"/{vm_vif_params['ipv6_addresses'][0].split('/')[1]}"
                                    else:
                                        network_prefix6 = ""

                                    if "gateway6" in network_change_list:
                                        network_gateway6 = network_params["gateway6"]
                                        vif_reconfigure_needed = True
                                    else:
                                        network_gateway6 = vm_vif_params["ipv6_gateway"]

                                    if vif_recreated or vif_reconfigure_needed:
                                        self.xapi_session.xenapi.VIF.configure_ipv6(
                                            vif_ref, network_type6, f"{network_ip6}{network_prefix6}", network_gateway6
                                        )

                                elif self.vm_params["customization_agent"] == "custom":
                                    vif_device = vm_vif_params["device"]

                                    # A user could have manually changed network
                                    # or mac e.g. through XenCenter and then also
                                    # make those changes in playbook manually.
                                    # In that case, module will not detect any
                                    # changes and info in xenstore_data will
                                    # become stale. For that reason we always
                                    # update name and mac in xenstore_data.

                                    # Since we handle name and mac differently,
                                    # we have to remove them from
                                    # network_change_list.
                                    network_change_list_tmp = [
                                        net_chg for net_chg in network_change_list if net_chg not in ["name", "mac"]
                                    ]

                                    for network_change in network_change_list_tmp + ["name", "mac"]:
                                        self.xapi_session.xenapi.VM.remove_from_xenstore_data(
                                            self.vm_ref, f"vm-data/networks/{vif_device}/{network_change}"
                                        )

                                    if network_params.get("name"):
                                        network_name = network_params["name"]
                                    else:
                                        network_name = vm_vif_params["network"]["name_label"]

                                    self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                        self.vm_ref, f"vm-data/networks/{vif_device}/name", network_name
                                    )

                                    if network_params.get("mac"):
                                        network_mac = network_params["mac"].lower()
                                    else:
                                        network_mac = vm_vif_params["MAC"].lower()

                                    self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                        self.vm_ref, f"vm-data/networks/{vif_device}/mac", network_mac
                                    )

                                    for network_change in network_change_list_tmp:
                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                            self.vm_ref,
                                            f"vm-data/networks/{vif_device}/{network_change}",
                                            network_params[network_change],
                                        )

                    elif change.get("networks_new"):
                        for position, vif_device in change["networks_new"]:
                            network_params = self.module.params["networks"][position]

                            network_ref = self.xapi_session.xenapi.network.get_by_name_label(network_params["name"])[0]

                            network_name = network_params["name"]
                            network_mac = network_params["mac"] if network_params.get("mac") else ""
                            network_type = network_params.get("type")
                            network_ip = network_params["ip"] if network_params.get("ip") else ""
                            network_prefix = network_params["prefix"] if network_params.get("prefix") else ""
                            network_netmask = network_params["netmask"] if network_params.get("netmask") else ""
                            network_gateway = network_params["gateway"] if network_params.get("gateway") else ""
                            network_type6 = network_params.get("type6")
                            network_ip6 = network_params["ip6"] if network_params.get("ip6") else ""
                            network_prefix6 = network_params["prefix6"] if network_params.get("prefix6") else ""
                            network_gateway6 = network_params["gateway6"] if network_params.get("gateway6") else ""

                            vif = {
                                "device": vif_device,
                                "network": network_ref,
                                "VM": self.vm_ref,
                                "MAC": network_mac,
                                "MTU": self.xapi_session.xenapi.network.get_MTU(network_ref),
                                "other_config": {},
                                "qos_algorithm_type": "",
                                "qos_algorithm_params": {},
                            }

                            vif_ref_new = self.xapi_session.xenapi.VIF.create(vif)

                            if self.vm_params["power_state"].lower() == "running":
                                self.xapi_session.xenapi.VIF.plug(vif_ref_new)

                            if self.vm_params["customization_agent"] == "native":
                                if network_type and network_type == "static":
                                    self.xapi_session.xenapi.VIF.configure_ipv4(
                                        vif_ref_new, "Static", f"{network_ip}/{network_prefix}", network_gateway
                                    )

                                if network_type6 and network_type6 == "static":
                                    self.xapi_session.xenapi.VIF.configure_ipv6(
                                        vif_ref_new, "Static", f"{network_ip6}/{network_prefix6}", network_gateway6
                                    )
                            elif self.vm_params["customization_agent"] == "custom":
                                # We first have to remove any existing data
                                # from xenstore_data because there could be
                                # some old leftover data from some interface
                                # that once occupied same device location as
                                # our new interface.
                                for network_param in [
                                    "name",
                                    "mac",
                                    "type",
                                    "ip",
                                    "prefix",
                                    "netmask",
                                    "gateway",
                                    "type6",
                                    "ip6",
                                    "prefix6",
                                    "gateway6",
                                ]:
                                    self.xapi_session.xenapi.VM.remove_from_xenstore_data(
                                        self.vm_ref, f"vm-data/networks/{vif_device}/{network_param}"
                                    )

                                self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                    self.vm_ref, f"vm-data/networks/{vif_device}/name", network_name
                                )

                                # We get MAC from VIF itself instead of
                                # networks.mac because it could be
                                # autogenerated.
                                vm_vif_mac = self.xapi_session.xenapi.VIF.get_MAC(vif_ref_new)
                                self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                    self.vm_ref, f"vm-data/networks/{vif_device}/mac", vm_vif_mac
                                )

                                if network_type:
                                    self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                        self.vm_ref, f"vm-data/networks/{vif_device}/type", network_type
                                    )

                                    if network_type == "static":
                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                            self.vm_ref, f"vm-data/networks/{vif_device}/ip", network_ip
                                        )
                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                            self.vm_ref, f"vm-data/networks/{vif_device}/prefix", network_prefix
                                        )
                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                            self.vm_ref, f"vm-data/networks/{vif_device}/netmask", network_netmask
                                        )
                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                            self.vm_ref, f"vm-data/networks/{vif_device}/gateway", network_gateway
                                        )

                                if network_type6:
                                    self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                        self.vm_ref, f"vm-data/networks/{vif_device}/type6", network_type6
                                    )

                                    if network_type6 == "static":
                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                            self.vm_ref, f"vm-data/networks/{vif_device}/ip6", network_ip6
                                        )
                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                            self.vm_ref, f"vm-data/networks/{vif_device}/prefix6", network_prefix6
                                        )
                                        self.xapi_session.xenapi.VM.add_to_xenstore_data(
                                            self.vm_ref, f"vm-data/networks/{vif_device}/gateway6", network_gateway6
                                        )

                    elif change.get("custom_params"):
                        for position in change["custom_params"]:
                            custom_param_key = self.module.params["custom_params"][position]["key"]
                            custom_param_value = self.module.params["custom_params"][position]["value"]
                            self.xapi_session.xenapi_request(
                                f"VM.set_{custom_param_key}", (self.vm_ref, custom_param_value)
                            )

            if self.module.params["is_template"]:
                self.xapi_session.xenapi.VM.set_is_a_template(self.vm_ref, True)
            elif (
                "need_poweredoff" in config_changes and self.module.params["force"] and vm_power_state_save != "halted"
            ):
                self.set_power_state("poweredon")

            # Gather new params after reconfiguration.
            self.gather_params()

        except XenAPI.Failure as f:
            self.module.fail_json(msg=f"XAPI ERROR: {f.details}")

        return config_changes

    def destroy(self):
        """Removes an existing VM with associated disks"""
        # Safety check.
        if not self.exists():
            self.module.fail_json(msg="Called destroy on non existing VM!")

        if self.vm_params["power_state"].lower() != "halted" and not self.module.params["force"]:
            self.module.fail_json(
                msg="VM destroy: VM has to be in powered off state to destroy but force was not specified!"
            )

        # Support for Ansible check mode.
        if self.module.check_mode:
            return

        # Make sure that VM is poweredoff before we can destroy it.
        self.set_power_state("poweredoff")

        try:
            # Destroy VM!
            self.xapi_session.xenapi.VM.destroy(self.vm_ref)

            vm_disk_params_list = [
                disk_params for disk_params in self.vm_params["VBDs"] if disk_params["type"] == "Disk"
            ]

            # Destroy all VDIs associated with VM!
            for vm_disk_params in vm_disk_params_list:
                vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(vm_disk_params["VDI"]["uuid"])

                self.xapi_session.xenapi.VDI.destroy(vdi_ref)

        except XenAPI.Failure as f:
            self.module.fail_json(msg=f"XAPI ERROR: {f.details}")

    def get_changes(self):
        """Finds VM parameters that differ from specified ones.

        This method builds a dictionary with hierarchy of VM parameters
        that differ from those specified in module parameters.

        Returns:
            list: VM parameters that differ from those specified in
            module parameters.
        """
        # Safety check.
        if not self.exists():
            self.module.fail_json(msg="Called get_changes on non existing VM!")

        need_poweredoff = False

        if self.module.params["is_template"]:
            need_poweredoff = True

        try:
            # This VM could be a template or a snapshot. In that case we fail
            # because we can't reconfigure them or it would just be too
            # dangerous.
            if self.vm_params["is_a_template"] and not self.vm_params["is_a_snapshot"]:
                self.module.fail_json(
                    msg="VM check: targeted VM is a template! Template reconfiguration is not supported."
                )

            if self.vm_params["is_a_snapshot"]:
                self.module.fail_json(
                    msg="VM check: targeted VM is a snapshot! Snapshot reconfiguration is not supported."
                )

            # Let's build a list of parameters that changed.
            config_changes = []

            # Name could only differ if we found an existing VM by uuid.
            if self.module.params["name"] is not None and self.module.params["name"] != self.vm_params["name_label"]:
                if self.module.params["name"]:
                    config_changes.append("name")
                else:
                    self.module.fail_json(msg="VM check name: VM name cannot be an empty string!")

            if (
                self.module.params["name_desc"] is not None
                and self.module.params["name_desc"] != self.vm_params["name_description"]
            ):
                config_changes.append("name_desc")

            # Folder parameter is found in other_config.
            vm_other_config = self.vm_params["other_config"]
            vm_folder = vm_other_config.get("folder", "")

            if self.module.params["folder"] is not None and self.module.params["folder"] != vm_folder:
                config_changes.append("folder")

            if self.module.params["home_server"] is not None:
                if self.module.params["home_server"] and (
                    not self.vm_params["affinity"]
                    or self.module.params["home_server"] != self.vm_params["affinity"]["name_label"]
                ):
                    # Check existence only. Ignore return value.
                    get_object_ref(
                        self.module,
                        self.module.params["home_server"],
                        uuid=None,
                        obj_type="home server",
                        fail=True,
                        msg_prefix="VM check home_server: ",
                    )

                    config_changes.append("home_server")
                elif not self.module.params["home_server"] and self.vm_params["affinity"]:
                    config_changes.append("home_server")

            config_changes_hardware = []

            if self.module.params["hardware"]:
                num_cpus = self.module.params["hardware"].get("num_cpus")

                if num_cpus is not None:
                    # Kept for compatibility with older Ansible versions that
                    # do not support subargument specs.
                    try:
                        num_cpus = int(num_cpus)
                    except ValueError:
                        self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be an integer value!")

                    if num_cpus < 1:
                        self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be greater than zero!")

                    # We can use VCPUs_at_startup or VCPUs_max parameter. I'd
                    # say the former is the way to go but this needs
                    # confirmation and testing.
                    if num_cpus != int(self.vm_params["VCPUs_at_startup"]):
                        config_changes_hardware.append("num_cpus")
                        # For now, we don't support hotpluging so VM has to be in
                        # poweredoff state to reconfigure.
                        need_poweredoff = True

                num_cpu_cores_per_socket = self.module.params["hardware"].get("num_cpu_cores_per_socket")

                if num_cpu_cores_per_socket is not None:
                    # Kept for compatibility with older Ansible versions that
                    # do not support subargument specs.
                    try:
                        num_cpu_cores_per_socket = int(num_cpu_cores_per_socket)
                    except ValueError:
                        self.module.fail_json(
                            msg="VM check hardware.num_cpu_cores_per_socket: parameter should be an integer value!"
                        )

                    if num_cpu_cores_per_socket < 1:
                        self.module.fail_json(
                            msg="VM check hardware.num_cpu_cores_per_socket: parameter should be greater than zero!"
                        )

                    if num_cpus and num_cpus % num_cpu_cores_per_socket != 0:
                        self.module.fail_json(
                            msg="VM check hardware.num_cpus: parameter should be a multiple of hardware.num_cpu_cores_per_socket!"
                        )

                    vm_platform = self.vm_params["platform"]
                    vm_cores_per_socket = int(vm_platform.get("cores-per-socket", 1))

                    if num_cpu_cores_per_socket != vm_cores_per_socket:
                        config_changes_hardware.append("num_cpu_cores_per_socket")
                        # For now, we don't support hotpluging so VM has to be
                        # in poweredoff state to reconfigure.
                        need_poweredoff = True

                memory_mb = self.module.params["hardware"].get("memory_mb")

                if memory_mb is not None:
                    # Kept for compatibility with older Ansible versions that
                    # do not support subargument specs.
                    try:
                        memory_mb = int(memory_mb)
                    except ValueError:
                        self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be an integer value!")

                    if memory_mb < 1:
                        self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be greater than zero!")

                    # There are multiple memory parameters:
                    #     - memory_dynamic_max
                    #     - memory_dynamic_min
                    #     - memory_static_max
                    #     - memory_static_min
                    #     - memory_target
                    #
                    # memory_target seems like a good candidate but it returns 0 for
                    # halted VMs so we can't use it.
                    #
                    # I decided to use memory_dynamic_max and memory_static_max
                    # and use whichever is larger. This strategy needs validation
                    # and testing.
                    #
                    # XenServer stores memory size in bytes so we need to divide
                    # it by 1024*1024 = 1048576.
                    if memory_mb != int(
                        max(int(self.vm_params["memory_dynamic_max"]), int(self.vm_params["memory_static_max"]))
                        / 1048576
                    ):
                        config_changes_hardware.append("memory_mb")
                        # For now, we don't support hotpluging so VM has to be in
                        # poweredoff state to reconfigure.
                        need_poweredoff = True

            if config_changes_hardware:
                config_changes.append({"hardware": config_changes_hardware})

            # Find allowed userdevices.
            vbd_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref)

            config_changes_cdrom = []

            if self.module.params["cdrom"]:
                # Get the list of all CD-ROMs. Filter out any regular disks
                # found. If we found no existing CD-ROM, we will create it
                # later else take the first one found.
                vm_cdrom_params_list = [
                    cdrom_params for cdrom_params in self.vm_params["VBDs"] if cdrom_params["type"] == "CD"
                ]

                # If no existing CD-ROM is found, we will need to add one.
                if not vm_cdrom_params_list:
                    # We need to check if there is any userdevice allowed.
                    if not vbd_userdevices_allowed:
                        self.module.fail_json(msg="VM check cdrom: maximum number of devices reached!")

                    # If userdevice position 3 is available, we reserve it for
                    # CD-ROM to prevent any disk taking the same position. If the
                    # position 3 is not available, we reserve the first available
                    # one to prevent any disk from taking it.
                    if "3" in vbd_userdevices_allowed:
                        vbd_userdevices_allowed.remove("3")
                    else:
                        vbd_userdevices_allowed.pop(0)

                cdrom_type = self.module.params["cdrom"].get("type")
                cdrom_iso_name = self.module.params["cdrom"].get("iso_name")

                # If cdrom.iso_name is specified but cdrom.type is not,
                # then set cdrom.type to 'iso', unless cdrom.iso_name is
                # an empty string, in that case set cdrom.type to 'none'.
                if not cdrom_type:
                    if cdrom_iso_name:
                        cdrom_type = "iso"
                    elif cdrom_iso_name is not None:
                        cdrom_type = "none"

                    self.module.params["cdrom"]["type"] = cdrom_type

                # If type changed.
                if cdrom_type and (
                    not vm_cdrom_params_list or cdrom_type != self.get_cdrom_type(vm_cdrom_params_list[0])
                ):
                    config_changes_cdrom.append("type")

                if cdrom_type == "iso":
                    # Check if ISO exists.
                    # Check existence only. Ignore return value.
                    get_object_ref(
                        self.module,
                        cdrom_iso_name,
                        uuid=None,
                        obj_type="ISO image",
                        fail=True,
                        msg_prefix="VM check cdrom.iso_name: ",
                    )

                    # Is ISO image changed?
                    if cdrom_iso_name and (
                        not vm_cdrom_params_list
                        or not vm_cdrom_params_list[0]["VDI"]
                        or cdrom_iso_name != vm_cdrom_params_list[0]["VDI"]["name_label"]
                    ):
                        config_changes_cdrom.append("iso_name")

            if config_changes_cdrom:
                config_changes.append({"cdrom": config_changes_cdrom})

            config_changes_disks = []
            config_new_disks = []

            if self.module.params["disks"]:
                # Get the list of all disk. Filter out any CDs found.
                vm_disk_params_list = [
                    disk_params for disk_params in self.vm_params["VBDs"] if disk_params["type"] == "Disk"
                ]

                # Number of disks defined in module params have to be same or
                # higher than a number of existing disks attached to the VM.
                # We don't support removal or detachment of disks.
                if len(self.module.params["disks"]) < len(vm_disk_params_list):
                    self.module.fail_json(
                        msg=f"VM check disks: provided disks configuration has less disks than the "
                        f"target VM ({len(self.module.params['disks'])} < {len(vm_disk_params_list)})!"
                    )

                # Find the highest disk occupied userdevice.
                if not vm_disk_params_list:
                    vm_disk_userdevice_highest = "-1"
                else:
                    vm_disk_userdevice_highest = vm_disk_params_list[-1]["userdevice"]

                for position in range(len(self.module.params["disks"])):
                    if position < len(vm_disk_params_list):
                        vm_disk_params = vm_disk_params_list[position]
                    else:
                        vm_disk_params = None

                    disk_params = self.module.params["disks"][position]

                    disk_size = self.get_normalized_disk_size(
                        self.module.params["disks"][position], f"VM check disks[{position}]: "
                    )

                    disk_name = disk_params.get("name")

                    if disk_name is not None and not disk_name:
                        self.module.fail_json(msg=f"VM check disks[{position}]: disk name cannot be an empty string!")

                    # If this is an existing disk.
                    if vm_disk_params and vm_disk_params["VDI"]:
                        disk_changes = []

                        if disk_name and disk_name != vm_disk_params["VDI"]["name_label"]:
                            disk_changes.append("name")

                        disk_name_desc = disk_params.get("name_desc")

                        if disk_name_desc is not None and disk_name_desc != vm_disk_params["VDI"]["name_description"]:
                            disk_changes.append("name_desc")

                        if disk_size:
                            if disk_size > int(vm_disk_params["VDI"]["virtual_size"]):
                                disk_changes.append("size")
                                need_poweredoff = True
                            elif disk_size < int(vm_disk_params["VDI"]["virtual_size"]):
                                self.module.fail_json(
                                    msg=f"VM check disks[{position}]: disk size is smaller than existing ({disk_size} bytes < "
                                    f"{vm_disk_params['VDI']['virtual_size']} bytes). Reducing disk size is not allowed!"
                                )

                        config_changes_disks.append(disk_changes)
                    # If this is a new disk.
                    else:
                        if not disk_size:
                            self.module.fail_json(
                                msg=f"VM check disks[{position}]: no valid disk size specification found!"
                            )

                        disk_sr_uuid = disk_params.get("sr_uuid")
                        disk_sr = disk_params.get("sr")

                        if disk_sr_uuid is not None or disk_sr is not None:
                            # Check existence only. Ignore return value.
                            get_object_ref(
                                self.module,
                                disk_sr,
                                disk_sr_uuid,
                                obj_type="SR",
                                fail=True,
                                msg_prefix=f"VM check disks[{position}]: ",
                            )
                        elif self.default_sr_ref == "OpaqueRef:NULL":
                            self.module.fail_json(
                                msg=f"VM check disks[{position}]: no default SR found! You must specify SR explicitly."
                            )

                        if not vbd_userdevices_allowed:
                            self.module.fail_json(msg=f"VM check disks[{position}]: maximum number of devices reached!")

                        disk_userdevice = None

                        # We need to place a new disk right above the highest
                        # placed existing disk to maintain relative disk
                        # positions pairable with disk specifications in
                        # module params. That place must not be occupied by
                        # some other device like CD-ROM. We also skip the
                        # position number 3 to keep it reserved for the
                        # CD-ROM drive. If CD-ROM drive ends up in the
                        # position above 3 because we used it for a disk,
                        # booting from ISO will not work (#11624).
                        for userdevice in vbd_userdevices_allowed:
                            if userdevice != "3" and int(userdevice) > int(vm_disk_userdevice_highest):
                                disk_userdevice = userdevice
                                vbd_userdevices_allowed.remove(userdevice)
                                vm_disk_userdevice_highest = userdevice
                                break

                        # If no place was found.
                        if disk_userdevice is None:
                            # Highest occupied place could be a CD-ROM device
                            # so we have to include all devices regardless of
                            # type when calculating out-of-bound position.
                            disk_userdevice = str(int(self.vm_params["VBDs"][-1]["userdevice"]) + 1)
                            self.module.fail_json(
                                msg=f"VM check disks[{position}]: new disk position {disk_userdevice} is out of bounds!"
                            )

                        # For new disks we only track their position.
                        config_new_disks.append((position, disk_userdevice))

            # We should append config_changes_disks to config_changes only
            # if there is at least one changed disk, else skip.
            for disk_change in config_changes_disks:
                if disk_change:
                    config_changes.append({"disks_changed": config_changes_disks})
                    break

            if config_new_disks:
                config_changes.append({"disks_new": config_new_disks})

            config_changes_networks = []
            config_new_networks = []

            # Find allowed devices.
            vif_devices_allowed = self.xapi_session.xenapi.VM.get_allowed_VIF_devices(self.vm_ref)

            if self.module.params["networks"]:
                # Number of VIFs defined in module params have to be same or
                # higher than a number of existing VIFs attached to the VM.
                # We don't support removal of VIFs.
                if len(self.module.params["networks"]) < len(self.vm_params["VIFs"]):
                    self.module.fail_json(
                        msg=f"VM check networks: provided networks configuration has less interfaces than the target "
                        f"VM ({len(self.module.params['networks'])} < {len(self.vm_params['VIFs'])})!"
                    )

                # Find the highest occupied device.
                if not self.vm_params["VIFs"]:
                    vif_device_highest = "-1"
                else:
                    vif_device_highest = self.vm_params["VIFs"][-1]["device"]

                for position in range(len(self.module.params["networks"])):
                    if position < len(self.vm_params["VIFs"]):
                        vm_vif_params = self.vm_params["VIFs"][position]
                    else:
                        vm_vif_params = None

                    network_params = self.module.params["networks"][position]

                    network_name = network_params.get("name")

                    if network_name is not None and not network_name:
                        self.module.fail_json(
                            msg=f"VM check networks[{position}]: network name cannot be an empty string!"
                        )

                    if network_name:
                        # Check existence only. Ignore return value.
                        get_object_ref(
                            self.module,
                            network_name,
                            uuid=None,
                            obj_type="network",
                            fail=True,
                            msg_prefix=f"VM check networks[{position}]: ",
                        )

                    network_mac = network_params.get("mac")

                    if network_mac is not None:
                        network_mac = network_mac.lower()

                        if not is_mac(network_mac):
                            self.module.fail_json(
                                msg=f"VM check networks[{position}]: specified MAC address '{network_mac}' is not valid!"
                            )

                    # IPv4 reconfiguration.
                    network_type = network_params.get("type")
                    network_ip = network_params.get("ip")
                    network_netmask = network_params.get("netmask")
                    network_prefix = None

                    # If networks.ip is specified and networks.type is not,
                    # then set networks.type to 'static'.
                    if not network_type and network_ip:
                        network_type = "static"

                    # XenServer natively supports only 'none' and 'static'
                    # type with 'none' being the same as 'dhcp'.
                    if self.vm_params["customization_agent"] == "native" and network_type and network_type == "dhcp":
                        network_type = "none"

                    if network_type and network_type == "static":
                        if network_ip is not None:
                            network_ip_split = network_ip.split("/")
                            network_ip = network_ip_split[0]

                            if network_ip and not is_valid_ip_addr(network_ip):
                                self.module.fail_json(
                                    msg=f"VM check networks[{position}]: specified IPv4 address '{network_ip}' is not valid!"
                                )

                            if len(network_ip_split) > 1:
                                network_prefix = network_ip_split[1]

                                if not is_valid_ip_prefix(network_prefix):
                                    self.module.fail_json(
                                        msg=f"VM check networks[{position}]: specified IPv4 prefix '{network_prefix}' is not valid!"
                                    )

                        if network_netmask is not None:
                            if not is_valid_ip_netmask(network_netmask):
                                self.module.fail_json(
                                    msg=f"VM check networks[{position}]: specified IPv4 netmask '{network_netmask}' is not valid!"
                                )

                            network_prefix = ip_netmask_to_prefix(network_netmask, skip_check=True)
                        elif network_prefix is not None:
                            network_netmask = ip_prefix_to_netmask(network_prefix, skip_check=True)

                    # If any parameter is overridden at this point, update it.
                    if network_type:
                        network_params["type"] = network_type

                    if network_ip:
                        network_params["ip"] = network_ip

                    if network_netmask:
                        network_params["netmask"] = network_netmask

                    if network_prefix:
                        network_params["prefix"] = network_prefix

                    network_gateway = network_params.get("gateway")

                    # Gateway can be an empty string (when removing gateway
                    # configuration) but if it is not, it should be validated.
                    if network_gateway and not is_valid_ip_addr(network_gateway):
                        self.module.fail_json(
                            msg=f"VM check networks[{position}]: specified IPv4 gateway '{network_gateway}' is not valid!"
                        )

                    # IPv6 reconfiguration.
                    network_type6 = network_params.get("type6")
                    network_ip6 = network_params.get("ip6")
                    network_prefix6 = None

                    # If networks.ip6 is specified and networks.type6 is not,
                    # then set networks.type6 to 'static'.
                    if not network_type6 and network_ip6:
                        network_type6 = "static"

                    # XenServer natively supports only 'none' and 'static'
                    # type with 'none' being the same as 'dhcp'.
                    if self.vm_params["customization_agent"] == "native" and network_type6 and network_type6 == "dhcp":
                        network_type6 = "none"

                    if network_type6 and network_type6 == "static":
                        if network_ip6 is not None:
                            network_ip6_split = network_ip6.split("/")
                            network_ip6 = network_ip6_split[0]

                            if network_ip6 and not is_valid_ip6_addr(network_ip6):
                                self.module.fail_json(
                                    msg=f"VM check networks[{position}]: specified IPv6 address '{network_ip6}' is not valid!"
                                )

                            if len(network_ip6_split) > 1:
                                network_prefix6 = network_ip6_split[1]

                                if not is_valid_ip6_prefix(network_prefix6):
                                    self.module.fail_json(
                                        msg=f"VM check networks[{position}]: specified IPv6 prefix '{network_prefix6}' is not valid!"
                                    )

                    # If any parameter is overridden at this point, update it.
                    if network_type6:
                        network_params["type6"] = network_type6

                    if network_ip6:
                        network_params["ip6"] = network_ip6

                    if network_prefix6:
                        network_params["prefix6"] = network_prefix6

                    network_gateway6 = network_params.get("gateway6")

                    # Gateway can be an empty string (when removing gateway
                    # configuration) but if it is not, it should be validated.
                    if network_gateway6 and not is_valid_ip6_addr(network_gateway6):
                        self.module.fail_json(
                            msg=f"VM check networks[{position}]: specified IPv6 gateway '{network_gateway6}' is not valid!"
                        )

                    # If this is an existing VIF.
                    if vm_vif_params and vm_vif_params["network"]:
                        network_changes = []

                        if network_name and network_name != vm_vif_params["network"]["name_label"]:
                            network_changes.append("name")

                        if network_mac and network_mac != vm_vif_params["MAC"].lower():
                            network_changes.append("mac")

                        if self.vm_params["customization_agent"] == "native":
                            if network_type and network_type != vm_vif_params["ipv4_configuration_mode"].lower():
                                network_changes.append("type")

                            if network_type and network_type == "static":
                                if network_ip and (
                                    not vm_vif_params["ipv4_addresses"]
                                    or not vm_vif_params["ipv4_addresses"][0]
                                    or network_ip != vm_vif_params["ipv4_addresses"][0].split("/")[0]
                                ):
                                    network_changes.append("ip")

                                if network_prefix and (
                                    not vm_vif_params["ipv4_addresses"]
                                    or not vm_vif_params["ipv4_addresses"][0]
                                    or network_prefix != vm_vif_params["ipv4_addresses"][0].split("/")[1]
                                ):
                                    network_changes.append("prefix")
                                    network_changes.append("netmask")

                                if network_gateway is not None and network_gateway != vm_vif_params["ipv4_gateway"]:
                                    network_changes.append("gateway")

                            if network_type6 and network_type6 != vm_vif_params["ipv6_configuration_mode"].lower():
                                network_changes.append("type6")

                            if network_type6 and network_type6 == "static":
                                if network_ip6 and (
                                    not vm_vif_params["ipv6_addresses"]
                                    or not vm_vif_params["ipv6_addresses"][0]
                                    or network_ip6 != vm_vif_params["ipv6_addresses"][0].split("/")[0]
                                ):
                                    network_changes.append("ip6")

                                if network_prefix6 and (
                                    not vm_vif_params["ipv6_addresses"]
                                    or not vm_vif_params["ipv6_addresses"][0]
                                    or network_prefix6 != vm_vif_params["ipv6_addresses"][0].split("/")[1]
                                ):
                                    network_changes.append("prefix6")

                                if network_gateway6 is not None and network_gateway6 != vm_vif_params["ipv6_gateway"]:
                                    network_changes.append("gateway6")

                        elif self.vm_params["customization_agent"] == "custom":
                            vm_xenstore_data = self.vm_params["xenstore_data"]

                            if network_type and network_type != vm_xenstore_data.get(
                                f"vm-data/networks/{vm_vif_params['device']}/type", "none"
                            ):
                                network_changes.append("type")
                                need_poweredoff = True

                            if network_type and network_type == "static":
                                if network_ip and network_ip != vm_xenstore_data.get(
                                    f"vm-data/networks/{vm_vif_params['device']}/ip", ""
                                ):
                                    network_changes.append("ip")
                                    need_poweredoff = True

                                if network_prefix and network_prefix != vm_xenstore_data.get(
                                    f"vm-data/networks/{vm_vif_params['device']}/prefix", ""
                                ):
                                    network_changes.append("prefix")
                                    network_changes.append("netmask")
                                    need_poweredoff = True

                                _device_gw_path = f"vm-data/networks/{vm_vif_params['device']}/gateway"
                                if network_gateway is not None and network_gateway != vm_xenstore_data.get(
                                    _device_gw_path, ""
                                ):
                                    network_changes.append("gateway")
                                    need_poweredoff = True

                            if network_type6 and network_type6 != vm_xenstore_data.get(
                                f"vm-data/networks/{vm_vif_params['device']}/type6", "none"
                            ):
                                network_changes.append("type6")
                                need_poweredoff = True

                            if network_type6 and network_type6 == "static":
                                if network_ip6 and network_ip6 != vm_xenstore_data.get(
                                    f"vm-data/networks/{vm_vif_params['device']}/ip6", ""
                                ):
                                    network_changes.append("ip6")
                                    need_poweredoff = True

                                if network_prefix6 and network_prefix6 != vm_xenstore_data.get(
                                    f"vm-data/networks/{vm_vif_params['device']}/prefix6", ""
                                ):
                                    network_changes.append("prefix6")
                                    need_poweredoff = True

                                _device_gw6_path = f"vm-data/networks/{vm_vif_params['device']}/gateway6"
                                if network_gateway6 is not None and network_gateway6 != vm_xenstore_data.get(
                                    _device_gw6_path, ""
                                ):
                                    network_changes.append("gateway6")
                                    need_poweredoff = True

                        config_changes_networks.append(network_changes)
                    # If this is a new VIF.
                    else:
                        if not network_name:
                            self.module.fail_json(
                                msg=f"VM check networks[{position}]: network name is required for new network interface!"
                            )

                        if network_type and network_type == "static" and network_ip and not network_netmask:
                            self.module.fail_json(
                                msg=f"VM check networks[{position}]: IPv4 netmask or prefix is required for new network interface!"
                            )

                        if network_type6 and network_type6 == "static" and network_ip6 and not network_prefix6:
                            self.module.fail_json(
                                msg=f"VM check networks[{position}]: IPv6 prefix is required for new network interface!"
                            )

                        # Restart is needed if we are adding new network
                        # interface with IP/gateway parameters specified
                        # and custom agent is used.
                        if self.vm_params["customization_agent"] == "custom":
                            for parameter in ["type", "ip", "prefix", "gateway", "type6", "ip6", "prefix6", "gateway6"]:
                                if network_params.get(parameter):
                                    need_poweredoff = True
                                    break

                        if not vif_devices_allowed:
                            self.module.fail_json(
                                msg=f"VM check networks[{position}]: maximum number of network interfaces reached!"
                            )

                        # We need to place a new network interface right above the
                        # highest placed existing interface to maintain relative
                        # positions pairable with network interface specifications
                        # in module params.
                        vif_device = str(int(vif_device_highest) + 1)

                        if vif_device not in vif_devices_allowed:
                            self.module.fail_json(
                                msg=f"VM check networks[{position}]: new network interface position {vif_device} is out of bounds!"
                            )

                        vif_devices_allowed.remove(vif_device)
                        vif_device_highest = vif_device

                        # For new VIFs we only track their position.
                        config_new_networks.append((position, vif_device))

            # We should append config_changes_networks to config_changes only
            # if there is at least one changed network, else skip.
            for network_change in config_changes_networks:
                if network_change:
                    config_changes.append({"networks_changed": config_changes_networks})
                    break

            if config_new_networks:
                config_changes.append({"networks_new": config_new_networks})

            config_changes_custom_params = []

            if self.module.params["custom_params"]:
                for position in range(len(self.module.params["custom_params"])):
                    custom_param = self.module.params["custom_params"][position]

                    custom_param_key = custom_param["key"]
                    custom_param_value = custom_param["value"]

                    if custom_param_key not in self.vm_params:
                        self.module.fail_json(
                            msg=f"VM check custom_params[{position}]: unknown VM param '{custom_param_key}'!"
                        )

                    if custom_param_value != self.vm_params[custom_param_key]:
                        # We only need to track custom param position.
                        config_changes_custom_params.append(position)

            if config_changes_custom_params:
                config_changes.append({"custom_params": config_changes_custom_params})

            if need_poweredoff:
                config_changes.append("need_poweredoff")

            return config_changes

        except XenAPI.Failure as f:
            self.module.fail_json(msg=f"XAPI ERROR: {f.details}")

    def get_normalized_disk_size(self, disk_params, msg_prefix=""):
        """Parses disk size parameters and returns disk size in bytes.

        This method tries to parse disk size module parameters. It fails
        with an error message if size cannot be parsed.

        Args:
            disk_params (dist): A dictionary with disk parameters.
            msg_prefix (str): A string error messages should be prefixed
                with (default: "").

        Returns:
            int: disk size in bytes if disk size is successfully parsed or
            None if no disk size parameters were found.
        """
        # There should be only single size spec but we make a list of all size
        # specs just in case. Priority is given to 'size' but if not found, we
        # check for 'size_tb', 'size_gb', 'size_mb' etc. and use first one
        # found.
        disk_size_spec = [
            x for x in disk_params.keys() if disk_params[x] is not None and (x.startswith("size_") or x == "size")
        ]

        if disk_size_spec:
            try:
                # size
                if "size" in disk_size_spec:
                    size_regex = re.compile(r"(\d+(?:\.\d+)?)\s*(.*)")
                    disk_size_m = size_regex.match(disk_params["size"])

                    if disk_size_m:
                        size = disk_size_m.group(1)
                        unit = disk_size_m.group(2)
                    else:
                        raise ValueError
                # size_tb, size_gb, size_mb, size_kb, size_b
                else:
                    size = disk_params[disk_size_spec[0]]
                    unit = disk_size_spec[0].split("_")[-1]

                if not unit:
                    unit = "b"
                else:
                    unit = unit.lower()

                if re.match(r"\d+\.\d+", size):
                    # We found float value in string, let's typecast it.
                    if unit == "b":
                        # If we found float but unit is bytes, we get the integer part only.
                        size = int(float(size))
                    else:
                        size = float(size)
                else:
                    # We found int value in string, let's typecast it.
                    size = int(size)

                if not size or size < 0:
                    raise ValueError

            except (TypeError, ValueError, NameError):
                # Common failure
                self.module.fail_json(
                    msg=f"{msg_prefix}failed to parse disk size! Please review value provided using documentation."
                )

            disk_units = dict(tb=4, gb=3, mb=2, kb=1, b=0)

            if unit in disk_units:
                return int(size * (1024 ** disk_units[unit]))
            else:
                str_supported_units = "', '".join(sorted(disk_units.keys(), key=lambda key: disk_units[key]))
                self.module.fail_json(
                    msg=f"{msg_prefix}'{unit}' is not a supported unit for disk size! Supported units are ['{str_supported_units}']."
                )
        else:
            return None

    @staticmethod
    def get_cdrom_type(vm_cdrom_params):
        """Returns VM CD-ROM type."""
        # TODO: implement support for detecting type host. No server to test
        # this on at the moment.
        if vm_cdrom_params["empty"]:
            return "none"
        else:
            return "iso"


def main():
    argument_spec = xenserver_common_argument_spec()
    argument_spec.update(
        state=dict(type="str", default="present", choices=["present", "absent", "poweredon"]),
        name=dict(type="str", aliases=["name_label"]),
        name_desc=dict(type="str"),
        uuid=dict(type="str"),
        template=dict(type="str", aliases=["template_src"]),
        template_uuid=dict(type="str"),
        is_template=dict(type="bool", default=False),
        folder=dict(type="str"),
        hardware=dict(
            type="dict",
            options=dict(
                num_cpus=dict(type="int"),
                num_cpu_cores_per_socket=dict(type="int"),
                memory_mb=dict(type="int"),
            ),
        ),
        disks=dict(
            type="list",
            elements="dict",
            options=dict(
                size=dict(type="str"),
                size_tb=dict(type="str"),
                size_gb=dict(type="str"),
                size_mb=dict(type="str"),
                size_kb=dict(type="str"),
                size_b=dict(type="str"),
                name=dict(type="str", aliases=["name_label"]),
                name_desc=dict(type="str"),
                sr=dict(type="str"),
                sr_uuid=dict(type="str"),
            ),
            aliases=["disk"],
            mutually_exclusive=[
                ["size", "size_tb", "size_gb", "size_mb", "size_kb", "size_b"],
                ["sr", "sr_uuid"],
            ],
        ),
        cdrom=dict(
            type="dict",
            options=dict(
                type=dict(type="str", choices=["none", "iso"]),
                iso_name=dict(type="str"),
            ),
            required_if=[
                ["type", "iso", ["iso_name"]],
            ],
        ),
        networks=dict(
            type="list",
            elements="dict",
            options=dict(
                name=dict(type="str", aliases=["name_label"]),
                mac=dict(type="str"),
                type=dict(type="str", choices=["none", "dhcp", "static"]),
                ip=dict(type="str"),
                netmask=dict(type="str"),
                gateway=dict(type="str"),
                type6=dict(type="str", choices=["none", "dhcp", "static"]),
                ip6=dict(type="str"),
                gateway6=dict(type="str"),
            ),
            aliases=["network"],
            required_if=[
                ["type", "static", ["ip"]],
                ["type6", "static", ["ip6"]],
            ],
        ),
        home_server=dict(type="str"),
        custom_params=dict(
            type="list",
            elements="dict",
            options=dict(
                key=dict(type="str", required=True, no_log=False),
                value=dict(type="raw", required=True),
            ),
        ),
        wait_for_ip_address=dict(type="bool", default=False),
        state_change_timeout=dict(type="int", default=0),
        linked_clone=dict(type="bool", default=False),
        force=dict(type="bool", default=False),
    )

    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
        required_one_of=[
            ["name", "uuid"],
        ],
        mutually_exclusive=[
            ["template", "template_uuid"],
        ],
    )

    result = {"failed": False, "changed": False}

    vm = XenServerVM(module)

    # Find existing VM
    if vm.exists():
        if module.params["state"] == "absent":
            vm.destroy()
            result["changed"] = True
        elif module.params["state"] == "present":
            config_changes = vm.reconfigure()

            if config_changes:
                result["changed"] = True

                # Make new disk and network changes more user friendly
                # and informative.
                for change in config_changes:
                    if isinstance(change, dict):
                        if change.get("disks_new"):
                            disks_new = []

                            for position, userdevice in change["disks_new"]:
                                disk_new_params = {"position": position, "vbd_userdevice": userdevice}
                                disk_params = module.params["disks"][position]

                                for k in disk_params.keys():
                                    if disk_params[k] is not None:
                                        disk_new_params[k] = disk_params[k]

                                disks_new.append(disk_new_params)

                            if disks_new:
                                change["disks_new"] = disks_new

                        elif change.get("networks_new"):
                            networks_new = []

                            for position, device in change["networks_new"]:
                                network_new_params = {"position": position, "vif_device": device}
                                network_params = module.params["networks"][position]

                                for k in network_params.keys():
                                    if network_params[k] is not None:
                                        network_new_params[k] = network_params[k]

                                networks_new.append(network_new_params)

                            if networks_new:
                                change["networks_new"] = networks_new

            result["changes"] = config_changes

        elif module.params["state"] in [
            "poweredon",
            "poweredoff",
            "restarted",
            "shutdownguest",
            "rebootguest",
            "suspended",
        ]:
            result["changed"] = vm.set_power_state(module.params["state"])
    elif module.params["state"] != "absent":
        vm.deploy()
        result["changed"] = True

    if module.params["wait_for_ip_address"] and module.params["state"] != "absent":
        vm.wait_for_ip_address()

    result["instance"] = vm.gather_facts()

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


if __name__ == "__main__":
    main()
