# Buggy code
def main():
    argument_spec = ec2_argument_spec()
    argument_spec.update(
        dict(
            state=dict(
                default="present", choices=["present", "absent", "enabled", "disabled"]
            ),
            name=dict(default="default"),
            enable_logging=dict(default=True, type="bool"),
            s3_bucket_name=dict(),
            s3_key_prefix=dict(),
            sns_topic_name=dict(),
            is_multi_region_trail=dict(default=False, type="bool"),
            enable_log_file_validation=dict(
                default=False, type="bool", aliases=["log_file_validation_enabled"]
            ),
            include_global_events=dict(
                default=True, type="bool", aliases=["include_global_service_events"]
            ),
            cloudwatch_logs_role_arn=dict(),
            cloudwatch_logs_log_group_arn=dict(),
            kms_key_id=dict(),
            tags=dict(default={}, type="dict"),
        )
    )

    required_if = [
        ("state", "present", ["s3_bucket_name"]),
        ("state", "enabled", ["s3_bucket_name"]),
    ]
    required_together = [("cloudwatch_logs_role_arn", "cloudwatch_logs_log_group_arn")]

    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
        required_together=required_together,
        required_if=required_if,
    )

    if not HAS_BOTO3:
        module.fail_json(msg="boto3 is required for this module")

    # collect parameters
    if module.params["state"] in ("present", "enabled"):
        state = "present"
    elif module.params["state"] in ("absent", "disabled"):
        state = "absent"
    tags = module.params["tags"]
    enable_logging = module.params["enable_logging"]
    ct_params = dict(
        Name=module.params["name"],
        S3BucketName=module.params["s3_bucket_name"],
        IncludeGlobalServiceEvents=module.params["include_global_events"],
        IsMultiRegionTrail=module.params["is_multi_region_trail"],
        EnableLogFileValidation=module.params["enable_log_file_validation"],
        S3KeyPrefix="",
        SnsTopicName="",
        CloudWatchLogsRoleArn="",
        CloudWatchLogsLogGroupArn="",
        KmsKeyId="",
    )

    if module.params["s3_key_prefix"]:
        ct_params["S3KeyPrefix"] = module.params["s3_key_prefix"].rstrip("/")

    if module.params["sns_topic_name"]:
        ct_params["SnsTopicName"] = module.params["sns_topic_name"]

    if module.params["cloudwatch_logs_role_arn"]:
        ct_params["CloudWatchLogsRoleArn"] = module.params["cloudwatch_logs_role_arn"]

    if module.params["cloudwatch_logs_log_group_arn"]:
        ct_params["CloudWatchLogsLogGroupArn"] = module.params[
            "cloudwatch_logs_log_group_arn"
        ]

    if module.params["kms_key_id"]:
        ct_params["KmsKeyId"] = module.params["kms_key_id"]

    try:
        region, ec2_url, aws_connect_params = get_aws_connection_info(
            module, boto3=True
        )
        client = boto3_conn(
            module,
            conn_type="client",
            resource="cloudtrail",
            region=region,
            endpoint=ec2_url,
            **aws_connect_params,
        )
    except ClientError as err:
        module.fail_json(
            msg=err.message,
            exception=traceback.format_exc(),
            **camel_dict_to_snake_dict(err.response),
        )

    results = dict(changed=False, exists=False)

    # Get existing trail facts
    trail = get_trail_facts(module, client, ct_params["Name"])

    # If the trail exists set the result exists variable
    if trail is not None:
        results["exists"] = True

    if state == "absent" and results["exists"]:
        # If Trail exists go ahead and delete
        results["changed"] = True
        results["exists"] = False
        results["trail"] = dict()
        if not module.check_mode:
            delete_trail(module, client, trail["TrailARN"])

    elif state == "present" and results["exists"]:
        # If Trail exists see if we need to update it
        do_update = False
        for key in ct_params:
            tkey = str(key)
            # boto3 has inconsistent parameter naming so we handle it here
            if key == "EnableLogFileValidation":
                tkey = "LogFileValidationEnabled"
            # We need to make an empty string equal None
            if ct_params.get(key) == "":
                val = None
            else:
                val = ct_params.get(key)
            if val != trail.get(tkey):
                do_update = True
                results["changed"] = True
                # If we are in check mode copy the changed values to the trail facts in result output to show what would change.
                if module.check_mode:
                    trail.update({tkey: ct_params.get(key)})

        if not module.check_mode and do_update:
            update_trail(module, client, ct_params)
            trail = get_trail_facts(module, client, ct_params["Name"])

        # Check if we need to start/stop logging
        if enable_logging and not trail["IsLogging"]:
            results["changed"] = True
            trail["IsLogging"] = True
            if not module.check_mode:
                set_logging(module, client, name=ct_params["Name"], action="start")
        if not enable_logging and trail["IsLogging"]:
            results["changed"] = True
            trail["IsLogging"] = False
            if not module.check_mode:
                set_logging(module, client, name=ct_params["Name"], action="stop")

        # Check if we need to update tags on resource
        tag_dry_run = False
        if module.check_mode:
            tag_dry_run = True
        tags_changed = tag_trail(
            module,
            client,
            tags=tags,
            trail_arn=trail["TrailARN"],
            curr_tags=trail["tags"],
            dry_run=tag_dry_run,
        )
        if tags_changed:
            results["changed"] = True
            trail["tags"] = tags
        # Populate trail facts in output
        results["trail"] = camel_dict_to_snake_dict(trail)

    elif state == "present" and not results["exists"]:
        # Trail doesn't exist just go create it
        results["changed"] = True
        if not module.check_mode:
            # If we aren't in check_mode then actually create it
            created_trail = create_trail(module, client, ct_params)
            # Apply tags
            tag_trail(module, client, tags=tags, trail_arn=created_trail["TrailARN"])
            # Get the trail status
            try:
                status_resp = client.get_trail_status(Name=created_trail["Name"])
            except ClientError as err:
                module.fail_json(
                    msg=err.message,
                    exception=traceback.format_exc(),
                    **camel_dict_to_snake_dict(err.response),
                )
            # Set the logging state for the trail to desired value
            if enable_logging and not status_resp["IsLogging"]:
                set_logging(module, client, name=ct_params["Name"], action="start")
            if not enable_logging and status_resp["IsLogging"]:
                set_logging(module, client, name=ct_params["Name"], action="stop")
            # Get facts for newly created Trail
            trail = get_trail_facts(module, client, ct_params["Name"])

        # If we are in check mode create a fake return structure for the newly minted trail
        if module.check_mode:
            acct_id = "123456789012"
            try:
                sts_client = boto3_conn(
                    module,
                    conn_type="client",
                    resource="sts",
                    region=region,
                    endpoint=ec2_url,
                    **aws_connect_params,
                )
                acct_id = sts_client.get_caller_identity()["Account"]
            except ClientError:
                pass
            trail = dict()
            trail.update(ct_params)
            trail["LogFileValidationEnabled"] = ct_params["EnableLogFileValidation"]
            trail.pop("EnableLogFileValidation")
            fake_arn = (
                "arn:aws:cloudtrail:"
                + region
                + ":"
                + acct_id
                + ":trail/"
                + ct_params["Name"]
            )
            trail["HasCustomEventSelectors"] = False
            trail["HomeRegion"] = region
            trail["TrailARN"] = fake_arn
            trail["IsLogging"] = enable_logging
            trail["tags"] = tags
        # Populate trail facts in output
        results["trail"] = camel_dict_to_snake_dict(trail)

    module.exit_json(**results)


