#!/usr/bin/python
# Copyright (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'community'}

DOCUMENTATION = '''
---

module: cloudfront_distribution

short_description: create, update and delete aws cloudfront distributions.

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

requirements:
  - boto3 >= 1.0.0
  - python >= 2.6

version_added: "2.5"

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

extends_documentation_fragment:
  - aws
  - ec2

options:

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

    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).

    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.

    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
          'YYYY-MM-DDTHH:MM:SS.ffffff'.

    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. "Priority:" '1'

    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: 'no'
      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.

    aliases:
      description:
        - A I(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.

    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: 'no'
      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.

    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).

    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.

    origins:
      description:
        - A config element that is a I(list[]) of complex origin objects to be specified for the distribution. Used for creating and updating distributions.
          Each origin item comprises the attributes
            I(id)
            I(domain_name) (defaults to default_origin_domain_name if not specified)
            I(origin_path) (defaults to default_origin_path if not specified)
            I(custom_headers[])
              I(header_name)
              I(header_value)
            I(s3_origin_access_identity_enabled)
            I(custom_origin_config)
              I(http_port)
              I(https_port)
              I(origin_protocol_policy)
              I(origin_ssl_protocols[])
              I(origin_read_timeout)
              I(origin_keepalive_timeout)

    purge_origins:
      description: Whether to remove any origins that aren't listed in I(origins)
      default: false
      type: bool
    default_cache_behavior:
      description:
        - A config element that is a complex object 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 I(cache_behavior) in I(cache_behaviors) with defaults.
          The default cache behavior comprises the attributes
            I(target_origin_id)
            I(forwarded_values)
              I(query_string)
              I(cookies)
                I(forward)
                I(whitelisted_names)
              I(headers[])
              I(query_string_cache_keys[])
              I(trusted_signers)
                I(enabled)
                I(items[])
              I(viewer_protocol_policy)
              I(min_ttl)
              I(allowed_methods)
                I(items[])
                I(cached_methods[])
              I(smooth_streaming)
              I(default_ttl)
              I(max_ttl)
              I(compress)
              I(lambda_function_associations[])
                I(lambda_function_arn)
                I(event_type)
              I(field_level_encryption_id)

    cache_behaviors:
      description:
        - A config element that is a I(list[]) of complex cache behavior objects to be specified for the distribution. The order
          of the list is preserved across runs unless C(purge_cache_behavior) is enabled.
          Each cache behavior comprises the attributes
            I(path_pattern)
            I(target_origin_id)
            I(forwarded_values)
              I(query_string)
              I(cookies)
              I(forward)
              I(whitelisted_names)
              I(headers[])
              I(query_string_cache_keys[])
            I(trusted_signers)
              I(enabled)
              I(items[])
            I(viewer_protocol_policy)
            I(min_ttl)
            I(allowed_methods)
              I(items[])
              I(cached_methods[])
            I(smooth_streaming)
            I(default_ttl)
            I(max_ttl)
            I(compress)
            I(lambda_function_associations[])
            I(field_level_encryption_id)

    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 cache_behaviors.
      default: false
      type: bool
    custom_error_responses:
      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.
          Each custom error response object comprises the attributes
            I(error_code)
            I(response_page_path)
            I(response_code)
            I(error_caching_min_ttl)

    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.

    logging:
      description:
        - A config element that is a complex object that defines logging for the distribution.
          The logging object comprises the attributes
            I(enabled)
            I(include_cookies)
            I(bucket)
            I(prefix)

    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
              Europe
            I(price_class=PriceClass_200) consists of the areas
              United States
              Canada
              Europe
              Hong Kong, Philippines, S. Korea, Singapore & Taiwan
              Japan
              India
            I(price_class=PriceClass_All) consists of the areas
              United States
              Canada
              Europe
              Hong Kong, Philippines, S. Korea, Singapore & Taiwan
              Japan
              India
              South America
              Australia
      choices: ['PriceClass_100', 'PriceClass_200', 'PriceClass_All']
      default: aws defaults this to 'PriceClass_All'

    enabled:
      description:
        - A boolean value that specifies whether the distribution is enabled or disabled.
      default: 'yes'
      type: bool

    viewer_certificate:
      description:
        - A config element that is a complex object that specifies the encryption details of the distribution.
          Comprises the following attributes
            I(cloudfront_default_certificate)
            I(iam_certificate_id)
            I(acm_certificate_arn)
            I(ssl_support_method)
            I(minimum_protocol_version)
            I(certificate)
            I(certificate_source)

    restrictions:
      description:
        - A config element that is a complex object that describes how a distribution should restrict it's content.
          The restriction object comprises the following attributes
            I(geo_restriction)
              I(restriction_type)
              I(items[])

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

    http_version:
      description:
        - The version of the http protocol to use for the distribution.
      choices: [ 'http1.1', 'http2' ]
      default: aws defaults this to 'http2'

    ipv6_enabled:
      description:
        - Determines whether IPv6 support is enabled or not.
      type: bool
      default: 'no'

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

    wait_timeout:
      description:
        - Specifies the duration in seconds to wait for a timeout of a cloudfront create or update. Defaults to 1800 seconds (30 minutes).
      default: 1800

'''

