Repository: boto/boto

Issue:
CloudFormation ValidateTemplate response is not parsed correctly.
I'm running Python 2.7.5 and boto 2.24.0, and I noticed an issue with the result returned by CloudFormationConnection.validate_template().  It appears the stack capabilities is not properly parsed.

Here's a code snippet that tickles the issue - the CloudFormation template (iam-user.json) is pasted below.

``` Python
import boto
import logging
boto_logger = logging.getLogger('boto')
boto_logger.setLevel(logging.DEBUG)
tpl_file = open('iam-user.json')
cfn_template_body = tpl_file.read()
tpl_file.close()
cfn_conn = boto.connect_cloudformation()
validation_response = cfn_conn.validate_template(template_body=cfn_template_body)
```

When inspecting Capabilities in the response, I see a string with a newline and spaces, rather than a list that includes something like CAPABILITY_IAM:

``` Python
In [11]: validation_response.Capabilities
Out[11]: u'\n    '
```

Here's the full output of the code example:

``` Python
In [2]: import boto

In [3]: import logging

In [4]: boto_logger = logging.getLogger('boto')

In [5]: boto_logger.setLevel(logging.DEBUG)

In [6]: tpl_file = open('iam-user.json')

In [7]: cfn_template_body = tpl_file.read()

In [8]: tpl_file.close()

In [9]: cfn_conn = boto.connect_cloudformation()
DEBUG:boto:Using access key found in environment variable.
DEBUG:boto:Using secret key found in config file.

In [10]: validation_response = cfn_conn.validate_template(template_body=cfn_template_body)
DEBUG:boto:Method: POST
DEBUG:boto:Path: /
DEBUG:boto:Data: 
DEBUG:boto:Headers: {}
DEBUG:boto:Host: cloudformation.us-east-1.amazonaws.com
DEBUG:boto:Port: 443
DEBUG:boto:Params: {'Action': 'ValidateTemplate', 'Version': '2010-05-15', 'TemplateBody': '{\n    "AWSTemplateFormatVersion" : "2010-09-09",\n    "Description" : "Test",\n\n    "Resources" : {\n\n        "IamUser" : {\n            "Type" : "AWS::IAM::User",\n            "Properties" : {\n                "Path" : "/",\n                "Policies" : [{\n                    "PolicyName" : "CfnInit",\n                    "PolicyDocument": {\n                        "Statement":[{\n                            "Effect"   : "Allow",\n                            "Action"   : [ "cloudformation:DescribeStackResource" ],\n                            "Resource" : "*"\n                        }]\n                    }\n                }]\n            }\n        },\n\n        "CfnKeys" : {\n            "Type" : "AWS::IAM::AccessKey",\n            "Properties" : {\n                "UserName" : { "Ref" : "IamUser" }\n            }\n        }\n    },\n\n    "Outputs" : {\n        "User" : {\n            "Description" : "IAM User",\n            "Value" : { "Ref" : "IamUser" }\n        },\n        "AccessKeyId" : {\n            "Description" : "AccessKeyId",\n            "Value" : { "Ref" : "CfnKeys" }\n        }\n    }\n}\n'}
DEBUG:boto:establishing HTTPS connection: host=cloudformation.us-east-1.amazonaws.com, kwargs={'port': 443, 'timeout': 70}
DEBUG:boto:Token: None
DEBUG:boto:CanonicalRequest:
POST
/

host:cloudformation.us-east-1.amazonaws.com
x-amz-date:20140204T211547Z

host;x-amz-date
ed2b6b57e57a95cefe1ed9a0a8a1d6cbb92060ccda448706ce9a7577107045bc
DEBUG:boto:StringToSign:
AWS4-HMAC-SHA256
20140204T211547Z
20140204/us-east-1/cloudformation/aws4_request
f2704f5688594252d7cd4f721c7a7e03d51d41a0c3f33e82a1f58bfba97fcafa
DEBUG:boto:Signature:
ac40d1a913cf3ca67cc2b80bc05e7c1f364eb02e83bb3c2c96ea18241f183564
DEBUG:boto:wrapping ssl socket; CA certificate file=/Users/vrivellino/.devninja.env/lib/python2.7/site-packages/boto/cacerts/cacerts.txt
DEBUG:boto:validating server certificate: hostname=cloudformation.us-east-1.amazonaws.com, certificate hosts=['cloudformation.us-east-1.amazonaws.com']
DEBUG:boto:<ValidateTemplateResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
  <ValidateTemplateResult>
    <Description>Test</Description>
    <CapabilitiesReason>The following resource(s) require capabilities: [AWS::IAM::User, AWS::IAM::AccessKey]</CapabilitiesReason>
    <Capabilities>
      <member>CAPABILITY_IAM</member>
    </Capabilities>
    <Parameters/>
  </ValidateTemplateResult>
  <ResponseMetadata>
    <RequestId>8465355d-8de1-11e3-ab21-addb98855f3a</RequestId>
  </ResponseMetadata>
</ValidateTemplateResponse>

In [11]: validation_response.Capabilities
Out[11]: u'\n    '
```

