"""
ProfitBricks Cloud Module
=========================

The ProfitBricks SaltStack cloud module allows a ProfitBricks server to
be automatically deployed and bootstraped with Salt.

:depends: profitbrick >= 3.1.0

The module requires ProfitBricks credentials to be supplied along with
an existing virtual datacenter UUID where the server resources will
reside. The server should also be assigned a public LAN, a private LAN,
or both along with SSH key pairs.
...

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

.. code-block:: yaml

    my-profitbricks-config:
      driver: profitbricks
      # The ProfitBricks login username
      username: user@example.com
      # The ProfitBricks login password
      password: secretpassword
      # The ProfitBricks virtual datacenter UUID
      datacenter_id: <UUID>
      # SSH private key filename
      ssh_private_key: /path/to/private.key
      # SSH public key filename
      ssh_public_key: /path/to/public.key

.. code-block:: yaml

    my-profitbricks-profile:
      provider: my-profitbricks-config
      # Name of a predefined server size.
      size: Micro Instance
      # Assign CPU family to server.
      cpu_family: INTEL_XEON
      # Number of CPU cores to allocate to node (overrides server size).
      cores: 4
      # Amount of RAM in multiples of 256 MB (overrides server size).
      ram: 4096
      # The server availability zone.
      availability_zone: ZONE_1
      # Name or UUID of the HDD image to use.
      image: <UUID>
      # Image alias could be provided instead of image.
      # Example 'ubuntu:latest'
      #image_alias: <IMAGE_ALIAS>
      # Size of the node disk in GB (overrides server size).
      disk_size: 40
      # Type of disk (HDD or SSD).
      disk_type: SSD
      # Storage availability zone to use.
      disk_availability_zone: ZONE_2
      # Assign the server to the specified public LAN.
      public_lan: <ID>
      # Assign firewall rules to the network interface.
      public_firewall_rules:
        SSH:
          protocol: TCP
          port_range_start: 22
          port_range_end: 22
      # Assign the server to the specified private LAN.
      private_lan: <ID>
      # Enable NAT on the private NIC.
      nat: true
      # Assign additional volumes to the server.
      volumes:
        data-volume:
          disk_size: 500
          disk_availability_zone: ZONE_3
        log-volume:
          disk_size: 50
          disk_type: SSD

To use a private IP for connecting and bootstrapping node:

.. code-block:: yaml

    my-profitbricks-profile:
      ssh_interface: private_lan

Set ``deploy`` to False if Salt should not be installed on the node.

.. code-block:: yaml

    my-profitbricks-profile:
      deploy: False
"""

import logging
import os
import pprint
import time

import salt.config as config
import salt.utils.cloud
import salt.utils.files
import salt.utils.stringutils
from salt.exceptions import (
    SaltCloudConfigError,
    SaltCloudExecutionFailure,
    SaltCloudExecutionTimeout,
    SaltCloudNotFound,
    SaltCloudSystemExit,
)
from salt.utils.versions import Version

try:
    # pylint: disable=no-name-in-module
    import profitbricks
    from profitbricks.client import (
        LAN,
        NIC,
        Datacenter,
        FirewallRule,
        IPBlock,
        LoadBalancer,
        PBError,
        PBNotFoundError,
        ProfitBricksService,
        Server,
        Volume,
    )

    # pylint: enable=no-name-in-module
    HAS_PROFITBRICKS = True
except ImportError:
    HAS_PROFITBRICKS = False

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

__virtualname__ = "profitbricks"


# Only load in this module if the ProfitBricks configurations are in place
def __virtual__():
    """
    Check for ProfitBricks configurations.
    """
    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__,
        ("username", "password", "datacenter_id"),
    )


def version_compatible(version):
    """
    Checks profitbricks version
    """
    return Version(profitbricks.API_VERSION) >= Version(version)


def get_dependencies():
    """
    Warn if dependencies are not met.
    """
    return config.check_driver_dependencies(
        __virtualname__, {"profitbricks": HAS_PROFITBRICKS}
    )


