# pylint: disable=C0302
"""
VMware Cloud Module
===================

.. versionadded:: 2015.5.4

The VMware cloud module allows you to manage VMware ESX, ESXi, and vCenter.

See :ref:`Getting started with VMware <cloud-getting-started-vmware>` to get started.

:codeauthor: Nitin Madhok <nmadhok@g.clemson.edu>


Dependencies
============

- pyVmomi Python Module

pyVmomi
-------

PyVmomi can be installed via pip:

.. code-block:: bash

    pip install pyVmomi

.. note::

    Version 6.0 of pyVmomi has some problems with SSL error handling on certain
    versions of Python. If using version 6.0 of pyVmomi, Python 2.6,
    Python 2.7.9, or newer must be present. This is due to an upstream dependency
    in pyVmomi 6.0 that is not supported in Python versions 2.7 to 2.7.8. If the
    version of Python is not in the supported range, you will need to install an
    earlier version of pyVmomi. See `Issue #29537`_ for more information.

.. _Issue #29537: https://github.com/saltstack/salt/issues/29537

Based on the note above, to install an earlier version of pyVmomi than the
version currently listed in PyPi, run the following:

.. code-block:: bash

    pip install pyVmomi==5.5.0.2014.1.1

The 5.5.0.2014.1.1 is a known stable version that this original VMware cloud
driver was developed against.

.. note::
    Ensure python pyVmomi module is installed by running following one-liner
    check. The output should be 0.

    .. code-block:: bash

       python -c "import pyVmomi" ; echo $?


Configuration
=============

To use this module, set up the vCenter or ESX/ESXi URL, username and password in the
cloud configuration at
``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/vmware.conf``:

.. code-block:: yaml

    my-vmware-config:
      driver: vmware
      user: 'DOMAIN\\user'
      password: 'verybadpass'
      url: '10.20.30.40'

    vcenter01:
      driver: vmware
      user: 'DOMAIN\\user'
      password: 'verybadpass'
      url: 'vcenter01.domain.com'
      protocol: 'https'
      port: 443

    vcenter02:
      driver: vmware
      user: 'DOMAIN\\user'
      password: 'verybadpass'
      url: 'vcenter02.domain.com'
      protocol: 'http'
      port: 80

    esx01:
      driver: vmware
      user: 'admin'
      password: 'verybadpass'
      url: 'esx01.domain.com'

.. note::

    Optionally, ``protocol`` and ``port`` can be specified if the vCenter
    server is not using the defaults. Default is ``protocol: https`` and
    ``port: 443``.

.. note::
    .. versionchanged:: 2015.8.0

    The ``provider`` parameter in cloud provider configuration was renamed to ``driver``.
    This change was made to avoid confusion with the ``provider`` parameter that is
    used in cloud profile configuration. Cloud provider configuration now uses ``driver``
    to refer to the salt-cloud driver that provides the underlying functionality to
    connect to a cloud provider, while cloud profile configuration continues to use
    ``provider`` to refer to the cloud provider configuration that you define.

To test the connection for ``my-vmware-config`` specified in the cloud
configuration, run :py:func:`test_vcenter_connection`
"""

import logging
import os.path
import pprint
import re
import subprocess
import time
from random import randint

import salt.config as config
import salt.utils.cloud
import salt.utils.network
import salt.utils.stringutils
import salt.utils.vmware
import salt.utils.xmlutil
from salt.exceptions import SaltCloudSystemExit

try:
    # Attempt to import pyVmomi libs
    from pyVmomi import vim  # pylint: disable=no-name-in-module

    HAS_PYVMOMI = True
except ImportError:
    HAS_PYVMOMI = False

# Disable InsecureRequestWarning generated on python > 2.6
try:
    from requests.packages.urllib3 import (
        disable_warnings,
    )  # pylint: disable=no-name-in-module

    disable_warnings()
except ImportError:
    pass

ESX_5_5_NAME_PORTION = "VMware ESXi 5.5"
SAFE_ESX_5_5_CONTROLLER_KEY_INDEX = 200
FLATTEN_DISK_FULL_CLONE = "moveAllDiskBackingsAndDisallowSharing"
COPY_ALL_DISKS_FULL_CLONE = "moveAllDiskBackingsAndAllowSharing"
CURRENT_STATE_LINKED_CLONE = "moveChildMostDiskBacking"
QUICK_LINKED_CLONE = "createNewChildDiskBacking"


IP_RE = r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"

# Get logging started
log = logging.getLogger(__name__)

__virtualname__ = "vmware"


# Only load in this module if the VMware configurations are in place
def __virtual__():
    """
    Check for VMware configuration and if required libs are available.
    """
    if get_configured_provider() is False:
        return False

    if get_dependencies() is False:
        return False

    return __virtualname__


def _get_active_provider_name():
    try:
        return __active_provider_name__.value()
    except AttributeError:
        return __active_provider_name__


def get_configured_provider():
    """
    Return the first configured instance.
    """
    return config.is_provider_configured(
        __opts__,
        _get_active_provider_name() or __virtualname__,
        (
            "url",
            "user",
            "password",
        ),
    )


def get_dependencies():
    """
    Warn if dependencies aren't met.
    """
    deps = {
        "pyVmomi": HAS_PYVMOMI,
    }
    return config.check_driver_dependencies(__virtualname__, deps)


def script(vm_):
    """
    Return the script deployment object
    """
    script_name = config.get_cloud_config_value("script", vm_, __opts__)
    if not script_name:
        script_name = "bootstrap-salt"

    return salt.utils.cloud.os_script(
        script_name,
        vm_,
        __opts__,
        salt.utils.cloud.salt_config_to_yaml(
            salt.utils.cloud.minion_config(__opts__, vm_)
        ),
    )


def _str_to_bool(var):
    if isinstance(var, bool):
        return var

    if isinstance(var, str):
        return True if var.lower() == "true" else False

    return None


def _get_si():
    """
    Authenticate with vCenter server and return service instance object.
    """

    url = config.get_cloud_config_value(
        "url", get_configured_provider(), __opts__, search_global=False
    )
    username = config.get_cloud_config_value(
        "user", get_configured_provider(), __opts__, search_global=False
    )
    password = config.get_cloud_config_value(
        "password", get_configured_provider(), __opts__, search_global=False
    )
    protocol = config.get_cloud_config_value(
        "protocol",
        get_configured_provider(),
        __opts__,
        search_global=False,
        default="https",
    )
    port = config.get_cloud_config_value(
        "port", get_configured_provider(), __opts__, search_global=False, default=443
    )
    verify_ssl = config.get_cloud_config_value(
        "verify_ssl",
        get_configured_provider(),
        __opts__,
        search_global=False,
        default=True,
    )
    return salt.utils.vmware.get_service_instance(
        url, username, password, protocol=protocol, port=port, verify_ssl=verify_ssl
    )


def _edit_existing_hard_disk_helper(disk, size_kb=None, size_gb=None, mode=None):
    if size_kb or size_gb:
        disk.capacityInKB = size_kb if size_kb else int(size_gb * 1024.0 * 1024.0)
    if mode:
        disk.backing.diskMode = mode
    disk_spec = vim.vm.device.VirtualDeviceSpec()
    disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
    disk_spec.device = disk

    return disk_spec


def _add_new_hard_disk_helper(
    disk_label,
    size_gb,
    unit_number,
    controller_key=1000,
    thin_provision=False,
    eagerly_scrub=False,
    datastore=None,
    vm_name=None,
):
    random_key = randint(-2099, -2000)
    size_kb = int(size_gb * 1024.0 * 1024.0)

    disk_spec = vim.vm.device.VirtualDeviceSpec()
    disk_spec.fileOperation = "create"
    disk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add

    disk_spec.device = vim.vm.device.VirtualDisk()
    disk_spec.device.key = random_key
    disk_spec.device.deviceInfo = vim.Description()
    disk_spec.device.deviceInfo.label = disk_label
    disk_spec.device.deviceInfo.summary = "{} GB".format(size_gb)

    disk_spec.device.backing = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
    disk_spec.device.backing.thinProvisioned = thin_provision
    disk_spec.device.backing.eagerlyScrub = eagerly_scrub
    disk_spec.device.backing.diskMode = "persistent"

    if datastore:
        datastore_ref = salt.utils.vmware.get_mor_using_container_view(
            _get_si(), vim.Datastore, datastore
        )

        if not datastore_ref:
            # check if it is a datastore cluster instead
            datastore_cluster_ref = salt.utils.vmware.get_mor_using_container_view(
                _get_si(), vim.StoragePod, datastore
            )

            if not datastore_cluster_ref:
                # datastore/datastore cluster specified does not exist
                raise SaltCloudSystemExit(
                    "Specified datastore/datastore cluster ({}) for disk ({}) does not"
                    " exist".format(datastore, disk_label)
                )

            # datastore cluster has been specified
            # find datastore with most free space available
            #
            # TODO: Get DRS Recommendations instead of finding datastore with most free space
            datastore_list = salt.utils.vmware.get_datastores(
                _get_si(), datastore_cluster_ref, get_all_datastores=True
            )
            datastore_free_space = 0
            for ds_ref in datastore_list:
                log.trace(
                    "Found datastore (%s) with free space (%s) in datastore "
                    "cluster (%s)",
                    ds_ref.name,
                    ds_ref.summary.freeSpace,
                    datastore,
                )
                if (
                    ds_ref.summary.accessible
                    and ds_ref.summary.freeSpace > datastore_free_space
                ):
                    datastore_free_space = ds_ref.summary.freeSpace
                    datastore_ref = ds_ref

            if not datastore_ref:
                # datastore cluster specified does not have any accessible datastores
                raise SaltCloudSystemExit(
                    "Specified datastore cluster ({}) for disk ({}) does not have any"
                    " accessible datastores available".format(datastore, disk_label)
                )

        datastore_path = "[" + str(datastore_ref.name) + "] " + vm_name
        disk_spec.device.backing.fileName = datastore_path + "/" + disk_label + ".vmdk"
        disk_spec.device.backing.datastore = datastore_ref
        log.trace(
            "Using datastore (%s) for disk (%s), vm_name (%s)",
            datastore_ref.name,
            disk_label,
            vm_name,
        )

    disk_spec.device.controllerKey = controller_key
    disk_spec.device.unitNumber = unit_number
    disk_spec.device.capacityInKB = size_kb

    return disk_spec


def _edit_existing_network_adapter(
    network_adapter, new_network_name, adapter_type, switch_type, container_ref=None
):
    adapter_type.strip().lower()
    switch_type.strip().lower()

    if adapter_type in ["vmxnet", "vmxnet2", "vmxnet3", "e1000", "e1000e"]:
        edited_network_adapter = salt.utils.vmware.get_network_adapter_type(
            adapter_type
        )
        if isinstance(network_adapter, type(edited_network_adapter)):
            edited_network_adapter = network_adapter
        else:
            log.debug(
                "Changing type of '%s' from '%s' to '%s'",
                network_adapter.deviceInfo.label,
                type(network_adapter).__name__.rsplit(".", 1)[1][7:].lower(),
                adapter_type,
            )
    else:
        # If type not specified or does not match, don't change adapter type
        if adapter_type:
            log.error(
                "Cannot change type of '%s' to '%s'. Not changing type",
                network_adapter.deviceInfo.label,
                adapter_type,
            )
        edited_network_adapter = network_adapter

    if switch_type == "standard":
        network_ref = salt.utils.vmware.get_mor_by_property(
            _get_si(), vim.Network, new_network_name, container_ref=container_ref
        )
        edited_network_adapter.backing = (
            vim.vm.device.VirtualEthernetCard.NetworkBackingInfo()
        )
        edited_network_adapter.backing.deviceName = new_network_name
        edited_network_adapter.backing.network = network_ref
    elif switch_type == "distributed":
        network_ref = salt.utils.vmware.get_mor_by_property(
            _get_si(),
            vim.dvs.DistributedVirtualPortgroup,
            new_network_name,
            container_ref=container_ref,
        )
        dvs_port_connection = vim.dvs.PortConnection(
            portgroupKey=network_ref.key,
            switchUuid=network_ref.config.distributedVirtualSwitch.uuid,
        )
        edited_network_adapter.backing = (
            vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo()
        )
        edited_network_adapter.backing.port = dvs_port_connection
    else:
        # If switch type not specified or does not match, show error and return
        if not switch_type:
            err_msg = (
                "The switch type to be used by '{}' has not been specified".format(
                    network_adapter.deviceInfo.label
                )
            )
        else:
            err_msg = "Cannot create '{}'. Invalid/unsupported switch type '{}'".format(
                network_adapter.deviceInfo.label, switch_type
            )
        raise SaltCloudSystemExit(err_msg)

    edited_network_adapter.key = network_adapter.key
    edited_network_adapter.deviceInfo = network_adapter.deviceInfo
    edited_network_adapter.deviceInfo.summary = new_network_name
    edited_network_adapter.connectable = network_adapter.connectable
    edited_network_adapter.slotInfo = network_adapter.slotInfo
    edited_network_adapter.controllerKey = network_adapter.controllerKey
    edited_network_adapter.unitNumber = network_adapter.unitNumber
    edited_network_adapter.addressType = network_adapter.addressType
    edited_network_adapter.macAddress = network_adapter.macAddress
    edited_network_adapter.wakeOnLanEnabled = network_adapter.wakeOnLanEnabled
    network_spec = vim.vm.device.VirtualDeviceSpec()
    network_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
    network_spec.device = edited_network_adapter

    return network_spec


def _add_new_network_adapter_helper(
    network_adapter_label,
    network_name,
    adapter_type,
    switch_type,
    mac,
    container_ref=None,
):
    random_key = randint(-4099, -4000)

    adapter_type.strip().lower()
    switch_type.strip().lower()
    network_spec = vim.vm.device.VirtualDeviceSpec()

    if adapter_type in ["vmxnet", "vmxnet2", "vmxnet3", "e1000", "e1000e"]:
        network_spec.device = salt.utils.vmware.get_network_adapter_type(adapter_type)
    else:
        # If type not specified or does not match, create adapter of type vmxnet3
        if not adapter_type:
            log.debug(
                "The type of '%s' has not been specified. "
                "Creating default type 'vmxnet3'",
                network_adapter_label,
            )
        else:
            log.error(
                "Cannot create network adapter of type '%s'. "
                "Creating '%s' of default type 'vmxnet3'",
                adapter_type,
                network_adapter_label,
            )
        network_spec.device = vim.vm.device.VirtualVmxnet3()

    network_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add

    if switch_type == "standard":
        network_spec.device.backing = (
            vim.vm.device.VirtualEthernetCard.NetworkBackingInfo()
        )
        network_spec.device.backing.deviceName = network_name
        network_spec.device.backing.network = salt.utils.vmware.get_mor_by_property(
            _get_si(), vim.Network, network_name, container_ref=container_ref
        )
    elif switch_type == "distributed":
        network_ref = salt.utils.vmware.get_mor_by_property(
            _get_si(),
            vim.dvs.DistributedVirtualPortgroup,
            network_name,
            container_ref=container_ref,
        )
        dvs_port_connection = vim.dvs.PortConnection(
            portgroupKey=network_ref.key,
            switchUuid=network_ref.config.distributedVirtualSwitch.uuid,
        )
        network_spec.device.backing = (
            vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo()
        )
        network_spec.device.backing.port = dvs_port_connection
    else:
        # If switch type not specified or does not match, show error and return
        if not switch_type:
            err_msg = (
                "The switch type to be used by '{}' has not been specified".format(
                    network_adapter_label
                )
            )
        else:
            err_msg = "Cannot create '{}'. Invalid/unsupported switch type '{}'".format(
                network_adapter_label, switch_type
            )
        raise SaltCloudSystemExit(err_msg)

    if mac != "":
        network_spec.device.addressType = "assigned"
        network_spec.device.macAddress = mac
    network_spec.device.key = random_key
    network_spec.device.deviceInfo = vim.Description()
    network_spec.device.deviceInfo.label = network_adapter_label
    network_spec.device.deviceInfo.summary = network_name
    network_spec.device.wakeOnLanEnabled = True
    network_spec.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo()
    network_spec.device.connectable.startConnected = True
    network_spec.device.connectable.allowGuestControl = True

    return network_spec


def _edit_existing_scsi_controller(scsi_controller, bus_sharing):
    scsi_controller.sharedBus = bus_sharing
    scsi_spec = vim.vm.device.VirtualDeviceSpec()
    scsi_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
    scsi_spec.device = scsi_controller

    return scsi_spec