EXAMPLES = '''

# create a basic distribution with defaults and tags

- cloudfront_distribution:
    state: present
    default_origin_domain_name: www.my-cloudfront-origin.com
    tags:
      Name: example distribution
      Project: example project
      Priority: '1'

# update a distribution comment by distribution_id

- cloudfront_distribution:
    state: present
    distribution_id: E1RP5A2MJ8073O
    comment: modified by ansible cloudfront.py

# update a distribution comment by caller_reference

- cloudfront_distribution:
    state: present
    caller_reference: my cloudfront distribution 001
    comment: modified by ansible cloudfront.py

# update a distribution's aliases and comment using the distribution_id as a reference

- cloudfront_distribution:
    state: present
    distribution_id: E1RP5A2MJ8073O
    comment: modified by cloudfront.py again
    aliases: [ 'www.my-distribution-source.com', 'zzz.aaa.io' ]

# update a distribution's aliases and comment using an alias as a reference

- cloudfront_distribution:
    state: present
    caller_reference: my test distribution
    comment: modified by cloudfront.py again
    aliases:
      - www.my-distribution-source.com
      - zzz.aaa.io

# update a distribution's comment and aliases and tags and remove existing tags

- cloudfront_distribution:
    state: present
    distribution_id: E15BU8SDCGSG57
    comment: modified by cloudfront.py again
    aliases:
      - tested.com
    tags:
      Project: distribution 1.2
    purge_tags: yes

# create a distribution with an origin, logging and default cache behavior

- 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

# delete a distribution

- cloudfront_distribution:
    state: absent
    caller_reference: replaceable distribution
'''

RETURN = '''
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) is C(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) is C(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: ''
    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.module_utils.aws.core import AnsibleAWSModule
from ansible.module_utils.aws.cloudfront_facts import CloudFrontFactsServiceManager
from ansible.module_utils.common.dict_transformations import recursive_diff
from ansible.module_utils.ec2 import get_aws_connection_info
from ansible.module_utils.ec2 import ec2_argument_spec, boto3_conn, compare_aws_tags
from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list
from ansible.module_utils.ec2 import snake_dict_to_camel_dict, boto3_tag_list_to_ansible_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


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(DistributionConfig=config)['Distribution']
        else:
            distribution_config_with_tags = {
                'DistributionConfig': config,
                'Tags': {
                    'Items': tags
                }
            }
            return client.create_distribution_with_tags(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(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(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(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(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(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'
        ])
        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['s3_origin_access_identity_enabled'] and existing_config.get('s3_origin_config', {}).get('origin_access_identity'):
            return existing_config['s3_origin_config']['origin_access_identity']
        if not origin['s3_origin_access_identity_enabled']:
            return None
        try:
            comment = "access-identity-by-ansible-%s-%s" % (origin.get('domain_name'), self.__default_datetime_string)
            cfoai_config = dict(CloudFrontOriginAccessIdentityConfig=dict(CallerReference=self.__default_datetime_string,
                                                                          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:
                    s3_origin_config = self.validate_s3_origin_configuration(client, existing_config, origin)
                    if s3_origin_config:
                        oai = s3_origin_config
                    else:
                        oai = ""
                    origin["s3_origin_config"] = dict(origin_access_identity=oai)
                    del(origin["s3_origin_access_identity_enabled"])
                    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):
        # only set field_level_encryption_id if it's already set or if it was passed
        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')
        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_v_6_enabled'] = ipv6_enabled or config.get('i_p_v_6_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:
                config = self.__cloudfront_facts_mgr.get_distribution(distribution_id)
                distribution = config.get(distribution_name)
                if distribution is not None:
                    distribution_config = distribution.get(distribution_config_name)
                    if distribution_config is not None and distribution_config.get('CallerReference') == caller_reference:
                        distribution['DistributionConfig'] = 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:
                    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(msg="Timeout waiting for cloudfront action. Waited for {0} seconds before timeout. "
                                  "Error: {1}".format(to_text(wait_timeout), to_native(e)))

        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 = ec2_argument_spec()

    argument_spec.update(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=[]),
        purge_aliases=dict(type='bool', default=False),
        default_root_object=dict(),
        origins=dict(type='list'),
        purge_origins=dict(type='bool', default=False),
        default_cache_behavior=dict(type='dict'),
        cache_behaviors=dict(type='list'),
        purge_cache_behaviors=dict(type='bool', default=False),
        custom_error_responses=dict(type='list'),
        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'],
        ]
    )

    region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True)
    client = boto3_conn(module, conn_type='client', resource='cloudfront', region=region, endpoint=ec2_url, **aws_connect_kwargs)

    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()
