diff options
Diffstat (limited to 'lib/ansible/plugins/inventory/gcp_compute.py')
-rw-r--r-- | lib/ansible/plugins/inventory/gcp_compute.py | 616 |
1 files changed, 0 insertions, 616 deletions
diff --git a/lib/ansible/plugins/inventory/gcp_compute.py b/lib/ansible/plugins/inventory/gcp_compute.py deleted file mode 100644 index a7a06fb72e..0000000000 --- a/lib/ansible/plugins/inventory/gcp_compute.py +++ /dev/null @@ -1,616 +0,0 @@ -# Copyright (c) 2017 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 = """ - name: gcp_compute - plugin_type: inventory - short_description: Google Cloud Compute Engine inventory source - requirements: - - requests >= 2.18.4 - - google-auth >= 1.3.0 - extends_documentation_fragment: - - constructed - - inventory_cache - description: - - Get inventory hosts from Google Cloud Platform GCE. - - Uses a YAML configuration file that ends with gcp_compute.(yml|yaml) or gcp.(yml|yaml). - options: - plugin: - description: token that ensures this is a source file for the 'gcp_compute' plugin. - required: True - choices: ['gcp_compute'] - zones: - description: A list of regions in which to describe GCE instances. - If none provided, it defaults to all zones available to a given project. - type: list - folders: - description: A folder that contains many projects - type: list - required: False - projects: - description: A list of projects in which to describe GCE instances. - type: list - required: False - filters: - description: > - A list of filter value pairs. Available filters are listed here - U(https://cloud.google.com/compute/docs/reference/rest/v1/instances/aggregatedList). - Each additional filter in the list will act be added as an AND condition - (filter1 and filter2) - type: list - hostnames: - description: A list of options that describe the ordering for which - hostnames should be assigned. Currently supported hostnames are - 'public_ip', 'private_ip', or 'name'. - default: ['public_ip', 'private_ip', 'name'] - type: list - auth_kind: - description: - - The type of credential used. - required: True - choices: ['application', 'serviceaccount', 'machineaccount'] - env: - - name: GCP_AUTH_KIND - version_added: "2.8.2" - scopes: - description: list of authentication scopes - type: list - default: ['https://www.googleapis.com/auth/compute'] - env: - - name: GCP_SCOPES - version_added: "2.8.2" - service_account_file: - description: - - The path of a Service Account JSON file if serviceaccount is selected as type. - type: path - env: - - name: GCP_SERVICE_ACCOUNT_FILE - version_added: "2.8.2" - - name: GCE_CREDENTIALS_FILE_PATH - version_added: "2.8" - service_account_contents: - description: - - A string representing the contents of a Service Account JSON file. This should not be passed in as a dictionary, - but a string that has the exact contents of a service account json file (valid JSON). - type: string - env: - - name: GCP_SERVICE_ACCOUNT_CONTENTS - version_added: "2.8.2" - service_account_email: - description: - - An optional service account email address if machineaccount is selected - and the user does not wish to use the default email. - env: - - name: GCP_SERVICE_ACCOUNT_EMAIL - version_added: "2.8.2" - vars_prefix: - description: prefix to apply to host variables, does not include facts nor params - default: '' - use_contrib_script_compatible_sanitization: - description: - - By default this plugin is using a general group name sanitization to create safe and usable group names for use in Ansible. - This option allows you to override that, in efforts to allow migration from the old inventory script. - - For this to work you should also turn off the TRANSFORM_INVALID_GROUP_CHARS setting, - otherwise the core engine will just use the standard sanitization on top. - - This is not the default as such names break certain functionality as not all characters are valid Python identifiers - which group names end up being used as. - type: bool - default: False - version_added: '2.8' - retrieve_image_info: - description: - - Populate the C(image) host fact for the instances returned with the GCP image name - - By default this plugin does not attempt to resolve the boot image of an instance to the image name cataloged in GCP - because of the performance overhead of the task. - - Unless this option is enabled, the C(image) host variable will be C(null) - type: bool - default: False - version_added: '2.8' -""" - -EXAMPLES = """ -plugin: gcp_compute -zones: # populate inventory with instances in these regions - - us-east1-a -projects: - - gcp-prod-gke-100 - - gcp-cicd-101 -filters: - - machineType = n1-standard-1 - - scheduling.automaticRestart = true AND machineType = n1-standard-1 -service_account_file: /tmp/service_account.json -auth_kind: serviceaccount -scopes: - - 'https://www.googleapis.com/auth/cloud-platform' - - 'https://www.googleapis.com/auth/compute.readonly' -keyed_groups: - # Create groups from GCE labels - - prefix: gcp - key: labels -hostnames: - # List host by name instead of the default public ip - - name -compose: - # Set an inventory parameter to use the Public IP address to connect to the host - # For Private ip use "networkInterfaces[0].networkIP" - ansible_host: networkInterfaces[0].accessConfigs[0].natIP -""" - -import json - -from ansible.errors import AnsibleError, AnsibleParserError -from ansible.module_utils._text import to_text -from ansible.module_utils.basic import missing_required_lib -from ansible.module_utils.gcp_utils import ( - GcpSession, - navigate_hash, - GcpRequestException, - HAS_GOOGLE_LIBRARIES, -) -from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable - - -# Mocking a module to reuse module_utils -class GcpMockModule(object): - def __init__(self, params): - self.params = params - - def fail_json(self, *args, **kwargs): - raise AnsibleError(kwargs["msg"]) - - -class GcpInstance(object): - def __init__(self, json, hostname_ordering, project_disks, should_format=True): - self.hostname_ordering = hostname_ordering - self.project_disks = project_disks - self.json = json - if should_format: - self.convert() - - def to_json(self): - return self.json - - def convert(self): - if "zone" in self.json: - self.json["zone_selflink"] = self.json["zone"] - self.json["zone"] = self.json["zone"].split("/")[-1] - if "machineType" in self.json: - self.json["machineType_selflink"] = self.json["machineType"] - self.json["machineType"] = self.json["machineType"].split("/")[-1] - - if "networkInterfaces" in self.json: - for network in self.json["networkInterfaces"]: - if "network" in network: - network["network"] = self._format_network_info(network["network"]) - if "subnetwork" in network: - network["subnetwork"] = self._format_network_info( - network["subnetwork"] - ) - - if "metadata" in self.json: - # If no metadata, 'items' will be blank. - # We want the metadata hash overriden anyways for consistency. - self.json["metadata"] = self._format_metadata( - self.json["metadata"].get("items", {}) - ) - - self.json["project"] = self.json["selfLink"].split("/")[6] - self.json["image"] = self._get_image() - - def _format_network_info(self, address): - """ - :param address: A GCP network address - :return a dict with network shortname and region - """ - split = address.split("/") - region = "" - if "global" in split: - region = "global" - else: - region = split[8] - return {"region": region, "name": split[-1], "selfLink": address} - - def _format_metadata(self, metadata): - """ - :param metadata: A list of dicts where each dict has keys "key" and "value" - :return a dict with key/value pairs for each in list. - """ - new_metadata = {} - for pair in metadata: - new_metadata[pair["key"]] = pair["value"] - return new_metadata - - def hostname(self): - """ - :return the hostname of this instance - """ - for order in self.hostname_ordering: - name = None - if order == "public_ip": - name = self._get_publicip() - elif order == "private_ip": - name = self._get_privateip() - elif order == "name": - name = self.json[u"name"] - else: - raise AnsibleParserError("%s is not a valid hostname precedent" % order) - - if name: - return name - - raise AnsibleParserError("No valid name found for host") - - def _get_publicip(self): - """ - :return the publicIP of this instance or None - """ - # Get public IP if exists - for interface in self.json["networkInterfaces"]: - if "accessConfigs" in interface: - for accessConfig in interface["accessConfigs"]: - if "natIP" in accessConfig: - return accessConfig[u"natIP"] - return None - - def _get_image(self): - """ - :param instance: A instance response from GCP - :return the image of this instance or None - """ - image = None - if self.project_disks and "disks" in self.json: - for disk in self.json["disks"]: - if disk.get("boot"): - image = self.project_disks[disk["source"]] - return image - - def _get_privateip(self): - """ - :param item: A host response from GCP - :return the privateIP of this instance or None - """ - # Fallback: Get private IP - for interface in self.json[u"networkInterfaces"]: - if "networkIP" in interface: - return interface[u"networkIP"] - - -class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): - - NAME = "gcp_compute" - - _instances = ( - r"https://www.googleapis.com/compute/v1/projects/%s/aggregated/instances" - ) - - def __init__(self): - super(InventoryModule, self).__init__() - - self.group_prefix = "gcp_" - - def _populate_host(self, item): - """ - :param item: A GCP instance - """ - hostname = item.hostname() - self.inventory.add_host(hostname) - for key in item.to_json(): - try: - self.inventory.set_variable( - hostname, self.get_option("vars_prefix") + key, item.to_json()[key] - ) - except (ValueError, TypeError) as e: - self.display.warning( - "Could not set host info hostvar for %s, skipping %s: %s" - % (hostname, key, to_text(e)) - ) - self.inventory.add_child("all", hostname) - - def verify_file(self, path): - """ - :param path: the path to the inventory config file - :return the contents of the config file - """ - if super(InventoryModule, self).verify_file(path): - if path.endswith(("gcp.yml", "gcp.yaml")): - return True - elif path.endswith(("gcp_compute.yml", "gcp_compute.yaml")): - return True - return False - - def fetch_list(self, params, link, query): - """ - :param params: a dict containing all of the fields relevant to build URL - :param link: a formatted URL - :param query: a formatted query string - :return the JSON response containing a list of instances. - """ - lists = [] - resp = self._return_if_object( - self.fake_module, self.auth_session.get(link, params={"filter": query}) - ) - if resp: - lists.append(resp.get("items")) - while resp.get("nextPageToken"): - resp = self._return_if_object( - self.fake_module, - self.auth_session.get( - link, - params={"filter": query, "pageToken": resp.get("nextPageToken")}, - ), - ) - lists.append(resp.get("items")) - return self.build_list(lists) - - def build_list(self, lists): - arrays_for_zones = {} - for resp in lists: - for zone in resp: - if "instances" in resp[zone]: - if zone in arrays_for_zones: - arrays_for_zones[zone] = ( - arrays_for_zones[zone] + resp[zone]["instances"] - ) - else: - arrays_for_zones[zone] = resp[zone]["instances"] - return arrays_for_zones - - def _get_query_options(self, filters): - """ - :param config_data: contents of the inventory config file - :return A fully built query string - """ - if not filters: - return "" - - if len(filters) == 1: - return filters[0] - else: - queries = [] - for f in filters: - # For multiple queries, all queries should have () - if f[0] != "(" and f[-1] != ")": - queries.append("(%s)" % "".join(f)) - else: - queries.append(f) - - return " ".join(queries) - - def _return_if_object(self, module, response): - """ - :param module: A GcpModule - :param response: A Requests response object - :return JSON response - """ - # If not found, return nothing. - if response.status_code == 404: - return None - - # If no content, return nothing. - if response.status_code == 204: - return None - - try: - response.raise_for_status - result = response.json() - except getattr(json.decoder, "JSONDecodeError", ValueError) as inst: - module.fail_json(msg="Invalid JSON response with error: %s" % inst) - except GcpRequestException as inst: - module.fail_json(msg="Network error: %s" % inst) - - if navigate_hash(result, ["error", "errors"]): - module.fail_json(msg=navigate_hash(result, ["error", "errors"])) - - return result - - def _add_hosts(self, items, config_data, format_items=True, project_disks=None): - """ - :param items: A list of hosts - :param config_data: configuration data - :param format_items: format items or not - """ - if not items: - return - - hostname_ordering = ["public_ip", "private_ip", "name"] - if self.get_option("hostnames"): - hostname_ordering = self.get_option("hostnames") - - for host_json in items: - host = GcpInstance( - host_json, hostname_ordering, project_disks, format_items - ) - self._populate_host(host) - - hostname = host.hostname() - self._set_composite_vars( - self.get_option("compose"), host.to_json(), hostname - ) - self._add_host_to_composed_groups( - self.get_option("groups"), host.to_json(), hostname - ) - self._add_host_to_keyed_groups( - self.get_option("keyed_groups"), host.to_json(), hostname - ) - - def _get_project_disks(self, config_data, query): - """ - project space disk images - """ - - try: - self._project_disks - except AttributeError: - self._project_disks = {} - request_params = {"maxResults": 500, "filter": query} - - for project in config_data["projects"]: - session_responses = [] - page_token = True - while page_token: - response = self.auth_session.get( - "https://www.googleapis.com/compute/v1/projects/{0}/aggregated/disks".format( - project - ), - params=request_params, - ) - response_json = response.json() - if "nextPageToken" in response_json: - request_params["pageToken"] = response_json["nextPageToken"] - elif "pageToken" in request_params: - del request_params["pageToken"] - - if "items" in response_json: - session_responses.append(response_json) - page_token = "pageToken" in request_params - - for response in session_responses: - if "items" in response: - # example k would be a zone or region name - # example v would be { "disks" : [], "otherkey" : "..." } - for zone_or_region, aggregate in response["items"].items(): - if "zones" in zone_or_region: - if "disks" in aggregate: - zone = zone_or_region.replace("zones/", "") - for disk in aggregate["disks"]: - if ( - "zones" in config_data - and zone in config_data["zones"] - ): - # If zones specified, only store those zones' data - if "sourceImage" in disk: - self._project_disks[ - disk["selfLink"] - ] = disk["sourceImage"].split("/")[-1] - else: - self._project_disks[ - disk["selfLink"] - ] = disk["selfLink"].split("/")[-1] - - else: - if "sourceImage" in disk: - self._project_disks[ - disk["selfLink"] - ] = disk["sourceImage"].split("/")[-1] - else: - self._project_disks[ - disk["selfLink"] - ] = disk["selfLink"].split("/")[-1] - - return self._project_disks - - def fetch_projects(self, params, link, query): - module = GcpMockModule(params) - auth = GcpSession(module, 'cloudresourcemanager') - response = auth.get(link, params={'filter': query}) - return self._return_if_object(module, response) - - def projects_for_folder(self, config_data, folder): - link = 'https://cloudresourcemanager.googleapis.com/v1/projects'.format() - query = 'parent.id = {0}'.format(folder) - projects = [] - config_data['scopes'] = ['https://www.googleapis.com/auth/cloud-platform'] - projects_response = self.fetch_projects(config_data, link, query) - - if 'projects' in projects_response: - for item in projects_response.get('projects'): - projects.append(item['name']) - return projects - - def parse(self, inventory, loader, path, cache=True): - - if not HAS_GOOGLE_LIBRARIES: - raise AnsibleParserError( - "gce inventory plugin cannot start: %s" - % missing_required_lib("google-auth") - ) - - super(InventoryModule, self).parse(inventory, loader, path) - - config_data = {} - config_data = self._read_config_data(path) - - if self.get_option("use_contrib_script_compatible_sanitization"): - self._sanitize_group_name = ( - self._legacy_script_compatible_group_sanitization - ) - - # setup parameters as expected by 'fake module class' to reuse module_utils w/o changing the API - params = { - "filters": self.get_option("filters"), - "projects": self.get_option("projects"), - "folders": self.get_option("folders"), - "scopes": self.get_option("scopes"), - "zones": self.get_option("zones"), - "auth_kind": self.get_option("auth_kind"), - "service_account_file": self.get_option("service_account_file"), - "service_account_contents": self.get_option("service_account_contents"), - "service_account_email": self.get_option("service_account_email"), - } - - self.fake_module = GcpMockModule(params) - self.auth_session = GcpSession(self.fake_module, "compute") - - query = self._get_query_options(params["filters"]) - - if self.get_option("retrieve_image_info"): - project_disks = self._get_project_disks(config_data, query) - else: - project_disks = None - - # Cache logic - if cache: - cache = self.get_option("cache") - cache_key = self.get_cache_key(path) - else: - cache_key = None - - cache_needs_update = False - if cache: - try: - results = self._cache[cache_key] - for project in results: - for zone in results[project]: - self._add_hosts( - results[project][zone], - config_data, - False, - project_disks=project_disks, - ) - except KeyError: - cache_needs_update = True - - projects = [] - if params["projects"]: - projects = projects + params["projects"] - - if params["folders"]: - for folder in params["folders"]: - projects = projects + self.projects_for_folder(config_data, folder) - - if not cache or cache_needs_update: - cached_data = {} - for project in projects: - cached_data[project] = {} - params["project"] = project - zones = params["zones"] - # Fetch all instances - link = self._instances % project - resp = self.fetch_list(params, link, query) - for key, value in resp.items(): - zone = key[6:] - if not zones or zone in zones: - self._add_hosts(value, config_data, project_disks=project_disks) - cached_data[project][zone] = value - - if cache_needs_update: - self._cache[cache_key] = cached_data - - @staticmethod - def _legacy_script_compatible_group_sanitization(name): - - return name |