def _add_new_scsi_controller_helper(scsi_controller_label, properties, bus_number):
    random_key = randint(-1050, -1000)
    adapter_type = properties["type"].strip().lower() if "type" in properties else None
    bus_sharing = (
        properties["bus_sharing"].strip().lower()
        if "bus_sharing" in properties
        else None
    )

    scsi_spec = vim.vm.device.VirtualDeviceSpec()

    if adapter_type == "lsilogic":
        summary = "LSI Logic"
        scsi_spec.device = vim.vm.device.VirtualLsiLogicController()
    elif adapter_type == "lsilogic_sas":
        summary = "LSI Logic Sas"
        scsi_spec.device = vim.vm.device.VirtualLsiLogicSASController()
    elif adapter_type == "paravirtual":
        summary = "VMware paravirtual SCSI"
        scsi_spec.device = vim.vm.device.ParaVirtualSCSIController()
    else:
        # If type not specified or does not match, show error and return
        if not adapter_type:
            err_msg = "The type of '{}' has not been specified".format(
                scsi_controller_label
            )
        else:
            err_msg = "Cannot create '{}'. Invalid/unsupported type '{}'".format(
                scsi_controller_label, adapter_type
            )
        raise SaltCloudSystemExit(err_msg)

    scsi_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add

    scsi_spec.device.key = random_key
    scsi_spec.device.busNumber = bus_number
    scsi_spec.device.deviceInfo = vim.Description()
    scsi_spec.device.deviceInfo.label = scsi_controller_label
    scsi_spec.device.deviceInfo.summary = summary

    if bus_sharing == "virtual":
        # Virtual disks can be shared between virtual machines on the same server
        scsi_spec.device.sharedBus = (
            vim.vm.device.VirtualSCSIController.Sharing.virtualSharing
        )

    elif bus_sharing == "physical":
        # Virtual disks can be shared between virtual machines on any server
        scsi_spec.device.sharedBus = (
            vim.vm.device.VirtualSCSIController.Sharing.physicalSharing
        )

    else:
        # Virtual disks cannot be shared between virtual machines
        scsi_spec.device.sharedBus = (
            vim.vm.device.VirtualSCSIController.Sharing.noSharing
        )

    return scsi_spec


def _add_new_ide_controller_helper(ide_controller_label, controller_key, bus_number):
    """
    Helper function for adding new IDE controllers

    .. versionadded:: 2016.3.0

    Args:
      ide_controller_label: label of the IDE controller
      controller_key: if not None, the controller key to use; otherwise it is randomly generated
      bus_number: bus number

    Returns: created device spec for an IDE controller

    """
    if controller_key is None:
        controller_key = randint(-200, 250)

    ide_spec = vim.vm.device.VirtualDeviceSpec()
    ide_spec.device = vim.vm.device.VirtualIDEController()

    ide_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add

    ide_spec.device.key = controller_key
    ide_spec.device.busNumber = bus_number
    ide_spec.device.deviceInfo = vim.Description()
    ide_spec.device.deviceInfo.label = ide_controller_label
    ide_spec.device.deviceInfo.summary = ide_controller_label

    return ide_spec


def _set_cd_or_dvd_backing_type(drive, device_type, mode, iso_path):
    if device_type == "datastore_iso_file":
        drive.backing = vim.vm.device.VirtualCdrom.IsoBackingInfo()
        drive.backing.fileName = iso_path

        datastore = iso_path.partition("[")[-1].rpartition("]")[0]
        datastore_ref = salt.utils.vmware.get_mor_by_property(
            _get_si(), vim.Datastore, datastore
        )
        if datastore_ref:
            drive.backing.datastore = datastore_ref

        drive.deviceInfo.summary = "ISO {}".format(iso_path)

    elif device_type == "client_device":
        if mode == "passthrough":
            drive.backing = vim.vm.device.VirtualCdrom.RemotePassthroughBackingInfo()
            drive.deviceInfo.summary = "Remote Device"
        elif mode == "atapi":
            drive.backing = vim.vm.device.VirtualCdrom.RemoteAtapiBackingInfo()
            drive.deviceInfo.summary = "Remote ATAPI"

    return drive


def _edit_existing_cd_or_dvd_drive(drive, device_type, mode, iso_path):
    device_type.strip().lower()
    mode.strip().lower()

    drive_spec = vim.vm.device.VirtualDeviceSpec()
    drive_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
    drive_spec.device = _set_cd_or_dvd_backing_type(drive, device_type, mode, iso_path)

    return drive_spec


def _add_new_cd_or_dvd_drive_helper(
    drive_label, controller_key, device_type, mode, iso_path
):
    random_key = randint(-3025, -3000)

    device_type.strip().lower()
    mode.strip().lower()

    drive_spec = vim.vm.device.VirtualDeviceSpec()
    drive_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
    drive_spec.device = vim.vm.device.VirtualCdrom()
    drive_spec.device.deviceInfo = vim.Description()

    if device_type in ["datastore_iso_file", "client_device"]:
        drive_spec.device = _set_cd_or_dvd_backing_type(
            drive_spec.device, device_type, mode, iso_path
        )
    else:
        # If device_type not specified or does not match, create drive of Client type with Passthough mode
        if not device_type:
            log.debug(
                "The 'device_type' of '%s' has not been specified. "
                "Creating default type 'client_device'",
                drive_label,
            )
        else:
            log.error(
                "Cannot create CD/DVD drive of type '%s'. "
                "Creating '%s' of default type 'client_device'",
                device_type,
                drive_label,
            )
        drive_spec.device.backing = (
            vim.vm.device.VirtualCdrom.RemotePassthroughBackingInfo()
        )
        drive_spec.device.deviceInfo.summary = "Remote Device"

    drive_spec.device.key = random_key
    drive_spec.device.deviceInfo.label = drive_label
    drive_spec.device.controllerKey = controller_key
    drive_spec.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo()
    drive_spec.device.connectable.startConnected = True
    drive_spec.device.connectable.allowGuestControl = True

    return drive_spec


def _set_network_adapter_mapping(adapter_specs):
    adapter_mapping = vim.vm.customization.AdapterMapping()
    adapter_mapping.adapter = vim.vm.customization.IPSettings()

    if "domain" in list(adapter_specs.keys()):
        domain = adapter_specs["domain"]
        adapter_mapping.adapter.dnsDomain = domain
    if "gateway" in list(adapter_specs.keys()):
        gateway = adapter_specs["gateway"]
        adapter_mapping.adapter.gateway = gateway
    if "ip" in list(adapter_specs.keys()):
        ip = str(adapter_specs["ip"])
        subnet_mask = str(adapter_specs["subnet_mask"])
        adapter_mapping.adapter.ip = vim.vm.customization.FixedIp(ipAddress=ip)
        adapter_mapping.adapter.subnetMask = subnet_mask
    else:
        adapter_mapping.adapter.ip = vim.vm.customization.DhcpIpGenerator()

    return adapter_mapping


def _get_mode_spec(device, mode, disk_spec):
    if device.backing.diskMode != mode:
        if not disk_spec:
            disk_spec = _edit_existing_hard_disk_helper(disk=device, mode=mode)
        else:
            disk_spec.device.backing.diskMode = mode
    return disk_spec


def _get_size_spec(device, size_gb=None, size_kb=None):
    if size_kb is None and size_gb is not None:
        size_kb = int(size_gb * 1024.0 * 1024.0)
    disk_spec = (
        _edit_existing_hard_disk_helper(disk=device, size_kb=size_kb)
        if device.capacityInKB < size_kb
        else None
    )
    return disk_spec


def _iter_disk_unit_number(unit_number):
    """
    Apparently vmware reserves ID 7 for SCSI controllers, so we cannot specify
    hard drives for 7.

    Skip 7 to make sure.
    """
    unit_number += 1
    if unit_number == 7:
        unit_number += 1
    return unit_number


def _manage_devices(devices, vm=None, container_ref=None, new_vm_name=None):
    unit_number = 0
    bus_number = 0
    device_specs = []
    existing_disks_label = []
    existing_scsi_controllers_label = []
    existing_ide_controllers_label = []
    existing_network_adapters_label = []
    existing_cd_drives_label = []
    ide_controllers = {}
    nics_map = []
    cloning_from_vm = vm is not None

    if cloning_from_vm:
        # loop through all the devices the vm/template has
        # check if the device needs to be created or configured
        for device in vm.config.hardware.device:
            if isinstance(device, vim.vm.device.VirtualDisk):
                # this is a hard disk
                if "disk" in list(devices.keys()):
                    # there is atleast one disk specified to be created/configured
                    unit_number = _iter_disk_unit_number(unit_number)
                    existing_disks_label.append(device.deviceInfo.label)
                    if device.deviceInfo.label in list(devices["disk"].keys()):
                        disk_spec = None
                        if "size" in devices["disk"][device.deviceInfo.label]:
                            size_gb = float(
                                devices["disk"][device.deviceInfo.label]["size"]
                            )
                            size_kb = int(size_gb * 1024.0 * 1024.0)
                        else:
                            # User didn't specify disk size in the cloud
                            # profile so use the existing disk size
                            size_kb = device.capacityInKB
                            size_gb = size_kb / (1024.0 * 1024.0)
                            log.debug(
                                "Virtual disk size for '%s' was not "
                                "specified in the cloud profile or map file. "
                                "Using existing virtual disk size of '%sGB'",
                                device.deviceInfo.label,
                                size_gb,
                            )

                        if device.capacityInKB > size_kb:
                            raise SaltCloudSystemExit(
                                "The specified disk size '{}GB' for '{}' is "
                                "smaller than the disk image size '{}GB'. It must "
                                "be equal to or greater than the disk image".format(
                                    float(
                                        devices["disk"][device.deviceInfo.label]["size"]
                                    ),
                                    device.deviceInfo.label,
                                    float(device.capacityInKB / (1024.0 * 1024.0)),
                                )
                            )
                        else:
                            disk_spec = _get_size_spec(device=device, size_kb=size_kb)

                        if "mode" in devices["disk"][device.deviceInfo.label]:
                            if devices["disk"][device.deviceInfo.label]["mode"] in [
                                "independent_persistent",
                                "independent_nonpersistent",
                                "dependent",
                            ]:
                                mode = devices["disk"][device.deviceInfo.label]["mode"]
                                disk_spec = _get_mode_spec(device, mode, disk_spec)
                            else:
                                raise SaltCloudSystemExit(
                                    "Invalid disk backing mode specified!"
                                )
                        if disk_spec is not None:
                            device_specs.append(disk_spec)

            elif isinstance(
                device.backing,
                (
                    vim.vm.device.VirtualEthernetCard.NetworkBackingInfo,
                    vim.vm.device.VirtualEthernetCard.DistributedVirtualPortBackingInfo,
                ),
            ):
                # this is a network adapter
                if "network" in list(devices.keys()):
                    # there is atleast one network adapter specified to be created/configured
                    existing_network_adapters_label.append(device.deviceInfo.label)
                    if device.deviceInfo.label in list(devices["network"].keys()):
                        network_name = devices["network"][device.deviceInfo.label][
                            "name"
                        ]
                        adapter_type = (
                            devices["network"][device.deviceInfo.label]["adapter_type"]
                            if "adapter_type"
                            in devices["network"][device.deviceInfo.label]
                            else ""
                        )
                        switch_type = (
                            devices["network"][device.deviceInfo.label]["switch_type"]
                            if "switch_type"
                            in devices["network"][device.deviceInfo.label]
                            else ""
                        )
                        network_spec = _edit_existing_network_adapter(
                            device,
                            network_name,
                            adapter_type,
                            switch_type,
                            container_ref,
                        )
                        adapter_mapping = _set_network_adapter_mapping(
                            devices["network"][device.deviceInfo.label]
                        )
                        device_specs.append(network_spec)
                        nics_map.append(adapter_mapping)

            elif hasattr(device, "scsiCtlrUnitNumber"):
                # this is a SCSI controller
                if "scsi" in list(devices.keys()):
                    # there is atleast one SCSI controller specified to be created/configured
                    bus_number += 1
                    existing_scsi_controllers_label.append(device.deviceInfo.label)
                    if device.deviceInfo.label in list(devices["scsi"].keys()):
                        # Modify the existing SCSI controller
                        scsi_controller_properties = devices["scsi"][
                            device.deviceInfo.label
                        ]
                        bus_sharing = (
                            scsi_controller_properties["bus_sharing"].strip().lower()
                            if "bus_sharing" in scsi_controller_properties
                            else None
                        )
                        if bus_sharing and bus_sharing in ["virtual", "physical", "no"]:
                            bus_sharing = "{}Sharing".format(bus_sharing)
                            if bus_sharing != device.sharedBus:
                                # Only edit the SCSI controller if bus_sharing is different
                                scsi_spec = _edit_existing_scsi_controller(
                                    device, bus_sharing
                                )
                                device_specs.append(scsi_spec)

            elif isinstance(device, vim.vm.device.VirtualCdrom):
                # this is a cd/dvd drive
                if "cd" in list(devices.keys()):
                    # there is atleast one cd/dvd drive specified to be created/configured
                    existing_cd_drives_label.append(device.deviceInfo.label)
                    if device.deviceInfo.label in list(devices["cd"].keys()):
                        device_type = (
                            devices["cd"][device.deviceInfo.label]["device_type"]
                            if "device_type" in devices["cd"][device.deviceInfo.label]
                            else ""
                        )
                        mode = (
                            devices["cd"][device.deviceInfo.label]["mode"]
                            if "mode" in devices["cd"][device.deviceInfo.label]
                            else ""
                        )
                        iso_path = (
                            devices["cd"][device.deviceInfo.label]["iso_path"]
                            if "iso_path" in devices["cd"][device.deviceInfo.label]
                            else ""
                        )
                        cd_drive_spec = _edit_existing_cd_or_dvd_drive(
                            device, device_type, mode, iso_path
                        )
                        device_specs.append(cd_drive_spec)

            elif isinstance(device, vim.vm.device.VirtualIDEController):
                # this is an IDE controller to add new cd drives to
                ide_controllers[device.key] = len(device.device)

    if "network" in list(devices.keys()):
        network_adapters_to_create = list(
            set(devices["network"].keys()) - set(existing_network_adapters_label)
        )
        network_adapters_to_create.sort()
        if network_adapters_to_create:
            log.debug("Networks adapters to create: %s", network_adapters_to_create)
        for network_adapter_label in network_adapters_to_create:
            network_name = devices["network"][network_adapter_label]["name"]
            adapter_type = (
                devices["network"][network_adapter_label]["adapter_type"]
                if "adapter_type" in devices["network"][network_adapter_label]
                else ""
            )
            switch_type = (
                devices["network"][network_adapter_label]["switch_type"]
                if "switch_type" in devices["network"][network_adapter_label]
                else ""
            )
            mac = (
                devices["network"][network_adapter_label]["mac"]
                if "mac" in devices["network"][network_adapter_label]
                else ""
            )
            # create the network adapter
            network_spec = _add_new_network_adapter_helper(
                network_adapter_label,
                network_name,
                adapter_type,
                switch_type,
                mac,
                container_ref,
            )
            adapter_mapping = _set_network_adapter_mapping(
                devices["network"][network_adapter_label]
            )
            device_specs.append(network_spec)
            nics_map.append(adapter_mapping)

    if "scsi" in list(devices.keys()):
        scsi_controllers_to_create = list(
            set(devices["scsi"].keys()) - set(existing_scsi_controllers_label)
        )
        scsi_controllers_to_create.sort()
        if scsi_controllers_to_create:
            log.debug("SCSI controllers to create: %s", scsi_controllers_to_create)
        for scsi_controller_label in scsi_controllers_to_create:
            # create the SCSI controller
            scsi_controller_properties = devices["scsi"][scsi_controller_label]
            scsi_spec = _add_new_scsi_controller_helper(
                scsi_controller_label, scsi_controller_properties, bus_number
            )
            device_specs.append(scsi_spec)
            bus_number += 1

    if "ide" in list(devices.keys()):
        ide_controllers_to_create = list(
            set(devices["ide"].keys()) - set(existing_ide_controllers_label)
        )
        ide_controllers_to_create.sort()
        if ide_controllers_to_create:
            log.debug("IDE controllers to create: %s", ide_controllers_to_create)

        # ESX 5.5 (and possibly earlier?) set the IDE controller key themselves, indexed starting at
        # 200. Rather than doing a create task/get vm/reconfig task dance we query the server and
        # if it's ESX 5.5 we supply a controller starting at 200 and work out way upwards from there
        # ESX 6 (and, one assumes, vCenter) does not display this problem and so continues to use
        # the randomly generated indexes
        vcenter_name = get_vcenter_version(call="function")
        controller_index = (
            SAFE_ESX_5_5_CONTROLLER_KEY_INDEX
            if ESX_5_5_NAME_PORTION in vcenter_name
            else None
        )

        for ide_controller_label in ide_controllers_to_create:
            # create the IDE controller
            ide_spec = _add_new_ide_controller_helper(
                ide_controller_label, controller_index, bus_number
            )
            device_specs.append(ide_spec)
            bus_number += 1
            if controller_index is not None:
                controller_index += 1

    if "disk" in list(devices.keys()):
        disks_to_create = list(set(devices["disk"].keys()) - set(existing_disks_label))
        disks_to_create.sort()
        if disks_to_create:
            log.debug("Hard disks to create: %s", disks_to_create)
        for disk_label in disks_to_create:
            # create the disk
            size_gb = float(devices["disk"][disk_label]["size"])
            thin_provision = (
                bool(devices["disk"][disk_label]["thin_provision"])
                if "thin_provision" in devices["disk"][disk_label]
                else False
            )
            eagerly_scrub = (
                bool(devices["disk"][disk_label]["eagerly_scrub"])
                if "eagerly_scrub" in devices["disk"][disk_label]
                else False
            )
            datastore = devices["disk"][disk_label].get("datastore", None)
            disk_spec = _add_new_hard_disk_helper(
                disk_label,
                size_gb,
                unit_number,
                thin_provision=thin_provision,
                eagerly_scrub=eagerly_scrub,
                datastore=datastore,
                vm_name=new_vm_name,
            )

            # when creating both SCSI controller and Hard disk at the same time we need the randomly
            # assigned (temporary) key of the newly created SCSI controller
            if "controller" in devices["disk"][disk_label]:
                for spec in device_specs:
                    if (
                        spec.device.deviceInfo.label
                        == devices["disk"][disk_label]["controller"]
                    ):
                        disk_spec.device.controllerKey = spec.device.key
                        break

            device_specs.append(disk_spec)
            unit_number = _iter_disk_unit_number(unit_number)

    if "cd" in list(devices.keys()):
        cd_drives_to_create = list(
            set(devices["cd"].keys()) - set(existing_cd_drives_label)
        )
        cd_drives_to_create.sort()
        if cd_drives_to_create:
            log.debug("CD/DVD drives to create: %s", cd_drives_to_create)
        for cd_drive_label in cd_drives_to_create:
            # create the CD/DVD drive
            device_type = (
                devices["cd"][cd_drive_label]["device_type"]
                if "device_type" in devices["cd"][cd_drive_label]
                else ""
            )
            mode = (
                devices["cd"][cd_drive_label]["mode"]
                if "mode" in devices["cd"][cd_drive_label]
                else ""
            )
            iso_path = (
                devices["cd"][cd_drive_label]["iso_path"]
                if "iso_path" in devices["cd"][cd_drive_label]
                else ""
            )
            controller_key = None

            # When creating both IDE controller and CD/DVD drive at the same time we need the randomly
            # assigned (temporary) key of the newly created IDE controller
            if "controller" in devices["cd"][cd_drive_label]:
                for spec in device_specs:
                    if (
                        spec.device.deviceInfo.label
                        == devices["cd"][cd_drive_label]["controller"]
                    ):
                        controller_key = spec.device.key
                        ide_controllers[controller_key] = 0
                        break
            else:
                for ide_controller_key, num_devices in ide_controllers.items():
                    if num_devices < 2:
                        controller_key = ide_controller_key
                        break

            if not controller_key:
                log.error(
                    "No more available controllers for '%s'. "
                    "All IDE controllers are currently in use",
                    cd_drive_label,
                )
            else:
                cd_drive_spec = _add_new_cd_or_dvd_drive_helper(
                    cd_drive_label, controller_key, device_type, mode, iso_path
                )
                device_specs.append(cd_drive_spec)
                ide_controllers[controller_key] += 1

    ret = {"device_specs": device_specs, "nics_map": nics_map}

    return ret


