# -*- coding: utf-8 -*-
"""
Linode Cloud Module using Linode's REST API
===========================================

The Linode cloud module is used to control access to the Linode VPS system.

Use of this module only requires the ``apikey`` parameter. However, the default root password for new instances
also needs to be set. The password needs to be 8 characters and contain lowercase, uppercase, and numbers.

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

.. code-block:: yaml

    my-linode-provider:
      apikey: f4ZsmwtB1c7f85Jdu43RgXVDFlNjuJaeIYV8QMftTqKScEB2vSosFSr...
      password: F00barbaz
      driver: linode

    linode-profile:
      provider: my-linode-provider
      size: Linode 1024
      image: CentOS 7
      location: London, England, UK

"""

# Import Python Libs
from __future__ import absolute_import, print_function, unicode_literals

import datetime
import logging
import pprint
import re
import time

# Import Salt Libs
import salt.config as config
from salt.exceptions import (
    SaltCloudConfigError,
    SaltCloudException,
    SaltCloudNotFound,
    SaltCloudSystemExit,
)
from salt.ext import six
from salt.ext.six.moves import range

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

# The epoch of the last time a query was made
LASTCALL = int(time.mktime(datetime.datetime.now().timetuple()))

# Human-readable status fields (documentation: https://www.linode.com/api/linode/linode.list)
LINODE_STATUS = {
    "boot_failed": {"code": -2, "descr": "Boot Failed (not in use)"},
    "beeing_created": {"code": -1, "descr": "Being Created"},
    "brand_new": {"code": 0, "descr": "Brand New"},
    "running": {"code": 1, "descr": "Running"},
    "poweroff": {"code": 2, "descr": "Powered Off"},
    "shutdown": {"code": 3, "descr": "Shutting Down (not in use)"},
    "save_to_disk": {"code": 4, "descr": "Saved to Disk (not in use)"},
}

__virtualname__ = "linode"


# Only load in this module if the Linode configurations are in place
def __virtual__():
    """
    Check for Linode configs.
    """
    if get_configured_provider() is False:
        return False

    return __virtualname__


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


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

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-images my-linode-config
        salt-cloud -f avail_images my-linode-config
    """
    if call == "action":
        raise SaltCloudException(
            "The avail_images function must be called with -f or --function."
        )

    response = _query("avail", "distributions")

    ret = {}
    for item in response["DATA"]:
        name = item["LABEL"]
        ret[name] = item

    return ret


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

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-locations my-linode-config
        salt-cloud -f avail_locations my-linode-config
    """
    if call == "action":
        raise SaltCloudException(
            "The avail_locations function must be called with -f or --function."
        )

    response = _query("avail", "datacenters")

    ret = {}
    for item in response["DATA"]:
        name = item["LOCATION"]
        ret[name] = item

    return ret


def avail_sizes(call=None):
    """
    Return available Linode sizes.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-sizes my-linode-config
        salt-cloud -f avail_sizes my-linode-config
    """
    if call == "action":
        raise SaltCloudException(
            "The avail_locations function must be called with -f or --function."
        )

    response = _query("avail", "LinodePlans")

    ret = {}
    for item in response["DATA"]:
        name = item["LABEL"]
        ret[name] = item

    return ret


def boot(name=None, kwargs=None, call=None):
    """
    Boot a Linode.

    name
        The name of the Linode to boot. Can be used instead of ``linode_id``.

    linode_id
        The ID of the Linode to boot. If provided, will be used as an
        alternative to ``name`` and reduces the number of API calls to
        Linode by one. Will be preferred over ``name``.

    config_id
        The ID of the Config to boot. Required.

    check_running
        Defaults to True. If set to False, overrides the call to check if
        the VM is running before calling the linode.boot API call. Change
        ``check_running`` to True is useful during the boot call in the
        create function, since the new VM will not be running yet.

    Can be called as an action (which requires a name):

    .. code-block:: bash

        salt-cloud -a boot my-instance config_id=10

    ...or as a function (which requires either a name or linode_id):

    .. code-block:: bash

        salt-cloud -f boot my-linode-config name=my-instance config_id=10
        salt-cloud -f boot my-linode-config linode_id=1225876 config_id=10
    """
    if name is None and call == "action":
        raise SaltCloudSystemExit("The boot action requires a 'name'.")

    if kwargs is None:
        kwargs = {}

    linode_id = kwargs.get("linode_id", None)
    config_id = kwargs.get("config_id", None)
    check_running = kwargs.get("check_running", True)

    if call == "function":
        name = kwargs.get("name", None)

    if name is None and linode_id is None:
        raise SaltCloudSystemExit(
            "The boot function requires either a 'name' or a 'linode_id'."
        )

    if config_id is None:
        raise SaltCloudSystemExit("The boot function requires a 'config_id'.")

    if linode_id is None:
        linode_id = get_linode_id_from_name(name)
        linode_item = name
    else:
        linode_item = linode_id

    # Check if Linode is running first
    if check_running is True:
        status = get_linode(kwargs={"linode_id": linode_id})["STATUS"]
        if status == "1":
            raise SaltCloudSystemExit(
                "Cannot boot Linode {0}. "
                "Linode {0} is already running.".format(linode_item)
            )

    # Boot the VM and get the JobID from Linode
    response = _query(
        "linode", "boot", args={"LinodeID": linode_id, "ConfigID": config_id}
    )["DATA"]
    boot_job_id = response["JobID"]

    if not _wait_for_job(linode_id, boot_job_id):
        log.error("Boot failed for Linode %s.", linode_item)
        return False

    return True


