HEX
Server: Apache
System: Linux sg241.singhost.net 2.6.32-896.16.1.lve1.4.51.el6.x86_64 #1 SMP Wed Jan 17 13:19:23 EST 2018 x86_64
User: honghock (909)
PHP: 8.0.30
Disabled: passthru,system,shell_exec,show_source,exec,popen,proc_open
Upload Files
File: //usr/lib/python2.7/site-packages/salt/cloud/clouds/digitalocean.py
# -*- coding: utf-8 -*-
'''
DigitalOcean Cloud Module
=========================

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

Use of this module requires a requires a ``personal_access_token``, an ``ssh_key_file``,
and at least one SSH key name in ``ssh_key_names``. More ``ssh_key_names`` can be added
by separating each key with a comma. The ``personal_access_token`` can be found in the
DigitalOcean web interface in the "Apps & API" section. The SSH key name can be found
under the "SSH Keys" section.

.. code-block:: yaml

    # Note: This example is for /etc/salt/cloud.providers or any file in the
    # /etc/salt/cloud.providers.d/ directory.

    my-digital-ocean-config:
      personal_access_token: xxx
      ssh_key_file: /path/to/ssh/key/file
      ssh_key_names: my-key-name,my-key-name-2
      driver: digitalocean

:depends: requests
'''

# Import Python Libs
from __future__ import absolute_import, print_function, unicode_literals
import decimal
import logging
import os
import pprint
import time

# Import Salt Libs
import salt.utils.cloud
import salt.utils.files
import salt.utils.json
import salt.utils.stringutils
import salt.config as config
from salt.exceptions import (
    SaltCloudConfigError,
    SaltInvocationError,
    SaltCloudNotFound,
    SaltCloudSystemExit,
    SaltCloudExecutionFailure,
    SaltCloudExecutionTimeout
)
from salt.ext import six
from salt.ext.six.moves import zip

# Import Third Party Libs
try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

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

__virtualname__ = 'digitalocean'
__virtual_aliases__ = ('digital_ocean', 'do')


# Only load in this module if the DIGITALOCEAN configurations are in place
def __virtual__():
    '''
    Check for DigitalOcean configurations
    '''
    if get_configured_provider() is False:
        return False

    if get_dependencies() is False:
        return False

    return __virtualname__


def get_configured_provider():
    '''
    Return the first configured instance.
    '''
    return config.is_provider_configured(
        opts=__opts__,
        provider=__active_provider_name__ or __virtualname__,
        aliases=__virtual_aliases__,
        required_keys=('personal_access_token',)
    )


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


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_locations function must be called with '
            '-f or --function, or with the --list-locations option'
        )

    items = query(method='regions')
    ret = {}
    for region in items['regions']:
        ret[region['name']] = {}
        for item in six.iterkeys(region):
            ret[region['name']][item] = six.text_type(region[item])

    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'
        )

    fetch = True
    page = 1
    ret = {}

    while fetch:
        items = query(method='images', command='?page=' + six.text_type(page) + '&per_page=200')

        for image in items['images']:
            ret[image['name']] = {}
            for item in six.iterkeys(image):
                ret[image['name']][item] = image[item]

        page += 1
        try:
            fetch = 'next' in items['links']['pages']
        except KeyError:
            fetch = False

    return ret


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

    items = query(method='sizes', command='?per_page=100')
    ret = {}
    for size in items['sizes']:
        ret[size['slug']] = {}
        for item in six.iterkeys(size):
            ret[size['slug']][item] = six.text_type(size[item])

    return ret


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