def _wait_for_vmware_tools(vm_ref, max_wait):
    time_counter = 0
    starttime = time.time()
    while time_counter < max_wait:
        if time_counter % 5 == 0:
            log.info(
                "[ %s ] Waiting for VMware tools to be running [%s s]",
                vm_ref.name,
                time_counter,
            )
        if str(vm_ref.summary.guest.toolsRunningStatus) == "guestToolsRunning":
            log.info(
                "[ %s ] Successfully got VMware tools running on the guest in "
                "%s seconds",
                vm_ref.name,
                time_counter,
            )
            return True

        time.sleep(1.0 - ((time.time() - starttime) % 1.0))
        time_counter += 1
    log.warning(
        "[ %s ] Timeout Reached. VMware tools still not running after waiting "
        "for %s seconds",
        vm_ref.name,
        max_wait,
    )
    return False


def _valid_ip(ip_address):
    """
    Check if the IP address is valid
    Return either True or False
    """

    # Make sure IP has four octets
    octets = ip_address.split(".")
    if len(octets) != 4:
        return False

    # convert octet from string to int
    for i, octet in enumerate(octets):

        try:
            octets[i] = int(octet)
        except ValueError:
            # couldn't convert octet to an integer
            return False

    # map variables to elements of octets list
    first_octet, second_octet, third_octet, fourth_octet = octets

    # Check first_octet meets conditions
    if first_octet < 1 or first_octet > 223 or first_octet == 127:
        return False

    # Check 169.254.X.X condition
    if first_octet == 169 and second_octet == 254:
        return False

    # Check 2nd - 4th octets
    for octet in (second_octet, third_octet, fourth_octet):
        if (octet < 0) or (octet > 255):
            return False
    # Passed all of the checks
    return True


def _wait_for_ip(vm_ref, max_wait):
    max_wait_vmware_tools = max_wait
    max_wait_ip = max_wait
    vmware_tools_status = _wait_for_vmware_tools(vm_ref, max_wait_vmware_tools)
    if not vmware_tools_status:
        # VMware will only report the IP if VMware tools are installed. Try to
        # determine the IP using DNS
        vm_name = vm_ref.summary.config.name
        resolved_ips = salt.utils.network.host_to_ips(vm_name)
        log.debug(
            "Timeout waiting for VMware tools. The name %s resolved to %s",
            vm_name,
            resolved_ips,
        )
        if isinstance(resolved_ips, list) and resolved_ips:
            return resolved_ips[0]
        return False
    time_counter = 0
    starttime = time.time()
    while time_counter < max_wait_ip:
        if time_counter % 5 == 0:
            log.info(
                "[ %s ] Waiting to retrieve IPv4 information [%s s]",
                vm_ref.name,
                time_counter,
            )

        if vm_ref.summary.guest.ipAddress and _valid_ip(vm_ref.summary.guest.ipAddress):
            log.info(
                "[ %s ] Successfully retrieved IPv4 information in %s seconds",
                vm_ref.name,
                time_counter,
            )
            return vm_ref.summary.guest.ipAddress
        for net in vm_ref.guest.net:
            if net.ipConfig.ipAddress:
                for current_ip in net.ipConfig.ipAddress:
                    if _valid_ip(current_ip.ipAddress):
                        log.info(
                            "[ %s ] Successfully retrieved IPv4 information "
                            "in %s seconds",
                            vm_ref.name,
                            time_counter,
                        )
                        return current_ip.ipAddress
        time.sleep(1.0 - ((time.time() - starttime) % 1.0))
        time_counter += 1
    log.warning(
        "[ %s ] Timeout Reached. Unable to retrieve IPv4 information after "
        "waiting for %s seconds",
        vm_ref.name,
        max_wait_ip,
    )
    return False


def _wait_for_host(host_ref, task_type, sleep_seconds=5, log_level="debug"):
    time_counter = 0
    starttime = time.time()
    while host_ref.runtime.connectionState != "notResponding":
        if time_counter % sleep_seconds == 0:
            log.log(
                logging.INFO if log_level == "info" else logging.DEBUG,
                "[ %s ] Waiting for host %s to finish [%s s]",
                host_ref.name,
                task_type,
                time_counter,
            )
        time.sleep(1.0 - ((time.time() - starttime) % 1.0))
        time_counter += 1
    while host_ref.runtime.connectionState != "connected":
        if time_counter % sleep_seconds == 0:
            log.log(
                logging.INFO if log_level == "info" else logging.DEBUG,
                "[ %s ] Waiting for host %s to finish [%s s]",
                host_ref.name,
                task_type,
                time_counter,
            )
        time.sleep(1.0 - ((time.time() - starttime) % 1.0))
        time_counter += 1
    if host_ref.runtime.connectionState == "connected":
        log.log(
            logging.INFO if log_level == "info" else logging.DEBUG,
            "[ %s ] Successfully completed host %s in %s seconds",
            host_ref.name,
            task_type,
            time_counter,
        )
    else:
        log.error("Could not connect back to the host system")


def _format_instance_info_select(vm, selection):
    def defaultto(machine, section, default="N/A"):
        """
        Return either a named value from a VirtualMachineConfig or a
        default string "N/A".
        """
        return default if section not in machine else machine[section]

    vm_select_info = {}

    if "id" in selection:
        vm_select_info["id"] = vm["name"]

    if "image" in selection:
        vm_select_info["image"] = "{} (Detected)".format(
            defaultto(vm, "config.guestFullName")
        )

    if "size" in selection:
        cpu = defaultto(vm, "config.hardware.numCPU")
        ram = "{} MB".format(defaultto(vm, "config.hardware.memoryMB"))
        vm_select_info["size"] = "cpu: {}\nram: {}".format(cpu, ram)
        vm_select_info["size_dict"] = {
            "cpu": cpu,
            "memory": ram,
        }

    if "state" in selection:
        vm_select_info["state"] = str(defaultto(vm, "summary.runtime.powerState"))

    if "guest_id" in selection:
        vm_select_info["guest_id"] = defaultto(vm, "config.guestId")

    if "hostname" in selection:
        vm_select_info["hostname"] = vm["object"].guest.hostName

    if "path" in selection:
        vm_select_info["path"] = defaultto(vm, "config.files.vmPathName")

    if "tools_status" in selection:
        vm_select_info["tools_status"] = str(defaultto(vm, "guest.toolsStatus"))

    if "private_ips" in selection or "networks" in selection:
        network_full_info = {}
        ip_addresses = []

        if "guest.net" in vm:
            for net in vm["guest.net"]:
                network_full_info[net.network] = {
                    "connected": net.connected,
                    "ip_addresses": net.ipAddress,
                    "mac_address": net.macAddress,
                }
                ip_addresses.extend(net.ipAddress)

        if "private_ips" in selection:
            vm_select_info["private_ips"] = ip_addresses

        if "networks" in selection:
            vm_select_info["networks"] = network_full_info

    if any(x in ["devices", "mac_address", "mac_addresses"] for x in selection):
        device_full_info = {}
        device_mac_addresses = []
        if "config.hardware.device" in vm:
            for device in vm["config.hardware.device"]:
                device_full_info[device.deviceInfo.label] = {}
                if "devices" in selection:
                    device_full_info[device.deviceInfo.label]["key"] = (device.key,)
                    device_full_info[device.deviceInfo.label]["label"] = (
                        device.deviceInfo.label,
                    )
                    device_full_info[device.deviceInfo.label]["summary"] = (
                        device.deviceInfo.summary,
                    )
                    device_full_info[device.deviceInfo.label]["type"] = type(
                        device
                    ).__name__.rsplit(".", 1)[1]

                    if device.unitNumber:
                        device_full_info[device.deviceInfo.label][
                            "unitNumber"
                        ] = device.unitNumber

                    if hasattr(device, "connectable") and device.connectable:
                        device_full_info[device.deviceInfo.label][
                            "startConnected"
                        ] = device.connectable.startConnected
                        device_full_info[device.deviceInfo.label][
                            "allowGuestControl"
                        ] = device.connectable.allowGuestControl
                        device_full_info[device.deviceInfo.label][
                            "connected"
                        ] = device.connectable.connected
                        device_full_info[device.deviceInfo.label][
                            "status"
                        ] = device.connectable.status

                    if hasattr(device, "controllerKey") and device.controllerKey:
                        device_full_info[device.deviceInfo.label][
                            "controllerKey"
                        ] = device.controllerKey

                    if hasattr(device, "addressType"):
                        device_full_info[device.deviceInfo.label][
                            "addressType"
                        ] = device.addressType

                    if hasattr(device, "busNumber"):
                        device_full_info[device.deviceInfo.label][
                            "busNumber"
                        ] = device.busNumber

                    if hasattr(device, "device"):
                        device_full_info[device.deviceInfo.label][
                            "deviceKeys"
                        ] = device.device

                    if hasattr(device, "videoRamSizeInKB"):
                        device_full_info[device.deviceInfo.label][
                            "videoRamSizeInKB"
                        ] = device.videoRamSizeInKB

                    if isinstance(device, vim.vm.device.VirtualDisk):
                        device_full_info[device.deviceInfo.label][
                            "capacityInKB"
                        ] = device.capacityInKB
                        device_full_info[device.deviceInfo.label][
                            "diskMode"
                        ] = device.backing.diskMode
                        device_full_info[device.deviceInfo.label][
                            "fileName"
                        ] = device.backing.fileName

                if hasattr(device, "macAddress"):
                    device_full_info[device.deviceInfo.label][
                        "macAddress"
                    ] = device.macAddress
                    device_mac_addresses.append(device.macAddress)

        if "devices" in selection:
            vm_select_info["devices"] = device_full_info

        if "mac_address" in selection or "mac_addresses" in selection:
            vm_select_info["mac_addresses"] = device_mac_addresses

    if "storage" in selection:
        storage_full_info = {
            "committed": int(vm["summary.storage.committed"])
            if "summary.storage.committed" in vm
            else "N/A",
            "uncommitted": int(vm["summary.storage.uncommitted"])
            if "summary.storage.uncommitted" in vm
            else "N/A",
            "unshared": int(vm["summary.storage.unshared"])
            if "summary.storage.unshared" in vm
            else "N/A",
        }
        vm_select_info["storage"] = storage_full_info

    if "files" in selection:
        file_full_info = {}
        if "layoutEx.file" in vm:
            for filename in vm["layoutEx.file"]:
                file_full_info[filename.key] = {
                    "key": filename.key,
                    "name": filename.name,
                    "size": filename.size,
                    "type": filename.type,
                }
        vm_select_info["files"] = file_full_info

    return vm_select_info