def clone(kwargs=None, call=None):
    """
    Clone a Linode.

    linode_id
        The ID of the Linode to clone. Required.

    datacenter_id
        The ID of the Datacenter where the Linode will be placed. Required.

    plan_id
        The ID of the plan (size) of the Linode. Required.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f clone my-linode-config linode_id=1234567 datacenter_id=2 plan_id=5
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The clone function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    linode_id = kwargs.get("linode_id", None)
    datacenter_id = kwargs.get("datacenter_id", None)
    plan_id = kwargs.get("plan_id", None)
    required_params = [linode_id, datacenter_id, plan_id]

    for item in required_params:
        if item is None:
            raise SaltCloudSystemExit(
                "The clone function requires a 'linode_id', 'datacenter_id', "
                "and 'plan_id' to be provided."
            )

    clone_args = {
        "LinodeID": linode_id,
        "DatacenterID": datacenter_id,
        "PlanID": plan_id,
    }

    return _query("linode", "clone", args=clone_args)


def create(vm_):
    """
    Create a single Linode VM.
    """
    name = vm_["name"]
    try:
        # Check for required profile parameters before sending any API calls.
        if (
            vm_["profile"]
            and config.is_profile_configured(
                __opts__, __active_provider_name__ or "linode", vm_["profile"], vm_=vm_
            )
            is False
        ):
            return False
    except AttributeError:
        pass

    if _validate_name(name) is False:
        return False

    __utils__["cloud.fire_event"](
        "event",
        "starting create",
        "salt/cloud/{0}/creating".format(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", name)

    data = {}
    kwargs = {"name": name}

    plan_id = None
    size = vm_.get("size")
    if size:
        kwargs["size"] = size
        plan_id = get_plan_id(kwargs={"label": size})

    datacenter_id = None
    location = vm_.get("location")
    if location:
        try:
            datacenter_id = get_datacenter_id(location)
        except KeyError:
            # Linode's default datacenter is Dallas, but we still have to set one to
            # use the create function from Linode's API. Dallas's datacenter id is 2.
            datacenter_id = 2

    clonefrom_name = vm_.get("clonefrom")
    cloning = True if clonefrom_name else False
    if cloning:
        linode_id = get_linode_id_from_name(clonefrom_name)
        clone_source = get_linode(kwargs={"linode_id": linode_id})

        kwargs = {
            "clonefrom": clonefrom_name,
            "image": "Clone of {0}".format(clonefrom_name),
        }

        if size is None:
            size = clone_source["TOTALRAM"]
            kwargs["size"] = size
            plan_id = clone_source["PLANID"]

        if location is None:
            datacenter_id = clone_source["DATACENTERID"]

        # Create new Linode from cloned Linode
        try:
            result = clone(
                kwargs={
                    "linode_id": linode_id,
                    "datacenter_id": datacenter_id,
                    "plan_id": plan_id,
                }
            )
        except Exception as err:  # pylint: disable=broad-except
            log.error(
                "Error cloning '%s' on Linode.\n\n"
                "The following exception was thrown by Linode when trying to "
                "clone the specified machine:\n%s",
                clonefrom_name,
                err,
                exc_info_on_loglevel=logging.DEBUG,
            )
            return False
    else:
        kwargs["image"] = vm_["image"]

        # Create Linode
        try:
            result = _query(
                "linode",
                "create",
                args={"PLANID": plan_id, "DATACENTERID": datacenter_id},
            )
        except Exception as err:  # pylint: disable=broad-except
            log.error(
                "Error creating %s on Linode\n\n"
                "The following exception was thrown by Linode when trying to "
                "run the initial deployment:\n%s",
                name,
                err,
                exc_info_on_loglevel=logging.DEBUG,
            )
            return False

    if "ERRORARRAY" in result:
        for error_data in result["ERRORARRAY"]:
            log.error(
                "Error creating %s on Linode\n\n"
                "The Linode API returned the following: %s\n",
                name,
                error_data["ERRORMESSAGE"],
            )
            return False

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

    node_id = _clean_data(result)["LinodeID"]
    data["id"] = node_id

    if not _wait_for_status(node_id, status=(_get_status_id_by_name("brand_new"))):
        log.error(
            "Error creating %s on LINODE\n\n" "while waiting for initial ready status",
            name,
            exc_info_on_loglevel=logging.DEBUG,
        )

    # Update the Linode's Label to reflect the given VM name
    update_linode(node_id, update_args={"Label": name})
    log.debug("Set name for %s - was linode%s.", name, node_id)

    # Add private IP address if requested
    private_ip_assignment = get_private_ip(vm_)
    if private_ip_assignment:
        create_private_ip(node_id)

    # Define which ssh_interface to use
    ssh_interface = _get_ssh_interface(vm_)

    # If ssh_interface is set to use private_ips, but assign_private_ip
    # wasn't set to True, let's help out and create a private ip.
    if ssh_interface == "private_ips" and private_ip_assignment is False:
        create_private_ip(node_id)
        private_ip_assignment = True

    if cloning:
        config_id = get_config_id(kwargs={"linode_id": node_id})["config_id"]
    else:
        # Create disks and get ids
        log.debug("Creating disks for %s", name)
        root_disk_id = create_disk_from_distro(vm_, node_id)["DiskID"]
        swap_disk_id = create_swap_disk(vm_, node_id)["DiskID"]

        # Create a ConfigID using disk ids
        config_id = create_config(
            kwargs={
                "name": name,
                "linode_id": node_id,
                "root_disk_id": root_disk_id,
                "swap_disk_id": swap_disk_id,
            }
        )["ConfigID"]

    # Boot the Linode
    boot(kwargs={"linode_id": node_id, "config_id": config_id, "check_running": False})

    node_data = get_linode(kwargs={"linode_id": node_id})
    ips = get_ips(node_id)
    state = int(node_data["STATUS"])

    data["image"] = kwargs["image"]
    data["name"] = name
    data["size"] = size
    data["state"] = _get_status_descr_by_id(state)
    data["private_ips"] = ips["private_ips"]
    data["public_ips"] = ips["public_ips"]

    # Pass the correct IP address to the bootstrap ssh_host key
    if ssh_interface == "private_ips":
        vm_["ssh_host"] = data["private_ips"][0]
    else:
        vm_["ssh_host"] = data["public_ips"][0]

    # If a password wasn't supplied in the profile or provider config, set it now.
    vm_["password"] = get_password(vm_)

    # Make public_ips and private_ips available to the bootstrap script.
    vm_["public_ips"] = ips["public_ips"]
    vm_["private_ips"] = ips["private_ips"]

    # Send event that the instance has booted.
    __utils__["cloud.fire_event"](
        "event",
        "waiting for ssh",
        "salt/cloud/{0}/waiting_for_ssh".format(name),
        sock_dir=__opts__["sock_dir"],
        args={"ip_address": vm_["ssh_host"]},
        transport=__opts__["transport"],
    )

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

    ret.update(data)

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

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

    return ret


def create_config(kwargs=None, call=None):
    """
    Creates a Linode Configuration Profile.

    name
        The name of the VM to create the config for.

    linode_id
        The ID of the Linode to create the configuration for.

    root_disk_id
        The Root Disk ID to be used for this config.

    swap_disk_id
        The Swap Disk ID to be used for this config.

    data_disk_id
        The Data Disk ID to be used for this config.

    .. versionadded:: 2016.3.0

    kernel_id
        The ID of the kernel to use for this configuration profile.
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The create_config function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    linode_id = kwargs.get("linode_id", None)
    root_disk_id = kwargs.get("root_disk_id", None)
    swap_disk_id = kwargs.get("swap_disk_id", None)
    data_disk_id = kwargs.get("data_disk_id", None)
    kernel_id = kwargs.get("kernel_id", None)

    if kernel_id is None:
        # 138 appears to always be the latest 64-bit kernel for Linux
        kernel_id = 138

    required_params = [name, linode_id, root_disk_id, swap_disk_id]
    for item in required_params:
        if item is None:
            raise SaltCloudSystemExit(
                "The create_config functions requires a 'name', 'linode_id', "
                "'root_disk_id', and 'swap_disk_id'."
            )

    disklist = "{0},{1}".format(root_disk_id, swap_disk_id)
    if data_disk_id is not None:
        disklist = "{0},{1},{2}".format(root_disk_id, swap_disk_id, data_disk_id)

    config_args = {
        "LinodeID": linode_id,
        "KernelID": kernel_id,
        "Label": name,
        "DiskList": disklist,
    }

    result = _query("linode", "config.create", args=config_args)

    return _clean_data(result)


