From 042b8be19e6273df5006b027f2d52f1160cb887c Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 23 Apr 2021 23:26:03 +0200 Subject: [PATCH] Add common helper for ec2 client 'ensure_tags' (#309) Add common helper for ec2 client 'ensure_tags' Reviewed-by: Ansibullbot manages ticket workflow https://github.com/ansibullbot --- changelogs/fragments/309-ec2_tags.yml | 6 ++ plugins/module_utils/ec2.py | 134 ++++++++++++++++++++++++++ plugins/modules/ec2_tag.py | 58 ++++------- plugins/modules/ec2_tag_info.py | 13 +-- plugins/modules/ec2_vol.py | 55 +---------- plugins/modules/ec2_vpc_subnet.py | 47 ++------- 6 files changed, 174 insertions(+), 139 deletions(-) create mode 100644 changelogs/fragments/309-ec2_tags.yml diff --git a/changelogs/fragments/309-ec2_tags.yml b/changelogs/fragments/309-ec2_tags.yml new file mode 100644 index 00000000000..3d8e47b7c25 --- /dev/null +++ b/changelogs/fragments/309-ec2_tags.yml @@ -0,0 +1,6 @@ +minor_changes: +- module_utils/ec2 - added additional helper functions for tagging EC2 resources (https://github.com/ansible-collections/amazon.aws/pull/309). +- ec2_tag - use common code for tagging resources (https://github.com/ansible-collections/amazon.aws/pull/309). +- ec2_tag_info - use common code for tagging resources (https://github.com/ansible-collections/amazon.aws/pull/309). +- ec2_vol - use common code for tagging resources (https://github.com/ansible-collections/amazon.aws/pull/309). +- ec2_vpc_subnet - use common code for tagging resources (https://github.com/ansible-collections/amazon.aws/pull/309). diff --git a/plugins/module_utils/ec2.py b/plugins/module_utils/ec2.py index f68f0ee6f21..74953b7f8a5 100644 --- a/plugins/module_utils/ec2.py +++ b/plugins/module_utils/ec2.py @@ -805,3 +805,137 @@ def compare_aws_tags(current_tags_dict, new_tags_dict, purge_tags=True): tag_key_value_pairs_to_set[key] = new_tags_dict[key] return tag_key_value_pairs_to_set, tag_keys_to_unset + + +@AWSRetry.jittered_backoff() +def _describe_ec2_tags(client, **params): + paginator = client.get_paginator('describe_tags') + return paginator.paginate(**params).build_full_result() + + +def add_ec2_tags(client, module, resource_id, tags_to_set, retry_codes=None): + """ + Sets Tags on an EC2 resource. + + :param client: an EC2 boto3 client + :param module: an AnsibleAWSModule object + :param resource_id: the identifier for the resource + :param tags_to_set: A dictionary of key/value pairs to set + :param retry_codes: additional boto3 error codes to trigger retries + """ + + if not tags_to_set: + return False + if module.check_mode: + return True + + if not retry_codes: + retry_codes = [] + + try: + tags_to_add = ansible_dict_to_boto3_tag_list(tags_to_set) + AWSRetry.jittered_backoff(retries=10, catch_extra_error_codes=retry_codes)( + client.create_tags + )( + Resources=[resource_id], Tags=tags_to_add + ) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Unable to add tags {0} to {1}".format(tags_to_set, resource_id)) + return True + + +def remove_ec2_tags(client, module, resource_id, tags_to_unset, retry_codes=None): + """ + Removes Tags from an EC2 resource. + + :param client: an EC2 boto3 client + :param module: an AnsibleAWSModule object + :param resource_id: the identifier for the resource + :param tags_to_unset: a list of tag keys to removes + :param retry_codes: additional boto3 error codes to trigger retries + """ + + if not tags_to_unset: + return False + if module.check_mode: + return True + + if not retry_codes: + retry_codes = [] + + tags_to_remove = [dict(Key=tagkey) for tagkey in tags_to_unset] + + try: + AWSRetry.jittered_backoff(retries=10, catch_extra_error_codes=retry_codes)( + client.delete_tags + )( + Resources=[resource_id], Tags=tags_to_remove + ) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Unable to delete tags {0} from {1}".format(tags_to_unset, resource_id)) + return True + + +def describe_ec2_tags(client, module, resource_id, resource_type=None, retry_codes=None): + """ + Performs a paginated search of EC2 resource tags. + + :param client: an EC2 boto3 client + :param module: an AnsibleAWSModule object + :param resource_id: the identifier for the resource + :param resource_type: the type of the resource + :param retry_codes: additional boto3 error codes to trigger retries + """ + filters = {'resource-id': resource_id} + if resource_type: + filters['resource-type'] = resource_type + filters = ansible_dict_to_boto3_filter_list(filters) + + if not retry_codes: + retry_codes = [] + + try: + results = AWSRetry.jittered_backoff(retries=10, catch_extra_error_codes=retry_codes)( + _describe_ec2_tags + )( + client, Filters=filters + ) + return boto3_tag_list_to_ansible_dict(results.get('Tags', None)) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Failed to describe tags for EC2 Resource: {0}".format(resource_id)) + + +def ensure_ec2_tags(client, module, resource_id, resource_type=None, tags=None, purge_tags=True, retry_codes=None): + """ + Updates the tags on an EC2 resource. + + To remove all tags the tags parameter must be explicitly set to an empty dictionary. + + :param client: an EC2 boto3 client + :param module: an AnsibleAWSModule object + :param resource_id: the identifier for the resource + :param resource_type: the type of the resource + :param tags: the Tags to apply to the resource + :param purge_tags: whether tags missing from the tag list should be removed + :param retry_codes: additional boto3 error codes to trigger retries + :return: changed: returns True if the tags are changed + """ + + if tags is None: + return False + + if not retry_codes: + retry_codes = [] + + changed = False + current_tags = describe_ec2_tags(client, module, resource_id, resource_type, retry_codes) + + tags_to_set, tags_to_unset = compare_aws_tags(current_tags, tags, purge_tags) + + if purge_tags and not tags: + tags_to_unset = current_tags + + changed |= remove_ec2_tags(client, module, resource_id, tags_to_unset, retry_codes) + changed |= add_ec2_tags(client, module, resource_id, tags_to_set, retry_codes) + + return changed diff --git a/plugins/modules/ec2_tag.py b/plugins/modules/ec2_tag.py index 1d8a1e6f810..f04d1102ff6 100644 --- a/plugins/modules/ec2_tag.py +++ b/plugins/modules/ec2_tag.py @@ -124,15 +124,9 @@ from ..module_utils.ec2 import ansible_dict_to_boto3_tag_list from ..module_utils.ec2 import boto3_tag_list_to_ansible_dict from ..module_utils.ec2 import compare_aws_tags - - -def get_tags(ec2, module, resource): - filters = [{'Name': 'resource-id', 'Values': [resource]}] - try: - result = AWSRetry.jittered_backoff()(ec2.describe_tags)(Filters=filters) - return boto3_tag_list_to_ansible_dict(result['Tags']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to fetch tags for resource {0}'.format(resource)) +from ..module_utils.ec2 import describe_ec2_tags +from ..module_utils.ec2 import ensure_ec2_tags +from ..module_utils.ec2 import remove_ec2_tags def main(): @@ -155,44 +149,32 @@ def main(): ec2 = module.client('ec2') - current_tags = get_tags(ec2, module, resource) + current_tags = describe_ec2_tags(ec2, module, resource) if state == 'list': module.deprecate( 'Using the "list" state has been deprecated. Please use the ec2_tag_info module instead', date='2022-06-01', collection_name='amazon.aws') module.exit_json(changed=False, tags=current_tags) - add_tags, remove = compare_aws_tags(current_tags, tags, purge_tags=purge_tags) - - remove_tags = {} if state == 'absent': + removed_tags = {} for key in tags: if key in current_tags and (tags[key] is None or current_tags[key] == tags[key]): - remove_tags[key] = current_tags[key] - - for key in remove: - remove_tags[key] = current_tags[key] - - if remove_tags: - result['changed'] = True - result['removed_tags'] = remove_tags - if not module.check_mode: - try: - AWSRetry.jittered_backoff()(ec2.delete_tags)(Resources=[resource], Tags=ansible_dict_to_boto3_tag_list(remove_tags)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to remove tags {0} from resource {1}'.format(remove_tags, resource)) - - if state == 'present' and add_tags: - result['changed'] = True - result['added_tags'] = add_tags - current_tags.update(add_tags) - if not module.check_mode: - try: - AWSRetry.jittered_backoff()(ec2.create_tags)(Resources=[resource], Tags=ansible_dict_to_boto3_tag_list(add_tags)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to set tags {0} on resource {1}'.format(add_tags, resource)) - - result['tags'] = get_tags(ec2, module, resource) + result['changed'] = True + removed_tags[key] = current_tags[key] + result['removed_tags'] = removed_tags + remove_ec2_tags(ec2, module, resource, removed_tags.keys()) + + if state == 'present': + tags_to_set, tags_to_unset = compare_aws_tags(current_tags, tags, purge_tags) + if tags_to_unset: + result['removed_tags'] = {} + for key in tags_to_unset: + result['removed_tags'][key] = current_tags[key] + result['added_tags'] = tags_to_set + result['changed'] = ensure_ec2_tags(ec2, module, resource, tags=tags, purge_tags=purge_tags) + + result['tags'] = describe_ec2_tags(ec2, module, resource) module.exit_json(**result) diff --git a/plugins/modules/ec2_tag_info.py b/plugins/modules/ec2_tag_info.py index 947ce363fd0..cf326fd20a5 100644 --- a/plugins/modules/ec2_tag_info.py +++ b/plugins/modules/ec2_tag_info.py @@ -58,13 +58,7 @@ pass # Handled by AnsibleAWSModule from ..module_utils.core import AnsibleAWSModule -from ..module_utils.ec2 import boto3_tag_list_to_ansible_dict, AWSRetry - - -@AWSRetry.jittered_backoff() -def get_tags(ec2, module, resource): - filters = [{'Name': 'resource-id', 'Values': [resource]}] - return boto3_tag_list_to_ansible_dict(ec2.describe_tags(Filters=filters)['Tags']) +from ..module_utils.ec2 import describe_ec2_tags def main(): @@ -76,10 +70,7 @@ def main(): resource = module.params['resource'] ec2 = module.client('ec2') - try: - current_tags = get_tags(ec2, module, resource) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to fetch tags for resource {0}'.format(resource)) + current_tags = describe_ec2_tags(ec2, module, resource) module.exit_json(changed=False, tags=current_tags) diff --git a/plugins/modules/ec2_vol.py b/plugins/modules/ec2_vol.py index 321b0084f31..734432d9561 100644 --- a/plugins/modules/ec2_vol.py +++ b/plugins/modules/ec2_vol.py @@ -250,6 +250,8 @@ from ..module_utils.ec2 import ansible_dict_to_boto3_filter_list from ..module_utils.ec2 import ansible_dict_to_boto3_tag_list from ..module_utils.ec2 import compare_aws_tags +from ..module_utils.ec2 import describe_ec2_tags +from ..module_utils.ec2 import ensure_ec2_tags from ..module_utils.ec2 import AWSRetry from ..module_utils.core import is_boto3_error_code @@ -612,57 +614,8 @@ def get_mapped_block_device(instance_dict=None, device_name=None): def ensure_tags(module, connection, res_id, res_type, tags, purge_tags): - changed = False - - filters = ansible_dict_to_boto3_filter_list({'resource-id': res_id, 'resource-type': res_type}) - cur_tags = None - try: - cur_tags = connection.describe_tags(aws_retry=True, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't describe tags") - - to_update, to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags) - final_tags = boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')) - - if to_update: - try: - if module.check_mode: - # update tags - final_tags.update(to_update) - else: - connection.create_tags( - aws_retry=True, - Resources=[res_id], - Tags=ansible_dict_to_boto3_tag_list(to_update) - ) - - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create tags") - - if to_delete: - try: - if module.check_mode: - # update tags - for key in to_delete: - del final_tags[key] - else: - tags_list = [] - for key in to_delete: - tags_list.append({'Key': key}) - - connection.delete_tags(aws_retry=True, Resources=[res_id], Tags=tags_list) - - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete tags") - - if not module.check_mode and (to_update or to_delete): - try: - response = connection.describe_tags(aws_retry=True, Filters=filters) - final_tags = boto3_tag_list_to_ansible_dict(response.get('Tags')) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't describe tags") + changed = ensure_ec2_tags(connection, module, res_id, res_type, tags, purge_tags, ['InvalidVolume.NotFound']) + final_tags = describe_ec2_tags(connection, module, res_id, res_type) return final_tags, changed diff --git a/plugins/modules/ec2_vpc_subnet.py b/plugins/modules/ec2_vpc_subnet.py index 60d54a33912..2fe34f6ff35 100644 --- a/plugins/modules/ec2_vpc_subnet.py +++ b/plugins/modules/ec2_vpc_subnet.py @@ -219,6 +219,8 @@ from ..module_utils.ec2 import ansible_dict_to_boto3_tag_list from ..module_utils.ec2 import boto3_tag_list_to_ansible_dict from ..module_utils.ec2 import compare_aws_tags +from ..module_utils.ec2 import describe_ec2_tags +from ..module_utils.ec2 import ensure_ec2_tags from ..module_utils.waiters import get_waiter @@ -299,46 +301,13 @@ def create_subnet(conn, module, vpc_id, cidr, ipv6_cidr=None, az=None, start_tim def ensure_tags(conn, module, subnet, tags, purge_tags, start_time): - changed = False - - filters = ansible_dict_to_boto3_filter_list({'resource-id': subnet['id'], 'resource-type': 'subnet'}) - try: - cur_tags = conn.describe_tags(aws_retry=True, Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't describe tags") - - to_update, to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags) - - if to_update: - try: - if not module.check_mode: - AWSRetry.jittered_backoff( - retries=10, - catch_extra_error_codes=['InvalidSubnetID.NotFound'] - )(conn.create_tags)( - Resources=[subnet['id']], - Tags=ansible_dict_to_boto3_tag_list(to_update) - ) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create tags") - - if to_delete: - try: - if not module.check_mode: - tags_list = [] - for key in to_delete: - tags_list.append({'Key': key}) - - AWSRetry.jittered_backoff( - retries=10, - catch_extra_error_codes=['InvalidSubnetID.NotFound'] - )(conn.delete_tags)(Resources=[subnet['id']], Tags=tags_list) - - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete tags") + changed = ensure_ec2_tags( + conn, module, subnet['id'], + resource_type='subnet', + purge_tags=purge_tags, + tags=tags, + retry_codes=['InvalidSubnetID.NotFound']) if module.params['wait'] and not module.check_mode: # Wait for tags to be updated