# Traceback (UnsupportedOperationException)
RUNNING HANDLER [aws_cloudtrail : manage cloudtrail trails] ****************************************************************************************************************************************************************************************************************************************
Using module file /Users/nbailey/automation/roles-aws/aws_modules/library/cloudtrail.py
<localhost> ESTABLISH LOCAL CONNECTION FOR USER: nbailey
<localhost> EXEC /bin/sh -c 'echo ~ &amp;&amp; sleep 0'
<localhost> EXEC /bin/sh -c '( umask 77 &amp;&amp; mkdir -p "` echo /Users/nbailey/.ansible/tmp/ansible-tmp-1515598328.47-33693836653100 `" &amp;&amp; echo ansible-tmp-1515598328.47-33693836653100="` echo /Users/nbailey/.ansible/tmp/ansible-tmp-1515598328.47-33693836653100 `" ) &amp;&amp; sleep 0'
<localhost> PUT /var/folders/3k/n0flxzzx3tn15hd44_0l4qt40038gw/T/tmpvORY0v TO /Users/nbailey/.ansible/tmp/ansible-tmp-1515598328.47-33693836653100/cloudtrail.py
<localhost> EXEC /bin/sh -c 'chmod u+x /Users/nbailey/.ansible/tmp/ansible-tmp-1515598328.47-33693836653100/ /Users/nbailey/.ansible/tmp/ansible-tmp-1515598328.47-33693836653100/cloudtrail.py &amp;&amp; sleep 0'
<localhost> EXEC /bin/sh -c '/usr/bin/python /Users/nbailey/.ansible/tmp/ansible-tmp-1515598328.47-33693836653100/cloudtrail.py; rm -rf "/Users/nbailey/.ansible/tmp/ansible-tmp-1515598328.47-33693836653100/" > /dev/null 2>&amp;1 &amp;&amp; sleep 0'
The full traceback is:
Traceback (most recent call last):
File "/var/folders/3k/n0flxzzx3tn15hd44_0l4qt40038gw/T/ansible_MCTZx7/ansible_module_cloudtrail.py", line 270, in create_trail
resp = client.create_trail(**ct_params)
File "/Users/nbailey/Library/Python/2.7/lib/python/site-packages/botocore/client.py", line 317, in _api_call
return self._make_api_call(operation_name, kwargs)
File "/Users/nbailey/Library/Python/2.7/lib/python/site-packages/botocore/client.py", line 615, in _make_api_call
raise error_class(parsed_response, operation_name)
UnsupportedOperationException: An error occurred (UnsupportedOperationException) when calling the CreateTrail operation: The operation requested is not supported in the region.

