Repository: ansible/ansible

Issue:
cloudformation module: check mode does not work when stack policy deny is present
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
cloudformation module: https://github.com/ansible/ansible/pull/23483

##### ANSIBLE VERSION
```
ansible 2.4.0.0
  config file = None
  configured module search path = ['/home/cytopia/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.5/dist-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.5.3 (default, Jan 19 2017, 14:11:04) [GCC 6.3.0 20170118]

```

##### CONFIGURATION

```
ANSIBLE_SSH_ARGS = -o ControlMaster=auto -o ControlPersist=10m
ANSIBLE_SSH_PIPELINING = True
DEFAULT_FORKS = 50
DEFAULT_GATHERING = smart
DEFAULT_REMOTE_USER = root
DEFAULT_ROLES_PATH = ['/home/cytopia/repo/ansible/roles']
DEFAULT_TIMEOUT = 10
DEFAULT_VAULT_PASSWORD_FILE = /home/cytopia/repo/ansible/vault/open_vault.sh
HOST_KEY_CHECKING = False
RETRY_FILES_ENABLED = False
```

##### OS / ENVIRONMENT

Debian Stretch 9.1
```
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.1 (stretch)
Release:        9.1
Codename:       stretch

$ uname -a
Linux dell-xps-13-9360 4.12.0-1-amd64 #1 SMP Debian 4.12.6-1 (2017-08-12) x86_64 GNU/Linux
```

##### SUMMARY
The cloudformation module check mode does not work if there is a stack policy in place that denies stack updates.

##### STEPS TO REPRODUCE

**Ansible task**
```yaml
- name: "Cloudformation: rds ({{ aws_shop_env }})"
  cloudformation:
    stack_name: "{{ aws_shop_env }}-rds"
    state: "present"
    region: "eu-central-1"
    template: "../files/cloudformation/templates/rds/rds.yml"
    stack_policy: "../files/cloudformation/policies/rds.json"
    profile: "{{ boto_profile }}"
    template_parameters:
      env: "{{ aws_shop_env }}"
      subnetCidr: "{{ aws_rds_subnet }}"
      instanceType: "{{ aws_rds_instance_type }}"
      allocatedStorage: "{{ aws_rds_storage_size }}"
      storageType: "{{ aws_rds_storage_type }}"
      storageIops: "{{ aws_rds_storage_iops }}"
      dbName: "{{ aws_rds_db }}"
      masterUsername: "{{ aws_rds_user }}"
      masterPassword: "{{ aws_rds_pass }}"
      storageEncrypted: "true"
      multiAZ: "true"
      backupRetentionPeriod: "7"
    tags:
      env: "{{ aws_shop_env }}"
  delegate_to: localhost
```

**Stack Policy**
```json
{
  "Statement" : [
    {
      "Effect" : "Deny",
      "Action" : "Update:*",
      "Principal": "*",
      "Resource" : "*",
      "Condition" : {
        "StringEquals" : {
          "ResourceType" : ["AWS::RDS::DBInstance"]
        }
      }
    },
    {
      "Effect" : "Allow",
      "Action" : "Update:*",
      "Principal": "*",
      "Resource" : "*"
    }
  ]
}
```

##### EXPECTED RESULTS
Show diff produced from changesets

