salt/cloud/clouds/linode.py
# -*- coding: utf-8 -*-
'''
Linode Cloud Module using Linode's REST API
===========================================
The Linode cloud module is used to control access to the Linode VPS system.
Use of this module only requires the ``apikey`` parameter. However, the default root password for new instances
also needs to be set. The password needs to be 8 characters and contain lowercase, uppercase, and numbers.
Set up the cloud configuration at ``/etc/salt/cloud.providers`` or ``/etc/salt/cloud.providers.d/linode.conf``:
.. code-block:: yaml
my-linode-provider:
apikey: f4ZsmwtB1c7f85Jdu43RgXVDFlNjuJaeIYV8QMftTqKScEB2vSosFSr...
password: F00barbaz
driver: linode
linode-profile:
provider: my-linode-provider
size: Linode 1024
image: CentOS 7
location: London, England, UK
'''
# Import Python Libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import pprint
import re
import time
import datetime
# Import Salt Libs
import salt.config as config
from salt.ext import six
from salt.ext.six.moves import range
from salt.exceptions import (
SaltCloudConfigError,
SaltCloudException,
SaltCloudNotFound,
SaltCloudSystemExit
)
# Get logging started
log = logging.getLogger(__name__)
# The epoch of the last time a query was made
LASTCALL = int(time.mktime(datetime.datetime.now().timetuple()))
# Human-readable status fields (documentation: https://www.linode.com/api/linode/linode.list)
LINODE_STATUS = {
'boot_failed': {
'code': -2,
'descr': 'Boot Failed (not in use)',
},
'beeing_created': {
'code': -1,
'descr': 'Being Created',
},
'brand_new': {
'code': 0,
'descr': 'Brand New',
},
'running': {
'code': 1,
'descr': 'Running',
},
'poweroff': {
'code': 2,
'descr': 'Powered Off',
},
'shutdown': {
'code': 3,
'descr': 'Shutting Down (not in use)',
},
'save_to_disk': {
'code': 4,
'descr': 'Saved to Disk (not in use)',
},
}
__virtualname__ = 'linode'
# Only load in this module if the Linode configurations are in place
def __virtual__():
'''
Check for Linode configs.
'''
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__,
('apikey', 'password',)
)
def avail_images(call=None):
'''
Return available Linode images.
CLI Example:
.. code-block:: bash
salt-cloud --list-images my-linode-config
salt-cloud -f avail_images my-linode-config
'''
if call == 'action':
raise SaltCloudException(
'The avail_images function must be called with -f or --function.'
)
response = _query('avail', 'distributions')
ret = {}
for item in response['DATA']:
name = item['LABEL']
ret[name] = item
return ret
def avail_locations(call=None):
'''
Return available Linode datacenter locations.
CLI Example:
.. code-block:: bash
salt-cloud --list-locations my-linode-config
salt-cloud -f avail_locations my-linode-config
'''
if call == 'action':
raise SaltCloudException(
'The avail_locations function must be called with -f or --function.'
)
response = _query('avail', 'datacenters')
ret = {}
for item in response['DATA']:
name = item['LOCATION']
ret[name] = item
return ret
def avail_sizes(call=None):
'''
Return available Linode sizes.
CLI Example:
.. code-block:: bash
salt-cloud --list-sizes my-linode-config
salt-cloud -f avail_sizes my-linode-config
'''
if call == 'action':
raise SaltCloudException(
'The avail_locations function must be called with -f or --function.'
)
response = _query('avail', 'LinodePlans')
ret = {}
for item in response['DATA']:
name = item['LABEL']
ret[name] = item
return ret
def boot(name=None, kwargs=None, call=None):
'''
Boot a Linode.
name
The name of the Linode to boot. Can be used instead of ``linode_id``.
linode_id
The ID of the Linode to boot. If provided, will be used as an
alternative to ``name`` and reduces the number of API calls to
Linode by one. Will be preferred over ``name``.
config_id
The ID of the Config to boot. Required.
check_running
Defaults to True. If set to False, overrides the call to check if
the VM is running before calling the linode.boot API call. Change
``check_running`` to True is useful during the boot call in the
create function, since the new VM will not be running yet.
Can be called as an action (which requires a name):
.. code-block:: bash
salt-cloud -a boot my-instance config_id=10
...or as a function (which requires either a name or linode_id):
.. code-block:: bash
salt-cloud -f boot my-linode-config name=my-instance config_id=10
salt-cloud -f boot my-linode-config linode_id=1225876 config_id=10
'''
if name is None and call == 'action':
raise SaltCloudSystemExit(
'The boot action requires a \'name\'.'
)
if kwargs is None:
kwargs = {}
linode_id = kwargs.get('linode_id', None)
config_id = kwargs.get('config_id', None)
check_running = kwargs.get('check_running', True)
if call == 'function':
name = kwargs.get('name', None)
if name is None and linode_id is None:
raise SaltCloudSystemExit(
'The boot function requires either a \'name\' or a \'linode_id\'.'
)
if config_id is None:
raise SaltCloudSystemExit(
'The boot function requires a \'config_id\'.'
)
if linode_id is None:
linode_id = get_linode_id_from_name(name)
linode_item = name
else:
linode_item = linode_id
# Check if Linode is running first
if check_running is True:
status = get_linode(kwargs={'linode_id': linode_id})['STATUS']
if status == '1':
raise SaltCloudSystemExit(
'Cannot boot Linode {0}. '
'Linode {0} is already running.'.format(linode_item)
)
# Boot the VM and get the JobID from Linode
response = _query('linode', 'boot',
args={'LinodeID': linode_id,
'ConfigID': config_id})['DATA']
boot_job_id = response['JobID']
if not _wait_for_job(linode_id, boot_job_id):
log.error('Boot failed for Linode %s.', linode_item)
return False
return True
def clone(kwargs=None, call=None):
'''
Clone a Linode.
linode_id
The ID of the Linode to clone. Required.
datacenter_id
The ID of the Datacenter where the Linode will be placed. Required.
plan_id
The ID of the plan (size) of the Linode. Required.
CLI Example:
.. code-block:: bash
salt-cloud -f clone my-linode-config linode_id=1234567 datacenter_id=2 plan_id=5
'''
if call == 'action':
raise SaltCloudSystemExit(
'The clone function must be called with -f or --function.'
)
if kwargs is None:
kwargs = {}
linode_id = kwargs.get('linode_id', None)
datacenter_id = kwargs.get('datacenter_id', None)
plan_id = kwargs.get('plan_id', None)
required_params = [linode_id, datacenter_id, plan_id]
for item in required_params:
if item is None:
raise SaltCloudSystemExit(
'The clone function requires a \'linode_id\', \'datacenter_id\', '
'and \'plan_id\' to be provided.'
)
clone_args = {
'LinodeID': linode_id,
'DatacenterID': datacenter_id,
'PlanID': plan_id
}
return _query('linode', 'clone', args=clone_args)
def create(vm_):
'''
Create a single Linode VM.
'''
name = vm_['name']
try:
# Check for required profile parameters before sending any API calls.
if vm_['profile'] and config.is_profile_configured(__opts__,
__active_provider_name__ or 'linode',
vm_['profile'],
vm_=vm_) is False:
return False
except AttributeError:
pass
if _validate_name(name) is False:
return False
__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)
data = {}
kwargs = {'name': name}
plan_id = None
size = vm_.get('size')
if size:
kwargs['size'] = size
plan_id = get_plan_id(kwargs={'label': size})
datacenter_id = None
location = vm_.get('location')
if location:
try:
datacenter_id = get_datacenter_id(location)
except KeyError:
# Linode's default datacenter is Dallas, but we still have to set one to
# use the create function from Linode's API. Dallas's datacenter id is 2.
datacenter_id = 2
clonefrom_name = vm_.get('clonefrom')
cloning = True if clonefrom_name else False
if cloning:
linode_id = get_linode_id_from_name(clonefrom_name)
clone_source = get_linode(kwargs={'linode_id': linode_id})
kwargs = {
'clonefrom': clonefrom_name,
'image': 'Clone of {0}'.format(clonefrom_name),
}
if size is None:
size = clone_source['TOTALRAM']
kwargs['size'] = size
plan_id = clone_source['PLANID']
if location is None:
datacenter_id = clone_source['DATACENTERID']
# Create new Linode from cloned Linode
try:
result = clone(kwargs={'linode_id': linode_id,
'datacenter_id': datacenter_id,
'plan_id': plan_id})
except Exception as err:
log.error(
'Error cloning \'%s\' on Linode.\n\n'
'The following exception was thrown by Linode when trying to '
'clone the specified machine:\n%s',
clonefrom_name, err, exc_info_on_loglevel=logging.DEBUG
)
return False
else:
kwargs['image'] = vm_['image']
# Create Linode
try:
result = _query('linode', 'create', args={
'PLANID': plan_id,
'DATACENTERID': datacenter_id
})
except Exception as err:
log.error(
'Error creating %s on Linode\n\n'
'The following exception was thrown by Linode when trying to '
'run the initial deployment:\n%s',
name, err, exc_info_on_loglevel=logging.DEBUG
)
return False
if 'ERRORARRAY' in result:
for error_data in result['ERRORARRAY']:
log.error(
'Error creating %s on Linode\n\n'
'The Linode API returned the following: %s\n',
name, error_data['ERRORMESSAGE']
)
return False
__utils__['cloud.fire_event'](
'event',
'requesting instance',
'salt/cloud/{0}/requesting'.format(name),
args=__utils__['cloud.filter_event']('requesting', vm_, ['name', 'profile', 'provider', 'driver']),
sock_dir=__opts__['sock_dir'],
transport=__opts__['transport']
)
node_id = _clean_data(result)['LinodeID']
data['id'] = node_id
if not _wait_for_status(node_id, status=(_get_status_id_by_name('brand_new'))):
log.error(
'Error creating %s on LINODE\n\n'
'while waiting for initial ready status',
name, exc_info_on_loglevel=logging.DEBUG
)
# Update the Linode's Label to reflect the given VM name
update_linode(node_id, update_args={'Label': name})
log.debug('Set name for %s - was linode%s.', name, node_id)
# Add private IP address if requested
private_ip_assignment = get_private_ip(vm_)
if private_ip_assignment:
create_private_ip(node_id)
# Define which ssh_interface to use
ssh_interface = _get_ssh_interface(vm_)
# If ssh_interface is set to use private_ips, but assign_private_ip
# wasn't set to True, let's help out and create a private ip.
if ssh_interface == 'private_ips' and private_ip_assignment is False:
create_private_ip(node_id)
private_ip_assignment = True
if cloning:
config_id = get_config_id(kwargs={'linode_id': node_id})['config_id']
else:
# Create disks and get ids
log.debug('Creating disks for %s', name)
root_disk_id = create_disk_from_distro(vm_, node_id)['DiskID']
swap_disk_id = create_swap_disk(vm_, node_id)['DiskID']
# Create a ConfigID using disk ids
config_id = create_config(kwargs={'name': name,
'linode_id': node_id,
'root_disk_id': root_disk_id,
'swap_disk_id': swap_disk_id})['ConfigID']
# Boot the Linode
boot(kwargs={'linode_id': node_id,
'config_id': config_id,
'check_running': False})
node_data = get_linode(kwargs={'linode_id': node_id})
ips = get_ips(node_id)
state = int(node_data['STATUS'])
data['image'] = kwargs['image']
data['name'] = name
data['size'] = size
data['state'] = _get_status_descr_by_id(state)
data['private_ips'] = ips['private_ips']
data['public_ips'] = ips['public_ips']
# Pass the correct IP address to the bootstrap ssh_host key
if ssh_interface == 'private_ips':
vm_['ssh_host'] = data['private_ips'][0]
else:
vm_['ssh_host'] = data['public_ips'][0]
# If a password wasn't supplied in the profile or provider config, set it now.
vm_['password'] = get_password(vm_)
# Make public_ips and private_ips available to the bootstrap script.
vm_['public_ips'] = ips['public_ips']
vm_['private_ips'] = ips['private_ips']
# Send event that the instance has booted.
__utils__['cloud.fire_event'](
'event',
'waiting for ssh',
'salt/cloud/{0}/waiting_for_ssh'.format(name),
sock_dir=__opts__['sock_dir'],
args={'ip_address': vm_['ssh_host']},
transport=__opts__['transport']
)
# Bootstrap!
ret = __utils__['cloud.bootstrap'](vm_, __opts__)
ret.update(data)
log.info('Created Cloud VM \'%s\'', name)
log.debug('\'%s\' VM creation details:\n%s', name, pprint.pformat(data))
__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 create_config(kwargs=None, call=None):
'''
Creates a Linode Configuration Profile.
name
The name of the VM to create the config for.
linode_id
The ID of the Linode to create the configuration for.
root_disk_id
The Root Disk ID to be used for this config.
swap_disk_id
The Swap Disk ID to be used for this config.
data_disk_id
The Data Disk ID to be used for this config.
.. versionadded:: 2016.3.0
kernel_id
The ID of the kernel to use for this configuration profile.
'''
if call == 'action':
raise SaltCloudSystemExit(
'The create_config function must be called with -f or --function.'
)
if kwargs is None:
kwargs = {}
name = kwargs.get('name', None)
linode_id = kwargs.get('linode_id', None)
root_disk_id = kwargs.get('root_disk_id', None)
swap_disk_id = kwargs.get('swap_disk_id', None)
data_disk_id = kwargs.get('data_disk_id', None)
kernel_id = kwargs.get('kernel_id', None)
if kernel_id is None:
# 138 appears to always be the latest 64-bit kernel for Linux
kernel_id = 138
required_params = [name, linode_id, root_disk_id, swap_disk_id]
for item in required_params:
if item is None:
raise SaltCloudSystemExit(
'The create_config functions requires a \'name\', \'linode_id\', '
'\'root_disk_id\', and \'swap_disk_id\'.'
)
disklist = '{0},{1}'.format(root_disk_id, swap_disk_id)
if data_disk_id is not None:
disklist = '{0},{1},{2}'.format(root_disk_id, swap_disk_id, data_disk_id)
config_args = {'LinodeID': linode_id,
'KernelID': kernel_id,
'Label': name,
'DiskList': disklist
}
result = _query('linode', 'config.create', args=config_args)
return _clean_data(result)
def create_disk_from_distro(vm_, linode_id, swap_size=None):
r'''
Creates the disk for the Linode from the distribution.
vm\_
The VM profile to create the disk for.
linode_id
The ID of the Linode to create the distribution disk for. Required.
swap_size
The size of the disk, in MB.
'''
kwargs = {}
if swap_size is None:
swap_size = get_swap_size(vm_)
pub_key = get_pub_key(vm_)
root_password = get_password(vm_)
if pub_key:
kwargs.update({'rootSSHKey': pub_key})
if root_password:
kwargs.update({'rootPass': root_password})
else:
raise SaltCloudConfigError(
'The Linode driver requires a password.'
)
kwargs.update({'LinodeID': linode_id,
'DistributionID': get_distribution_id(vm_),
'Label': vm_['name'],
'Size': get_disk_size(vm_, swap_size, linode_id)})
result = _query('linode', 'disk.createfromdistribution', args=kwargs)
return _clean_data(result)
def create_swap_disk(vm_, linode_id, swap_size=None):
r'''
Creates the disk for the specified Linode.
vm\_
The VM profile to create the swap disk for.
linode_id
The ID of the Linode to create the swap disk for.
swap_size
The size of the disk, in MB.
'''
kwargs = {}
if not swap_size:
swap_size = get_swap_size(vm_)
kwargs.update({'LinodeID': linode_id,
'Label': vm_['name'],
'Type': 'swap',
'Size': swap_size
})
result = _query('linode', 'disk.create', args=kwargs)
return _clean_data(result)
def create_data_disk(vm_=None, linode_id=None, data_size=None):
r'''
Create a data disk for the linode (type is hardcoded to ext4 at the moment)
.. versionadded:: 2016.3.0
vm\_
The VM profile to create the data disk for.
linode_id
The ID of the Linode to create the data disk for.
data_size
The size of the disk, in MB.
'''
kwargs = {}
kwargs.update({'LinodeID': linode_id,
'Label': vm_['name']+"_data",
'Type': 'ext4',
'Size': data_size
})
result = _query('linode', 'disk.create', args=kwargs)
return _clean_data(result)
def create_private_ip(linode_id):
r'''
Creates a private IP for the specified Linode.
linode_id
The ID of the Linode to create the IP address for.
'''
kwargs = {'LinodeID': linode_id}
result = _query('linode', 'ip.addprivate', args=kwargs)
return _clean_data(result)
def destroy(name, call=None):
'''
Destroys a Linode by name.
name
The name of VM to be be destroyed.
CLI Example:
.. code-block:: bash
salt-cloud -d vm_name
'''
if call == 'function':
raise SaltCloudException(
'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']
)
linode_id = get_linode_id_from_name(name)
response = _query('linode', 'delete', args={'LinodeID': linode_id, 'skipChecks': True})
__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 get_config_id(kwargs=None, call=None):
'''
Returns a config_id for a given linode.
.. versionadded:: 2015.8.0
name
The name of the Linode for which to get the config_id. Can be used instead
of ``linode_id``.h
linode_id
The ID of the Linode for which to get the config_id. Can be used instead
of ``name``.
CLI Example:
.. code-block:: bash
salt-cloud -f get_config_id my-linode-config name=my-linode
salt-cloud -f get_config_id my-linode-config linode_id=1234567
'''
if call == 'action':
raise SaltCloudException(
'The get_config_id function must be called with -f or --function.'
)
if kwargs is None:
kwargs = {}
name = kwargs.get('name', None)
linode_id = kwargs.get('linode_id', None)
if name is None and linode_id is None:
raise SaltCloudSystemExit(
'The get_config_id function requires either a \'name\' or a \'linode_id\' '
'to be provided.'
)
if linode_id is None:
linode_id = get_linode_id_from_name(name)
response = _query('linode', 'config.list', args={'LinodeID': linode_id})['DATA']
config_id = {'config_id': response[0]['ConfigID']}
return config_id
def get_datacenter_id(location):
'''
Returns the Linode Datacenter ID.
location
The location, or name, of the datacenter to get the ID from.
'''
return avail_locations()[location]['DATACENTERID']
def get_disk_size(vm_, swap, linode_id):
r'''
Returns the size of of the root disk in MB.
vm\_
The VM to get the disk size for.
'''
disk_size = get_linode(kwargs={'linode_id': linode_id})['TOTALHD']
return config.get_cloud_config_value(
'disk_size', vm_, __opts__, default=disk_size - swap
)
def get_data_disk_size(vm_, swap, linode_id):
'''
Return the size of of the data disk in MB
.. versionadded:: 2016.3.0
'''
disk_size = get_linode(kwargs={'linode_id': linode_id})['TOTALHD']
root_disk_size = config.get_cloud_config_value(
'disk_size', vm_, __opts__, default=disk_size - swap
)
return disk_size - root_disk_size - swap
def get_distribution_id(vm_):
r'''
Returns the distribution ID for a VM
vm\_
The VM to get the distribution ID for
'''
distributions = _query('avail', 'distributions')['DATA']
vm_image_name = config.get_cloud_config_value('image', vm_, __opts__)
distro_id = ''
for distro in distributions:
if vm_image_name == distro['LABEL']:
distro_id = distro['DISTRIBUTIONID']
return distro_id
if not distro_id:
raise SaltCloudNotFound(
'The DistributionID for the \'{0}\' profile could not be found.\n'
'The \'{1}\' instance could not be provisioned. The following distributions '
'are available:\n{2}'.format(
vm_image_name,
vm_['name'],
pprint.pprint(sorted([distro['LABEL'].encode(__salt_system_encoding__) for distro in distributions]))
)
)
def get_ips(linode_id=None):
'''
Returns public and private IP addresses.
linode_id
Limits the IP addresses returned to the specified Linode ID.
'''
if linode_id:
ips = _query('linode', 'ip.list', args={'LinodeID': linode_id})
else:
ips = _query('linode', 'ip.list')
ips = ips['DATA']
ret = {}
for item in ips:
node_id = six.text_type(item['LINODEID'])
if item['ISPUBLIC'] == 1:
key = 'public_ips'
else:
key = 'private_ips'
if ret.get(node_id) is None:
ret.update({node_id: {'public_ips': [], 'private_ips': []}})
ret[node_id][key].append(item['IPADDRESS'])
# If linode_id was specified, only return the ips, and not the
# dictionary based on the linode ID as a key.
if linode_id:
_all_ips = {'public_ips': [], 'private_ips': []}
matching_id = ret.get(six.text_type(linode_id))
if matching_id:
_all_ips['private_ips'] = matching_id['private_ips']
_all_ips['public_ips'] = matching_id['public_ips']
ret = _all_ips
return ret
def get_linode(kwargs=None, call=None):
'''
Returns data for a single named Linode.
name
The name of the Linode for which to get data. Can be used instead
``linode_id``. Note this will induce an additional API call
compared to using ``linode_id``.
linode_id
The ID of the Linode for which to get data. Can be used instead of
``name``.
CLI Example:
.. code-block:: bash
salt-cloud -f get_linode my-linode-config name=my-instance
salt-cloud -f get_linode my-linode-config linode_id=1234567
'''
if call == 'action':
raise SaltCloudSystemExit(
'The get_linode function must be called with -f or --function.'
)
if kwargs is None:
kwargs = {}
name = kwargs.get('name', None)
linode_id = kwargs.get('linode_id', None)
if name is None and linode_id is None:
raise SaltCloudSystemExit(
'The get_linode function requires either a \'name\' or a \'linode_id\'.'
)
if linode_id is None:
linode_id = get_linode_id_from_name(name)
result = _query('linode', 'list', args={'LinodeID': linode_id})
return result['DATA'][0]
def get_linode_id_from_name(name):
'''
Returns the Linode ID for a VM from the provided name.
name
The name of the Linode from which to get the Linode ID. Required.
'''
nodes = _query('linode', 'list')['DATA']
linode_id = ''
for node in nodes:
if name == node['LABEL']:
linode_id = node['LINODEID']
return linode_id
if not linode_id:
raise SaltCloudNotFound(
'The specified name, {0}, could not be found.'.format(name)
)
def get_password(vm_):
r'''
Return the password to use for a VM.
vm\_
The configuration to obtain the password from.
'''
return config.get_cloud_config_value(
'password', vm_, __opts__,
default=config.get_cloud_config_value(
'passwd', vm_, __opts__,
search_global=False
),
search_global=False
)
def _decode_linode_plan_label(label):
'''
Attempts to decode a user-supplied Linode plan label
into the format in Linode API output
label
The label, or name, of the plan to decode.
Example:
`Linode 2048` will decode to `Linode 2GB`
'''
sizes = avail_sizes()
if label not in sizes:
if 'GB' in label:
raise SaltCloudException(
'Invalid Linode plan ({}) specified - call avail_sizes() for all available options'.format(label)
)
else:
plan = label.split()
if len(plan) != 2:
raise SaltCloudException(
'Invalid Linode plan ({}) specified - call avail_sizes() for all available options'.format(label)
)
plan_type = plan[0]
try:
plan_size = int(plan[1])
except TypeError:
plan_size = 0
log.debug('Failed to decode Linode plan label in Cloud Profile: %s', label)
if plan_type == 'Linode' and plan_size == 1024:
plan_type = 'Nanode'
plan_size = plan_size/1024
new_label = "{} {}GB".format(plan_type, plan_size)
if new_label not in sizes:
raise SaltCloudException(
'Invalid Linode plan ({}) specified - call avail_sizes() for all available options'.format(new_label)
)
log.warning(
'An outdated Linode plan label was detected in your Cloud '
'Profile (%s). Please update the profile to use the new '
'label format (%s) for the requested Linode plan size.',
label, new_label
)
label = new_label
return sizes[label]['PLANID']
def get_plan_id(kwargs=None, call=None):
'''
Returns the Linode Plan ID.
label
The label, or name, of the plan to get the ID from.
CLI Example:
.. code-block:: bash
salt-cloud -f get_plan_id linode label="Linode 1024"
'''
if call == 'action':
raise SaltCloudException(
'The show_instance action must be called with -f or --function.'
)
if kwargs is None:
kwargs = {}
label = kwargs.get('label', None)
if label is None:
raise SaltCloudException(
'The get_plan_id function requires a \'label\'.'
)
label = _decode_linode_plan_label(label)
return label
def get_private_ip(vm_):
'''
Return True if a private ip address is requested
'''
return config.get_cloud_config_value(
'assign_private_ip', vm_, __opts__, default=False
)
def get_data_disk(vm_):
'''
Return True if a data disk is requested
.. versionadded:: 2016.3.0
'''
return config.get_cloud_config_value(
'allocate_data_disk', vm_, __opts__, default=False
)
def get_pub_key(vm_):
r'''
Return the SSH pubkey.
vm\_
The configuration to obtain the public key from.
'''
return config.get_cloud_config_value(
'ssh_pubkey', vm_, __opts__, search_global=False
)
def get_swap_size(vm_):
r'''
Returns the amoutn of swap space to be used in MB.
vm\_
The VM profile to obtain the swap size from.
'''
return config.get_cloud_config_value(
'swap', vm_, __opts__, default=128
)
def get_vm_size(vm_):
r'''
Returns the VM's size.
vm\_
The VM to get the size for.
'''
vm_size = config.get_cloud_config_value('size', vm_, __opts__)
ram = avail_sizes()[vm_size]['RAM']
if vm_size.startswith('Linode'):
vm_size = vm_size.replace('Linode ', '')
if ram == int(vm_size):
return ram
else:
raise SaltCloudNotFound(
'The specified size, {0}, could not be found.'.format(vm_size)
)
def list_nodes(call=None):
'''
Returns a list of linodes, keeping only a brief listing.
CLI Example:
.. code-block:: bash
salt-cloud -Q
salt-cloud --query
salt-cloud -f list_nodes my-linode-config
.. note::
The ``image`` label only displays information about the VM's distribution vendor,
such as "Debian" or "RHEL" and does not display the actual image name. This is
due to a limitation of the Linode API.
'''
if call == 'action':
raise SaltCloudException(
'The list_nodes function must be called with -f or --function.'
)
return _list_linodes(full=False)
def list_nodes_full(call=None):
'''
List linodes, with all available information.
CLI Example:
.. code-block:: bash
salt-cloud -F
salt-cloud --full-query
salt-cloud -f list_nodes_full my-linode-config
.. note::
The ``image`` label only displays information about the VM's distribution vendor,
such as "Debian" or "RHEL" and does not display the actual image name. This is
due to a limitation of the Linode API.
'''
if call == 'action':
raise SaltCloudException(
'The list_nodes_full function must be called with -f or --function.'
)
return _list_linodes(full=True)
def list_nodes_min(call=None):
'''
Return a list of the VMs that are on the provider. Only a list of VM names and
their state is returned. This is the minimum amount of information needed to
check for existing VMs.
.. versionadded:: 2015.8.0
CLI Example:
.. code-block:: bash
salt-cloud -f list_nodes_min my-linode-config
salt-cloud --function list_nodes_min my-linode-config
'''
if call == 'action':
raise SaltCloudSystemExit(
'The list_nodes_min function must be called with -f or --function.'
)
ret = {}
nodes = _query('linode', 'list')['DATA']
for node in nodes:
name = node['LABEL']
this_node = {
'id': six.text_type(node['LINODEID']),
'state': _get_status_descr_by_id(int(node['STATUS']))
}
ret[name] = this_node
return ret
def list_nodes_select(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 reboot(name, call=None):
'''
Reboot a linode.
.. versionadded:: 2015.8.0
name
The name of the VM to reboot.
CLI Example:
.. code-block:: bash
salt-cloud -a reboot vm_name
'''
if call != 'action':
raise SaltCloudException(
'The show_instance action must be called with -a or --action.'
)
node_id = get_linode_id_from_name(name)
response = _query('linode', 'reboot', args={'LinodeID': node_id})
data = _clean_data(response)
reboot_jid = data['JobID']
if not _wait_for_job(node_id, reboot_jid):
log.error('Reboot failed for %s.', name)
return False
return data
def show_instance(name, call=None):
'''
Displays details about a particular Linode VM. Either a name or a linode_id must
be provided.
.. versionadded:: 2015.8.0
name
The name of the VM for which to display details.
CLI Example:
.. code-block:: bash
salt-cloud -a show_instance vm_name
.. note::
The ``image`` label only displays information about the VM's distribution vendor,
such as "Debian" or "RHEL" and does not display the actual image name. This is
due to a limitation of the Linode API.
'''
if call != 'action':
raise SaltCloudException(
'The show_instance action must be called with -a or --action.'
)
node_id = get_linode_id_from_name(name)
node_data = get_linode(kwargs={'linode_id': node_id})
ips = get_ips(node_id)
state = int(node_data['STATUS'])
ret = {'id': node_data['LINODEID'],
'image': node_data['DISTRIBUTIONVENDOR'],
'name': node_data['LABEL'],
'size': node_data['TOTALRAM'],
'state': _get_status_descr_by_id(state),
'private_ips': ips['private_ips'],
'public_ips': ips['public_ips']}
return ret
def show_pricing(kwargs=None, call=None):
'''
Show pricing for a particular profile. This is only an estimate, based on
unofficial pricing sources.
.. versionadded:: 2015.8.0
CLI Example:
.. code-block:: bash
salt-cloud -f show_pricing my-linode-config profile=my-linode-profile
'''
if call != 'function':
raise SaltCloudException(
'The show_instance action must be called with -f or --function.'
)
profile = __opts__['profiles'].get(kwargs['profile'], {})
if not profile:
raise SaltCloudNotFound(
'The requested profile was not found.'
)
# Make sure the profile belongs to Linode
provider = profile.get('provider', '0:0')
comps = provider.split(':')
if len(comps) < 2 or comps[1] != 'linode':
raise SaltCloudException(
'The requested profile does not belong to Linode.'
)
plan_id = get_plan_id(kwargs={'label': profile['size']})
response = _query('avail', 'linodeplans', args={'PlanID': plan_id})['DATA'][0]
ret = {}
ret['per_hour'] = response['HOURLY']
ret['per_day'] = ret['per_hour'] * 24
ret['per_week'] = ret['per_day'] * 7
ret['per_month'] = response['PRICE']
ret['per_year'] = ret['per_month'] * 12
return {profile['profile']: ret}
def start(name, call=None):
'''
Start a VM in Linode.
name
The name of the VM to start.
CLI Example:
.. code-block:: bash
salt-cloud -a stop vm_name
'''
if call != 'action':
raise SaltCloudException(
'The start action must be called with -a or --action.'
)
node_id = get_linode_id_from_name(name)
node = get_linode(kwargs={'linode_id': node_id})
if node['STATUS'] == 1:
return {'success': True,
'action': 'start',
'state': 'Running',
'msg': 'Machine already running'}
response = _query('linode', 'boot', args={'LinodeID': node_id})['DATA']
if _wait_for_job(node_id, response['JobID']):
return {'state': 'Running',
'action': 'start',
'success': True}
else:
return {'action': 'start',
'success': False}
def stop(name, call=None):
'''
Stop a VM in Linode.
name
The name of the VM to stop.
CLI Example:
.. code-block:: bash
salt-cloud -a stop vm_name
'''
if call != 'action':
raise SaltCloudException(
'The stop action must be called with -a or --action.'
)
node_id = get_linode_id_from_name(name)
node = get_linode(kwargs={'linode_id': node_id})
if node['STATUS'] == 2:
return {'success': True,
'state': 'Stopped',
'msg': 'Machine already stopped'}
response = _query('linode', 'shutdown', args={'LinodeID': node_id})['DATA']
if _wait_for_job(node_id, response['JobID']):
return {'state': 'Stopped',
'action': 'stop',
'success': True}
else:
return {'action': 'stop',
'success': False}
def update_linode(linode_id, update_args=None):
'''
Updates a Linode's properties.
linode_id
The ID of the Linode to shutdown. Required.
update_args
The args to update the Linode with. Must be in dictionary form.
'''
update_args.update({'LinodeID': linode_id})
result = _query('linode', 'update', args=update_args)
return _clean_data(result)
def _clean_data(api_response):
'''
Returns the DATA response from a Linode API query as a single pre-formatted dictionary
api_response
The query to be cleaned.
'''
data = {}
data.update(api_response['DATA'])
if not data:
response_data = api_response['DATA']
data.update(response_data)
return data
def _list_linodes(full=False):
'''
Helper function to format and parse linode data
'''
nodes = _query('linode', 'list')['DATA']
ips = get_ips()
ret = {}
for node in nodes:
this_node = {}
linode_id = six.text_type(node['LINODEID'])
this_node['id'] = linode_id
this_node['image'] = node['DISTRIBUTIONVENDOR']
this_node['name'] = node['LABEL']
this_node['size'] = node['TOTALRAM']
state = int(node['STATUS'])
this_node['state'] = _get_status_descr_by_id(state)
for key, val in six.iteritems(ips):
if key == linode_id:
this_node['private_ips'] = val['private_ips']
this_node['public_ips'] = val['public_ips']
if full:
this_node['extra'] = node
ret[node['LABEL']] = this_node
return ret
def _query(action=None,
command=None,
args=None,
method='GET',
header_dict=None,
data=None,
url='https://api.linode.com/'):
'''
Make a web call to the Linode API.
'''
global LASTCALL
vm_ = get_configured_provider()
ratelimit_sleep = config.get_cloud_config_value(
'ratelimit_sleep', vm_, __opts__, search_global=False, default=0,
)
apikey = config.get_cloud_config_value(
'apikey', vm_, __opts__, search_global=False
)
if not isinstance(args, dict):
args = {}
if 'api_key' not in args.keys():
args['api_key'] = apikey
if action and 'api_action' not in args.keys():
args['api_action'] = '{0}.{1}'.format(action, command)
if header_dict is None:
header_dict = {}
if method != 'POST':
header_dict['Accept'] = 'application/json'
decode = True
if method == 'DELETE':
decode = False
now = int(time.mktime(datetime.datetime.now().timetuple()))
if LASTCALL >= now:
time.sleep(ratelimit_sleep)
result = __utils__['http.query'](
url,
method,
params=args,
data=data,
header_dict=header_dict,
decode=decode,
decode_type='json',
text=True,
status=True,
hide_fields=['api_key', 'rootPass'],
opts=__opts__,
)
if 'ERRORARRAY' in result['dict']:
if result['dict']['ERRORARRAY']:
error_list = []
for error in result['dict']['ERRORARRAY']:
msg = error['ERRORMESSAGE']
if msg == "Authentication failed":
raise SaltCloudSystemExit(
'Linode API Key is expired or invalid'
)
else:
error_list.append(msg)
raise SaltCloudException(
'Linode API reported error(s): {}'.format(", ".join(error_list))
)
LASTCALL = int(time.mktime(datetime.datetime.now().timetuple()))
log.debug('Linode Response Status Code: %s', result['status'])
return result['dict']
def _wait_for_job(linode_id, job_id, timeout=300, quiet=True):
'''
Wait for a Job to return.
linode_id
The ID of the Linode to wait on. Required.
job_id
The ID of the job to wait for.
timeout
The amount of time to wait for a status to update.
quiet
Log status updates to debug logs when True. Otherwise, logs to info.
'''
interval = 5
iterations = int(timeout / interval)
for i in range(0, iterations):
jobs_result = _query('linode',
'job.list',
args={'LinodeID': linode_id})['DATA']
if jobs_result[0]['JOBID'] == job_id and jobs_result[0]['HOST_SUCCESS'] == 1:
return True
time.sleep(interval)
log.log(
logging.INFO if not quiet else logging.DEBUG,
'Still waiting on Job %s for Linode %s.', job_id, linode_id
)
return False
def _wait_for_status(linode_id, status=None, timeout=300, quiet=True):
'''
Wait for a certain status from Linode.
linode_id
The ID of the Linode to wait on. Required.
status
The status to look for to update.
timeout
The amount of time to wait for a status to update.
quiet
Log status updates to debug logs when False. Otherwise, logs to info.
'''
if status is None:
status = _get_status_id_by_name('brand_new')
status_desc_waiting = _get_status_descr_by_id(status)
interval = 5
iterations = int(timeout / interval)
for i in range(0, iterations):
result = get_linode(kwargs={'linode_id': linode_id})
if result['STATUS'] == status:
return True
status_desc_result = _get_status_descr_by_id(result['STATUS'])
time.sleep(interval)
log.log(
logging.INFO if not quiet else logging.DEBUG,
'Status for Linode %s is \'%s\', waiting for \'%s\'.',
linode_id, status_desc_result, status_desc_waiting
)
return False
def _get_status_descr_by_id(status_id):
'''
Return linode status by ID
status_id
linode VM status ID
'''
for status_name, status_data in six.iteritems(LINODE_STATUS):
if status_data['code'] == int(status_id):
return status_data['descr']
return LINODE_STATUS.get(status_id, None)
def _get_status_id_by_name(status_name):
'''
Return linode status description by internalstatus name
status_name
internal linode VM status name
'''
return LINODE_STATUS.get(status_name, {}).get('code', None)
def _validate_name(name):
'''
Checks if the provided name fits Linode's labeling parameters.
.. versionadded:: 2015.5.6
name
The VM name to validate
'''
name = six.text_type(name)
name_length = len(name)
regex = re.compile(r'^[a-zA-Z0-9][A-Za-z0-9_-]*[a-zA-Z0-9]$')
if name_length < 3 or name_length > 48:
ret = False
elif not re.match(regex, name):
ret = False
else:
ret = True
if ret is False:
log.warning(
'A Linode label may only contain ASCII letters or numbers, dashes, and '
'underscores, must begin and end with letters or numbers, and be at least '
'three characters in length.'
)
return ret
def _get_ssh_interface(vm_):
'''
Return the ssh_interface type to connect to. Either 'public_ips' (default)
or 'private_ips'.
'''
return config.get_cloud_config_value(
'ssh_interface', vm_, __opts__, default='public_ips',
search_global=False
)