def list_nodes_full(call=None, for_output=True):
    '''
    Return a list of the VMs that are on the provider
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The list_nodes_full function must be called with -f or --function.'
        )
    return _list_nodes(full=True, for_output=for_output)


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


def get_image(vm_):
    '''
    Return the image object to use
    '''
    images = avail_images()
    vm_image = config.get_cloud_config_value(
        'image', vm_, __opts__, search_global=False
    )
    if not isinstance(vm_image, six.string_types):
        vm_image = six.text_type(vm_image)

    for image in images:
        if vm_image in (images[image]['name'],
                        images[image]['slug'],
                        images[image]['id']):
            if images[image]['slug'] is not None:
                return images[image]['slug']
            return int(images[image]['id'])
    raise SaltCloudNotFound(
        'The specified image, \'{0}\', could not be found.'.format(vm_image)
    )


def get_size(vm_):
    '''
    Return the VM's size. Used by create_node().
    '''
    sizes = avail_sizes()
    vm_size = six.text_type(config.get_cloud_config_value(
        'size', vm_, __opts__, search_global=False
    ))
    for size in sizes:
        if vm_size.lower() == sizes[size]['slug']:
            return sizes[size]['slug']
    raise SaltCloudNotFound(
        'The specified size, \'{0}\', could not be found.'.format(vm_size)
    )


def get_location(vm_):
    '''
    Return the VM's location
    '''
    locations = avail_locations()
    vm_location = six.text_type(config.get_cloud_config_value(
        'location', vm_, __opts__, search_global=False
    ))

    for location in locations:
        if vm_location in (locations[location]['name'],
                           locations[location]['slug']):
            return locations[location]['slug']
    raise SaltCloudNotFound(
        'The specified location, \'{0}\', could not be found.'.format(
            vm_location
        )
    )


def create_node(args):
    '''
    Create a node
    '''
    node = query(method='droplets', args=args, http_method='post')
    return node


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__,
                                                           __active_provider_name__ or 'digitalocean',
                                                           vm_['profile'],
                                                           vm_=vm_) is False:
            return False
    except AttributeError:
        pass

    __utils__['cloud.fire_event'](
        'event',
        'starting create',
        'salt/cloud/{0}/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'],
        'size': get_size(vm_),
        'image': get_image(vm_),
        'region': get_location(vm_),
        'ssh_keys': [],
        'tags': []
    }

    # backwards compat
    ssh_key_name = config.get_cloud_config_value(
        'ssh_key_name', vm_, __opts__, search_global=False
    )

    if ssh_key_name:
        kwargs['ssh_keys'].append(get_keyid(ssh_key_name))

    ssh_key_names = config.get_cloud_config_value(
        'ssh_key_names', vm_, __opts__, search_global=False, default=False
    )

    if ssh_key_names:
        for key in ssh_key_names.split(','):
            kwargs['ssh_keys'].append(get_keyid(key))

    key_filename = config.get_cloud_config_value(
        'ssh_key_file', vm_, __opts__, search_global=False, default=None
    )

    if key_filename is not None and not os.path.isfile(key_filename):
        raise SaltCloudConfigError(
            'The defined key_filename \'{0}\' does not exist'.format(
                key_filename
            )
        )

    if not __opts__.get('ssh_agent', False) and key_filename is None:
        raise SaltCloudConfigError(
            'The DigitalOcean driver requires an ssh_key_file and an ssh_key_name '
            'because it does not supply a root password upon building the server.'
        )

    ssh_interface = config.get_cloud_config_value(
        'ssh_interface', vm_, __opts__, search_global=False, default='public'
    )

    if ssh_interface in ['private', 'public']:
        log.info("ssh_interface: Setting interface for ssh to %s", ssh_interface)
        kwargs['ssh_interface'] = ssh_interface
    else:
        raise SaltCloudConfigError(
            "The DigitalOcean driver requires ssh_interface to be defined as 'public' or 'private'."
        )

    private_networking = config.get_cloud_config_value(
        'private_networking', vm_, __opts__, search_global=False, default=None,
    )

    if private_networking is not None:
        if not isinstance(private_networking, bool):
            raise SaltCloudConfigError("'private_networking' should be a boolean value.")
        kwargs['private_networking'] = private_networking

    if not private_networking and ssh_interface == 'private':
        raise SaltCloudConfigError(
                "The DigitalOcean driver requires ssh_interface if defined as 'private' "
                "then private_networking should be set as 'True'."
    )

    backups_enabled = config.get_cloud_config_value(
        'backups_enabled', vm_, __opts__, search_global=False, default=None,
    )

    if backups_enabled is not None:
        if not isinstance(backups_enabled, bool):
            raise SaltCloudConfigError("'backups_enabled' should be a boolean value.")
        kwargs['backups'] = backups_enabled

    ipv6 = config.get_cloud_config_value(
        'ipv6', vm_, __opts__, search_global=False, default=None,
    )

    if ipv6 is not None:
        if not isinstance(ipv6, bool):
            raise SaltCloudConfigError("'ipv6' should be a boolean value.")
        kwargs['ipv6'] = ipv6

    monitoring = config.get_cloud_config_value(
        'monitoring', vm_, __opts__, search_global=False, default=None,
    )

    if monitoring is not None:
        if not isinstance(monitoring, bool):
            raise SaltCloudConfigError("'monitoring' should be a boolean value.")
        kwargs['monitoring'] = monitoring

    kwargs['tags'] = config.get_cloud_config_value(
        'tags', vm_, __opts__, search_global=False, default=False
    )

    userdata_file = config.get_cloud_config_value(
        'userdata_file', vm_, __opts__, search_global=False, default=None
    )
    if userdata_file is not None:
        try:
            with salt.utils.files.fopen(userdata_file, 'r') as fp_:
                kwargs['user_data'] = salt.utils.cloud.userdata_template(
                    __opts__, vm_, salt.utils.stringutils.to_unicode(fp_.read())
                )
        except Exception as exc:  # pylint: disable=broad-except
            log.exception(
                'Failed to read userdata from %s: %s', userdata_file, exc)

    create_dns_record = config.get_cloud_config_value(
        'create_dns_record', vm_, __opts__, search_global=False, default=None,
    )

    if create_dns_record:
        log.info('create_dns_record: will attempt to write DNS records')
        default_dns_domain = None
        dns_domain_name = vm_['name'].split('.')
        if len(dns_domain_name) > 2:
            log.debug('create_dns_record: inferring default dns_hostname, dns_domain from minion name as FQDN')
            default_dns_hostname = '.'.join(dns_domain_name[:-2])
            default_dns_domain = '.'.join(dns_domain_name[-2:])
        else:
            log.debug("create_dns_record: can't infer dns_domain from %s", vm_['name'])
            default_dns_hostname = dns_domain_name[0]

        dns_hostname = config.get_cloud_config_value(
            'dns_hostname', vm_, __opts__, search_global=False, default=default_dns_hostname,
        )
        dns_domain = config.get_cloud_config_value(
            'dns_domain', vm_, __opts__, search_global=False, default=default_dns_domain,
        )
        if dns_hostname and dns_domain:
            log.info('create_dns_record: using dns_hostname="%s", dns_domain="%s"', dns_hostname, dns_domain)
            __add_dns_addr__ = lambda t, d: post_dns_record(dns_domain=dns_domain,
                                                            name=dns_hostname,
                                                            record_type=t,
                                                            record_data=d)

            log.debug('create_dns_record: %s', __add_dns_addr__)
        else:
            log.error('create_dns_record: could not determine dns_hostname and/or dns_domain')
            raise SaltCloudConfigError(
                '\'create_dns_record\' must be a dict specifying "domain" '
                'and "hostname" or the minion name must be an FQDN.'
            )

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

    try:
        ret = create_node(kwargs)
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            'Error creating %s on DIGITALOCEAN\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

    def __query_node_data(vm_name):
        data = show_instance(vm_name, 'action')
        if not data:
            # Trigger an error in the wait_for_ip function
            return False
        if data['networks'].get('v4'):
            for network in data['networks']['v4']:
                if network['type'] == 'public':
                    return data
        return False

    try:
        data = salt.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=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(six.text_type(exc))

    if not vm_.get('ssh_host'):
        vm_['ssh_host'] = None

    # add DNS records, set ssh_host, default to first found IP, preferring IPv4 for ssh bootstrap script target
    addr_families, dns_arec_types = (('v4', 'v6'), ('A', 'AAAA'))
    arec_map = dict(list(zip(addr_families, dns_arec_types)))
    for facing, addr_family, ip_address in [(net['type'], family, net['ip_address'])
                                            for family in addr_families
                                            for net in data['networks'][family]]:
        log.info('found %s IP%s interface for "%s"', facing, addr_family, ip_address)
        dns_rec_type = arec_map[addr_family]
        if facing == 'public':
            if create_dns_record:
                __add_dns_addr__(dns_rec_type, ip_address)
        if facing == ssh_interface:
            if not vm_['ssh_host']:
                vm_['ssh_host'] = ip_address

    if vm_['ssh_host'] is None:
        raise SaltCloudSystemExit(
            'No suitable IP addresses found for ssh minion bootstrapping: {0}'.format(repr(data['networks']))
        )

    log.debug(
        'Found public IP address to use for ssh minion bootstrapping: %s',
        vm_['ssh_host']
    )

    vm_['key_filename'] = key_filename
    ret = __utils__['cloud.bootstrap'](vm_, __opts__)
    ret.update(data)

    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/{0}/created'.format(vm_['name']),
        args=__utils__['cloud.filter_event']('created', vm_, ['name', 'profile', 'provider', 'driver']),
        sock_dir=__opts__['sock_dir'],
        transport=__opts__['transport']
    )

    return ret


def query(method='droplets', droplet_id=None, command=None, args=None, http_method='get'):
    '''
    Make a web call to DigitalOcean
    '''
    base_path = six.text_type(config.get_cloud_config_value(
        'api_root',
        get_configured_provider(),
        __opts__,
        search_global=False,
        default='https://api.digitalocean.com/v2'
    ))

    path = '{0}/{1}/'.format(base_path, method)

    if droplet_id:
        path += '{0}/'.format(droplet_id)

    if command:
        path += command

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

    personal_access_token = config.get_cloud_config_value(
        'personal_access_token', get_configured_provider(), __opts__, search_global=False
    )

    data = salt.utils.json.dumps(args)

    requester = getattr(requests, http_method)
    request = requester(path, data=data, headers={'Authorization': 'Bearer ' + personal_access_token, 'Content-Type': 'application/json'})
    if request.status_code > 299:
        raise SaltCloudSystemExit(
            'An error occurred while querying DigitalOcean. HTTP Code: {0}  '
            'Error: \'{1}\''.format(
                request.status_code,
                # request.read()
                request.text
            )
        )

    log.debug(request.url)

    # success without data
    if request.status_code == 204:
        return True

    content = request.text

    result = salt.utils.json.loads(content)
    if result.get('status', '').lower() == 'error':
        raise SaltCloudSystemExit(
            pprint.pformat(result.get('error_message', {}))
        )

    return result


def script(vm_):
    '''
    Return the script deployment object
    '''
    deploy_script = salt.utils.cloud.os_script(
        config.get_cloud_config_value('script', vm_, __opts__),
        vm_,
        __opts__,
        salt.utils.cloud.salt_config_to_yaml(
            salt.utils.cloud.minion_config(__opts__, vm_)
        )
    )
    return deploy_script


def show_instance(name, call=None):
    '''
    Show the details from DigitalOcean concerning a droplet
    '''
    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, __active_provider_name__, __opts__)
    return node


def _get_node(name):
    attempts = 10
    while attempts >= 0:
        try:
            return list_nodes_full(for_output=False)[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 list_keypairs(call=None):
    '''
    Return a dict of all available VM locations on the cloud provider with
    relevant data
    '''
    if call != 'function':
        log.error(
            'The list_keypairs function must be called with -f or --function.'
        )
        return False

    fetch = True
    page = 1
    ret = {}

    while fetch:
        items = query(method='account/keys', command='?page=' + six.text_type(page) +
                      '&per_page=100')

        for key_pair in items['ssh_keys']:
            name = key_pair['name']
            if name in ret:
                raise SaltCloudSystemExit(
                    'A duplicate key pair name, \'{0}\', was found in DigitalOcean\'s '
                    'key pair list. Please change the key name stored by DigitalOcean. '
                    'Be sure to adjust the value of \'ssh_key_file\' in your cloud '
                    'profile or provider configuration, if necessary.'.format(
                        name
                    )
                )
            ret[name] = {}
            for item in six.iterkeys(key_pair):
                ret[name][item] = six.text_type(key_pair[item])

        page += 1
        try:
            fetch = 'next' in items['links']['pages']
        except KeyError:
            fetch = False

    return ret


def show_keypair(kwargs=None, call=None):
    '''
    Show the details of an SSH keypair
    '''
    if call != 'function':
        log.error(
            'The show_keypair function must be called with -f or --function.'
        )
        return False

    if not kwargs:
        kwargs = {}

    if 'keyname' not in kwargs:
        log.error('A keyname is required.')
        return False

    keypairs = list_keypairs(call='function')
    keyid = keypairs[kwargs['keyname']]['id']
    log.debug('Key ID is %s', keyid)

    details = query(method='account/keys', command=keyid)

    return details


def import_keypair(kwargs=None, call=None):
    '''
    Upload public key to cloud provider.
    Similar to EC2 import_keypair.

    .. versionadded:: 2016.11.0

    kwargs
        file(mandatory): public key file-name
        keyname(mandatory): public key name in the provider
    '''
    with salt.utils.files.fopen(kwargs['file'], 'r') as public_key_filename:
        public_key_content = salt.utils.stringutils.to_unicode(public_key_filename.read())

    digitalocean_kwargs = {
        'name': kwargs['keyname'],
        'public_key': public_key_content
    }

    created_result = create_key(digitalocean_kwargs, call=call)
    return created_result


def create_key(kwargs=None, call=None):
    '''
    Upload a public key
    '''
    if call != 'function':
        log.error(
            'The create_key function must be called with -f or --function.'
        )
        return False

    try:
        result = query(
            method='account',
            command='keys',
            args={'name': kwargs['name'],
                  'public_key': kwargs['public_key']},
            http_method='post'
        )
    except KeyError:
        log.info('`name` and `public_key` arguments must be specified')
        return False

    return result


def remove_key(kwargs=None, call=None):
    '''
    Delete public key
    '''
    if call != 'function':
        log.error(
            'The create_key function must be called with -f or --function.'
        )
        return False

    try:
        result = query(
            method='account',
            command='keys/' + kwargs['id'],
            http_method='delete'
        )
    except KeyError:
        log.info('`id` argument must be specified')
        return False

    return result


def get_keyid(keyname):
    '''
    Return the ID of the keyname
    '''
    if not keyname:
        return None
    keypairs = list_keypairs(call='function')
    keyid = keypairs[keyname]['id']
    if keyid:
        return keyid
    raise SaltCloudNotFound('The specified ssh key could not be found.')


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

    CLI Example:

    .. code-block:: bash

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

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

    data = show_instance(name, call='action')
    node = query(method='droplets', droplet_id=data['id'], http_method='delete')

    ## This is all terribly optomistic:
    # vm_ = get_vm_config(name=name)
    # delete_dns_record = config.get_cloud_config_value(
    #     'delete_dns_record', vm_, __opts__, search_global=False, default=None,
    # )
    # TODO: when _vm config data can be made available, we should honor the configuration settings,
    # but until then, we should assume stale DNS records are bad, and default behavior should be to
    # delete them if we can. When this is resolved, also resolve the comments a couple of lines below.
    delete_dns_record = True

    if not isinstance(delete_dns_record, bool):
        raise SaltCloudConfigError(
            '\'delete_dns_record\' should be a boolean value.'
        )
    # When the "to do" a few lines up is resolved, remove these lines and use the if/else logic below.
    log.debug('Deleting DNS records for %s.', name)
    destroy_dns_records(name)

    # Until the "to do" from line 754 is taken care of, we don't need this logic.
    # if delete_dns_record:
    #    log.debug('Deleting DNS records for %s.', name)
    #    destroy_dns_records(name)
    # else:
    #    log.debug('delete_dns_record : %s', delete_dns_record)
    #    for line in pprint.pformat(dir()).splitlines():
    #       log.debug('delete context: %s', line)

    __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 node


def post_dns_record(**kwargs):
    '''
    Creates a DNS record for the given name if the domain is managed with DO.
    '''
    if 'kwargs' in kwargs:  # flatten kwargs if called via salt-cloud -f
        f_kwargs = kwargs['kwargs']
        del kwargs['kwargs']
        kwargs.update(f_kwargs)
    mandatory_kwargs = ('dns_domain', 'name', 'record_type', 'record_data')
    for i in mandatory_kwargs:
        if kwargs[i]:
            pass
        else:
            error = '{0}="{1}" ## all mandatory args must be provided: {2}'.format(i, kwargs[i], mandatory_kwargs)
            raise SaltInvocationError(error)

    domain = query(method='domains', droplet_id=kwargs['dns_domain'])

    if domain:
        result = query(
            method='domains',
            droplet_id=kwargs['dns_domain'],
            command='records',
            args={'type': kwargs['record_type'], 'name': kwargs['name'], 'data': kwargs['record_data']},
            http_method='post'
        )
        return result

    return False


def destroy_dns_records(fqdn):
    '''
    Deletes DNS records for the given hostname if the domain is managed with DO.
    '''
    domain = '.'.join(fqdn.split('.')[-2:])
    hostname = '.'.join(fqdn.split('.')[:-2])
    # TODO: remove this when the todo on 754 is available
    try:
        response = query(method='domains', droplet_id=domain, command='records')
    except SaltCloudSystemExit:
        log.debug('Failed to find domains.')
        return False
    log.debug("found DNS records: %s", pprint.pformat(response))
    records = response['domain_records']

    if records:
        record_ids = [r['id'] for r in records if r['name'].decode() == hostname]
        log.debug("deleting DNS record IDs: %s", record_ids)
        for id_ in record_ids:
            try:
                log.info('deleting DNS record %s', id_)
                ret = query(
                    method='domains',
                    droplet_id=domain,
                    command='records/{0}'.format(id_),
                    http_method='delete'
                )
            except SaltCloudSystemExit:
                log.error('failed to delete DNS domain %s record ID %s.', domain, hostname)
            log.debug('DNS deletion REST call returned: %s', pprint.pformat(ret))

    return False


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 Examples:

    .. code-block:: bash

        salt-cloud -f show_pricing my-digitalocean-config profile=my-profile
    '''
    profile = __opts__['profiles'].get(kwargs['profile'], {})
    if not profile:
        return {'Error': 'The requested profile was not found'}

    # Make sure the profile belongs to DigitalOcean
    provider = profile.get('provider', '0:0')
    comps = provider.split(':')
    if len(comps) < 2 or comps[1] != 'digitalocean':
        return {'Error': 'The requested profile does not belong to DigitalOcean'}

    raw = {}
    ret = {}
    sizes = avail_sizes()
    ret['per_hour'] = decimal.Decimal(sizes[profile['size']]['price_hourly'])

    ret['per_day'] = ret['per_hour'] * 24
    ret['per_week'] = ret['per_day'] * 7
    ret['per_month'] = decimal.Decimal(sizes[profile['size']]['price_monthly'])
    ret['per_year'] = ret['per_week'] * 52

    if kwargs.get('raw', False):
        ret['_raw'] = raw

    return {profile['profile']: ret}


