%PDF- %PDF-
Direktori : /proc/227033/root/lib/python2.7/site-packages/salt/cloud/clouds/ |
Current File : //proc/227033/root/lib/python2.7/site-packages/salt/cloud/clouds/digitalocean.py |
# -*- coding: utf-8 -*- ''' DigitalOcean Cloud Module ========================= The DigitalOcean cloud module is used to control access to the DigitalOcean VPS system. Use of this module requires a requires a ``personal_access_token``, an ``ssh_key_file``, and at least one SSH key name in ``ssh_key_names``. More ``ssh_key_names`` can be added by separating each key with a comma. The ``personal_access_token`` can be found in the DigitalOcean web interface in the "Apps & API" section. The SSH key name can be found under the "SSH Keys" section. .. code-block:: yaml # Note: This example is for /etc/salt/cloud.providers or any file in the # /etc/salt/cloud.providers.d/ directory. my-digital-ocean-config: personal_access_token: xxx ssh_key_file: /path/to/ssh/key/file ssh_key_names: my-key-name,my-key-name-2 driver: digitalocean :depends: requests ''' # Import Python Libs from __future__ import absolute_import, print_function, unicode_literals import decimal import logging import os import pprint import time # Import Salt Libs import salt.utils.cloud import salt.utils.files import salt.utils.json import salt.utils.stringutils import salt.config as config from salt.exceptions import ( SaltCloudConfigError, SaltInvocationError, SaltCloudNotFound, SaltCloudSystemExit, SaltCloudExecutionFailure, SaltCloudExecutionTimeout ) from salt.ext import six from salt.ext.six.moves import zip # Import Third Party Libs try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False # Get logging started log = logging.getLogger(__name__) __virtualname__ = 'digitalocean' __virtual_aliases__ = ('digital_ocean', 'do') # Only load in this module if the DIGITALOCEAN configurations are in place def __virtual__(): ''' Check for DigitalOcean 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=__opts__, provider=__active_provider_name__ or __virtualname__, aliases=__virtual_aliases__, required_keys=('personal_access_token',) ) def get_dependencies(): ''' Warn if dependencies aren't met. ''' return config.check_driver_dependencies( __virtualname__, {'requests': HAS_REQUESTS} ) def avail_locations(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' ) items = query(method='regions') ret = {} for region in items['regions']: ret[region['name']] = {} for item in six.iterkeys(region): ret[region['name']][item] = six.text_type(region[item]) return ret 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' ) fetch = True page = 1 ret = {} while fetch: items = query(method='images', command='?page=' + six.text_type(page) + '&per_page=200') for image in items['images']: ret[image['name']] = {} for item in six.iterkeys(image): ret[image['name']][item] = image[item] page += 1 try: fetch = 'next' in items['links']['pages'] except KeyError: fetch = False return ret def avail_sizes(call=None): ''' Return a list of the image sizes that are on the provider ''' if call == 'action': raise SaltCloudSystemExit( 'The avail_sizes function must be called with ' '-f or --function, or with the --list-sizes option' ) items = query(method='sizes', command='?per_page=100') ret = {} for size in items['sizes']: ret[size['slug']] = {} for item in six.iterkeys(size): ret[size['slug']][item] = six.text_type(size[item]) 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.' ) return _list_nodes() def list_nodes_full(call=None, for_output=True): ''' 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.' ) return _list_nodes(full=True, for_output=for_output) 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('function'), __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 ) if not isinstance(vm_image, six.string_types): vm_image = six.text_type(vm_image) for image in images: if vm_image in (images[image]['name'], images[image]['slug'], images[image]['id']): if images[image]['slug'] is not None: return images[image]['slug'] return int(images[image]['id']) raise SaltCloudNotFound( 'The specified image, \'{0}\', could not be found.'.format(vm_image) ) def get_size(vm_): ''' Return the VM's size. Used by create_node(). ''' sizes = avail_sizes() vm_size = six.text_type(config.get_cloud_config_value( 'size', vm_, __opts__, search_global=False )) for size in sizes: if vm_size.lower() == sizes[size]['slug']: return sizes[size]['slug'] raise SaltCloudNotFound( 'The specified size, \'{0}\', could not be found.'.format(vm_size) ) def get_location(vm_): ''' Return the VM's location ''' locations = avail_locations() vm_location = six.text_type(config.get_cloud_config_value( 'location', vm_, __opts__, search_global=False )) for location in locations: if vm_location in (locations[location]['name'], locations[location]['slug']): return locations[location]['slug'] raise SaltCloudNotFound( 'The specified location, \'{0}\', could not be found.'.format( vm_location ) ) def create_node(args): ''' Create a node ''' node = query(method='droplets', args=args, http_method='post') 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 'digitalocean', 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']) kwargs = { 'name': vm_['name'], 'size': get_size(vm_), 'image': get_image(vm_), 'region': get_location(vm_), 'ssh_keys': [], 'tags': [] } # backwards compat ssh_key_name = config.get_cloud_config_value( 'ssh_key_name', vm_, __opts__, search_global=False ) if ssh_key_name: kwargs['ssh_keys'].append(get_keyid(ssh_key_name)) ssh_key_names = config.get_cloud_config_value( 'ssh_key_names', vm_, __opts__, search_global=False, default=False ) if ssh_key_names: for key in ssh_key_names.split(','): kwargs['ssh_keys'].append(get_keyid(key)) key_filename = config.get_cloud_config_value( 'ssh_key_file', vm_, __opts__, search_global=False, default=None ) if key_filename is not None and not os.path.isfile(key_filename): raise SaltCloudConfigError( 'The defined key_filename \'{0}\' does not exist'.format( key_filename ) ) if not __opts__.get('ssh_agent', False) and key_filename is None: raise SaltCloudConfigError( 'The DigitalOcean driver requires an ssh_key_file and an ssh_key_name ' 'because it does not supply a root password upon building the server.' ) ssh_interface = config.get_cloud_config_value( 'ssh_interface', vm_, __opts__, search_global=False, default='public' ) if ssh_interface in ['private', 'public']: log.info("ssh_interface: Setting interface for ssh to %s", ssh_interface) kwargs['ssh_interface'] = ssh_interface else: raise SaltCloudConfigError( "The DigitalOcean driver requires ssh_interface to be defined as 'public' or 'private'." ) private_networking = config.get_cloud_config_value( 'private_networking', vm_, __opts__, search_global=False, default=None, ) if private_networking is not None: if not isinstance(private_networking, bool): raise SaltCloudConfigError("'private_networking' should be a boolean value.") kwargs['private_networking'] = private_networking if not private_networking and ssh_interface == 'private': raise SaltCloudConfigError( "The DigitalOcean driver requires ssh_interface if defined as 'private' " "then private_networking should be set as 'True'." ) backups_enabled = config.get_cloud_config_value( 'backups_enabled', vm_, __opts__, search_global=False, default=None, ) if backups_enabled is not None: if not isinstance(backups_enabled, bool): raise SaltCloudConfigError("'backups_enabled' should be a boolean value.") kwargs['backups'] = backups_enabled ipv6 = config.get_cloud_config_value( 'ipv6', vm_, __opts__, search_global=False, default=None, ) if ipv6 is not None: if not isinstance(ipv6, bool): raise SaltCloudConfigError("'ipv6' should be a boolean value.") kwargs['ipv6'] = ipv6 monitoring = config.get_cloud_config_value( 'monitoring', vm_, __opts__, search_global=False, default=None, ) if monitoring is not None: if not isinstance(monitoring, bool): raise SaltCloudConfigError("'monitoring' should be a boolean value.") kwargs['monitoring'] = monitoring kwargs['tags'] = config.get_cloud_config_value( 'tags', vm_, __opts__, search_global=False, default=False ) userdata_file = config.get_cloud_config_value( 'userdata_file', vm_, __opts__, search_global=False, default=None ) if userdata_file is not None: try: with salt.utils.files.fopen(userdata_file, 'r') as fp_: kwargs['user_data'] = salt.utils.cloud.userdata_template( __opts__, vm_, salt.utils.stringutils.to_unicode(fp_.read()) ) except Exception as exc: log.exception( 'Failed to read userdata from %s: %s', userdata_file, exc) create_dns_record = config.get_cloud_config_value( 'create_dns_record', vm_, __opts__, search_global=False, default=None, ) if create_dns_record: log.info('create_dns_record: will attempt to write DNS records') default_dns_domain = None dns_domain_name = vm_['name'].split('.') if len(dns_domain_name) > 2: log.debug('create_dns_record: inferring default dns_hostname, dns_domain from minion name as FQDN') default_dns_hostname = '.'.join(dns_domain_name[:-2]) default_dns_domain = '.'.join(dns_domain_name[-2:]) else: log.debug("create_dns_record: can't infer dns_domain from %s", vm_['name']) default_dns_hostname = dns_domain_name[0] dns_hostname = config.get_cloud_config_value( 'dns_hostname', vm_, __opts__, search_global=False, default=default_dns_hostname, ) dns_domain = config.get_cloud_config_value( 'dns_domain', vm_, __opts__, search_global=False, default=default_dns_domain, ) if dns_hostname and dns_domain: log.info('create_dns_record: using dns_hostname="%s", dns_domain="%s"', dns_hostname, dns_domain) __add_dns_addr__ = lambda t, d: post_dns_record(dns_domain=dns_domain, name=dns_hostname, record_type=t, record_data=d) log.debug('create_dns_record: %s', __add_dns_addr__) else: log.error('create_dns_record: could not determine dns_hostname and/or dns_domain') raise SaltCloudConfigError( '\'create_dns_record\' must be a dict specifying "domain" ' 'and "hostname" or the minion name must be an FQDN.' ) __utils__['cloud.fire_event']( 'event', 'requesting instance', 'salt/cloud/{0}/requesting'.format(vm_['name']), args=__utils__['cloud.filter_event']('requesting', kwargs, list(kwargs)), sock_dir=__opts__['sock_dir'], transport=__opts__['transport'] ) try: ret = create_node(kwargs) except Exception as exc: log.error( 'Error creating %s on DIGITALOCEAN\n\n' 'The following exception was thrown when trying to ' 'run the initial deployment: %s', vm_['name'], exc, # Show the traceback if the debug logging level is enabled exc_info_on_loglevel=logging.DEBUG ) return False def __query_node_data(vm_name): data = show_instance(vm_name, 'action') if not data: # Trigger an error in the wait_for_ip function return False if data['networks'].get('v4'): for network in data['networks']['v4']: if network['type'] == 'public': return data return False 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=10 * 60), interval=config.get_cloud_config_value( 'wait_for_ip_interval', vm_, __opts__, default=10), ) 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)) if not vm_.get('ssh_host'): vm_['ssh_host'] = None # add DNS records, set ssh_host, default to first found IP, preferring IPv4 for ssh bootstrap script target addr_families, dns_arec_types = (('v4', 'v6'), ('A', 'AAAA')) arec_map = dict(list(zip(addr_families, dns_arec_types))) for facing, addr_family, ip_address in [(net['type'], family, net['ip_address']) for family in addr_families for net in data['networks'][family]]: log.info('found %s IP%s interface for "%s"', facing, addr_family, ip_address) dns_rec_type = arec_map[addr_family] if facing == 'public': if create_dns_record: __add_dns_addr__(dns_rec_type, ip_address) if facing == ssh_interface: if not vm_['ssh_host']: vm_['ssh_host'] = ip_address if vm_['ssh_host'] is None: raise SaltCloudSystemExit( 'No suitable IP addresses found for ssh minion bootstrapping: {0}'.format(repr(data['networks'])) ) log.debug( 'Found public IP address to use for ssh minion bootstrapping: %s', vm_['ssh_host'] ) vm_['key_filename'] = key_filename ret = __utils__['cloud.bootstrap'](vm_, __opts__) ret.update(data) 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(method='droplets', droplet_id=None, command=None, args=None, http_method='get'): ''' Make a web call to DigitalOcean ''' base_path = six.text_type(config.get_cloud_config_value( 'api_root', get_configured_provider(), __opts__, search_global=False, default='https://api.digitalocean.com/v2' )) path = '{0}/{1}/'.format(base_path, method) if droplet_id: path += '{0}/'.format(droplet_id) if command: path += command if not isinstance(args, dict): args = {} personal_access_token = config.get_cloud_config_value( 'personal_access_token', get_configured_provider(), __opts__, search_global=False ) data = salt.utils.json.dumps(args) requester = getattr(requests, http_method) request = requester(path, data=data, headers={'Authorization': 'Bearer ' + personal_access_token, 'Content-Type': 'application/json'}) if request.status_code > 299: raise SaltCloudSystemExit( 'An error occurred while querying DigitalOcean. HTTP Code: {0} ' 'Error: \'{1}\''.format( request.status_code, # request.read() request.text ) ) log.debug(request.url) # success without data if request.status_code == 204: return True content = request.text result = salt.utils.json.loads(content) if result.get('status', '').lower() == 'error': raise SaltCloudSystemExit( pprint.pformat(result.get('error_message', {})) ) return result 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 show_instance(name, call=None): ''' Show the details from DigitalOcean concerning a droplet ''' if call != 'action': raise SaltCloudSystemExit( 'The show_instance action must be called with -a or --action.' ) node = _get_node(name) __utils__['cloud.cache_node'](node, __active_provider_name__, __opts__) return node def _get_node(name): attempts = 10 while attempts >= 0: try: return list_nodes_full(for_output=False)[name] except KeyError: attempts -= 1 log.debug( 'Failed to get the data for node \'%s\'. Remaining ' 'attempts: %s', name, attempts ) # Just a little delay between attempts... time.sleep(0.5) return {} def list_keypairs(call=None): ''' Return a dict of all available VM locations on the cloud provider with relevant data ''' if call != 'function': log.error( 'The list_keypairs function must be called with -f or --function.' ) return False fetch = True page = 1 ret = {} while fetch: items = query(method='account/keys', command='?page=' + six.text_type(page) + '&per_page=100') for key_pair in items['ssh_keys']: name = key_pair['name'] if name in ret: raise SaltCloudSystemExit( 'A duplicate key pair name, \'{0}\', was found in DigitalOcean\'s ' 'key pair list. Please change the key name stored by DigitalOcean. ' 'Be sure to adjust the value of \'ssh_key_file\' in your cloud ' 'profile or provider configuration, if necessary.'.format( name ) ) ret[name] = {} for item in six.iterkeys(key_pair): ret[name][item] = six.text_type(key_pair[item]) page += 1 try: fetch = 'next' in items['links']['pages'] except KeyError: fetch = False return ret def show_keypair(kwargs=None, call=None): ''' Show the details of an SSH keypair ''' if call != 'function': log.error( 'The show_keypair function must be called with -f or --function.' ) return False if not kwargs: kwargs = {} if 'keyname' not in kwargs: log.error('A keyname is required.') return False keypairs = list_keypairs(call='function') keyid = keypairs[kwargs['keyname']]['id'] log.debug('Key ID is %s', keyid) details = query(method='account/keys', command=keyid) return details def import_keypair(kwargs=None, call=None): ''' Upload public key to cloud provider. Similar to EC2 import_keypair. .. versionadded:: 2016.11.0 kwargs file(mandatory): public key file-name keyname(mandatory): public key name in the provider ''' with salt.utils.files.fopen(kwargs['file'], 'r') as public_key_filename: public_key_content = salt.utils.stringutils.to_unicode(public_key_filename.read()) digitalocean_kwargs = { 'name': kwargs['keyname'], 'public_key': public_key_content } created_result = create_key(digitalocean_kwargs, call=call) return created_result def create_key(kwargs=None, call=None): ''' Upload a public key ''' if call != 'function': log.error( 'The create_key function must be called with -f or --function.' ) return False try: result = query( method='account', command='keys', args={'name': kwargs['name'], 'public_key': kwargs['public_key']}, http_method='post' ) except KeyError: log.info('`name` and `public_key` arguments must be specified') return False return result def remove_key(kwargs=None, call=None): ''' Delete public key ''' if call != 'function': log.error( 'The create_key function must be called with -f or --function.' ) return False try: result = query( method='account', command='keys/' + kwargs['id'], http_method='delete' ) except KeyError: log.info('`id` argument must be specified') return False return result def get_keyid(keyname): ''' Return the ID of the keyname ''' if not keyname: return None keypairs = list_keypairs(call='function') keyid = keypairs[keyname]['id'] if keyid: return keyid raise SaltCloudNotFound('The specified ssh key could not be found.') def destroy(name, call=None): ''' Destroy a node. Will check termination protection and warn if enabled. 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'] ) data = show_instance(name, call='action') node = query(method='droplets', droplet_id=data['id'], http_method='delete') ## This is all terribly optomistic: # vm_ = get_vm_config(name=name) # delete_dns_record = config.get_cloud_config_value( # 'delete_dns_record', vm_, __opts__, search_global=False, default=None, # ) # TODO: when _vm config data can be made available, we should honor the configuration settings, # but until then, we should assume stale DNS records are bad, and default behavior should be to # delete them if we can. When this is resolved, also resolve the comments a couple of lines below. delete_dns_record = True if not isinstance(delete_dns_record, bool): raise SaltCloudConfigError( '\'delete_dns_record\' should be a boolean value.' ) # When the "to do" a few lines up is resolved, remove these lines and use the if/else logic below. log.debug('Deleting DNS records for %s.', name) destroy_dns_records(name) # Until the "to do" from line 754 is taken care of, we don't need this logic. # if delete_dns_record: # log.debug('Deleting DNS records for %s.', name) # destroy_dns_records(name) # else: # log.debug('delete_dns_record : %s', delete_dns_record) # for line in pprint.pformat(dir()).splitlines(): # log.debug('delete context: %s', line) __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 node def post_dns_record(**kwargs): ''' Creates a DNS record for the given name if the domain is managed with DO. ''' if 'kwargs' in kwargs: # flatten kwargs if called via salt-cloud -f f_kwargs = kwargs['kwargs'] del kwargs['kwargs'] kwargs.update(f_kwargs) mandatory_kwargs = ('dns_domain', 'name', 'record_type', 'record_data') for i in mandatory_kwargs: if kwargs[i]: pass else: error = '{0}="{1}" ## all mandatory args must be provided: {2}'.format(i, kwargs[i], mandatory_kwargs) raise SaltInvocationError(error) domain = query(method='domains', droplet_id=kwargs['dns_domain']) if domain: result = query( method='domains', droplet_id=kwargs['dns_domain'], command='records', args={'type': kwargs['record_type'], 'name': kwargs['name'], 'data': kwargs['record_data']}, http_method='post' ) return result return False def destroy_dns_records(fqdn): ''' Deletes DNS records for the given hostname if the domain is managed with DO. ''' domain = '.'.join(fqdn.split('.')[-2:]) hostname = '.'.join(fqdn.split('.')[:-2]) # TODO: remove this when the todo on 754 is available try: response = query(method='domains', droplet_id=domain, command='records') except SaltCloudSystemExit: log.debug('Failed to find domains.') return False log.debug("found DNS records: %s", pprint.pformat(response)) records = response['domain_records'] if records: record_ids = [r['id'] for r in records if r['name'].decode() == hostname] log.debug("deleting DNS record IDs: %s", record_ids) for id_ in record_ids: try: log.info('deleting DNS record %s', id_) ret = query( method='domains', droplet_id=domain, command='records/{0}'.format(id_), http_method='delete' ) except SaltCloudSystemExit: log.error('failed to delete DNS domain %s record ID %s.', domain, hostname) log.debug('DNS deletion REST call returned: %s', pprint.pformat(ret)) return False 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 Examples: .. code-block:: bash salt-cloud -f show_pricing my-digitalocean-config profile=my-profile ''' profile = __opts__['profiles'].get(kwargs['profile'], {}) if not profile: return {'Error': 'The requested profile was not found'} # Make sure the profile belongs to DigitalOcean provider = profile.get('provider', '0:0') comps = provider.split(':') if len(comps) < 2 or comps[1] != 'digitalocean': return {'Error': 'The requested profile does not belong to DigitalOcean'} raw = {} ret = {} sizes = avail_sizes() ret['per_hour'] = decimal.Decimal(sizes[profile['size']]['price_hourly']) ret['per_day'] = ret['per_hour'] * 24 ret['per_week'] = ret['per_day'] * 7 ret['per_month'] = decimal.Decimal(sizes[profile['size']]['price_monthly']) ret['per_year'] = ret['per_week'] * 52 if kwargs.get('raw', False): ret['_raw'] = raw return {profile['profile']: ret} def list_floating_ips(call=None): ''' Return a list of the floating ips that are on the provider .. versionadded:: 2016.3.0 CLI Examples: .. code-block:: bash salt-cloud -f list_floating_ips my-digitalocean-config ''' if call == 'action': raise SaltCloudSystemExit( 'The list_floating_ips function must be called with ' '-f or --function, or with the --list-floating-ips option' ) fetch = True page = 1 ret = {} while fetch: items = query(method='floating_ips', command='?page=' + six.text_type(page) + '&per_page=200') for floating_ip in items['floating_ips']: ret[floating_ip['ip']] = {} for item in six.iterkeys(floating_ip): ret[floating_ip['ip']][item] = floating_ip[item] page += 1 try: fetch = 'next' in items['links']['pages'] except KeyError: fetch = False return ret def show_floating_ip(kwargs=None, call=None): ''' Show the details of a floating IP .. versionadded:: 2016.3.0 CLI Examples: .. code-block:: bash salt-cloud -f show_floating_ip my-digitalocean-config floating_ip='45.55.96.47' ''' if call != 'function': log.error( 'The show_floating_ip function must be called with -f or --function.' ) return False if not kwargs: kwargs = {} if 'floating_ip' not in kwargs: log.error('A floating IP is required.') return False floating_ip = kwargs['floating_ip'] log.debug('Floating ip is %s', floating_ip) details = query(method='floating_ips', command=floating_ip) return details def create_floating_ip(kwargs=None, call=None): ''' Create a new floating IP .. versionadded:: 2016.3.0 CLI Examples: .. code-block:: bash salt-cloud -f create_floating_ip my-digitalocean-config region='NYC2' salt-cloud -f create_floating_ip my-digitalocean-config droplet_id='1234567' ''' if call != 'function': log.error( 'The create_floating_ip function must be called with -f or --function.' ) return False if not kwargs: kwargs = {} if 'droplet_id' in kwargs: result = query(method='floating_ips', args={'droplet_id': kwargs['droplet_id']}, http_method='post') return result elif 'region' in kwargs: result = query(method='floating_ips', args={'region': kwargs['region']}, http_method='post') return result else: log.error('A droplet_id or region is required.') return False def delete_floating_ip(kwargs=None, call=None): ''' Delete a floating IP .. versionadded:: 2016.3.0 CLI Examples: .. code-block:: bash salt-cloud -f delete_floating_ip my-digitalocean-config floating_ip='45.55.96.47' ''' if call != 'function': log.error( 'The delete_floating_ip function must be called with -f or --function.' ) return False if not kwargs: kwargs = {} if 'floating_ip' not in kwargs: log.error('A floating IP is required.') return False floating_ip = kwargs['floating_ip'] log.debug('Floating ip is %s', kwargs['floating_ip']) result = query(method='floating_ips', command=floating_ip, http_method='delete') return result def assign_floating_ip(kwargs=None, call=None): ''' Assign a floating IP .. versionadded:: 2016.3.0 CLI Examples: .. code-block:: bash salt-cloud -f assign_floating_ip my-digitalocean-config droplet_id=1234567 floating_ip='45.55.96.47' ''' if call != 'function': log.error( 'The assign_floating_ip function must be called with -f or --function.' ) return False if not kwargs: kwargs = {} if 'floating_ip' and 'droplet_id' not in kwargs: log.error('A floating IP and droplet_id is required.') return False result = query(method='floating_ips', command=kwargs['floating_ip'] + '/actions', args={'droplet_id': kwargs['droplet_id'], 'type': 'assign'}, http_method='post') return result def unassign_floating_ip(kwargs=None, call=None): ''' Unassign a floating IP .. versionadded:: 2016.3.0 CLI Examples: .. code-block:: bash salt-cloud -f unassign_floating_ip my-digitalocean-config floating_ip='45.55.96.47' ''' if call != 'function': log.error( 'The inassign_floating_ip function must be called with -f or --function.' ) return False if not kwargs: kwargs = {} if 'floating_ip' not in kwargs: log.error('A floating IP is required.') return False result = query(method='floating_ips', command=kwargs['floating_ip'] + '/actions', args={'type': 'unassign'}, http_method='post') return result def _list_nodes(full=False, for_output=False): ''' Helper function to format and parse node data. ''' fetch = True page = 1 ret = {} while fetch: items = query(method='droplets', command='?page=' + six.text_type(page) + '&per_page=200') for node in items['droplets']: name = node['name'] ret[name] = {} if full: ret[name] = _get_full_output(node, for_output=for_output) else: public_ips, private_ips = _get_ips(node['networks']) ret[name] = { 'id': node['id'], 'image': node['image']['name'], 'name': name, 'private_ips': private_ips, 'public_ips': public_ips, 'size': node['size_slug'], 'state': six.text_type(node['status']), } page += 1 try: fetch = 'next' in items['links']['pages'] except KeyError: fetch = False return ret def reboot(name, call=None): ''' Reboot a droplet in DigitalOcean. .. versionadded:: 2015.8.8 name The name of the droplet to restart. CLI Example: .. code-block:: bash salt-cloud -a reboot droplet_name ''' if call != 'action': raise SaltCloudSystemExit( 'The restart action must be called with -a or --action.' ) data = show_instance(name, call='action') if data.get('status') == 'off': return {'success': True, 'action': 'stop', 'status': 'off', 'msg': 'Machine is already off.'} ret = query(droplet_id=data['id'], command='actions', args={'type': 'reboot'}, http_method='post') return {'success': True, 'action': ret['action']['type'], 'state': ret['action']['status']} def start(name, call=None): ''' Start a droplet in DigitalOcean. .. versionadded:: 2015.8.8 name The name of the droplet to start. CLI Example: .. code-block:: bash salt-cloud -a start droplet_name ''' if call != 'action': raise SaltCloudSystemExit( 'The start action must be called with -a or --action.' ) data = show_instance(name, call='action') if data.get('status') == 'active': return {'success': True, 'action': 'start', 'status': 'active', 'msg': 'Machine is already running.'} ret = query(droplet_id=data['id'], command='actions', args={'type': 'power_on'}, http_method='post') return {'success': True, 'action': ret['action']['type'], 'state': ret['action']['status']} def stop(name, call=None): ''' Stop a droplet in DigitalOcean. .. versionadded:: 2015.8.8 name The name of the droplet to stop. CLI Example: .. code-block:: bash salt-cloud -a stop droplet_name ''' if call != 'action': raise SaltCloudSystemExit( 'The stop action must be called with -a or --action.' ) data = show_instance(name, call='action') if data.get('status') == 'off': return {'success': True, 'action': 'stop', 'status': 'off', 'msg': 'Machine is already off.'} ret = query(droplet_id=data['id'], command='actions', args={'type': 'shutdown'}, http_method='post') return {'success': True, 'action': ret['action']['type'], 'state': ret['action']['status']} def _get_full_output(node, for_output=False): ''' Helper function for _list_nodes to loop through all node information. Returns a dictionary containing the full information of a node. ''' ret = {} for item in six.iterkeys(node): value = node[item] if value is not None and for_output: value = six.text_type(value) ret[item] = value return ret def _get_ips(networks): ''' Helper function for list_nodes. Returns public and private ip lists based on a given network dictionary. ''' v4s = networks.get('v4') v6s = networks.get('v6') public_ips = [] private_ips = [] if v4s: for item in v4s: ip_type = item.get('type') ip_address = item.get('ip_address') if ip_type == 'public': public_ips.append(ip_address) if ip_type == 'private': private_ips.append(ip_address) if v6s: for item in v6s: ip_type = item.get('type') ip_address = item.get('ip_address') if ip_type == 'public': public_ips.append(ip_address) if ip_type == 'private': private_ips.append(ip_address) return public_ips, private_ips