def create_disk_from_distro(vm_, linode_id, swap_size=None):
    r"""
    Creates the disk for the Linode from the distribution.

    vm\_
        The VM profile to create the disk for.

    linode_id
        The ID of the Linode to create the distribution disk for. Required.

    swap_size
        The size of the disk, in MB.

    """
    kwargs = {}

    if swap_size is None:
        swap_size = get_swap_size(vm_)

    pub_key = get_pub_key(vm_)
    root_password = get_password(vm_)

    if pub_key:
        kwargs.update({"rootSSHKey": pub_key})
    if root_password:
        kwargs.update({"rootPass": root_password})
    else:
        raise SaltCloudConfigError("The Linode driver requires a password.")

    kwargs.update(
        {
            "LinodeID": linode_id,
            "DistributionID": get_distribution_id(vm_),
            "Label": vm_["name"],
            "Size": get_disk_size(vm_, swap_size, linode_id),
        }
    )

    result = _query("linode", "disk.createfromdistribution", args=kwargs)

    return _clean_data(result)


def create_swap_disk(vm_, linode_id, swap_size=None):
    r"""
    Creates the disk for the specified Linode.

    vm\_
        The VM profile to create the swap disk for.

    linode_id
        The ID of the Linode to create the swap disk for.

    swap_size
        The size of the disk, in MB.
    """
    kwargs = {}

    if not swap_size:
        swap_size = get_swap_size(vm_)

    kwargs.update(
        {"LinodeID": linode_id, "Label": vm_["name"], "Type": "swap", "Size": swap_size}
    )

    result = _query("linode", "disk.create", args=kwargs)

    return _clean_data(result)