def list_floating_ips(call=None):
    '''
    Return a list of the floating ips that are on the provider

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f list_floating_ips my-digitalocean-config
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The list_floating_ips function must be called with '
            '-f or --function, or with the --list-floating-ips option'
        )

    fetch = True
    page = 1
    ret = {}

    while fetch:
        items = query(method='floating_ips',
                      command='?page=' + six.text_type(page) + '&per_page=200')

        for floating_ip in items['floating_ips']:
            ret[floating_ip['ip']] = {}
            for item in six.iterkeys(floating_ip):
                ret[floating_ip['ip']][item] = floating_ip[item]

        page += 1
        try:
            fetch = 'next' in items['links']['pages']
        except KeyError:
            fetch = False

    return ret


def show_floating_ip(kwargs=None, call=None):
    '''
    Show the details of a floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f show_floating_ip my-digitalocean-config floating_ip='45.55.96.47'
    '''
    if call != 'function':
        log.error(
            'The show_floating_ip function must be called with -f or --function.'
        )
        return False

    if not kwargs:
        kwargs = {}

    if 'floating_ip' not in kwargs:
        log.error('A floating IP is required.')
        return False

    floating_ip = kwargs['floating_ip']
    log.debug('Floating ip is %s', floating_ip)

    details = query(method='floating_ips', command=floating_ip)

    return details


def create_floating_ip(kwargs=None, call=None):
    '''
    Create a new floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f create_floating_ip my-digitalocean-config region='NYC2'

        salt-cloud -f create_floating_ip my-digitalocean-config droplet_id='1234567'
    '''
    if call != 'function':
        log.error(
            'The create_floating_ip function must be called with -f or --function.'
        )
        return False

    if not kwargs:
        kwargs = {}

    if 'droplet_id' in kwargs:
        result = query(method='floating_ips',
                           args={'droplet_id': kwargs['droplet_id']},
                           http_method='post')

        return result

    elif 'region' in kwargs:
        result = query(method='floating_ips',
                           args={'region': kwargs['region']},
                           http_method='post')

        return result

    else:
        log.error('A droplet_id or region is required.')
        return False


def delete_floating_ip(kwargs=None, call=None):
    '''
    Delete a floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f delete_floating_ip my-digitalocean-config floating_ip='45.55.96.47'
    '''
    if call != 'function':
        log.error(
            'The delete_floating_ip function must be called with -f or --function.'
        )
        return False

    if not kwargs:
        kwargs = {}

    if 'floating_ip' not in kwargs:
        log.error('A floating IP is required.')
        return False

    floating_ip = kwargs['floating_ip']
    log.debug('Floating ip is %s', kwargs['floating_ip'])

    result = query(method='floating_ips',
                   command=floating_ip,
                   http_method='delete')

    return result


def assign_floating_ip(kwargs=None, call=None):
    '''
    Assign a floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f assign_floating_ip my-digitalocean-config droplet_id=1234567 floating_ip='45.55.96.47'
    '''
    if call != 'function':
        log.error(
            'The assign_floating_ip function must be called with -f or --function.'
        )
        return False

    if not kwargs:
        kwargs = {}

    if 'floating_ip' and 'droplet_id' not in kwargs:
        log.error('A floating IP and droplet_id is required.')
        return False

    result = query(method='floating_ips',
                   command=kwargs['floating_ip'] + '/actions',
                   args={'droplet_id': kwargs['droplet_id'], 'type': 'assign'},
                   http_method='post')

    return result


def unassign_floating_ip(kwargs=None, call=None):
    '''
    Unassign a floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f unassign_floating_ip my-digitalocean-config floating_ip='45.55.96.47'
    '''
    if call != 'function':
        log.error(
            'The inassign_floating_ip function must be called with -f or --function.'
        )
        return False

    if not kwargs:
        kwargs = {}

    if 'floating_ip' not in kwargs:
        log.error('A floating IP is required.')
        return False

    result = query(method='floating_ips',
                   command=kwargs['floating_ip'] + '/actions',
                   args={'type': 'unassign'},
                   http_method='post')

    return result


def _list_nodes(full=False, for_output=False):
    '''
    Helper function to format and parse node data.
    '''
    fetch = True
    page = 1
    ret = {}

    while fetch:
        items = query(method='droplets',
                      command='?page=' + six.text_type(page) + '&per_page=200')
        for node in items['droplets']:
            name = node['name']
            ret[name] = {}
            if full:
                ret[name] = _get_full_output(node, for_output=for_output)
            else:
                public_ips, private_ips = _get_ips(node['networks'])
                ret[name] = {
                    'id': node['id'],
                    'image': node['image']['name'],
                    'name': name,
                    'private_ips': private_ips,
                    'public_ips': public_ips,
                    'size': node['size_slug'],
                    'state': six.text_type(node['status']),
                }

        page += 1
        try:
            fetch = 'next' in items['links']['pages']
        except KeyError:
            fetch = False

    return ret


def reboot(name, call=None):
    '''
    Reboot a droplet in DigitalOcean.

    .. versionadded:: 2015.8.8

    name
        The name of the droplet to restart.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a reboot droplet_name
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The restart action must be called with -a or --action.'
        )

    data = show_instance(name, call='action')
    if data.get('status') == 'off':
        return {'success': True,
                'action': 'stop',
                'status': 'off',
                'msg': 'Machine is already off.'}

    ret = query(droplet_id=data['id'],
                command='actions',
                args={'type': 'reboot'},
                http_method='post')

    return {'success': True,
            'action': ret['action']['type'],
            'state': ret['action']['status']}