Here's the CloudFormation template I used:

``` JSON
{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "Test",

    "Resources" : {

        "IamUser" : {
            "Type" : "AWS::IAM::User",
            "Properties" : {
                "Path" : "/",
                "Policies" : [{
                    "PolicyName" : "CfnInit",
                    "PolicyDocument": {
                        "Statement":[{
                            "Effect"   : "Allow",
                            "Action"   : [ "cloudformation:DescribeStackResource" ],
                            "Resource" : "*"
                        }]
                    }
                }]
            }
        },

        "CfnKeys" : {
            "Type" : "AWS::IAM::AccessKey",
            "Properties" : {
                "UserName" : { "Ref" : "IamUser" }
            }
        }
    },

    "Outputs" : {
        "User" : {
            "Description" : "IAM User",
            "Value" : { "Ref" : "IamUser" }
        },
        "AccessKeyId" : {
            "Description" : "AccessKeyId",
            "Value" : { "Ref" : "CfnKeys" }
        }
    }
}
```

Just for reference, this is the output when using the AWS cli tools to validate the template:

```
$ aws cloudformation validate-template --template-body file://./iam-user.json
{
    "CapabilitiesReason": "The following resource(s) require capabilities: [AWS::IAM::User, AWS::IAM::AccessKey]",
    "Description": "Test",
    "Parameters": [],
    "Capabilities": [
        "CAPABILITY_IAM"
    ]
}
```



Discussion:

Resolving patch:
diff --git a/boto/cloudformation/template.py b/boto/cloudformation/template.py
--- a/boto/cloudformation/template.py
+++ b/boto/cloudformation/template.py
@@ -1,21 +1,29 @@
 from boto.resultset import ResultSet
+from boto.cloudformation.stack import Capability
 
 class Template(object):
     def __init__(self, connection=None):
         self.connection = connection
         self.description = None
         self.template_parameters = None
+        self.capabilities_reason = None
+        self.capabilities = None
 
     def startElement(self, name, attrs, connection):
         if name == "Parameters":
             self.template_parameters = ResultSet([('member', TemplateParameter)])
             return self.template_parameters
+        elif name == "Capabilities":
+            self.capabilities = ResultSet([('member', Capability)])
+            return self.capabilities
         else:
             return None
 
     def endElement(self, name, value, connection):
         if name == "Description":
             self.description = value
+        elif name == "CapabilitiesReason":
+            self.capabilities_reason = value
         else:
             setattr(self, name, value)
 


Test patch:
diff --git a/tests/unit/cloudformation/test_connection.py b/tests/unit/cloudformation/test_connection.py
--- a/tests/unit/cloudformation/test_connection.py
+++ b/tests/unit/cloudformation/test_connection.py
@@ -569,6 +569,10 @@ def default_body(self):
                     <Description>EC2 KeyPair</Description>
                   </member>
                 </Parameters>
+                <CapabilitiesReason>Reason</CapabilitiesReason>
+                <Capabilities>
+                  <member>CAPABILITY_IAM</member>
+                </Capabilities>
               </ValidateTemplateResult>
               <ResponseMetadata>
                 <RequestId>0be7b6e8-e4a0-11e0-a5bd-9f8d5a7dbc91</RequestId>
@@ -593,6 +597,11 @@ def test_validate_template(self):
         self.assertEqual(param2.no_echo, True)
         self.assertEqual(param2.parameter_key, 'KeyName')
 
+        self.assertEqual(template.capabilities_reason, 'Reason')
+
+        self.assertEqual(len(template.capabilities), 1)
+        self.assertEqual(template.capabilities[0].value, 'CAPABILITY_IAM')
+
         self.assert_request_parameters({
             'Action': 'ValidateTemplate',
             'TemplateBody': SAMPLE_TEMPLATE,