"""
OpenNebula Cloud Module
=======================

The OpenNebula cloud module is used to control access to an OpenNebula cloud.

.. versionadded:: 2014.7.0

:depends: lxml
:depends: OpenNebula installation running version ``4.14`` or later.

Use of this module requires the ``xml_rpc``, ``user``, and ``password``
parameters to be set.

Set up the cloud configuration at ``/etc/salt/cloud.providers`` or
``/etc/salt/cloud.providers.d/opennebula.conf``:

.. code-block:: yaml

    my-opennebula-config:
      xml_rpc: http://localhost:2633/RPC2
      user: oneadmin
      password: JHGhgsayu32jsa
      driver: opennebula

This driver supports accessing new VM instances via DNS entry instead
of IP address.  To enable this feature, in the provider or profile file
add `fqdn_base` with a value matching the base of your fully-qualified
domain name.  Example:

.. code-block:: yaml

    my-opennebula-config:
      [...]
      fqdn_base: <my.basedomain.com>
      [...]

The driver will prepend the hostname to the fqdn_base and do a DNS lookup
to find the IP of the new VM.

.. note:

    Whenever ``data`` is provided as a kwarg to a function and the
    attribute=value syntax is used, the entire ``data`` value must be
    wrapped in single or double quotes. If the value given in the
    attribute=value data string contains multiple words, double quotes
    *must* be used for the value while the entire data string should
    be encapsulated in single quotes. Failing to do so will result in
    an error. Example:

.. code-block:: bash

    salt-cloud -f image_allocate opennebula datastore_name=default \\
        data='NAME="My New Image" DESCRIPTION="Description of the image." \\
        PATH=/home/one_user/images/image_name.img'
    salt-cloud -f secgroup_allocate opennebula \\
        data="Name = test RULE = [PROTOCOL = TCP, RULE_TYPE = inbound, \\
        RANGE = 1000:2000]"

"""

import logging
import os
import pprint
import time

import salt.config as config
import salt.utils.data
import salt.utils.files
from salt.exceptions import (
    SaltCloudConfigError,
    SaltCloudExecutionFailure,
    SaltCloudExecutionTimeout,
    SaltCloudNotFound,
    SaltCloudSystemExit,
)

try:
    import xmlrpc.client  # nosec

    from lxml import etree

    HAS_XML_LIBS = True
except ImportError:
    HAS_XML_LIBS = False


log = logging.getLogger(__name__)

__virtualname__ = "opennebula"


def __virtual__():
    """
    Check for OpenNebula configs.
    """
    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__,
        ("xml_rpc", "user", "password"),
    )


def get_dependencies():
    """
    Warn if dependencies aren't met.
    """
    return config.check_driver_dependencies(__virtualname__, {"lmxl": HAS_XML_LIBS})


def avail_images(call=None):
    """
    Return available OpenNebula images.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-images opennebula
        salt-cloud --function avail_images opennebula
        salt-cloud -f avail_images opennebula

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])

    image_pool = server.one.imagepool.info(auth, -2, -1, -1)[1]

    images = {}
    for image in _get_xml(image_pool):
        images[image.find("NAME").text] = _xml_to_dict(image)

    return images


def avail_locations(call=None):
    """
    Return available OpenNebula locations.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-locations opennebula
        salt-cloud --function avail_locations opennebula
        salt-cloud -f avail_locations opennebula

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    host_pool = server.one.hostpool.info(auth)[1]

    locations = {}
    for host in _get_xml(host_pool):
        locations[host.find("NAME").text] = _xml_to_dict(host)

    return locations


def avail_sizes(call=None):
    """
    Because sizes are built into templates with OpenNebula, there will be no sizes to
    return here.
    """
    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 OpenNebula, there are no sizes "
        "to return."
    )

    return {}


def list_clusters(call=None):
    """
    Returns a list of clusters in OpenNebula.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    cluster_pool = server.one.clusterpool.info(auth)[1]

    clusters = {}
    for cluster in _get_xml(cluster_pool):
        clusters[cluster.find("NAME").text] = _xml_to_dict(cluster)

    return clusters


def list_datastores(call=None):
    """
    Returns a list of data stores on OpenNebula.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    datastore_pool = server.one.datastorepool.info(auth)[1]

    datastores = {}
    for datastore in _get_xml(datastore_pool):
        datastores[datastore.find("NAME").text] = _xml_to_dict(datastore)

    return datastores


def list_hosts(call=None):
    """
    Returns a list of hosts on OpenNebula.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

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

    return avail_locations()


def list_nodes(call=None):
    """
    Return a list of VMs on OpenNebula.

    CLI Example:

    .. code-block:: bash

        salt-cloud -Q
        salt-cloud --query
        salt-cloud --function list_nodes opennebula
        salt-cloud -f list_nodes opennebula

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

    return _list_nodes(full=False)


def list_nodes_full(call=None):
    """
    Return a list of the VMs on OpenNebula.

    CLI Example:

    .. code-block:: bash

        salt-cloud -F
        salt-cloud --full-query
        salt-cloud --function list_nodes_full opennebula
        salt-cloud -f list_nodes_full opennebula

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

    return _list_nodes(full=True)


def list_nodes_select(call=None):
    """
    Return a list of the VMs that are on the provider, with select fields.
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes_full function must be called with -f or --function."
        )

    return __utils__["cloud.list_nodes_select"](
        list_nodes_full("function"),
        __opts__["query.selection"],
        call,
    )


def list_security_groups(call=None):
    """
    Lists all security groups available to the user and the user's groups.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    secgroup_pool = server.one.secgrouppool.info(auth, -2, -1, -1)[1]

    groups = {}
    for group in _get_xml(secgroup_pool):
        groups[group.find("NAME").text] = _xml_to_dict(group)

    return groups


def list_templates(call=None):
    """
    Lists all templates available to the user and the user's groups.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    template_pool = server.one.templatepool.info(auth, -2, -1, -1)[1]

    templates = {}
    for template in _get_xml(template_pool):
        templates[template.find("NAME").text] = _xml_to_dict(template)

    return templates


def list_vns(call=None):
    """
    Lists all virtual networks available to the user and the user's groups.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vn_pool = server.one.vnpool.info(auth, -2, -1, -1)[1]

    vns = {}
    for v_network in _get_xml(vn_pool):
        vns[v_network.find("NAME").text] = _xml_to_dict(v_network)

    return vns


def reboot(name, call=None):
    """
    Reboot a VM.

    .. versionadded:: 2016.3.0

    name
        The name of the VM to reboot.

    CLI Example:

    .. code-block:: bash

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

    log.info("Rebooting node %s", name)

    return vm_action(name, kwargs={"action": "reboot"}, call=call)


def start(name, call=None):
    """
    Start a VM.

    .. versionadded:: 2016.3.0

    name
        The name of the VM to start.

    CLI Example:

    .. code-block:: bash

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

    log.info("Starting node %s", name)

    return vm_action(name, kwargs={"action": "resume"}, call=call)


def stop(name, call=None):
    """
    Stop a VM.

    .. versionadded:: 2016.3.0

    name
        The name of the VM to stop.

    CLI Example:

    .. code-block:: bash

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

    log.info("Stopping node %s", name)

    return vm_action(name, kwargs={"action": "stop"}, call=call)


def get_one_version(kwargs=None, call=None):
    """
    Returns the OpenNebula version.

    .. versionadded:: 2016.3.5

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_one_version one_provider_name
    """

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

    if kwargs is None:
        kwargs = {}

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])

    return server.one.system.version(auth)[1]


def get_cluster_id(kwargs=None, call=None):
    """
    Returns a cluster's ID from the given cluster name.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_cluster_id opennebula name=my-cluster-name
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_cluster_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_cluster_id function requires a name.")

    try:
        ret = list_clusters()[name]["id"]
    except KeyError:
        raise SaltCloudSystemExit(f"The cluster '{name}' could not be found")

    return ret


def get_datastore_id(kwargs=None, call=None):
    """
    Returns a data store's ID from the given data store name.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_datastore_id opennebula name=my-datastore-name
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_datastore_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_datastore_id function requires a name.")

    try:
        ret = list_datastores()[name]["id"]
    except KeyError:
        raise SaltCloudSystemExit(f"The datastore '{name}' could not be found.")

    return ret


def get_host_id(kwargs=None, call=None):
    """
    Returns a host's ID from the given host name.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_host_id opennebula name=my-host-name
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_host_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_host_id function requires a name.")

    try:
        ret = avail_locations()[name]["id"]
    except KeyError:
        raise SaltCloudSystemExit(f"The host '{name}' could not be found")

    return ret


def get_image(vm_):
    r"""
    Return the image object to use.

    vm\_
        The VM dictionary for which to obtain an image.
    """
    images = avail_images()
    vm_image = str(
        config.get_cloud_config_value("image", vm_, __opts__, search_global=False)
    )
    for image in images:
        if vm_image in (images[image]["name"], images[image]["id"]):
            return images[image]["id"]
    raise SaltCloudNotFound(f"The specified image, '{vm_image}', could not be found.")


def get_image_id(kwargs=None, call=None):
    """
    Returns an image's ID from the given image name.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_image_id opennebula name=my-image-name
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_image_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_image_id function requires a name.")

    try:
        ret = avail_images()[name]["id"]
    except KeyError:
        raise SaltCloudSystemExit(f"The image '{name}' could not be found")

    return ret


def get_location(vm_):
    r"""
    Return the VM's location.

    vm\_
        The VM dictionary for which to obtain a location.
    """
    locations = avail_locations()
    vm_location = str(
        config.get_cloud_config_value("location", vm_, __opts__, search_global=False)
    )

    if vm_location == "None":
        return None

    for location in locations:
        if vm_location in (locations[location]["name"], locations[location]["id"]):
            return locations[location]["id"]
    raise SaltCloudNotFound(
        f"The specified location, '{vm_location}', could not be found."
    )


def get_secgroup_id(kwargs=None, call=None):
    """
    Returns a security group's ID from the given security group name.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_secgroup_id opennebula name=my-secgroup-name
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_secgroup_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_secgroup_id function requires a 'name'.")

    try:
        ret = list_security_groups()[name]["id"]
    except KeyError:
        raise SaltCloudSystemExit(f"The security group '{name}' could not be found.")

    return ret


def get_template_image(kwargs=None, call=None):
    """
    Returns a template's image from the given template name.

    .. versionadded:: 2018.3.0

    .. code-block:: bash

        salt-cloud -f get_template_image opennebula name=my-template-name
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_template_image function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_template_image function requires a 'name'.")

    try:
        ret = list_templates()[name]["template"]["disk"]["image"]
    except KeyError:
        raise SaltCloudSystemExit(
            f"The image for template '{name}' could not be found."
        )

    return ret


