Skip to content

Commit

Permalink
Add common helper for ec2 client 'ensure_tags' (ansible-collections#309)
Browse files Browse the repository at this point in the history
Add common helper for ec2 client 'ensure_tags'

Reviewed-by: Ansibullbot manages ticket workflow
             https://github.com/ansibullbot
  • Loading branch information
tremble authored Apr 23, 2021
1 parent dad4f88 commit 042b8be
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 139 deletions.
6 changes: 6 additions & 0 deletions changelogs/fragments/309-ec2_tags.yml
Original file line number Diff line number Diff line change
@@ -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).
134 changes: 134 additions & 0 deletions plugins/module_utils/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 20 additions & 38 deletions plugins/modules/ec2_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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)


Expand Down
13 changes: 2 additions & 11 deletions plugins/modules/ec2_tag_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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)

Expand Down
55 changes: 4 additions & 51 deletions plugins/modules/ec2_vol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
47 changes: 8 additions & 39 deletions plugins/modules/ec2_vpc_subnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 042b8be

Please sign in to comment.