def get_conn():
    """
    Return a conn object for the passed VM data
    """
    return ProfitBricksService(
        username=config.get_cloud_config_value(
            "username", get_configured_provider(), __opts__, search_global=False
        ),
        password=config.get_cloud_config_value(
            "password", get_configured_provider(), __opts__, search_global=False
        ),
    )


def avail_locations(call=None):
    """
    Return a dict of all available VM locations on the cloud provider with
    relevant data
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_images function must be called with "
            "-f or --function, or with the --list-locations option"
        )

    ret = {}
    conn = get_conn()

    for item in conn.list_locations()["items"]:
        reg, loc = item["id"].split("/")
        location = {"id": item["id"]}

        if reg not in ret:
            ret[reg] = {}

        ret[reg][loc] = location
    return ret


def avail_images(call=None):
    """
    Return a list of the images that are on the provider
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_images function must be called with "
            "-f or --function, or with the --list-images option"
        )

    ret = {}
    conn = get_conn()

    for item in conn.list_images()["items"]:
        image = {"id": item["id"]}
        image.update(item["properties"])
        ret[image["name"]] = image

    return ret


def list_images(call=None, kwargs=None):
    """
    List all the images with alias by location

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_images my-profitbricks-config location=us/las
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_images function must be called with -f or --function."
        )

    if not version_compatible("4.0"):
        raise SaltCloudNotFound(
            "The 'image_alias' feature requires the profitbricks SDK v4.0.0 or greater."
        )

    ret = {}
    conn = get_conn()

    if kwargs.get("location") is not None:
        item = conn.get_location(kwargs.get("location"), 3)
        ret[item["id"]] = {"image_alias": item["properties"]["imageAliases"]}
        return ret

    for item in conn.list_locations(3)["items"]:
        ret[item["id"]] = {"image_alias": item["properties"]["imageAliases"]}

    return ret


def avail_sizes(call=None):
    """
    Return a dict of all available VM sizes on the cloud provider with
    relevant data. Latest version can be found at:
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_sizes function must be called with "
            "-f or --function, or with the --list-sizes option"
        )

    sizes = {
        "Micro Instance": {"id": "1", "ram": 1024, "disk": 50, "cores": 1},
        "Small Instance": {"id": "2", "ram": 2048, "disk": 50, "cores": 1},
        "Medium Instance": {"id": "3", "ram": 4096, "disk": 50, "cores": 2},
        "Large Instance": {"id": "4", "ram": 7168, "disk": 50, "cores": 4},
        "Extra Large Instance": {"id": "5", "ram": 14336, "disk": 50, "cores": 8},
        "Memory Intensive Instance Medium": {
            "id": "6",
            "ram": 28672,
            "disk": 50,
            "cores": 4,
        },
        "Memory Intensive Instance Large": {
            "id": "7",
            "ram": 57344,
            "disk": 50,
            "cores": 8,
        },
    }

    return sizes


def get_size(vm_):
    """
    Return the VM's size object
    """
    vm_size = config.get_cloud_config_value("size", vm_, __opts__)
    sizes = avail_sizes()

    if not vm_size:
        return sizes["Small Instance"]

    for size in sizes:
        combinations = (str(sizes[size]["id"]), str(size))
        if vm_size and str(vm_size) in combinations:
            return sizes[size]
    raise SaltCloudNotFound(f"The specified size, '{vm_size}', could not be found.")


def get_datacenter_id():
    """
    Return datacenter ID from provider configuration
    """
    datacenter_id = config.get_cloud_config_value(
        "datacenter_id", get_configured_provider(), __opts__, search_global=False
    )

    conn = get_conn()

    try:
        conn.get_datacenter(datacenter_id=datacenter_id)
    except PBNotFoundError:
        log.error("Failed to get datacenter: %s", datacenter_id)
        raise

    return datacenter_id


