#!/usr/bin/python # Copyright (c) 2017 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: cloudfront_invalidation short_description: create invalidations for aws cloudfront distributions description: - Allows for invalidation of a batch of paths for a CloudFront distribution. requirements: - boto3 >= 1.0.0 - python >= 2.6 version_added: "2.5" author: Willem van Ketwich (@wilvk) extends_documentation_fragment: - aws - ec2 options: distribution_id: description: - The id of the cloudfront distribution to invalidate paths for. Can be specified insted of the alias. required: false alias: description: - The alias of the cloudfront distribution to invalidate paths for. Can be specified instead of distribution_id. required: false caller_reference: description: - A unique reference identifier for the invalidation paths. required: false default: current datetime stamp target_paths: description: - A list of paths on the distribution to invalidate. Each path should begin with '/'. Wildcards are allowed. eg. '/foo/bar/*' required: true notes: - does not support check mode ''' EXAMPLES = ''' - name: create a batch of invalidations using a distribution_id for a reference cloudfront_invalidation: distribution_id: E15BU8SDCGSG57 caller_reference: testing 123 target_paths: - /testpathone/test1.css - /testpathtwo/test2.js - /testpaththree/test3.ss - name: create a batch of invalidations using an alias as a reference and one path using a wildcard match cloudfront_invalidation: alias: alias.test.com caller_reference: testing 123 target_paths: - /testpathone/test4.css - /testpathtwo/test5.js - /testpaththree/* ''' RETURN = ''' invalidation: description: The invalidation's information. returned: always type: complex contains: create_time: description: The date and time the invalidation request was first made. returned: always type: string sample: '2018-02-01T15:50:41.159000+00:00' id: description: The identifier for the invalidation request. returned: always type: string sample: I2G9MOWJZFV612 invalidation_batch: description: The current invalidation information for the batch request. returned: always type: complex contains: caller_reference: description: The value used to uniquely identify an invalidation request. returned: always type: string sample: testing 123 paths: description: A dict that contains information about the objects that you want to invalidate. returned: always type: complex contains: items: description: A list of the paths that you want to invalidate. returned: always type: list sample: - /testpathtwo/test2.js - /testpathone/test1.css - /testpaththree/test3.ss quantity: description: The number of objects that you want to invalidate. returned: always type: int sample: 3 status: description: The status of the invalidation request. returned: always type: string sample: Completed location: description: The fully qualified URI of the distribution and invalidation batch request. returned: always type: string sample: https://cloudfront.amazonaws.com/2017-03-25/distribution/E1ZID6KZJECZY7/invalidation/I2G9MOWJZFV622 ''' from ansible.module_utils.ec2 import get_aws_connection_info from ansible.module_utils.ec2 import ec2_argument_spec, boto3_conn from ansible.module_utils.ec2 import snake_dict_to_camel_dict from ansible.module_utils.ec2 import camel_dict_to_snake_dict from ansible.module_utils.aws.core import AnsibleAWSModule from ansible.module_utils.aws.cloudfront_facts import CloudFrontFactsServiceManager import datetime try: from botocore.exceptions import ClientError, BotoCoreError except ImportError: pass # caught by imported AnsibleAWSModule class CloudFrontInvalidationServiceManager(object): """ Handles CloudFront service calls to AWS for invalidations """ def __init__(self, module): self.module = module self.create_client('cloudfront') def create_client(self, resource): region, ec2_url, aws_connect_kwargs = get_aws_connection_info(self.module, boto3=True) self.client = boto3_conn(self.module, conn_type='client', resource=resource, region=region, endpoint=ec2_url, **aws_connect_kwargs) def create_invalidation(self, distribution_id, invalidation_batch): current_invalidation_response = self.get_invalidation(distribution_id, invalidation_batch['CallerReference']) try: response = self.client.create_invalidation(DistributionId=distribution_id, InvalidationBatch=invalidation_batch) response.pop('ResponseMetadata', None) if current_invalidation_response: return response, False else: return response, True except BotoCoreError as e: self.module.fail_json_aws(e, msg="Error creating CloudFront invalidations.") except ClientError as e: if ('Your request contains a caller reference that was used for a previous invalidation batch ' 'for the same distribution.' in e.response['Error']['Message']): self.module.warn("InvalidationBatch target paths are not modifiable. " "To make a new invalidation please update caller_reference.") return current_invalidation_response, False else: self.module.fail_json_aws(e, msg="Error creating CloudFront invalidations.") def get_invalidation(self, distribution_id, caller_reference): current_invalidation = {} # find all invalidations for the distribution try: paginator = self.client.get_paginator('list_invalidations') invalidations = paginator.paginate(DistributionId=distribution_id).build_full_result().get('InvalidationList', {}).get('Items', []) invalidation_ids = [inv['Id'] for inv in invalidations] except (BotoCoreError, ClientError) as e: self.module.fail_json_aws(e, msg="Error listing CloudFront invalidations.") # check if there is an invalidation with the same caller reference for inv_id in invalidation_ids: try: invalidation = self.client.get_invalidation(DistributionId=distribution_id, Id=inv_id)['Invalidation'] caller_ref = invalidation.get('InvalidationBatch', {}).get('CallerReference') except (BotoCoreError, ClientError) as e: self.module.fail_json_aws(e, msg="Error getting Cloudfront invalidation {0}".format(inv_id)) if caller_ref == caller_reference: current_invalidation = invalidation break current_invalidation.pop('ResponseMetadata', None) return current_invalidation class CloudFrontInvalidationValidationManager(object): """ Manages Cloudfront validations for invalidation batches """ def __init__(self, module): self.module = module self.__cloudfront_facts_mgr = CloudFrontFactsServiceManager(module) def validate_distribution_id(self, distribution_id, alias): try: if distribution_id is None and alias is None: self.module.fail_json(msg="distribution_id or alias must be specified") if distribution_id is None: distribution_id = self.__cloudfront_facts_mgr.get_distribution_id_from_domain_name(alias) return distribution_id except (ClientError, BotoCoreError) as e: self.module.fail_json_aws(e, msg="Error validating parameters.") def create_aws_list(self, invalidation_batch): aws_list = {} aws_list["Quantity"] = len(invalidation_batch) aws_list["Items"] = invalidation_batch return aws_list def validate_invalidation_batch(self, invalidation_batch, caller_reference): try: if caller_reference is not None: valid_caller_reference = caller_reference else: valid_caller_reference = datetime.datetime.now().isoformat() valid_invalidation_batch = { 'paths': self.create_aws_list(invalidation_batch), 'caller_reference': valid_caller_reference } return valid_invalidation_batch except (ClientError, BotoCoreError) as e: self.module.fail_json_aws(e, msg="Error validating invalidation batch.") def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( caller_reference=dict(), distribution_id=dict(), alias=dict(), target_paths=dict(required=True, type='list') )) module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=False, mutually_exclusive=[['distribution_id', 'alias']]) validation_mgr = CloudFrontInvalidationValidationManager(module) service_mgr = CloudFrontInvalidationServiceManager(module) caller_reference = module.params.get('caller_reference') distribution_id = module.params.get('distribution_id') alias = module.params.get('alias') target_paths = module.params.get('target_paths') result = {} distribution_id = validation_mgr.validate_distribution_id(distribution_id, alias) valid_target_paths = validation_mgr.validate_invalidation_batch(target_paths, caller_reference) valid_pascal_target_paths = snake_dict_to_camel_dict(valid_target_paths, True) result, changed = service_mgr.create_invalidation(distribution_id, valid_pascal_target_paths) module.exit_json(changed=changed, **camel_dict_to_snake_dict(result)) if __name__ == '__main__': main()