def get_template_id(kwargs=None, call=None):
    """
    Returns a template's ID from the given template name.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_template_id opennebula name=my-template-name
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_template_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_template_id function requires a 'name'.")

    try:
        ret = list_templates()[name]["id"]
    except KeyError:
        raise SaltCloudSystemExit(f"The template '{name}' could not be found.")

    return ret


def get_template(vm_):
    r"""
    Return the template id for a VM.

    .. versionadded:: 2016.11.0

    vm\_
        The VM dictionary for which to obtain a template.
    """

    vm_template = str(
        config.get_cloud_config_value("template", vm_, __opts__, search_global=False)
    )
    try:
        return list_templates()[vm_template]["id"]
    except KeyError:
        raise SaltCloudNotFound(
            f"The specified template, '{vm_template}', could not be found."
        )


def get_vm_id(kwargs=None, call=None):
    """
    Returns a virtual machine's ID from the given virtual machine's name.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_vm_id opennebula name=my-vm
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_vm_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_vm_id function requires a name.")

    try:
        ret = list_nodes()[name]["id"]
    except KeyError:
        raise SaltCloudSystemExit(f"The VM '{name}' could not be found.")

    return ret


def get_vn_id(kwargs=None, call=None):
    """
    Returns a virtual network's ID from the given virtual network's name.

    .. versionadded:: 2016.3.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_vn_id opennebula name=my-vn-name
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_vn_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    if name is None:
        raise SaltCloudSystemExit("The get_vn_id function requires a name.")

    try:
        ret = list_vns()[name]["id"]
    except KeyError:
        raise SaltCloudSystemExit(f"The VN '{name}' could not be found.")

    return ret


def _get_device_template(disk, disk_info, template=None):
    """
    Returns the template format to create a disk in open nebula

    .. versionadded:: 2018.3.0

    """

    def _require_disk_opts(*args):
        for arg in args:
            if arg not in disk_info:
                raise SaltCloudSystemExit(f"The disk {disk} requires a {arg} argument")

    _require_disk_opts("disk_type", "size")

    size = disk_info["size"]
    disk_type = disk_info["disk_type"]

    if disk_type == "clone":
        if "image" in disk_info:
            clone_image = disk_info["image"]
        else:
            clone_image = get_template_image(kwargs={"name": template})

        clone_image_id = get_image_id(kwargs={"name": clone_image})
        temp = "DISK=[IMAGE={}, IMAGE_ID={}, CLONE=YES, SIZE={}]".format(
            clone_image, clone_image_id, size
        )
        return temp

    if disk_type == "volatile":
        _require_disk_opts("type")
        v_type = disk_info["type"]
        temp = f"DISK=[TYPE={v_type}, SIZE={size}]"

        if v_type == "fs":
            _require_disk_opts("format")
            format = disk_info["format"]
            temp = f"DISK=[TYPE={v_type}, SIZE={size}, FORMAT={format}]"
        return temp
    # TODO add persistant disk_type


def create(vm_):
    r"""
    Create a single VM from a data dict.

    vm\_
        The dictionary use to create a VM.

    Optional vm\_ dict options for overwriting template:

    region_id
        Optional - OpenNebula Zone ID

    memory
        Optional - In MB

    cpu
        Optional - Percent of host CPU to allocate

    vcpu
        Optional - Amount of vCPUs to allocate

     CLI Example:

     .. code-block:: bash

         salt-cloud -p my-opennebula-profile vm_name

        salt-cloud -p my-opennebula-profile vm_name memory=16384 cpu=2.5 vcpu=16

    """
    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 "opennebula", vm_["profile"]
            )
            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"],
    )

    log.info("Creating Cloud VM %s", vm_["name"])
    kwargs = {
        "name": vm_["name"],
        "template_id": get_template(vm_),
        "region_id": get_location(vm_),
    }
    if "template" in vm_:
        kwargs["image_id"] = get_template_id({"name": vm_["template"]})

    private_networking = config.get_cloud_config_value(
        "private_networking", vm_, __opts__, search_global=False, default=None
    )
    kwargs["private_networking"] = "true" if private_networking else "false"

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

    template = []
    if kwargs.get("region_id"):
        template.append('SCHED_REQUIREMENTS="ID={}"'.format(kwargs.get("region_id")))
    if vm_.get("memory"):
        template.append("MEMORY={}".format(vm_.get("memory")))
    if vm_.get("cpu"):
        template.append("CPU={}".format(vm_.get("cpu")))
    if vm_.get("vcpu"):
        template.append("VCPU={}".format(vm_.get("vcpu")))
    if vm_.get("disk"):
        get_disks = vm_.get("disk")
        template_name = vm_["image"]
        for disk in get_disks:
            template.append(
                _get_device_template(disk, get_disks[disk], template=template_name)
            )
        if "CLONE" not in str(template):
            raise SaltCloudSystemExit(
                "Missing an image disk to clone. Must define a clone disk alongside all"
                " other disk definitions."
            )

    template_args = "\n".join(template)

    try:
        server, user, password = _get_xml_rpc()
        auth = ":".join([user, password])
        cret = server.one.template.instantiate(
            auth, int(kwargs["template_id"]), kwargs["name"], False, template_args
        )
        if not cret[0]:
            log.error(
                "Error creating %s on OpenNebula\n\n"
                "The following error was returned when trying to "
                "instantiate the template: %s",
                vm_["name"],
                cret[1],
                # Show the traceback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG,
            )
            return False
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error creating %s on OpenNebula\n\n"
            "The following exception was thrown when trying to "
            "run the initial deployment: %s",
            vm_["name"],
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return False

    fqdn = vm_.get("fqdn_base")
    if fqdn is not None:
        fqdn = "{}.{}".format(vm_["name"], fqdn)

    def __query_node_data(vm_name):
        node_data = show_instance(vm_name, call="action")
        if not node_data:
            # Trigger an error in the wait_for_ip function
            return False
        if node_data["state"] == "7":
            return False
        if node_data["lcm_state"] == "3":
            return node_data

    try:
        data = __utils__["cloud.wait_for_ip"](
            __query_node_data,
            update_args=(vm_["name"],),
            timeout=config.get_cloud_config_value(
                "wait_for_ip_timeout", vm_, __opts__, default=10 * 60
            ),
            interval=config.get_cloud_config_value(
                "wait_for_ip_interval", vm_, __opts__, default=2
            ),
        )
    except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc:
        try:
            # It might be already up, let's destroy it!
            destroy(vm_["name"])
        except SaltCloudSystemExit:
            pass
        finally:
            raise SaltCloudSystemExit(str(exc))

    key_filename = config.get_cloud_config_value(
        "private_key", vm_, __opts__, search_global=False, default=None
    )
    if key_filename is not None and not os.path.isfile(key_filename):
        raise SaltCloudConfigError(
            f"The defined key_filename '{key_filename}' does not exist"
        )

    if fqdn:
        vm_["ssh_host"] = fqdn
        private_ip = "0.0.0.0"
    else:
        try:
            private_ip = data["private_ips"][0]
        except KeyError:
            try:
                private_ip = data["template"]["nic"]["ip"]
            except KeyError:
                # if IPv6 is used try this as last resort
                # OpenNebula does not yet show ULA address here so take global
                private_ip = data["template"]["nic"]["ip6_global"]

            vm_["ssh_host"] = private_ip

    ssh_username = config.get_cloud_config_value(
        "ssh_username", vm_, __opts__, default="root"
    )

    vm_["username"] = ssh_username
    vm_["key_filename"] = key_filename

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

    ret["id"] = data["id"]
    ret["image"] = vm_["image"]
    ret["name"] = vm_["name"]
    ret["size"] = data["template"]["memory"]
    ret["state"] = data["state"]
    ret["private_ips"] = private_ip
    ret["public_ips"] = []

    log.info("Created Cloud VM '%s'", vm_["name"])
    log.debug("'%s' VM creation details:\n%s", vm_["name"], pprint.pformat(data))

    __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"],
    )

    return ret


def destroy(name, call=None):
    """
    Destroy a node. Will check termination protection and warn if enabled.

    name
        The name of the vm to be destroyed.

    CLI Example:

    .. code-block:: bash

        salt-cloud --destroy vm_name
        salt-cloud -d vm_name
        salt-cloud --action destroy vm_name
        salt-cloud -a destroy vm_name

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

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])

    data = show_instance(name, call="action")
    node = server.one.vm.action(auth, "delete", int(data["id"]))

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

    if __opts__.get("update_cachedir", False) is True:
        __utils__["cloud.delete_minion_cachedir"](
            name, _get_active_provider_name().split(":")[0], __opts__
        )

    data = {
        "action": "vm.delete",
        "deleted": node[0],
        "node_id": node[1],
        "error_code": node[2],
    }

    return data


