# -*- coding: utf-8 -*- # Copyright: (c), Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r""" name: digitalocean author: - Janos Gerzson (@grzs) - Tadej Borovšak (@tadeboro) - Max Truxa (@maxtruxa) short_description: DigitalOcean Inventory Plugin version_added: "1.1.0" description: - DigitalOcean (DO) inventory plugin. - Acquires droplet list from DO API. - Uses configuration file that ends with '(do_hosts|digitalocean|digital_ocean).(yaml|yml)'. extends_documentation_fragment: - community.digitalocean.digital_ocean.documentation - constructed - inventory_cache options: plugin: description: - The name of the DigitalOcean Inventory Plugin, this should always be C(community.digitalocean.digitalocean). required: true choices: ['community.digitalocean.digitalocean'] api_token: description: - DigitalOcean OAuth token. - Template expressions can be used in this field. required: true type: str aliases: [ oauth_token ] env: - name: DO_API_TOKEN attributes: description: >- Droplet attributes to add as host vars to each inventory host. Check out the DO API docs for full list of attributes at U(https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_droplets). type: list elements: str default: - id - name - networks - region - size_slug var_prefix: description: - Prefix of generated varible names (e.g. C(tags) -> C(do_tags)) type: str default: 'do_' pagination: description: - Maximum droplet objects per response page. - If the number of droplets related to the account exceeds this value, the query will be broken to multiple requests (pages). - DigitalOcean currently allows a maximum of 200. type: int default: 200 filters: description: - Filter hosts with Jinja templates. - If no filters are specified, all hosts are added to the inventory. type: list elements: str default: [] version_added: '1.5.0' """ EXAMPLES = r""" # Using keyed groups and compose for hostvars plugin: community.digitalocean.digitalocean api_token: '{{ lookup("pipe", "./get-do-token.sh" }}' attributes: - id - name - memory - vcpus - disk - size - image - networks - volume_ids - tags - region keyed_groups: - key: do_region.slug prefix: 'region' separator: '_' - key: do_tags | lower prefix: '' separator: '' compose: ansible_host: do_networks.v4 | selectattr('type','eq','public') | map(attribute='ip_address') | first class: do_size.description | lower distro: do_image.distribution | lower filters: - '"kubernetes" in do_tags' - 'do_region.slug == "fra1"' """ import re import json from ansible.errors import AnsibleError, AnsibleParserError from ansible.inventory.group import to_safe_group_name from ansible.module_utils._text import to_native from ansible.module_utils.urls import Request from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): NAME = "community.digitalocean.digitalocean" # Constructable methods use the following function to construct group names. By # default, characters that are not valid in python variables, are always replaced by # underscores. We are overriding this with a function that respects the # TRANSFORM_INVALID_GROUP_CHARS configuration option and allows users to control the # behavior. _sanitize_group_name = staticmethod(to_safe_group_name) def verify_file(self, path): valid = False if super(InventoryModule, self).verify_file(path): if path.endswith( ( "do_hosts.yaml", "do_hosts.yml", "digitalocean.yaml", "digitalocean.yml", "digital_ocean.yaml", "digital_ocean.yml", ) ): valid = True else: self.display.vvv( "Skipping due to inventory source file name mismatch. " "The file name has to end with one of the following: " "do_hosts.yaml, do_hosts.yml " "digitalocean.yaml, digitalocean.yml, " "digital_ocean.yaml, digital_ocean.yml." ) return valid def _template_option(self, option): value = self.get_option(option) self.templar.available_variables = {} return self.templar.template(value) def _get_payload(self): # request parameters api_token = self._template_option("api_token") headers = { "Content-Type": "application/json", "Authorization": "Bearer {0}".format(api_token), } # build url pagination = self.get_option("pagination") url = "https://api.digitalocean.com/v2/droplets?per_page=" + str(pagination) # send request(s) self.req = Request(headers=headers, timeout=self.get_option("timeout")) payload = [] try: while url: self.display.vvv("Sending request to {0}".format(url)) response = json.load(self.req.get(url)) payload.extend(response["droplets"]) url = response.get("links", {}).get("pages", {}).get("next") except ValueError: raise AnsibleParserError("something went wrong with JSON loading") except (URLError, HTTPError) as error: raise AnsibleParserError(error) return payload def _populate(self, records): attributes = self.get_option("attributes") var_prefix = self.get_option("var_prefix") strict = self.get_option("strict") host_filters = self.get_option("filters") for record in records: host_name = record.get("name") if not host_name: continue host_vars = {} for k, v in record.items(): if k in attributes: host_vars[var_prefix + k] = v if not self._passes_filters(host_filters, host_vars, host_name, strict): self.display.vvv("Host {0} did not pass all filters".format(host_name)) continue # add host to inventory self.inventory.add_host(host_name) # set variables for host for k, v in host_vars.items(): self.inventory.set_variable(host_name, k, v) self._set_composite_vars( self.get_option("compose"), self.inventory.get_host(host_name).get_vars(), host_name, strict, ) # set composed and keyed groups self._add_host_to_composed_groups( self.get_option("groups"), dict(), host_name, strict ) self._add_host_to_keyed_groups( self.get_option("keyed_groups"), dict(), host_name, strict ) def _passes_filters(self, filters, variables, host, strict=False): if filters and isinstance(filters, list): for template in filters: try: if not self._compose(template, variables): return False except Exception as e: if strict: raise AnsibleError( "Could not evaluate host filter {0} for host {1}: {2}".format( template, host, to_native(e) ) ) # Better be safe and not include any hosts by accident. return False return True def parse(self, inventory, loader, path, cache=True): super(InventoryModule, self).parse(inventory, loader, path) self._read_config_data(path) # cache settings cache_key = self.get_cache_key(path) use_cache = self.get_option("cache") and cache update_cache = self.get_option("cache") and not cache records = None if use_cache: try: records = self._cache[cache_key] except KeyError: update_cache = True if records is None: records = self._get_payload() if update_cache: self._cache[cache_key] = records self._populate(records)