From 3955e528b129f69c8541831ac11c7a8f8e8c260a Mon Sep 17 00:00:00 2001 From: Ramki Subramanian Date: Mon, 14 May 2018 13:11:48 -0600 Subject: [PATCH] ec2_ami_copy: add tag_equality option for idempotence using tags (#40088) * Allow idempotent use of ec2_ami_copy When `tag_equality` is set true, use tags to determine if AMIs in different accounts are the same, and don't copy the AMI twice if they are the same. Use AnsibleAWSModule and make imports more consistent with other modules * Update version added * More code review changes * Review changes - Recommended way to start EC2 connection --- .../modules/cloud/amazon/ec2_ami_copy.py | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/lib/ansible/modules/cloud/amazon/ec2_ami_copy.py b/lib/ansible/modules/cloud/amazon/ec2_ami_copy.py index df7b121db6e..00a6023dc6f 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_ami_copy.py +++ b/lib/ansible/modules/cloud/amazon/ec2_ami_copy.py @@ -65,6 +65,12 @@ options: tags: description: - A hash/dictionary of tags to add to the new copied AMI; '{"key":"value"}' and '{"key":"value","key":"value"}' + tag_equality: + description: + - Whether to use tags if the source AMI already exists in the target region. If this is set, and all tags match + in an existing AMI, the AMI will not be copied again. + default: false + version_added: 2.6 author: "Amir Moulavi , Tim C " extends_documentation_fragment: - aws @@ -97,7 +103,7 @@ EXAMPLES = ''' name: My-Awesome-AMI description: latest patch -# Tagged AMI copy +# Tagged AMI copy (will not copy the same AMI twice) - ec2_ami_copy: source_region: us-east-1 region: eu-west-1 @@ -105,6 +111,7 @@ EXAMPLES = ''' tags: Name: My-Super-AMI Patch: 1.2.3 + tag_equality: yes # Encrypted AMI copy - ec2_ami_copy: @@ -130,13 +137,12 @@ image_id: sample: ami-e689729e ''' -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (boto3_conn, ec2_argument_spec, get_aws_connection_info) - -import traceback +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import ec2_argument_spec +from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list try: - from botocore.exceptions import ClientError, NoCredentialsError, WaiterError + from botocore.exceptions import ClientError, NoCredentialsError, WaiterError, BotoCoreError HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False @@ -150,6 +156,10 @@ def copy_image(module, ec2): ec2: ec2 connection object """ + image = None + changed = False + tags = module.params.get('tags') + params = {'SourceRegion': module.params.get('source_region'), 'SourceImageId': module.params.get('source_image_id'), 'Name': module.params.get('name'), @@ -160,7 +170,20 @@ def copy_image(module, ec2): params['KmsKeyId'] = module.params.get('kms_key_id') try: - image_id = ec2.copy_image(**params)['ImageId'] + if module.params.get('tag_equality'): + filters = [{'Name': 'tag:%s' % k, 'Values': [v]} for (k, v) in module.params.get('tags').items()] + filters.append(dict(Name='state', Values=['available', 'pending'])) + images = ec2.describe_images(Filters=filters) + if len(images['Images']) > 0: + image = images['Images'][0] + if not image: + image = ec2.copy_image(**params) + image_id = image['ImageId'] + if tags: + ec2.create_tags(Resources=[image_id], + Tags=ansible_dict_to_boto3_tag_list(tags)) + changed = True + if module.params.get('wait'): delay = 15 max_attempts = module.params.get('wait_timeout') // delay @@ -168,19 +191,12 @@ def copy_image(module, ec2): ImageIds=[image_id], WaiterConfig={'Delay': delay, 'MaxAttempts': max_attempts} ) - if module.params.get('tags'): - ec2.create_tags( - Resources=[image_id], - Tags=[{'Key': k, 'Value': v} for k, v in module.params.get('tags').items()] - ) - module.exit_json(changed=True, image_id=image_id) - except WaiterError as we: - module.fail_json(msg='An error occurred waiting for the image to become available. (%s)' % str(we), exception=traceback.format_exc()) - except ClientError as ce: - module.fail_json(msg=ce.message) - except NoCredentialsError: - module.fail_json(msg='Unable to authenticate, AWS credentials are invalid.') + module.exit_json(changed=changed, **camel_dict_to_snake_dict(image)) + except WaiterError as e: + module.fail_json_aws(e, msg='An error occurred waiting for the image to become available') + except (ClientError, BotoCoreError) as e: + module.fail_json_aws(e, msg="Could not copy AMI") except Exception as e: module.fail_json(msg='Unhandled exception. (%s)' % str(e)) @@ -196,14 +212,12 @@ def main(): kms_key_id=dict(type='str', required=False), wait=dict(type='bool', default=False), wait_timeout=dict(type='int', default=600), - tags=dict(type='dict'))) - - module = AnsibleModule(argument_spec=argument_spec) - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - ec2 = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, - **aws_connect_params) + tags=dict(type='dict')), + tag_equality=dict(type='bool', default=False)) + module = AnsibleAWSModule(argument_spec=argument_spec) + # TODO: Check botocore version + ec2 = module.client('ec2') copy_image(module, ec2)