def start(name, call=None):
    '''
    Start a droplet in DigitalOcean.

    .. versionadded:: 2015.8.8

    name
        The name of the droplet to start.

    CLI Example:

    .. code-block:: bash

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

    data = show_instance(name, call='action')
    if data.get('status') == 'active':
        return {'success': True,
                'action': 'start',
                'status': 'active',
                'msg': 'Machine is already running.'}

    ret = query(droplet_id=data['id'],
                command='actions',
                args={'type': 'power_on'},
                http_method='post')

    return {'success': True,
            'action': ret['action']['type'],
            'state': ret['action']['status']}


def stop(name, call=None):
    '''
    Stop a droplet in DigitalOcean.

    .. versionadded:: 2015.8.8

    name
        The name of the droplet to stop.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a stop droplet_name
    '''
    if call != 'action':
        raise SaltCloudSystemExit(
            'The stop action must be called with -a or --action.'
        )

    data = show_instance(name, call='action')
    if data.get('status') == 'off':
        return {'success': True,
                'action': 'stop',
                'status': 'off',
                'msg': 'Machine is already off.'}

    ret = query(droplet_id=data['id'],
                command='actions',
                args={'type': 'shutdown'},
                http_method='post')

    return {'success': True,
            'action': ret['action']['type'],
            'state': ret['action']['status']}


def _get_full_output(node, for_output=False):
    '''
    Helper function for _list_nodes to loop through all node information.
    Returns a dictionary containing the full information of a node.
    '''
    ret = {}
    for item in six.iterkeys(node):
        value = node[item]
        if value is not None and for_output:
            value = six.text_type(value)
        ret[item] = value
    return ret


def _get_ips(networks):
    '''
    Helper function for list_nodes. Returns public and private ip lists based on a
    given network dictionary.
    '''
    v4s = networks.get('v4')
    v6s = networks.get('v6')
    public_ips = []
    private_ips = []

    if v4s:
        for item in v4s:
            ip_type = item.get('type')
            ip_address = item.get('ip_address')
            if ip_type == 'public':
                public_ips.append(ip_address)
            if ip_type == 'private':
                private_ips.append(ip_address)

    if v6s:
        for item in v6s:
            ip_type = item.get('type')
            ip_address = item.get('ip_address')
            if ip_type == 'public':
                public_ips.append(ip_address)
            if ip_type == 'private':
                private_ips.append(ip_address)

    return public_ips, private_ips