plugins/inventory/alicloud_ecs.py (296 lines of code) (raw):

# Copyright (c) 2017-present Alibaba Group Holding Limited. He Guimin <heguimin36@163.com> # 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: alibaba.alicloud.alicloud_ecs plugin_type: inventory short_description: ECS inventory source requirements: - python3 - footmark extends_documentation_fragment: - inventory_cache - constructed - alicloud description: - Get inventory hosts from Alicloud ECS. - Uses a yaml configuration file that ends with C(alicloud.(yml|yaml)). author: - "He Guimin (@xiaozhu36)" - "Li Xue (@lixue323)" options: plugin: description: Token that ensures this is a source file for the plugin. required: True choices: ['alibaba.alicloud.alicloud_ecs'] regions: description: - A list of regions in which to describe ECS instances. - If empty (the default) default this will include all regions, except possibly restricted ones like cn-beijing type: list default: [] hostnames: description: - A list in order of precedence for hostname variables. - You can use the options specified in U(https://www.alibabacloud.com/help/doc-detail/25506.htm). type: list default: [] filters: description: - A dictionary of filter value pairs. - Available filters are listed here U(https://www.alibabacloud.com/help/doc-detail/25506.htm). type: dict default: {} use_contrib_script_compatible_sanitization: description: - By default this plugin has a behavior of using ``replace_dash_in_groups = True`` to replace hyphens with underscores. This option allows you to override that, in efforts to allow migration from the old inventory script and matches the sanitization of groups when the script's ``replace_dash_in_groups`` option is set to ``False``. type: bool default: False ''' EXAMPLES = ''' # Fetch all hosts in cn-beijing plugin: alibaba.alicloud.alicloud_ecs regions: - cn-beijing # Example using filters and specifying the hostname precedence plugin: alibaba.alicloud.alicloud_ecs regions: - cn-beijing - cn-qingdao filters: instance_type: ecs.g6.4xlarge hostnames: - instance_id - public_ip_address - tag:foo=bar,foo2 # Example using constructed features to create groups and set ansible_host plugin: alibaba.alicloud.alicloud_ecs # If true make invalid entries a fatal error, otherwise skip and continue # Since it is possible to use facts in the expressions they might not always be available and we ignore those errors by default. strict: False # Add hosts to group based on the values of a variable keyed_groups: - key: instance_name prefix: name # Set individual variables with compose # Create vars from jinja2 expressions compose: # Use the public ip address to connect to the host # (note: this does not modify inventory_hostname, which is set via I(hostnames)) ansible_host: public_ip_address # Example of enabling caching for an individual YAML configuration file plugin: alibaba.alicloud.alicloud_ecs cache: yes cache_plugin: jsonfile cache_timeout: 7200 cache_connection: /tmp/alicloud_inventory cache_prefix: alicloud_ecs ''' import os import re from ansible.errors import AnsibleError from ansible.module_utils._text import to_native, to_text from ansible_collections.alibaba.alicloud.plugins.module_utils.alicloud_ecs import connect_to_acs, get_profile from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable from ansible.utils.display import Display try: import footmark import footmark.ecs import footmark.regioninfo HAS_FOOTMARK = True except ImportError: raise AnsibleError('The ecs dynamic inventory plugin requires footmark.') display = Display() class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): NAME = 'alibaba.alicloud.alicloud_ecs' def __init__(self): super(InventoryModule, self).__init__() self.group_prefix = 'alicloud_ecs_' def _set_credentials(self): ''' Reads the settings from the file ''' access_key = os.environ.get('ALICLOUD_ACCESS_KEY', os.environ.get('ALICLOUD_ACCESS_KEY_ID', None)) if not access_key: access_key = self.get_option('alicloud_access_key') secret_key = os.environ.get('ALICLOUD_SECRET_KEY', os.environ.get('ALICLOUD_SECRET_ACCESS_KEY', None)) if not secret_key: secret_key = self.get_option('alicloud_secret_key') security_token = os.environ.get('ALICLOUD_SECURITY_TOKEN', None) if not security_token: self.security_token = self.get_option('alicloud_security_token') alicloud_region = os.environ.get('ALICLOUD_REGION', None) if not alicloud_region: alicloud_region = self.get_option('alicloud_region') ecs_role_name = os.environ.get('ALICLOUD_ECS_ROLE_NAME', None) if not ecs_role_name: ecs_role_name = self.get_option('ecs_role_name') profile = os.environ.get('ALICLOUD_PROFILE', None) if not profile: profile = self.get_option('profile') shared_credentials_file = os.environ.get('ALICLOUD_SHARED_CREDENTIALS_FILE', None) if not shared_credentials_file: shared_credentials_file = self.get_option('shared_credentials_file') assume_role = self.get_option('alicloud_assume_role') assume_role_params = {} role_arn = os.environ.get('ALICLOUD_ASSUME_ROLE_ARN', None) if not role_arn and assume_role: assume_role_params['role_arn'] = assume_role.get('role_arn') session_name = os.environ.get('ALICLOUD_ASSUME_ROLE_SESSION_NAME', None) if not session_name and assume_role: assume_role_params['session_name'] = assume_role.get('session_name') session_expiration = os.environ.get('ALICLOUD_ASSUME_ROLE_SESSION_EXPIRATION', None) if not session_expiration and assume_role: assume_role_params['session_expiration'] = assume_role.get('session_expiration') if assume_role: assume_role_params['policy'] = assume_role.get('policy') credentials = { 'alicloud_access_key': access_key, 'alicloud_secret_key': secret_key, 'security_token': security_token, 'ecs_role_name': ecs_role_name, 'profile': profile, 'shared_credentials_file': shared_credentials_file, 'assume_role': assume_role_params, 'alicloud_region': alicloud_region } self.credentials = get_profile(credentials) def connect_to_ecs(self, module, region): # Check module args for credentials, then check environment vars access key pair and region connect_args = self.credentials connect_args['user_agent'] = 'Ansible-Provider-Alicloud/Dynamic-Inventory' conn = connect_to_acs(module, region, **connect_args) if conn is None: self.fail_with_error("region name: %s likely not supported. Connection to region failed." % region) return conn def _get_instances_by_region(self, regions, filters): ''' :param regions: a list of regions in which to describe instances :param filters: a list of ECS filter dictionaries :return A list of instance dictionaries ''' all_instances = [] if not regions: try: regions = list(map(lambda x: x.id, self.connect_to_ecs(footmark.ecs, "cn-beijing").describe_regions())) except Exception as e: raise AnsibleError('Unable to get regions list from available methods, you must specify the "regions" option to continue.') for region in regions: try: conn = connect_to_acs(footmark.ecs, region, **self.credentials) insts = conn.describe_instances(**filters) all_instances.extend(map(lambda x: x.read(), insts)) except Exception as e: raise AnsibleError("Failed to describe instances: %s" % to_native(e)) return sorted(all_instances, key=lambda x: x['instance_id']) def _query(self, regions, filters): ''' :param regions: a list of regions to query :param filters: a dict of ECS filter params :param hostnames: a list of hostname destination variables in order of preference ''' return {'alicloud': self._get_instances_by_region(regions, filters)} def _populate(self, groups, hostnames): for group in groups: group = self.inventory.add_group(group) self._add_hosts(hosts=groups[group], group=group, hostnames=hostnames) self.inventory.add_child('all', group) def _get_tag_hostname(self, preference, instance): tag_hostnames = preference.split('tag:', 1)[1] if ',' in tag_hostnames: tag_hostnames = tag_hostnames.split(',') else: tag_hostnames = [tag_hostnames] tags = instance.get('tags', {}) for v in tag_hostnames: if '=' in v: tag_name, tag_value = v.split('=') if tags.get(tag_name) == tag_value: return to_text(tag_name) + "_" + to_text(tag_value) else: tag_value = tags.get(v) if tag_value: return to_text(tag_value) return None def _get_instance_attr(self, filter_name, instance): ''' :param filter_name: The filter :param instance: instance dict returned by describe_instances() ''' if filter_name not in instance: raise AnsibleError("Invalid filter '%s' provided" % filter_name) return instance[filter_name] def _get_hostname(self, instance, hostnames): ''' :param instance: an instance dict returned by describe_instances() :param hostnames: a list of hostname destination variables in order of preference :return the preferred identifier for the host ''' if not hostnames: hostnames = ['instance_id', 'instance_name'] hostname = None for preference in hostnames: if preference.startswith('tag:'): hostname = self._get_tag_hostname(preference, instance) else: hostname = self._get_instance_attr(preference, instance) if hostname: break if hostname: if ':' in to_text(hostname): return self._sanitize_group_name((to_text(hostname))) else: return to_text(hostname) def _add_hosts(self, hosts, group, hostnames): ''' :param hosts: a list of hosts to be added to a group :param group: the name of the group to which the hosts belong :param hostnames: a list of hostname destination variables in order of preference ''' for host in hosts: hostname = self._get_hostname(host, hostnames) # Allow easier grouping by region host['region'] = host['availability_zone'] if not hostname: continue self.inventory.add_host(hostname, group=group) for hostvar, hostval in host.items(): self.inventory.set_variable(hostname, hostvar, hostval) strict = self.get_option('strict') # Composed variables self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict) # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict) # Create groups based on variable values and add the corresponding hosts to it self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict) def verify_file(self, path): ''' :param loader: an ansible.parsing.dataloader.DataLoader object :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(('alicloud.yaml', 'alicloud.yml')): return True display.debug("alicloud inventory filename must end with 'alicloud.yaml' or 'alicloud.yml'") return False def parse(self, inventory, loader, path, cache=True): super(InventoryModule, self).parse(inventory, loader, path) self._read_config_data(path) self._set_credentials() if self.get_option('use_contrib_script_compatible_sanitization'): self._sanitize_group_name = self._legacy_script_compatible_group_sanitization # get user specifications regions = self.get_option('regions') filters = self.get_option('filters') hostnames = self.get_option('hostnames') cache_key = self.get_cache_key(path) # false when refresh_cache or --flush-cache is used if cache: # get the user-specified directive cache = self.get_option('cache') # Generate inventory cache_needs_update = False if cache: try: results = self._cache[cache_key] except KeyError: # if cache expires or cache file doesn't exist cache_needs_update = True if not cache or cache_needs_update: results = self._query(regions, filters) self._populate(results, hostnames) # If the cache has expired/doesn't exist or if refresh_inventory/flush cache is used # when the user is using caching, update the cached inventory if cache_needs_update or (not cache and self.get_option('cache')): self._cache[cache_key] = results @staticmethod def _legacy_script_compatible_group_sanitization(name): # note that while this mirrors what the script used to do, # it has many issues with unicode and usability in python regex = re.compile(r"[^A-Za-z0-9\_\-]") return regex.sub('_', name)