def create_data_disk(vm_=None, linode_id=None, data_size=None):
    r"""
    Create a data disk for the linode (type is hardcoded to ext4 at the moment)

    .. versionadded:: 2016.3.0

    vm\_
        The VM profile to create the data disk for.

    linode_id
        The ID of the Linode to create the data disk for.

    data_size
        The size of the disk, in MB.

    """
    kwargs = {}

    kwargs.update(
        {
            "LinodeID": linode_id,
            "Label": vm_["name"] + "_data",
            "Type": "ext4",
            "Size": data_size,
        }
    )

    result = _query("linode", "disk.create", args=kwargs)
    return _clean_data(result)


def create_private_ip(linode_id):
    r"""
    Creates a private IP for the specified Linode.

    linode_id
        The ID of the Linode to create the IP address for.
    """
    kwargs = {"LinodeID": linode_id}
    result = _query("linode", "ip.addprivate", args=kwargs)

    return _clean_data(result)


def destroy(name, call=None):
    """
    Destroys a Linode by name.

    name
        The name of VM to be be destroyed.

    CLI Example:

    .. code-block:: bash

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

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

    linode_id = get_linode_id_from_name(name)

    response = _query(
        "linode", "delete", args={"LinodeID": linode_id, "skipChecks": True}
    )

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

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

    return response


def get_config_id(kwargs=None, call=None):
    """
    Returns a config_id for a given linode.

    .. versionadded:: 2015.8.0

    name
        The name of the Linode for which to get the config_id. Can be used instead
        of ``linode_id``.h

    linode_id
        The ID of the Linode for which to get the config_id. Can be used instead
        of ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_config_id my-linode-config name=my-linode
        salt-cloud -f get_config_id my-linode-config linode_id=1234567
    """
    if call == "action":
        raise SaltCloudException(
            "The get_config_id function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    linode_id = kwargs.get("linode_id", None)
    if name is None and linode_id is None:
        raise SaltCloudSystemExit(
            "The get_config_id function requires either a 'name' or a 'linode_id' "
            "to be provided."
        )
    if linode_id is None:
        linode_id = get_linode_id_from_name(name)

    response = _query("linode", "config.list", args={"LinodeID": linode_id})["DATA"]
    config_id = {"config_id": response[0]["ConfigID"]}

    return config_id


def get_datacenter_id(location):
    """
    Returns the Linode Datacenter ID.

    location
        The location, or name, of the datacenter to get the ID from.
    """

    return avail_locations()[location]["DATACENTERID"]


def get_disk_size(vm_, swap, linode_id):
    r"""
    Returns the size of of the root disk in MB.

    vm\_
        The VM to get the disk size for.
    """
    disk_size = get_linode(kwargs={"linode_id": linode_id})["TOTALHD"]
    return config.get_cloud_config_value(
        "disk_size", vm_, __opts__, default=disk_size - swap
    )


def get_data_disk_size(vm_, swap, linode_id):
    """
    Return the size of of the data disk in MB

    .. versionadded:: 2016.3.0
    """
    disk_size = get_linode(kwargs={"linode_id": linode_id})["TOTALHD"]
    root_disk_size = config.get_cloud_config_value(
        "disk_size", vm_, __opts__, default=disk_size - swap
    )
    return disk_size - root_disk_size - swap


def get_distribution_id(vm_):
    r"""
    Returns the distribution ID for a VM

    vm\_
        The VM to get the distribution ID for
    """
    distributions = _query("avail", "distributions")["DATA"]
    vm_image_name = config.get_cloud_config_value("image", vm_, __opts__)

    distro_id = ""

    for distro in distributions:
        if vm_image_name == distro["LABEL"]:
            distro_id = distro["DISTRIBUTIONID"]
            return distro_id

    if not distro_id:
        raise SaltCloudNotFound(
            "The DistributionID for the '{0}' profile could not be found.\n"
            "The '{1}' instance could not be provisioned. The following distributions "
            "are available:\n{2}".format(
                vm_image_name,
                vm_["name"],
                pprint.pprint(
                    sorted(
                        [
                            distro["LABEL"].encode(__salt_system_encoding__)
                            for distro in distributions
                        ]
                    )
                ),
            )
        )


def get_ips(linode_id=None):
    """
    Returns public and private IP addresses.

    linode_id
        Limits the IP addresses returned to the specified Linode ID.
    """
    if linode_id:
        ips = _query("linode", "ip.list", args={"LinodeID": linode_id})
    else:
        ips = _query("linode", "ip.list")

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

    for item in ips:
        node_id = six.text_type(item["LINODEID"])
        if item["ISPUBLIC"] == 1:
            key = "public_ips"
        else:
            key = "private_ips"

        if ret.get(node_id) is None:
            ret.update({node_id: {"public_ips": [], "private_ips": []}})
        ret[node_id][key].append(item["IPADDRESS"])

    # If linode_id was specified, only return the ips, and not the
    # dictionary based on the linode ID as a key.
    if linode_id:
        _all_ips = {"public_ips": [], "private_ips": []}
        matching_id = ret.get(six.text_type(linode_id))
        if matching_id:
            _all_ips["private_ips"] = matching_id["private_ips"]
            _all_ips["public_ips"] = matching_id["public_ips"]

        ret = _all_ips

    return ret


def get_linode(kwargs=None, call=None):
    """
    Returns data for a single named Linode.

    name
        The name of the Linode for which to get data. Can be used instead
        ``linode_id``. Note this will induce an additional API call
        compared to using ``linode_id``.

    linode_id
        The ID of the Linode for which to get data. Can be used instead of
        ``name``.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_linode my-linode-config name=my-instance
        salt-cloud -f get_linode my-linode-config linode_id=1234567
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The get_linode function must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    name = kwargs.get("name", None)
    linode_id = kwargs.get("linode_id", None)
    if name is None and linode_id is None:
        raise SaltCloudSystemExit(
            "The get_linode function requires either a 'name' or a 'linode_id'."
        )

    if linode_id is None:
        linode_id = get_linode_id_from_name(name)

    result = _query("linode", "list", args={"LinodeID": linode_id})

    return result["DATA"][0]