def list_loadbalancers(call=None):
    """
    Return a list of the loadbalancers that are on the provider
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_images function must be called with "
            "-f or --function, or with the --list-loadbalancers option"
        )

    ret = {}
    conn = get_conn()
    datacenter = get_datacenter(conn)

    for item in conn.list_loadbalancers(datacenter["id"])["items"]:
        lb = {"id": item["id"]}
        lb.update(item["properties"])
        ret[lb["name"]] = lb

    return ret


def create_loadbalancer(call=None, kwargs=None):
    """
    Creates a loadbalancer within the datacenter from the provider config.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_loadbalancer profitbricks name=mylb
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The create_address function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    conn = get_conn()
    datacenter_id = get_datacenter_id()
    loadbalancer = LoadBalancer(
        name=kwargs.get("name"), ip=kwargs.get("ip"), dhcp=kwargs.get("dhcp")
    )

    response = conn.create_loadbalancer(datacenter_id, loadbalancer)
    _wait_for_completion(conn, response, 60, "loadbalancer")

    return response


def get_datacenter(conn):
    """
    Return the datacenter from the config provider datacenter ID
    """
    datacenter_id = get_datacenter_id()

    for item in conn.list_datacenters()["items"]:
        if item["id"] == datacenter_id:
            return item

    raise SaltCloudNotFound(
        f"The specified datacenter '{datacenter_id}' could not be found."
    )


def create_datacenter(call=None, kwargs=None):
    """
    Creates a virtual datacenter based on supplied parameters.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f create_datacenter profitbricks name=mydatacenter
        location=us/las description="my description"
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The create_address function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    if kwargs.get("name") is None:
        raise SaltCloudExecutionFailure('The "name" parameter is required')

    if kwargs.get("location") is None:
        raise SaltCloudExecutionFailure('The "location" parameter is required')

    conn = get_conn()
    datacenter = Datacenter(
        name=kwargs["name"],
        location=kwargs["location"],
        description=kwargs.get("description"),
    )

    response = conn.create_datacenter(datacenter)
    _wait_for_completion(conn, response, 60, "create_datacenter")

    return response


def get_disk_type(vm_):
    """
    Return the type of disk to use. Either 'HDD' (default) or 'SSD'.
    """
    return config.get_cloud_config_value(
        "disk_type", vm_, __opts__, default="HDD", search_global=False
    )


def get_wait_timeout(vm_):
    """
    Return the wait_for_timeout for resource provisioning.
    """
    return config.get_cloud_config_value(
        "wait_for_timeout", vm_, __opts__, default=15 * 60, search_global=False
    )


def get_image(vm_):
    """
    Return the image object to use
    """
    vm_image = config.get_cloud_config_value("image", vm_, __opts__).encode(
        "ascii", "salt-cloud-force-ascii"
    )

    images = avail_images()
    for key in images:
        if vm_image and vm_image in (images[key]["id"], images[key]["name"]):
            return images[key]

    raise SaltCloudNotFound(f"The specified image, '{vm_image}', could not be found.")


def list_datacenters(conn=None, call=None):
    """
    List all the data centers

    CLI Example:

    .. code-block:: bash

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

    datacenters = []

    if not conn:
        conn = get_conn()

    for item in conn.list_datacenters()["items"]:
        datacenter = {"id": item["id"]}
        datacenter.update(item["properties"])
        datacenters.append({item["properties"]["name"]: datacenter})

    return {"Datacenters": datacenters}


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

    if not conn:
        conn = get_conn()

    ret = {}
    datacenter_id = get_datacenter_id()

    try:
        nodes = conn.list_servers(datacenter_id=datacenter_id)
    except PBNotFoundError:
        log.error("Failed to get nodes list from datacenter: %s", datacenter_id)
        raise

    for item in nodes["items"]:
        node = {"id": item["id"]}
        node.update(item["properties"])
        node["state"] = node.pop("vmState")
        ret[node["name"]] = node

    return ret


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

    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    ret = {}
    datacenter_id = get_datacenter_id()
    nodes = conn.list_servers(datacenter_id=datacenter_id, depth=3)

    for item in nodes["items"]:
        node = {"id": item["id"]}
        node.update(item["properties"])
        node["state"] = node.pop("vmState")
        node["public_ips"] = []
        node["private_ips"] = []
        if item["entities"]["nics"]["items"] > 0:
            for nic in item["entities"]["nics"]["items"]:
                if nic["properties"]["ips"]:
                    pass
                ip_address = nic["properties"]["ips"][0]
                if salt.utils.cloud.is_public_ip(ip_address):
                    node["public_ips"].append(ip_address)
                else:
                    node["private_ips"].append(ip_address)

        ret[node["name"]] = node

    __utils__["cloud.cache_node_list"](
        ret, _get_active_provider_name().split(":")[0], __opts__
    )

    return ret


