Repository: ansible-collections/amazon.aws

Issue:
Ignore ansible-deprecated-no-version in 2.9
##### SUMMARY
Until 2.9.10 is released with  #70078


##### ISSUE TYPE
- Bugfix Pull Request

##### COMPONENT NAME
tests/sanity/ignore-2.9.txt



Discussion:

Resolving patch:
diff --git a/plugins/modules/cloudwatchlogs_log_group.py b/plugins/modules/cloudwatchlogs_log_group.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/cloudwatchlogs_log_group.py
@@ -0,0 +1,348 @@
+#!/usr/bin/python
+# Copyright: 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 = '''
+---
+module: cloudwatchlogs_log_group
+version_added: 1.0.0
+short_description: create or delete log_group in CloudWatchLogs
+notes:
+  - For details of the parameters and returns see U(http://boto3.readthedocs.io/en/latest/reference/services/logs.html).
+  - Support for I(purge_tags) was added in release 4.0.0.
+description:
+  - Create or delete log_group in CloudWatchLogs.
+author:
+  - Willian Ricardo (@willricardo) <willricardo@gmail.com>
+options:
+  state:
+    description:
+      - Whether the rule is present or absent.
+    choices: ["present", "absent"]
+    default: present
+    required: false
+    type: str
+  log_group_name:
+    description:
+      - The name of the log group.
+    required: true
+    type: str
+  kms_key_id:
+    description:
+      - The Amazon Resource Name (ARN) of the CMK to use when encrypting log data.
+    required: false
+    type: str
+  retention:
+    description:
+      - The number of days to retain the log events in the specified log group.
+      - "Valid values are: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]"
+      - Mutually exclusive with I(purge_retention_policy).
+    required: false
+    type: int
+  purge_retention_policy:
+    description:
+      - "Whether to purge the retention policy or not."
+      - "Mutually exclusive with I(retention) and I(overwrite)."
+    default: false
+    required: false
+    type: bool
+  overwrite:
+    description:
+      - Whether an existing log group should be overwritten on create.
+      - Mutually exclusive with I(purge_retention_policy).
+    default: false
+    required: false
+    type: bool
+extends_documentation_fragment:
+  - amazon.aws.aws
+  - amazon.aws.ec2
+  - amazon.aws.tags
+
+'''
+
+EXAMPLES = '''
+# Note: These examples do not set authentication details, see the AWS Guide for details.
+
+- amazon.aws.cloudwatchlogs_log_group:
+    log_group_name: test-log-group
+
+- amazon.aws.cloudwatchlogs_log_group:
+    state: present
+    log_group_name: test-log-group
+    tags: { "Name": "test-log-group", "Env" : "QA" }
+
+- amazon.aws.cloudwatchlogs_log_group:
+    state: present
+    log_group_name: test-log-group
+    tags: { "Name": "test-log-group", "Env" : "QA" }
+    kms_key_id: arn:aws:kms:region:account-id:key/key-id
+
+- amazon.aws.cloudwatchlogs_log_group:
+    state: absent
+    log_group_name: test-log-group
+
+'''
+
+RETURN = '''
+log_groups:
+    description: Return the list of complex objects representing log groups
+    returned: success
+    type: complex
+    version_added: 4.0.0
+    contains:
+        log_group_name:
+            description: The name of the log group.
+            returned: always
+            type: str
+        creation_time:
+            description: The creation time of the log group.
+            returned: always
+            type: int
+        retention_in_days:
+            description: The number of days to retain the log events in the specified log group.
+            returned: always
+            type: int
+        metric_filter_count:
+            description: The number of metric filters.
+            returned: always
+            type: int
+        arn:
+            description: The Amazon Resource Name (ARN) of the log group.
+            returned: always
+            type: str
+        stored_bytes:
+            description: The number of bytes stored.
+            returned: always
+            type: str
+        kms_key_id:
+            description: The Amazon Resource Name (ARN) of the CMK to use when encrypting log data.
+            returned: always
+            type: str
+        tags:
+            description: A dictionary representing the tags on the log group.
+            returned: always
+            type: dict
+'''
+
+try:
+    import botocore
+except ImportError:
+    pass  # Handled by AnsibleAWSModule
+
+from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
+
+from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
+from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
+from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags
+
+
+def create_log_group(client, log_group_name, kms_key_id, tags, retention, module):
+    request = {'logGroupName': log_group_name}
+    if kms_key_id:
+        request['kmsKeyId'] = kms_key_id
+    if tags:
+        request['tags'] = tags
+
+    if module.check_mode:
+        module.exit_json(changed=True, msg="Would have created log group if not in check_mode.")
+
+    try:
+        client.create_log_group(**request)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Unable to create log group")
+
+    if retention:
+        input_retention_policy(client=client,
+                               log_group_name=log_group_name,
+                               retention=retention, module=module)
+
+    found_log_group = describe_log_group(client=client, log_group_name=log_group_name, module=module)
+
+    if not found_log_group:
+        module.fail_json(msg="The aws CloudWatchLogs log group was not created. \n please try again!")
+    return found_log_group
+
+
+def input_retention_policy(client, log_group_name, retention, module):
+    try:
+        permited_values = [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]
+
+        if retention in permited_values:
+            response = client.put_retention_policy(logGroupName=log_group_name,
+                                                   retentionInDays=retention)
+        else:
+            delete_log_group(client=client, log_group_name=log_group_name, module=module)
+            module.fail_json(msg="Invalid retention value. Valid values are: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]")
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Unable to put retention policy for log group {0}".format(log_group_name))
+
+
+def delete_retention_policy(client, log_group_name, module):
+    if module.check_mode:
+        return True
+
+    try:
+        client.delete_retention_policy(logGroupName=log_group_name)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Unable to delete retention policy for log group {0}".format(log_group_name))
+
+
+def delete_log_group(client, log_group_name, module):
+    if module.check_mode:
+        module.exit_json(changed=True, msg="Would have deleted log group if not in check_mode.")
+
+    try:
+        client.delete_log_group(logGroupName=log_group_name)
+    except is_boto3_error_code('ResourceNotFoundException'):
+        return {}
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:  # pylint: disable=duplicate-except
+        module.fail_json_aws(e, msg="Unable to delete log group {0}".format(log_group_name))
+
+
+def describe_log_group(client, log_group_name, module):
+    try:
+        desc_log_group = client.describe_log_groups(logGroupNamePrefix=log_group_name)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Unable to describe log group {0}".format(log_group_name))
+
+    matching_logs = [log for log in desc_log_group.get('logGroups', []) if log['logGroupName'] == log_group_name]
+
+    if not matching_logs:
+        return {}
+
+    found_log_group = matching_logs[0]
+
+    try:
+        tags = client.list_tags_log_group(logGroupName=log_group_name)
+    except is_boto3_error_code('AccessDeniedException'):
+        tags = {}
+        module.warn('Permission denied listing tags for log group {0}'.format(log_group_name))
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:  # pylint: disable=duplicate-except
+        module.fail_json_aws(e, msg="Unable to describe tags for log group {0}".format(log_group_name))
+
+    found_log_group['tags'] = tags.get('tags', {})
+    return found_log_group
+
+
+def format_result(found_log_group):
+    # Prior to 4.0.0 we documented returning log_groups=[log_group], but returned **log_group
+    # Return both to avoid a breaking change.
+    log_group = camel_dict_to_snake_dict(found_log_group, ignore_list=['tags'])
+    return dict(log_groups=[log_group], **log_group)
+
+
+def ensure_tags(client, found_log_group, desired_tags, purge_tags, module):
+    if desired_tags is None:
+        return False
+
+    group_name = module.params.get('log_group_name')
+    current_tags = found_log_group.get('tags', {})
+    tags_to_add, tags_to_remove = compare_aws_tags(current_tags, desired_tags, purge_tags)
+
+    if not tags_to_add and not tags_to_remove:
+        return False
+    if module.check_mode:
+        return True
+
+    try:
+        if tags_to_remove:
+            client.untag_log_group(logGroupName=group_name, tags=tags_to_remove)
+        if tags_to_add:
+            client.tag_log_group(logGroupName=group_name, tags=tags_to_add)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg='Failed to update tags')
+
+    return True
+
+
+def main():
+    argument_spec = dict(
+        log_group_name=dict(required=True, type='str'),
+        state=dict(choices=['present', 'absent'],
+                   default='present'),
+        kms_key_id=dict(required=False, type='str'),
+        tags=dict(required=False, type='dict', aliases=['resource_tags']),
+        purge_tags=dict(required=False, type='bool', default=True),
+        retention=dict(required=False, type='int'),
+        purge_retention_policy=dict(required=False, type='bool', default=False),
+        overwrite=dict(required=False, type='bool', default=False),
+    )
+
+    mutually_exclusive = [['retention', 'purge_retention_policy'], ['purge_retention_policy', 'overwrite']]
+    module = AnsibleAWSModule(supports_check_mode=True, argument_spec=argument_spec, mutually_exclusive=mutually_exclusive)
+
+    try:
+        logs = module.client('logs')
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg='Failed to connect to AWS')
+
+    state = module.params.get('state')
+    changed = False
+
+    # Determine if the log group exists
+    found_log_group = describe_log_group(client=logs, log_group_name=module.params['log_group_name'], module=module)
+
+    if state == 'present':
+        if found_log_group:
+            if module.params['overwrite'] is True:
+                changed = True
+                delete_log_group(client=logs, log_group_name=module.params['log_group_name'], module=module)
+                found_log_group = create_log_group(client=logs,
+                                                   log_group_name=module.params['log_group_name'],
+                                                   kms_key_id=module.params['kms_key_id'],
+                                                   tags=module.params['tags'],
+                                                   retention=module.params['retention'],
+                                                   module=module)
+            else:
+                changed |= ensure_tags(client=logs,
+                                       found_log_group=found_log_group,
+                                       desired_tags=module.params['tags'],
+                                       purge_tags=module.params['purge_tags'],
+                                       module=module)
+                if module.params['purge_retention_policy']:
+                    if found_log_group.get('retentionInDays'):
+                        changed = True
+                        delete_retention_policy(client=logs,
+                                                log_group_name=module.params['log_group_name'],
+                                                module=module)
+                elif module.params['retention'] != found_log_group.get('retentionInDays'):
+                    if module.params['retention'] is not None:
+                        changed = True
+                        input_retention_policy(client=logs,
+                                               log_group_name=module.params['log_group_name'],
+                                               retention=module.params['retention'],
+                                               module=module)
+                if changed:
+                    found_log_group = describe_log_group(client=logs,
+                                                         log_group_name=module.params['log_group_name'],
+                                                         module=module)
+
+        elif not found_log_group:
+            changed = True
+            found_log_group = create_log_group(client=logs,
+                                               log_group_name=module.params['log_group_name'],
+                                               kms_key_id=module.params['kms_key_id'],
+                                               tags=module.params['tags'],
+                                               retention=module.params['retention'],
+                                               module=module)
+
+        result = format_result(found_log_group)
+        module.exit_json(changed=changed, **result)
+
+    elif state == 'absent':
+        if found_log_group:
+            changed = True
+            delete_log_group(client=logs,
+                             log_group_name=module.params['log_group_name'],
+                             module=module)
+
+    module.exit_json(changed=changed)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/plugins/modules/cloudwatchlogs_log_group_info.py b/plugins/modules/cloudwatchlogs_log_group_info.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/cloudwatchlogs_log_group_info.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python
+# Copyright: 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 = '''
+---
+module: cloudwatchlogs_log_group_info
+version_added: 1.0.0
+short_description: Get information about log_group in CloudWatchLogs
+description:
+  - Lists the specified log groups. You can list all your log groups or filter the results by prefix.
+author:
+  - Willian Ricardo (@willricardo) <willricardo@gmail.com>
+options:
+  log_group_name:
+    description:
+      - The name or prefix of the log group to filter by.
+    type: str
+extends_documentation_fragment:
+  - amazon.aws.aws
+  - amazon.aws.ec2
+'''
+
+EXAMPLES = '''
+# Note: These examples do not set authentication details, see the AWS Guide for details.
+- amazon.aws.cloudwatchlogs_log_group_info:
+    log_group_name: test-log-group
+'''
+
+RETURN = '''
+log_groups:
+    description: Return the list of complex objects representing log groups
+    returned: success
+    type: complex
+    contains:
+        log_group_name:
+            description: The name of the log group.
+            returned: always
+            type: str
+        creation_time:
+            description: The creation time of the log group.
+            returned: always
+            type: int
+        retention_in_days:
+            description: The number of days to retain the log events in the specified log group.
+            returned: always
+            type: int
+        metric_filter_count:
+            description: The number of metric filters.
+            returned: always
+            type: int
+        arn:
+            description: The Amazon Resource Name (ARN) of the log group.
+            returned: always
+            type: str
+        stored_bytes:
+            description: The number of bytes stored.
+            returned: always
+            type: str
+        kms_key_id:
+            description: The Amazon Resource Name (ARN) of the CMK to use when encrypting log data.
+            returned: always
+            type: str
+        tags:
+            description: A dictionary representing the tags on the log group.
+            returned: always
+            type: dict
+            version_added: 4.0.0
+'''
+
+try:
+    import botocore
+except ImportError:
+    pass  # Handled by AnsibleAWSModule
+
+from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
+
+from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
+from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
+
+
+def describe_log_group(client, log_group_name, module):
+    params = {}
+    if log_group_name:
+        params['logGroupNamePrefix'] = log_group_name
+    try:
+        paginator = client.get_paginator('describe_log_groups')
+        desc_log_group = paginator.paginate(**params).build_full_result()
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Unable to describe log group {0}".format(log_group_name))
+
+    for log_group in desc_log_group['logGroups']:
+        log_group_name = log_group['logGroupName']
+        try:
+            tags = client.list_tags_log_group(logGroupName=log_group_name)
+        except is_boto3_error_code('AccessDeniedException'):
+            tags = {}
+            module.warn('Permission denied listing tags for log group {0}'.format(log_group_name))
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:  # pylint: disable=duplicate-except
+            module.fail_json_aws(e, msg="Unable to describe tags for log group {0}".format(log_group_name))
+        log_group['tags'] = tags.get('tags', {})
+
+    return desc_log_group
+
+
+def main():
+    argument_spec = dict(
+        log_group_name=dict(),
+    )
+
+    module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)
+
+    try:
+        logs = module.client('logs')
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg='Failed to connect to AWS')
+
+    desc_log_group = describe_log_group(client=logs,
+                                        log_group_name=module.params['log_group_name'],
+                                        module=module)
+    final_log_group_snake = []
+
+    for log_group in desc_log_group['logGroups']:
+        final_log_group_snake.append(camel_dict_to_snake_dict(log_group, ignore_list=['tags']))
+
+    desc_log_group_result = dict(changed=False, log_groups=final_log_group_snake)
+    module.exit_json(**desc_log_group_result)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/plugins/modules/cloudwatchlogs_log_group_metric_filter.py b/plugins/modules/cloudwatchlogs_log_group_metric_filter.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/cloudwatchlogs_log_group_metric_filter.py
@@ -0,0 +1,216 @@
+#!/usr/bin/python
+# Copyright: 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 = '''
+---
+module: cloudwatchlogs_log_group_metric_filter
+version_added: 1.0.0
+author:
+  - "Markus Bergholz (@markuman)"
+short_description: Manage CloudWatch log group metric filter
+description:
+  - Create, modify and delete CloudWatch log group metric filter.
+  - CloudWatch log group metric filter can be use with M(community.aws.ec2_metric_alarm).
+options:
+    state:
+      description:
+        - Whether the rule is present or absent.
+      choices: ["present", "absent"]
+      required: true
+      type: str
+    log_group_name:
+      description:
+        - The name of the log group where the metric filter is applied on.
+      required: true
+      type: str
+    filter_name:
+      description:
+        - A name for the metric filter you create.
+      required: true
+      type: str
+    filter_pattern:
+      description:
+        - A filter pattern for extracting metric data out of ingested log events. Required when I(state=present).
+      type: str
+    metric_transformation:
+      description:
+        - A collection of information that defines how metric data gets emitted. Required when I(state=present).
+      type: dict
+      suboptions:
+        metric_name:
+          description:
+            - The name of the cloudWatch metric.
+          type: str
+        metric_namespace:
+          description:
+            - The namespace of the cloudWatch metric.
+          type: str
+        metric_value:
+          description:
+            - The value to publish to the cloudWatch metric when a filter pattern matches a log event.
+          type: str
+        default_value:
+          description:
+            - The value to emit when a filter pattern does not match a log event.
+          type: float
+extends_documentation_fragment:
+- amazon.aws.aws
+- amazon.aws.ec2
+
+'''
+
+EXAMPLES = '''
+- name: set metric filter on log group /fluentd/testcase
+  amazon.aws.cloudwatchlogs_log_group_metric_filter:
+    log_group_name: /fluentd/testcase
+    filter_name: BoxFreeStorage
+    filter_pattern: '{($.value = *) && ($.hostname = "box")}'
+    state: present
+    metric_transformation:
+        metric_name: box_free_space
+        metric_namespace: fluentd_metrics
+        metric_value: "$.value"
+
+- name: delete metric filter on log group /fluentd/testcase
+  amazon.aws.cloudwatchlogs_log_group_metric_filter:
+    log_group_name: /fluentd/testcase
+    filter_name: BoxFreeStorage
+    state: absent
+'''
+
+RETURN = """
+metric_filters:
+    description: Return the origin response value
+    returned: success
+    type: list
+    sample: [
+        {
+            "default_value": 3.1415,
+            "metric_name": "box_free_space",
+            "metric_namespace": "made_with_ansible",
+            "metric_value": "$.value"
+        }
+    ]
+
+"""
+
+from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
+
+from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
+
+
+def metricTransformationHandler(metricTransformations, originMetricTransformations=None):
+
+    if originMetricTransformations:
+        change = False
+        originMetricTransformations = camel_dict_to_snake_dict(
+            originMetricTransformations)
+        for item in ["default_value", "metric_name", "metric_namespace", "metric_value"]:
+            if metricTransformations.get(item) != originMetricTransformations.get(item):
+                change = True
+    else:
+        change = True
+
+    defaultValue = metricTransformations.get("default_value")
+    if isinstance(defaultValue, int) or isinstance(defaultValue, float):
+        retval = [
+            {
+                'metricName': metricTransformations.get("metric_name"),
+                'metricNamespace': metricTransformations.get("metric_namespace"),
+                'metricValue': metricTransformations.get("metric_value"),
+                'defaultValue': defaultValue
+            }
+        ]
+    else:
+        retval = [
+            {
+                'metricName': metricTransformations.get("metric_name"),
+                'metricNamespace': metricTransformations.get("metric_namespace"),
+                'metricValue': metricTransformations.get("metric_value"),
+            }
+        ]
+
+    return retval, change
+
+
+def main():
+
+    arg_spec = dict(
+        state=dict(type='str', required=True, choices=['present', 'absent']),
+        log_group_name=dict(type='str', required=True),
+        filter_name=dict(type='str', required=True),
+        filter_pattern=dict(type='str'),
+        metric_transformation=dict(type='dict', options=dict(
+            metric_name=dict(type='str'),
+            metric_namespace=dict(type='str'),
+            metric_value=dict(type='str'),
+            default_value=dict(type='float')
+        )),
+    )
+
+    module = AnsibleAWSModule(
+        argument_spec=arg_spec,
+        supports_check_mode=True,
+        required_if=[('state', 'present', ['metric_transformation', 'filter_pattern'])]
+    )
+
+    log_group_name = module.params.get("log_group_name")
+    filter_name = module.params.get("filter_name")
+    filter_pattern = module.params.get("filter_pattern")
+    metric_transformation = module.params.get("metric_transformation")
+    state = module.params.get("state")
+
+    cwl = module.client('logs')
+
+    # check if metric filter exists
+    response = cwl.describe_metric_filters(
+        logGroupName=log_group_name,
+        filterNamePrefix=filter_name
+    )
+
+    if len(response.get("metricFilters")) == 1:
+        originMetricTransformations = response.get(
+            "metricFilters")[0].get("metricTransformations")[0]
+        originFilterPattern = response.get("metricFilters")[
+            0].get("filterPattern")
+    else:
+        originMetricTransformations = None
+        originFilterPattern = None
+    change = False
+    metricTransformation = None
+
+    if state == "absent" and originMetricTransformations:
+        if not module.check_mode:
+            response = cwl.delete_metric_filter(
+                logGroupName=log_group_name,
+                filterName=filter_name
+            )
+        change = True
+        metricTransformation = [camel_dict_to_snake_dict(item) for item in [originMetricTransformations]]
+
+    elif state == "present":
+        metricTransformation, change = metricTransformationHandler(
+            metricTransformations=metric_transformation, originMetricTransformations=originMetricTransformations)
+
+        change = change or filter_pattern != originFilterPattern
+
+        if change:
+            if not module.check_mode:
+                response = cwl.put_metric_filter(
+                    logGroupName=log_group_name,
+                    filterName=filter_name,
+                    filterPattern=filter_pattern,
+                    metricTransformations=metricTransformation
+                )
+
+        metricTransformation = [camel_dict_to_snake_dict(item) for item in metricTransformation]
+
+    module.exit_json(changed=change, metric_filters=metricTransformation)
+
+
+if __name__ == '__main__':
+    main()


Test patch:
diff --git a/tests/integration/targets/cloudwatchlogs/aliases b/tests/integration/targets/cloudwatchlogs/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/cloudwatchlogs/aliases
@@ -0,0 +1,5 @@
+cloud/aws
+
+cloudwatchlogs_log_group
+cloudwatchlogs_log_group_info
+cloudwatchlogs_log_group_metric_filter
diff --git a/tests/integration/targets/cloudwatchlogs/defaults/main.yml b/tests/integration/targets/cloudwatchlogs/defaults/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/cloudwatchlogs/defaults/main.yml
@@ -0,0 +1,2 @@
+log_group_name: '{{ resource_prefix }}/integrationtest'
+filter_name: '{{ resource_prefix }}/AnsibleTest'
diff --git a/tests/integration/targets/cloudwatchlogs/meta/main.yml b/tests/integration/targets/cloudwatchlogs/meta/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/cloudwatchlogs/meta/main.yml
@@ -0,0 +1 @@
+dependencies: []
diff --git a/tests/integration/targets/cloudwatchlogs/tasks/cloudwatchlogs_tests.yml b/tests/integration/targets/cloudwatchlogs/tasks/cloudwatchlogs_tests.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/cloudwatchlogs/tasks/cloudwatchlogs_tests.yml
@@ -0,0 +1,151 @@
+# Tests for changes to the cloudwatchlogs_log_group and cloudwatchlogs_log_group_metric_filter
+
+- block:
+
+  - name: create cloudwatch log group for integration test
+    cloudwatchlogs_log_group:
+      state: present
+      log_group_name: '{{ log_group_name }}'
+      retention: 1
+
+  - name: check_mode set metric filter on '{{ log_group_name }}'
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      filter_pattern: '{ ($.value = *) && ($.hostname = "box")}'
+      state: present
+      metric_transformation:
+        metric_name: box_free_space
+        metric_namespace: fluentd_metrics
+        metric_value: $.value
+    check_mode: yes
+    register: out
+
+  - name: check_mode state must be changed
+    assert:
+      that:
+      - out is changed
+      - out.metric_filters | count == 1
+
+  - name: set metric filter on '{{ log_group_name }}'
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      filter_pattern: '{ ($.value = *) && ($.hostname = "box")}'
+      state: present
+      metric_transformation:
+        metric_name: box_free_space
+        metric_namespace: fluentd_metrics
+        metric_value: $.value
+    register: out
+
+  - name: create metric filter
+    assert:
+      that:
+      - out is changed
+      - out.metric_filters | count == 1
+
+  - name: re-set metric filter on '{{ log_group_name }}'
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      filter_pattern: '{ ($.value = *) && ($.hostname = "box")}'
+      state: present
+      metric_transformation:
+        metric_name: box_free_space
+        metric_namespace: fluentd_metrics
+        metric_value: $.value
+    register: out
+
+  - name: metric filter must not change
+    assert:
+      that:
+      - out is not changed
+
+  - name: update metric transformation on '{{ log_group_name }}'
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      filter_pattern: '{ ($.value = *) && ($.hostname = "box")}'
+      state: present
+      metric_transformation:
+        metric_name: box_free_space
+        metric_namespace: made_with_ansible
+        metric_value: $.value
+        default_value: 3.1415
+    register: out
+
+  - name: update metric filter
+    assert:
+      that:
+      - out is changed
+      - out.metric_filters[0].metric_namespace == "made_with_ansible"
+      - out.metric_filters[0].default_value == 3.1415
+
+  - name: update filter_pattern on '{{ log_group_name }}'
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      filter_pattern: '{ ($.value = *) && ($.hostname = "ansible")}'
+      state: present
+      metric_transformation:
+        metric_name: box_free_space
+        metric_namespace: made_with_ansible
+        metric_value: $.value
+    register: out
+
+  - name: update metric filter
+    assert:
+      that:
+      - out is changed
+      - out.metric_filters[0].metric_namespace == "made_with_ansible"
+
+  - name: checkmode delete metric filter on '{{ log_group_name }}'
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      state: absent
+    check_mode: yes
+    register: out
+
+  - name: check_mode state must be changed
+    assert:
+      that:
+      - out is changed
+
+  - name: delete metric filter on '{{ log_group_name }}'
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      state: absent
+    register: out
+
+  - name: delete metric filter
+    assert:
+      that:
+      - out is changed
+
+  - name: delete metric filter on '{{ log_group_name }}' which does not exist
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      state: absent
+    register: out
+
+  - name: delete metric filter
+    assert:
+      that:
+      - out is not changed
+
+  always:
+  - name: delete metric filter
+    cloudwatchlogs_log_group_metric_filter:
+      log_group_name: '{{ log_group_name }}'
+      filter_name: '{{ filter_name }}'
+      state: absent
+
+  - name: delete cloudwatch log group for integration test
+    cloudwatchlogs_log_group:
+      state: absent
+      log_group_name: '{{ log_group_name }}'
+  ignore_errors: true
diff --git a/tests/integration/targets/cloudwatchlogs/tasks/create-delete-tags.yml b/tests/integration/targets/cloudwatchlogs/tasks/create-delete-tags.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/cloudwatchlogs/tasks/create-delete-tags.yml
@@ -0,0 +1,444 @@
+# Tests relating to create/delete and set tags on cloudwatchlogs_log_group
+
+- name: Tests relating to setting tags on cloudwatchlogs_log_group
+  vars:
+    first_tags:
+      Key with Spaces: Value with spaces
+      CamelCaseKey: CamelCaseValue
+      pascalCaseKey: pascalCaseValue
+      snake_case_key: snake_case_value
+    second_tags:
+      New Key with Spaces: Value with spaces
+      NewCamelCaseKey: CamelCaseValue
+      newPascalCaseKey: pascalCaseValue
+      new_snake_case_key: snake_case_value
+    third_tags:
+      Key with Spaces: Value with spaces
+      CamelCaseKey: CamelCaseValue
+      pascalCaseKey: pascalCaseValue
+      snake_case_key: snake_case_value
+      New Key with Spaces: Updated Value with spaces
+    final_tags:
+      Key with Spaces: Value with spaces
+      CamelCaseKey: CamelCaseValue
+      pascalCaseKey: pascalCaseValue
+      snake_case_key: snake_case_value
+      New Key with Spaces: Updated Value with spaces
+      NewCamelCaseKey: CamelCaseValue
+      newPascalCaseKey: pascalCaseValue
+      new_snake_case_key: snake_case_value
+  # Mandatory settings
+  module_defaults:
+    amazon.aws.cloudwatchlogs_log_group:
+      state: present
+      log_group_name: '{{ log_group_name }}'
+    amazon.aws.cloudwatchlogs_log_group_info:
+      log_group_name: '{{ log_group_name }}'
+  block:
+
+  - name: create cloudwatch log group for integration test (check_mode)
+    cloudwatchlogs_log_group:
+      state: present
+      log_group_name: '{{ log_group_name }}'
+      retention: 1
+      tags:
+        CamelCase: Value
+        snake_case: value
+    check_mode: true
+    register: result
+
+  - assert:
+      that:
+      - result is changed
+      - '"log_groups" not in result'
+      - '"logs:CreateLogGroup" not in result.resource_actions'
+
+  - name: create cloudwatch log group for integration test
+    cloudwatchlogs_log_group:
+      state: present
+      log_group_name: '{{ log_group_name }}'
+      retention: 1
+      tags:
+        CamelCase: Value
+        snake_case: value
+    register: result
+
+  - assert:
+      that:
+      - result is changed
+      - '"log_groups" in result'
+      - result.log_groups | length == 1
+      - '"log_group_name" in log_group'
+      - '"creation_time" in log_group'
+      - '"retention_in_days" in log_group'
+      - '"metric_filter_count" in log_group'
+      - '"arn" in log_group'
+      - '"stored_bytes" in log_group'
+      # - '"kms_key_id" in log_group'
+      # pre-4.0.0 upgrade compatability
+      - '"log_group_name" in result'
+      - '"creation_time" in result'
+      - '"retention_in_days" in result'
+      - '"metric_filter_count" in result'
+      - '"arn" in result'
+      - '"stored_bytes" in result'
+      # - '"kms_key_id" in result'
+      - '"CamelCase" in log_group.tags'
+      - '"snake_case" in log_group.tags'
+    vars:
+      log_group: '{{ result.log_groups[0] }}'
+
+  - name: create cloudwatch log group for integration test (check_mode - idempotent)
+    cloudwatchlogs_log_group:
+      state: present
+      log_group_name: '{{ log_group_name }}'
+      retention: 1
+    check_mode: true
+    register: result
+
+  - assert:
+      that:
+      - result is not changed
+      - '"log_groups" in result'
+      - result.log_groups | length == 1
+
+  - name: create cloudwatch log group for integration test (idempotent)
+    cloudwatchlogs_log_group:
+      state: present
+      log_group_name: '{{ log_group_name }}'
+      retention: 1
+    register: result
+
+  - assert:
+      that:
+      - result is not changed
+      - '"log_groups" in result'
+      - result.log_groups | length == 1
+    vars:
+      log_group: '{{ result.log_groups[0] }}'
+
+  - name: describe all log groups
+    cloudwatchlogs_log_group_info: {}
+    register: result
+
+  - assert:
+      that:
+      - '"log_groups" in result'
+      - result.log_groups | length >= 1
+
+  - name: describe log group
+    cloudwatchlogs_log_group_info:
+      log_group_name: '{{ log_group_name }}'
+    register: result
+
+  - assert:
+      that:
+      - '"log_groups" in result'
+      - result.log_groups | length == 1
+      - '"log_group_name" in log_group'
+      - '"creation_time" in log_group'
+      - '"retention_in_days" in log_group'
+      - '"metric_filter_count" in log_group'
+      - '"arn" in log_group'
+      - '"stored_bytes" in log_group'
+      # - '"kms_key_id" in log_group'
+      - '"tags" in log_group'
+    vars:
+      log_group: '{{ result.log_groups[0] }}'
+  - name: test adding tags to cloudwatchlogs_log_group (check_mode)
+    cloudwatchlogs_log_group:
+      tags: '{{ first_tags }}'
+      purge_tags: true
+    check_mode: true
+    register: update_result
+
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is changed
+      - '"logs:UntagLogGroup" not in update_result'
+      - '"logs:TagLogGroup" not in update_result'
+
+  - name: test adding tags to cloudwatchlogs_log_group
+    cloudwatchlogs_log_group:
+      tags: '{{ first_tags }}'
+      purge_tags: true
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is changed
+      - update_result.log_groups[0].tags == first_tags
+
+  - name: test adding tags to cloudwatchlogs_log_group - idempotency (check mode)
+    cloudwatchlogs_log_group:
+      tags: '{{ first_tags }}'
+      purge_tags: true
+    check_mode: true
+    register: update_result
+
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is not changed
+      - '"logs:UntagLogGroup" not in update_result'
+      - '"logs:TagLogGroup" not in update_result'
+
+  - name: test adding tags to cloudwatchlogs_log_group - idempotency
+    cloudwatchlogs_log_group:
+      tags: '{{ first_tags }}'
+      purge_tags: true
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is not changed
+      - update_result.log_groups[0].tags == first_tags
+
+  ###
+
+  - name: test updating tags with purge on cloudwatchlogs_log_group (check mode)
+    cloudwatchlogs_log_group:
+      tags: '{{ second_tags }}'
+      purge_tags: true
+    check_mode: true
+    register: update_result
+
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is changed
+      - '"logs:UntagLogGroup" not in update_result'
+      - '"logs:TagLogGroup" not in update_result'
+
+  - name: test updating tags with purge on cloudwatchlogs_log_group
+    cloudwatchlogs_log_group:
+      tags: '{{ second_tags }}'
+      purge_tags: true
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is changed
+      - update_result.log_groups[0].tags == second_tags
+
+  - name: test updating tags with purge on cloudwatchlogs_log_group - idempotency
+      (check mode)
+    cloudwatchlogs_log_group:
+      tags: '{{ second_tags }}'
+      purge_tags: true
+    check_mode: true
+    register: update_result
+
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is not changed
+      - '"logs:UntagLogGroup" not in update_result'
+      - '"logs:TagLogGroup" not in update_result'
+
+  - name: test updating tags with purge on cloudwatchlogs_log_group - idempotency
+    cloudwatchlogs_log_group:
+      tags: '{{ second_tags }}'
+      purge_tags: true
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is not changed
+      - update_result.log_groups[0].tags == second_tags
+
+  ###
+
+  - name: test updating tags without purge on cloudwatchlogs_log_group (check mode)
+    cloudwatchlogs_log_group:
+      tags: '{{ third_tags }}'
+      purge_tags: false
+    check_mode: true
+    register: update_result
+
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is changed
+      - '"logs:UntagLogGroup" not in update_result'
+      - '"logs:TagLogGroup" not in update_result'
+
+  - name: test updating tags without purge on cloudwatchlogs_log_group
+    cloudwatchlogs_log_group:
+      tags: '{{ third_tags }}'
+      purge_tags: false
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is changed
+      - update_result.log_groups[0].tags == final_tags
+
+  - name: test updating tags without purge on cloudwatchlogs_log_group - idempotency
+      (check mode)
+    cloudwatchlogs_log_group:
+      tags: '{{ third_tags }}'
+      purge_tags: false
+    check_mode: true
+    register: update_result
+
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is not changed
+      - '"logs:UntagLogGroup" not in update_result'
+      - '"logs:TagLogGroup" not in update_result'
+
+  - name: test updating tags without purge on cloudwatchlogs_log_group - idempotency
+    cloudwatchlogs_log_group:
+      tags: '{{ third_tags }}'
+      purge_tags: false
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is not changed
+      - update_result.log_groups[0].tags == final_tags
+
+  ###
+
+  - name: test that cloudwatchlogs_log_group_info returns the tags
+    cloudwatchlogs_log_group_info:
+    register: tag_info
+  - name: assert tags present
+    assert:
+      that:
+      - tag_info.log_groups | length == 1
+      - tag_info.log_groups[0].tags == final_tags
+
+  ###
+
+  - name: test no tags param cloudwatchlogs_log_group (check mode)
+    cloudwatchlogs_log_group: {}
+    check_mode: true
+    register: update_result
+
+  - name: assert no change
+    assert:
+      that:
+      - update_result is not changed
+      - update_result.log_groups[0].tags == final_tags
+
+  - name: test no tags param cloudwatchlogs_log_group
+    cloudwatchlogs_log_group: {}
+    register: update_result
+  - name: assert no change
+    assert:
+      that:
+      - update_result is not changed
+      - update_result.log_groups[0].tags == final_tags
+
+  ###
+
+  - name: test removing tags from cloudwatchlogs_log_group (check mode)
+    cloudwatchlogs_log_group:
+      tags: {}
+      purge_tags: true
+    check_mode: true
+    register: update_result
+
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is changed
+
+  - name: test removing tags from cloudwatchlogs_log_group
+    cloudwatchlogs_log_group:
+      tags: {}
+      purge_tags: true
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is changed
+      - update_result.log_groups[0].tags == {}
+
+  - name: test removing tags from cloudwatchlogs_log_group - idempotency (check mode)
+    cloudwatchlogs_log_group:
+      tags: {}
+      purge_tags: true
+    check_mode: true
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is not changed
+
+  - name: test removing tags from cloudwatchlogs_log_group - idempotency
+    cloudwatchlogs_log_group:
+      tags: {}
+      purge_tags: true
+    register: update_result
+  - name: assert that update succeeded
+    assert:
+      that:
+      - update_result is not changed
+      - update_result.log_groups[0].tags == {}
+
+  - name: delete cloudwatch log group for integration test (check_mode)
+    cloudwatchlogs_log_group:
+      state: absent
+      log_group_name: '{{ log_group_name }}'
+    check_mode: true
+    register: result
+
+  - assert:
+      that:
+      - result is changed
+      - '"logs:DeleteLogGroup" not in result.resource_actions'
+
+  - name: delete cloudwatch log group for integration test
+    cloudwatchlogs_log_group:
+      state: absent
+      log_group_name: '{{ log_group_name }}'
+    register: result
+
+  - assert:
+      that:
+      - result is changed
+
+  - name: delete cloudwatch log group for integration test (check_mode - idempotent)
+    cloudwatchlogs_log_group:
+      state: absent
+      log_group_name: '{{ log_group_name }}'
+    check_mode: true
+    register: result
+
+  - assert:
+      that:
+      - result is not changed
+      - '"logs:DeleteLogGroup" not in result.resource_actions'
+
+  - name: delete cloudwatch log group for integration test (idempotent)
+    cloudwatchlogs_log_group:
+      state: absent
+      log_group_name: '{{ log_group_name }}'
+    register: result
+
+  - assert:
+      that:
+      - result is not changed
+
+  - name: describe missing log group
+    cloudwatchlogs_log_group_info:
+      log_group_name: '{{ log_group_name }}'
+    register: result
+
+  - assert:
+      that:
+      - '"log_groups" in result'
+      - result.log_groups | length == 0
+
+  always:
+
+  - name: delete cloudwatch log group for integration test
+    cloudwatchlogs_log_group:
+      state: absent
+      log_group_name: '{{ log_group_name }}'
+    ignore_errors: true
diff --git a/tests/integration/targets/cloudwatchlogs/tasks/main.yml b/tests/integration/targets/cloudwatchlogs/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/cloudwatchlogs/tasks/main.yml
@@ -0,0 +1,16 @@
+# Tests for cloudwatchlogs_log_group, cloudwatchlogs_log_group_info, and cloudwatchlogs_log_group_metric_filter modules
+
+- module_defaults:
+    group/aws:
+      aws_access_key: '{{ aws_access_key }}'
+      aws_secret_key: '{{ aws_secret_key }}'
+      security_token: '{{ security_token | default(omit) }}'
+      region: '{{ aws_region }}'
+
+  block:
+
+  - name: Run tests for changes to the cloudwatchlogs_log_group and cloudwatchlogs_log_group_metric_filter
+    include_tasks: cloudwatchlogs_tests.yml
+
+  - name: Run tests relating to create/delete and set tags on cloudwatchlogs_log_group
+    include_tasks: create-delete-tags.yml