##### ACTUAL RESULTS
```
The full traceback is:
Traceback (most recent call last):
  File "/tmp/ansible_oo3a_kd3/ansible_module_cloudformation.py", line 604, in <module>
    main()
  File "/tmp/ansible_oo3a_kd3/ansible_module_cloudformation.py", line 550, in main
    module.exit_json(**check_mode_changeset(module, stack_params, cfn))
  File "/tmp/ansible_oo3a_kd3/ansible_module_cloudformation.py", line 417, in check_mode_changeset
    change_set = cfn.create_change_set(**stack_params)
  File "/tmp/ansible_oo3a_kd3/ansible_modlib.zip/ansible/module_utils/cloud.py", line 153, in retry_func
  File "/tmp/ansible_oo3a_kd3/ansible_modlib.zip/ansible/module_utils/cloud.py", line 140, in retry_func
  File "/home/cytopia/.local/lib/python3.5/site-packages/botocore/client.py", line 310, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/cytopia/.local/lib/python3.5/site-packages/botocore/client.py", line 573, in _make_api_call
    api_params, operation_model, context=request_context)
  File "/home/cytopia/.local/lib/python3.5/site-packages/botocore/client.py", line 628, in _convert_to_request_dict
    api_params, operation_model)
  File "/home/cytopia/.local/lib/python3.5/site-packages/botocore/validate.py", line 291, in serialize_to_request
    raise ParamValidationError(report=report.generate_report())
botocore.exceptions.ParamValidationError: Parameter validation failed:
Unknown parameter in input: "StackPolicyBody", must be one of: StackName, TemplateBody, TemplateURL, UsePreviousTemplate, Parameters, Capabilities, ResourceTypes, RoleARN, NotificationARNs, Tags, ChangeSetName, ClientToken, Description, ChangeSetType

fatal: [localhost -> localhost]: FAILED! => {
    "changed": false,
    "failed": true,
    "module_stderr": "Traceback (most recent call last):\n  File \"/tmp/ansible_oo3a_kd3/ansible_module_cloudformation.py\", line 604, in <module>\n    main()\n  File \"/tmp/ansible_oo3a_kd3/ansible_module_cloudformation.py\", line 550, in main\n    module.exit_json(**check_mode_changeset(module, stack_params, cfn))\n  File \"/tmp/ansible_oo3a_kd3/ansible_module_cloudformation.py\", line 417, in check_mode_changeset\n    change_set = cfn.create_change_set(**stack_params)\n  File \"/tmp/ansible_oo3a_kd3/ansible_modlib.zip/ansible/module_utils/cloud.py\", line 153, in retry_func\n  File \"/tmp/ansible_oo3a_kd3/ansible_modlib.zip/ansible/module_utils/cloud.py\", line 140, in retry_func\n  File \"/home/cytopia/.local/lib/python3.5/site-packages/botocore/client.py\", line 310, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n  File \"/home/cytopia/.local/lib/python3.5/site-packages/botocore/client.py\", line 573, in _make_api_call\n    api_params, operation_model, context=request_context)\n  File \"/home/cytopia/.local/lib/python3.5/site-packages/botocore/client.py\", line 628, in _convert_to_request_dict\n    api_params, operation_model)\n  File \"/home/cytopia/.local/lib/python3.5/site-packages/botocore/validate.py\", line 291, in serialize_to_request\n    raise ParamValidationError(report=report.generate_report())\nbotocore.exceptions.ParamValidationError: Parameter validation failed:\nUnknown parameter in input: \"StackPolicyBody\", must be one of: StackName, TemplateBody, TemplateURL, UsePreviousTemplate, Parameters, Capabilities, ResourceTypes, RoleARN, NotificationARNs, Tags, ChangeSetName, ClientToken, Description, ChangeSetType\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE",
    "rc": 0
}

```



Discussion:

Resolving patch:
diff --git a/lib/ansible/modules/cloud/amazon/cloudformation.py b/lib/ansible/modules/cloud/amazon/cloudformation.py
--- a/lib/ansible/modules/cloud/amazon/cloudformation.py
+++ b/lib/ansible/modules/cloud/amazon/cloudformation.py
@@ -121,7 +121,7 @@
 extends_documentation_fragment:
 - aws
 - ec2
-requirements: [ botocore>=1.4.57 ]
+requirements: [ boto3, botocore>=1.4.57 ]
 '''
 
 EXAMPLES = '''
@@ -302,7 +302,8 @@ def list_changesets(cfn, stack_name):
 def create_changeset(module, stack_params, cfn):
     if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
         module.fail_json(msg="Either 'template' or 'template_url' is required.")
-
+    if module.params['changeset_name'] is not None:
+        stack_params['ChangeSetName'] = module.params['changeset_name']
     try:
         changeset_name = build_changeset_name(stack_params)
         stack_params['ChangeSetName'] = changeset_name
@@ -502,11 +503,10 @@ def main():
     else:
         stack_params['NotificationARNs'] = []
 
-    if module.params['stack_policy'] is not None:
+    # can't check the policy when verifying.
+    if module.params['stack_policy'] is not None and not module.check_mode:
         stack_params['StackPolicyBody'] = open(module.params['stack_policy'], 'r').read()
 
-    if module.params['changeset_name'] is not None:
-        stack_params['ChangeSetName'] = module.params['changeset_name']
 
     template_parameters = module.params['template_parameters']
     stack_params['Parameters'] = [{'ParameterKey':k, 'ParameterValue':str(v)} for k, v in template_parameters.items()]


Test patch: