Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
ansible / community / aws / plugins / modules / cloudfront_distribution.py
Size: Mime:
#!/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)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type


DOCUMENTATION = r'''
---

version_added: 1.0.0
module: cloudfront_distribution

short_description: Create, update and delete AWS CloudFront distributions.

description:
    - Allows for easy creation, updating and deletion of CloudFront distributions.

author:
  - Willem van Ketwich (@wilvk)
  - Will Thames (@willthames)

extends_documentation_fragment:
- amazon.aws.aws
- amazon.aws.ec2


options:

    state:
      description:
        - The desired state of the distribution.
        - I(state=present) creates a new distribution or updates an existing distribution.
        - I(state=absent) deletes an existing distribution.
      choices: ['present', 'absent']
      default: 'present'
      type: str

    distribution_id:
      description:
        - The ID of the CloudFront distribution.
        - This parameter can be exchanged with I(alias) or I(caller_reference) and is used in conjunction with I(e_tag).
      type: str

    e_tag:
      description:
        - A unique identifier of a modified or existing distribution. Used in conjunction with I(distribution_id).
        - Is determined automatically if not specified.
      type: str

    caller_reference:
      description:
        - A unique identifier for creating and updating CloudFront distributions.
        - Each caller reference must be unique across all distributions. e.g. a caller reference used in a web
          distribution cannot be reused in a streaming distribution. This parameter can be used instead of I(distribution_id)
          to reference an existing distribution. If not specified, this defaults to a datetime stamp of the format
          C(YYYY-MM-DDTHH:MM:SS.ffffff).
      type: str

    tags:
      description:
        - Should be input as a dict of key-value pairs.
        - "Note that numeric keys or values must be wrapped in quotes. e.g. C(Priority: '1')"
      type: dict

    purge_tags:
      description:
        - Specifies whether existing tags will be removed before adding new tags.
        - When I(purge_tags=yes), existing tags are removed and I(tags) are added, if specified.
          If no tags are specified, it removes all existing tags for the distribution.
        - When I(purge_tags=no), existing tags are kept and I(tags) are added, if specified.
      default: false
      type: bool

    alias:
      description:
        - The name of an alias (CNAME) that is used in a distribution. This is used to effectively reference a distribution by its alias as an alias can only
          be used by one distribution per AWS account. This variable avoids having to provide the I(distribution_id) as well as
          the I(e_tag), or I(caller_reference) of an existing distribution.
      type: str

    aliases:
      description:
        - A list of domain name aliases (CNAMEs) as strings to be used for the distribution.
        - Each alias must be unique across all distribution for the AWS account.
      type: list
      elements: str

    purge_aliases:
      description:
        - Specifies whether existing aliases will be removed before adding new aliases.
        - When I(purge_aliases=yes), existing aliases are removed and I(aliases) are added.
      default: false
      type: bool

    default_root_object:
      description:
        - A config element that specifies the path to request when the user requests the origin.
        - e.g. if specified as 'index.html', this maps to www.example.com/index.html when www.example.com is called by the user.
        - This prevents the entire distribution origin from being exposed at the root.
      type: str

    default_origin_domain_name:
      description:
        - The domain name to use for an origin if no I(origins) have been specified.
        - Should only be used on a first run of generating a distribution and not on
          subsequent runs.
        - Should not be used in conjunction with I(distribution_id), I(caller_reference) or I(alias).
      type: str

    default_origin_path:
      description:
        - The default origin path to specify for an origin if no I(origins) have been specified. Defaults to empty if not specified.
      type: str

    origins:
      type: list
      elements: dict
      description:
        - A config element that is a list of complex origin objects to be specified for the distribution. Used for creating and updating distributions.
      suboptions:
        id:
          description: A unique identifier for the origin or origin group. I(id) must be unique within the distribution.
          type: str
        domain_name:
          description:
            - The domain name which CloudFront will query as the origin.
            - For more information see the CloudFront documentation
              at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesDomainName)
          type: str
        origin_path:
          description: Tells CloudFront to request your content from a directory in your Amazon S3 bucket or your custom origin.
          type: str
        custom_headers:
          description:
            - Custom headers you wish to add to the request before passing it to the origin.
            - For more information see the CloudFront documentation
              at U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/forward-custom-headers.html).
          type: list
          elements: dict
          suboptions:
            header_name:
              description: The name of a header that you want CloudFront to forward to your origin.
              type: str
            header_value:
              description: The value for the header that you specified in the I(header_name) field.
              type: str
        s3_origin_access_identity_enabled:
          description:
            - Use an origin access identity to configure the origin so that viewers can only access objects in an Amazon S3 bucket through CloudFront.
            - Will automatically create an Identity for you if no I(s3_origin_config) is specified.
            - See also U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html).
          type: bool
        s3_origin_config:
          description: Specify origin access identity for S3 origins.
          type: dict
          suboptions:
            origin_access_identity:
              description: Existing origin access identity in the format C(origin-access-identity/cloudfront/OID_ID).
              type: str
        custom_origin_config:
          description: Connection information about the origin.
          type: dict
          suboptions:
            http_port:
              description: The HTTP port the custom origin listens on.
              type: int
            https_port:
              description: The HTTPS port the custom origin listens on.
              type: int
            origin_protocol_policy:
              description: The origin protocol policy to apply to your origin.
              type: str
            origin_ssl_protocols:
              description: A list of SSL/TLS protocols that you want CloudFront to use when communicating to the origin over HTTPS.
              type: list
              elements: str
            origin_read_timeout:
              description: A timeout (in seconds) when reading from your origin.
              type: int
            origin_keepalive_timeout:
              description: A keep-alive timeout (in seconds).
              type: int

    purge_origins:
      description: Whether to remove any origins that aren't listed in I(origins).
      default: false
      type: bool

    default_cache_behavior:
      type: dict
      description:
        - A dict specifying the default cache behavior of the distribution.
        - If not specified, the I(target_origin_id) is defined as the I(target_origin_id) of the first valid
          cache_behavior in I(cache_behaviors) with defaults.
      suboptions:
        target_origin_id:
          description:
            - The ID of the origin that you want CloudFront to route requests to
              by default.
          type: str
        response_headers_policy_id:
          description:
            - The ID of the header policy that CloudFront adds to responses that it sends to viewers.
          type: str
        forwarded_values:
          description:
            - A dict that specifies how CloudFront handles query strings and cookies.
          type: dict
          suboptions:
            query_string:
              description:
                - Indicates whether you want CloudFront to forward query strings
                  to the origin that is associated with this cache behavior.
              type: bool
            cookies:
              description: A dict that specifies whether you want CloudFront to forward cookies to the origin and, if so, which ones.
              type: dict
              suboptions:
                forward:
                  description:
                    - Specifies which cookies to forward to the origin for this cache behavior.
                    - Valid values are C(all), C(none), or C(whitelist).
                  type: str
                whitelisted_names:
                  type: list
                  elements: str
                  description: A list of cookies to forward to the origin for this cache behavior.
            headers:
              description:
              - A list of headers to forward to the origin for this cache behavior.
              - To forward all headers use a list containing a single element '*' (C(['*']))
              type: list
              elements: str
            query_string_cache_keys:
              description:
                - A list that contains the query string parameters you want CloudFront to use as a basis for caching for a cache behavior.
              type: list
              elements: str
            trusted_signers:
              description:
                - A dict that specifies the AWS accounts that you want to allow to create signed URLs for private content.
              type: dict
              suboptions:
                enabled:
                  description: Whether you want to require viewers to use signed URLs to access the files specified by I(target_origin_id)
                  type: bool
                items:
                  description: A list of trusted signers for this cache behavior.
                  elements: str
                  type: list
            viewer_protocol_policy:
              description:
                - The protocol that viewers can use to access the files in the origin specified by I(target_origin_id).
                - Valid values are C(allow-all), C(redirect-to-https) and C(https-only).
              type: str
            default_ttl:
              description: The default amount of time that you want objects to stay in CloudFront caches.
              type: int
            max_ttl:
              description: The maximum amount of time that you want objects to stay in CloudFront caches.
              type: int
            min_ttl:
              description: The minimum amount of time that you want objects to stay in CloudFront caches.
              type: int
            allowed_methods:
              description: A dict that controls which HTTP methods CloudFront processes and forwards.
              type: dict
              suboptions:
                items:
                  description: A list of HTTP methods that you want CloudFront to process and forward.
                  type: list
                  elements: str
                cached_methods:
                  description:
                    - A list of HTTP methods that you want CloudFront to apply caching to.
                    - This can either be C([GET,HEAD]), or C([GET,HEAD,OPTIONS]).
                  type: list
                  elements: str
            smooth_streaming:
              description:
                - Whether you want to distribute media files in the Microsoft Smooth Streaming format.
              type: bool
            compress:
              description:
                - Whether you want CloudFront to automatically compress files.
              type: bool
            lambda_function_associations:
              description:
                - A list of Lambda function associations to use for this cache behavior.
              type: list
              elements: dict
              suboptions:
                lambda_function_arn:
                  description: The ARN of the Lambda function.
                  type: str
                event_type:
                  description:
                    - Specifies the event type that triggers a Lambda function invocation.
                    - This can be C(viewer-request), C(origin-request), C(origin-response) or C(viewer-response).
                  type: str
            field_level_encryption_id:
              description:
                - The field-level encryption configuration that you want CloudFront to use for encrypting specific fields of data.
              type: str

    cache_behaviors:
      type: list
      elements: dict
      description:
        - A list of dictionaries describing the cache behaviors for the distribution.
        - The order of the list is preserved across runs unless I(purge_cache_behaviors) is enabled.
      suboptions:
        path_pattern:
          description:
            - The pattern that specifies which requests to apply the behavior to.
          type: str
        target_origin_id:
          description:
            - The ID of the origin that you want CloudFront to route requests to
              by default.
          type: str
        response_headers_policy_id:
          description:
            - The ID of the header policy that CloudFront adds to responses that it sends to viewers.
          type: str
        forwarded_values:
          description:
            - A dict that specifies how CloudFront handles query strings and cookies.
          type: dict
          suboptions:
            query_string:
              description:
                - Indicates whether you want CloudFront to forward query strings
                  to the origin that is associated with this cache behavior.
              type: bool
            cookies:
              description: A dict that specifies whether you want CloudFront to forward cookies to the origin and, if so, which ones.
              type: dict
              suboptions:
                forward:
                  description:
                    - Specifies which cookies to forward to the origin for this cache behavior.
                    - Valid values are C(all), C(none), or C(whitelist).
                  type: str
                whitelisted_names:
                  type: list
                  elements: str
                  description: A list of cookies to forward to the origin for this cache behavior.
            headers:
              description:
              - A list of headers to forward to the origin for this cache behavior.
              - To forward all headers use a list containing a single element '*' (C(['*']))
              type: list
              elements: str
            query_string_cache_keys:
              description:
                - A list that contains the query string parameters you want CloudFront to use as a basis for caching for a cache behavior.
              type: list
              elements: str
            trusted_signers:
              description:
                - A dict that specifies the AWS accounts that you want to allow to create signed URLs for private content.
              type: dict
              suboptions:
                enabled:
                  description: Whether you want to require viewers to use signed URLs to access the files specified by I(path_pattern) and I(target_origin_id)
                  type: bool
                items:
                  description: A list of trusted signers for this cache behavior.
                  elements: str
                  type: list
            viewer_protocol_policy:
              description:
                - The protocol that viewers can use to access the files in the origin specified by I(target_origin_id) when a request matches I(path_pattern).
                - Valid values are C(allow-all), C(redirect-to-https) and C(https-only).
              type: str
            default_ttl:
              description: The default amount of time that you want objects to stay in CloudFront caches.
              type: int
            max_ttl:
              description: The maximum amount of time that you want objects to stay in CloudFront caches.
              type: int
            min_ttl:
              description: The minimum amount of time that you want objects to stay in CloudFront caches.
              type: int
            allowed_methods:
              description: A dict that controls which HTTP methods CloudFront processes and forwards.
              type: dict
              suboptions:
                items:
                  description: A list of HTTP methods that you want CloudFront to process and forward.
                  type: list
                  elements: str
                cached_methods:
                  description:
                    - A list of HTTP methods that you want CloudFront to apply caching to.
                    - This can either be C([GET,HEAD]), or C([GET,HEAD,OPTIONS]).
                  type: list
                  elements: str
            smooth_streaming:
              description:
                - Whether you want to distribute media files in the Microsoft Smooth Streaming format.
              type: bool
            compress:
              description:
                - Whether you want CloudFront to automatically compress files.
              type: bool
            lambda_function_associations:
              description:
                - A list of Lambda function associations to use for this cache behavior.
              type: list
              elements: dict
              suboptions:
                lambda_function_arn:
                  description: The ARN of the Lambda function.
                  type: str
                event_type:
                  description:
                    - Specifies the event type that triggers a Lambda function invocation.
                    - This can be C(viewer-request), C(origin-request), C(origin-response) or C(viewer-response).
                  type: str
            field_level_encryption_id:
              description:
                - The field-level encryption configuration that you want CloudFront to use for encrypting specific fields of data.
              type: str


    purge_cache_behaviors:
      description:
        - Whether to remove any cache behaviors that aren't listed in I(cache_behaviors).
        - This switch also allows the reordering of I(cache_behaviors).
      default: false
      type: bool

    custom_error_responses:
      type: list
      elements: dict
      description:
        - A config element that is a I(list[]) of complex custom error responses to be specified for the distribution.
        - This attribute configures custom http error messages returned to the user.
      suboptions:
        error_code:
          type: int
          description: The error code the custom error page is for.
        error_caching_min_ttl:
          type: int
          description: The length of time (in seconds) that CloudFront will cache status codes for.
        response_code:
          type: int
          description:
            - The HTTP status code that CloudFront should return to a user when the origin returns the HTTP status code specified by I(error_code).
        response_page_path:
          type: str
          description:
            - The path to the custom error page that you want CloudFront to return to a viewer when your origin returns
              the HTTP status code specified by I(error_code).

    purge_custom_error_responses:
      description: Whether to remove any custom error responses that aren't listed in I(custom_error_responses).
      default: false
      type: bool

    comment:
      description:
        - A comment that describes the CloudFront distribution.
        - If not specified, it defaults to a generic message that it has been created with Ansible, and a datetime stamp.
      type: str

    logging:
      description:
        - A config element that is a complex object that defines logging for the distribution.
      suboptions:
        enabled:
          description: When I(enabled=true) CloudFront will log access to an S3 bucket.
          type: bool
        include_cookies:
          description: When I(include_cookies=true) CloudFront will include cookies in the logs.
          type: bool
        bucket:
          description: The S3 bucket to store the log in.
          type: str
        prefix:
          description: A prefix to include in the S3 object names.
          type: str
      type: dict

    price_class:
      description:
        - A string that specifies the pricing class of the distribution. As per
          U(https://aws.amazon.com/cloudfront/pricing/)
        - I(price_class=PriceClass_100) consists of the areas United States, Canada and Europe.
        - I(price_class=PriceClass_200) consists of the areas United States, Canada, Europe, Japan, India,
          Hong Kong, Philippines, S. Korea, Singapore & Taiwan.
        - I(price_class=PriceClass_All) consists of the areas United States, Canada, Europe, Japan, India,
          South America, Australia, Hong Kong, Philippines, S. Korea, Singapore & Taiwan.
        - AWS defaults this to C(PriceClass_All).
        - Valid values are C(PriceClass_100), C(PriceClass_200) and C(PriceClass_All)
      type: str

    enabled:
      description:
        - A boolean value that specifies whether the distribution is enabled or disabled.
        - Defaults to C(false).
      type: bool

    viewer_certificate:
      type: dict
      description:
        - A dict that specifies the encryption details of the distribution.
      suboptions:
        cloudfront_default_certificate:
          type: bool
          description:
            - If you're using the CloudFront domain name for your distribution, such as C(123456789abcde.cloudfront.net)
              you should set I(cloudfront_default_certificate=true).
            - If I(cloudfront_default_certificate=true) do not set I(ssl_support_method).
        iam_certificate_id:
          type: str
          description:
            - The ID of a certificate stored in IAM to use for HTTPS connections.
            - If I(iam_certificate_id) is set then you must also specify I(ssl_support_method).
        acm_certificate_arn:
          type: str
          description:
            - The ID of a certificate stored in ACM to use for HTTPS connections.
            - If I(acm_certificate_id) is set then you must also specify I(ssl_support_method).
        ssl_support_method:
          type: str
          description:
            - How CloudFront should serve SSL certificates.
            - Valid values are C(sni-only) for SNI, and C(vip) if CloudFront is configured to use a dedicated IP for your content.
        minimum_protocol_version:
          type: str
          description:
            - The security policy that you want CloudFront to use for HTTPS connections.
            - See U(https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/secure-connections-supported-viewer-protocols-ciphers.html)
              for supported security policies.

    restrictions:
      type: dict
      description:
        - A config element that is a complex object that describes how a distribution should restrict it's content.
      suboptions:
        geo_restriction:
          description: Apply a restriction based on the location of the requester.
          type: dict
          suboptions:
            restriction_type:
              type: str
              description:
              - The method that you want to use to restrict distribution of your content by country.
              - Valid values are C(none), C(whitelist), C(blacklist).
            items:
              description:
              - A list of ISO 3166-1 two letter (Alpha 2) country codes that the
                restriction should apply to.
              - 'See the ISO website for a full list of codes U(https://www.iso.org/obp/ui/#search/code/).'
              type: list

    web_acl_id:
      description:
        - The ID of a Web Application Firewall (WAF) Access Control List (ACL).
      type: str

    http_version:
      description:
        - The version of the http protocol to use for the distribution.
        - AWS defaults this to C(http2).
        - Valid values are C(http1.1) and C(http2).
      type: str

    ipv6_enabled:
      description:
        - Determines whether IPv6 support is enabled or not.
        - Defaults to C(false).
      type: bool

    wait:
      description:
        - Specifies whether the module waits until the distribution has completed processing the creation or update.
      type: bool
      default: false

    wait_timeout:
      description:
        - Specifies the duration in seconds to wait for a timeout of a cloudfront create or update.
      default: 1800
      type: int

'''

EXAMPLES = r'''
- name: create a basic distribution with defaults and tags
  community.aws.cloudfront_distribution:
    state: present
    default_origin_domain_name: www.my-cloudfront-origin.com
    tags:
      Name: example distribution
      Project: example project
      Priority: '1'

- name: update a distribution comment by distribution_id
  community.aws.cloudfront_distribution:
    state: present
    distribution_id: E1RP5A2MJ8073O
    comment: modified by ansible cloudfront.py

- name: update a distribution comment by caller_reference
  community.aws.cloudfront_distribution:
    state: present
    caller_reference: my cloudfront distribution 001
    comment: modified by ansible cloudfront.py

- name: update a distribution's aliases and comment using the distribution_id as a reference
  community.aws.cloudfront_distribution:
    state: present
    distribution_id: E1RP5A2MJ8073O
    comment: modified by cloudfront.py again
    aliases: [ 'www.my-distribution-source.com', 'zzz.aaa.io' ]

- name: update a distribution's aliases and comment using an alias as a reference
  community.aws.cloudfront_distribution:
    state: present
    caller_reference: my test distribution
    comment: modified by cloudfront.py again
    aliases:
      - www.my-distribution-source.com
      - zzz.aaa.io

- name: update a distribution's comment and aliases and tags and remove existing tags
  community.aws.cloudfront_distribution:
    state: present
    distribution_id: E15BU8SDCGSG57
    comment: modified by cloudfront.py again
    aliases:
      - tested.com
    tags:
      Project: distribution 1.2
    purge_tags: yes

- name: create a distribution with an origin, logging and default cache behavior
  community.aws.cloudfront_distribution:
    state: present
    caller_reference: unique test distribution ID
    origins:
        - id: 'my test origin-000111'
          domain_name: www.example.com
          origin_path: /production
          custom_headers:
            - header_name: MyCustomHeaderName
              header_value: MyCustomHeaderValue
    default_cache_behavior:
      target_origin_id: 'my test origin-000111'
      forwarded_values:
        query_string: true
        cookies:
          forward: all
        headers:
         - '*'
      viewer_protocol_policy: allow-all
      smooth_streaming: true
      compress: true
      allowed_methods:
        items:
          - GET
          - HEAD
        cached_methods:
          - GET
          - HEAD
    logging:
      enabled: true
      include_cookies: false
      bucket: mylogbucket.s3.amazonaws.com
      prefix: myprefix/
    enabled: false
    comment: this is a CloudFront distribution with logging

- name: delete a distribution
  community.aws.cloudfront_distribution:
    state: absent
    caller_reference: replaceable distribution
'''