def _format_instance_info(vm):
    device_full_info = {}
    device_mac_addresses = []
    if "config.hardware.device" in vm:
        for device in vm["config.hardware.device"]:
            device_full_info[device.deviceInfo.label] = {
                "key": device.key,
                "label": device.deviceInfo.label,
                "summary": device.deviceInfo.summary,
                "type": type(device).__name__.rsplit(".", 1)[1],
            }

            if device.unitNumber:
                device_full_info[device.deviceInfo.label][
                    "unitNumber"
                ] = device.unitNumber

            if hasattr(device, "connectable") and device.connectable:
                device_full_info[device.deviceInfo.label][
                    "startConnected"
                ] = device.connectable.startConnected
                device_full_info[device.deviceInfo.label][
                    "allowGuestControl"
                ] = device.connectable.allowGuestControl
                device_full_info[device.deviceInfo.label][
                    "connected"
                ] = device.connectable.connected
                device_full_info[device.deviceInfo.label][
                    "status"
                ] = device.connectable.status

            if hasattr(device, "controllerKey") and device.controllerKey:
                device_full_info[device.deviceInfo.label][
                    "controllerKey"
                ] = device.controllerKey

            if hasattr(device, "addressType"):
                device_full_info[device.deviceInfo.label][
                    "addressType"
                ] = device.addressType

            if hasattr(device, "macAddress"):
                device_full_info[device.deviceInfo.label][
                    "macAddress"
                ] = device.macAddress
                device_mac_addresses.append(device.macAddress)

            if hasattr(device, "busNumber"):
                device_full_info[device.deviceInfo.label][
                    "busNumber"
                ] = device.busNumber

            if hasattr(device, "device"):
                device_full_info[device.deviceInfo.label]["deviceKeys"] = device.device

            if hasattr(device, "videoRamSizeInKB"):
                device_full_info[device.deviceInfo.label][
                    "videoRamSizeInKB"
                ] = device.videoRamSizeInKB

            if isinstance(device, vim.vm.device.VirtualDisk):
                device_full_info[device.deviceInfo.label][
                    "capacityInKB"
                ] = device.capacityInKB
                device_full_info[device.deviceInfo.label][
                    "diskMode"
                ] = device.backing.diskMode
                device_full_info[device.deviceInfo.label][
                    "fileName"
                ] = device.backing.fileName

    storage_full_info = {
        "committed": int(vm["summary.storage.committed"])
        if "summary.storage.committed" in vm
        else "N/A",
        "uncommitted": int(vm["summary.storage.uncommitted"])
        if "summary.storage.uncommitted" in vm
        else "N/A",
        "unshared": int(vm["summary.storage.unshared"])
        if "summary.storage.unshared" in vm
        else "N/A",
    }

    file_full_info = {}
    if "layoutEx.file" in vm:
        for filename in vm["layoutEx.file"]:
            file_full_info[filename.key] = {
                "key": filename.key,
                "name": filename.name,
                "size": filename.size,
                "type": filename.type,
            }

    network_full_info = {}
    ip_addresses = []
    if "guest.net" in vm:
        for net in vm["guest.net"]:
            network_full_info[net.network] = {
                "connected": net.connected,
                "ip_addresses": net.ipAddress,
                "mac_address": net.macAddress,
            }
            ip_addresses.extend(net.ipAddress)

    cpu = vm["config.hardware.numCPU"] if "config.hardware.numCPU" in vm else "N/A"
    ram = (
        "{} MB".format(vm["config.hardware.memoryMB"])
        if "config.hardware.memoryMB" in vm
        else "N/A"
    )
    vm_full_info = {
        "id": str(vm["name"]),
        "image": "{} (Detected)".format(vm["config.guestFullName"])
        if "config.guestFullName" in vm
        else "N/A",
        "size": "cpu: {}\nram: {}".format(cpu, ram),
        "size_dict": {"cpu": cpu, "memory": ram},
        "state": str(vm["summary.runtime.powerState"])
        if "summary.runtime.powerState" in vm
        else "N/A",
        "private_ips": ip_addresses,
        "public_ips": [],
        "devices": device_full_info,
        "storage": storage_full_info,
        "files": file_full_info,
        "guest_id": str(vm["config.guestId"]) if "config.guestId" in vm else "N/A",
        "hostname": str(vm["object"].guest.hostName),
        "mac_addresses": device_mac_addresses,
        "networks": network_full_info,
        "path": str(vm["config.files.vmPathName"])
        if "config.files.vmPathName" in vm
        else "N/A",
        "tools_status": str(vm["guest.toolsStatus"])
        if "guest.toolsStatus" in vm
        else "N/A",
    }

    return vm_full_info


def _get_snapshots(snapshot_list, current_snapshot=None, parent_snapshot_path=""):
    snapshots = {}
    for snapshot in snapshot_list:
        snapshot_path = "{}/{}".format(parent_snapshot_path, snapshot.name)
        snapshots[snapshot_path] = {
            "name": snapshot.name,
            "description": snapshot.description,
            "created": str(snapshot.createTime).split(".")[0],
            "state": snapshot.state,
            "path": snapshot_path,
        }

        if current_snapshot and current_snapshot == snapshot.snapshot:
            return snapshots[snapshot_path]

        # Check if child snapshots exist
        if snapshot.childSnapshotList:
            ret = _get_snapshots(
                snapshot.childSnapshotList, current_snapshot, snapshot_path
            )
            if current_snapshot:
                return ret
            snapshots.update(ret)

    return snapshots


def _get_snapshot_ref_helper(base_snapshot, snapshot_name):
    if base_snapshot.name == snapshot_name:
        return base_snapshot

    for snapshot in base_snapshot.childSnapshotList:
        snapshot_ref = _get_snapshot_ref_helper(snapshot, snapshot_name)
        if snapshot_ref is not None:
            return snapshot_ref

    return None


def _get_snapshot_ref_by_name(vm_ref, snapshot_name):
    snapshot_ref = None
    try:
        for root_snapshot in vm_ref.snapshot.rootSnapshotList:
            snapshot_ref = _get_snapshot_ref_helper(root_snapshot, snapshot_name)
            if snapshot_ref is not None:
                break
    except (IndexError, AttributeError):
        snapshot_ref = None

    return snapshot_ref


def _upg_tools_helper(vm, reboot=False):
    # Exit if template
    if vm.config.template:
        status = "VMware tools cannot be updated on a template"

    # Exit if VMware tools is already up to date
    elif vm.guest.toolsStatus == "toolsOk":
        status = "VMware tools is already up to date"

    # Exit if VM is not powered on
    elif vm.summary.runtime.powerState != "poweredOn":
        status = "VM must be powered on to upgrade tools"

    # Exit if VMware tools is either not running or not installed
    elif vm.guest.toolsStatus in ["toolsNotRunning", "toolsNotInstalled"]:
        status = "VMware tools is either not running or not installed"

    # If vmware tools is out of date, check major OS family
    # Upgrade tools on Linux and Windows guests
    elif vm.guest.toolsStatus == "toolsOld":
        log.info("Upgrading VMware tools on %s", vm.name)
        try:
            if vm.guest.guestFamily == "windowsGuest" and not reboot:
                log.info("Reboot suppressed on %s", vm.name)
                task = vm.UpgradeTools('/S /v"/qn REBOOT=R"')
            elif vm.guest.guestFamily in ["linuxGuest", "windowsGuest"]:
                task = vm.UpgradeTools()
            else:
                return "Only Linux and Windows guests are currently supported"
            salt.utils.vmware.wait_for_task(
                task, vm.name, "tools upgrade", sleep_seconds=5, log_level="info"
            )
        except Exception as exc:  # pylint: disable=broad-except
            log.error(
                "Error while upgrading VMware tools on VM %s: %s",
                vm.name,
                exc,
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG,
            )
            return "VMware tools upgrade failed"
        status = "VMware tools upgrade succeeded"
    else:
        status = "VMWare tools could not be upgraded"

    return status


def _get_hba_type(hba_type):
    """
    Convert a string representation of a HostHostBusAdapter into an
    object reference.
    """
    if hba_type == "parallel":
        return vim.host.ParallelScsiHba
    elif hba_type == "block":
        return vim.host.BlockHba
    elif hba_type == "iscsi":
        return vim.host.InternetScsiHba
    elif hba_type == "fibre":
        return vim.host.FibreChannelHba

    raise ValueError("Unknown Host Bus Adapter Type")


def test_vcenter_connection(kwargs=None, call=None):
    """
    Test if the connection can be made to the vCenter server using
    the specified credentials inside ``/etc/salt/cloud.providers``
    or ``/etc/salt/cloud.providers.d/vmware.conf``

    CLI Example:

    .. code-block:: bash

        salt-cloud -f test_vcenter_connection my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The test_vcenter_connection function must be called with -f or --function."
        )

    try:
        # Get the service instance object
        _get_si()
    except Exception as exc:  # pylint: disable=broad-except
        return "failed to connect: {}".format(exc)

    return "connection successful"


def get_vcenter_version(kwargs=None, call=None):
    """
    Show the vCenter Server version with build number.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_vcenter_version my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The get_vcenter_version function must be called with -f or --function."
        )

    # Get the inventory
    inv = salt.utils.vmware.get_inventory(_get_si())

    return inv.about.fullName


def list_datacenters(kwargs=None, call=None):
    """
    List all the data centers for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_datacenters my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_datacenters function must be called with -f or --function."
        )

    return {"Datacenters": salt.utils.vmware.list_datacenters(_get_si())}


def list_portgroups(kwargs=None, call=None):
    """
    List all the distributed virtual portgroups for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_portgroups my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_portgroups function must be called with -f or --function."
        )

    return {"Portgroups": salt.utils.vmware.list_portgroups(_get_si())}


def list_clusters(kwargs=None, call=None):
    """
    List all the clusters for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_clusters my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_clusters function must be called with -f or --function."
        )

    return {"Clusters": salt.utils.vmware.list_clusters(_get_si())}


def list_datastore_clusters(kwargs=None, call=None):
    """
    List all the datastore clusters for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_datastore_clusters my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_datastore_clusters function must be called with -f or --function."
        )

    return {"Datastore Clusters": salt.utils.vmware.list_datastore_clusters(_get_si())}


def list_datastores(kwargs=None, call=None):
    """
    List all the datastores for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_datastores my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_datastores function must be called with -f or --function."
        )

    return {"Datastores": salt.utils.vmware.list_datastores(_get_si())}


def list_hosts(kwargs=None, call=None):
    """
    List all the hosts for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hosts my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_hosts function must be called with -f or --function."
        )

    return {"Hosts": salt.utils.vmware.list_hosts(_get_si())}


def list_resourcepools(kwargs=None, call=None):
    """
    List all the resource pools for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_resourcepools my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_resourcepools function must be called with -f or --function."
        )

    return {"Resource Pools": salt.utils.vmware.list_resourcepools(_get_si())}


def list_networks(kwargs=None, call=None):
    """
    List all the standard networks for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_networks my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_networks function must be called with -f or --function."
        )

    return {"Networks": salt.utils.vmware.list_networks(_get_si())}


def list_nodes_min(kwargs=None, call=None):
    """
    Return a list of all VMs and templates that are on the specified provider, with no details

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_nodes_min my-vmware-config
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes_min function must be called with -f or --function."
        )

    ret = {}
    vm_properties = ["name"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        ret[vm["name"]] = {"state": "Running", "id": vm["name"]}

    return ret


def list_nodes(kwargs=None, call=None):
    """
    Return a list of all VMs and templates that are on the specified provider, with basic fields

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_nodes my-vmware-config

    To return a list of all VMs and templates present on ALL configured providers, with basic
    fields:

    CLI Example:

    .. code-block:: bash

        salt-cloud -Q
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes function must be called with -f or --function."
        )

    ret = {}
    vm_properties = [
        "name",
        "guest.ipAddress",
        "config.guestFullName",
        "config.hardware.numCPU",
        "config.hardware.memoryMB",
        "summary.runtime.powerState",
    ]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        cpu = vm["config.hardware.numCPU"] if "config.hardware.numCPU" in vm else "N/A"
        ram = (
            "{} MB".format(vm["config.hardware.memoryMB"])
            if "config.hardware.memoryMB" in vm
            else "N/A"
        )
        vm_info = {
            "id": vm["name"],
            "image": "{} (Detected)".format(vm["config.guestFullName"])
            if "config.guestFullName" in vm
            else "N/A",
            "size": "cpu: {}\nram: {}".format(cpu, ram),
            "size_dict": {"cpu": cpu, "memory": ram},
            "state": str(vm["summary.runtime.powerState"])
            if "summary.runtime.powerState" in vm
            else "N/A",
            "private_ips": [vm["guest.ipAddress"]] if "guest.ipAddress" in vm else [],
            "public_ips": [],
        }
        ret[vm_info["id"]] = vm_info

    return ret


def list_nodes_full(kwargs=None, call=None):
    """
    Return a list of all VMs and templates that are on the specified provider, with full details

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_nodes_full my-vmware-config

    To return a list of all VMs and templates present on ALL configured providers, with full
    details:

    CLI Example:

    .. code-block:: bash

        salt-cloud -F
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes_full function must be called with -f or --function."
        )

    ret = {}
    vm_properties = [
        "config.hardware.device",
        "summary.storage.committed",
        "summary.storage.uncommitted",
        "summary.storage.unshared",
        "layoutEx.file",
        "config.guestFullName",
        "config.guestId",
        "guest.net",
        "config.hardware.memoryMB",
        "name",
        "config.hardware.numCPU",
        "config.files.vmPathName",
        "summary.runtime.powerState",
        "guest.toolsStatus",
    ]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        ret[vm["name"]] = _format_instance_info(vm)

    return ret


def list_nodes_select(call=None):
    """
    Return a list of all VMs and templates that are on the specified provider, with fields
    specified under ``query.selection`` in ``/etc/salt/cloud``

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_nodes_select my-vmware-config

    To return a list of all VMs and templates present on ALL configured providers, with
    fields specified under ``query.selection`` in ``/etc/salt/cloud``:

    CLI Example:

    .. code-block:: bash

        salt-cloud -S
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes_select function must be called with -f or --function."
        )

    ret = {}
    vm_properties = []
    selection = __opts__.get("query.selection")

    if not selection:
        raise SaltCloudSystemExit("query.selection not found in /etc/salt/cloud")

    if "id" in selection:
        vm_properties.append("name")

    if "image" in selection:
        vm_properties.append("config.guestFullName")

    if "size" in selection:
        vm_properties.extend(["config.hardware.numCPU", "config.hardware.memoryMB"])

    if "state" in selection:
        vm_properties.append("summary.runtime.powerState")

    if "private_ips" in selection or "networks" in selection:
        vm_properties.append("guest.net")

    if (
        "devices" in selection
        or "mac_address" in selection
        or "mac_addresses" in selection
    ):
        vm_properties.append("config.hardware.device")

    if "storage" in selection:
        vm_properties.extend(
            [
                "config.hardware.device",
                "summary.storage.committed",
                "summary.storage.uncommitted",
                "summary.storage.unshared",
            ]
        )

    if "files" in selection:
        vm_properties.append("layoutEx.file")

    if "guest_id" in selection:
        vm_properties.append("config.guestId")

    if "hostname" in selection:
        vm_properties.append("guest.hostName")

    if "path" in selection:
        vm_properties.append("config.files.vmPathName")

    if "tools_status" in selection:
        vm_properties.append("guest.toolsStatus")

    if not vm_properties:
        return {}
    elif "name" not in vm_properties:
        vm_properties.append("name")

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        ret[vm["name"]] = _format_instance_info_select(vm, selection)
    return ret


def show_instance(name, call=None):
    """
    List all available details of the specified VM

    CLI Example:

    .. code-block:: bash

        salt-cloud -a show_instance vmname
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The show_instance action must be called with -a or --action."
        )

    vm_properties = [
        "config.hardware.device",
        "summary.storage.committed",
        "summary.storage.uncommitted",
        "summary.storage.unshared",
        "layoutEx.file",
        "config.guestFullName",
        "config.guestId",
        "guest.net",
        "config.hardware.memoryMB",
        "name",
        "config.hardware.numCPU",
        "config.files.vmPathName",
        "summary.runtime.powerState",
        "guest.toolsStatus",
    ]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if vm["name"] == name:
            return _format_instance_info(vm)

    return {}


def avail_images(call=None):
    """
    Return a list of all the templates present in this VMware environment with basic
    details

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-images my-vmware-config
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_images function must be called with "
            "-f or --function, or with the --list-images option."
        )

    templates = {}
    vm_properties = [
        "name",
        "config.template",
        "config.guestFullName",
        "config.hardware.numCPU",
        "config.hardware.memoryMB",
    ]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if "config.template" in vm and vm["config.template"]:
            templates[vm["name"]] = {
                "name": vm["name"],
                "guest_fullname": vm["config.guestFullName"]
                if "config.guestFullName" in vm
                else "N/A",
                "cpus": vm["config.hardware.numCPU"]
                if "config.hardware.numCPU" in vm
                else "N/A",
                "ram": vm["config.hardware.memoryMB"]
                if "config.hardware.memoryMB" in vm
                else "N/A",
            }

    return templates


def avail_locations(call=None):
    """
    Return a list of all the available locations/datacenters in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-locations my-vmware-config
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_locations function must be called with "
            "-f or --function, or with the --list-locations option."
        )

    return list_datacenters(call="function")


def avail_sizes(call=None):
    """
    Return a list of all the available sizes in this VMware environment.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-sizes my-vmware-config

    .. note::

        Since sizes are built into templates, this function will return
        an empty dictionary.

    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_sizes function must be called with "
            "-f or --function, or with the --list-sizes option."
        )

    log.warning(
        "Because sizes are built into templates with VMware, there are no sizes "
        "to return."
    )

    return {}