def reserve_ipblock(call=None, kwargs=None):
    """
    Reserve the IP Block
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The reserve_ipblock function must be called with -f or --function."
        )

    conn = get_conn()

    if kwargs is None:
        kwargs = {}

    ret = {}
    ret["ips"] = []

    if kwargs.get("location") is None:
        raise SaltCloudExecutionFailure('The "location" parameter is required')
    location = kwargs.get("location")

    size = 1
    if kwargs.get("size") is not None:
        size = kwargs.get("size")

    block = conn.reserve_ipblock(IPBlock(size=size, location=location))
    for item in block["properties"]["ips"]:
        ret["ips"].append(item)

    return ret


def show_instance(name, call=None):
    """
    Show the details from the provider concerning an instance
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The show_instance action must be called with -a or --action."
        )

    nodes = list_nodes_full()
    __utils__["cloud.cache_node"](nodes[name], _get_active_provider_name(), __opts__)
    return nodes[name]


def get_node(conn, name):
    """
    Return a node for the named VM
    """
    datacenter_id = get_datacenter_id()

    for item in conn.list_servers(datacenter_id)["items"]:
        if item["properties"]["name"] == name:
            node = {"id": item["id"]}
            node.update(item["properties"])
            return node


def ssh_interface(vm_):
    """
    Return the ssh_interface type to connect to. Either 'public_ips' (default)
    or 'private_ips'.
    """
    return config.get_cloud_config_value(
        "ssh_interface", vm_, __opts__, default="public_ips", search_global=False
    )


def _get_nics(vm_):
    """
    Create network interfaces on appropriate LANs as defined in cloud profile.
    """
    nics = []
    if "public_lan" in vm_:
        firewall_rules = []
        # Set LAN to public if it already exists, otherwise create a new
        # public LAN.
        if "public_firewall_rules" in vm_:
            firewall_rules = _get_firewall_rules(vm_["public_firewall_rules"])
        nic = NIC(
            lan=set_public_lan(int(vm_["public_lan"])),
            name="public",
            firewall_rules=firewall_rules,
        )
        if "public_ips" in vm_:
            nic.ips = _get_ip_addresses(vm_["public_ips"])
        nics.append(nic)

    if "private_lan" in vm_:
        firewall_rules = []
        if "private_firewall_rules" in vm_:
            firewall_rules = _get_firewall_rules(vm_["private_firewall_rules"])
        nic = NIC(
            lan=int(vm_["private_lan"]), name="private", firewall_rules=firewall_rules
        )
        if "private_ips" in vm_:
            nic.ips = _get_ip_addresses(vm_["private_ips"])
        if "nat" in vm_ and "private_ips" not in vm_:
            nic.nat = vm_["nat"]
        nics.append(nic)
    return nics


def set_public_lan(lan_id):
    """
    Enables public Internet access for the specified public_lan. If no public
    LAN is available, then a new public LAN is created.
    """
    conn = get_conn()
    datacenter_id = get_datacenter_id()

    try:
        lan = conn.get_lan(datacenter_id=datacenter_id, lan_id=lan_id)
        if not lan["properties"]["public"]:
            conn.update_lan(datacenter_id=datacenter_id, lan_id=lan_id, public=True)
        return lan["id"]
    except Exception:  # pylint: disable=broad-except
        lan = conn.create_lan(datacenter_id, LAN(public=True, name="Public LAN"))
        return lan["id"]


def get_public_keys(vm_):
    """
    Retrieve list of SSH public keys.
    """
    key_filename = config.get_cloud_config_value(
        "ssh_public_key", vm_, __opts__, search_global=False, default=None
    )
    if key_filename is not None:
        key_filename = os.path.expanduser(key_filename)
        if not os.path.isfile(key_filename):
            raise SaltCloudConfigError(
                f"The defined ssh_public_key '{key_filename}' does not exist"
            )
        ssh_keys = []
        with salt.utils.files.fopen(key_filename) as rfh:
            for key in rfh.readlines():
                ssh_keys.append(salt.utils.stringutils.to_unicode(key))

        return ssh_keys