RETURN = r'''
active_trusted_signers:
  description: Key pair IDs that CloudFront is aware of for each trusted signer.
  returned: always
  type: complex
  contains:
    enabled:
      description: Whether trusted signers are in use.
      returned: always
      type: bool
      sample: false
    quantity:
      description: Number of trusted signers.
      returned: always
      type: int
      sample: 1
    items:
      description: Number of trusted signers.
      returned: when there are trusted signers
      type: list
      sample:
      - key_pair_id
aliases:
  description: Aliases that refer to the distribution.
  returned: always
  type: complex
  contains:
    items:
      description: List of aliases.
      returned: always
      type: list
      sample:
      - test.example.com
    quantity:
      description: Number of aliases.
      returned: always
      type: int
      sample: 1
arn:
  description: Amazon Resource Name of the distribution.
  returned: always
  type: str
  sample: arn:aws:cloudfront::123456789012:distribution/E1234ABCDEFGHI
cache_behaviors:
  description: CloudFront cache behaviors.
  returned: always
  type: complex
  contains:
    items:
      description: List of cache behaviors.
      returned: always
      type: complex
      contains:
        allowed_methods:
          description: Methods allowed by the cache behavior.
          returned: always
          type: complex
          contains:
            cached_methods:
              description: Methods cached by the cache behavior.
              returned: always
              type: complex
              contains:
                items:
                  description: List of cached methods.
                  returned: always
                  type: list
                  sample:
                  - HEAD
                  - GET
                quantity:
                  description: Count of cached methods.
                  returned: always
                  type: int
                  sample: 2
            items:
              description: List of methods allowed by the cache behavior.
              returned: always
              type: list
              sample:
              - HEAD
              - GET
            quantity:
              description: Count of methods allowed by the cache behavior.
              returned: always
              type: int
              sample: 2
        compress:
          description: Whether compression is turned on for the cache behavior.
          returned: always
          type: bool
          sample: false
        default_ttl:
          description: Default Time to Live of the cache behavior.
          returned: always
          type: int
          sample: 86400
        forwarded_values:
          description: Values forwarded to the origin for this cache behavior.
          returned: always
          type: complex
          contains:
            cookies:
              description: Cookies to forward to the origin.
              returned: always
              type: complex
              contains:
                forward:
                  description: Which cookies to forward to the origin for this cache behavior.
                  returned: always
                  type: str
                  sample: none
                whitelisted_names:
                  description: The names of the cookies to forward to the origin for this cache behavior.
                  returned: when I(forward=whitelist)
                  type: complex
                  contains:
                    quantity:
                      description: Count of cookies to forward.
                      returned: always
                      type: int
                      sample: 1
                    items:
                      description: List of cookies to forward.
                      returned: when list is not empty
                      type: list
                      sample: my_cookie
            headers:
              description: Which headers are used to vary on cache retrievals.
              returned: always
              type: complex
              contains:
                quantity:
                  description: Count of headers to vary on.
                  returned: always
                  type: int
                  sample: 1
                items:
                  description: List of headers to vary on.
                  returned: when list is not empty
                  type: list
                  sample:
                  - Host
            query_string:
              description: Whether the query string is used in cache lookups.
              returned: always
              type: bool
              sample: false
            query_string_cache_keys:
              description: Which query string keys to use in cache lookups.
              returned: always
              type: complex
              contains:
                quantity:
                  description: Count of query string cache keys to use in cache lookups.
                  returned: always
                  type: int
                  sample: 1
                items:
                  description: List of query string cache keys to use in cache lookups.
                  returned: when list is not empty
                  type: list
                  sample:
        lambda_function_associations:
          description: Lambda function associations for a cache behavior.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of lambda function associations.
              returned: always
              type: int
              sample: 1
            items:
              description: List of lambda function associations.
              returned: when list is not empty
              type: list
              sample:
              - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function
                event_type: viewer-response
        max_ttl:
          description: Maximum Time to Live.
          returned: always
          type: int
          sample: 31536000
        min_ttl:
          description: Minimum Time to Live.
          returned: always
          type: int
          sample: 0
        path_pattern:
          description: Path pattern that determines this cache behavior.
          returned: always
          type: str
          sample: /path/to/files/*
        smooth_streaming:
          description: Whether smooth streaming is enabled.
          returned: always
          type: bool
          sample: false
        target_origin_id:
          description: ID of origin reference by this cache behavior.
          returned: always
          type: str
          sample: origin_abcd
        trusted_signers:
          description: Trusted signers.
          returned: always
          type: complex
          contains:
            enabled:
              description: Whether trusted signers are enabled for this cache behavior.
              returned: always
              type: bool
              sample: false
            quantity:
              description: Count of trusted signers.
              returned: always
              type: int
              sample: 1
        viewer_protocol_policy:
          description: Policy of how to handle http/https.
          returned: always
          type: str
          sample: redirect-to-https
    quantity:
      description: Count of cache behaviors.
      returned: always
      type: int
      sample: 1

caller_reference:
  description: Idempotency reference given when creating CloudFront distribution.
  returned: always
  type: str
  sample: '1484796016700'
comment:
  description: Any comments you want to include about the distribution.
  returned: always
  type: str
  sample: 'my first CloudFront distribution'
custom_error_responses:
  description: Custom error responses to use for error handling.
  returned: always
  type: complex
  contains:
    items:
      description: List of custom error responses.
      returned: always
      type: complex
      contains:
        error_caching_min_ttl:
          description: Minimum time to cache this error response.
          returned: always
          type: int
          sample: 300
        error_code:
          description: Origin response code that triggers this error response.
          returned: always
          type: int
          sample: 500
        response_code:
          description: Response code to return to the requester.
          returned: always
          type: str
          sample: '500'
        response_page_path:
          description: Path that contains the error page to display.
          returned: always
          type: str
          sample: /errors/5xx.html
    quantity:
      description: Count of custom error response items
      returned: always
      type: int
      sample: 1
default_cache_behavior:
  description: Default cache behavior.
  returned: always
  type: complex
  contains:
    allowed_methods:
      description: Methods allowed by the cache behavior.
      returned: always
      type: complex
      contains:
        cached_methods:
          description: Methods cached by the cache behavior.
          returned: always
          type: complex
          contains:
            items:
              description: List of cached methods.
              returned: always
              type: list
              sample:
              - HEAD
              - GET
            quantity:
              description: Count of cached methods.
              returned: always
              type: int
              sample: 2
        items:
          description: List of methods allowed by the cache behavior.
          returned: always
          type: list
          sample:
          - HEAD
          - GET
        quantity:
          description: Count of methods allowed by the cache behavior.
          returned: always
          type: int
          sample: 2
    compress:
      description: Whether compression is turned on for the cache behavior.
      returned: always
      type: bool
      sample: false
    default_ttl:
      description: Default Time to Live of the cache behavior.
      returned: always
      type: int
      sample: 86400
    forwarded_values:
      description: Values forwarded to the origin for this cache behavior.
      returned: always
      type: complex
      contains:
        cookies:
          description: Cookies to forward to the origin.
          returned: always
          type: complex
          contains:
            forward:
              description: Which cookies to forward to the origin for this cache behavior.
              returned: always
              type: str
              sample: none
            whitelisted_names:
              description: The names of the cookies to forward to the origin for this cache behavior.
              returned: when I(forward=whitelist)
              type: complex
              contains:
                quantity:
                  description: Count of cookies to forward.
                  returned: always
                  type: int
                  sample: 1
                items:
                  description: List of cookies to forward.
                  returned: when list is not empty
                  type: list
                  sample: my_cookie
        headers:
          description: Which headers are used to vary on cache retrievals.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of headers to vary on.
              returned: always
              type: int
              sample: 1
            items:
              description: List of headers to vary on.
              returned: when list is not empty
              type: list
              sample:
              - Host
        query_string:
          description: Whether the query string is used in cache lookups.
          returned: always
          type: bool
          sample: false
        query_string_cache_keys:
          description: Which query string keys to use in cache lookups.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of query string cache keys to use in cache lookups.
              returned: always
              type: int
              sample: 1
            items:
              description: List of query string cache keys to use in cache lookups.
              returned: when list is not empty
              type: list
              sample:
    lambda_function_associations:
      description: Lambda function associations for a cache behavior.
      returned: always
      type: complex
      contains:
        quantity:
          description: Count of lambda function associations.
          returned: always
          type: int
          sample: 1
        items:
          description: List of lambda function associations.
          returned: when list is not empty
          type: list
          sample:
          - lambda_function_arn: arn:aws:lambda:123456789012:us-east-1/lambda/lambda-function
            event_type: viewer-response
    max_ttl:
      description: Maximum Time to Live.
      returned: always
      type: int
      sample: 31536000
    min_ttl:
      description: Minimum Time to Live.
      returned: always
      type: int
      sample: 0
    path_pattern:
      description: Path pattern that determines this cache behavior.
      returned: always
      type: str
      sample: /path/to/files/*
    smooth_streaming:
      description: Whether smooth streaming is enabled.
      returned: always
      type: bool
      sample: false
    target_origin_id:
      description: ID of origin reference by this cache behavior.
      returned: always
      type: str
      sample: origin_abcd
    trusted_signers:
      description: Trusted signers.
      returned: always
      type: complex
      contains:
        enabled:
          description: Whether trusted signers are enabled for this cache behavior.
          returned: always
          type: bool
          sample: false
        quantity:
          description: Count of trusted signers.
          returned: always
          type: int
          sample: 1
    viewer_protocol_policy:
      description: Policy of how to handle http/https.
      returned: always
      type: str
      sample: redirect-to-https
default_root_object:
  description: The object that you want CloudFront to request from your origin (for example, index.html)
    when a viewer requests the root URL for your distribution.
  returned: always
  type: str
  sample: ''
diff:
  description: Difference between previous configuration and new configuration.
  returned: always
  type: dict
  sample: {}
domain_name:
  description: Domain name of CloudFront distribution.
  returned: always
  type: str
  sample: d1vz8pzgurxosf.cloudfront.net
enabled:
  description: Whether the CloudFront distribution is enabled or not.
  returned: always
  type: bool
  sample: true
http_version:
  description: Version of HTTP supported by the distribution.
  returned: always
  type: str
  sample: http2
id:
  description: CloudFront distribution ID.
  returned: always
  type: str
  sample: E123456ABCDEFG
in_progress_invalidation_batches:
  description: The number of invalidation batches currently in progress.
  returned: always
  type: int
  sample: 0
is_ipv6_enabled:
  description: Whether IPv6 is enabled.
  returned: always
  type: bool
  sample: true
last_modified_time:
  description: Date and time distribution was last modified.
  returned: always
  type: str
  sample: '2017-10-13T01:51:12.656000+00:00'
logging:
  description: Logging information.
  returned: always
  type: complex
  contains:
    bucket:
      description: S3 bucket logging destination.
      returned: always
      type: str
      sample: logs-example-com.s3.amazonaws.com
    enabled:
      description: Whether logging is enabled.
      returned: always
      type: bool
      sample: true
    include_cookies:
      description: Whether to log cookies.
      returned: always
      type: bool
      sample: false
    prefix:
      description: Prefix added to logging object names.
      returned: always
      type: str
      sample: cloudfront/test
origins:
  description: Origins in the CloudFront distribution.
  returned: always
  type: complex
  contains:
    items:
      description: List of origins.
      returned: always
      type: complex
      contains:
        custom_headers:
          description: Custom headers passed to the origin.
          returned: always
          type: complex
          contains:
            quantity:
              description: Count of headers.
              returned: always
              type: int
              sample: 1
        custom_origin_config:
          description: Configuration of the origin.
          returned: always
          type: complex
          contains:
            http_port:
              description: Port on which HTTP is listening.
              returned: always
              type: int
              sample: 80
            https_port:
              description: Port on which HTTPS is listening.
              returned: always
              type: int
              sample: 443
            origin_keepalive_timeout:
              description: Keep-alive timeout.
              returned: always
              type: int
              sample: 5
            origin_protocol_policy:
              description: Policy of which protocols are supported.
              returned: always
              type: str
              sample: https-only
            origin_read_timeout:
              description: Timeout for reads to the origin.
              returned: always
              type: int
              sample: 30
            origin_ssl_protocols:
              description: SSL protocols allowed by the origin.
              returned: always
              type: complex
              contains:
                items:
                  description: List of SSL protocols.
                  returned: always
                  type: list
                  sample:
                  - TLSv1
                  - TLSv1.1
                  - TLSv1.2
                quantity:
                  description: Count of SSL protocols.
                  returned: always
                  type: int
                  sample: 3
        domain_name:
          description: Domain name of the origin.
          returned: always
          type: str
          sample: test-origin.example.com
        id:
          description: ID of the origin.
          returned: always
          type: str
          sample: test-origin.example.com
        origin_path:
          description: Subdirectory to prefix the request from the S3 or HTTP origin.
          returned: always
          type: str
          sample: ''
        s3_origin_config:
          description: Origin access identity configuration for S3 Origin.
          returned: when s3_origin_access_identity_enabled is true
          type: dict
          contains:
            origin_access_identity:
              type: str
              description: The origin access id as a path.
              sample: origin-access-identity/cloudfront/EXAMPLEID
    quantity:
      description: Count of origins.
      returned: always
      type: int
      sample: 1
price_class:
  description: Price class of CloudFront distribution.
  returned: always
  type: str
  sample: PriceClass_All
restrictions:
  description: Restrictions in use by CloudFront.
  returned: always
  type: complex
  contains:
    geo_restriction:
      description: Controls the countries in which your content is distributed.
      returned: always
      type: complex
      contains:
        quantity:
          description: Count of restrictions.
          returned: always
          type: int
          sample: 1
        items:
          description: List of country codes allowed or disallowed.
          returned: always
          type: list
          sample: xy
        restriction_type:
          description: Type of restriction.
          returned: always
          type: str
          sample: blacklist
status:
  description: Status of the CloudFront distribution.
  returned: always
  type: str
  sample: InProgress
tags:
  description: Distribution tags.
  returned: always
  type: dict
  sample:
    Hello: World
viewer_certificate:
  description: Certificate used by CloudFront distribution.
  returned: always
  type: complex
  contains:
    acm_certificate_arn:
      description: ARN of ACM certificate.
      returned: when certificate comes from ACM
      type: str
      sample: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-1234-1234-abcd-123456abcdef
    certificate:
      description: Reference to certificate.
      returned: always
      type: str
      sample: arn:aws:acm:us-east-1:123456789012:certificate/abcd1234-1234-1234-abcd-123456abcdef
    certificate_source:
      description: Where certificate comes from.
      returned: always
      type: str
      sample: acm
    minimum_protocol_version:
      description: Minimum SSL/TLS protocol supported by this distribution.
      returned: always
      type: str
      sample: TLSv1
    ssl_support_method:
      description: Support for pre-SNI browsers or not.
      returned: always
      type: str
      sample: sni-only
web_acl_id:
  description: ID of Web Access Control List (from WAF service).
  returned: always
  type: str
  sample: abcd1234-1234-abcd-abcd-abcd12345678
'''

