# 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: aws_ec2 plugin_type: inventory short_description: EC2 inventory source requirements: - boto3 - botocore extends_documentation_fragment: - inventory_cache - constructed description: - Get inventory hosts from Amazon Web Services EC2. - Uses a YAML configuration file that ends with C(aws_ec2.(yml|yaml)). notes: - If no credentials are provided and the control node has an associated IAM instance profile then the role will be used for authentication. author: - Sloane Hertel (@s-hertel) options: aws_profile: description: The AWS profile type: str aliases: [ boto_profile ] env: - name: AWS_DEFAULT_PROFILE - name: AWS_PROFILE aws_access_key: description: The AWS access key to use. type: str aliases: [ aws_access_key_id ] env: - name: EC2_ACCESS_KEY - name: AWS_ACCESS_KEY - name: AWS_ACCESS_KEY_ID aws_secret_key: description: The AWS secret key that corresponds to the access key. type: str aliases: [ aws_secret_access_key ] env: - name: EC2_SECRET_KEY - name: AWS_SECRET_KEY - name: AWS_SECRET_ACCESS_KEY aws_security_token: description: The AWS security token if using temporary access and secret keys. type: str env: - name: EC2_SECURITY_TOKEN - name: AWS_SESSION_TOKEN - name: AWS_SECURITY_TOKEN plugin: description: Token that ensures this is a source file for the plugin. required: True choices: ['aws_ec2'] iam_role_arn: description: The ARN of the IAM role to assume to perform the inventory lookup. You should still provide AWS credentials with enough privilege to perform the AssumeRole action. version_added: '2.9' regions: description: - A list of regions in which to describe EC2 instances. - If empty (the default) default this will include all regions, except possibly restricted ones like us-gov-west-1 and cn-north-1. type: list default: [] hostnames: description: - A list in order of precedence for hostname variables. - You can use the options specified in U(http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html#options). - To use tags as hostnames use the syntax tag:Name=Value to use the hostname Name_Value, or tag:Name to use the value of the Name tag. type: list default: [] filters: description: - A dictionary of filter value pairs. - Available filters are listed here U(http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html#options). type: dict default: {} include_extra_api_calls: description: - Add two additional API calls for every instance to include 'persistent' and 'events' host variables. - Spot instances may be persistent and instances may have associated events. type: bool default: False version_added: '2.8' strict_permissions: description: - By default if a 403 (Forbidden) error code is encountered this plugin will fail. - You can set this option to False in the inventory config file which will allow 403 errors to be gracefully skipped. type: bool default: True 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 and matches the sanitization of groups when the script's ``replace_dash_in_groups`` option is set to ``False``. To replicate behavior of ``replace_dash_in_groups = True`` with constructed groups, you will need to replace hyphens with underscores via the regex_replace filter for those entries. - 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' ''' EXAMPLES = ''' # Minimal example using environment vars or instance role credentials # Fetch all hosts in us-east-1, the hostname is the public DNS if it exists, otherwise the private IP address plugin: aws_ec2 regions: - us-east-1 # Example using filters, ignoring permission errors, and specifying the hostname precedence plugin: aws_ec2 boto_profile: aws_profile # Populate inventory with instances in these regions regions: - us-east-1 - us-east-2 filters: # All instances with their `Environment` tag set to `dev` tag:Environment: dev # All dev and QA hosts tag:Environment: - dev - qa instance.group-id: sg-xxxxxxxx # Ignores 403 errors rather than failing strict_permissions: False # Note: I(hostnames) sets the inventory_hostname. To modify ansible_host without modifying # inventory_hostname use compose (see example below). hostnames: - tag:Name=Tag1,Name=Tag2 # Return specific hosts only - tag:CustomDNSName - dns-name - private-ip-address # Example using constructed features to create groups and set ansible_host plugin: aws_ec2 regions: - us-east-1 - us-west-1 # keyed_groups may be used to create custom groups strict: False keyed_groups: # Add e.g. x86_64 hosts to an arch_x86_64 group - prefix: arch key: 'architecture' # Add hosts to tag_Name_Value groups for each Name/Value tag pair - prefix: tag key: tags # Add hosts to e.g. instance_type_z3_tiny - prefix: instance_type key: instance_type # Create security_groups_sg_abcd1234 group for each SG - key: 'security_groups|json_query("[].group_id")' prefix: 'security_groups' # Create a group for each value of the Application tag - key: tags.Application separator: '' # Create a group per region e.g. aws_region_us_east_2 - key: placement.region prefix: aws_region # Create a group (or groups) based on the value of a custom tag "Role" and add them to a metagroup called "project" - key: tags['Role'] prefix: foo parent_group: "project" # Set individual variables with compose compose: # Use the private IP address to connect to the host # (note: this does not modify inventory_hostname, which is set via I(hostnames)) ansible_host: private_ip_address ''' import re from ansible.errors import AnsibleError from ansible.module_utils._text import to_native, to_text from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable from ansible.utils.display import Display from ansible.module_utils.six import string_types try: import boto3 import botocore except ImportError: raise AnsibleError('The ec2 dynamic inventory plugin requires boto3 and botocore.') display = Display() # The mappings give an array of keys to get from the filter name to the value # returned by boto3's EC2 describe_instances method. instance_meta_filter_to_boto_attr = { 'group-id': ('Groups', 'GroupId'), 'group-name': ('Groups', 'GroupName'), 'network-interface.attachment.instance-owner-id': ('OwnerId',), 'owner-id': ('OwnerId',), 'requester-id': ('RequesterId',), 'reservation-id': ('ReservationId',), } instance_data_filter_to_boto_attr = { 'affinity': ('Placement', 'Affinity'), 'architecture': ('Architecture',), 'availability-zone': ('Placement', 'AvailabilityZone'), 'block-device-mapping.attach-time': ('BlockDeviceMappings', 'Ebs', 'AttachTime'), 'block-device-mapping.delete-on-termination': ('BlockDeviceMappings', 'Ebs', 'DeleteOnTermination'), 'block-device-mapping.device-name': ('BlockDeviceMappings', 'DeviceName'), 'block-device-mapping.status': ('BlockDeviceMappings', 'Ebs', 'Status'), 'block-device-mapping.volume-id': ('BlockDeviceMappings', 'Ebs', 'VolumeId'), 'client-token': ('ClientToken',), 'dns-name': ('PublicDnsName',), 'host-id': ('Placement', 'HostId'), 'hypervisor': ('Hypervisor',), 'iam-instance-profile.arn': ('IamInstanceProfile', 'Arn'), 'image-id': ('ImageId',), 'instance-id': ('InstanceId',), 'instance-lifecycle': ('InstanceLifecycle',), 'instance-state-code': ('State', 'Code'), 'instance-state-name': ('State', 'Name'), 'instance-type': ('InstanceType',), 'instance.group-id': ('SecurityGroups', 'GroupId'), 'instance.group-name': ('SecurityGroups', 'GroupName'), 'ip-address': ('PublicIpAddress',), 'kernel-id': ('KernelId',), 'key-name': ('KeyName',), 'launch-index': ('AmiLaunchIndex',), 'launch-time': ('LaunchTime',), 'monitoring-state': ('Monitoring', 'State'), 'network-interface.addresses.private-ip-address': ('NetworkInterfaces', 'PrivateIpAddress'), 'network-interface.addresses.primary': ('NetworkInterfaces', 'PrivateIpAddresses', 'Primary'), 'network-interface.addresses.association.public-ip': ('NetworkInterfaces', 'PrivateIpAddresses', 'Association', 'PublicIp'), 'network-interface.addresses.association.ip-owner-id': ('NetworkInterfaces', 'PrivateIpAddresses', 'Association', 'IpOwnerId'), 'network-interface.association.public-ip': ('NetworkInterfaces', 'Association', 'PublicIp'), 'network-interface.association.ip-owner-id': ('NetworkInterfaces', 'Association', 'IpOwnerId'), 'network-interface.association.allocation-id': ('ElasticGpuAssociations', 'ElasticGpuId'), 'network-interface.association.association-id': ('ElasticGpuAssociations', 'ElasticGpuAssociationId'), 'network-interface.attachment.attachment-id': ('NetworkInterfaces', 'Attachment', 'AttachmentId'), 'network-interface.attachment.instance-id': ('InstanceId',), 'network-interface.attachment.device-index': ('NetworkInterfaces', 'Attachment', 'DeviceIndex'), 'network-interface.attachment.status': ('NetworkInterfaces', 'Attachment', 'Status'), 'network-interface.attachment.attach-time': ('NetworkInterfaces', 'Attachment', 'AttachTime'), 'network-interface.attachment.delete-on-termination': ('NetworkInterfaces', 'Attachment', 'DeleteOnTermination'), 'network-interface.availability-zone': ('Placement', 'AvailabilityZone'), 'network-interface.description': ('NetworkInterfaces', 'Description'), 'network-interface.group-id': ('NetworkInterfaces', 'Groups', 'GroupId'), 'network-interface.group-name': ('NetworkInterfaces', 'Groups', 'GroupName'), 'network-interface.ipv6-addresses.ipv6-address': ('NetworkInterfaces', 'Ipv6Addresses', 'Ipv6Address'), 'network-interface.mac-address': ('NetworkInterfaces', 'MacAddress'), 'network-interface.network-interface-id': ('NetworkInterfaces', 'NetworkInterfaceId'), 'network-interface.owner-id': ('NetworkInterfaces', 'OwnerId'), 'network-interface.private-dns-name': ('NetworkInterfaces', 'PrivateDnsName'), # 'network-interface.requester-id': (), 'network-interface.requester-managed': ('NetworkInterfaces', 'Association', 'IpOwnerId'), 'network-interface.status': ('NetworkInterfaces', 'Status'), 'network-interface.source-dest-check': ('NetworkInterfaces', 'SourceDestCheck'), 'network-interface.subnet-id': ('NetworkInterfaces', 'SubnetId'), 'network-interface.vpc-id': ('NetworkInterfaces', 'VpcId'), 'placement-group-name': ('Placement', 'GroupName'), 'platform': ('Platform',), 'private-dns-name': ('PrivateDnsName',), 'private-ip-address': ('PrivateIpAddress',), 'product-code': ('ProductCodes', 'ProductCodeId'), 'product-code.type': ('ProductCodes', 'ProductCodeType'), 'ramdisk-id': ('RamdiskId',), 'reason': ('StateTransitionReason',), 'root-device-name': ('RootDeviceName',), 'root-device-type': ('RootDeviceType',), 'source-dest-check': ('SourceDestCheck',), 'spot-instance-request-id': ('SpotInstanceRequestId',), 'state-reason-code': ('StateReason', 'Code'), 'state-reason-message': ('StateReason', 'Message'), 'subnet-id': ('SubnetId',), 'tag': ('Tags',), 'tag-key': ('Tags',), 'tag-value': ('Tags',), 'tenancy': ('Placement', 'Tenancy'), 'virtualization-type': ('VirtualizationType',), 'vpc-id': ('VpcId',), } class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): NAME = 'aws_ec2' def __init__(self): super(InventoryModule, self).__init__() self.group_prefix = 'aws_ec2_' # credentials self.boto_profile = None self.aws_secret_access_key = None self.aws_access_key_id = None self.aws_security_token = None self.iam_role_arn = None def _compile_values(self, obj, attr): ''' :param obj: A list or dict of instance attributes :param attr: A key :return The value(s) found via the attr ''' if obj is None: return temp_obj = [] if isinstance(obj, list) or isinstance(obj, tuple): for each in obj: value = self._compile_values(each, attr) if value: temp_obj.append(value) else: temp_obj = obj.get(attr) has_indexes = any([isinstance(temp_obj, list), isinstance(temp_obj, tuple)]) if has_indexes and len(temp_obj) == 1: return temp_obj[0] return temp_obj def _get_boto_attr_chain(self, filter_name, instance): ''' :param filter_name: The filter :param instance: instance dict returned by boto3 ec2 describe_instances() ''' allowed_filters = sorted(list(instance_data_filter_to_boto_attr.keys()) + list(instance_meta_filter_to_boto_attr.keys())) if filter_name not in allowed_filters: raise AnsibleError("Invalid filter '%s' provided; filter must be one of %s." % (filter_name, allowed_filters)) if filter_name in instance_data_filter_to_boto_attr: boto_attr_list = instance_data_filter_to_boto_attr[filter_name] else: boto_attr_list = instance_meta_filter_to_boto_attr[filter_name] instance_value = instance for attribute in boto_attr_list: instance_value = self._compile_values(instance_value, attribute) return instance_value def _get_credentials(self): ''' :return A dictionary of boto client credentials ''' boto_params = {} for credential in (('aws_access_key_id', self.aws_access_key_id), ('aws_secret_access_key', self.aws_secret_access_key), ('aws_session_token', self.aws_security_token)): if credential[1]: boto_params[credential[0]] = credential[1] return boto_params def _get_connection(self, credentials, region='us-east-1'): try: connection = boto3.session.Session(profile_name=self.boto_profile).client('ec2', region, **credentials) except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: if self.boto_profile: try: connection = boto3.session.Session(profile_name=self.boto_profile).client('ec2', region) except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) else: raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) return connection def _boto3_assume_role(self, credentials, region): """ Assume an IAM role passed by iam_role_arn parameter :return: a dict containing the credentials of the assumed role """ iam_role_arn = self.iam_role_arn try: sts_connection = boto3.session.Session(profile_name=self.boto_profile).client('sts', region, **credentials) sts_session = sts_connection.assume_role(RoleArn=iam_role_arn, RoleSessionName='ansible_aws_ec2_dynamic_inventory') return dict( aws_access_key_id=sts_session['Credentials']['AccessKeyId'], aws_secret_access_key=sts_session['Credentials']['SecretAccessKey'], aws_session_token=sts_session['Credentials']['SessionToken'] ) except botocore.exceptions.ClientError as e: raise AnsibleError("Unable to assume IAM role: %s" % to_native(e)) def _boto3_conn(self, regions): ''' :param regions: A list of regions to create a boto3 client Generator that yields a boto3 client and the region ''' credentials = self._get_credentials() iam_role_arn = self.iam_role_arn if not regions: try: # as per https://boto3.amazonaws.com/v1/documentation/api/latest/guide/ec2-example-regions-avail-zones.html client = self._get_connection(credentials) resp = client.describe_regions() regions = [x['RegionName'] for x in resp.get('Regions', [])] except botocore.exceptions.NoRegionError: # above seems to fail depending on boto3 version, ignore and lets try something else pass # fallback to local list hardcoded in boto3 if still no regions if not regions: session = boto3.Session() regions = session.get_available_regions('ec2') # I give up, now you MUST give me regions if not regions: raise AnsibleError('Unable to get regions list from available methods, you must specify the "regions" option to continue.') for region in regions: connection = self._get_connection(credentials, region) try: if iam_role_arn is not None: assumed_credentials = self._boto3_assume_role(credentials, region) else: assumed_credentials = credentials connection = boto3.session.Session(profile_name=self.boto_profile).client('ec2', region, **assumed_credentials) except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: if self.boto_profile: try: connection = boto3.session.Session(profile_name=self.boto_profile).client('ec2', region) except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) else: raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) yield connection, region def _get_instances_by_region(self, regions, filters, strict_permissions): ''' :param regions: a list of regions in which to describe instances :param filters: a list of boto3 filter dictionaries :param strict_permissions: a boolean determining whether to fail or ignore 403 error codes :return A list of instance dictionaries ''' all_instances = [] for connection, region in self._boto3_conn(regions): try: # By default find non-terminated/terminating instances if not any([f['Name'] == 'instance-state-name' for f in filters]): filters.append({'Name': 'instance-state-name', 'Values': ['running', 'pending', 'stopping', 'stopped']}) paginator = connection.get_paginator('describe_instances') reservations = paginator.paginate(Filters=filters).build_full_result().get('Reservations') instances = [] for r in reservations: new_instances = r['Instances'] for instance in new_instances: instance.update(self._get_reservation_details(r)) if self.get_option('include_extra_api_calls'): instance.update(self._get_event_set_and_persistence(connection, instance['InstanceId'], instance.get('SpotInstanceRequestId'))) instances.extend(new_instances) except botocore.exceptions.ClientError as e: if e.response['ResponseMetadata']['HTTPStatusCode'] == 403 and not strict_permissions: instances = [] else: raise AnsibleError("Failed to describe instances: %s" % to_native(e)) except botocore.exceptions.BotoCoreError as e: raise AnsibleError("Failed to describe instances: %s" % to_native(e)) all_instances.extend(instances) return sorted(all_instances, key=lambda x: x['InstanceId']) def _get_reservation_details(self, reservation): return { 'OwnerId': reservation['OwnerId'], 'RequesterId': reservation.get('RequesterId', ''), 'ReservationId': reservation['ReservationId'] } def _get_event_set_and_persistence(self, connection, instance_id, spot_instance): host_vars = {'Events': '', 'Persistent': False} try: kwargs = {'InstanceIds': [instance_id]} host_vars['Events'] = connection.describe_instance_status(**kwargs)['InstanceStatuses'][0].get('Events', '') except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: if not self.get_option('strict_permissions'): pass else: raise AnsibleError("Failed to describe instance status: %s" % to_native(e)) if spot_instance: try: kwargs = {'SpotInstanceRequestIds': [spot_instance]} host_vars['Persistent'] = bool( connection.describe_spot_instance_requests(**kwargs)['SpotInstanceRequests'][0].get('Type') == 'persistent' ) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: if not self.get_option('strict_permissions'): pass else: raise AnsibleError("Failed to describe spot instance requests: %s" % to_native(e)) return host_vars 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 = boto3_tag_list_to_ansible_dict(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_hostname(self, instance, hostnames): ''' :param instance: an instance dict returned by boto3 ec2 describe_instances() :param hostnames: a list of hostname destination variables in order of preference :return the preferred identifer for the host ''' if not hostnames: hostnames = ['dns-name', 'private-dns-name'] hostname = None for preference in hostnames: if 'tag' in preference: if not preference.startswith('tag:'): raise AnsibleError("To name a host by tags name_value, use 'tag:name=value'.") hostname = self._get_tag_hostname(preference, instance) else: hostname = self._get_boto_attr_chain(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 _query(self, regions, filters, strict_permissions): ''' :param regions: a list of regions to query :param filters: a list of boto3 filter dictionaries :param hostnames: a list of hostname destination variables in order of preference :param strict_permissions: a boolean determining whether to fail or ignore 403 error codes ''' return {'aws_ec2': self._get_instances_by_region(regions, filters, strict_permissions)} 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 _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) host = camel_dict_to_snake_dict(host, ignore_list=['Tags']) host['tags'] = boto3_tag_list_to_ansible_dict(host.get('tags', [])) # Allow easier grouping by region host['placement']['region'] = host['placement']['availability_zone'][:-1] if not hostname: continue self.inventory.add_host(hostname, group=group) for hostvar, hostval in host.items(): self.inventory.set_variable(hostname, hostvar, hostval) # Use constructed if applicable 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 _set_credentials(self): ''' :param config_data: contents of the inventory config file ''' self.boto_profile = self.get_option('aws_profile') self.aws_access_key_id = self.get_option('aws_access_key') self.aws_secret_access_key = self.get_option('aws_secret_key') self.aws_security_token = self.get_option('aws_security_token') self.iam_role_arn = self.get_option('iam_role_arn') if not self.boto_profile and not (self.aws_access_key_id and self.aws_secret_access_key): session = botocore.session.get_session() try: credentials = session.get_credentials().get_frozen_credentials() except AttributeError: pass else: self.aws_access_key_id = credentials.access_key self.aws_secret_access_key = credentials.secret_key self.aws_security_token = credentials.token if not self.boto_profile and not (self.aws_access_key_id and self.aws_secret_access_key): raise AnsibleError("Insufficient boto credentials found. Please provide them in your " "inventory configuration file or set them as environment variables.") 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(('aws_ec2.yml', 'aws_ec2.yaml')): return True display.debug("aws_ec2 inventory filename must end with 'aws_ec2.yml' or 'aws_ec2.yaml'") return False def parse(self, inventory, loader, path, cache=True): super(InventoryModule, self).parse(inventory, loader, path) self._read_config_data(path) if self.get_option('use_contrib_script_compatible_sanitization'): self._sanitize_group_name = self._legacy_script_compatible_group_sanitization self._set_credentials() # get user specifications regions = self.get_option('regions') filters = ansible_dict_to_boto3_filter_list(self.get_option('filters')) hostnames = self.get_option('hostnames') strict_permissions = self.get_option('strict_permissions') 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, strict_permissions) 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) def ansible_dict_to_boto3_filter_list(filters_dict): """ Convert an Ansible dict of filters to list of dicts that boto3 can use Args: filters_dict (dict): Dict of AWS filters. Basic Usage: >>> filters = {'some-aws-id': 'i-01234567'} >>> ansible_dict_to_boto3_filter_list(filters) { 'some-aws-id': 'i-01234567' } Returns: List: List of AWS filters and their values [ { 'Name': 'some-aws-id', 'Values': [ 'i-01234567', ] } ] """ filters_list = [] for k, v in filters_dict.items(): filter_dict = {'Name': k} if isinstance(v, string_types): filter_dict['Values'] = [v] else: filter_dict['Values'] = v filters_list.append(filter_dict) return filters_list def boto3_tag_list_to_ansible_dict(tags_list, tag_name_key_name=None, tag_value_key_name=None): """ Convert a boto3 list of resource tags to a flat dict of key:value pairs Args: tags_list (list): List of dicts representing AWS tags. tag_name_key_name (str): Value to use as the key for all tag keys (useful because boto3 doesn't always use "Key") tag_value_key_name (str): Value to use as the key for all tag values (useful because boto3 doesn't always use "Value") Basic Usage: >>> tags_list = [{'Key': 'MyTagKey', 'Value': 'MyTagValue'}] >>> boto3_tag_list_to_ansible_dict(tags_list) [ { 'Key': 'MyTagKey', 'Value': 'MyTagValue' } ] Returns: Dict: Dict of key:value pairs representing AWS tags { 'MyTagKey': 'MyTagValue', } """ if tag_name_key_name and tag_value_key_name: tag_candidates = {tag_name_key_name: tag_value_key_name} else: tag_candidates = {'key': 'value', 'Key': 'Value'} if not tags_list: return {} for k, v in tag_candidates.items(): if k in tags_list[0] and v in tags_list[0]: return dict((tag[k], tag[v]) for tag in tags_list) raise ValueError("Couldn't find tag key (candidates %s) in tag list %s" % (str(tag_candidates), str(tags_list)))