fatal: [test-trail-1 -> localhost]: FAILED! => {
"attempts": 1,
"changed": false,
"error": {
"code": "UnsupportedOperationException",
"message": "The operation requested is not supported in the region."
},
"invocation": {
"module_args": {
"aws_access_key": null,
"aws_secret_key": null,
"cloudwatch_logs_log_group_arn": null,
"cloudwatch_logs_role_arn": null,
"ec2_url": null,
"enable_log_file_validation": true,
"enable_logging": false,
"include_global_events": true,
"is_multi_region_trail": true,
"kms_key_id": null,
"name": "test-trail-1",
"profile": "<redacted>",
"region": "cn-north-1",
"s3_bucket_name": "aws-cloudtrail-test-bucket-a",
"s3_key_prefix": null,
"security_token": null,
"sns_topic_name": null,
"state": "present",
"tags": {},
"validate_certs": true
}
},
"msg": "An error occurred (UnsupportedOperationException) when calling the CreateTrail operation: The operation requested is not supported in the region.",
"response_metadata": {
"http_headers": {
"connection": "close",
"content-length": "110",
"content-type": "application/x-amz-json-1.1",
"date": "Wed, 10 Jan 2018 15:32:10 GMT",
"x-amzn-requestid": "<redacted>"
},
"http_status_code": 400,
"request_id": "<redacted>",
"retry_attempts": 0
}
}

