saltstack/salt

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

Summary

Maintainability
F
1 wk
Test Coverage
# -*- coding: utf-8 -*-
'''
SoftLayer Cloud Module
======================

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

Use of this module only requires the ``apikey`` parameter. Set up the cloud
configuration at:

``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/softlayer.conf``:

.. code-block:: yaml

    my-softlayer-config:
      # SoftLayer account api key
      user: MYLOGIN
      apikey: JVkbSJDGHSDKUKSDJfhsdklfjgsjdkflhjlsdfffhgdgjkenrtuinv
      driver: softlayer

The SoftLayer Python Library needs to be installed in order to use the
SoftLayer salt.cloud modules. See: https://pypi.python.org/pypi/SoftLayer

:depends: softlayer
'''

# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import time

# Import salt cloud libs
import salt.utils.cloud
import salt.config as config
from salt.exceptions import SaltCloudSystemExit

# Import 3rd-party libs
from salt.ext import six

# Attempt to import softlayer lib
try:
    import SoftLayer
    HAS_SLLIBS = True
except ImportError:
    HAS_SLLIBS = False

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

__virtualname__ = 'softlayer'


# Only load in this module if the SoftLayer configurations are in place
def __virtual__():
    '''
    Check for SoftLayer 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__,
        __active_provider_name__ or __virtualname__,
        ('apikey',)
    )


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


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 get_conn(service='SoftLayer_Virtual_Guest'):
    '''
    Return a conn object for the passed VM data
    '''
    client = SoftLayer.Client(
        username=config.get_cloud_config_value(
            'user', get_configured_provider(), __opts__, search_global=False
        ),
        api_key=config.get_cloud_config_value(
            'apikey', get_configured_provider(), __opts__, search_global=False
        ),
    )
    return client[service]


def avail_locations(call=None):
    '''
    List all available locations
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_locations function must be called with '
            '-f or --function, or with the --list-locations option'
        )

    ret = {}
    conn = get_conn()
    response = conn.getCreateObjectOptions()
    #return response
    for datacenter in response['datacenters']:
        #return data center
        ret[datacenter['template']['datacenter']['name']] = {
            'name': datacenter['template']['datacenter']['name'],
        }
    return ret


def avail_sizes(call=None):
    '''
    Return a dict of all available VM sizes on the cloud provider with
    relevant data. This data is provided in three dicts.
    '''
    if call == 'action':
        raise SaltCloudSystemExit(
            'The avail_sizes function must be called with '
            '-f or --function, or with the --list-sizes option'
        )

    ret = {
        'block devices': {},
        'memory': {},
        'processors': {},
    }
    conn = get_conn()
    response = conn.getCreateObjectOptions()
    for device in response['blockDevices']:
        #return device['template']['blockDevices']
        ret['block devices'][device['itemPrice']['item']['description']] = {
            'name': device['itemPrice']['item']['description'],
            'capacity':
                device['template']['blockDevices'][0]['diskImage']['capacity'],
        }
    for memory in response['memory']:
        ret['memory'][memory['itemPrice']['item']['description']] = {
            'name': memory['itemPrice']['item']['description'],
            'maxMemory': memory['template']['maxMemory'],
        }
    for processors in response['processors']:
        ret['processors'][processors['itemPrice']['item']['description']] = {
            'name': processors['itemPrice']['item']['description'],
            'start cpus': processors['template']['startCpus'],
        }
    return ret


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

    ret = {}
    conn = get_conn()
    response = conn.getCreateObjectOptions()
    for image in response['operatingSystems']:
        ret[image['itemPrice']['item']['description']] = {
            'name': image['itemPrice']['item']['description'],
            'template': image['template']['operatingSystemReferenceCode'],
        }
    return ret