from ansible.module_utils._text import to_text, to_native
from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.cloudfront_facts import CloudFrontFactsServiceManager
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry, compare_aws_tags, ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict, snake_dict_to_camel_dict
import datetime

try:
    from collections import OrderedDict
except ImportError:
    try:
        from ordereddict import OrderedDict
    except ImportError:
        pass  # caught by AnsibleAWSModule (as python 2.6 + boto3 => ordereddict is installed)

try:
    import botocore
except ImportError:
    pass  # caught by AnsibleAWSModule


def change_dict_key_name(dictionary, old_key, new_key):
    if old_key in dictionary:
        dictionary[new_key] = dictionary.get(old_key)
        dictionary.pop(old_key, None)
    return dictionary


def merge_validation_into_config(config, validated_node, node_name):
    if validated_node is not None:
        if isinstance(validated_node, dict):
            config_node = config.get(node_name)
            if config_node is not None:
                config_node_items = list(config_node.items())
            else:
                config_node_items = []
            config[node_name] = dict(config_node_items + list(validated_node.items()))
        if isinstance(validated_node, list):
            config[node_name] = list(set(config.get(node_name) + validated_node))
    return config


def ansible_list_to_cloudfront_list(list_items=None, include_quantity=True):
    if list_items is None:
        list_items = []
    if not isinstance(list_items, list):
        raise ValueError('Expected a list, got a {0} with value {1}'.format(type(list_items).__name__, str(list_items)))
    result = {}
    if include_quantity:
        result['quantity'] = len(list_items)
    if len(list_items) > 0:
        result['items'] = list_items
    return result


def create_distribution(client, module, config, tags):
    try:
        if not tags:
            return client.create_distribution(aws_retry=True, DistributionConfig=config)['Distribution']
        else:
            distribution_config_with_tags = {
                'DistributionConfig': config,
                'Tags': {
                    'Items': tags
                }
            }
            return client.create_distribution_with_tags(aws_retry=True, DistributionConfigWithTags=distribution_config_with_tags)['Distribution']
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error creating distribution")


def delete_distribution(client, module, distribution):
    try:
        return client.delete_distribution(aws_retry=True, Id=distribution['Distribution']['Id'], IfMatch=distribution['ETag'])
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error deleting distribution %s" % to_native(distribution['Distribution']))


def update_distribution(client, module, config, distribution_id, e_tag):
    try:
        return client.update_distribution(aws_retry=True, DistributionConfig=config, Id=distribution_id, IfMatch=e_tag)['Distribution']
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error updating distribution to %s" % to_native(config))