def get_linode_id_from_name(name):
    """
    Returns the Linode ID for a VM from the provided name.

    name
        The name of the Linode from which to get the Linode ID. Required.
    """
    nodes = _query("linode", "list")["DATA"]

    linode_id = ""
    for node in nodes:
        if name == node["LABEL"]:
            linode_id = node["LINODEID"]
            return linode_id

    if not linode_id:
        raise SaltCloudNotFound(
            "The specified name, {0}, could not be found.".format(name)
        )


def get_password(vm_):
    r"""
    Return the password to use for a VM.

    vm\_
        The configuration to obtain the password from.
    """
    return config.get_cloud_config_value(
        "password",
        vm_,
        __opts__,
        default=config.get_cloud_config_value(
            "passwd", vm_, __opts__, search_global=False
        ),
        search_global=False,
    )


def _decode_linode_plan_label(label):
    """
    Attempts to decode a user-supplied Linode plan label
    into the format in Linode API output

    label
        The label, or name, of the plan to decode.

    Example:
        `Linode 2048` will decode to `Linode 2GB`
    """
    sizes = avail_sizes()

    if label not in sizes:
        if "GB" in label:
            raise SaltCloudException(
                "Invalid Linode plan ({}) specified - call avail_sizes() for all available options".format(
                    label
                )
            )
        else:
            plan = label.split()

            if len(plan) != 2:
                raise SaltCloudException(
                    "Invalid Linode plan ({}) specified - call avail_sizes() for all available options".format(
                        label
                    )
                )

            plan_type = plan[0]
            try:
                plan_size = int(plan[1])
            except TypeError:
                plan_size = 0
                log.debug(
                    "Failed to decode Linode plan label in Cloud Profile: %s", label
                )

            if plan_type == "Linode" and plan_size == 1024:
                plan_type = "Nanode"

            plan_size = plan_size / 1024
            new_label = "{} {}GB".format(plan_type, plan_size)

            if new_label not in sizes:
                raise SaltCloudException(
                    "Invalid Linode plan ({}) specified - call avail_sizes() for all available options".format(
                        new_label
                    )
                )

            log.warning(
                "An outdated Linode plan label was detected in your Cloud "
                "Profile (%s). Please update the profile to use the new "
                "label format (%s) for the requested Linode plan size.",
                label,
                new_label,
            )

            label = new_label

    return sizes[label]["PLANID"]