def image_allocate(call=None, kwargs=None):
    """
    Allocates a new image in OpenNebula.

    .. versionadded:: 2016.3.0

    path
        The path to a file containing the template of the image to allocate.
        Syntax within the file can be the usual attribute=value or XML. Can be
        used instead of ``data``.

    data
        The data containing the template of the image to allocate. Syntax can be the
        usual attribute=value or XML. Can be used instead of ``path``.

    datastore_id
        The ID of the data-store to be used for the new image. Can be used instead
        of ``datastore_name``.

    datastore_name
        The name of the data-store to be used for the new image. Can be used instead of
        ``datastore_id``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_allocate opennebula path=/path/to/image_file.txt datastore_id=1
        salt-cloud -f image_allocate opennebula datastore_name=default \\
            data='NAME="Ubuntu 14.04" PATH="/home/one_user/images/ubuntu_desktop.img" \\
            DESCRIPTION="Ubuntu 14.04 for development."'
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_allocate function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    path = kwargs.get("path", None)
    data = kwargs.get("data", None)
    datastore_id = kwargs.get("datastore_id", None)
    datastore_name = kwargs.get("datastore_name", None)

    if datastore_id:
        if datastore_name:
            log.warning(
                "Both a 'datastore_id' and a 'datastore_name' were provided. "
                "'datastore_id' will take precedence."
            )
    elif datastore_name:
        datastore_id = get_datastore_id(kwargs={"name": datastore_name})
    else:
        raise SaltCloudSystemExit(
            "The image_allocate function requires either a 'datastore_id' or a "
            "'datastore_name' to be provided."
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The image_allocate function requires either a file 'path' or 'data' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.image.allocate(auth, data, int(datastore_id))

    ret = {
        "action": "image.allocate",
        "allocated": response[0],
        "image_id": response[1],
        "error_code": response[2],
    }

    return ret


def image_clone(call=None, kwargs=None):
    """
    Clones an existing image.

    .. versionadded:: 2016.3.0

    name
        The name of the new image.

    image_id
        The ID of the image to be cloned. Can be used instead of ``image_name``.

    image_name
        The name of the image to be cloned. Can be used instead of ``image_id``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_clone opennebula name=my-new-image image_id=10
        salt-cloud -f image_clone opennebula name=my-new-image image_name=my-image-to-clone
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_clone function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    image_id = kwargs.get("image_id", None)
    image_name = kwargs.get("image_name", None)

    if name is None:
        raise SaltCloudSystemExit(
            "The image_clone function requires a 'name' to be provided."
        )

    if image_id:
        if image_name:
            log.warning(
                "Both the 'image_id' and 'image_name' arguments were provided. "
                "'image_id' will take precedence."
            )
    elif image_name:
        image_id = get_image_id(kwargs={"name": image_name})
    else:
        raise SaltCloudSystemExit(
            "The image_clone function requires either an 'image_id' or an "
            "'image_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.image.clone(auth, int(image_id), name)

    data = {
        "action": "image.clone",
        "cloned": response[0],
        "cloned_image_id": response[1],
        "cloned_image_name": name,
        "error_code": response[2],
    }

    return data


def image_delete(call=None, kwargs=None):
    """
    Deletes the given image from OpenNebula. Either a name or an image_id must
    be supplied.

    .. versionadded:: 2016.3.0

    name
        The name of the image to delete. Can be used instead of ``image_id``.

    image_id
        The ID of the image to delete. Can be used instead of ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_delete opennebula name=my-image
        salt-cloud --function image_delete opennebula image_id=100
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_delete function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    image_id = kwargs.get("image_id", None)

    if image_id:
        if name:
            log.warning(
                "Both the 'image_id' and 'name' arguments were provided. "
                "'image_id' will take precedence."
            )
    elif name:
        image_id = get_image_id(kwargs={"name": name})
    else:
        raise SaltCloudSystemExit(
            "The image_delete function requires either an 'image_id' or a "
            "'name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.image.delete(auth, int(image_id))

    data = {
        "action": "image.delete",
        "deleted": response[0],
        "image_id": response[1],
        "error_code": response[2],
    }

    return data


def image_info(call=None, kwargs=None):
    """
    Retrieves information for a given image. Either a name or an image_id must be
    supplied.

    .. versionadded:: 2016.3.0

    name
        The name of the image for which to gather information. Can be used instead
        of ``image_id``.

    image_id
        The ID of the image for which to gather information. Can be used instead of
        ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_info opennebula name=my-image
        salt-cloud --function image_info opennebula image_id=5
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_info function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    image_id = kwargs.get("image_id", None)

    if image_id:
        if name:
            log.warning(
                "Both the 'image_id' and 'name' arguments were provided. "
                "'image_id' will take precedence."
            )
    elif name:
        image_id = get_image_id(kwargs={"name": name})
    else:
        raise SaltCloudSystemExit(
            "The image_info function requires either a 'name or an 'image_id' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])

    info = {}
    response = server.one.image.info(auth, int(image_id))[1]
    tree = _get_xml(response)
    info[tree.find("NAME").text] = _xml_to_dict(tree)

    return info


def image_persistent(call=None, kwargs=None):
    """
    Sets the Image as persistent or not persistent.

    .. versionadded:: 2016.3.0

    name
        The name of the image to set. Can be used instead of ``image_id``.

    image_id
        The ID of the image to set. Can be used instead of ``name``.

    persist
        A boolean value to set the image as persistent or not. Set to true
        for persistent, false for non-persistent.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_persistent opennebula name=my-image persist=True
        salt-cloud --function image_persistent opennebula image_id=5 persist=False
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_persistent function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    persist = kwargs.get("persist", None)
    image_id = kwargs.get("image_id", None)

    if persist is None:
        raise SaltCloudSystemExit(
            "The image_persistent function requires 'persist' to be set to 'True' "
            "or 'False'."
        )

    if image_id:
        if name:
            log.warning(
                "Both the 'image_id' and 'name' arguments were provided. "
                "'image_id' will take precedence."
            )
    elif name:
        image_id = get_image_id(kwargs={"name": name})
    else:
        raise SaltCloudSystemExit(
            "The image_persistent function requires either a 'name' or an "
            "'image_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.image.persistent(
        auth, int(image_id), salt.utils.data.is_true(persist)
    )

    data = {
        "action": "image.persistent",
        "response": response[0],
        "image_id": response[1],
        "error_code": response[2],
    }

    return data


def image_snapshot_delete(call=None, kwargs=None):
    """
    Deletes a snapshot from the image.

    .. versionadded:: 2016.3.0

    image_id
        The ID of the image from which to delete the snapshot. Can be used instead of
        ``image_name``.

    image_name
        The name of the image from which to delete the snapshot. Can be used instead
        of ``image_id``.

    snapshot_id
        The ID of the snapshot to delete.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_snapshot_delete vm_id=106 snapshot_id=45
        salt-cloud -f image_snapshot_delete vm_name=my-vm snapshot_id=111
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_snapshot_delete function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    image_id = kwargs.get("image_id", None)
    image_name = kwargs.get("image_name", None)
    snapshot_id = kwargs.get("snapshot_id", None)

    if snapshot_id is None:
        raise SaltCloudSystemExit(
            "The image_snapshot_delete function requires a 'snapshot_id' to be"
            " provided."
        )

    if image_id:
        if image_name:
            log.warning(
                "Both the 'image_id' and 'image_name' arguments were provided. "
                "'image_id' will take precedence."
            )
    elif image_name:
        image_id = get_image_id(kwargs={"name": image_name})
    else:
        raise SaltCloudSystemExit(
            "The image_snapshot_delete function requires either an 'image_id' "
            "or a 'image_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.image.snapshotdelete(auth, int(image_id), int(snapshot_id))

    data = {
        "action": "image.snapshotdelete",
        "deleted": response[0],
        "snapshot_id": response[1],
        "error_code": response[2],
    }

    return data


def image_snapshot_revert(call=None, kwargs=None):
    """
    Reverts an image state to a previous snapshot.

    .. versionadded:: 2016.3.0

    image_id
        The ID of the image to revert. Can be used instead of ``image_name``.

    image_name
        The name of the image to revert. Can be used instead of ``image_id``.

    snapshot_id
        The ID of the snapshot to which the image will be reverted.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_snapshot_revert vm_id=106 snapshot_id=45
        salt-cloud -f image_snapshot_revert vm_name=my-vm snapshot_id=120
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_snapshot_revert function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    image_id = kwargs.get("image_id", None)
    image_name = kwargs.get("image_name", None)
    snapshot_id = kwargs.get("snapshot_id", None)

    if snapshot_id is None:
        raise SaltCloudSystemExit(
            "The image_snapshot_revert function requires a 'snapshot_id' to be"
            " provided."
        )

    if image_id:
        if image_name:
            log.warning(
                "Both the 'image_id' and 'image_name' arguments were provided. "
                "'image_id' will take precedence."
            )
    elif image_name:
        image_id = get_image_id(kwargs={"name": image_name})
    else:
        raise SaltCloudSystemExit(
            "The image_snapshot_revert function requires either an 'image_id' or "
            "an 'image_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.image.snapshotrevert(auth, int(image_id), int(snapshot_id))

    data = {
        "action": "image.snapshotrevert",
        "reverted": response[0],
        "snapshot_id": response[1],
        "error_code": response[2],
    }

    return data


def image_snapshot_flatten(call=None, kwargs=None):
    """
    Flattens the snapshot of an image and discards others.

    .. versionadded:: 2016.3.0

    image_id
        The ID of the image. Can be used instead of ``image_name``.

    image_name
        The name of the image. Can be used instead of ``image_id``.

    snapshot_id
        The ID of the snapshot to flatten.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_snapshot_flatten vm_id=106 snapshot_id=45
        salt-cloud -f image_snapshot_flatten vm_name=my-vm snapshot_id=45
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_snapshot_flatten function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    image_id = kwargs.get("image_id", None)
    image_name = kwargs.get("image_name", None)
    snapshot_id = kwargs.get("snapshot_id", None)

    if snapshot_id is None:
        raise SaltCloudSystemExit(
            "The image_stanpshot_flatten function requires a 'snapshot_id' "
            "to be provided."
        )

    if image_id:
        if image_name:
            log.warning(
                "Both the 'image_id' and 'image_name' arguments were provided. "
                "'image_id' will take precedence."
            )
    elif image_name:
        image_id = get_image_id(kwargs={"name": image_name})
    else:
        raise SaltCloudSystemExit(
            "The image_snapshot_flatten function requires either an "
            "'image_id' or an 'image_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.image.snapshotflatten(auth, int(image_id), int(snapshot_id))

    data = {
        "action": "image.snapshotflatten",
        "flattened": response[0],
        "snapshot_id": response[1],
        "error_code": response[2],
    }

    return data


def image_update(call=None, kwargs=None):
    """
    Replaces the image template contents.

    .. versionadded:: 2016.3.0

    image_id
        The ID of the image to update. Can be used instead of ``image_name``.

    image_name
        The name of the image to update. Can be used instead of ``image_id``.

    path
        The path to a file containing the template of the image. Syntax within the
        file can be the usual attribute=value or XML. Can be used instead of ``data``.

    data
        Contains the template of the image. Syntax can be the usual attribute=value
        or XML. Can be used instead of ``path``.

    update_type
        There are two ways to update an image: ``replace`` the whole template
        or ``merge`` the new template with the existing one.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f image_update opennebula image_id=0 file=/path/to/image_update_file.txt update_type=replace
        salt-cloud -f image_update opennebula image_name="Ubuntu 14.04" update_type=merge \\
            data='NAME="Ubuntu Dev" PATH="/home/one_user/images/ubuntu_desktop.img" \\
            DESCRIPTION = "Ubuntu 14.04 for development."'
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The image_allocate function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    image_id = kwargs.get("image_id", None)
    image_name = kwargs.get("image_name", None)
    path = kwargs.get("path", None)
    data = kwargs.get("data", None)
    update_type = kwargs.get("update_type", None)
    update_args = ["replace", "merge"]

    if update_type is None:
        raise SaltCloudSystemExit(
            "The image_update function requires an 'update_type' to be provided."
        )

    if update_type == update_args[0]:
        update_number = 0
    elif update_type == update_args[1]:
        update_number = 1
    else:
        raise SaltCloudSystemExit(
            "The update_type argument must be either {} or {}.".format(
                update_args[0], update_args[1]
            )
        )

    if image_id:
        if image_name:
            log.warning(
                "Both the 'image_id' and 'image_name' arguments were provided. "
                "'image_id' will take precedence."
            )
    elif image_name:
        image_id = get_image_id(kwargs={"name": image_name})
    else:
        raise SaltCloudSystemExit(
            "The image_update function requires either an 'image_id' or an "
            "'image_name' to be provided."
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The image_update function requires either 'data' or a file 'path' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.image.update(auth, int(image_id), data, int(update_number))

    ret = {
        "action": "image.update",
        "updated": response[0],
        "image_id": response[1],
        "error_code": response[2],
    }

    return ret


def show_instance(name, call=None):
    """
    Show the details from OpenNebula concerning a named VM.

    name
        The name of the VM for which to display details.

    call
        Type of call to use with this function such as ``function``.

    CLI Example:

    .. code-block:: bash

        salt-cloud --action show_instance vm_name
        salt-cloud -a show_instance vm_name

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

    node = _get_node(name)
    __utils__["cloud.cache_node"](node, _get_active_provider_name(), __opts__)

    return node


def secgroup_allocate(call=None, kwargs=None):
    """
    Allocates a new security group in OpenNebula.

    .. versionadded:: 2016.3.0

    path
        The path to a file containing the template of the security group. Syntax
        within the file can be the usual attribute=value or XML. Can be used
        instead of ``data``.

    data
        The template data of the security group. Syntax can be the usual
        attribute=value or XML. Can be used instead of ``path``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f secgroup_allocate opennebula path=/path/to/secgroup_file.txt
        salt-cloud -f secgroup_allocate opennebula \\
            data="NAME = test RULE = [PROTOCOL = TCP, RULE_TYPE = inbound, \\
            RANGE = 1000:2000]"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The secgroup_allocate function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The secgroup_allocate function requires either 'data' or a file "
            "'path' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.secgroup.allocate(auth, data)

    ret = {
        "action": "secgroup.allocate",
        "allocated": response[0],
        "secgroup_id": response[1],
        "error_code": response[2],
    }

    return ret


def secgroup_clone(call=None, kwargs=None):
    """
    Clones an existing security group.

    .. versionadded:: 2016.3.0

    name
        The name of the new template.

    secgroup_id
        The ID of the security group to be cloned. Can be used instead of
        ``secgroup_name``.

    secgroup_name
        The name of the security group to be cloned. Can be used instead of
        ``secgroup_id``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f secgroup_clone opennebula name=my-cloned-secgroup secgroup_id=0
        salt-cloud -f secgroup_clone opennebula name=my-cloned-secgroup secgroup_name=my-secgroup
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The secgroup_clone function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    secgroup_id = kwargs.get("secgroup_id", None)
    secgroup_name = kwargs.get("secgroup_name", None)

    if name is None:
        raise SaltCloudSystemExit(
            "The secgroup_clone function requires a 'name' to be provided."
        )

    if secgroup_id:
        if secgroup_name:
            log.warning(
                "Both the 'secgroup_id' and 'secgroup_name' arguments were provided. "
                "'secgroup_id' will take precedence."
            )
    elif secgroup_name:
        secgroup_id = get_secgroup_id(kwargs={"name": secgroup_name})
    else:
        raise SaltCloudSystemExit(
            "The secgroup_clone function requires either a 'secgroup_id' or a "
            "'secgroup_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.secgroup.clone(auth, int(secgroup_id), name)

    data = {
        "action": "secgroup.clone",
        "cloned": response[0],
        "cloned_secgroup_id": response[1],
        "cloned_secgroup_name": name,
        "error_code": response[2],
    }

    return data


def secgroup_delete(call=None, kwargs=None):
    """
    Deletes the given security group from OpenNebula. Either a name or a secgroup_id
    must be supplied.

    .. versionadded:: 2016.3.0

    name
        The name of the security group to delete. Can be used instead of
        ``secgroup_id``.

    secgroup_id
        The ID of the security group to delete. Can be used instead of ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f secgroup_delete opennebula name=my-secgroup
        salt-cloud --function secgroup_delete opennebula secgroup_id=100
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The secgroup_delete function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    secgroup_id = kwargs.get("secgroup_id", None)

    if secgroup_id:
        if name:
            log.warning(
                "Both the 'secgroup_id' and 'name' arguments were provided. "
                "'secgroup_id' will take precedence."
            )
    elif name:
        secgroup_id = get_secgroup_id(kwargs={"name": name})
    else:
        raise SaltCloudSystemExit(
            "The secgroup_delete function requires either a 'name' or a "
            "'secgroup_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.secgroup.delete(auth, int(secgroup_id))

    data = {
        "action": "secgroup.delete",
        "deleted": response[0],
        "secgroup_id": response[1],
        "error_code": response[2],
    }

    return data


def secgroup_info(call=None, kwargs=None):
    """
    Retrieves information for the given security group. Either a name or a
    secgroup_id must be supplied.

    .. versionadded:: 2016.3.0

    name
        The name of the security group for which to gather information. Can be
        used instead of ``secgroup_id``.

    secgroup_id
        The ID of the security group for which to gather information. Can be
        used instead of ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f secgroup_info opennebula name=my-secgroup
        salt-cloud --function secgroup_info opennebula secgroup_id=5
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The secgroup_info function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    secgroup_id = kwargs.get("secgroup_id", None)

    if secgroup_id:
        if name:
            log.warning(
                "Both the 'secgroup_id' and 'name' arguments were provided. "
                "'secgroup_id' will take precedence."
            )
    elif name:
        secgroup_id = get_secgroup_id(kwargs={"name": name})
    else:
        raise SaltCloudSystemExit(
            "The secgroup_info function requires either a name or a secgroup_id "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])

    info = {}
    response = server.one.secgroup.info(auth, int(secgroup_id))[1]
    tree = _get_xml(response)
    info[tree.find("NAME").text] = _xml_to_dict(tree)

    return info


def secgroup_update(call=None, kwargs=None):
    """
    Replaces the security group template contents.

    .. versionadded:: 2016.3.0

    secgroup_id
        The ID of the security group to update. Can be used instead of
        ``secgroup_name``.

    secgroup_name
        The name of the security group to update. Can be used instead of
        ``secgroup_id``.

    path
        The path to a file containing the template of the security group. Syntax
        within the file can be the usual attribute=value or XML. Can be used instead
        of ``data``.

    data
        The template data of the security group. Syntax can be the usual attribute=value
        or XML. Can be used instead of ``path``.

    update_type
        There are two ways to update a security group: ``replace`` the whole template
        or ``merge`` the new template with the existing one.

    CLI Example:

    .. code-block:: bash

        salt-cloud --function secgroup_update opennebula secgroup_id=100 \\
            path=/path/to/secgroup_update_file.txt \\
            update_type=replace
        salt-cloud -f secgroup_update opennebula secgroup_name=my-secgroup update_type=merge \\
            data="Name = test RULE = [PROTOCOL = TCP, RULE_TYPE = inbound, RANGE = 1000:2000]"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The secgroup_allocate function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    secgroup_id = kwargs.get("secgroup_id", None)
    secgroup_name = kwargs.get("secgroup_name", None)
    path = kwargs.get("path", None)
    data = kwargs.get("data", None)
    update_type = kwargs.get("update_type", None)
    update_args = ["replace", "merge"]

    if update_type is None:
        raise SaltCloudSystemExit(
            "The secgroup_update function requires an 'update_type' to be provided."
        )

    if update_type == update_args[0]:
        update_number = 0
    elif update_type == update_args[1]:
        update_number = 1
    else:
        raise SaltCloudSystemExit(
            "The update_type argument must be either {} or {}.".format(
                update_args[0], update_args[1]
            )
        )

    if secgroup_id:
        if secgroup_name:
            log.warning(
                "Both the 'secgroup_id' and 'secgroup_name' arguments were provided. "
                "'secgroup_id' will take precedence."
            )
    elif secgroup_name:
        secgroup_id = get_secgroup_id(kwargs={"name": secgroup_name})
    else:
        raise SaltCloudSystemExit(
            "The secgroup_update function requires either a 'secgroup_id' or a "
            "'secgroup_name' to be provided."
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The secgroup_update function requires either 'data' or a file 'path' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.secgroup.update(
        auth, int(secgroup_id), data, int(update_number)
    )

    ret = {
        "action": "secgroup.update",
        "updated": response[0],
        "secgroup_id": response[1],
        "error_code": response[2],
    }

    return ret


def template_allocate(call=None, kwargs=None):
    """
    Allocates a new template in OpenNebula.

    .. versionadded:: 2016.3.0

    path
        The path to a file containing the elements of the template to be allocated.
        Syntax within the file can be the usual attribute=value or XML. Can be used
        instead of ``data``.

    data
        Contains the elements of the template to be allocated. Syntax can be the usual
        attribute=value or XML. Can be used instead of ``path``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f template_allocate opennebula path=/path/to/template_file.txt
        salt-cloud -f template_allocate opennebula \\
            data='CPU="1.0" DISK=[IMAGE="Ubuntu-14.04"] GRAPHICS=[LISTEN="0.0.0.0",TYPE="vnc"] \\
            MEMORY="1024" NETWORK="yes" NIC=[NETWORK="192net",NETWORK_UNAME="oneadmin"] \\
            OS=[ARCH="x86_64"] SUNSTONE_CAPACITY_SELECT="YES" SUNSTONE_NETWORK_SELECT="YES" \\
            VCPU="1"'
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The template_allocate function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The template_allocate function requires either 'data' or a file "
            "'path' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.template.allocate(auth, data)

    ret = {
        "action": "template.allocate",
        "allocated": response[0],
        "template_id": response[1],
        "error_code": response[2],
    }

    return ret


def template_clone(call=None, kwargs=None):
    """
    Clones an existing virtual machine template.

    .. versionadded:: 2016.3.0

    name
        The name of the new template.

    template_id
        The ID of the template to be cloned. Can be used instead of ``template_name``.

    template_name
        The name of the template to be cloned. Can be used instead of ``template_id``.

    clone_images
        Optional, defaults to False. Indicates if the images attached to the template should be cloned as well.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f template_clone opennebula name=my-new-template template_id=0
        salt-cloud -f template_clone opennebula name=my-new-template template_name=my-template
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The template_clone function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    template_id = kwargs.get("template_id", None)
    template_name = kwargs.get("template_name", None)
    clone_images = kwargs.get("clone_images", False)

    if name is None:
        raise SaltCloudSystemExit(
            "The template_clone function requires a name to be provided."
        )

    if template_id:
        if template_name:
            log.warning(
                "Both the 'template_id' and 'template_name' arguments were provided. "
                "'template_id' will take precedence."
            )
    elif template_name:
        template_id = get_template_id(kwargs={"name": template_name})
    else:
        raise SaltCloudSystemExit(
            "The template_clone function requires either a 'template_id' "
            "or a 'template_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])

    response = server.one.template.clone(auth, int(template_id), name, clone_images)

    data = {
        "action": "template.clone",
        "cloned": response[0],
        "cloned_template_id": response[1],
        "cloned_template_name": name,
        "error_code": response[2],
    }

    return data


def template_delete(call=None, kwargs=None):
    """
    Deletes the given template from OpenNebula. Either a name or a template_id must
    be supplied.

    .. versionadded:: 2016.3.0

    name
        The name of the template to delete. Can be used instead of ``template_id``.

    template_id
        The ID of the template to delete. Can be used instead of ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f template_delete opennebula name=my-template
        salt-cloud --function template_delete opennebula template_id=5
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The template_delete function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    template_id = kwargs.get("template_id", None)

    if template_id:
        if name:
            log.warning(
                "Both the 'template_id' and 'name' arguments were provided. "
                "'template_id' will take precedence."
            )
    elif name:
        template_id = get_template_id(kwargs={"name": name})
    else:
        raise SaltCloudSystemExit(
            "The template_delete function requires either a 'name' or a 'template_id' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.template.delete(auth, int(template_id))

    data = {
        "action": "template.delete",
        "deleted": response[0],
        "template_id": response[1],
        "error_code": response[2],
    }

    return data


def template_instantiate(call=None, kwargs=None):
    """
    Instantiates a new virtual machine from a template.

    .. versionadded:: 2016.3.0

    .. note::
        ``template_instantiate`` creates a VM on OpenNebula from a template, but it
        does not install Salt on the new VM. Use the ``create`` function for that
        functionality: ``salt-cloud -p opennebula-profile vm-name``.

    vm_name
        Name for the new VM instance.

    template_id
        The ID of the template from which the VM will be created. Can be used instead
        of ``template_name``.

    template_name
        The name of the template from which the VM will be created. Can be used instead
        of ``template_id``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f template_instantiate opennebula vm_name=my-new-vm template_id=0

    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The template_instantiate function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    vm_name = kwargs.get("vm_name", None)
    template_id = kwargs.get("template_id", None)
    template_name = kwargs.get("template_name", None)

    if vm_name is None:
        raise SaltCloudSystemExit(
            "The template_instantiate function requires a 'vm_name' to be provided."
        )

    if template_id:
        if template_name:
            log.warning(
                "Both the 'template_id' and 'template_name' arguments were provided. "
                "'template_id' will take precedence."
            )
    elif template_name:
        template_id = get_template_id(kwargs={"name": template_name})
    else:
        raise SaltCloudSystemExit(
            "The template_instantiate function requires either a 'template_id' "
            "or a 'template_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.template.instantiate(auth, int(template_id), vm_name)

    data = {
        "action": "template.instantiate",
        "instantiated": response[0],
        "instantiated_vm_id": response[1],
        "vm_name": vm_name,
        "error_code": response[2],
    }

    return data


def template_update(call=None, kwargs=None):
    """
    Replaces the template contents.

    .. versionadded:: 2016.3.0

    template_id
        The ID of the template to update. Can be used instead of ``template_name``.

    template_name
        The name of the template to update. Can be used instead of ``template_id``.

    path
        The path to a file containing the elements of the template to be updated.
        Syntax within the file can be the usual attribute=value or XML. Can be
        used instead of ``data``.

    data
        Contains the elements of the template to be updated. Syntax can be the
        usual attribute=value or XML. Can be used instead of ``path``.

    update_type
        There are two ways to update a template: ``replace`` the whole template
        or ``merge`` the new template with the existing one.

    CLI Example:

    .. code-block:: bash

        salt-cloud --function template_update opennebula template_id=1 update_type=replace \\
            path=/path/to/template_update_file.txt
        salt-cloud -f template_update opennebula template_name=my-template update_type=merge \\
            data='CPU="1.0" DISK=[IMAGE="Ubuntu-14.04"] GRAPHICS=[LISTEN="0.0.0.0",TYPE="vnc"] \\
            MEMORY="1024" NETWORK="yes" NIC=[NETWORK="192net",NETWORK_UNAME="oneadmin"] \\
            OS=[ARCH="x86_64"] SUNSTONE_CAPACITY_SELECT="YES" SUNSTONE_NETWORK_SELECT="YES" \\
            VCPU="1"'
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The template_update function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    template_id = kwargs.get("template_id", None)
    template_name = kwargs.get("template_name", None)
    path = kwargs.get("path", None)
    data = kwargs.get("data", None)
    update_type = kwargs.get("update_type", None)
    update_args = ["replace", "merge"]

    if update_type is None:
        raise SaltCloudSystemExit(
            "The template_update function requires an 'update_type' to be provided."
        )

    if update_type == update_args[0]:
        update_number = 0
    elif update_type == update_args[1]:
        update_number = 1
    else:
        raise SaltCloudSystemExit(
            "The update_type argument must be either {} or {}.".format(
                update_args[0], update_args[1]
            )
        )

    if template_id:
        if template_name:
            log.warning(
                "Both the 'template_id' and 'template_name' arguments were provided. "
                "'template_id' will take precedence."
            )
    elif template_name:
        template_id = get_template_id(kwargs={"name": template_name})
    else:
        raise SaltCloudSystemExit(
            "The template_update function requires either a 'template_id' "
            "or a 'template_name' to be provided."
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The template_update function requires either 'data' or a file "
            "'path' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.template.update(
        auth, int(template_id), data, int(update_number)
    )

    ret = {
        "action": "template.update",
        "updated": response[0],
        "template_id": response[1],
        "error_code": response[2],
    }

    return ret


def vm_action(name, kwargs=None, call=None):
    """
    Submits an action to be performed on a given virtual machine.

    .. versionadded:: 2016.3.0

    name
        The name of the VM to action.

    action
        The action to be performed on the VM. Available options include:
          - boot
          - delete
          - delete-recreate
          - hold
          - poweroff
          - poweroff-hard
          - reboot
          - reboot-hard
          - release
          - resched
          - resume
          - shutdown
          - shutdown-hard
          - stop
          - suspend
          - undeploy
          - undeploy-hard
          - unresched

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_action my-vm action='release'
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_action function must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    action = kwargs.get("action", None)
    if action is None:
        raise SaltCloudSystemExit(
            "The vm_action function must have an 'action' provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.action(auth, action, vm_id)

    data = {
        "action": "vm.action." + str(action),
        "actioned": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_allocate(call=None, kwargs=None):
    """
    Allocates a new virtual machine in OpenNebula.

    .. versionadded:: 2016.3.0

    path
        The path to a file defining the template of the VM to allocate.
        Syntax within the file can be the usual attribute=value or XML.
        Can be used instead of ``data``.

    data
        Contains the template definitions of the VM to allocate. Syntax can
        be the usual attribute=value or XML. Can be used instead of ``path``.

    hold
        If this parameter is set to ``True``, the VM will be created in
        the ``HOLD`` state. If not set, the VM is created in the ``PENDING``
        state. Default is ``False``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vm_allocate path=/path/to/vm_template.txt
        salt-cloud --function vm_allocate path=/path/to/vm_template.txt hold=True
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vm_allocate function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    path = kwargs.get("path", None)
    data = kwargs.get("data", None)
    hold = kwargs.get("hold", False)

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vm_allocate function requires either 'data' or a file 'path' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vm.allocate(auth, data, salt.utils.data.is_true(hold))

    ret = {
        "action": "vm.allocate",
        "allocated": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return ret


def vm_attach(name, kwargs=None, call=None):
    """
    Attaches a new disk to the given virtual machine.

    .. versionadded:: 2016.3.0

    name
        The name of the VM for which to attach the new disk.

    path
        The path to a file containing a single disk vector attribute.
        Syntax within the file can be the usual attribute=value or XML.
        Can be used instead of ``data``.

    data
        Contains the data needed to attach a single disk vector attribute.
        Syntax can be the usual attribute=value or XML. Can be used instead
        of ``path``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_attach my-vm path=/path/to/disk_file.txt
        salt-cloud -a vm_attach my-vm data="DISK=[DISK_ID=1]"
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_attach action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vm_attach function requires either 'data' or a file "
            "'path' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.attach(auth, vm_id, data)

    ret = {
        "action": "vm.attach",
        "attached": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return ret


def vm_attach_nic(name, kwargs=None, call=None):
    """
    Attaches a new network interface to the given virtual machine.

    .. versionadded:: 2016.3.0

    name
        The name of the VM for which to attach the new network interface.

    path
        The path to a file containing a single NIC vector attribute.
        Syntax within the file can be the usual attribute=value or XML. Can
        be used instead of ``data``.

    data
        Contains the single NIC vector attribute to attach to the VM.
        Syntax can be the usual attribute=value or XML. Can be used instead
        of ``path``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_attach_nic my-vm path=/path/to/nic_file.txt
        salt-cloud -a vm_attach_nic my-vm data="NIC=[NETWORK_ID=1]"
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_attach_nic action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vm_attach_nic function requires either 'data' or a file "
            "'path' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.attachnic(auth, vm_id, data)

    ret = {
        "action": "vm.attachnic",
        "nic_attached": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return ret


def vm_deploy(name, kwargs=None, call=None):
    """
    Initiates the instance of the given VM on the target host.

    .. versionadded:: 2016.3.0

    name
        The name of the VM to deploy.

    host_id
        The ID of the target host where the VM will be deployed. Can be used instead
        of ``host_name``.

    host_name
        The name of the target host where the VM will be deployed. Can be used instead
        of ``host_id``.

    capacity_maintained
        True to enforce the Host capacity is not over-committed. This parameter is only
        acknowledged for users in the ``oneadmin`` group. Host capacity will be always
        enforced for regular users.

    datastore_id
        The ID of the target system data-store where the VM will be deployed. Optional
        and can be used instead of ``datastore_name``. If neither ``datastore_id`` nor
        ``datastore_name`` are set, OpenNebula will choose the data-store.

    datastore_name
        The name of the target system data-store where the VM will be deployed. Optional,
        and can be used instead of ``datastore_id``. If neither ``datastore_id`` nor
        ``datastore_name`` are set, OpenNebula will choose the data-store.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_deploy my-vm host_id=0
        salt-cloud -a vm_deploy my-vm host_id=1 capacity_maintained=False
        salt-cloud -a vm_deploy my-vm host_name=host01 datastore_id=1
        salt-cloud -a vm_deploy my-vm host_name=host01 datastore_name=default
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_deploy action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    host_id = kwargs.get("host_id", None)
    host_name = kwargs.get("host_name", None)
    capacity_maintained = kwargs.get("capacity_maintained", True)
    datastore_id = kwargs.get("datastore_id", None)
    datastore_name = kwargs.get("datastore_name", None)

    if host_id:
        if host_name:
            log.warning(
                "Both the 'host_id' and 'host_name' arguments were provided. "
                "'host_id' will take precedence."
            )
    elif host_name:
        host_id = get_host_id(kwargs={"name": host_name})
    else:
        raise SaltCloudSystemExit(
            "The vm_deploy function requires a 'host_id' or a 'host_name' "
            "to be provided."
        )

    if datastore_id:
        if datastore_name:
            log.warning(
                "Both the 'datastore_id' and 'datastore_name' arguments were provided. "
                "'datastore_id' will take precedence."
            )
    elif datastore_name:
        datastore_id = get_datastore_id(kwargs={"name": datastore_name})
    else:
        datastore_id = "-1"

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = get_vm_id(kwargs={"name": name})
    response = server.one.vm.deploy(
        auth,
        int(vm_id),
        int(host_id),
        salt.utils.data.is_true(capacity_maintained),
        int(datastore_id),
    )

    data = {
        "action": "vm.deploy",
        "deployed": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_detach(name, kwargs=None, call=None):
    """
    Detaches a disk from a virtual machine.

    .. versionadded:: 2016.3.0

    name
        The name of the VM from which to detach the disk.

    disk_id
        The ID of the disk to detach.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_detach my-vm disk_id=1
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_detach action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    disk_id = kwargs.get("disk_id", None)
    if disk_id is None:
        raise SaltCloudSystemExit(
            "The vm_detach function requires a 'disk_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.detach(auth, vm_id, int(disk_id))

    data = {
        "action": "vm.detach",
        "detached": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_detach_nic(name, kwargs=None, call=None):
    """
    Detaches a disk from a virtual machine.

    .. versionadded:: 2016.3.0

    name
        The name of the VM from which to detach the network interface.

    nic_id
        The ID of the nic to detach.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_detach_nic my-vm nic_id=1
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_detach_nic action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    nic_id = kwargs.get("nic_id", None)
    if nic_id is None:
        raise SaltCloudSystemExit(
            "The vm_detach_nic function requires a 'nic_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.detachnic(auth, vm_id, int(nic_id))

    data = {
        "action": "vm.detachnic",
        "nic_detached": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_disk_save(name, kwargs=None, call=None):
    """
    Sets the disk to be saved in the given image.

    .. versionadded:: 2016.3.0

    name
        The name of the VM containing the disk to save.

    disk_id
        The ID of the disk to save.

    image_name
        The name of the new image where the disk will be saved.

    image_type
        The type for the new image. If not set, then the default ``ONED`` Configuration
        will be used. Other valid types include: OS, CDROM, DATABLOCK, KERNEL, RAMDISK,
        and CONTEXT.

    snapshot_id
        The ID of the snapshot to export. If not set, the current image state will be
        used.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_disk_save my-vm disk_id=1 image_name=my-new-image
        salt-cloud -a vm_disk_save my-vm disk_id=1 image_name=my-new-image image_type=CONTEXT snapshot_id=10
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_disk_save action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    disk_id = kwargs.get("disk_id", None)
    image_name = kwargs.get("image_name", None)
    image_type = kwargs.get("image_type", "")
    snapshot_id = int(kwargs.get("snapshot_id", "-1"))

    if disk_id is None or image_name is None:
        raise SaltCloudSystemExit(
            "The vm_disk_save function requires a 'disk_id' and an 'image_name' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.disksave(
        auth, vm_id, int(disk_id), image_name, image_type, snapshot_id
    )

    data = {
        "action": "vm.disksave",
        "saved": response[0],
        "image_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_disk_snapshot_create(name, kwargs=None, call=None):
    """
    Takes a new snapshot of the disk image.

    .. versionadded:: 2016.3.0

    name
        The name of the VM of which to take the snapshot.

    disk_id
        The ID of the disk to save.

    description
        The description for the snapshot.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_disk_snapshot_create my-vm disk_id=0 description="My Snapshot Description"
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_disk_snapshot_create action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    disk_id = kwargs.get("disk_id", None)
    description = kwargs.get("description", None)

    if disk_id is None or description is None:
        raise SaltCloudSystemExit(
            "The vm_disk_snapshot_create function requires a 'disk_id' and a"
            " 'description' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.disksnapshotcreate(auth, vm_id, int(disk_id), description)

    data = {
        "action": "vm.disksnapshotcreate",
        "created": response[0],
        "snapshot_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_disk_snapshot_delete(name, kwargs=None, call=None):
    """
    Deletes a disk snapshot based on the given VM and the disk_id.

    .. versionadded:: 2016.3.0

    name
        The name of the VM containing the snapshot to delete.

    disk_id
        The ID of the disk to save.

    snapshot_id
        The ID of the snapshot to be deleted.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_disk_snapshot_delete my-vm disk_id=0 snapshot_id=6
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_disk_snapshot_delete action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    disk_id = kwargs.get("disk_id", None)
    snapshot_id = kwargs.get("snapshot_id", None)

    if disk_id is None or snapshot_id is None:
        raise SaltCloudSystemExit(
            "The vm_disk_snapshot_create function requires a 'disk_id' and a"
            " 'snapshot_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.disksnapshotdelete(
        auth, vm_id, int(disk_id), int(snapshot_id)
    )

    data = {
        "action": "vm.disksnapshotdelete",
        "deleted": response[0],
        "snapshot_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_disk_snapshot_revert(name, kwargs=None, call=None):
    """
    Reverts a disk state to a previously taken snapshot.

    .. versionadded:: 2016.3.0

    name
        The name of the VM containing the snapshot.

    disk_id
        The ID of the disk to revert its state.

    snapshot_id
        The ID of the snapshot to which the snapshot should be reverted.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_disk_snapshot_revert my-vm disk_id=0 snapshot_id=6
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_disk_snapshot_revert action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    disk_id = kwargs.get("disk_id", None)
    snapshot_id = kwargs.get("snapshot_id", None)

    if disk_id is None or snapshot_id is None:
        raise SaltCloudSystemExit(
            "The vm_disk_snapshot_revert function requires a 'disk_id' and a"
            " 'snapshot_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.disksnapshotrevert(
        auth, vm_id, int(disk_id), int(snapshot_id)
    )

    data = {
        "action": "vm.disksnapshotrevert",
        "deleted": response[0],
        "snapshot_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_info(name, call=None):
    """
    Retrieves information for a given virtual machine. A VM name must be supplied.

    .. versionadded:: 2016.3.0

    name
        The name of the VM for which to gather information.

    CLI Example:

    .. code-block:: bash

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.info(auth, vm_id)

    if response[0] is False:
        return response[1]
    else:
        info = {}
        tree = _get_xml(response[1])
        info[tree.find("NAME").text] = _xml_to_dict(tree)
        return info


def vm_migrate(name, kwargs=None, call=None):
    """
    Migrates the specified virtual machine to the specified target host.

    .. versionadded:: 2016.3.0

    name
        The name of the VM to migrate.

    host_id
        The ID of the host to which the VM will be migrated. Can be used instead
        of ``host_name``.

    host_name
        The name of the host to which the VM will be migrated. Can be used instead
        of ``host_id``.

    live_migration
        If set to ``True``, a live-migration will be performed. Default is ``False``.

    capacity_maintained
        True to enforce the Host capacity is not over-committed. This parameter is only
        acknowledged for users in the ``oneadmin`` group. Host capacity will be always
        enforced for regular users.

    datastore_id
        The target system data-store ID where the VM will be migrated. Can be used
        instead of ``datastore_name``.

    datastore_name
        The name of the data-store target system where the VM will be migrated. Can be
        used instead of ``datastore_id``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_migrate my-vm host_id=0 datastore_id=1
        salt-cloud -a vm_migrate my-vm host_id=0 datastore_id=1 live_migration=True
        salt-cloud -a vm_migrate my-vm host_name=host01 datastore_name=default
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_migrate action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    host_id = kwargs.get("host_id", None)
    host_name = kwargs.get("host_name", None)
    live_migration = kwargs.get("live_migration", False)
    capacity_maintained = kwargs.get("capacity_maintained", True)
    datastore_id = kwargs.get("datastore_id", None)
    datastore_name = kwargs.get("datastore_name", None)

    if datastore_id:
        if datastore_name:
            log.warning(
                "Both the 'datastore_id' and 'datastore_name' arguments were provided. "
                "'datastore_id' will take precedence."
            )
    elif datastore_name:
        datastore_id = get_datastore_id(kwargs={"name": datastore_name})
    else:
        raise SaltCloudSystemExit(
            "The vm_migrate function requires either a 'datastore_id' or a "
            "'datastore_name' to be provided."
        )

    if host_id:
        if host_name:
            log.warning(
                "Both the 'host_id' and 'host_name' arguments were provided. "
                "'host_id' will take precedence."
            )
    elif host_name:
        host_id = get_host_id(kwargs={"name": host_name})
    else:
        raise SaltCloudSystemExit(
            "The vm_migrate function requires either a 'host_id' "
            "or a 'host_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.migrate(
        auth,
        vm_id,
        int(host_id),
        salt.utils.data.is_true(live_migration),
        salt.utils.data.is_true(capacity_maintained),
        int(datastore_id),
    )

    data = {
        "action": "vm.migrate",
        "migrated": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_monitoring(name, call=None):
    """
    Returns the monitoring records for a given virtual machine. A VM name must be
    supplied.

    The monitoring information returned is a list of VM elements. Each VM element
    contains the complete dictionary of the VM with the updated information returned
    by the poll action.

    .. versionadded:: 2016.3.0

    name
        The name of the VM for which to gather monitoring records.

    CLI Example:

    .. code-block:: bash

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

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.monitoring(auth, vm_id)

    if response[0] is False:
        log.error(
            "There was an error retrieving the specified VM's monitoring information."
        )
        return {}
    else:
        info = {}
        for vm_ in _get_xml(response[1]):
            info[vm_.find("ID").text] = _xml_to_dict(vm_)
        return info


def vm_resize(name, kwargs=None, call=None):
    """
    Changes the capacity of the virtual machine.

    .. versionadded:: 2016.3.0

    name
        The name of the VM to resize.

    path
        The path to a file containing new capacity elements CPU, VCPU, MEMORY. If one
        of them is not present, or its value is 0, the VM will not be re-sized. Syntax
        within the file can be the usual attribute=value or XML. Can be used instead
        of ``data``.

    data
        Contains the new capacity elements CPU, VCPU, and MEMORY. If one of them is not
        present, or its value is 0, the VM will not be re-sized. Can be used instead of
        ``path``.

    capacity_maintained
        True to enforce the Host capacity is not over-committed. This parameter is only
        acknowledged for users in the ``oneadmin`` group. Host capacity will be always
        enforced for regular users.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_resize my-vm path=/path/to/capacity_template.txt
        salt-cloud -a vm_resize my-vm path=/path/to/capacity_template.txt capacity_maintained=False
        salt-cloud -a vm_resize my-vm data="CPU=1 VCPU=1 MEMORY=1024"
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_resize action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    path = kwargs.get("path", None)
    data = kwargs.get("data", None)
    capacity_maintained = kwargs.get("capacity_maintained", True)

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vm_resize function requires either 'data' or a file 'path' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.resize(
        auth, vm_id, data, salt.utils.data.is_true(capacity_maintained)
    )

    ret = {
        "action": "vm.resize",
        "resized": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return ret


def vm_snapshot_create(vm_name, kwargs=None, call=None):
    """
    Creates a new virtual machine snapshot from the provided VM.

    .. versionadded:: 2016.3.0

    vm_name
        The name of the VM from which to create the snapshot.

    snapshot_name
        The name of the snapshot to be created.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_snapshot_create my-vm snapshot_name=my-new-snapshot
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_snapshot_create action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    snapshot_name = kwargs.get("snapshot_name", None)
    if snapshot_name is None:
        raise SaltCloudSystemExit(
            "The vm_snapshot_create function requires a 'snapshot_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": vm_name}))
    response = server.one.vm.snapshotcreate(auth, vm_id, snapshot_name)

    data = {
        "action": "vm.snapshotcreate",
        "snapshot_created": response[0],
        "snapshot_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_snapshot_delete(vm_name, kwargs=None, call=None):
    """
    Deletes a virtual machine snapshot from the provided VM.

    .. versionadded:: 2016.3.0

    vm_name
        The name of the VM from which to delete the snapshot.

    snapshot_id
        The ID of the snapshot to be deleted.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_snapshot_delete my-vm snapshot_id=8
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_snapshot_delete action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    snapshot_id = kwargs.get("snapshot_id", None)
    if snapshot_id is None:
        raise SaltCloudSystemExit(
            "The vm_snapshot_delete function requires a 'snapshot_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": vm_name}))
    response = server.one.vm.snapshotdelete(auth, vm_id, int(snapshot_id))

    data = {
        "action": "vm.snapshotdelete",
        "snapshot_deleted": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_snapshot_revert(vm_name, kwargs=None, call=None):
    """
    Reverts a virtual machine to a snapshot

    .. versionadded:: 2016.3.0

    vm_name
        The name of the VM to revert.

    snapshot_id
        The snapshot ID.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_snapshot_revert my-vm snapshot_id=42
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_snapshot_revert action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    snapshot_id = kwargs.get("snapshot_id", None)
    if snapshot_id is None:
        raise SaltCloudSystemExit(
            "The vm_snapshot_revert function requires a 'snapshot_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": vm_name}))
    response = server.one.vm.snapshotrevert(auth, vm_id, int(snapshot_id))

    data = {
        "action": "vm.snapshotrevert",
        "snapshot_reverted": response[0],
        "vm_id": response[1],
        "error_code": response[2],
    }

    return data


def vm_update(name, kwargs=None, call=None):
    """
    Replaces the user template contents.

    .. versionadded:: 2016.3.0

    name
        The name of the VM to update.

    path
        The path to a file containing new user template contents. Syntax within the
        file can be the usual attribute=value or XML. Can be used instead of ``data``.

    data
        Contains the new user template contents. Syntax can be the usual attribute=value
        or XML. Can be used instead of ``path``.

    update_type
        There are two ways to update a VM: ``replace`` the whole template
        or ``merge`` the new template with the existing one.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a vm_update my-vm path=/path/to/user_template_file.txt update_type='replace'
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The vm_update action must be called with -a or --action."
        )

    if kwargs is None:
        kwargs = {}

    path = kwargs.get("path", None)
    data = kwargs.get("data", None)
    update_type = kwargs.get("update_type", None)
    update_args = ["replace", "merge"]

    if update_type is None:
        raise SaltCloudSystemExit(
            "The vm_update function requires an 'update_type' to be provided."
        )

    if update_type == update_args[0]:
        update_number = 0
    elif update_type == update_args[1]:
        update_number = 1
    else:
        raise SaltCloudSystemExit(
            "The update_type argument must be either {} or {}.".format(
                update_args[0], update_args[1]
            )
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vm_update function requires either 'data' or a file 'path' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    vm_id = int(get_vm_id(kwargs={"name": name}))
    response = server.one.vm.update(auth, vm_id, data, int(update_number))

    ret = {
        "action": "vm.update",
        "updated": response[0],
        "resource_id": response[1],
        "error_code": response[2],
    }

    return ret


def vn_add_ar(call=None, kwargs=None):
    """
    Adds address ranges to a given virtual network.

    .. versionadded:: 2016.3.0

    vn_id
        The ID of the virtual network to add the address range. Can be used
        instead of ``vn_name``.

    vn_name
        The name of the virtual network to add the address range. Can be used
        instead of ``vn_id``.

    path
        The path to a file containing the template of the address range to add.
        Syntax within the file can be the usual attribute=value or XML. Can be
        used instead of ``data``.

    data
        Contains the template of the address range to add. Syntax can be the
        usual attribute=value or XML. Can be used instead of ``path``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vn_add_ar opennebula vn_id=3 path=/path/to/address_range.txt
        salt-cloud -f vn_add_ar opennebula vn_name=my-vn \\
            data="AR=[TYPE=IP4, IP=192.168.0.5, SIZE=10]"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vn_add_ar function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    vn_id = kwargs.get("vn_id", None)
    vn_name = kwargs.get("vn_name", None)
    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if vn_id:
        if vn_name:
            log.warning(
                "Both the 'vn_id' and 'vn_name' arguments were provided. "
                "'vn_id' will take precedence."
            )
    elif vn_name:
        vn_id = get_vn_id(kwargs={"name": vn_name})
    else:
        raise SaltCloudSystemExit(
            "The vn_add_ar function requires a 'vn_id' and a 'vn_name' to be provided."
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vn_add_ar function requires either 'data' or a file 'path' "
            "to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vn.add_ar(auth, int(vn_id), data)

    ret = {
        "action": "vn.add_ar",
        "address_range_added": response[0],
        "resource_id": response[1],
        "error_code": response[2],
    }

    return ret


def vn_allocate(call=None, kwargs=None):
    """
    Allocates a new virtual network in OpenNebula.

    .. versionadded:: 2016.3.0

    path
        The path to a file containing the template of the virtual network to allocate.
        Syntax within the file can be the usual attribute=value or XML. Can be used
        instead of ``data``.

    data
        Contains the template of the virtual network to allocate. Syntax can be the
        usual attribute=value or XML. Can be used instead of ``path``.

    cluster_id
        The ID of the cluster for which to add the new virtual network. Can be used
        instead of ``cluster_name``. If neither ``cluster_id`` nor ``cluster_name``
        are provided, the virtual network won’t be added to any cluster.

    cluster_name
        The name of the cluster for which to add the new virtual network. Can be used
        instead of ``cluster_id``. If neither ``cluster_name`` nor ``cluster_id`` are
        provided, the virtual network won't be added to any cluster.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vn_allocate opennebula path=/path/to/vn_file.txt
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vn_allocate function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    cluster_id = kwargs.get("cluster_id", None)
    cluster_name = kwargs.get("cluster_name", None)
    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vn_allocate function requires either 'data' or a file 'path' "
            "to be provided."
        )

    if cluster_id:
        if cluster_name:
            log.warning(
                "Both the 'cluster_id' and 'cluster_name' arguments were provided. "
                "'cluster_id' will take precedence."
            )
    elif cluster_name:
        cluster_id = get_cluster_id(kwargs={"name": cluster_name})
    else:
        cluster_id = "-1"

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vn.allocate(auth, data, int(cluster_id))

    ret = {
        "action": "vn.allocate",
        "allocated": response[0],
        "vn_id": response[1],
        "error_code": response[2],
    }

    return ret


def vn_delete(call=None, kwargs=None):
    """
    Deletes the given virtual network from OpenNebula. Either a name or a vn_id must
    be supplied.

    .. versionadded:: 2016.3.0

    name
        The name of the virtual network to delete. Can be used instead of ``vn_id``.

    vn_id
        The ID of the virtual network to delete. Can be used instead of ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vn_delete opennebula name=my-virtual-network
        salt-cloud --function vn_delete opennebula vn_id=3
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vn_delete function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    vn_id = kwargs.get("vn_id", None)

    if vn_id:
        if name:
            log.warning(
                "Both the 'vn_id' and 'name' arguments were provided. "
                "'vn_id' will take precedence."
            )
    elif name:
        vn_id = get_vn_id(kwargs={"name": name})
    else:
        raise SaltCloudSystemExit(
            "The vn_delete function requires a 'name' or a 'vn_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vn.delete(auth, int(vn_id))

    data = {
        "action": "vn.delete",
        "deleted": response[0],
        "vn_id": response[1],
        "error_code": response[2],
    }

    return data


def vn_free_ar(call=None, kwargs=None):
    """
    Frees a reserved address range from a virtual network.

    .. versionadded:: 2016.3.0

    vn_id
        The ID of the virtual network from which to free an address range.
        Can be used instead of ``vn_name``.

    vn_name
        The name of the virtual network from which to free an address range.
        Can be used instead of ``vn_id``.

    ar_id
        The ID of the address range to free.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vn_free_ar opennebula vn_id=3 ar_id=1
        salt-cloud -f vn_free_ar opennebula vn_name=my-vn ar_id=1
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vn_free_ar function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    vn_id = kwargs.get("vn_id", None)
    vn_name = kwargs.get("vn_name", None)
    ar_id = kwargs.get("ar_id", None)

    if ar_id is None:
        raise SaltCloudSystemExit(
            "The vn_free_ar function requires an 'rn_id' to be provided."
        )

    if vn_id:
        if vn_name:
            log.warning(
                "Both the 'vn_id' and 'vn_name' arguments were provided. "
                "'vn_id' will take precedence."
            )
    elif vn_name:
        vn_id = get_vn_id(kwargs={"name": vn_name})
    else:
        raise SaltCloudSystemExit(
            "The vn_free_ar function requires a 'vn_id' or a 'vn_name' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vn.free_ar(auth, int(vn_id), int(ar_id))

    data = {
        "action": "vn.free_ar",
        "ar_freed": response[0],
        "resource_id": response[1],
        "error_code": response[2],
    }

    return data


def vn_hold(call=None, kwargs=None):
    """
    Holds a virtual network lease as used.

    .. versionadded:: 2016.3.0

    vn_id
        The ID of the virtual network from which to hold the lease. Can be used
        instead of ``vn_name``.

    vn_name
        The name of the virtual network from which to hold the lease. Can be used
        instead of ``vn_id``.

    path
        The path to a file defining the template of the lease to hold.
        Syntax within the file can be the usual attribute=value or XML. Can be
        used instead of ``data``.

    data
        Contains the template of the lease to hold. Syntax can be the usual
        attribute=value or XML. Can be used instead of ``path``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vn_hold opennebula vn_id=3 path=/path/to/vn_hold_file.txt
        salt-cloud -f vn_hold opennebula vn_name=my-vn data="LEASES=[IP=192.168.0.5]"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vn_hold function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    vn_id = kwargs.get("vn_id", None)
    vn_name = kwargs.get("vn_name", None)
    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if vn_id:
        if vn_name:
            log.warning(
                "Both the 'vn_id' and 'vn_name' arguments were provided. "
                "'vn_id' will take precedence."
            )
    elif vn_name:
        vn_id = get_vn_id(kwargs={"name": vn_name})
    else:
        raise SaltCloudSystemExit(
            "The vn_hold function requires a 'vn_id' or a 'vn_name' to be provided."
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vn_hold function requires either 'data' or a 'path' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vn.hold(auth, int(vn_id), data)

    ret = {
        "action": "vn.hold",
        "held": response[0],
        "resource_id": response[1],
        "error_code": response[2],
    }

    return ret


def vn_info(call=None, kwargs=None):
    """
    Retrieves information for the virtual network.

    .. versionadded:: 2016.3.0

    name
        The name of the virtual network for which to gather information. Can be
        used instead of ``vn_id``.

    vn_id
        The ID of the virtual network for which to gather information. Can be
        used instead of ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vn_info opennebula vn_id=3
        salt-cloud --function vn_info opennebula name=public
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vn_info function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    vn_id = kwargs.get("vn_id", None)

    if vn_id:
        if name:
            log.warning(
                "Both the 'vn_id' and 'name' arguments were provided. "
                "'vn_id' will take precedence."
            )
    elif name:
        vn_id = get_vn_id(kwargs={"name": name})
    else:
        raise SaltCloudSystemExit(
            "The vn_info function requires either a 'name' or a 'vn_id' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vn.info(auth, int(vn_id))

    if response[0] is False:
        return response[1]
    else:
        info = {}
        tree = _get_xml(response[1])
        info[tree.find("NAME").text] = _xml_to_dict(tree)
        return info


def vn_release(call=None, kwargs=None):
    """
    Releases a virtual network lease that was previously on hold.

    .. versionadded:: 2016.3.0

    vn_id
        The ID of the virtual network from which to release the lease. Can be
        used instead of ``vn_name``.

    vn_name
        The name of the virtual network from which to release the lease.
        Can be used instead of ``vn_id``.

    path
        The path to a file defining the template of the lease to release.
        Syntax within the file can be the usual attribute=value or XML. Can be
        used instead of ``data``.

    data
        Contains the template defining the lease to release. Syntax can be the
        usual attribute=value or XML. Can be used instead of ``path``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vn_release opennebula vn_id=3 path=/path/to/vn_release_file.txt
        salt-cloud =f vn_release opennebula vn_name=my-vn data="LEASES=[IP=192.168.0.5]"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vn_reserve function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    vn_id = kwargs.get("vn_id", None)
    vn_name = kwargs.get("vn_name", None)
    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if vn_id:
        if vn_name:
            log.warning(
                "Both the 'vn_id' and 'vn_name' arguments were provided. "
                "'vn_id' will take precedence."
            )
    elif vn_name:
        vn_id = get_vn_id(kwargs={"name": vn_name})
    else:
        raise SaltCloudSystemExit(
            "The vn_release function requires a 'vn_id' or a 'vn_name' to be provided."
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vn_release function requires either 'data' or a 'path' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vn.release(auth, int(vn_id), data)

    ret = {
        "action": "vn.release",
        "released": response[0],
        "resource_id": response[1],
        "error_code": response[2],
    }

    return ret


def vn_reserve(call=None, kwargs=None):
    """
    Reserve network addresses.

    .. versionadded:: 2016.3.0

    vn_id
        The ID of the virtual network from which to reserve addresses. Can be used
        instead of vn_name.

    vn_name
        The name of the virtual network from which to reserve addresses. Can be
        used instead of vn_id.

    path
        The path to a file defining the template of the address reservation.
        Syntax within the file can be the usual attribute=value or XML. Can be used
        instead of ``data``.

    data
        Contains the template defining the address reservation. Syntax can be the
        usual attribute=value or XML. Data provided must be wrapped in double
        quotes. Can be used instead of ``path``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f vn_reserve opennebula vn_id=3 path=/path/to/vn_reserve_file.txt
        salt-cloud -f vn_reserve opennebula vn_name=my-vn data="SIZE=10 AR_ID=8 NETWORK_ID=1"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The vn_reserve function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    vn_id = kwargs.get("vn_id", None)
    vn_name = kwargs.get("vn_name", None)
    path = kwargs.get("path", None)
    data = kwargs.get("data", None)

    if vn_id:
        if vn_name:
            log.warning(
                "Both the 'vn_id' and 'vn_name' arguments were provided. "
                "'vn_id' will take precedence."
            )
    elif vn_name:
        vn_id = get_vn_id(kwargs={"name": vn_name})
    else:
        raise SaltCloudSystemExit(
            "The vn_reserve function requires a 'vn_id' or a 'vn_name' to be provided."
        )

    if data:
        if path:
            log.warning(
                "Both the 'data' and 'path' arguments were provided. "
                "'data' will take precedence."
            )
    elif path:
        with salt.utils.files.fopen(path, mode="r") as rfh:
            data = rfh.read()
    else:
        raise SaltCloudSystemExit(
            "The vn_reserve function requires a 'path' to be provided."
        )

    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])
    response = server.one.vn.reserve(auth, int(vn_id), data)

    ret = {
        "action": "vn.reserve",
        "reserved": response[0],
        "resource_id": response[1],
        "error_code": response[2],
    }

    return ret


# Helper Functions


def _get_node(name):
    """
    Helper function that returns all information about a named node.

    name
        The name of the node for which to get information.
    """
    attempts = 10

    while attempts >= 0:
        try:
            return list_nodes_full()[name]
        except KeyError:
            attempts -= 1
            log.debug(
                "Failed to get the data for node '%s'. Remaining attempts: %s",
                name,
                attempts,
            )

            # Just a little delay between attempts...
            time.sleep(0.5)

    return {}


def _get_xml(xml_str):
    """
    Intrepret the data coming from opennebula and raise if it's not XML.
    """
    try:
        xml_data = etree.XML(xml_str)
    # XMLSyntaxError seems to be only available from lxml, but that is the xml
    # library loaded by this module
    except etree.XMLSyntaxError as err:
        # opennebula returned invalid XML, which could be an error message, so
        # log it
        raise SaltCloudSystemExit(f"opennebula returned: {xml_str}")
    return xml_data


def _get_xml_rpc():
    """
    Uses the OpenNebula cloud provider configurations to connect to the
    OpenNebula API.

    Returns the server connection created as well as the user and password
    values from the cloud provider config file used to make the connection.
    """
    vm_ = get_configured_provider()

    xml_rpc = config.get_cloud_config_value(
        "xml_rpc", vm_, __opts__, search_global=False
    )

    user = config.get_cloud_config_value("user", vm_, __opts__, search_global=False)

    password = config.get_cloud_config_value(
        "password", vm_, __opts__, search_global=False
    )

    server = xmlrpc.client.ServerProxy(xml_rpc)

    return server, user, password


def _list_nodes(full=False):
    """
    Helper function for the list_* query functions - Constructs the
    appropriate dictionaries to return from the API query.

    full
        If performing a full query, such as in list_nodes_full, change
        this parameter to ``True``.
    """
    server, user, password = _get_xml_rpc()
    auth = ":".join([user, password])

    vm_pool = server.one.vmpool.info(auth, -2, -1, -1, -1)[1]

    vms = {}
    for vm in _get_xml(vm_pool):
        name = vm.find("NAME").text
        vms[name] = {}

        cpu_size = vm.find("TEMPLATE").find("CPU").text
        memory_size = vm.find("TEMPLATE").find("MEMORY").text

        private_ips = []
        for nic in vm.find("TEMPLATE").findall("NIC"):
            try:
                private_ips.append(nic.find("IP").text)
            except Exception:  # pylint: disable=broad-except
                pass

        vms[name]["id"] = vm.find("ID").text
        if "TEMPLATE_ID" in vm.find("TEMPLATE"):
            vms[name]["image"] = vm.find("TEMPLATE").find("TEMPLATE_ID").text
        vms[name]["name"] = name
        vms[name]["size"] = {"cpu": cpu_size, "memory": memory_size}
        vms[name]["state"] = vm.find("STATE").text
        vms[name]["private_ips"] = private_ips
        vms[name]["public_ips"] = []

        if full:
            vms[vm.find("NAME").text] = _xml_to_dict(vm)

    return vms


def _xml_to_dict(xml):
    """
    Helper function to covert xml into a data dictionary.

    xml
        The xml data to convert.
    """
    dicts = {}
    for item in xml:
        key = item.tag.lower()
        idx = 1
        while key in dicts:
            key += str(idx)
            idx += 1
        if item.text is None:
            dicts[key] = _xml_to_dict(item)
        else:
            dicts[key] = item.text

    return dicts