def list_templates(kwargs=None, call=None):
    """
    List all the templates present in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_templates my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_templates function must be called with -f or --function."
        )

    return {"Templates": avail_images(call="function")}


def list_folders(kwargs=None, call=None):
    """
    List all the folders for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_folders my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_folders function must be called with -f or --function."
        )

    return {"Folders": salt.utils.vmware.list_folders(_get_si())}


def list_snapshots(kwargs=None, call=None):
    """
    List snapshots either for all VMs and templates or for a specific VM/template
    in this VMware environment

    To list snapshots for all VMs and templates:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_snapshots my-vmware-config

    To list snapshots for a specific VM/template:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_snapshots my-vmware-config name="vmname"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_snapshots function must be called with -f or --function."
        )

    ret = {}
    vm_properties = ["name", "rootSnapshot", "snapshot"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if vm["rootSnapshot"]:
            if kwargs and kwargs.get("name") == vm["name"]:
                return {vm["name"]: _get_snapshots(vm["snapshot"].rootSnapshotList)}
            else:
                ret[vm["name"]] = _get_snapshots(vm["snapshot"].rootSnapshotList)
        else:
            if kwargs and kwargs.get("name") == vm["name"]:
                return {}

    return ret


def start(name, call=None):
    """
    To start/power on a VM using its name

    CLI Example:

    .. code-block:: bash

        salt-cloud -a start vmname
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The start action must be called with -a or --action."
        )

    vm_properties = ["name", "summary.runtime.powerState"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if vm["name"] == name:
            if vm["summary.runtime.powerState"] == "poweredOn":
                ret = "already powered on"
                log.info("VM %s %s", name, ret)
                return ret
            try:
                log.info("Starting VM %s", name)
                task = vm["object"].PowerOn()
                salt.utils.vmware.wait_for_task(task, name, "power on")
            except Exception as exc:  # pylint: disable=broad-except
                log.error(
                    "Error while powering on VM %s: %s",
                    name,
                    exc,
                    # Show the traceback if the debug logging level is enabled
                    exc_info_on_loglevel=logging.DEBUG,
                )
                return "failed to power on"

    return "powered on"


def stop(name, soft=False, call=None):
    """
    To stop/power off a VM using its name

    .. note::

        If ``soft=True`` then issues a command to the guest operating system
        asking it to perform a clean shutdown of all services.
        Default is soft=False

        For ``soft=True`` vmtools should be installed on guest system.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a stop vmname
        salt-cloud -a stop vmname soft=True
    """
    if call != "action":
        raise SaltCloudSystemExit("The stop action must be called with -a or --action.")

    vm_properties = ["name", "summary.runtime.powerState"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if vm["name"] == name:
            if vm["summary.runtime.powerState"] == "poweredOff":
                ret = "already powered off"
                log.info("VM %s %s", name, ret)
                return ret
            try:
                log.info("Stopping VM %s", name)
                if soft:
                    vm["object"].ShutdownGuest()
                else:
                    task = vm["object"].PowerOff()
                    salt.utils.vmware.wait_for_task(task, name, "power off")
            except Exception as exc:  # pylint: disable=broad-except
                log.error(
                    "Error while powering off VM %s: %s",
                    name,
                    exc,
                    # Show the traceback if the debug logging level is enabled
                    exc_info_on_loglevel=logging.DEBUG,
                )
                return "failed to power off"

    return "powered off"


def suspend(name, call=None):
    """
    To suspend a VM using its name

    CLI Example:

    .. code-block:: bash

        salt-cloud -a suspend vmname
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The suspend action must be called with -a or --action."
        )

    vm_properties = ["name", "summary.runtime.powerState"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if vm["name"] == name:
            if vm["summary.runtime.powerState"] == "poweredOff":
                ret = "cannot suspend in powered off state"
                log.info("VM %s %s", name, ret)
                return ret
            elif vm["summary.runtime.powerState"] == "suspended":
                ret = "already suspended"
                log.info("VM %s %s", name, ret)
                return ret
            try:
                log.info("Suspending VM %s", name)
                task = vm["object"].Suspend()
                salt.utils.vmware.wait_for_task(task, name, "suspend")
            except Exception as exc:  # pylint: disable=broad-except
                log.error(
                    "Error while suspending VM %s: %s",
                    name,
                    exc,
                    # Show the traceback if the debug logging level is enabled
                    exc_info_on_loglevel=logging.DEBUG,
                )
                return "failed to suspend"

    return "suspended"


def reset(name, soft=False, call=None):
    """
    To reset a VM using its name

    .. note::

        If ``soft=True`` then issues a command to the guest operating system
        asking it to perform a reboot. Otherwise hypervisor will terminate VM and start it again.
        Default is soft=False

        For ``soft=True`` vmtools should be installed on guest system.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a reset vmname
        salt-cloud -a reset vmname soft=True
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The reset action must be called with -a or --action."
        )

    vm_properties = ["name", "summary.runtime.powerState"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if vm["name"] == name:
            if (
                vm["summary.runtime.powerState"] == "suspended"
                or vm["summary.runtime.powerState"] == "poweredOff"
            ):
                ret = "cannot reset in suspended/powered off state"
                log.info("VM %s %s", name, ret)
                return ret
            try:
                log.info("Resetting VM %s", name)
                if soft:
                    vm["object"].RebootGuest()
                else:
                    task = vm["object"].ResetVM_Task()
                    salt.utils.vmware.wait_for_task(task, name, "reset")
            except Exception as exc:  # pylint: disable=broad-except
                log.error(
                    "Error while resetting VM %s: %s",
                    name,
                    exc,
                    # Show the traceback if the debug logging level is enabled
                    exc_info_on_loglevel=logging.DEBUG,
                )
                return "failed to reset"

    return "reset"


def terminate(name, call=None):
    """
    To do an immediate power off of a VM using its name. A ``SIGKILL``
    is issued to the vmx process of the VM

    CLI Example:

    .. code-block:: bash

        salt-cloud -a terminate vmname
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The terminate action must be called with -a or --action."
        )

    vm_properties = ["name", "summary.runtime.powerState"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if vm["name"] == name:
            if vm["summary.runtime.powerState"] == "poweredOff":
                ret = "already powered off"
                log.info("VM %s %s", name, ret)
                return ret
            try:
                log.info("Terminating VM %s", name)
                vm["object"].Terminate()
            except Exception as exc:  # pylint: disable=broad-except
                log.error(
                    "Error while terminating VM %s: %s",
                    name,
                    exc,
                    # Show the traceback if the debug logging level is enabled
                    exc_info_on_loglevel=logging.DEBUG,
                )
                return "failed to terminate"

    return "terminated"


def destroy(name, call=None):
    """
    To destroy a VM from the VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -d vmname
        salt-cloud --destroy vmname
        salt-cloud -a destroy vmname
    """
    if call == "function":
        raise SaltCloudSystemExit(
            "The destroy action must be called with -d, --destroy, -a or --action."
        )

    __utils__["cloud.fire_event"](
        "event",
        "destroying instance",
        "salt/cloud/{}/destroying".format(name),
        args={"name": name},
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )

    vm_properties = ["name", "summary.runtime.powerState"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        if vm["name"] == name:
            if vm["summary.runtime.powerState"] != "poweredOff":
                # Power off the vm first
                try:
                    log.info("Powering Off VM %s", name)
                    task = vm["object"].PowerOff()
                    salt.utils.vmware.wait_for_task(task, name, "power off")
                except Exception as exc:  # pylint: disable=broad-except
                    log.error(
                        "Error while powering off VM %s: %s",
                        name,
                        exc,
                        # Show the traceback if the debug logging level is enabled
                        exc_info_on_loglevel=logging.DEBUG,
                    )
                    return "failed to destroy"
            try:
                log.info("Destroying VM %s", name)
                task = vm["object"].Destroy_Task()
                salt.utils.vmware.wait_for_task(task, name, "destroy")
            except Exception as exc:  # pylint: disable=broad-except
                log.error(
                    "Error while destroying VM %s: %s",
                    name,
                    exc,
                    # Show the traceback if the debug logging level is enabled
                    exc_info_on_loglevel=logging.DEBUG,
                )
                return "failed to destroy"

    __utils__["cloud.fire_event"](
        "event",
        "destroyed instance",
        "salt/cloud/{}/destroyed".format(name),
        args={"name": name},
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )
    if __opts__.get("update_cachedir", False) is True:
        __utils__["cloud.delete_minion_cachedir"](
            name, _get_active_provider_name().split(":")[0], __opts__
        )

    return True


def create(vm_):
    """
    To create a single VM in the VMware environment.

    Sample profile and arguments that can be specified in it can be found
    :ref:`here. <vmware-cloud-profile>`

    CLI Example:

    .. code-block:: bash

        salt-cloud -p vmware-centos6.5 vmname
    """
    try:
        # Check for required profile parameters before sending any API calls.
        if (
            vm_["profile"]
            and config.is_profile_configured(
                __opts__,
                _get_active_provider_name() or "vmware",
                vm_["profile"],
                vm_=vm_,
            )
            is False
        ):
            return False
    except AttributeError:
        pass

    __utils__["cloud.fire_event"](
        "event",
        "starting create",
        "salt/cloud/{}/creating".format(vm_["name"]),
        args=__utils__["cloud.filter_event"](
            "creating", vm_, ["name", "profile", "provider", "driver"]
        ),
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )

    vm_name = config.get_cloud_config_value("name", vm_, __opts__, default=None)
    folder = config.get_cloud_config_value("folder", vm_, __opts__, default=None)
    datacenter = config.get_cloud_config_value(
        "datacenter", vm_, __opts__, default=None
    )
    resourcepool = config.get_cloud_config_value(
        "resourcepool", vm_, __opts__, default=None
    )
    cluster = config.get_cloud_config_value("cluster", vm_, __opts__, default=None)
    datastore = config.get_cloud_config_value("datastore", vm_, __opts__, default=None)
    host = config.get_cloud_config_value("host", vm_, __opts__, default=None)
    template = config.get_cloud_config_value("template", vm_, __opts__, default=False)
    num_cpus = config.get_cloud_config_value("num_cpus", vm_, __opts__, default=None)
    cores_per_socket = config.get_cloud_config_value(
        "cores_per_socket", vm_, __opts__, default=None
    )
    memory = config.get_cloud_config_value("memory", vm_, __opts__, default=None)
    devices = config.get_cloud_config_value("devices", vm_, __opts__, default=None)
    extra_config = config.get_cloud_config_value(
        "extra_config", vm_, __opts__, default=None
    )
    annotation = config.get_cloud_config_value(
        "annotation", vm_, __opts__, default=None
    )
    power = config.get_cloud_config_value("power_on", vm_, __opts__, default=True)
    key_filename = config.get_cloud_config_value(
        "private_key", vm_, __opts__, search_global=False, default=None
    )
    deploy = config.get_cloud_config_value(
        "deploy", vm_, __opts__, search_global=True, default=True
    )
    wait_for_ip_timeout = config.get_cloud_config_value(
        "wait_for_ip_timeout", vm_, __opts__, default=20 * 60
    )
    domain = config.get_cloud_config_value(
        "domain", vm_, __opts__, search_global=False, default="local"
    )
    hardware_version = config.get_cloud_config_value(
        "hardware_version", vm_, __opts__, search_global=False, default=None
    )
    guest_id = config.get_cloud_config_value(
        "image", vm_, __opts__, search_global=False, default=None
    )
    customization = config.get_cloud_config_value(
        "customization", vm_, __opts__, search_global=False, default=True
    )
    customization_spec = config.get_cloud_config_value(
        "customization_spec", vm_, __opts__, search_global=False, default=None
    )
    win_password = config.get_cloud_config_value(
        "win_password", vm_, __opts__, search_global=False, default=None
    )
    win_organization_name = config.get_cloud_config_value(
        "win_organization_name",
        vm_,
        __opts__,
        search_global=False,
        default="Organization",
    )
    plain_text = config.get_cloud_config_value(
        "plain_text", vm_, __opts__, search_global=False, default=False
    )
    win_user_fullname = config.get_cloud_config_value(
        "win_user_fullname", vm_, __opts__, search_global=False, default="Windows User"
    )
    win_run_once = config.get_cloud_config_value(
        "win_run_once", vm_, __opts__, search_global=False, default=None
    )

    # Get service instance object
    si = _get_si()

    container_ref = None

    # If datacenter is specified, set the container reference to start search from it instead
    if datacenter:
        datacenter_ref = salt.utils.vmware.get_mor_by_property(
            _get_si(), vim.Datacenter, datacenter
        )
        container_ref = datacenter_ref if datacenter_ref else None

    if "clonefrom" in vm_:
        # If datacenter is specified, set the container reference to start search from it instead
        if datacenter:
            datacenter_ref = salt.utils.vmware.get_mor_by_property(
                si, vim.Datacenter, datacenter
            )
            container_ref = datacenter_ref if datacenter_ref else None

        # Clone VM/template from specified VM/template
        object_ref = salt.utils.vmware.get_mor_by_property(
            si, vim.VirtualMachine, vm_["clonefrom"], container_ref=container_ref
        )
        if object_ref:
            clone_type = "template" if object_ref.config.template else "vm"
        else:
            raise SaltCloudSystemExit(
                "The VM/template that you have specified under clonefrom does not"
                " exist."
            )
    else:
        clone_type = None
        object_ref = None

    # Either a cluster, or a resource pool must be specified when cloning from template or creating.
    if resourcepool:
        resourcepool_ref = salt.utils.vmware.get_mor_by_property(
            si, vim.ResourcePool, resourcepool, container_ref=container_ref
        )
        if not resourcepool_ref:
            log.error("Specified resource pool: '%s' does not exist", resourcepool)
            if not clone_type or clone_type == "template":
                raise SaltCloudSystemExit(
                    "You must specify a resource pool that exists."
                )
    elif cluster:
        cluster_ref = salt.utils.vmware.get_mor_by_property(
            si, vim.ClusterComputeResource, cluster, container_ref=container_ref
        )
        if not cluster_ref:
            log.error("Specified cluster: '%s' does not exist", cluster)
            if not clone_type or clone_type == "template":
                raise SaltCloudSystemExit("You must specify a cluster that exists.")
        else:
            resourcepool_ref = cluster_ref.resourcePool
    elif clone_type == "template":
        raise SaltCloudSystemExit(
            "You must either specify a cluster or a resource pool when cloning from a"
            " template."
        )
    elif not clone_type:
        raise SaltCloudSystemExit(
            "You must either specify a cluster or a resource pool when creating."
        )
    else:
        log.debug("Using resource pool used by the %s %s", clone_type, vm_["clonefrom"])

    # Either a datacenter or a folder can be optionally specified when cloning, required when creating.
    # If not specified when cloning, the existing VM/template\'s parent folder is used.
    if folder:
        folder_parts = folder.split("/")
        search_reference = container_ref
        for folder_part in folder_parts:
            if folder_part:
                folder_ref = salt.utils.vmware.get_mor_by_property(
                    si, vim.Folder, folder_part, container_ref=search_reference
                )
                search_reference = folder_ref
        if not folder_ref:
            log.error("Specified folder: '%s' does not exist", folder)
            log.debug(
                "Using folder in which %s %s is present", clone_type, vm_["clonefrom"]
            )
            folder_ref = object_ref.parent
    elif datacenter:
        if not datacenter_ref:
            log.error("Specified datacenter: '%s' does not exist", datacenter)
            log.debug(
                "Using datacenter folder in which %s %s is present",
                clone_type,
                vm_["clonefrom"],
            )
            folder_ref = object_ref.parent
        else:
            folder_ref = datacenter_ref.vmFolder
    elif not clone_type:
        raise SaltCloudSystemExit(
            "You must either specify a folder or a datacenter when creating not"
            " cloning."
        )
    else:
        log.debug(
            "Using folder in which %s %s is present", clone_type, vm_["clonefrom"]
        )
        folder_ref = object_ref.parent

    if "clonefrom" in vm_:
        # Create the relocation specs
        reloc_spec = vim.vm.RelocateSpec()

        if (resourcepool and resourcepool_ref) or (cluster and cluster_ref):
            reloc_spec.pool = resourcepool_ref

        # Either a datastore/datastore cluster can be optionally specified.
        # If not specified, the current datastore is used.
        if datastore:
            datastore_ref = salt.utils.vmware.get_mor_by_property(
                si, vim.Datastore, datastore, container_ref=container_ref
            )
            if datastore_ref:
                # specific datastore has been specified
                reloc_spec.datastore = datastore_ref
            else:
                datastore_cluster_ref = salt.utils.vmware.get_mor_by_property(
                    si, vim.StoragePod, datastore, container_ref=container_ref
                )
                if not datastore_cluster_ref:
                    log.error(
                        "Specified datastore/datastore cluster: '%s' does not exist",
                        datastore,
                    )
                    log.debug(
                        "Using datastore used by the %s %s",
                        clone_type,
                        vm_["clonefrom"],
                    )
        else:
            log.debug("No datastore/datastore cluster specified")
            log.debug("Using datastore used by the %s %s", clone_type, vm_["clonefrom"])

        if host:
            host_ref = salt.utils.vmware.get_mor_by_property(
                si, vim.HostSystem, host, container_ref=container_ref
            )
            if host_ref:
                reloc_spec.host = host_ref
            else:
                log.error("Specified host: '%s' does not exist", host)
    else:
        if not datastore:
            raise SaltCloudSystemExit(
                "You must specify a datastore when creating not cloning."
            )
        else:
            datastore_ref = salt.utils.vmware.get_mor_by_property(
                si, vim.Datastore, datastore
            )
            if not datastore_ref:
                raise SaltCloudSystemExit(
                    "Specified datastore: '{}' does not exist".format(datastore)
                )

        if host:
            host_ref = salt.utils.vmware.get_mor_by_property(
                _get_si(), vim.HostSystem, host, container_ref=container_ref
            )
            if not host_ref:
                log.error("Specified host: '%s' does not exist", host)

    # Create the config specs
    config_spec = vim.vm.ConfigSpec()

    # If the hardware version is specified and if it is different from the current
    # hardware version, then schedule a hardware version upgrade
    if hardware_version and object_ref is not None:
        hardware_version = "vmx-{:02}".format(hardware_version)
        if hardware_version != object_ref.config.version:
            log.debug(
                "Scheduling hardware version upgrade from %s to %s",
                object_ref.config.version,
                hardware_version,
            )
            scheduled_hardware_upgrade = vim.vm.ScheduledHardwareUpgradeInfo()
            scheduled_hardware_upgrade.upgradePolicy = "always"
            scheduled_hardware_upgrade.versionKey = hardware_version
            config_spec.scheduledHardwareUpgradeInfo = scheduled_hardware_upgrade
        else:
            log.debug("Virtual hardware version already set to %s", hardware_version)

    if num_cpus:
        log.debug("Setting cpu to: %s", num_cpus)
        config_spec.numCPUs = int(num_cpus)

    if cores_per_socket:
        log.debug("Setting cores per socket to: %s", cores_per_socket)
        config_spec.numCoresPerSocket = int(cores_per_socket)

    if memory:
        try:
            memory_num, memory_unit = re.findall(r"[^\W\d_]+|\d+.\d+|\d+", memory)
            if memory_unit.lower() == "mb":
                memory_mb = int(memory_num)
            elif memory_unit.lower() == "gb":
                memory_mb = int(float(memory_num) * 1024.0)
            else:
                err_msg = "Invalid memory type specified: '{}'".format(memory_unit)
                log.error(err_msg)
                return {"Error": err_msg}
        except (TypeError, ValueError):
            memory_mb = int(memory)
        log.debug("Setting memory to: %s MB", memory_mb)
        config_spec.memoryMB = memory_mb

    if devices:
        specs = _manage_devices(
            devices, vm=object_ref, container_ref=container_ref, new_vm_name=vm_name
        )
        config_spec.deviceChange = specs["device_specs"]

    if extra_config:
        for key, value in extra_config.items():
            option = vim.option.OptionValue(key=key, value=value)
            config_spec.extraConfig.append(option)

    if annotation:
        config_spec.annotation = str(annotation)

    if "clonefrom" in vm_:
        clone_spec = handle_snapshot(config_spec, object_ref, reloc_spec, template, vm_)
        if not clone_spec:
            clone_spec = build_clonespec(config_spec, object_ref, reloc_spec, template)

        if customization and customization_spec:
            customization_spec = salt.utils.vmware.get_customizationspec_ref(
                si=si, customization_spec_name=customization_spec
            )
            clone_spec.customization = customization_spec.spec
        elif customization and (devices and "network" in list(devices.keys())):
            global_ip = vim.vm.customization.GlobalIPSettings()
            if "dns_servers" in list(vm_.keys()):
                global_ip.dnsServerList = vm_["dns_servers"]

            if "domain" in list(vm_.keys()):
                global_ip.dnsSuffixList = vm_["domain"]

            non_hostname_chars = re.compile(r"[^\w-]")
            if re.search(non_hostname_chars, vm_name):
                host_name = re.split(non_hostname_chars, vm_name, maxsplit=1)[0]
                domain_name = re.split(non_hostname_chars, vm_name, maxsplit=1)[-1]
            else:
                host_name = vm_name
                domain_name = domain

            if "Windows" not in object_ref.config.guestFullName:
                identity = vim.vm.customization.LinuxPrep()
                identity.hostName = vim.vm.customization.FixedName(name=host_name)
                identity.domain = domain_name
            else:
                identity = vim.vm.customization.Sysprep()
                identity.guiUnattended = vim.vm.customization.GuiUnattended()
                identity.guiUnattended.autoLogon = True
                identity.guiUnattended.autoLogonCount = 1
                identity.guiUnattended.password = vim.vm.customization.Password()
                identity.guiUnattended.password.value = win_password
                identity.guiUnattended.password.plainText = plain_text
                if win_run_once:
                    identity.guiRunOnce = vim.vm.customization.GuiRunOnce()
                    identity.guiRunOnce.commandList = win_run_once
                identity.userData = vim.vm.customization.UserData()
                identity.userData.fullName = win_user_fullname
                identity.userData.orgName = win_organization_name
                identity.userData.computerName = vim.vm.customization.FixedName()
                identity.userData.computerName.name = host_name
                identity.identification = vim.vm.customization.Identification()
            custom_spec = vim.vm.customization.Specification(
                globalIPSettings=global_ip,
                identity=identity,
                nicSettingMap=specs["nics_map"],
            )
            clone_spec.customization = custom_spec

        if not template:
            clone_spec.powerOn = power

        log.debug("clone_spec set to:\n%s", pprint.pformat(clone_spec))

    else:
        config_spec.name = vm_name
        config_spec.files = vim.vm.FileInfo()
        config_spec.files.vmPathName = "[{0}] {1}/{1}.vmx".format(datastore, vm_name)
        config_spec.guestId = guest_id

        log.debug("config_spec set to:\n%s", pprint.pformat(config_spec))

    event_kwargs = vm_.copy()
    if event_kwargs.get("password"):
        del event_kwargs["password"]

    try:
        __utils__["cloud.fire_event"](
            "event",
            "requesting instance",
            "salt/cloud/{}/requesting".format(vm_["name"]),
            args=__utils__["cloud.filter_event"](
                "requesting", event_kwargs, list(event_kwargs)
            ),
            sock_dir=__opts__["sock_dir"],
            transport=__opts__["transport"],
        )

        if "clonefrom" in vm_:
            log.info(
                "Creating %s from %s(%s)", vm_["name"], clone_type, vm_["clonefrom"]
            )

            if datastore and not datastore_ref and datastore_cluster_ref:
                # datastore cluster has been specified so apply Storage DRS recommendations
                pod_spec = vim.storageDrs.PodSelectionSpec(
                    storagePod=datastore_cluster_ref
                )

                storage_spec = vim.storageDrs.StoragePlacementSpec(
                    type="clone",
                    vm=object_ref,
                    podSelectionSpec=pod_spec,
                    cloneSpec=clone_spec,
                    cloneName=vm_name,
                    folder=folder_ref,
                )

                # get recommended datastores
                recommended_datastores = (
                    si.content.storageResourceManager.RecommendDatastores(
                        storageSpec=storage_spec
                    )
                )

                # apply storage DRS recommendations
                task = si.content.storageResourceManager.ApplyStorageDrsRecommendation_Task(
                    recommended_datastores.recommendations[0].key
                )
                salt.utils.vmware.wait_for_task(
                    task, vm_name, "apply storage DRS recommendations", 5, "info"
                )
            else:
                # clone the VM/template
                task = object_ref.Clone(folder_ref, vm_name, clone_spec)
                salt.utils.vmware.wait_for_task(task, vm_name, "clone", 5, "info")
        else:
            log.info("Creating %s", vm_["name"])

            if host:
                task = folder_ref.CreateVM_Task(config_spec, resourcepool_ref, host_ref)
            else:
                task = folder_ref.CreateVM_Task(config_spec, resourcepool_ref)
            salt.utils.vmware.wait_for_task(task, vm_name, "create", 15, "info")
    except Exception as exc:  # pylint: disable=broad-except
        err_msg = "Error creating {}: {}".format(vm_["name"], exc)
        log.error(
            err_msg,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {"Error": err_msg}

    new_vm_ref = salt.utils.vmware.get_mor_by_property(
        si, vim.VirtualMachine, vm_name, container_ref=container_ref
    )

    # Find how to power on in CreateVM_Task (if possible), for now this will do
    try:
        if not clone_type and power:
            task = new_vm_ref.PowerOn()
            salt.utils.vmware.wait_for_task(task, vm_name, "power", 5, "info")
    except Exception as exc:  # pylint: disable=broad-except
        log.info("Powering on the VM threw this exception. Ignoring.")
        log.info(exc)

    # If it a template or if it does not need to be powered on then do not wait for the IP
    out = None
    if not template and power:
        ip = _wait_for_ip(new_vm_ref, wait_for_ip_timeout)
        if ip:
            log.info("[ %s ] IPv4 is: %s", vm_name, ip)
            # ssh or smb using ip and install salt only if deploy is True
            if deploy:
                vm_["key_filename"] = key_filename
                # if specified, prefer ssh_host to the discovered ip address
                if "ssh_host" not in vm_:
                    vm_["ssh_host"] = ip
                log.info("[ %s ] Deploying to %s", vm_name, vm_["ssh_host"])

                out = __utils__["cloud.bootstrap"](vm_, __opts__)

    data = show_instance(vm_name, call="action")

    if deploy and isinstance(out, dict):
        data["deploy_kwargs"] = out.get("deploy_kwargs", {})

    __utils__["cloud.fire_event"](
        "event",
        "created instance",
        "salt/cloud/{}/created".format(vm_["name"]),
        args=__utils__["cloud.filter_event"](
            "created", vm_, ["name", "profile", "provider", "driver"]
        ),
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )
    return data


def handle_snapshot(config_spec, object_ref, reloc_spec, template, vm_):
    """
    Returns a clone spec for cloning from shapshots
    :rtype vim.vm.CloneSpec
    """
    if "snapshot" not in vm_:
        return None

    allowed_types = [
        FLATTEN_DISK_FULL_CLONE,
        COPY_ALL_DISKS_FULL_CLONE,
        CURRENT_STATE_LINKED_CLONE,
        QUICK_LINKED_CLONE,
    ]

    clone_spec = get_clonespec_for_valid_snapshot(
        config_spec, object_ref, reloc_spec, template, vm_
    )
    if not clone_spec:
        raise SaltCloudSystemExit(
            "Invalid disk move type specified supported types are {}".format(
                " ".join(allowed_types)
            )
        )
    return clone_spec


def get_clonespec_for_valid_snapshot(
    config_spec, object_ref, reloc_spec, template, vm_
):
    """
    return clonespec only if values are valid
    """
    moving = True
    if QUICK_LINKED_CLONE == vm_["snapshot"]["disk_move_type"]:
        reloc_spec.diskMoveType = QUICK_LINKED_CLONE
    elif CURRENT_STATE_LINKED_CLONE == vm_["snapshot"]["disk_move_type"]:
        reloc_spec.diskMoveType = CURRENT_STATE_LINKED_CLONE
    elif COPY_ALL_DISKS_FULL_CLONE == vm_["snapshot"]["disk_move_type"]:
        reloc_spec.diskMoveType = COPY_ALL_DISKS_FULL_CLONE
    elif FLATTEN_DISK_FULL_CLONE == vm_["snapshot"]["disk_move_type"]:
        reloc_spec.diskMoveType = FLATTEN_DISK_FULL_CLONE
    else:
        moving = False

    if moving:
        return build_clonespec(config_spec, object_ref, reloc_spec, template)

    return None


def build_clonespec(config_spec, object_ref, reloc_spec, template):
    """
    Returns the clone spec
    """
    if reloc_spec.diskMoveType == QUICK_LINKED_CLONE:
        return vim.vm.CloneSpec(
            template=template,
            location=reloc_spec,
            config=config_spec,
            snapshot=object_ref.snapshot.currentSnapshot,
        )

    return vim.vm.CloneSpec(template=template, location=reloc_spec, config=config_spec)


def create_datacenter(kwargs=None, call=None):
    """
    Create a new data center in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_datacenter my-vmware-config name="MyNewDatacenter"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The create_datacenter function must be called with -f or --function."
        )

    datacenter_name = kwargs.get("name") if kwargs and "name" in kwargs else None

    if not datacenter_name:
        raise SaltCloudSystemExit(
            "You must specify name of the new datacenter to be created."
        )

    if not datacenter_name or len(datacenter_name) >= 80:
        raise SaltCloudSystemExit(
            "The datacenter name must be a non empty string of less than 80 characters."
        )

    # Get the service instance
    si = _get_si()

    # Check if datacenter already exists
    datacenter_ref = salt.utils.vmware.get_mor_by_property(
        si, vim.Datacenter, datacenter_name
    )
    if datacenter_ref:
        return {datacenter_name: "datacenter already exists"}

    folder = si.content.rootFolder

    # Verify that the folder is of type vim.Folder
    if isinstance(folder, vim.Folder):
        try:
            folder.CreateDatacenter(name=datacenter_name)
        except Exception as exc:  # pylint: disable=broad-except
            log.error(
                "Error creating datacenter %s: %s",
                datacenter_name,
                exc,
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG,
            )
            return False

        log.debug("Created datacenter %s", datacenter_name)
        return {datacenter_name: "created"}

    return False


def create_cluster(kwargs=None, call=None):
    """
    Create a new cluster under the specified datacenter in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_cluster my-vmware-config name="myNewCluster" datacenter="datacenterName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The create_cluster function must be called with -f or --function."
        )

    cluster_name = kwargs.get("name") if kwargs and "name" in kwargs else None
    datacenter = kwargs.get("datacenter") if kwargs and "datacenter" in kwargs else None

    if not cluster_name:
        raise SaltCloudSystemExit(
            "You must specify name of the new cluster to be created."
        )

    if not datacenter:
        raise SaltCloudSystemExit(
            "You must specify name of the datacenter where the cluster should be"
            " created."
        )

    # Get the service instance
    si = _get_si()

    if not isinstance(datacenter, vim.Datacenter):
        datacenter = salt.utils.vmware.get_mor_by_property(
            si, vim.Datacenter, datacenter
        )
        if not datacenter:
            raise SaltCloudSystemExit("The specified datacenter does not exist.")

    # Check if cluster already exists
    cluster_ref = salt.utils.vmware.get_mor_by_property(
        si, vim.ClusterComputeResource, cluster_name
    )
    if cluster_ref:
        return {cluster_name: "cluster already exists"}

    cluster_spec = vim.cluster.ConfigSpecEx()
    folder = datacenter.hostFolder

    # Verify that the folder is of type vim.Folder
    if isinstance(folder, vim.Folder):
        try:
            folder.CreateClusterEx(name=cluster_name, spec=cluster_spec)
        except Exception as exc:  # pylint: disable=broad-except
            log.error(
                "Error creating cluster %s: %s",
                cluster_name,
                exc,
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG,
            )
            return False

        log.debug(
            "Created cluster %s under datacenter %s", cluster_name, datacenter.name
        )
        return {cluster_name: "created"}

    return False


def rescan_hba(kwargs=None, call=None):
    """
    To rescan a specified HBA or all the HBAs on the Host System

    CLI Example:

    .. code-block:: bash

        salt-cloud -f rescan_hba my-vmware-config host="hostSystemName"
        salt-cloud -f rescan_hba my-vmware-config hba="hbaDeviceName" host="hostSystemName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The rescan_hba function must be called with -f or --function."
        )

    hba = kwargs.get("hba") if kwargs and "hba" in kwargs else None
    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None

    if not host_name:
        raise SaltCloudSystemExit("You must specify name of the host system.")

    host_ref = salt.utils.vmware.get_mor_by_property(
        _get_si(), vim.HostSystem, host_name
    )

    try:
        if hba:
            log.info("Rescanning HBA %s on host %s", hba, host_name)
            host_ref.configManager.storageSystem.RescanHba(hba)
            ret = "rescanned HBA {}".format(hba)
        else:
            log.info("Rescanning all HBAs on host %s", host_name)
            host_ref.configManager.storageSystem.RescanAllHba()
            ret = "rescanned all HBAs"
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while rescaning HBA on host %s: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to rescan HBA"}

    return {host_name: ret}