def get_plan_id(kwargs=None, call=None):
    """
    Returns the Linode Plan ID.

    label
        The label, or name, of the plan to get the ID from.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f get_plan_id linode label="Linode 1024"
    """
    if call == "action":
        raise SaltCloudException(
            "The show_instance action must be called with -f or --function."
        )

    if kwargs is None:
        kwargs = {}

    label = kwargs.get("label", None)
    if label is None:
        raise SaltCloudException("The get_plan_id function requires a 'label'.")

    label = _decode_linode_plan_label(label)

    return label


def get_private_ip(vm_):
    """
    Return True if a private ip address is requested
    """
    return config.get_cloud_config_value(
        "assign_private_ip", vm_, __opts__, default=False
    )


def get_data_disk(vm_):
    """
    Return True if a data disk is requested

    .. versionadded:: 2016.3.0
    """
    return config.get_cloud_config_value(
        "allocate_data_disk", vm_, __opts__, default=False
    )


def get_pub_key(vm_):
    r"""
    Return the SSH pubkey.

    vm\_
        The configuration to obtain the public key from.
    """
    return config.get_cloud_config_value(
        "ssh_pubkey", vm_, __opts__, search_global=False
    )


def get_swap_size(vm_):
    r"""
    Returns the amount of swap space to be used in MB.

    vm\_
        The VM profile to obtain the swap size from.
    """
    return config.get_cloud_config_value("swap", vm_, __opts__, default=128)


def get_vm_size(vm_):
    r"""
    Returns the VM's size.

    vm\_
        The VM to get the size for.
    """
    vm_size = config.get_cloud_config_value("size", vm_, __opts__)
    ram = avail_sizes()[vm_size]["RAM"]

    if vm_size.startswith("Linode"):
        vm_size = vm_size.replace("Linode ", "")

    if ram == int(vm_size):
        return ram
    else:
        raise SaltCloudNotFound(
            "The specified size, {0}, could not be found.".format(vm_size)
        )


def list_nodes(call=None):
    """
    Returns a list of linodes, keeping only a brief listing.

    CLI Example:

    .. code-block:: bash

        salt-cloud -Q
        salt-cloud --query
        salt-cloud -f list_nodes my-linode-config

    .. note::

        The ``image`` label only displays information about the VM's distribution vendor,
        such as "Debian" or "RHEL" and does not display the actual image name. This is
        due to a limitation of the Linode API.
    """
    if call == "action":
        raise SaltCloudException(
            "The list_nodes function must be called with -f or --function."
        )
    return _list_linodes(full=False)


def list_nodes_full(call=None):
    """
    List linodes, with all available information.

    CLI Example:

    .. code-block:: bash

        salt-cloud -F
        salt-cloud --full-query
        salt-cloud -f list_nodes_full my-linode-config

    .. note::

        The ``image`` label only displays information about the VM's distribution vendor,
        such as "Debian" or "RHEL" and does not display the actual image name. This is
        due to a limitation of the Linode API.
    """
    if call == "action":
        raise SaltCloudException(
            "The list_nodes_full function must be called with -f or --function."
        )
    return _list_linodes(full=True)


