cloudformation_facts: don't fail on nonexistent stack - fixes #23419
diff --git a/lib/ansible/modules/cloud/amazon/cloudformation_facts.py b/lib/ansible/modules/cloud/amazon/cloudformation_facts.py
index 46712accdf71fe..dc0f808dc6dfe1 100644
--- a/lib/ansible/modules/cloud/amazon/cloudformation_facts.py
+++ b/lib/ansible/modules/cloud/amazon/cloudformation_facts.py
@@ -80,6 +80,13 @@
     stack_resources: true
     stack_policy: true
 
+# Fail if the stack doesn't exist
+- name: try to get facts about a stack but fail if it doesn't exist
+  cloudformation_facts:
+    stack_name: nonexistent-stack
+    all_facts: yes
+  failed_when: cloudformation['nonexistent-stack'] is undefined
+
 # Example dictionary outputs for stack_outputs, stack_parameters and stack_resources:
 # "stack_outputs": {
 #     "ApplicationDatabaseName": "dazvlpr01xj55a.ap-southeast-2.rds.amazonaws.com",
@@ -102,38 +109,38 @@
 RETURN = '''
 stack_description:
     description: Summary facts about the stack
-    returned: always
+    returned: if the stack exists
     type: dict
 stack_outputs:
     description: Dictionary of stack outputs keyed by the value of each output 'OutputKey' parameter and corresponding value of each
                  output 'OutputValue' parameter
-    returned: always
+    returned: if the stack exists
     type: dict
 stack_parameters:
     description: Dictionary of stack parameters keyed by the value of each parameter 'ParameterKey' parameter and corresponding value of
                  each parameter 'ParameterValue' parameter
-    returned: always
+    returned: if the stack exists
     type: dict
 stack_events:
     description: All stack events for the stack
-    returned: only if all_facts or stack_events is true
+    returned: only if all_facts or stack_events is true and the stack exists
     type: list
 stack_policy:
     description: Describes the stack policy for the stack
-    returned: only if all_facts or stack_policy is true
+    returned: only if all_facts or stack_policy is true and the stack exists
     type: dict
 stack_template:
     description: Describes the stack template for the stack
-    returned: only if all_facts or stack_template is true
+    returned: only if all_facts or stack_template is true and the stack exists
     type: dict
 stack_resource_list:
     description: Describes stack resources for the stack
-    returned: only if all_facts or stack_resourses is true
+    returned: only if all_facts or stack_resourses is true and the stack exists
     type: list
 stack_resources:
     description: Dictionary of stack resources keyed by the value of each resource 'LogicalResourceId' parameter and corresponding value of each
                  resource 'PhysicalResourceId' parameter
-    returned: only if all_facts or stack_resourses is true
+    returned: only if all_facts or stack_resourses is true and the stack exists
     type: dict
 '''
 
@@ -148,6 +155,7 @@
 except ImportError:
     HAS_BOTO3 = False
 
+from ansible.module_utils._text import to_native
 from ansible.module_utils.basic import AnsibleModule
 from ansible.module_utils.ec2 import (get_aws_connection_info, ec2_argument_spec, boto3_conn,
                                       camel_dict_to_snake_dict, AWSRetry, boto3_tag_list_to_ansible_dict)
@@ -180,22 +188,25 @@ def describe_stacks(self, stack_name=None):
             kwargs = {'StackName': stack_name} if stack_name else {}
             func = partial(self.client.describe_stacks, **kwargs)
             response = self.paginated_response(func, 'Stacks')
-            if response:
+            if response is not None:
                 return response
             self.module.fail_json(msg="Error describing stack(s) - an empty response was returned")
         except Exception as e:
-            self.module.fail_json(msg="Error describing stack(s) - " + str(e), exception=traceback.format_exc())
+            if 'does not exist' in e.response['Error']['Message']:
+                # missing stack, don't bail.
+                return {}
+            self.module.fail_json(msg="Error describing stack - " + to_native(e), exception=traceback.format_exc())
 
     def list_stack_resources(self, stack_name):
         try:
-            func = partial(self.client.list_stack_resources,StackName=stack_name)
+            func = partial(self.client.list_stack_resources, StackName=stack_name)
             return self.paginated_response(func, 'StackResourceSummaries')
         except Exception as e:
             self.module.fail_json(msg="Error listing stack resources - " + str(e), exception=traceback.format_exc())
 
     def describe_stack_events(self, stack_name):
         try:
-            func = partial(self.client.describe_stack_events,StackName=stack_name)
+            func = partial(self.client.describe_stack_events, StackName=stack_name)
             return self.paginated_response(func, 'StackEvents')
         except Exception as e:
             self.module.fail_json(msg="Error describing stack events - " + str(e), exception=traceback.format_exc())
@@ -222,7 +233,7 @@ def paginated_response(self, func, result_key, next_token=None):
         Returns expanded response for paginated operations.
         The 'result_key' is used to define the concatenated results that are combined from each paginated response.
         '''
-        args=dict()
+        args = dict()
         if next_token:
             args['NextToken'] = next_token
         response = func(**args)
@@ -232,6 +243,7 @@ def paginated_response(self, func, result_key, next_token=None):
             return result
         return result + self.paginated_response(func, result_key, next_token)
 
+
 def to_dict(items, key, value):
     ''' Transforms a list of items to a Key/Value dictionary '''
     if items:
@@ -239,6 +251,7 @@ def to_dict(items, key, value):
     else:
         return dict()
 
+
 def main():
     argument_spec = ec2_argument_spec()
     argument_spec.update(dict(
diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt
index 62d04da6cc0c64..758d95a0c495fb 100644
--- a/test/sanity/pep8/legacy-files.txt
+++ b/test/sanity/pep8/legacy-files.txt
@@ -12,7 +12,6 @@ lib/ansible/modules/cloud/openstack/_os_server_actions.py
 lib/ansible/modules/cloud/ovirt/_ovirt_affinity_groups.py
 lib/ansible/modules/cloud/amazon/aws_kms.py
 lib/ansible/modules/cloud/amazon/cloudformation.py
-lib/ansible/modules/cloud/amazon/cloudformation_facts.py
 lib/ansible/modules/cloud/amazon/cloudfront_facts.py
 lib/ansible/modules/cloud/amazon/dynamodb_table.py
 lib/ansible/modules/cloud/amazon/ec2_ami_copy.py

##### SUMMARY
Return empty results and message if there is no stack from which to get facts. Fixes #23419. Made pep8.

##### ISSUE TYPE
 - Bugfix Pull Request

##### COMPONENT NAME
lib/ansible/modules/cloud/amazon/cloudformation_facts.py

##### ANSIBLE VERSION
```
2.4.0
```

Before:
```
The full traceback is:
Traceback (most recent call last):
  File "/var/folders/by/k8_fbl593dlctgqmwq5wzl2c0000gn/T/ansible_NPiajF/ansible_module_cloudformation_facts.py", line 179, in describe_stack
    response = self.paginated_response(func, 'Stacks')
  File "/var/folders/by/k8_fbl593dlctgqmwq5wzl2c0000gn/T/ansible_NPiajF/ansible_module_cloudformation_facts.py", line 225, in paginated_response
    response = func(**args)
  File "/Users/shertel/Workspace/01-18-17/ansible/venv/python2env/lib/python2.7/site-packages/botocore/client.py", line 253, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/Users/shertel/Workspace/01-18-17/ansible/venv/python2env/lib/python2.7/site-packages/botocore/client.py", line 543, in _make_api_call
    raise error_class(parsed_response, operation_name)
ClientError: An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id notthere does not exist

fatal: [localhost]: FAILED! => {
    "changed": false,
    "failed": true,
    "invocation": {
        "module_args": {
            "all_facts": true,
            "aws_access_key": null,
            "aws_secret_key": null,
            "ec2_url": null,
            "profile": "shertel",
            "region": "us-east-1",
            "security_token": null,
            "stack_events": false,
            "stack_name": "notthere",
            "stack_policy": false,
            "stack_resources": false,
            "stack_template": false,
            "validate_certs": true
        }
    },
    "msg": "Error describing stack - An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id notthere does not exist"
}
	to retry, use: --limit @/Users/shertel/Workspace/01-18-17/ansible/my_playbooks/cloudformation/nonexistent_stack.retry

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1
```