def upgrade_tools_all(call=None):
    """
    To upgrade VMware Tools on all virtual machines present in
    the specified provider

    .. note::

        If the virtual machine is running Windows OS, this function
        will attempt to suppress the automatic reboot caused by a
        VMware Tools upgrade.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f upgrade_tools_all my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The upgrade_tools_all function must be called with -f or --function."
        )

    ret = {}
    vm_properties = ["name"]

    vm_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.VirtualMachine, vm_properties
    )

    for vm in vm_list:
        ret[vm["name"]] = _upg_tools_helper(vm["object"])

    return ret


def upgrade_tools(name, reboot=False, call=None):
    """
    To upgrade VMware Tools on a specified virtual machine.

    .. note::

        If the virtual machine is running Windows OS, use ``reboot=True``
        to reboot the virtual machine after VMware tools upgrade. Default
        is ``reboot=False``

    CLI Example:

    .. code-block:: bash

        salt-cloud -a upgrade_tools vmname
        salt-cloud -a upgrade_tools vmname reboot=True
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The upgrade_tools action must be called with -a or --action."
        )

    vm_ref = salt.utils.vmware.get_mor_by_property(_get_si(), vim.VirtualMachine, name)

    return _upg_tools_helper(vm_ref, reboot)


def list_hosts_by_cluster(kwargs=None, call=None):
    """
    List hosts for each cluster; or hosts for a specified cluster in
    this VMware environment

    To list hosts for each cluster:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hosts_by_cluster my-vmware-config

    To list hosts for a specified cluster:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hosts_by_cluster my-vmware-config cluster="clusterName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_hosts_by_cluster function must be called with -f or --function."
        )

    ret = {}
    cluster_name = kwargs.get("cluster") if kwargs and "cluster" in kwargs else None
    cluster_properties = ["name"]

    cluster_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.ClusterComputeResource, cluster_properties
    )

    for cluster in cluster_list:
        ret[cluster["name"]] = []
        for host in cluster["object"].host:
            if isinstance(host, vim.HostSystem):
                ret[cluster["name"]].append(host.name)
        if cluster_name and cluster_name == cluster["name"]:
            return {"Hosts by Cluster": {cluster_name: ret[cluster_name]}}

    return {"Hosts by Cluster": ret}


def list_clusters_by_datacenter(kwargs=None, call=None):
    """
    List clusters for each datacenter; or clusters for a specified datacenter in
    this VMware environment

    To list clusters for each datacenter:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_clusters_by_datacenter my-vmware-config

    To list clusters for a specified datacenter:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_clusters_by_datacenter my-vmware-config datacenter="datacenterName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_clusters_by_datacenter function must be called with "
            "-f or --function."
        )

    ret = {}
    datacenter_name = (
        kwargs.get("datacenter") if kwargs and "datacenter" in kwargs else None
    )
    datacenter_properties = ["name"]

    datacenter_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.Datacenter, datacenter_properties
    )

    for datacenter in datacenter_list:
        ret[datacenter["name"]] = []
        for cluster in datacenter["object"].hostFolder.childEntity:
            if isinstance(cluster, vim.ClusterComputeResource):
                ret[datacenter["name"]].append(cluster.name)
        if datacenter_name and datacenter_name == datacenter["name"]:
            return {"Clusters by Datacenter": {datacenter_name: ret[datacenter_name]}}

    return {"Clusters by Datacenter": ret}