def tag_resource(client, module, arn, tags):
    try:
        return client.tag_resource(aws_retry=True, Resource=arn, Tags=dict(Items=tags))
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error tagging resource")


def untag_resource(client, module, arn, tag_keys):
    try:
        return client.untag_resource(aws_retry=True, Resource=arn, TagKeys=dict(Items=tag_keys))
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error untagging resource")


def list_tags_for_resource(client, module, arn):
    try:
        response = client.list_tags_for_resource(aws_retry=True, Resource=arn)
        return boto3_tag_list_to_ansible_dict(response.get('Tags').get('Items'))
    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
        module.fail_json_aws(e, msg="Error listing tags for resource")


def update_tags(client, module, existing_tags, valid_tags, purge_tags, arn):
    changed = False
    to_add, to_remove = compare_aws_tags(existing_tags, valid_tags, purge_tags)
    if to_remove:
        untag_resource(client, module, arn, to_remove)
        changed = True
    if to_add:
        tag_resource(client, module, arn, ansible_dict_to_boto3_tag_list(to_add))
        changed = True
    return changed


class CloudFrontValidationManager(object):
    """
    Manages CloudFront validations
    """

    def __init__(self, module):
        self.__cloudfront_facts_mgr = CloudFrontFactsServiceManager(module)
        self.module = module
        self.__default_distribution_enabled = True
        self.__default_http_port = 80
        self.__default_https_port = 443
        self.__default_ipv6_enabled = False
        self.__default_origin_ssl_protocols = [
            'TLSv1',
            'TLSv1.1',
            'TLSv1.2'
        ]
        self.__default_custom_origin_protocol_policy = 'match-viewer'
        self.__default_custom_origin_read_timeout = 30
        self.__default_custom_origin_keepalive_timeout = 5
        self.__default_datetime_string = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%f')
        self.__default_cache_behavior_min_ttl = 0
        self.__default_cache_behavior_max_ttl = 31536000
        self.__default_cache_behavior_default_ttl = 86400
        self.__default_cache_behavior_compress = False
        self.__default_cache_behavior_viewer_protocol_policy = 'allow-all'
        self.__default_cache_behavior_smooth_streaming = False
        self.__default_cache_behavior_forwarded_values_forward_cookies = 'none'
        self.__default_cache_behavior_forwarded_values_query_string = True
        self.__default_trusted_signers_enabled = False
        self.__valid_price_classes = set([
            'PriceClass_100',
            'PriceClass_200',
            'PriceClass_All'
        ])
        self.__valid_origin_protocol_policies = set([
            'http-only',
            'match-viewer',
            'https-only'
        ])
        self.__valid_origin_ssl_protocols = set([
            'SSLv3',
            'TLSv1',
            'TLSv1.1',
            'TLSv1.2'
        ])
        self.__valid_cookie_forwarding = set([
            'none',
            'whitelist',
            'all'
        ])
        self.__valid_viewer_protocol_policies = set([
            'allow-all',
            'https-only',
            'redirect-to-https'
        ])
        self.__valid_methods = set([
            'GET',
            'HEAD',
            'POST',
            'PUT',
            'PATCH',
            'OPTIONS',
            'DELETE'
        ])
        self.__valid_methods_cached_methods = [
            set([
                'GET',
                'HEAD'
            ]),
            set([
                'GET',
                'HEAD',
                'OPTIONS'
            ])
        ]
        self.__valid_methods_allowed_methods = [
            self.__valid_methods_cached_methods[0],
            self.__valid_methods_cached_methods[1],
            self.__valid_methods
        ]
        self.__valid_lambda_function_association_event_types = set([
            'viewer-request',
            'viewer-response',
            'origin-request',
            'origin-response'
        ])
        self.__valid_viewer_certificate_ssl_support_methods = set([
            'sni-only',
            'vip'
        ])
        self.__valid_viewer_certificate_minimum_protocol_versions = set([
            'SSLv3',
            'TLSv1',
            'TLSv1_2016',
            'TLSv1.1_2016',
            'TLSv1.2_2018',
            'TLSv1.2_2019',
            'TLSv1.2_2021'
        ])
        self.__valid_viewer_certificate_certificate_sources = set([
            'cloudfront',
            'iam',
            'acm'
        ])
        self.__valid_http_versions = set([
            'http1.1',
            'http2'
        ])
        self.__s3_bucket_domain_identifier = '.s3.amazonaws.com'

    def add_missing_key(self, dict_object, key_to_set, value_to_set):
        if key_to_set not in dict_object and value_to_set is not None:
            dict_object[key_to_set] = value_to_set
        return dict_object

    def add_key_else_change_dict_key(self, dict_object, old_key, new_key, value_to_set):
        if old_key not in dict_object and value_to_set is not None:
            dict_object[new_key] = value_to_set
        else:
            dict_object = change_dict_key_name(dict_object, old_key, new_key)
        return dict_object

    def add_key_else_validate(self, dict_object, key_name, attribute_name, value_to_set, valid_values, to_aws_list=False):
        if key_name in dict_object:
            self.validate_attribute_with_allowed_values(value_to_set, attribute_name, valid_values)
        else:
            if to_aws_list:
                dict_object[key_name] = ansible_list_to_cloudfront_list(value_to_set)
            elif value_to_set is not None:
                dict_object[key_name] = value_to_set
        return dict_object

    def validate_logging(self, logging):
        try:
            if logging is None:
                return None
            valid_logging = {}
            if logging and not set(['enabled', 'include_cookies', 'bucket', 'prefix']).issubset(logging):
                self.module.fail_json(msg="The logging parameters enabled, include_cookies, bucket and prefix must be specified.")
            valid_logging['include_cookies'] = logging.get('include_cookies')
            valid_logging['enabled'] = logging.get('enabled')
            valid_logging['bucket'] = logging.get('bucket')
            valid_logging['prefix'] = logging.get('prefix')
            return valid_logging
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution logging")

    def validate_is_list(self, list_to_validate, list_name):
        if not isinstance(list_to_validate, list):
            self.module.fail_json(msg='%s is of type %s. Must be a list.' % (list_name, type(list_to_validate).__name__))

    def validate_required_key(self, key_name, full_key_name, dict_object):
        if key_name not in dict_object:
            self.module.fail_json(msg="%s must be specified." % full_key_name)

    def validate_origins(self, client, config, origins, default_origin_domain_name,
                         default_origin_path, create_distribution, purge_origins=False):
        try:
            if origins is None:
                if default_origin_domain_name is None and not create_distribution:
                    if purge_origins:
                        return None
                    else:
                        return ansible_list_to_cloudfront_list(config)
                if default_origin_domain_name is not None:
                    origins = [{
                        'domain_name': default_origin_domain_name,
                        'origin_path': default_origin_path or ''
                    }]
                else:
                    origins = []
            self.validate_is_list(origins, 'origins')
            if not origins and default_origin_domain_name is None and create_distribution:
                self.module.fail_json(msg="Both origins[] and default_origin_domain_name have not been specified. Please specify at least one.")
            all_origins = OrderedDict()
            new_domains = list()
            for origin in config:
                all_origins[origin.get('domain_name')] = origin
            for origin in origins:
                origin = self.validate_origin(client, all_origins.get(origin.get('domain_name'), {}), origin, default_origin_path)
                all_origins[origin['domain_name']] = origin
                new_domains.append(origin['domain_name'])
            if purge_origins:
                for domain in list(all_origins.keys()):
                    if domain not in new_domains:
                        del(all_origins[domain])
            return ansible_list_to_cloudfront_list(list(all_origins.values()))
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution origins")

    def validate_s3_origin_configuration(self, client, existing_config, origin):
        if origin.get('s3_origin_config', {}).get('origin_access_identity'):
            return origin['s3_origin_config']['origin_access_identity']

        if existing_config.get('s3_origin_config', {}).get('origin_access_identity'):
            return existing_config['s3_origin_config']['origin_access_identity']

        try:
            comment = "access-identity-by-ansible-%s-%s" % (origin.get('domain_name'), self.__default_datetime_string)
            caller_reference = "%s-%s" % (origin.get('domain_name'), self.__default_datetime_string)
            cfoai_config = dict(CloudFrontOriginAccessIdentityConfig=dict(CallerReference=caller_reference,
                                                                          Comment=comment))
            oai = client.create_cloud_front_origin_access_identity(**cfoai_config)['CloudFrontOriginAccessIdentity']['Id']
        except Exception as e:
            self.module.fail_json_aws(e, msg="Couldn't create Origin Access Identity for id %s" % origin['id'])
        return "origin-access-identity/cloudfront/%s" % oai

    def validate_origin(self, client, existing_config, origin, default_origin_path):
        try:
            origin = self.add_missing_key(origin, 'origin_path', existing_config.get('origin_path', default_origin_path or ''))
            self.validate_required_key('origin_path', 'origins[].origin_path', origin)
            origin = self.add_missing_key(origin, 'id', existing_config.get('id', self.__default_datetime_string))
            if 'custom_headers' in origin and len(origin.get('custom_headers')) > 0:
                for custom_header in origin.get('custom_headers'):
                    if 'header_name' not in custom_header or 'header_value' not in custom_header:
                        self.module.fail_json(msg="Both origins[].custom_headers.header_name and origins[].custom_headers.header_value must be specified.")
                origin['custom_headers'] = ansible_list_to_cloudfront_list(origin.get('custom_headers'))
            else:
                origin['custom_headers'] = ansible_list_to_cloudfront_list()
            if self.__s3_bucket_domain_identifier in origin.get('domain_name').lower():
                if origin.get("s3_origin_access_identity_enabled") is not None:
                    if origin['s3_origin_access_identity_enabled']:
                        s3_origin_config = self.validate_s3_origin_configuration(client, existing_config, origin)
                    else:
                        s3_origin_config = None

                    del(origin["s3_origin_access_identity_enabled"])

                    if s3_origin_config:
                        oai = s3_origin_config
                    else:
                        oai = ""

                    origin["s3_origin_config"] = dict(origin_access_identity=oai)

                    if 'custom_origin_config' in origin:
                        self.module.fail_json(msg="s3_origin_access_identity_enabled and custom_origin_config are mutually exclusive")
            else:
                origin = self.add_missing_key(origin, 'custom_origin_config', existing_config.get('custom_origin_config', {}))
                custom_origin_config = origin.get('custom_origin_config')
                custom_origin_config = self.add_key_else_validate(custom_origin_config, 'origin_protocol_policy',
                                                                  'origins[].custom_origin_config.origin_protocol_policy',
                                                                  self.__default_custom_origin_protocol_policy, self.__valid_origin_protocol_policies)
                custom_origin_config = self.add_missing_key(custom_origin_config, 'origin_read_timeout', self.__default_custom_origin_read_timeout)
                custom_origin_config = self.add_missing_key(custom_origin_config, 'origin_keepalive_timeout', self.__default_custom_origin_keepalive_timeout)
                custom_origin_config = self.add_key_else_change_dict_key(custom_origin_config, 'http_port', 'h_t_t_p_port', self.__default_http_port)
                custom_origin_config = self.add_key_else_change_dict_key(custom_origin_config, 'https_port', 'h_t_t_p_s_port', self.__default_https_port)
                if custom_origin_config.get('origin_ssl_protocols', {}).get('items'):
                    custom_origin_config['origin_ssl_protocols'] = custom_origin_config['origin_ssl_protocols']['items']
                if custom_origin_config.get('origin_ssl_protocols'):
                    self.validate_attribute_list_with_allowed_list(custom_origin_config['origin_ssl_protocols'], 'origins[].origin_ssl_protocols',
                                                                   self.__valid_origin_ssl_protocols)
                else:
                    custom_origin_config['origin_ssl_protocols'] = self.__default_origin_ssl_protocols
                custom_origin_config['origin_ssl_protocols'] = ansible_list_to_cloudfront_list(custom_origin_config['origin_ssl_protocols'])
            return origin
        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            self.module.fail_json_aws(e, msg="Error validating distribution origin")

    def validate_cache_behaviors(self, config, cache_behaviors, valid_origins, purge_cache_behaviors=False):
        try:
            if cache_behaviors is None and valid_origins is not None and purge_cache_behaviors is False:
                return ansible_list_to_cloudfront_list(config)
            all_cache_behaviors = OrderedDict()
            # cache behaviors are order dependent so we don't preserve the existing ordering when purge_cache_behaviors
            # is true (if purge_cache_behaviors is not true, we can't really know the full new order)
            if not purge_cache_behaviors:
                for behavior in config:
                    all_cache_behaviors[behavior['path_pattern']] = behavior
            for cache_behavior in cache_behaviors:
                valid_cache_behavior = self.validate_cache_behavior(all_cache_behaviors.get(cache_behavior.get('path_pattern'), {}),
                                                                    cache_behavior, valid_origins)
                all_cache_behaviors[cache_behavior['path_pattern']] = valid_cache_behavior
            if purge_cache_behaviors:
                for target_origin_id in set(all_cache_behaviors.keys()) - set([cb['path_pattern'] for cb in cache_behaviors]):
                    del(all_cache_behaviors[target_origin_id])
            return ansible_list_to_cloudfront_list(list(all_cache_behaviors.values()))
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution cache behaviors")

    def validate_cache_behavior(self, config, cache_behavior, valid_origins, is_default_cache=False):
        if is_default_cache and cache_behavior is None:
            cache_behavior = {}
        if cache_behavior is None and valid_origins is not None:
            return config
        cache_behavior = self.validate_cache_behavior_first_level_keys(config, cache_behavior, valid_origins, is_default_cache)
        cache_behavior = self.validate_forwarded_values(config, cache_behavior.get('forwarded_values'), cache_behavior)
        cache_behavior = self.validate_allowed_methods(config, cache_behavior.get('allowed_methods'), cache_behavior)
        cache_behavior = self.validate_lambda_function_associations(config, cache_behavior.get('lambda_function_associations'), cache_behavior)
        cache_behavior = self.validate_trusted_signers(config, cache_behavior.get('trusted_signers'), cache_behavior)
        cache_behavior = self.validate_field_level_encryption_id(config, cache_behavior.get('field_level_encryption_id'), cache_behavior)
        return cache_behavior

    def validate_cache_behavior_first_level_keys(self, config, cache_behavior, valid_origins, is_default_cache):
        try:
            cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'min_ttl', 'min_t_t_l',
                                                               config.get('min_t_t_l', self.__default_cache_behavior_min_ttl))
            cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'max_ttl', 'max_t_t_l',
                                                               config.get('max_t_t_l', self.__default_cache_behavior_max_ttl))
            cache_behavior = self.add_key_else_change_dict_key(cache_behavior, 'default_ttl', 'default_t_t_l',
                                                               config.get('default_t_t_l', self.__default_cache_behavior_default_ttl))
            cache_behavior = self.add_missing_key(cache_behavior, 'compress', config.get('compress', self.__default_cache_behavior_compress))
            target_origin_id = cache_behavior.get('target_origin_id', config.get('target_origin_id'))
            if not target_origin_id:
                target_origin_id = self.get_first_origin_id_for_default_cache_behavior(valid_origins)
            if target_origin_id not in [origin['id'] for origin in valid_origins.get('items', [])]:
                if is_default_cache:
                    cache_behavior_name = 'Default cache behavior'
                else:
                    cache_behavior_name = 'Cache behavior for path %s' % cache_behavior['path_pattern']
                self.module.fail_json(msg="%s has target_origin_id pointing to an origin that does not exist." %
                                      cache_behavior_name)
            cache_behavior['target_origin_id'] = target_origin_id
            cache_behavior = self.add_key_else_validate(cache_behavior, 'viewer_protocol_policy', 'cache_behavior.viewer_protocol_policy',
                                                        config.get('viewer_protocol_policy',
                                                                   self.__default_cache_behavior_viewer_protocol_policy),
                                                        self.__valid_viewer_protocol_policies)
            cache_behavior = self.add_missing_key(cache_behavior, 'smooth_streaming',
                                                  config.get('smooth_streaming', self.__default_cache_behavior_smooth_streaming))
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution cache behavior first level keys")

    def validate_forwarded_values(self, config, forwarded_values, cache_behavior):
        try:
            if not forwarded_values:
                forwarded_values = dict()
            existing_config = config.get('forwarded_values', {})
            headers = forwarded_values.get('headers', existing_config.get('headers', {}).get('items'))
            if headers:
                headers.sort()
            forwarded_values['headers'] = ansible_list_to_cloudfront_list(headers)
            if 'cookies' not in forwarded_values:
                forward = existing_config.get('cookies', {}).get('forward', self.__default_cache_behavior_forwarded_values_forward_cookies)
                forwarded_values['cookies'] = {'forward': forward}
            else:
                existing_whitelist = existing_config.get('cookies', {}).get('whitelisted_names', {}).get('items')
                whitelist = forwarded_values.get('cookies').get('whitelisted_names', existing_whitelist)
                if whitelist:
                    self.validate_is_list(whitelist, 'forwarded_values.whitelisted_names')
                    forwarded_values['cookies']['whitelisted_names'] = ansible_list_to_cloudfront_list(whitelist)
                cookie_forwarding = forwarded_values.get('cookies').get('forward', existing_config.get('cookies', {}).get('forward'))
                self.validate_attribute_with_allowed_values(cookie_forwarding, 'cache_behavior.forwarded_values.cookies.forward',
                                                            self.__valid_cookie_forwarding)
                forwarded_values['cookies']['forward'] = cookie_forwarding
            query_string_cache_keys = forwarded_values.get('query_string_cache_keys', existing_config.get('query_string_cache_keys', {}).get('items', []))
            self.validate_is_list(query_string_cache_keys, 'forwarded_values.query_string_cache_keys')
            forwarded_values['query_string_cache_keys'] = ansible_list_to_cloudfront_list(query_string_cache_keys)
            forwarded_values = self.add_missing_key(forwarded_values, 'query_string',
                                                    existing_config.get('query_string', self.__default_cache_behavior_forwarded_values_query_string))
            cache_behavior['forwarded_values'] = forwarded_values
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating forwarded values")

    def validate_lambda_function_associations(self, config, lambda_function_associations, cache_behavior):
        try:
            if lambda_function_associations is not None:
                self.validate_is_list(lambda_function_associations, 'lambda_function_associations')
                for association in lambda_function_associations:
                    association = change_dict_key_name(association, 'lambda_function_arn', 'lambda_function_a_r_n')
                    self.validate_attribute_with_allowed_values(association.get('event_type'), 'cache_behaviors[].lambda_function_associations.event_type',
                                                                self.__valid_lambda_function_association_event_types)
                cache_behavior['lambda_function_associations'] = ansible_list_to_cloudfront_list(lambda_function_associations)
            else:
                if 'lambda_function_associations' in config:
                    cache_behavior['lambda_function_associations'] = config.get('lambda_function_associations')
                else:
                    cache_behavior['lambda_function_associations'] = ansible_list_to_cloudfront_list([])
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating lambda function associations")

    def validate_field_level_encryption_id(self, config, field_level_encryption_id, cache_behavior):
        if field_level_encryption_id is not None:
            cache_behavior['field_level_encryption_id'] = field_level_encryption_id
        elif 'field_level_encryption_id' in config:
            cache_behavior['field_level_encryption_id'] = config.get('field_level_encryption_id')
        else:
            cache_behavior['field_level_encryption_id'] = ""
        return cache_behavior

    def validate_allowed_methods(self, config, allowed_methods, cache_behavior):
        try:
            if allowed_methods is not None:
                self.validate_required_key('items', 'cache_behavior.allowed_methods.items[]', allowed_methods)
                temp_allowed_items = allowed_methods.get('items')
                self.validate_is_list(temp_allowed_items, 'cache_behavior.allowed_methods.items')
                self.validate_attribute_list_with_allowed_list(temp_allowed_items, 'cache_behavior.allowed_methods.items[]',
                                                               self.__valid_methods_allowed_methods)
                cached_items = allowed_methods.get('cached_methods')
                if 'cached_methods' in allowed_methods:
                    self.validate_is_list(cached_items, 'cache_behavior.allowed_methods.cached_methods')
                    self.validate_attribute_list_with_allowed_list(cached_items, 'cache_behavior.allowed_items.cached_methods[]',
                                                                   self.__valid_methods_cached_methods)
                # we don't care if the order of how cloudfront stores the methods differs - preserving existing
                # order reduces likelihood of making unnecessary changes
                if 'allowed_methods' in config and set(config['allowed_methods']['items']) == set(temp_allowed_items):
                    cache_behavior['allowed_methods'] = config['allowed_methods']
                else:
                    cache_behavior['allowed_methods'] = ansible_list_to_cloudfront_list(temp_allowed_items)

                if cached_items and set(cached_items) == set(config.get('allowed_methods', {}).get('cached_methods', {}).get('items', [])):
                    cache_behavior['allowed_methods']['cached_methods'] = config['allowed_methods']['cached_methods']
                else:
                    cache_behavior['allowed_methods']['cached_methods'] = ansible_list_to_cloudfront_list(cached_items)
            else:
                if 'allowed_methods' in config:
                    cache_behavior['allowed_methods'] = config.get('allowed_methods')
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating allowed methods")

    def validate_trusted_signers(self, config, trusted_signers, cache_behavior):
        try:
            if trusted_signers is None:
                trusted_signers = {}
            if 'items' in trusted_signers:
                valid_trusted_signers = ansible_list_to_cloudfront_list(trusted_signers.get('items'))
            else:
                valid_trusted_signers = dict(quantity=config.get('quantity', 0))
                if 'items' in config:
                    valid_trusted_signers = dict(items=config['items'])
            valid_trusted_signers['enabled'] = trusted_signers.get('enabled', config.get('enabled', self.__default_trusted_signers_enabled))
            cache_behavior['trusted_signers'] = valid_trusted_signers
            return cache_behavior
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating trusted signers")

    def validate_viewer_certificate(self, viewer_certificate):
        try:
            if viewer_certificate is None:
                return None
            if viewer_certificate.get('cloudfront_default_certificate') and viewer_certificate.get('ssl_support_method') is not None:
                self.module.fail_json(msg="viewer_certificate.ssl_support_method should not be specified with viewer_certificate_cloudfront_default" +
                                      "_certificate set to true.")
            self.validate_attribute_with_allowed_values(viewer_certificate.get('ssl_support_method'), 'viewer_certificate.ssl_support_method',
                                                        self.__valid_viewer_certificate_ssl_support_methods)
            self.validate_attribute_with_allowed_values(viewer_certificate.get('minimum_protocol_version'), 'viewer_certificate.minimum_protocol_version',
                                                        self.__valid_viewer_certificate_minimum_protocol_versions)
            self.validate_attribute_with_allowed_values(viewer_certificate.get('certificate_source'), 'viewer_certificate.certificate_source',
                                                        self.__valid_viewer_certificate_certificate_sources)
            viewer_certificate = change_dict_key_name(viewer_certificate, 'cloudfront_default_certificate', 'cloud_front_default_certificate')
            viewer_certificate = change_dict_key_name(viewer_certificate, 'ssl_support_method', 's_s_l_support_method')
            viewer_certificate = change_dict_key_name(viewer_certificate, 'iam_certificate_id', 'i_a_m_certificate_id')
            viewer_certificate = change_dict_key_name(viewer_certificate, 'acm_certificate_arn', 'a_c_m_certificate_arn')
            return viewer_certificate
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating viewer certificate")

    def validate_custom_error_responses(self, config, custom_error_responses, purge_custom_error_responses):
        try:
            if custom_error_responses is None and not purge_custom_error_responses:
                return ansible_list_to_cloudfront_list(config)
            self.validate_is_list(custom_error_responses, 'custom_error_responses')
            result = list()
            existing_responses = dict((response['error_code'], response) for response in custom_error_responses)
            for custom_error_response in custom_error_responses:
                self.validate_required_key('error_code', 'custom_error_responses[].error_code', custom_error_response)
                custom_error_response = change_dict_key_name(custom_error_response, 'error_caching_min_ttl', 'error_caching_min_t_t_l')
                if 'response_code' in custom_error_response:
                    custom_error_response['response_code'] = str(custom_error_response['response_code'])
                if custom_error_response['error_code'] in existing_responses:
                    del(existing_responses[custom_error_response['error_code']])
                result.append(custom_error_response)
            if not purge_custom_error_responses:
                result.extend(existing_responses.values())

            return ansible_list_to_cloudfront_list(result)
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating custom error responses")

    def validate_restrictions(self, config, restrictions, purge_restrictions=False):
        try:
            if restrictions is None:
                if purge_restrictions:
                    return None
                else:
                    return config
            self.validate_required_key('geo_restriction', 'restrictions.geo_restriction', restrictions)
            geo_restriction = restrictions.get('geo_restriction')
            self.validate_required_key('restriction_type', 'restrictions.geo_restriction.restriction_type', geo_restriction)
            existing_restrictions = config.get('geo_restriction', {}).get(geo_restriction['restriction_type'], {}).get('items', [])
            geo_restriction_items = geo_restriction.get('items')
            if not purge_restrictions:
                geo_restriction_items.extend([rest for rest in existing_restrictions if
                                              rest not in geo_restriction_items])
            valid_restrictions = ansible_list_to_cloudfront_list(geo_restriction_items)
            valid_restrictions['restriction_type'] = geo_restriction.get('restriction_type')
            return {'geo_restriction': valid_restrictions}
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating restrictions")

    def validate_distribution_config_parameters(self, config, default_root_object, ipv6_enabled, http_version, web_acl_id):
        try:
            config['default_root_object'] = default_root_object or config.get('default_root_object', '')
            config['is_i_p_v6_enabled'] = ipv6_enabled if ipv6_enabled is not None else config.get('is_i_p_v6_enabled', self.__default_ipv6_enabled)
            if http_version is not None or config.get('http_version'):
                self.validate_attribute_with_allowed_values(http_version, 'http_version', self.__valid_http_versions)
                config['http_version'] = http_version or config.get('http_version')
            if web_acl_id or config.get('web_a_c_l_id'):
                config['web_a_c_l_id'] = web_acl_id or config.get('web_a_c_l_id')
            return config
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution config parameters")

    def validate_common_distribution_parameters(self, config, enabled, aliases, logging, price_class, purge_aliases=False):
        try:
            if config is None:
                config = {}
            if aliases is not None:
                if not purge_aliases:
                    aliases.extend([alias for alias in config.get('aliases', {}).get('items', [])
                                    if alias not in aliases])
                config['aliases'] = ansible_list_to_cloudfront_list(aliases)
            if logging is not None:
                config['logging'] = self.validate_logging(logging)
            config['enabled'] = enabled or config.get('enabled', self.__default_distribution_enabled)
            if price_class is not None:
                self.validate_attribute_with_allowed_values(price_class, 'price_class', self.__valid_price_classes)
                config['price_class'] = price_class
            return config
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating common distribution parameters")

    def validate_comment(self, config, comment):
        config['comment'] = comment or config.get('comment', "Distribution created by Ansible with datetime stamp " + self.__default_datetime_string)
        return config

    def validate_caller_reference(self, caller_reference):
        return caller_reference or self.__default_datetime_string

    def get_first_origin_id_for_default_cache_behavior(self, valid_origins):
        try:
            if valid_origins is not None:
                valid_origins_list = valid_origins.get('items')
                if valid_origins_list is not None and isinstance(valid_origins_list, list) and len(valid_origins_list) > 0:
                    return str(valid_origins_list[0].get('id'))
            self.module.fail_json(msg="There are no valid origins from which to specify a target_origin_id for the default_cache_behavior configuration.")
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error getting first origin_id for default cache behavior")

    def validate_attribute_list_with_allowed_list(self, attribute_list, attribute_list_name, allowed_list):
        try:
            self.validate_is_list(attribute_list, attribute_list_name)
            if (isinstance(allowed_list, list) and set(attribute_list) not in allowed_list or
                    isinstance(allowed_list, set) and not set(allowed_list).issuperset(attribute_list)):
                self.module.fail_json(msg='The attribute list {0} must be one of [{1}]'.format(attribute_list_name, ' '.join(str(a) for a in allowed_list)))
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating attribute list with allowed value list")

    def validate_attribute_with_allowed_values(self, attribute, attribute_name, allowed_list):
        if attribute is not None and attribute not in allowed_list:
            self.module.fail_json(msg='The attribute {0} must be one of [{1}]'.format(attribute_name, ' '.join(str(a) for a in allowed_list)))

    def validate_distribution_from_caller_reference(self, caller_reference):
        try:
            distributions = self.__cloudfront_facts_mgr.list_distributions(False)
            distribution_name = 'Distribution'
            distribution_config_name = 'DistributionConfig'
            distribution_ids = [dist.get('Id') for dist in distributions]
            for distribution_id in distribution_ids:
                distribution = self.__cloudfront_facts_mgr.get_distribution(distribution_id)
                if distribution is not None:
                    distribution_config = distribution[distribution_name].get(distribution_config_name)
                    if distribution_config is not None and distribution_config.get('CallerReference') == caller_reference:
                        distribution[distribution_name][distribution_config_name] = distribution_config
                        return distribution

        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution from caller reference")

    def validate_distribution_from_aliases_caller_reference(self, distribution_id, aliases, caller_reference):
        try:
            if caller_reference is not None:
                return self.validate_distribution_from_caller_reference(caller_reference)
            else:
                if aliases and distribution_id is None:
                    distribution_id = self.validate_distribution_id_from_alias(aliases)
                if distribution_id:
                    return self.__cloudfront_facts_mgr.get_distribution(distribution_id)
            return None
        except Exception as e:
            self.module.fail_json_aws(e, msg="Error validating distribution_id from alias, aliases and caller reference")

    def validate_distribution_id_from_alias(self, aliases):
        distributions = self.__cloudfront_facts_mgr.list_distributions(False)
        if distributions:
            for distribution in distributions:
                distribution_aliases = distribution.get('Aliases', {}).get('Items', [])
                if set(aliases) & set(distribution_aliases):
                    return distribution['Id']
        return None

    def wait_until_processed(self, client, wait_timeout, distribution_id, caller_reference):
        if distribution_id is None:
            distribution_id = self.validate_distribution_from_caller_reference(caller_reference=caller_reference)['Id']

        try:
            waiter = client.get_waiter('distribution_deployed')
            attempts = 1 + int(wait_timeout / 60)
            waiter.wait(Id=distribution_id, WaiterConfig={'MaxAttempts': attempts})
        except botocore.exceptions.WaiterError as e:
            self.module.fail_json_aws(e, msg="Timeout waiting for CloudFront action."
                                      " Waited for {0} seconds before timeout.".format(to_text(wait_timeout)))

        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
            self.module.fail_json_aws(e, msg="Error getting distribution {0}".format(distribution_id))