def list_custom_images(call=None):
    '''
    Return a dict of all custom VM images on the cloud provider.
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_vlans function must be called with -f or --function.'
        )

    ret = {}
    conn = get_conn('SoftLayer_Account')
    response = conn.getBlockDeviceTemplateGroups()
    for image in response:
        if 'globalIdentifier' not in image:
            continue
        ret[image['name']] = {
            'id': image['id'],
            'name': image['name'],
            'globalIdentifier': image['globalIdentifier'],
        }
        if 'note' in image:
            ret[image['name']]['note'] = image['note']
    return ret


def get_location(vm_=None):
    '''
    Return the location to use, in this order:
        - CLI parameter
        - VM parameter
        - Cloud profile setting
    '''
    return __opts__.get(
        'location',
        config.get_cloud_config_value(
            'location',
            vm_ or get_configured_provider(),
            __opts__,
            #default=DEFAULT_LOCATION,
            search_global=False
        )
    )


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

    name = vm_['name']
    hostname = name
    domain = config.get_cloud_config_value(
        'domain', vm_, __opts__, default=None
    )
    if domain is None:
        SaltCloudSystemExit(
            'A domain name is required for the SoftLayer driver.'
        )

    if vm_.get('use_fqdn'):
        name = '.'.join([name, domain])
        vm_['name'] = name

    __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)
    conn = get_conn()
    kwargs = {
        'hostname': hostname,
        'domain': domain,
        'startCpus': vm_['cpu_number'],
        'maxMemory': vm_['ram'],
        'hourlyBillingFlag': vm_['hourly_billing'],
    }

    local_disk_flag = config.get_cloud_config_value(
        'local_disk', vm_, __opts__, default=False
    )
    kwargs['localDiskFlag'] = local_disk_flag

    if 'image' in vm_:
        kwargs['operatingSystemReferenceCode'] = vm_['image']
        kwargs['blockDevices'] = []
        disks = vm_['disk_size']

        if isinstance(disks, int):
            disks = [six.text_type(disks)]
        elif isinstance(disks, six.string_types):
            disks = [size.strip() for size in disks.split(',')]

        count = 0
        for disk in disks:
            # device number '1' is reserved for the SWAP disk
            if count == 1:
                count += 1
            block_device = {'device': six.text_type(count),
                            'diskImage': {'capacity': six.text_type(disk)}}
            kwargs['blockDevices'].append(block_device)
            count += 1

            # Upper bound must be 5 as we're skipping '1' for the SWAP disk ID
            if count > 5:
                log.warning('More that 5 disks were specified for %s .'
                            'The first 5 disks will be applied to the VM, '
                            'but the remaining disks will be ignored.\n'
                            'Please adjust your cloud configuration to only '
                            'specify a maximum of 5 disks.', name)
                break

    elif 'global_identifier' in vm_:
        kwargs['blockDeviceTemplateGroup'] = {
            'globalIdentifier': vm_['global_identifier']
        }

    location = get_location(vm_)
    if location:
        kwargs['datacenter'] = {'name': location}

    private_vlan = config.get_cloud_config_value(
        'private_vlan', vm_, __opts__, default=False
    )
    if private_vlan:
        kwargs['primaryBackendNetworkComponent'] = {
            'networkVlan': {
                'id': private_vlan,
            }
        }

    private_network = config.get_cloud_config_value(
        'private_network', vm_, __opts__, default=False
    )
    if bool(private_network) is True:
        kwargs['privateNetworkOnlyFlag'] = 'True'

    public_vlan = config.get_cloud_config_value(
        'public_vlan', vm_, __opts__, default=False
    )
    if public_vlan:
        kwargs['primaryNetworkComponent'] = {
            'networkVlan': {
                'id': public_vlan,
            }
        }

    public_security_groups = config.get_cloud_config_value(
        'public_security_groups', vm_, __opts__, default=False
    )
    if public_security_groups:
        secgroups = [{'securityGroup': {'id': int(sg)}}
                     for sg in public_security_groups]
        pnc = kwargs.get('primaryNetworkComponent', {})
        pnc['securityGroupBindings'] = secgroups
        kwargs.update({'primaryNetworkComponent': pnc})

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

    if private_security_groups:
        secgroups = [{'securityGroup': {'id': int(sg)}}
                     for sg in private_security_groups]
        pbnc = kwargs.get('primaryBackendNetworkComponent', {})
        pbnc['securityGroupBindings'] = secgroups
        kwargs.update({'primaryBackendNetworkComponent': pbnc})

    max_net_speed = config.get_cloud_config_value(
        'max_net_speed', vm_, __opts__, default=10
    )
    if max_net_speed:
        kwargs['networkComponents'] = [{
            'maxSpeed': int(max_net_speed)
        }]

    post_uri = config.get_cloud_config_value(
        'post_uri', vm_, __opts__, default=None
    )
    if post_uri:
        kwargs['postInstallScriptUri'] = post_uri

    dedicated_host_id = config.get_cloud_config_value(
        'dedicated_host_id', vm_, __opts__, default=None
    )
    if dedicated_host_id:
        kwargs['dedicatedHost'] = {'id': dedicated_host_id}

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

    try:
        response = conn.createObject(kwargs)
    except Exception as exc:
        log.error(
            'Error creating %s on SoftLayer\n\n'
            'The following exception was thrown when trying to '
            'run the initial deployment: \n%s', name, exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG
        )
        return False

    ip_type = 'primaryIpAddress'
    private_ssh = config.get_cloud_config_value(
        'private_ssh', vm_, __opts__, default=False
    )
    private_wds = config.get_cloud_config_value(
        'private_windows', vm_, __opts__, default=False
    )
    if private_ssh or private_wds or public_vlan is None:
        ip_type = 'primaryBackendIpAddress'

    def wait_for_ip():
        '''
        Wait for the IP address to become available
        '''
        nodes = list_nodes_full()
        if ip_type in nodes[hostname]:
            return nodes[hostname][ip_type]
        time.sleep(1)
        return False

    ip_address = salt.utils.cloud.wait_for_fun(
        wait_for_ip,
        timeout=config.get_cloud_config_value(
            'wait_for_fun_timeout', vm_, __opts__, default=15 * 60),
    )
    if config.get_cloud_config_value('deploy', vm_, __opts__) is not True:
        return show_instance(hostname, call='action')

    SSH_PORT = 22
    WINDOWS_DS_PORT = 445
    managing_port = SSH_PORT
    if config.get_cloud_config_value('windows', vm_, __opts__) or \
            config.get_cloud_config_value('win_installer', vm_, __opts__):
        managing_port = WINDOWS_DS_PORT

    ssh_connect_timeout = config.get_cloud_config_value(
        'ssh_connect_timeout', vm_, __opts__, 15 * 60
    )
    connect_timeout = config.get_cloud_config_value(
        'connect_timeout', vm_, __opts__, ssh_connect_timeout
    )
    if not salt.utils.cloud.wait_for_port(ip_address,
                                          port=managing_port,
                                          timeout=connect_timeout):
        raise SaltCloudSystemExit(
            'Failed to authenticate against remote ssh'
        )

    pass_conn = get_conn(service='SoftLayer_Account')
    mask = {
        'virtualGuests': {
            'powerState': '',
            'operatingSystem': {
                'passwords': ''
            },
        },
    }

    def get_credentials():
        '''
        Wait for the password to become available
        '''
        node_info = pass_conn.getVirtualGuests(id=response['id'], mask=mask)
        for node in node_info:
            if node['id'] == response['id'] and \
                            'passwords' in node['operatingSystem'] and \
                            node['operatingSystem']['passwords']:
                return node['operatingSystem']['passwords'][0]['username'], node['operatingSystem']['passwords'][0]['password']
        time.sleep(5)
        return False

    username, passwd = salt.utils.cloud.wait_for_fun(  # pylint: disable=W0633
        get_credentials,
        timeout=config.get_cloud_config_value(
            'wait_for_fun_timeout', vm_, __opts__, default=15 * 60),
    )
    response['username'] = username
    response['password'] = passwd
    response['public_ip'] = ip_address

    ssh_username = config.get_cloud_config_value(
        'ssh_username', vm_, __opts__, default=username
    )

    vm_['ssh_host'] = ip_address
    vm_['password'] = passwd
    ret = __utils__['cloud.bootstrap'](vm_, __opts__)

    ret.update(response)

    __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 list_nodes_full(mask='mask[id]', call=None):
    '''
    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.'
        )

    ret = {}
    conn = get_conn(service='SoftLayer_Account')
    response = conn.getVirtualGuests()
    for node_id in response:
        hostname = node_id['hostname']
        ret[hostname] = node_id
    __utils__['cloud.cache_node_list'](ret, __active_provider_name__.split(':')[0], __opts__)
    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.'
        )

    ret = {}
    nodes = list_nodes_full()
    if 'error' in nodes:
        raise SaltCloudSystemExit(
            'An error occurred while listing nodes: {0}'.format(
                nodes['error']['Errors']['Error']['Message']
            )
        )
    for node in nodes:
        ret[node] = {
            'id': nodes[node]['hostname'],
            'ram': nodes[node]['maxMemory'],
            'cpus': nodes[node]['maxCpu'],
        }
        if 'primaryIpAddress' in nodes[node]:
            ret[node]['public_ips'] = nodes[node]['primaryIpAddress']
        if 'primaryBackendIpAddress' in nodes[node]:
            ret[node]['private_ips'] = nodes[node]['primaryBackendIpAddress']
        if 'status' in nodes[node]:
            ret[node]['state'] = six.text_type(nodes[node]['status']['name'])
    return ret


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(), __opts__['query.selection'], call,
    )


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

    nodes = list_nodes_full()
    __utils__['cloud.cache_node'](nodes[name], __active_provider_name__, __opts__)
    return nodes[name]


def destroy(name, call=None):
    '''
    Destroy a node.

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

    node = show_instance(name, call='action')
    conn = get_conn()
    response = conn.deleteObject(id=node['id'])

    __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 list_vlans(call=None):
    '''
    List all VLANs associated with the account
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The list_vlans function must be called with -f or --function.'
        )

    conn = get_conn(service='SoftLayer_Account')
    return conn.getNetworkVlans()