def list_nodes_min(call=None):
    """
    Return a list of the VMs that are on the provider. Only a list of VM names and
    their state is returned. This is the minimum amount of information needed to
    check for existing VMs.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

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

    ret = {}
    nodes = _query("linode", "list")["DATA"]

    for node in nodes:
        name = node["LABEL"]
        this_node = {
            "id": six.text_type(node["LINODEID"]),
            "state": _get_status_descr_by_id(int(node["STATUS"])),
        }

        ret[name] = this_node

    return ret


def list_nodes_select(call=None):
    """
    Return a list of the VMs that are on the provider, with select fields.
    """
    return __utils__["cloud.list_nodes_select"](
        list_nodes_full(), __opts__["query.selection"], call,
    )


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

    .. versionadded:: 2015.8.0

    name
        The name of the VM to reboot.

    CLI Example:

    .. code-block:: bash

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

    node_id = get_linode_id_from_name(name)
    response = _query("linode", "reboot", args={"LinodeID": node_id})
    data = _clean_data(response)
    reboot_jid = data["JobID"]

    if not _wait_for_job(node_id, reboot_jid):
        log.error("Reboot failed for %s.", name)
        return False

    return data


def show_instance(name, call=None):
    """
    Displays details about a particular Linode VM. Either a name or a linode_id must
    be provided.

    .. versionadded:: 2015.8.0

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

    CLI Example:

    .. code-block:: bash

        salt-cloud -a show_instance vm_name

    .. note::

        The ``image`` label only displays information about the VM's distribution vendor,
        such as "Debian" or "RHEL" and does not display the actual image name. This is
        due to a limitation of the Linode API.
    """
    if call != "action":
        raise SaltCloudException(
            "The show_instance action must be called with -a or --action."
        )

    node_id = get_linode_id_from_name(name)
    node_data = get_linode(kwargs={"linode_id": node_id})
    ips = get_ips(node_id)
    state = int(node_data["STATUS"])

    ret = {
        "id": node_data["LINODEID"],
        "image": node_data["DISTRIBUTIONVENDOR"],
        "name": node_data["LABEL"],
        "size": node_data["TOTALRAM"],
        "state": _get_status_descr_by_id(state),
        "private_ips": ips["private_ips"],
        "public_ips": ips["public_ips"],
    }

    return ret


def show_pricing(kwargs=None, call=None):
    """
    Show pricing for a particular profile. This is only an estimate, based on
    unofficial pricing sources.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

        salt-cloud -f show_pricing my-linode-config profile=my-linode-profile
    """
    if call != "function":
        raise SaltCloudException(
            "The show_instance action must be called with -f or --function."
        )

    profile = __opts__["profiles"].get(kwargs["profile"], {})
    if not profile:
        raise SaltCloudNotFound("The requested profile was not found.")

    # Make sure the profile belongs to Linode
    provider = profile.get("provider", "0:0")
    comps = provider.split(":")
    if len(comps) < 2 or comps[1] != "linode":
        raise SaltCloudException("The requested profile does not belong to Linode.")

    plan_id = get_plan_id(kwargs={"label": profile["size"]})
    response = _query("avail", "linodeplans", args={"PlanID": plan_id})["DATA"][0]

    ret = {}
    ret["per_hour"] = response["HOURLY"]
    ret["per_day"] = ret["per_hour"] * 24
    ret["per_week"] = ret["per_day"] * 7
    ret["per_month"] = response["PRICE"]
    ret["per_year"] = ret["per_month"] * 12

    return {profile["profile"]: ret}


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

    name
        The name of the VM to start.

    CLI Example:

    .. code-block:: bash

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

    node_id = get_linode_id_from_name(name)
    node = get_linode(kwargs={"linode_id": node_id})

    if node["STATUS"] == 1:
        return {
            "success": True,
            "action": "start",
            "state": "Running",
            "msg": "Machine already running",
        }

    response = _query("linode", "boot", args={"LinodeID": node_id})["DATA"]

    if _wait_for_job(node_id, response["JobID"]):
        return {"state": "Running", "action": "start", "success": True}
    else:
        return {"action": "start", "success": False}


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

    name
        The name of the VM to stop.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a stop vm_name
    """
    if call != "action":
        raise SaltCloudException("The stop action must be called with -a or --action.")

    node_id = get_linode_id_from_name(name)
    node = get_linode(kwargs={"linode_id": node_id})

    if node["STATUS"] == 2:
        return {"success": True, "state": "Stopped", "msg": "Machine already stopped"}

    response = _query("linode", "shutdown", args={"LinodeID": node_id})["DATA"]

    if _wait_for_job(node_id, response["JobID"]):
        return {"state": "Stopped", "action": "stop", "success": True}
    else:
        return {"action": "stop", "success": False}


def update_linode(linode_id, update_args=None):
    """
    Updates a Linode's properties.

    linode_id
        The ID of the Linode to shutdown. Required.

    update_args
        The args to update the Linode with. Must be in dictionary form.
    """
    update_args.update({"LinodeID": linode_id})

    result = _query("linode", "update", args=update_args)

    return _clean_data(result)


def _clean_data(api_response):
    """
    Returns the DATA response from a Linode API query as a single pre-formatted dictionary

    api_response
        The query to be cleaned.
    """
    data = {}
    data.update(api_response["DATA"])

    if not data:
        response_data = api_response["DATA"]
        data.update(response_data)

    return data


def _list_linodes(full=False):
    """
    Helper function to format and parse linode data
    """
    nodes = _query("linode", "list")["DATA"]
    ips = get_ips()

    ret = {}
    for node in nodes:
        this_node = {}
        linode_id = six.text_type(node["LINODEID"])

        this_node["id"] = linode_id
        this_node["image"] = node["DISTRIBUTIONVENDOR"]
        this_node["name"] = node["LABEL"]
        this_node["size"] = node["TOTALRAM"]

        state = int(node["STATUS"])
        this_node["state"] = _get_status_descr_by_id(state)

        for key, val in six.iteritems(ips):
            if key == linode_id:
                this_node["private_ips"] = val["private_ips"]
                this_node["public_ips"] = val["public_ips"]

        if full:
            this_node["extra"] = node

        ret[node["LABEL"]] = this_node

    return ret