def main():
    argument_spec = dict(
        state=dict(choices=['present', 'absent'], default='present'),
        caller_reference=dict(),
        comment=dict(),
        distribution_id=dict(),
        e_tag=dict(),
        tags=dict(type='dict', default={}),
        purge_tags=dict(type='bool', default=False),
        alias=dict(),
        aliases=dict(type='list', default=[], elements='str'),
        purge_aliases=dict(type='bool', default=False),
        default_root_object=dict(),
        origins=dict(type='list', elements='dict'),
        purge_origins=dict(type='bool', default=False),
        default_cache_behavior=dict(type='dict'),
        cache_behaviors=dict(type='list', elements='dict'),
        purge_cache_behaviors=dict(type='bool', default=False),
        custom_error_responses=dict(type='list', elements='dict'),
        purge_custom_error_responses=dict(type='bool', default=False),
        logging=dict(type='dict'),
        price_class=dict(),
        enabled=dict(type='bool'),
        viewer_certificate=dict(type='dict'),
        restrictions=dict(type='dict'),
        web_acl_id=dict(),
        http_version=dict(),
        ipv6_enabled=dict(type='bool'),
        default_origin_domain_name=dict(),
        default_origin_path=dict(),
        wait=dict(default=False, type='bool'),
        wait_timeout=dict(default=1800, type='int')
    )

    result = {}
    changed = True

    module = AnsibleAWSModule(
        argument_spec=argument_spec,
        supports_check_mode=False,
        mutually_exclusive=[
            ['distribution_id', 'alias'],
            ['default_origin_domain_name', 'distribution_id'],
            ['default_origin_domain_name', 'alias'],
        ]
    )

    client = module.client('cloudfront', retry_decorator=AWSRetry.jittered_backoff())

    validation_mgr = CloudFrontValidationManager(module)

    state = module.params.get('state')
    caller_reference = module.params.get('caller_reference')
    comment = module.params.get('comment')
    e_tag = module.params.get('e_tag')
    tags = module.params.get('tags')
    purge_tags = module.params.get('purge_tags')
    distribution_id = module.params.get('distribution_id')
    alias = module.params.get('alias')
    aliases = module.params.get('aliases')
    purge_aliases = module.params.get('purge_aliases')
    default_root_object = module.params.get('default_root_object')
    origins = module.params.get('origins')
    purge_origins = module.params.get('purge_origins')
    default_cache_behavior = module.params.get('default_cache_behavior')
    cache_behaviors = module.params.get('cache_behaviors')
    purge_cache_behaviors = module.params.get('purge_cache_behaviors')
    custom_error_responses = module.params.get('custom_error_responses')
    purge_custom_error_responses = module.params.get('purge_custom_error_responses')
    logging = module.params.get('logging')
    price_class = module.params.get('price_class')
    enabled = module.params.get('enabled')
    viewer_certificate = module.params.get('viewer_certificate')
    restrictions = module.params.get('restrictions')
    purge_restrictions = module.params.get('purge_restrictions')
    web_acl_id = module.params.get('web_acl_id')
    http_version = module.params.get('http_version')
    ipv6_enabled = module.params.get('ipv6_enabled')
    default_origin_domain_name = module.params.get('default_origin_domain_name')
    default_origin_path = module.params.get('default_origin_path')
    wait = module.params.get('wait')
    wait_timeout = module.params.get('wait_timeout')

    if alias and alias not in aliases:
        aliases.append(alias)

    distribution = validation_mgr.validate_distribution_from_aliases_caller_reference(distribution_id, aliases, caller_reference)

    update = state == 'present' and distribution
    create = state == 'present' and not distribution
    delete = state == 'absent' and distribution

    if not (update or create or delete):
        module.exit_json(changed=False)

    if update or delete:
        config = distribution['Distribution']['DistributionConfig']
        e_tag = distribution['ETag']
        distribution_id = distribution['Distribution']['Id']
    else:
        config = dict()
    if update:
        config = camel_dict_to_snake_dict(config, reversible=True)

    if create or update:
        config = validation_mgr.validate_common_distribution_parameters(config, enabled, aliases, logging, price_class, purge_aliases)
        config = validation_mgr.validate_distribution_config_parameters(config, default_root_object, ipv6_enabled, http_version, web_acl_id)
        config['origins'] = validation_mgr.validate_origins(client, config.get('origins', {}).get('items', []), origins, default_origin_domain_name,
                                                            default_origin_path, create, purge_origins)
        config['cache_behaviors'] = validation_mgr.validate_cache_behaviors(config.get('cache_behaviors', {}).get('items', []),
                                                                            cache_behaviors, config['origins'], purge_cache_behaviors)
        config['default_cache_behavior'] = validation_mgr.validate_cache_behavior(config.get('default_cache_behavior', {}),
                                                                                  default_cache_behavior, config['origins'], True)
        config['custom_error_responses'] = validation_mgr.validate_custom_error_responses(config.get('custom_error_responses', {}).get('items', []),
                                                                                          custom_error_responses, purge_custom_error_responses)
        valid_restrictions = validation_mgr.validate_restrictions(config.get('restrictions', {}), restrictions, purge_restrictions)
        if valid_restrictions:
            config['restrictions'] = valid_restrictions
        valid_viewer_certificate = validation_mgr.validate_viewer_certificate(viewer_certificate)
        config = merge_validation_into_config(config, valid_viewer_certificate, 'viewer_certificate')
        config = validation_mgr.validate_comment(config, comment)
        config = snake_dict_to_camel_dict(config, capitalize_first=True)

    if create:
        config['CallerReference'] = validation_mgr.validate_caller_reference(caller_reference)
        result = create_distribution(client, module, config, ansible_dict_to_boto3_tag_list(tags))
        result = camel_dict_to_snake_dict(result)
        result['tags'] = list_tags_for_resource(client, module, result['arn'])

    if delete:
        if config['Enabled']:
            config['Enabled'] = False
            result = update_distribution(client, module, config, distribution_id, e_tag)
            validation_mgr.wait_until_processed(client, wait_timeout, distribution_id, config.get('CallerReference'))
        distribution = validation_mgr.validate_distribution_from_aliases_caller_reference(distribution_id, aliases, caller_reference)
        # e_tag = distribution['ETag']
        result = delete_distribution(client, module, distribution)

    if update:
        changed = config != distribution['Distribution']['DistributionConfig']
        if changed:
            result = update_distribution(client, module, config, distribution_id, e_tag)
        else:
            result = distribution['Distribution']
        existing_tags = list_tags_for_resource(client, module, result['ARN'])
        distribution['Distribution']['DistributionConfig']['tags'] = existing_tags
        changed |= update_tags(client, module, existing_tags, tags, purge_tags, result['ARN'])
        result = camel_dict_to_snake_dict(result)
        result['distribution_config']['tags'] = config['tags'] = list_tags_for_resource(client, module, result['arn'])
        result['diff'] = dict()
        diff = recursive_diff(distribution['Distribution']['DistributionConfig'], config)
        if diff:
            result['diff']['before'] = diff[0]
            result['diff']['after'] = diff[1]

    if wait and (create or update):
        validation_mgr.wait_until_processed(client, wait_timeout, distribution_id, config.get('CallerReference'))

    if 'distribution_config' in result:
        result.update(result['distribution_config'])
        del(result['distribution_config'])

    module.exit_json(changed=changed, **result)


if __name__ == '__main__':
    main()