def get_key_filename(vm_):
    """
    Check SSH private key file and return absolute path if exists.
    """
    key_filename = config.get_cloud_config_value(
        "ssh_private_key", vm_, __opts__, search_global=False, default=None
    )
    if key_filename is not None:
        key_filename = os.path.expanduser(key_filename)
        if not os.path.isfile(key_filename):
            raise SaltCloudConfigError(
                f"The defined ssh_private_key '{key_filename}' does not exist"
            )

        return key_filename


def signal_event(vm_, event, description):
    args = __utils__["cloud.filter_event"](
        event, vm_, ["name", "profile", "provider", "driver"]
    )

    __utils__["cloud.fire_event"](
        "event",
        description,
        "salt/cloud/{}/creating".format(vm_["name"]),
        args=args,
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )


def create(vm_):
    """
    Create a single VM from a data dict
    """
    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 "profitbricks"),
                vm_["profile"],
            )
            is False
        ):
            return False
    except AttributeError:
        pass

    if "image_alias" in vm_ and not version_compatible("4.0"):
        raise SaltCloudNotFound(
            "The 'image_alias' parameter requires the profitbricks "
            "SDK v4.0.0 or greater."
        )

    if "image" not in vm_ and "image_alias" not in vm_:
        log.error("The image or image_alias parameter is required.")

    signal_event(vm_, "creating", "starting create")

    data = None
    datacenter_id = get_datacenter_id()
    conn = get_conn()

    # Assemble list of network interfaces from the cloud profile config.
    nics = _get_nics(vm_)

    # Assemble list of volumes from the cloud profile config.
    volumes = [_get_system_volume(vm_)]
    if "volumes" in vm_:
        volumes.extend(_get_data_volumes(vm_))

    # Assembla the composite server object.
    server = _get_server(vm_, volumes, nics)

    signal_event(vm_, "requesting", "requesting instance")

    try:
        data = conn.create_server(datacenter_id=datacenter_id, server=server)
        log.info(
            "Create server request ID: %s",
            data["requestId"],
            exc_info_on_loglevel=logging.DEBUG,
        )

        _wait_for_completion(conn, data, get_wait_timeout(vm_), "create_server")
    except PBError as exc:
        log.error(
            "Error creating %s on ProfitBricks\n\n"
            "The following exception was thrown by the profitbricks library "
            "when trying to run the initial deployment: \n%s",
            vm_["name"],
            exc,
            exc_info_on_loglevel=logging.DEBUG,
        )
        return False
    except Exception as exc:  # pylint: disable=W0703
        log.error(
            "Error creating %s \n\nError: \n%s",
            vm_["name"],
            exc,
            exc_info_on_loglevel=logging.DEBUG,
        )
        return False

    vm_["server_id"] = data["id"]

    def __query_node_data(vm_, data):
        """
        Query node data until node becomes available.
        """
        running = False
        try:
            data = show_instance(vm_["name"], "action")
            if not data:
                return False
            log.debug(
                "Loaded node data for %s:\nname: %s\nstate: %s",
                vm_["name"],
                pprint.pformat(data["name"]),
                data["state"],
            )
        except Exception as err:  # pylint: disable=broad-except
            log.error(
                "Failed to get nodes list: %s",
                err,
                # Show the trackback if the debug logging level is enabled
                exc_info_on_loglevel=logging.DEBUG,
            )
            # Trigger a failure in the wait for IP function
            return False

        running = data["state"] == "RUNNING"
        if not running:
            # Still not running, trigger another iteration
            return

        if ssh_interface(vm_) == "private_lan" and data["private_ips"]:
            vm_["ssh_host"] = data["private_ips"][0]

        if ssh_interface(vm_) != "private_lan" and data["public_ips"]:
            vm_["ssh_host"] = data["public_ips"][0]

        return data

    try:
        data = salt.utils.cloud.wait_for_ip(
            __query_node_data,
            update_args=(vm_, data),
            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=10
            ),
        )
    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.message))

    log.debug("VM is now running")
    log.info("Created Cloud VM %s", vm_)
    log.debug("%s VM creation details:\n%s", vm_, pprint.pformat(data))

    signal_event(vm_, "created", "created instance")

    if "ssh_host" in vm_:
        vm_["key_filename"] = get_key_filename(vm_)
        ret = __utils__["cloud.bootstrap"](vm_, __opts__)
        ret.update(data)
        return ret
    else:
        raise SaltCloudSystemExit("A valid IP address was not found.")


