salt/cloud/libcloudfuncs.py
# -*- coding: utf-8 -*-
'''
The generic libcloud template used to create the connections and deploy the
cloud virtual machines
'''
from __future__ import absolute_import, print_function, unicode_literals
# Import python libs
import os
import logging
from salt.ext import six
from salt.ext.six.moves import zip
# pylint: disable=W0611
# Import libcloud
try:
import libcloud
import re
from libcloud.compute.types import Provider
from libcloud.compute.providers import get_driver
from libcloud.compute.deployment import (
MultiStepDeployment,
ScriptDeployment
)
HAS_LIBCLOUD = True
LIBCLOUD_VERSION_INFO = tuple([
int(part) for part in libcloud.__version__.replace('-', '.').replace('rc', '.').split('.')[:3]
])
except ImportError:
HAS_LIBCLOUD = False
LIBCLOUD_VERSION_INFO = (1000,)
# pylint: enable=W0611
# Import salt libs
import salt.utils.event
import salt.client
# Import salt cloud libs
import salt.utils.cloud
import salt.utils.data
import salt.config as config
from salt.exceptions import SaltCloudNotFound, SaltCloudSystemExit
# Get logging started
log = logging.getLogger(__name__)
LIBCLOUD_MINIMAL_VERSION = (0, 14, 0)
def node_state(id_):
'''
Libcloud supported node states
'''
states_int = {
0: 'RUNNING',
1: 'REBOOTING',
2: 'TERMINATED',
3: 'PENDING',
4: 'UNKNOWN',
5: 'STOPPED',
6: 'SUSPENDED',
7: 'ERROR',
8: 'PAUSED'}
states_str = {
'running': 'RUNNING',
'rebooting': 'REBOOTING',
'starting': 'STARTING',
'terminated': 'TERMINATED',
'pending': 'PENDING',
'unknown': 'UNKNOWN',
'stopping': 'STOPPING',
'stopped': 'STOPPED',
'suspended': 'SUSPENDED',
'error': 'ERROR',
'paused': 'PAUSED',
'reconfiguring': 'RECONFIGURING'
}
return states_str[id_] if isinstance(id_, six.string_types) else states_int[id_]
def check_libcloud_version(reqver=LIBCLOUD_MINIMAL_VERSION, why=None):
'''
Compare different libcloud versions
'''
if not HAS_LIBCLOUD:
return False
if not isinstance(reqver, (list, tuple)):
raise RuntimeError(
'\'reqver\' needs to passed as a tuple or list, i.e., (0, 14, 0)'
)
try:
import libcloud # pylint: disable=redefined-outer-name
except ImportError:
raise ImportError(
'salt-cloud requires >= libcloud {0} which is not installed'.format(
'.'.join([six.text_type(num) for num in reqver])
)
)
if LIBCLOUD_VERSION_INFO >= reqver:
return libcloud.__version__
errormsg = 'Your version of libcloud is {0}. '.format(libcloud.__version__)
errormsg += 'salt-cloud requires >= libcloud {0}'.format(
'.'.join([six.text_type(num) for num in reqver])
)
if why:
errormsg += ' for {0}'.format(why)
errormsg += '. Please upgrade.'
raise ImportError(errormsg)
def get_node(conn, name):
'''
Return a libcloud node for the named VM
'''
nodes = conn.list_nodes()
for node in nodes:
if node.name == name:
__utils__['cloud.cache_node'](salt.utils.data.simple_types_filter(node.__dict__), __active_provider_name__, __opts__)
return node
def avail_locations(conn=None, 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'
)
if not conn:
conn = get_conn() # pylint: disable=E0602
locations = conn.list_locations()
ret = {}
for img in locations:
if isinstance(img.name, six.string_types) and not six.PY3:
img_name = img.name.encode('ascii', 'salt-cloud-force-ascii')
else:
img_name = str(img.name) # future lint: disable=blacklisted-function
ret[img_name] = {}
for attr in dir(img):
if attr.startswith('_') or attr == 'driver':
continue
attr_value = getattr(img, attr)
if isinstance(attr_value, six.string_types) and not six.PY3:
attr_value = attr_value.encode(
'ascii', 'salt-cloud-force-ascii'
)
ret[img_name][attr] = attr_value
return ret
def avail_images(conn=None, call=None):
'''
Return a dict of all available VM images on the cloud provider with
relevant data
'''
if call == 'action':
raise SaltCloudSystemExit(
'The avail_images function must be called with '
'-f or --function, or with the --list-images option'
)
if not conn:
conn = get_conn() # pylint: disable=E0602
images = conn.list_images()
ret = {}
for img in images:
if isinstance(img.name, six.string_types) and not six.PY3:
img_name = img.name.encode('ascii', 'salt-cloud-force-ascii')
else:
img_name = str(img.name) # future lint: disable=blacklisted-function
ret[img_name] = {}
for attr in dir(img):
if attr.startswith('_') or attr in ('driver', 'get_uuid'):
continue
attr_value = getattr(img, attr)
if isinstance(attr_value, six.string_types) and not six.PY3:
attr_value = attr_value.encode(
'ascii', 'salt-cloud-force-ascii'
)
ret[img_name][attr] = attr_value
return ret
def avail_sizes(conn=None, call=None):
'''
Return a dict of all available VM images on the cloud provider with
relevant data
'''
if call == 'action':
raise SaltCloudSystemExit(
'The avail_sizes function must be called with '
'-f or --function, or with the --list-sizes option'
)
if not conn:
conn = get_conn() # pylint: disable=E0602
sizes = conn.list_sizes()
ret = {}
for size in sizes:
if isinstance(size.name, six.string_types) and not six.PY3:
size_name = size.name.encode('ascii', 'salt-cloud-force-ascii')
else:
size_name = str(size.name) # future lint: disable=blacklisted-function
ret[size_name] = {}
for attr in dir(size):
if attr.startswith('_') or attr in ('driver', 'get_uuid'):
continue
try:
attr_value = getattr(size, attr)
except Exception:
pass
if isinstance(attr_value, six.string_types) and not six.PY3:
attr_value = attr_value.encode(
'ascii', 'salt-cloud-force-ascii'
)
ret[size_name][attr] = attr_value
return ret
def get_location(conn, vm_):
'''
Return the location object to use
'''
locations = conn.list_locations()
vm_location = config.get_cloud_config_value('location', vm_, __opts__)
if not six.PY3:
vm_location = vm_location.encode(
'ascii', 'salt-cloud-force-ascii'
)
for img in locations:
if isinstance(img.id, six.string_types) and not six.PY3:
img_id = img.id.encode('ascii', 'salt-cloud-force-ascii')
else:
img_id = str(img.id) # future lint: disable=blacklisted-function
if isinstance(img.name, six.string_types) and not six.PY3:
img_name = img.name.encode('ascii', 'salt-cloud-force-ascii')
else:
img_name = str(img.name) # future lint: disable=blacklisted-function
if vm_location and vm_location in (img_id, img_name):
return img
raise SaltCloudNotFound(
'The specified location, \'{0}\', could not be found.'.format(
vm_location
)
)
def get_image(conn, vm_):
'''
Return the image object to use
'''
images = conn.list_images()
vm_image = config.get_cloud_config_value('image', vm_, __opts__)
if not six.PY3:
vm_image = vm_image.encode('ascii', 'salt-cloud-force-ascii')
for img in images:
if isinstance(img.id, six.string_types) and not six.PY3:
img_id = img.id.encode('ascii', 'salt-cloud-force-ascii')
else:
img_id = str(img.id) # future lint: disable=blacklisted-function
if isinstance(img.name, six.string_types) and not six.PY3:
img_name = img.name.encode('ascii', 'salt-cloud-force-ascii')
else:
img_name = str(img.name) # future lint: disable=blacklisted-function
if vm_image and vm_image in (img_id, img_name):
return img
raise SaltCloudNotFound(
'The specified image, \'{0}\', could not be found.'.format(vm_image)
)
def get_size(conn, vm_):
'''
Return the VM's size object
'''
sizes = conn.list_sizes()
vm_size = config.get_cloud_config_value('size', vm_, __opts__)
if not vm_size:
return sizes[0]
for size in sizes:
if vm_size and str(vm_size) in (str(size.id), str(size.name)): # pylint: disable=blacklisted-function
return size
raise SaltCloudNotFound(
'The specified size, \'{0}\', could not be found.'.format(vm_size)
)
def script(vm_):
'''
Return the script deployment object
'''
return ScriptDeployment(
salt.utils.cloud.os_script(
config.get_cloud_config_value('os', vm_, __opts__),
vm_,
__opts__,
salt.utils.cloud.salt_config_to_yaml(
salt.utils.cloud.minion_config(__opts__, vm_)
)
)
)
def destroy(name, conn=None, call=None):
'''
Delete a single VM
'''
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']
)
if not conn:
conn = get_conn() # pylint: disable=E0602
node = get_node(conn, name)
profiles = get_configured_provider()['profiles'] # pylint: disable=E0602
if node is None:
log.error('Unable to find the VM %s', name)
profile = None
if 'metadata' in node.extra and 'profile' in node.extra['metadata']:
profile = node.extra['metadata']['profile']
flush_mine_on_destroy = False
if profile and profile in profiles and 'flush_mine_on_destroy' in profiles[profile]:
flush_mine_on_destroy = profiles[profile]['flush_mine_on_destroy']
if flush_mine_on_destroy:
log.info('Clearing Salt Mine: %s', name)
mopts_ = salt.config.DEFAULT_MINION_OPTS
conf_path = '/'.join(__opts__['conf_file'].split('/')[:-1])
mopts_.update(
salt.config.minion_config(os.path.join(conf_path, 'minion'))
)
client = salt.client.get_local_client(mopts_)
minions = client.cmd(name, 'mine.flush')
log.info('Clearing Salt Mine: %s, %s', name, flush_mine_on_destroy)
log.info('Destroying VM: %s', name)
ret = conn.destroy_node(node)
if ret:
log.info('Destroyed VM: %s', name)
# Fire destroy action
__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__['delete_sshkeys'] is True:
public_ips = getattr(node, __opts__.get('ssh_interface', 'public_ips'))
if public_ips:
salt.utils.cloud.remove_sshkey(public_ips[0])
private_ips = getattr(node, __opts__.get('ssh_interface', 'private_ips'))
if private_ips:
salt.utils.cloud.remove_sshkey(private_ips[0])
if __opts__.get('update_cachedir', False) is True:
__utils__['cloud.delete_minion_cachedir'](name, __active_provider_name__.split(':')[0], __opts__)
return True
log.error('Failed to Destroy VM: %s', name)
return False
def reboot(name, conn=None):
'''
Reboot a single VM
'''
if not conn:
conn = get_conn() # pylint: disable=E0602
node = get_node(conn, name)
if node is None:
log.error('Unable to find the VM %s', name)
log.info('Rebooting VM: %s', name)
ret = conn.reboot_node(node)
if ret:
log.info('Rebooted VM: %s', name)
# Fire reboot action
__utils__['cloud.fire_event'](
'event',
'{0} has been rebooted'.format(name), 'salt-cloud'
'salt/cloud/{0}/rebooting'.format(name),
args={'name': name},
sock_dir=__opts__['sock_dir'],
transport=__opts__['transport']
)
return True
log.error('Failed to reboot VM: %s', name)
return False
def list_nodes(conn=None, 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.'
)
if not conn:
conn = get_conn() # pylint: disable=E0602
nodes = conn.list_nodes()
ret = {}
for node in nodes:
ret[node.name] = {
'id': node.id,
'image': node.image,
'name': node.name,
'private_ips': node.private_ips,
'public_ips': node.public_ips,
'size': node.size,
'state': node_state(node.state)
}
return ret
def list_nodes_full(conn=None, call=None):
'''
Return a list of the VMs that are on the provider, with all fields
'''
if call == 'action':
raise SaltCloudSystemExit(
'The list_nodes_full function must be called with -f or --function.'
)
if not conn:
conn = get_conn() # pylint: disable=E0602
nodes = conn.list_nodes()
ret = {}
for node in nodes:
pairs = {}
for key, value in zip(node.__dict__, six.itervalues(node.__dict__)):
pairs[key] = value
ret[node.name] = pairs
del ret[node.name]['driver']
__utils__['cloud.cache_node_list'](ret, __active_provider_name__.split(':')[0], __opts__)
return ret
def list_nodes_select(conn=None, call=None):
'''
Return a list of the VMs that are on the provider, with select fields
'''
if not conn:
conn = get_conn() # pylint: disable=E0602
return salt.utils.cloud.list_nodes_select(
list_nodes_full(conn, 'function'), __opts__['query.selection'], call,
)
def show_instance(name, call=None):
'''
Show the details from the provider concerning an instance
'''
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 conn_has_method(conn, method_name):
'''
Find if the provided connection object has a specific method
'''
if method_name in dir(conn):
return True
log.error('Method \'%s\' not yet supported!', method_name)
return False