salt/cloud/clouds/vultrpy.py
# -*- coding: utf-8 -*-
'''
Vultr Cloud Module using python-vultr bindings
==============================================
.. versionadded:: 2016.3.0
The Vultr cloud module is used to control access to the Vultr VPS system.
Use of this module only requires the ``api_key`` parameter.
Set up the cloud configuration at ``/etc/salt/cloud.providers`` or
``/etc/salt/cloud.providers.d/vultr.conf``:
.. code-block:: yaml
my-vultr-config:
# Vultr account api key
api_key: <supersecretapi_key>
driver: vultr
Set up the cloud profile at ``/etc/salt/cloud.profiles`` or
``/etc/salt/cloud.profiles.d/vultr.conf``:
.. code-block:: yaml
nyc-4gb-4cpu-ubuntu-14-04:
location: 1
provider: my-vultr-config
image: 160
size: 95
enable_private_network: True
This driver also supports Vultr's `startup script` feature. You can list startup
scripts in your account with
.. code-block:: bash
salt-cloud -f list_scripts <name of vultr provider>
That list will include the IDs of the scripts in your account. Thus, if you
have a script called 'setup-networking' with an ID of 493234 you can specify
that startup script in a profile like so:
.. code-block:: yaml
nyc-2gb-1cpu-ubuntu-17-04:
location: 1
provider: my-vultr-config
image: 223
size: 13
startup_script_id: 493234
'''
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import pprint
import logging
import time
# Import salt libs
import salt.config as config
from salt.ext import six
from salt.ext.six.moves.urllib.parse import urlencode as _urlencode # pylint: disable=E0611
from salt.exceptions import (
SaltCloudConfigError,
SaltCloudSystemExit
)
# Get logging started
log = logging.getLogger(__name__)
__virtualname__ = 'vultr'
DETAILS = {}
def __virtual__():
'''
Set up the Vultr functions and check for 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 'vultr',
('api_key',)
)
def _cache_provider_details(conn=None):
'''
Provide a place to hang onto results of --list-[locations|sizes|images]
so we don't have to go out to the API and get them every time.
'''
DETAILS['avail_locations'] = {}
DETAILS['avail_sizes'] = {}
DETAILS['avail_images'] = {}
locations = avail_locations(conn)
images = avail_images(conn)
sizes = avail_sizes(conn)
for key, location in six.iteritems(locations):
DETAILS['avail_locations'][location['name']] = location
DETAILS['avail_locations'][key] = location
for key, image in six.iteritems(images):
DETAILS['avail_images'][image['name']] = image
DETAILS['avail_images'][key] = image
for key, vm_size in six.iteritems(sizes):
DETAILS['avail_sizes'][vm_size['name']] = vm_size
DETAILS['avail_sizes'][key] = vm_size
def avail_locations(conn=None):
'''
return available datacenter locations
'''
return _query('regions/list')
def avail_scripts(conn=None):
'''
return available startup scripts
'''
return _query('startupscript/list')
def list_scripts(conn=None, call=None):
'''
return list of Startup Scripts
'''
return avail_scripts()
def avail_sizes(conn=None):
'''
Return available sizes ("plans" in VultrSpeak)
'''
return _query('plans/list')
def avail_images(conn=None):
'''
Return available images
'''
return _query('os/list')
def list_nodes(**kwargs):
'''
Return basic data on nodes
'''
ret = {}
nodes = list_nodes_full()
for node in nodes:
ret[node] = {}
for prop in 'id', 'image', 'size', 'state', 'private_ips', 'public_ips':
ret[node][prop] = nodes[node][prop]
return ret
def list_nodes_full(**kwargs):
'''
Return all data on nodes
'''
nodes = _query('server/list')
ret = {}
for node in nodes:
name = nodes[node]['label']
ret[name] = nodes[node].copy()
ret[name]['id'] = node
ret[name]['image'] = nodes[node]['os']
ret[name]['size'] = nodes[node]['VPSPLANID']
ret[name]['state'] = nodes[node]['status']
ret[name]['private_ips'] = nodes[node]['internal_ip']
ret[name]['public_ips'] = nodes[node]['main_ip']
return ret
def list_nodes_select(conn=None, call=None):
'''
Return a list of the VMs that are on the provider, with select fields
'''
return __utils__['cloud.list_nodes_select'](
list_nodes_full(), __opts__['query.selection'], call,
)
def destroy(name):
'''
Remove a node from Vultr
'''
node = show_instance(name, call='action')
params = {'SUBID': node['SUBID']}
result = _query('server/destroy', method='POST', decode=False, data=_urlencode(params))
# The return of a destroy call is empty in the case of a success.
# Errors are only indicated via HTTP status code. Status code 200
# effetively therefore means "success".
if result.get('body') == '' and result.get('text') == '':
return True
return result
def stop(*args, **kwargs):
'''
Execute a "stop" action on a VM
'''
return _query('server/halt')
def start(*args, **kwargs):
'''
Execute a "start" action on a VM
'''
return _query('server/start')
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()
# Find under which cloud service the name is listed, if any
if name not in nodes:
return {}
__utils__['cloud.cache_node'](nodes[name], __active_provider_name__, __opts__)
return nodes[name]
def _lookup_vultrid(which_key, availkey, keyname):
'''
Helper function to retrieve a Vultr ID
'''
if DETAILS == {}:
_cache_provider_details()
which_key = six.text_type(which_key)
try:
return DETAILS[availkey][which_key][keyname]
except KeyError:
return False
def create(vm_):
'''
Create a single VM from a data dict
'''
if 'driver' not in vm_:
vm_['driver'] = vm_['provider']
private_networking = config.get_cloud_config_value(
'enable_private_network', vm_, __opts__, search_global=False, default=False,
)
startup_script = config.get_cloud_config_value(
'startup_script_id', vm_, __opts__, search_global=False, default=None,
)
if startup_script and str(startup_script) not in avail_scripts():
log.error('Your Vultr account does not have a startup script with ID %s', str(startup_script))
return False
if private_networking is not None:
if not isinstance(private_networking, bool):
raise SaltCloudConfigError("'private_networking' should be a boolean value.")
if private_networking is True:
enable_private_network = 'yes'
else:
enable_private_network = 'no'
__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']
)
osid = _lookup_vultrid(vm_['image'], 'avail_images', 'OSID')
if not osid:
log.error('Vultr does not have an image with id or name %s', vm_['image'])
return False
vpsplanid = _lookup_vultrid(vm_['size'], 'avail_sizes', 'VPSPLANID')
if not vpsplanid:
log.error('Vultr does not have a size with id or name %s', vm_['size'])
return False
dcid = _lookup_vultrid(vm_['location'], 'avail_locations', 'DCID')
if not dcid:
log.error('Vultr does not have a location with id or name %s', vm_['location'])
return False
kwargs = {
'label': vm_['name'],
'OSID': osid,
'VPSPLANID': vpsplanid,
'DCID': dcid,
'hostname': vm_['name'],
'enable_private_network': enable_private_network,
}
if startup_script:
kwargs['SCRIPTID'] = startup_script
log.info('Creating Cloud VM %s', vm_['name'])
__utils__['cloud.fire_event'](
'event',
'requesting instance',
'salt/cloud/{0}/requesting'.format(vm_['name']),
args={
'kwargs': __utils__['cloud.filter_event']('requesting', kwargs, list(kwargs)),
},
sock_dir=__opts__['sock_dir'],
transport=__opts__['transport'],
)
try:
data = _query('server/create', method='POST', data=_urlencode(kwargs))
if int(data.get('status', '200')) >= 300:
log.error(
'Error creating %s on Vultr\n\n'
'Vultr API returned %s\n', vm_['name'], data
)
log.error('Status 412 may mean that you are requesting an\n'
'invalid location, image, or size.')
__utils__['cloud.fire_event'](
'event',
'instance request failed',
'salt/cloud/{0}/requesting/failed'.format(vm_['name']),
args={'kwargs': kwargs},
sock_dir=__opts__['sock_dir'],
transport=__opts__['transport'],
)
return False
except Exception as exc:
log.error(
'Error creating %s on Vultr\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
)
__utils__['cloud.fire_event'](
'event',
'instance request failed',
'salt/cloud/{0}/requesting/failed'.format(vm_['name']),
args={'kwargs': kwargs},
sock_dir=__opts__['sock_dir'],
transport=__opts__['transport'],
)
return False
def wait_for_hostname():
'''
Wait for the IP address to become available
'''
data = show_instance(vm_['name'], call='action')
main_ip = six.text_type(data.get('main_ip', '0'))
if main_ip.startswith('0'):
time.sleep(3)
return False
return data['main_ip']
def wait_for_default_password():
'''
Wait for the IP address to become available
'''
data = show_instance(vm_['name'], call='action')
# print("Waiting for default password")
# pprint.pprint(data)
if six.text_type(data.get('default_password', '')) == '':
time.sleep(1)
return False
return data['default_password']
def wait_for_status():
'''
Wait for the IP address to become available
'''
data = show_instance(vm_['name'], call='action')
# print("Waiting for status normal")
# pprint.pprint(data)
if six.text_type(data.get('status', '')) != 'active':
time.sleep(1)
return False
return data['default_password']
def wait_for_server_state():
'''
Wait for the IP address to become available
'''
data = show_instance(vm_['name'], call='action')
# print("Waiting for server state ok")
# pprint.pprint(data)
if six.text_type(data.get('server_state', '')) != 'ok':
time.sleep(1)
return False
return data['default_password']
vm_['ssh_host'] = __utils__['cloud.wait_for_fun'](
wait_for_hostname,
timeout=config.get_cloud_config_value(
'wait_for_fun_timeout', vm_, __opts__, default=15 * 60),
)
vm_['password'] = __utils__['cloud.wait_for_fun'](
wait_for_default_password,
timeout=config.get_cloud_config_value(
'wait_for_fun_timeout', vm_, __opts__, default=15 * 60),
)
__utils__['cloud.wait_for_fun'](
wait_for_status,
timeout=config.get_cloud_config_value(
'wait_for_fun_timeout', vm_, __opts__, default=15 * 60),
)
__utils__['cloud.wait_for_fun'](
wait_for_server_state,
timeout=config.get_cloud_config_value(
'wait_for_fun_timeout', vm_, __opts__, default=15 * 60),
)
__opts__['hard_timeout'] = config.get_cloud_config_value(
'hard_timeout',
get_configured_provider(),
__opts__,
search_global=False,
default=None,
)
# Bootstrap
ret = __utils__['cloud.bootstrap'](vm_, __opts__)
ret.update(show_instance(vm_['name'], call='action'))
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(path, method='GET', data=None, params=None, header_dict=None, decode=True):
'''
Perform a query directly against the Vultr REST API
'''
api_key = config.get_cloud_config_value(
'api_key',
get_configured_provider(),
__opts__,
search_global=False,
)
management_host = config.get_cloud_config_value(
'management_host',
get_configured_provider(),
__opts__,
search_global=False,
default='api.vultr.com'
)
url = 'https://{management_host}/v1/{path}?api_key={api_key}'.format(
management_host=management_host,
path=path,
api_key=api_key,
)
if header_dict is None:
header_dict = {}
result = __utils__['http.query'](
url,
method=method,
params=params,
data=data,
header_dict=header_dict,
port=443,
text=True,
decode=decode,
decode_type='json',
hide_fields=['api_key'],
opts=__opts__,
)
if 'dict' in result:
return result['dict']
return result