# Fixed code
def main():
    argument_spec = ec2_argument_spec()
    argument_spec.update(
        dict(
            state=dict(
                default="present", choices=["present", "absent", "enabled", "disabled"]
            ),
            name=dict(default="default"),
            enable_logging=dict(default=True, type="bool"),
            s3_bucket_name=dict(),
            s3_key_prefix=dict(),
            sns_topic_name=dict(),
            is_multi_region_trail=dict(default=False, type="bool"),
            enable_log_file_validation=dict(
                type="bool", aliases=["log_file_validation_enabled"]
            ),
            include_global_events=dict(
                default=True, type="bool", aliases=["include_global_service_events"]
            ),
            cloudwatch_logs_role_arn=dict(),
            cloudwatch_logs_log_group_arn=dict(),
            kms_key_id=dict(),
            tags=dict(default={}, type="dict"),
        )
    )

    required_if = [
        ("state", "present", ["s3_bucket_name"]),
        ("state", "enabled", ["s3_bucket_name"]),
    ]
    required_together = [("cloudwatch_logs_role_arn", "cloudwatch_logs_log_group_arn")]

    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
        required_together=required_together,
        required_if=required_if,
    )

    if not HAS_BOTO3:
        module.fail_json(msg="boto3 is required for this module")

    # collect parameters
    if module.params["state"] in ("present", "enabled"):
        state = "present"
    elif module.params["state"] in ("absent", "disabled"):
        state = "absent"
    tags = module.params["tags"]
    enable_logging = module.params["enable_logging"]
    ct_params = dict(
        Name=module.params["name"],
        S3BucketName=module.params["s3_bucket_name"],
        IncludeGlobalServiceEvents=module.params["include_global_events"],
        IsMultiRegionTrail=module.params["is_multi_region_trail"],
    )

    if module.params["s3_key_prefix"]:
        ct_params["S3KeyPrefix"] = module.params["s3_key_prefix"].rstrip("/")

    if module.params["sns_topic_name"]:
        ct_params["SnsTopicName"] = module.params["sns_topic_name"]

    if module.params["cloudwatch_logs_role_arn"]:
        ct_params["CloudWatchLogsRoleArn"] = module.params["cloudwatch_logs_role_arn"]

    if module.params["cloudwatch_logs_log_group_arn"]:
        ct_params["CloudWatchLogsLogGroupArn"] = module.params[
            "cloudwatch_logs_log_group_arn"
        ]

    if module.params["enable_log_file_validation"] is not None:
        ct_params["EnableLogFileValidation"] = module.params[
            "enable_log_file_validation"
        ]

    if module.params["kms_key_id"]:
        ct_params["KmsKeyId"] = module.params["kms_key_id"]

    try:
        region, ec2_url, aws_connect_params = get_aws_connection_info(
            module, boto3=True
        )
        client = boto3_conn(
            module,
            conn_type="client",
            resource="cloudtrail",
            region=region,
            endpoint=ec2_url,
            **aws_connect_params,
        )
    except ClientError as err:
        module.fail_json(
            msg=err.message,
            exception=traceback.format_exc(),
            **camel_dict_to_snake_dict(err.response),
        )

    results = dict(changed=False, exists=False)

    # Get existing trail facts
    trail = get_trail_facts(module, client, ct_params["Name"])

    # If the trail exists set the result exists variable
    if trail is not None:
        results["exists"] = True

    if state == "absent" and results["exists"]:
        # If Trail exists go ahead and delete
        results["changed"] = True
        results["exists"] = False
        results["trail"] = dict()
        if not module.check_mode:
            delete_trail(module, client, trail["TrailARN"])

    elif state == "present" and results["exists"]:
        # If Trail exists see if we need to update it
        do_update = False
        for key in ct_params:
            tkey = str(key)
            # boto3 has inconsistent parameter naming so we handle it here
            if key == "EnableLogFileValidation":
                tkey = "LogFileValidationEnabled"
            # We need to make an empty string equal None
            if ct_params.get(key) == "":
                val = None
            else:
                val = ct_params.get(key)
            if val != trail.get(tkey):
                do_update = True
                results["changed"] = True
                # If we are in check mode copy the changed values to the trail facts in result output to show what would change.
                if module.check_mode:
                    trail.update({tkey: ct_params.get(key)})

        if not module.check_mode and do_update:
            update_trail(module, client, ct_params)
            trail = get_trail_facts(module, client, ct_params["Name"])

        # Check if we need to start/stop logging
        if enable_logging and not trail["IsLogging"]:
            results["changed"] = True
            trail["IsLogging"] = True
            if not module.check_mode:
                set_logging(module, client, name=ct_params["Name"], action="start")
        if not enable_logging and trail["IsLogging"]:
            results["changed"] = True
            trail["IsLogging"] = False
            if not module.check_mode:
                set_logging(module, client, name=ct_params["Name"], action="stop")

        # Check if we need to update tags on resource
        tag_dry_run = False
        if module.check_mode:
            tag_dry_run = True
        tags_changed = tag_trail(
            module,
            client,
            tags=tags,
            trail_arn=trail["TrailARN"],
            curr_tags=trail["tags"],
            dry_run=tag_dry_run,
        )
        if tags_changed:
            results["changed"] = True
            trail["tags"] = tags
        # Populate trail facts in output
        results["trail"] = camel_dict_to_snake_dict(trail)

    elif state == "present" and not results["exists"]:
        # Trail doesn't exist just go create it
        results["changed"] = True
        if not module.check_mode:
            # If we aren't in check_mode then actually create it
            created_trail = create_trail(module, client, ct_params)
            # Apply tags
            tag_trail(module, client, tags=tags, trail_arn=created_trail["TrailARN"])
            # Get the trail status
            try:
                status_resp = client.get_trail_status(Name=created_trail["Name"])
            except ClientError as err:
                module.fail_json(
                    msg=err.message,
                    exception=traceback.format_exc(),
                    **camel_dict_to_snake_dict(err.response),
                )
            # Set the logging state for the trail to desired value
            if enable_logging and not status_resp["IsLogging"]:
                set_logging(module, client, name=ct_params["Name"], action="start")
            if not enable_logging and status_resp["IsLogging"]:
                set_logging(module, client, name=ct_params["Name"], action="stop")
            # Get facts for newly created Trail
            trail = get_trail_facts(module, client, ct_params["Name"])

        # If we are in check mode create a fake return structure for the newly minted trail
        if module.check_mode:
            acct_id = "123456789012"
            try:
                sts_client = boto3_conn(
                    module,
                    conn_type="client",
                    resource="sts",
                    region=region,
                    endpoint=ec2_url,
                    **aws_connect_params,
                )
                acct_id = sts_client.get_caller_identity()["Account"]
            except ClientError:
                pass
            trail = dict()
            trail.update(ct_params)
            if "EnableLogFileValidation" not in ct_params:
                ct_params["EnableLogFileValidation"] = False
            trail["EnableLogFileValidation"] = ct_params["EnableLogFileValidation"]
            trail.pop("EnableLogFileValidation")
            fake_arn = (
                "arn:aws:cloudtrail:"
                + region
                + ":"
                + acct_id
                + ":trail/"
                + ct_params["Name"]
            )
            trail["HasCustomEventSelectors"] = False
            trail["HomeRegion"] = region
            trail["TrailARN"] = fake_arn
            trail["IsLogging"] = enable_logging
            trail["tags"] = tags
        # Populate trail facts in output
        results["trail"] = camel_dict_to_snake_dict(trail)

    module.exit_json(**results)