def _query(
    action=None,
    command=None,
    args=None,
    method="GET",
    header_dict=None,
    data=None,
    url="https://api.linode.com/",
):
    """
    Make a web call to the Linode API.
    """
    global LASTCALL
    vm_ = get_configured_provider()

    ratelimit_sleep = config.get_cloud_config_value(
        "ratelimit_sleep", vm_, __opts__, search_global=False, default=0,
    )
    apikey = config.get_cloud_config_value("apikey", vm_, __opts__, search_global=False)

    if not isinstance(args, dict):
        args = {}

    if "api_key" not in args.keys():
        args["api_key"] = apikey

    if action and "api_action" not in args.keys():
        args["api_action"] = "{0}.{1}".format(action, command)

    if header_dict is None:
        header_dict = {}

    if method != "POST":
        header_dict["Accept"] = "application/json"

    decode = True
    if method == "DELETE":
        decode = False

    now = int(time.mktime(datetime.datetime.now().timetuple()))
    if LASTCALL >= now:
        time.sleep(ratelimit_sleep)

    result = __utils__["http.query"](
        url,
        method,
        params=args,
        data=data,
        header_dict=header_dict,
        decode=decode,
        decode_type="json",
        text=True,
        status=True,
        hide_fields=["api_key", "rootPass"],
        opts=__opts__,
    )

    if "ERRORARRAY" in result["dict"]:
        if result["dict"]["ERRORARRAY"]:
            error_list = []

            for error in result["dict"]["ERRORARRAY"]:
                msg = error["ERRORMESSAGE"]

                if msg == "Authentication failed":
                    raise SaltCloudSystemExit("Linode API Key is expired or invalid")
                else:
                    error_list.append(msg)
            raise SaltCloudException(
                "Linode API reported error(s): {}".format(", ".join(error_list))
            )

    LASTCALL = int(time.mktime(datetime.datetime.now().timetuple()))
    log.debug("Linode Response Status Code: %s", result["status"])

    return result["dict"]


def _wait_for_job(linode_id, job_id, timeout=300, quiet=True):
    """
    Wait for a Job to return.

    linode_id
        The ID of the Linode to wait on. Required.

    job_id
        The ID of the job to wait for.

    timeout
        The amount of time to wait for a status to update.

    quiet
        Log status updates to debug logs when True. Otherwise, logs to info.
    """
    interval = 5
    iterations = int(timeout / interval)

    for i in range(0, iterations):
        jobs_result = _query("linode", "job.list", args={"LinodeID": linode_id})["DATA"]
        if jobs_result[0]["JOBID"] == job_id and jobs_result[0]["HOST_SUCCESS"] == 1:
            return True

        time.sleep(interval)
        log.log(
            logging.INFO if not quiet else logging.DEBUG,
            "Still waiting on Job %s for Linode %s.",
            job_id,
            linode_id,
        )
    return False


def _wait_for_status(linode_id, status=None, timeout=300, quiet=True):
    """
    Wait for a certain status from Linode.

    linode_id
        The ID of the Linode to wait on. Required.

    status
        The status to look for to update.

    timeout
        The amount of time to wait for a status to update.

    quiet
        Log status updates to debug logs when False. Otherwise, logs to info.
    """
    if status is None:
        status = _get_status_id_by_name("brand_new")

    status_desc_waiting = _get_status_descr_by_id(status)

    interval = 5
    iterations = int(timeout / interval)

    for i in range(0, iterations):
        result = get_linode(kwargs={"linode_id": linode_id})

        if result["STATUS"] == status:
            return True

        status_desc_result = _get_status_descr_by_id(result["STATUS"])

        time.sleep(interval)
        log.log(
            logging.INFO if not quiet else logging.DEBUG,
            "Status for Linode %s is '%s', waiting for '%s'.",
            linode_id,
            status_desc_result,
            status_desc_waiting,
        )

    return False


def _get_status_descr_by_id(status_id):
    """
    Return linode status by ID

    status_id
        linode VM status ID
    """
    for status_name, status_data in six.iteritems(LINODE_STATUS):
        if status_data["code"] == int(status_id):
            return status_data["descr"]
    return LINODE_STATUS.get(status_id, None)


def _get_status_id_by_name(status_name):
    """
    Return linode status description by internalstatus name

    status_name
        internal linode VM status name
    """
    return LINODE_STATUS.get(status_name, {}).get("code", None)


def _validate_name(name):
    """
    Checks if the provided name fits Linode's labeling parameters.

    .. versionadded:: 2015.5.6

    name
        The VM name to validate
    """
    name = six.text_type(name)
    name_length = len(name)
    regex = re.compile(r"^[a-zA-Z0-9][A-Za-z0-9_-]*[a-zA-Z0-9]$")

    if name_length < 3 or name_length > 48:
        ret = False
    elif not re.match(regex, name):
        ret = False
    else:
        ret = True

    if ret is False:
        log.warning(
            "A Linode label may only contain ASCII letters or numbers, dashes, and "
            "underscores, must begin and end with letters or numbers, and be at least "
            "three characters in length."
        )

    return ret


def _get_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
    )