def destroy(name, call=None):
    """
    destroy a machine by name

    :param name: name given to the machine
    :param call: call value in this case is 'action'
    :return: array of booleans , true if successfully stopped and true if
             successfully removed

    CLI Example:

    .. code-block:: bash

        salt-cloud -d 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"],
        transport=__opts__["transport"],
    )

    datacenter_id = get_datacenter_id()
    conn = get_conn()
    node = get_node(conn, name)
    attached_volumes = None

    delete_volumes = config.get_cloud_config_value(
        "delete_volumes", get_configured_provider(), __opts__, search_global=False
    )
    # Get volumes before the server is deleted
    attached_volumes = conn.get_attached_volumes(
        datacenter_id=datacenter_id, server_id=node["id"]
    )

    conn.delete_server(datacenter_id=datacenter_id, server_id=node["id"])

    # The server is deleted and now is safe to delete the volumes
    if delete_volumes:
        for vol in attached_volumes["items"]:
            log.debug("Deleting volume %s", vol["id"])
            conn.delete_volume(datacenter_id=datacenter_id, volume_id=vol["id"])
            log.debug("Deleted volume %s", vol["id"])

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

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

    return True


def reboot(name, call=None):
    """
    reboot a machine by name
    :param name: name given to the machine
    :param call: call value in this case is 'action'
    :return: true if successful

    CLI Example:

    .. code-block:: bash

        salt-cloud -a reboot vm_name
    """
    datacenter_id = get_datacenter_id()
    conn = get_conn()
    node = get_node(conn, name)

    conn.reboot_server(datacenter_id=datacenter_id, server_id=node["id"])

    return True


def stop(name, call=None):
    """
    stop a machine by name
    :param name: name given to the machine
    :param call: call value in this case is 'action'
    :return: true if successful

    CLI Example:

    .. code-block:: bash

        salt-cloud -a stop vm_name
    """
    datacenter_id = get_datacenter_id()
    conn = get_conn()
    node = get_node(conn, name)

    conn.stop_server(datacenter_id=datacenter_id, server_id=node["id"])

    return True


def start(name, call=None):
    """
    start a machine by name
    :param name: name given to the machine
    :param call: call value in this case is 'action'
    :return: true if successful

    CLI Example:

    .. code-block:: bash

        salt-cloud -a start vm_name
    """
    datacenter_id = get_datacenter_id()
    conn = get_conn()
    node = get_node(conn, name)

    conn.start_server(datacenter_id=datacenter_id, server_id=node["id"])

    return True


def _override_size(vm_):
    """
    Apply any extra component overrides to VM from the cloud profile.
    """
    vm_size = get_size(vm_)

    if "cores" in vm_:
        vm_size["cores"] = vm_["cores"]

    if "ram" in vm_:
        vm_size["ram"] = vm_["ram"]

    return vm_size


def _get_server(vm_, volumes, nics):
    """
    Construct server instance from cloud profile config
    """
    # Apply component overrides to the size from the cloud profile config
    vm_size = _override_size(vm_)

    # Set the server availability zone from the cloud profile config
    availability_zone = config.get_cloud_config_value(
        "availability_zone", vm_, __opts__, default=None, search_global=False
    )

    # Assign CPU family from the cloud profile config
    cpu_family = config.get_cloud_config_value(
        "cpu_family", vm_, __opts__, default=None, search_global=False
    )

    # Contruct server object
    return Server(
        name=vm_["name"],
        ram=vm_size["ram"],
        availability_zone=availability_zone,
        cores=vm_size["cores"],
        cpu_family=cpu_family,
        create_volumes=volumes,
        nics=nics,
    )


def _get_system_volume(vm_):
    """
    Construct VM system volume list from cloud profile config
    """

    # Override system volume size if 'disk_size' is defined in cloud profile
    disk_size = get_size(vm_)["disk"]
    if "disk_size" in vm_:
        disk_size = vm_["disk_size"]

    # Construct the system volume
    volume = Volume(
        name="{} Storage".format(vm_["name"]),
        size=disk_size,
        disk_type=get_disk_type(vm_),
    )

    if "image_password" in vm_:
        image_password = vm_["image_password"]
        volume.image_password = image_password

    # Retrieve list of SSH public keys
    ssh_keys = get_public_keys(vm_)
    volume.ssh_keys = ssh_keys

    if "image_alias" in vm_.keys():
        volume.image_alias = vm_["image_alias"]
    else:
        volume.image = get_image(vm_)["id"]
        # Set volume availability zone if defined in the cloud profile
        if "disk_availability_zone" in vm_:
            volume.availability_zone = vm_["disk_availability_zone"]

    return volume


def _get_data_volumes(vm_):
    """
    Construct a list of optional data volumes from the cloud profile
    """
    ret = []
    volumes = vm_["volumes"]
    for key, value in volumes.items():
        # Verify the required 'disk_size' property is present in the cloud
        # profile config
        if "disk_size" not in volumes[key].keys():
            raise SaltCloudConfigError(f"The volume '{key}' is missing 'disk_size'")
        # Use 'HDD' if no 'disk_type' property is present in cloud profile
        if "disk_type" not in volumes[key].keys():
            volumes[key]["disk_type"] = "HDD"

        # Construct volume object and assign to a list.
        volume = Volume(
            name=key,
            size=volumes[key]["disk_size"],
            disk_type=volumes[key]["disk_type"],
            licence_type="OTHER",
        )

        # Set volume availability zone if defined in the cloud profile
        if "disk_availability_zone" in volumes[key].keys():
            volume.availability_zone = volumes[key]["disk_availability_zone"]

        ret.append(volume)

    return ret


def _get_ip_addresses(ip_addresses):
    """
    Construct a list of ip address
    """
    ret = []
    for item in ip_addresses:
        ret.append(item)

    return ret


def _get_firewall_rules(firewall_rules):
    """
    Construct a list of optional firewall rules from the cloud profile.
    """
    ret = []
    for key, value in firewall_rules.items():
        # Verify the required 'protocol' property is present in the cloud
        # profile config
        if "protocol" not in firewall_rules[key].keys():
            raise SaltCloudConfigError(
                f"The firewall rule '{key}' is missing 'protocol'"
            )
        ret.append(
            FirewallRule(
                name=key,
                protocol=firewall_rules[key].get("protocol", None),
                source_mac=firewall_rules[key].get("source_mac", None),
                source_ip=firewall_rules[key].get("source_ip", None),
                target_ip=firewall_rules[key].get("target_ip", None),
                port_range_start=firewall_rules[key].get("port_range_start", None),
                port_range_end=firewall_rules[key].get("port_range_end", None),
                icmp_type=firewall_rules[key].get("icmp_type", None),
                icmp_code=firewall_rules[key].get("icmp_code", None),
            )
        )

    return ret


def _wait_for_completion(conn, promise, wait_timeout, msg):
    """
    Poll request status until resource is provisioned.
    """
    if not promise:
        return
    wait_timeout = time.time() + wait_timeout
    while wait_timeout > time.time():
        time.sleep(5)
        operation_result = conn.get_request(
            request_id=promise["requestId"], status=True
        )

        if operation_result["metadata"]["status"] == "DONE":
            return
        elif operation_result["metadata"]["status"] == "FAILED":
            raise Exception(
                "Request: {}, requestId: {} failed to complete:\n{}".format(
                    msg,
                    str(promise["requestId"]),
                    operation_result["metadata"]["message"],
                )
            )

    raise Exception(
        'Timed out waiting for asynchronous operation {} "{}" to complete.'.format(
            msg, str(promise["requestId"])
        )
    )