def list_hosts_by_datacenter(kwargs=None, call=None):
    """
    List hosts for each datacenter; or hosts for a specified datacenter in
    this VMware environment

    To list hosts for each datacenter:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hosts_by_datacenter my-vmware-config

    To list hosts for a specified datacenter:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hosts_by_datacenter my-vmware-config datacenter="datacenterName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_hosts_by_datacenter function must be called with "
            "-f or --function."
        )

    ret = {}
    datacenter_name = (
        kwargs.get("datacenter") if kwargs and "datacenter" in kwargs else None
    )
    datacenter_properties = ["name"]

    datacenter_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.Datacenter, datacenter_properties
    )

    for datacenter in datacenter_list:
        ret[datacenter["name"]] = []
        for cluster in datacenter["object"].hostFolder.childEntity:
            if isinstance(cluster, vim.ClusterComputeResource):
                for host in cluster.host:
                    if isinstance(host, vim.HostSystem):
                        ret[datacenter["name"]].append(host.name)
        if datacenter_name and datacenter_name == datacenter["name"]:
            return {"Hosts by Datacenter": {datacenter_name: ret[datacenter_name]}}

    return {"Hosts by Datacenter": ret}


def list_hbas(kwargs=None, call=None):
    """
    List all HBAs for each host system; or all HBAs for a specified host
    system; or HBAs of specified type for each host system; or HBAs of
    specified type for a specified host system in this VMware environment

    .. note::

        You can specify type as either ``parallel``, ``iscsi``, ``block``
        or ``fibre``.

    To list all HBAs for each host system:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hbas my-vmware-config

    To list all HBAs for a specified host system:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hbas my-vmware-config host="hostSystemName"

    To list HBAs of specified type for each host system:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hbas my-vmware-config type="HBAType"

    To list HBAs of specified type for a specified host system:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_hbas my-vmware-config host="hostSystemName" type="HBAtype"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_hbas function must be called with -f or --function."
        )

    ret = {}
    hba_type = kwargs.get("type").lower() if kwargs and "type" in kwargs else None
    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None
    host_properties = ["name", "config.storageDevice.hostBusAdapter"]

    if hba_type and hba_type not in ["parallel", "block", "iscsi", "fibre"]:
        raise SaltCloudSystemExit(
            "Specified hba type {} currently not supported.".format(hba_type)
        )

    host_list = salt.utils.vmware.get_mors_with_properties(
        _get_si(), vim.HostSystem, host_properties
    )

    for host in host_list:
        ret[host["name"]] = {}
        for hba in host["config.storageDevice.hostBusAdapter"]:
            hba_spec = {
                "driver": hba.driver,
                "status": hba.status,
                "type": type(hba).__name__.rsplit(".", 1)[1],
            }
            if hba_type:
                if isinstance(hba, _get_hba_type(hba_type)):
                    if hba.model in ret[host["name"]]:
                        ret[host["name"]][hba.model][hba.device] = hba_spec
                    else:
                        ret[host["name"]][hba.model] = {hba.device: hba_spec}
            else:
                if hba.model in ret[host["name"]]:
                    ret[host["name"]][hba.model][hba.device] = hba_spec
                else:
                    ret[host["name"]][hba.model] = {hba.device: hba_spec}
        if host["name"] == host_name:
            return {"HBAs by Host": {host_name: ret[host_name]}}

    return {"HBAs by Host": ret}


def list_dvs(kwargs=None, call=None):
    """
    List all the distributed virtual switches for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_dvs my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_dvs function must be called with -f or --function."
        )

    return {"Distributed Virtual Switches": salt.utils.vmware.list_dvs(_get_si())}


def list_vapps(kwargs=None, call=None):
    """
    List all the vApps for this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_vapps my-vmware-config
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_vapps function must be called with -f or --function."
        )

    return {"vApps": salt.utils.vmware.list_vapps(_get_si())}


def enter_maintenance_mode(kwargs=None, call=None):
    """
    To put the specified host system in maintenance mode in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f enter_maintenance_mode my-vmware-config host="myHostSystemName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The enter_maintenance_mode function must be called with -f or --function."
        )

    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None

    host_ref = salt.utils.vmware.get_mor_by_property(
        _get_si(), vim.HostSystem, host_name
    )

    if not host_name or not host_ref:
        raise SaltCloudSystemExit("You must specify a valid name of the host system.")

    if host_ref.runtime.inMaintenanceMode:
        return {host_name: "already in maintenance mode"}

    try:
        task = host_ref.EnterMaintenanceMode(timeout=0, evacuatePoweredOffVms=True)
        salt.utils.vmware.wait_for_task(task, host_name, "enter maintenance mode")
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while moving host system %s in maintenance mode: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to enter maintenance mode"}

    return {host_name: "entered maintenance mode"}


def exit_maintenance_mode(kwargs=None, call=None):
    """
    To take the specified host system out of maintenance mode in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f exit_maintenance_mode my-vmware-config host="myHostSystemName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The exit_maintenance_mode function must be called with -f or --function."
        )

    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None

    host_ref = salt.utils.vmware.get_mor_by_property(
        _get_si(), vim.HostSystem, host_name
    )

    if not host_name or not host_ref:
        raise SaltCloudSystemExit("You must specify a valid name of the host system.")

    if not host_ref.runtime.inMaintenanceMode:
        return {host_name: "already not in maintenance mode"}

    try:
        task = host_ref.ExitMaintenanceMode(timeout=0)
        salt.utils.vmware.wait_for_task(task, host_name, "exit maintenance mode")
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while moving host system %s out of maintenance mode: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to exit maintenance mode"}

    return {host_name: "exited maintenance mode"}


def create_folder(kwargs=None, call=None):
    """
    Create the specified folder path in this VMware environment

    .. note::

        To create a Host and Cluster Folder under a Datacenter, specify
        ``path="/yourDatacenterName/host/yourFolderName"``

        To create a Network Folder under a Datacenter, specify
        ``path="/yourDatacenterName/network/yourFolderName"``

        To create a Storage Folder under a Datacenter, specify
        ``path="/yourDatacenterName/datastore/yourFolderName"``

        To create a VM and Template Folder under a Datacenter, specify
        ``path="/yourDatacenterName/vm/yourFolderName"``

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_folder my-vmware-config path="/Local/a/b/c"
        salt-cloud -f create_folder my-vmware-config path="/MyDatacenter/vm/MyVMFolder"
        salt-cloud -f create_folder my-vmware-config path="/MyDatacenter/host/MyHostFolder"
        salt-cloud -f create_folder my-vmware-config path="/MyDatacenter/network/MyNetworkFolder"
        salt-cloud -f create_folder my-vmware-config path="/MyDatacenter/storage/MyStorageFolder"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The create_folder function must be called with -f or --function."
        )

    # Get the service instance object
    si = _get_si()

    folder_path = kwargs.get("path") if kwargs and "path" in kwargs else None

    if not folder_path:
        raise SaltCloudSystemExit("You must specify a non empty folder path.")

    folder_refs = []
    inventory_path = "/"
    path_exists = True

    # Split the path in a list and loop over it to check for its existence
    for index, folder_name in enumerate(
        os.path.normpath(folder_path.strip("/")).split("/")
    ):
        inventory_path = os.path.join(inventory_path, folder_name)
        folder_ref = si.content.searchIndex.FindByInventoryPath(
            inventoryPath=inventory_path
        )
        if isinstance(folder_ref, vim.Folder):
            # This is a folder that exists so just append and skip it
            log.debug("Path %s/ exists in the inventory", inventory_path)
            folder_refs.append(folder_ref)
        elif isinstance(folder_ref, vim.Datacenter):
            # This is a datacenter that exists so just append and skip it
            log.debug("Path %s/ exists in the inventory", inventory_path)
            folder_refs.append(folder_ref)
        else:
            path_exists = False
            if not folder_refs:
                # If this is the first folder, create it under the rootFolder
                log.debug(
                    "Creating folder %s under rootFolder in the inventory", folder_name
                )
                folder_refs.append(si.content.rootFolder.CreateFolder(folder_name))
            else:
                # Create the folder under the parent folder
                log.debug("Creating path %s/ in the inventory", inventory_path)
                folder_refs.append(folder_refs[index - 1].CreateFolder(folder_name))

    if path_exists:
        return {inventory_path: "specfied path already exists"}

    return {inventory_path: "created the specified path"}


def create_snapshot(name, kwargs=None, call=None):
    """
    Create a snapshot of the specified virtual machine in this VMware
    environment

    .. note::

        If the VM is powered on, the internal state of the VM (memory
        dump) is included in the snapshot by default which will also set
        the power state of the snapshot to "powered on". You can set
        ``memdump=False`` to override this. This field is ignored if
        the virtual machine is powered off or if the VM does not support
        snapshots with memory dumps. Default is ``memdump=True``

    .. note::

        If the VM is powered on when the snapshot is taken, VMware Tools
        can be used to quiesce the file system in the virtual machine by
        setting ``quiesce=True``. This field is ignored if the virtual
        machine is powered off; if VMware Tools are not available or if
        ``memdump=True``. Default is ``quiesce=False``

    CLI Example:

    .. code-block:: bash

        salt-cloud -a create_snapshot vmname snapshot_name="mySnapshot"
        salt-cloud -a create_snapshot vmname snapshot_name="mySnapshot" [description="My snapshot"] [memdump=False] [quiesce=True]
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The create_snapshot action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    snapshot_name = (
        kwargs.get("snapshot_name") if kwargs and "snapshot_name" in kwargs else None
    )

    if not snapshot_name:
        raise SaltCloudSystemExit(
            "You must specify snapshot name for the snapshot to be created."
        )

    memdump = _str_to_bool(kwargs.get("memdump", True))
    quiesce = _str_to_bool(kwargs.get("quiesce", False))

    vm_ref = salt.utils.vmware.get_mor_by_property(_get_si(), vim.VirtualMachine, name)

    if vm_ref.summary.runtime.powerState != "poweredOn":
        log.debug(
            "VM %s is not powered on. Setting both memdump and quiesce to False", name
        )
        memdump = False
        quiesce = False

    if memdump and quiesce:
        # Either memdump or quiesce should be set to True
        log.warning(
            "You can only set either memdump or quiesce to True. Setting quiesce=False"
        )
        quiesce = False

    desc = kwargs.get("description") if "description" in kwargs else ""

    try:
        task = vm_ref.CreateSnapshot(snapshot_name, desc, memdump, quiesce)
        salt.utils.vmware.wait_for_task(task, name, "create snapshot", 5, "info")
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while creating snapshot of %s: %s",
            name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return "failed to create snapshot"

    return {
        "Snapshot created successfully": _get_snapshots(
            vm_ref.snapshot.rootSnapshotList, vm_ref.snapshot.currentSnapshot
        )
    }


