saltstack/salt

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

Summary

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

The Parallels cloud module is used to control access to cloud providers using
the Parallels VPS system.

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

.. code-block:: yaml

    my-parallels-config:
      # Parallels account information
      user: myuser
      password: mypassword
      url: https://api.cloud.xmission.com:4465/paci/v1.0/
      driver: parallels

'''

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

# Import Salt libs
from salt._compat import ElementTree as ET
# pylint: disable=import-error,no-name-in-module
from salt.ext.six.moves.urllib.error import URLError
from salt.ext.six.moves.urllib.parse import urlencode as _urlencode
from salt.ext.six.moves.urllib.request import (
        HTTPBasicAuthHandler as _HTTPBasicAuthHandler,
        Request as _Request,
        urlopen as _urlopen,
        build_opener as _build_opener,
        install_opener as _install_opener
)
# pylint: enable=import-error,no-name-in-module

# Import salt cloud libs
import salt.utils.cloud
import salt.config as config
from salt.exceptions import (
    SaltCloudNotFound,
    SaltCloudSystemExit,
    SaltCloudExecutionFailure,
    SaltCloudExecutionTimeout
)

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

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

__virtualname__ = 'parallels'


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


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(action='template')
    ret = {}
    for item in items:
        ret[item.attrib['name']] = item.attrib

    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 = {}
    items = query(action='ve')

    for item in items:
        name = item.attrib['name']
        node = show_instance(name, call='action')

        ret[name] = {
            'id': node['id'],
            'image': node['platform']['template-info']['name'],
            'state': node['state'],
        }
        if 'private-ip' in node['network']:
            ret[name]['private_ips'] = [node['network']['private-ip']]
        if 'public-ip' in node['network']:
            ret[name]['public_ips'] = [node['network']['public-ip']]

    return ret


def list_nodes_full(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 = {}
    items = query(action='ve')

    for item in items:
        name = item.attrib['name']
        node = show_instance(name, call='action')

        ret[name] = node
        ret[name]['image'] = node['platform']['template-info']['name']
        if 'private-ip' in node['network']:
            ret[name]['private_ips'] = [
                node['network']['private-ip']['address']
            ]
        if 'public-ip' in node['network']:
            ret[name]['public_ips'] = [
                node['network']['public-ip']['address']
            ]

    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 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
    )
    for image in images:
        if six.text_type(vm_image) in (images[image]['name'], images[image]['id']):
            return images[image]['id']
    raise SaltCloudNotFound('The specified image could not be found.')


def create_node(vm_):
    '''
    Build and submit the XML to create a node
    '''
    # Start the tree
    content = ET.Element('ve')

    # Name of the instance
    name = ET.SubElement(content, 'name')
    name.text = vm_['name']

    # Description, defaults to name
    desc = ET.SubElement(content, 'description')
    desc.text = config.get_cloud_config_value(
        'desc', vm_, __opts__, default=vm_['name'], search_global=False
    )

    # How many CPU cores, and how fast they are
    cpu = ET.SubElement(content, 'cpu')
    cpu.attrib['number'] = config.get_cloud_config_value(
        'cpu_number', vm_, __opts__, default='1', search_global=False
    )
    cpu.attrib['power'] = config.get_cloud_config_value(
        'cpu_power', vm_, __opts__, default='1000', search_global=False
    )

    # How many megabytes of RAM
    ram = ET.SubElement(content, 'ram-size')
    ram.text = config.get_cloud_config_value(
        'ram', vm_, __opts__, default='256', search_global=False
    )

    # Bandwidth available, in kbps
    bandwidth = ET.SubElement(content, 'bandwidth')
    bandwidth.text = config.get_cloud_config_value(
        'bandwidth', vm_, __opts__, default='100', search_global=False
    )

    # How many public IPs will be assigned to this instance
    ip_num = ET.SubElement(content, 'no-of-public-ip')
    ip_num.text = config.get_cloud_config_value(
        'ip_num', vm_, __opts__, default='1', search_global=False
    )

    # Size of the instance disk
    disk = ET.SubElement(content, 've-disk')
    disk.attrib['local'] = 'true'
    disk.attrib['size'] = config.get_cloud_config_value(
        'disk_size', vm_, __opts__, default='10', search_global=False
    )

    # Attributes for the image
    vm_image = config.get_cloud_config_value(
        'image', vm_, __opts__, search_global=False
    )
    image = show_image({'image': vm_image}, call='function')
    platform = ET.SubElement(content, 'platform')
    template = ET.SubElement(platform, 'template-info')
    template.attrib['name'] = vm_image
    os_info = ET.SubElement(platform, 'os-info')
    os_info.attrib['technology'] = image[vm_image]['technology']
    os_info.attrib['type'] = image[vm_image]['osType']

    # Username and password
    admin = ET.SubElement(content, 'admin')
    admin.attrib['login'] = config.get_cloud_config_value(
        'ssh_username', vm_, __opts__, default='root'
    )
    admin.attrib['password'] = config.get_cloud_config_value(
        'password', vm_, __opts__, search_global=False
    )

    data = ET.tostring(content, encoding='UTF-8')

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

    node = query(action='ve', method='POST', data=data)
    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 'parallels',
                                                           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'])

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

    name = vm_['name']
    if not wait_until(name, 'CREATED'):
        return {'Error': 'Unable to start {0}, command timed out'.format(name)}
    start(vm_['name'], call='action')

    if not wait_until(name, 'STARTED'):
        return {'Error': 'Unable to start {0}, command timed out'.format(name)}

    def __query_node_data(vm_name):
        data = show_instance(vm_name, call='action')
        if 'public-ip' not in data['network']:
            # Trigger another iteration
            return
        return data

    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=5 * 60),
            interval=config.get_cloud_config_value(
                'wait_for_ip_interval', vm_, __opts__, default=5),
        )
    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))

    comps = data['network']['public-ip']['address'].split('/')
    public_ip = comps[0]

    vm_['ssh_host'] = public_ip
    ret = __utils__['cloud.bootstrap'](vm_, __opts__)

    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 data


def query(action=None, command=None, args=None, method='GET', data=None):
    '''
    Make a web call to a Parallels provider
    '''
    path = config.get_cloud_config_value(
        'url', get_configured_provider(), __opts__, search_global=False
    )
    auth_handler = _HTTPBasicAuthHandler()
    auth_handler.add_password(
        realm='Parallels Instance Manager',
        uri=path,
        user=config.get_cloud_config_value(
            'user', get_configured_provider(), __opts__, search_global=False
        ),
        passwd=config.get_cloud_config_value(
            'password', get_configured_provider(), __opts__,
            search_global=False
        )
    )
    opener = _build_opener(auth_handler)
    _install_opener(opener)

    if action:
        path += action

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

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

    kwargs = {'data': data}
    if isinstance(data, six.string_types) and '<?xml' in data:
        kwargs['headers'] = {
            'Content-type': 'application/xml',
        }

    if args:
        params = _urlencode(args)
        req = _Request(url='{0}?{1}'.format(path, params), **kwargs)
    else:
        req = _Request(url=path, **kwargs)

    req.get_method = lambda: method

    log.debug('%s %s', method, req.get_full_url())
    if data:
        log.debug(data)

    try:
        result = _urlopen(req)
        log.debug('PARALLELS Response Status Code: %s', result.getcode())

        if 'content-length' in result.headers:
            content = result.read()
            result.close()
            items = ET.fromstring(content)
            return items

        return {}
    except URLError as exc:
        log.error('PARALLELS Response Status Code: %s %s', exc.code, exc.msg)
        root = ET.fromstring(exc.read())
        log.error(root)
        return {'error': root}


def script(vm_):
    '''
    Return the script deployment object
    '''
    return 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_)
        )
    )


def show_image(kwargs, call=None):
    '''
    Show the details from Parallels concerning an image
    '''
    if call != 'function':
        raise SaltCloudSystemExit(
            'The show_image function must be called with -f or --function.'
        )

    items = query(action='template', command=kwargs['image'])
    if 'error' in items:
        return items['error']

    ret = {}
    for item in items:
        ret.update({item.attrib['name']: item.attrib})

    return ret


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

    items = query(action='ve', command=name)

    ret = {}
    for item in items:
        if 'text' in item.__dict__:
            ret[item.tag] = item.text
        else:
            ret[item.tag] = item.attrib

        if item._children:
            ret[item.tag] = {}
            children = item._children
            for child in children:
                ret[item.tag][child.tag] = child.attrib

    __utils__['cloud.cache_node'](ret, __active_provider_name__, __opts__)
    return ret


def wait_until(name, state, timeout=300):
    '''
    Wait until a specific state has been reached on  a node
    '''
    start_time = time.time()
    node = show_instance(name, call='action')
    while True:
        if node['state'] == state:
            return True
        time.sleep(1)
        if time.time() - start_time > timeout:
            return False
        node = show_instance(name, call='action')


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')
    if node['state'] == 'STARTED':
        stop(name, call='action')
        if not wait_until(name, 'STOPPED'):
            return {
                'Error': 'Unable to destroy {0}, command timed out'.format(
                    name
                )
            }

    data = query(action='ve', command=name, method='DELETE')

    if 'error' in data:
        return data['error']

    __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 {'Destroyed': '{0} was destroyed.'.format(name)}


def start(name, call=None):
    '''
    Start a node.

    CLI Example:

    .. code-block:: bash

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

    data = query(action='ve', command='{0}/start'.format(name), method='PUT')

    if 'error' in data:
        return data['error']

    return {'Started': '{0} was started.'.format(name)}


def stop(name, call=None):
    '''
    Stop a node.

    CLI Example:

    .. code-block:: bash

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

    data = query(action='ve', command='{0}/stop'.format(name), method='PUT')

    if 'error' in data:
        return data['error']

    return {'Stopped': '{0} was stopped.'.format(name)}