saltstack/salt

View on GitHub
salt/cloud/clouds/scaleway.py

Summary

Maintainability
F
4 days
Test Coverage
# -*- coding: utf-8 -*-
'''
Scaleway Cloud Module
=====================

.. versionadded:: 2015.8.0

The Scaleway cloud module is used to interact with your Scaleway BareMetal
Servers.

Use of this module only requires the ``api_key`` parameter to be set. Set up
the cloud configuration at ``/etc/salt/cloud.providers`` or
``/etc/salt/cloud.providers.d/scaleway.conf``:

.. code-block:: yaml

    scaleway-config:
      # Scaleway organization and token
      access_key: 0e604a2c-aea6-4081-acb2-e1d1258ef95c
      token: be8fd96b-04eb-4d39-b6ba-a9edbcf17f12
      driver: scaleway

'''

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

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

log = logging.getLogger(__name__)

__virtualname__ = 'scaleway'


# Only load in this module if the Scaleway configurations are in place
def __virtual__():
    '''
    Check for Scaleway configurations.
    '''
    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__,
        ('token',)
    )


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

    items = query(method='images', root='marketplace_root')
    ret = {}
    for image in items['images']:
        ret[image['id']] = {}
        for item in image:
            ret[image['id']][item] = six.text_type(image[item])

    return ret


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

    items = query(method='servers')

    ret = {}
    for node in items['servers']:
        public_ips = []
        private_ips = []
        image_id = ''

        if node.get('public_ip'):
            public_ips = [node['public_ip']['address']]

        if node.get('private_ip'):
            private_ips = [node['private_ip']]

        if node.get('image'):
            image_id = node['image']['id']

        ret[node['name']] = {
            'id': node['id'],
            'image_id': image_id,
            'public_ips': public_ips,
            'private_ips': private_ips,
            'size': node['volumes']['0']['size'],
            'state': node['state'],
        }
    return ret


def list_nodes_full(call=None):
    ''' Return a list of the BareMetal servers that are on the provider.
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'list_nodes_full must be called with -f or --function'
        )

    items = query(method='servers')

    # For each server, iterate on its parameters.
    ret = {}
    for node in items['servers']:
        ret[node['name']] = {}
        for item in node:
            value = node[item]
            ret[node['name']][item] = value
    return ret


def list_nodes_select(call=None):
    ''' Return a list of the BareMetal servers 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(server_):
    ''' Return the image object to use.
    '''
    images = avail_images()
    server_image = six.text_type(config.get_cloud_config_value(
        'image', server_, __opts__, search_global=False
    ))
    for image in images:
        if server_image in (images[image]['name'], images[image]['id']):
            return images[image]['id']
    raise SaltCloudNotFound(
        'The specified image, \'{0}\', could not be found.'.format(server_image)
    )


def create_node(args):
    ''' Create a node.
    '''
    node = query(method='servers', args=args, http_method='POST')

    action = query(
        method='servers',
        server_id=node['server']['id'],
        command='action',
        args={'action': 'poweron'},
        http_method='POST'
    )
    return node


def create(server_):
    '''
    Create a single BareMetal server from a data dict.
    '''
    try:
        # Check for required profile parameters before sending any API calls.
        if server_['profile'] and config.is_profile_configured(__opts__,
                                                               __active_provider_name__ or 'scaleway',
                                                               server_['profile'],
                                                               vm_=server_) is False:
            return False
    except AttributeError:
        pass

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

    log.info('Creating a BareMetal server %s', server_['name'])

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

    commercial_type = config.get_cloud_config_value(
        'commercial_type', server_, __opts__, default='C1'
    )

    key_filename = config.get_cloud_config_value(
        'ssh_key_file', server_, __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
            )
        )

    ssh_password = config.get_cloud_config_value(
        'ssh_password', server_, __opts__
    )

    kwargs = {
        'name': server_['name'],
        'organization': access_key,
        'image': get_image(server_),
        'commercial_type': commercial_type,
    }

    __utils__['cloud.fire_event'](
        'event',
        'requesting instance',
        'salt/cloud/{0}/requesting'.format(server_['name']),
        args={
            'kwargs': __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:
        log.error(
            'Error creating %s on Scaleway\n\n'
            'The following exception was thrown when trying to '
            'run the initial deployment: %s',
            server_['name'], exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG
        )
        return False

    def __query_node_data(server_name):
        ''' Called to check if the server has a public IP address.
        '''
        data = show_instance(server_name, 'action')
        if data and data.get('public_ip'):
            return data
        return False

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

    server_['ssh_host'] = data['public_ip']['address']
    server_['ssh_password'] = ssh_password
    server_['key_filename'] = key_filename
    ret = __utils__['cloud.bootstrap'](server_, __opts__)

    ret.update(data)

    log.info('Created BareMetal server \'%s\'', server_['name'])
    log.debug(
        '\'%s\' BareMetal server creation details:\n%s',
        server_['name'], pprint.pformat(data)
    )

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

    return ret


def query(method='servers', server_id=None, command=None, args=None,
          http_method='GET', root='api_root'):
    ''' Make a call to the Scaleway API.
    '''

    if root == 'api_root':
        default_url = 'https://cp-par1.scaleway.com'
    else:
        default_url = 'https://api-marketplace.scaleway.com'

    base_path = six.text_type(config.get_cloud_config_value(
        root,
        get_configured_provider(),
        __opts__,
        search_global=False,
        default=default_url
    ))

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

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

    if command:
        path += command

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

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

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

    request = __utils__["http.query"](path,
                                      method=http_method,
                                      data=data,
                                      status=True,
                                      decode=True,
                                      decode_type='json',
                                      data_render=True,
                                      data_renderer='json',
                                      headers=True,
                                      header_dict={'X-Auth-Token': token,
                                                   'User-Agent': "salt-cloud",
                                                   'Content-Type': 'application/json'})
    if request['status'] > 299:
        raise SaltCloudSystemExit(
            'An error occurred while querying Scaleway. HTTP Code: {0}  '
            'Error: \'{1}\''.format(
                request['status'],
                request['error']
            )
        )

    # success without data
    if request['status'] == 204:
        return True

    return salt.utils.json.loads(request['body'])


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


def show_instance(name, call=None):
    ''' Show the details from a Scaleway BareMetal server.
    '''
    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):
    for attempt in reversed(list(range(10))):
        try:
            return list_nodes_full()[name]
        except KeyError:
            log.debug(
                'Failed to get the data for node \'%s\'. Remaining '
                'attempts: %s', name, attempt
            )
            # Just a little delay between attempts...
            time.sleep(0.5)
    return {}


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='servers', server_id=data['id'], command='action',
        args={'action': 'terminate'}, http_method='POST'
    )

    __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