def revert_to_snapshot(name, kwargs=None, call=None):
    """
    Revert virtual machine to its current snapshot. If no snapshot
    exists, the state of the virtual machine remains unchanged

    .. note::

        The virtual machine will be powered on if the power state of
        the snapshot when it was created was set to "Powered On". Set
        ``power_off=True`` so that the virtual machine stays powered
        off regardless of the power state of the snapshot when it was
        created. Default is ``power_off=False``.

        If the power state of the snapshot when it was created was
        "Powered On" and if ``power_off=True``, the VM will be put in
        suspended state after it has been reverted to the snapshot.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a revert_to_snapshot vmame [power_off=True]
        salt-cloud -a revert_to_snapshot vmame snapshot_name="selectedSnapshot" [power_off=True]
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The revert_to_snapshot action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    snapshot_name = (
        kwargs.get("snapshot_name") if kwargs and "snapshot_name" in kwargs else None
    )

    suppress_power_on = _str_to_bool(kwargs.get("power_off", False))

    vm_ref = salt.utils.vmware.get_mor_by_property(_get_si(), vim.VirtualMachine, name)

    if not vm_ref.rootSnapshot:
        log.error("VM %s does not contain any current snapshots", name)
        return "revert failed"

    msg = "reverted to current snapshot"

    try:
        if snapshot_name is None:
            log.debug("Reverting VM %s to current snapshot", name)
            task = vm_ref.RevertToCurrentSnapshot(suppressPowerOn=suppress_power_on)
        else:
            log.debug("Reverting VM %s to snapshot %s", name, snapshot_name)
            msg = "reverted to snapshot {}".format(snapshot_name)
            snapshot_ref = _get_snapshot_ref_by_name(vm_ref, snapshot_name)
            if snapshot_ref is None:
                return "specified snapshot '{}' does not exist".format(snapshot_name)
            task = snapshot_ref.snapshot.Revert(suppressPowerOn=suppress_power_on)

        salt.utils.vmware.wait_for_task(task, name, "revert to snapshot", 5, "info")

    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while reverting VM %s to snapshot: %s",
            name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return "revert failed"

    return msg


def remove_snapshot(name, kwargs=None, call=None):
    """
    Remove a snapshot of the specified virtual machine in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -a remove_snapshot vmname snapshot_name="mySnapshot"
        salt-cloud -a remove_snapshot vmname snapshot_name="mySnapshot" [remove_children="True"]
    """

    if call != "action":
        raise SaltCloudSystemExit(
            "The create_snapshot action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    snapshot_name = (
        kwargs.get("snapshot_name") if kwargs and "snapshot_name" in kwargs else None
    )
    remove_children = _str_to_bool(kwargs.get("remove_children", False))

    if not snapshot_name:
        raise SaltCloudSystemExit(
            "You must specify snapshot name for the snapshot to be deleted."
        )

    vm_ref = salt.utils.vmware.get_mor_by_property(_get_si(), vim.VirtualMachine, name)

    if not _get_snapshot_ref_by_name(vm_ref, snapshot_name):
        raise SaltCloudSystemExit(
            "Сould not find the snapshot with the specified name."
        )

    try:
        snap_obj = _get_snapshot_ref_by_name(vm_ref, snapshot_name).snapshot
        task = snap_obj.RemoveSnapshot_Task(remove_children)
        salt.utils.vmware.wait_for_task(task, name, "remove snapshot", 5, "info")

    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while removing snapshot of %s: %s",
            name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return "failed to remove snapshot"

    if vm_ref.snapshot:
        return {
            "Snapshot removed successfully": _get_snapshots(
                vm_ref.snapshot.rootSnapshotList, vm_ref.snapshot.currentSnapshot
            )
        }

    return "Snapshots removed successfully"


def remove_all_snapshots(name, kwargs=None, call=None):
    """
    Remove all the snapshots present for the specified virtual machine.

    .. note::

        All the snapshots higher up in the hierarchy of the current snapshot tree
        are consolidated and their virtual disks are merged. To override this
        behavior and only remove all snapshots, set ``merge_snapshots=False``.
        Default is ``merge_snapshots=True``

    CLI Example:

    .. code-block:: bash

        salt-cloud -a remove_all_snapshots vmname [merge_snapshots=False]
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The remove_all_snapshots action must be called with -a or --action."
        )

    vm_ref = salt.utils.vmware.get_mor_by_property(_get_si(), vim.VirtualMachine, name)

    try:
        task = vm_ref.RemoveAllSnapshots()
        salt.utils.vmware.wait_for_task(task, name, "remove snapshots", 5, "info")
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while removing snapshots on VM %s: %s",
            name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return "Failed to remove snapshots"

    return "Removed all snapshots"


def convert_to_template(name, kwargs=None, call=None):
    """
    Convert the specified virtual machine to template.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a convert_to_template vmname
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The convert_to_template action must be called with -a or --action."
        )

    vm_ref = salt.utils.vmware.get_mor_by_property(_get_si(), vim.VirtualMachine, name)

    if vm_ref.config.template:
        raise SaltCloudSystemExit("{} already a template".format(name))

    try:
        vm_ref.MarkAsTemplate()
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while converting VM to template %s: %s",
            name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return "failed to convert to teamplate"

    return "{} converted to template".format(name)


def add_host(kwargs=None, call=None):
    """
    Add a host system to the specified cluster or datacenter in this VMware environment

    .. note::

        To use this function, you need to specify ``esxi_host_user`` and
        ``esxi_host_password`` under your provider configuration set up at
        ``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/vmware.conf``:

        .. code-block:: yaml

            vcenter01:
              driver: vmware
              user: 'DOMAIN\\user'
              password: 'verybadpass'
              url: 'vcenter01.domain.com'

              # Required when adding a host system
              esxi_host_user: 'root'
              esxi_host_password: 'myhostpassword'
              # Optional fields that can be specified when adding a host system
              esxi_host_ssl_thumbprint: '12:A3:45:B6:CD:7E:F8:90:A1:BC:23:45:D6:78:9E:FA:01:2B:34:CD'

        The SSL thumbprint of the host system can be optionally specified by setting
        ``esxi_host_ssl_thumbprint`` under your provider configuration. To get the SSL
        thumbprint of the host system, execute the following command from a remote
        server:

        .. code-block:: bash

            echo -n | openssl s_client -connect <YOUR-HOSTSYSTEM-DNS/IP>:443 2>/dev/null | openssl x509 -noout -fingerprint -sha1

    CLI Example:

    .. code-block:: bash

        salt-cloud -f add_host my-vmware-config host="myHostSystemName" cluster="myClusterName"
        salt-cloud -f add_host my-vmware-config host="myHostSystemName" datacenter="myDatacenterName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The add_host function must be called with -f or --function."
        )

    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None
    cluster_name = kwargs.get("cluster") if kwargs and "cluster" in kwargs else None
    datacenter_name = (
        kwargs.get("datacenter") if kwargs and "datacenter" in kwargs else None
    )

    host_user = config.get_cloud_config_value(
        "esxi_host_user", get_configured_provider(), __opts__, search_global=False
    )
    host_password = config.get_cloud_config_value(
        "esxi_host_password", get_configured_provider(), __opts__, search_global=False
    )
    host_ssl_thumbprint = config.get_cloud_config_value(
        "esxi_host_ssl_thumbprint",
        get_configured_provider(),
        __opts__,
        search_global=False,
    )

    if not host_user:
        raise SaltCloudSystemExit(
            "You must specify the ESXi host username in your providers config."
        )

    if not host_password:
        raise SaltCloudSystemExit(
            "You must specify the ESXi host password in your providers config."
        )

    if not host_name:
        raise SaltCloudSystemExit(
            "You must specify either the IP or DNS name of the host system."
        )

    if (cluster_name and datacenter_name) or not (cluster_name or datacenter_name):
        raise SaltCloudSystemExit(
            "You must specify either the cluster name or the datacenter name."
        )

    # Get the service instance
    si = _get_si()

    if cluster_name:
        cluster_ref = salt.utils.vmware.get_mor_by_property(
            si, vim.ClusterComputeResource, cluster_name
        )
        if not cluster_ref:
            raise SaltCloudSystemExit("Specified cluster does not exist.")

    if datacenter_name:
        datacenter_ref = salt.utils.vmware.get_mor_by_property(
            si, vim.Datacenter, datacenter_name
        )
        if not datacenter_ref:
            raise SaltCloudSystemExit("Specified datacenter does not exist.")

    spec = vim.host.ConnectSpec(
        hostName=host_name,
        userName=host_user,
        password=host_password,
    )

    if host_ssl_thumbprint:
        spec.sslThumbprint = host_ssl_thumbprint
    else:
        log.warning("SSL thumbprint has not been specified in provider configuration")
        # This smells like a not-so-good idea. A plenty of VMWare VCenters
        # do not listen to the default port 443.
        try:
            log.debug("Trying to get the SSL thumbprint directly from the host system")
            p1 = subprocess.Popen(
                ("echo", "-n"), stdout=subprocess.PIPE, stderr=subprocess.PIPE
            )
            p2 = subprocess.Popen(
                ("openssl", "s_client", "-connect", "{}:443".format(host_name)),
                stdin=p1.stdout,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            p3 = subprocess.Popen(
                ("openssl", "x509", "-noout", "-fingerprint", "-sha1"),
                stdin=p2.stdout,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
            out = salt.utils.stringutils.to_str(p3.stdout.read())
            ssl_thumbprint = out.split("=")[-1].strip()
            log.debug(
                "SSL thumbprint received from the host system: %s", ssl_thumbprint
            )
            spec.sslThumbprint = ssl_thumbprint
        except Exception as exc:  # pylint: disable=broad-except
            log.error(
                "Error while trying to get SSL thumbprint of host %s: %s",
                host_name,
                exc,
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG,
            )
            return {host_name: "failed to add host"}

    try:
        if cluster_name:
            task = cluster_ref.AddHost(spec=spec, asConnected=True)
            ret = "added host system to cluster {}".format(cluster_name)
        if datacenter_name:
            task = datacenter_ref.hostFolder.AddStandaloneHost(
                spec=spec, addConnected=True
            )
            ret = "added host system to datacenter {}".format(datacenter_name)
        salt.utils.vmware.wait_for_task(task, host_name, "add host system", 5, "info")
    except Exception as exc:  # pylint: disable=broad-except
        if isinstance(exc, vim.fault.SSLVerifyFault):
            log.error("Authenticity of the host's SSL certificate is not verified")
            log.info(
                "Try again after setting the esxi_host_ssl_thumbprint "
                "to %s in provider configuration",
                spec.sslThumbprint,
            )
        log.error(
            "Error while adding host %s: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to add host"}

    return {host_name: ret}


def remove_host(kwargs=None, call=None):
    """
    Remove the specified host system from this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f remove_host my-vmware-config host="myHostSystemName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The remove_host function must be called with -f or --function."
        )

    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None

    if not host_name:
        raise SaltCloudSystemExit("You must specify name of the host system.")

    # Get the service instance
    si = _get_si()

    host_ref = salt.utils.vmware.get_mor_by_property(si, vim.HostSystem, host_name)
    if not host_ref:
        raise SaltCloudSystemExit("Specified host system does not exist.")

    try:
        if isinstance(host_ref.parent, vim.ClusterComputeResource):
            # This is a host system that is part of a Cluster
            task = host_ref.Destroy_Task()
        else:
            # This is a standalone host system
            task = host_ref.parent.Destroy_Task()
        salt.utils.vmware.wait_for_task(
            task, host_name, "remove host", log_level="info"
        )
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while removing host %s: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to remove host"}

    return {host_name: "removed host from vcenter"}


def connect_host(kwargs=None, call=None):
    """
    Connect the specified host system in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f connect_host my-vmware-config host="myHostSystemName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The connect_host function must be called with -f or --function."
        )

    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None

    if not host_name:
        raise SaltCloudSystemExit("You must specify name of the host system.")

    # Get the service instance
    si = _get_si()

    host_ref = salt.utils.vmware.get_mor_by_property(si, vim.HostSystem, host_name)
    if not host_ref:
        raise SaltCloudSystemExit("Specified host system does not exist.")

    if host_ref.runtime.connectionState == "connected":
        return {host_name: "host system already connected"}

    try:
        task = host_ref.ReconnectHost_Task()
        salt.utils.vmware.wait_for_task(task, host_name, "connect host", 5, "info")
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while connecting host %s: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to connect host"}

    return {host_name: "connected host"}


def disconnect_host(kwargs=None, call=None):
    """
    Disconnect the specified host system in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f disconnect_host my-vmware-config host="myHostSystemName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The disconnect_host function must be called with -f or --function."
        )

    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None

    if not host_name:
        raise SaltCloudSystemExit("You must specify name of the host system.")

    # Get the service instance
    si = _get_si()

    host_ref = salt.utils.vmware.get_mor_by_property(si, vim.HostSystem, host_name)
    if not host_ref:
        raise SaltCloudSystemExit("Specified host system does not exist.")

    if host_ref.runtime.connectionState == "disconnected":
        return {host_name: "host system already disconnected"}

    try:
        task = host_ref.DisconnectHost_Task()
        salt.utils.vmware.wait_for_task(
            task, host_name, "disconnect host", log_level="info"
        )
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while disconnecting host %s: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to disconnect host"}

    return {host_name: "disconnected host"}


def reboot_host(kwargs=None, call=None):
    """
    Reboot the specified host system in this VMware environment

    .. note::

        If the host system is not in maintenance mode, it will not be rebooted. If you
        want to reboot the host system regardless of whether it is in maintenance mode,
        set ``force=True``. Default is ``force=False``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f reboot_host my-vmware-config host="myHostSystemName" [force=True]
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The reboot_host function must be called with -f or --function."
        )

    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None
    force = _str_to_bool(kwargs.get("force")) if kwargs and "force" in kwargs else False

    if not host_name:
        raise SaltCloudSystemExit("You must specify name of the host system.")

    # Get the service instance
    si = _get_si()

    host_ref = salt.utils.vmware.get_mor_by_property(si, vim.HostSystem, host_name)
    if not host_ref:
        raise SaltCloudSystemExit("Specified host system does not exist.")

    if host_ref.runtime.connectionState == "notResponding":
        raise SaltCloudSystemExit(
            "Specified host system cannot be rebooted in it's current state (not"
            " responding)."
        )

    if not host_ref.capability.rebootSupported:
        raise SaltCloudSystemExit("Specified host system does not support reboot.")

    if not host_ref.runtime.inMaintenanceMode and not force:
        raise SaltCloudSystemExit(
            "Specified host system is not in maintenance mode. Specify force=True to"
            " force reboot even if there are virtual machines running or other"
            " operations in progress."
        )

    try:
        host_ref.RebootHost_Task(force)
        _wait_for_host(host_ref, "reboot", 10, "info")
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while rebooting host %s: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to reboot host"}

    return {host_name: "rebooted host"}


def create_datastore_cluster(kwargs=None, call=None):
    """
    Create a new datastore cluster for the specified datacenter in this VMware environment

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_datastore_cluster my-vmware-config name="datastoreClusterName" datacenter="datacenterName"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The create_datastore_cluster function must be called with "
            "-f or --function."
        )

    datastore_cluster_name = kwargs.get("name") if kwargs and "name" in kwargs else None
    datacenter_name = (
        kwargs.get("datacenter") if kwargs and "datacenter" in kwargs else None
    )

    if not datastore_cluster_name:
        raise SaltCloudSystemExit(
            "You must specify name of the new datastore cluster to be created."
        )

    if not datastore_cluster_name or len(datastore_cluster_name) >= 80:
        raise SaltCloudSystemExit(
            "The datastore cluster name must be a non empty string of less than 80"
            " characters."
        )

    if not datacenter_name:
        raise SaltCloudSystemExit(
            "You must specify name of the datacenter where the datastore cluster should"
            " be created."
        )

    # Get the service instance
    si = _get_si()

    # Check if datastore cluster already exists
    datastore_cluster_ref = salt.utils.vmware.get_mor_by_property(
        si, vim.StoragePod, datastore_cluster_name
    )
    if datastore_cluster_ref:
        return {datastore_cluster_name: "datastore cluster already exists"}

    datacenter_ref = salt.utils.vmware.get_mor_by_property(
        si, vim.Datacenter, datacenter_name
    )
    if not datacenter_ref:
        raise SaltCloudSystemExit("The specified datacenter does not exist.")

    try:
        datacenter_ref.datastoreFolder.CreateStoragePod(name=datastore_cluster_name)
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error creating datastore cluster %s: %s",
            datastore_cluster_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return False

    return {datastore_cluster_name: "created"}


def shutdown_host(kwargs=None, call=None):
    """
    Shut down the specified host system in this VMware environment

    .. note::

        If the host system is not in maintenance mode, it will not be shut down. If you
        want to shut down the host system regardless of whether it is in maintenance mode,
        set ``force=True``. Default is ``force=False``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f shutdown_host my-vmware-config host="myHostSystemName" [force=True]
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The shutdown_host function must be called with -f or --function."
        )

    host_name = kwargs.get("host") if kwargs and "host" in kwargs else None
    force = _str_to_bool(kwargs.get("force")) if kwargs and "force" in kwargs else False

    if not host_name:
        raise SaltCloudSystemExit("You must specify name of the host system.")

    # Get the service instance
    si = _get_si()

    host_ref = salt.utils.vmware.get_mor_by_property(si, vim.HostSystem, host_name)
    if not host_ref:
        raise SaltCloudSystemExit("Specified host system does not exist.")

    if host_ref.runtime.connectionState == "notResponding":
        raise SaltCloudSystemExit(
            "Specified host system cannot be shut down in it's current state (not"
            " responding)."
        )

    if not host_ref.capability.rebootSupported:
        raise SaltCloudSystemExit("Specified host system does not support shutdown.")

    if not host_ref.runtime.inMaintenanceMode and not force:
        raise SaltCloudSystemExit(
            "Specified host system is not in maintenance mode. Specify force=True to"
            " force reboot even if there are virtual machines running or other"
            " operations in progress."
        )

    try:
        host_ref.ShutdownHost_Task(force)
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error while shutting down host %s: %s",
            host_name,
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return {host_name: "failed to shut down host"}

    return {host_name: "shut down host"}
