--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import re
+import logging
+from heat.common import utils as heat_utils
+from heat.db import api as db_api
+from heat.engine import parser
+
+
+logger = logging.getLogger('heat.engine.manager')
+
+PARAM_KEYS = (
+ PARAM_TIMEOUT,
+ PARAM_USER_KEY_re,
+ PARAM_USER_VALUE_fmt,
+) = (
+ 'TimeoutInMinutes',
+ re.compile(r'Parameters\.member\.(.*?)\.ParameterKey$'),
+ 'Parameters.member.%s.ParameterValue',
+)
+
+
+def extract_user_params(params):
+ '''
+ Extract a dictionary of user parameters (to e.g. a stack create command)
+ from the parameter dictionary passed through the API.
+
+ In the API parameters, each user parameter appears as two key-value pairs
+ with keys of the form:
+
+ Parameters.member.1.ParameterKey
+ Parameters.member.1.ParameterValue
+ '''
+ def get_param_pairs():
+ for k in params:
+ keymatch = PARAM_USER_KEY_re.match(k)
+ if keymatch:
+ key = params[k]
+ v = PARAM_USER_VALUE_fmt % keymatch.group(1)
+ try:
+ value = params[v]
+ except KeyError:
+ logger.error('Could not apply parameter %s' % key)
+
+ yield (key, value)
+
+ return dict(get_param_pairs())
+
+
+def extract_args(params):
+ '''
+ Extract any arguments passed as parameters through the API and return them
+ as a dictionary.
+ '''
+ kwargs = {}
+ try:
+ timeout_mins = int(params.get(PARAM_TIMEOUT, 0))
+ except (ValueError, TypeError):
+ logger.exception('create timeout conversion')
+ else:
+ if timeout_mins > 0:
+ kwargs['timeout_in_minutes'] = timeout_mins
+ return kwargs
+
+
+def _filter_keys(data, keys):
+ '''
+ Filter the provided data so that only the dictionary keys specified are
+ present. If keys is None, return all of the data.
+ '''
+ if keys is not None:
+ data = dict((k, v) for (k, v) in data.iteritems() if k in keys)
+
+ return data
+
+
+STACK_KEYS = (
+ STACK_NAME, STACK_ID,
+ STACK_CREATION_TIME, STACK_UPDATED_TIME, STACK_DELETION_TIME,
+ STACK_NOTIFICATION_TOPICS,
+ STACK_DESCRIPTION, STACK_TMPL_DESCRIPTION,
+ STACK_PARAMETERS, STACK_OUTPUTS,
+ STACK_STATUS, STACK_STATUS_DATA,
+ STACK_TIMEOUT,
+) = (
+ 'StackName', 'StackId',
+ 'CreationTime', 'LastUpdatedTime', 'DeletionTime',
+ 'NotificationARNs',
+ 'Description', 'TemplateDescription',
+ 'Parameters', 'Outputs',
+ 'StackStatus', 'StackStatusReason',
+ PARAM_TIMEOUT,
+)
+
+KEYS_STACK = (
+ STACK_NAME, STACK_ID,
+ STACK_CREATION_TIME, STACK_UPDATED_TIME,
+ STACK_NOTIFICATION_TOPICS,
+ STACK_DESCRIPTION,
+ STACK_PARAMETERS, STACK_DESCRIPTION, STACK_OUTPUTS,
+ STACK_STATUS, STACK_STATUS_DATA,
+ STACK_TIMEOUT,
+)
+KEYS_STACK_SUMMARY = (
+ STACK_CREATION_TIME, STACK_DELETION_TIME,
+ STACK_UPDATED_TIME,
+ STACK_ID, STACK_NAME,
+ STACK_TMPL_DESCRIPTION,
+ STACK_STATUS, STACK_STATUS_DATA,
+)
+
+
+STACK_OUTPUT_KEYS = (
+ OUTPUT_DESCRIPTION,
+ OUTPUT_KEY, OUTPUT_VALUE,
+) = (
+ 'Description',
+ 'OutputKey', 'OutputValue',
+)
+
+
+def format_stack_outputs(stack, outputs):
+ '''
+ Return a representation of the given output template for the given stack
+ that matches the API output expectations.
+ '''
+ def format_stack_output(k):
+ return {OUTPUT_DESCRIPTION: outputs[k].get('Description',
+ 'No description given'),
+ OUTPUT_KEY: k,
+ OUTPUT_VALUE: stack.output(k)}
+
+ return [format_stack_output(key) for key in outputs]
+
+
+def format_stack(stack, keys=None):
+ '''
+ Return a representation of the given stack that matches the API output
+ expectations.
+ '''
+ s = db_api.stack_get(stack.context, stack.id)
+ info = {
+ STACK_NAME: stack.name,
+ STACK_ID: stack.stack_id(),
+ STACK_CREATION_TIME: heat_utils.strtime(s.created_at),
+ STACK_UPDATED_TIME: heat_utils.strtime(s.updated_at),
+ STACK_NOTIFICATION_TOPICS: [], # TODO Not implemented yet
+ STACK_PARAMETERS: stack.t[parser.PARAMETERS],
+ STACK_DESCRIPTION: stack.t[parser.DESCRIPTION],
+ STACK_TMPL_DESCRIPTION: stack.t[parser.DESCRIPTION],
+ STACK_STATUS: s.status,
+ STACK_STATUS_DATA: s.status_reason,
+ }
+
+ # only show the outputs on a completely created stack
+ if s.status == stack.CREATE_COMPLETE:
+ info[STACK_OUTPUTS] = format_stack_outputs(stack, stack.outputs)
+
+ return _filter_keys(info, keys)
+
+
+RES_KEYS = (
+ RES_DESCRIPTION, RES_UPDATED_TIME,
+ RES_NAME, RES_PHYSICAL_ID, RES_METADATA,
+ RES_STATUS, RES_STATUS_DATA, RES_TYPE,
+ RES_STACK_ID, RES_STACK_NAME,
+ RES_TIMESTAMP,
+) = (
+ 'Description', 'LastUpdatedTimestamp',
+ 'LogicalResourceId', 'PhysicalResourceId', 'Metadata',
+ 'ResourceStatus', 'ResourceStatusReason', 'ResourceType',
+ STACK_ID, STACK_NAME,
+ 'Timestamp',
+)
+
+KEYS_RESOURCE_DETAIL = (
+ RES_DESCRIPTION, RES_UPDATED_TIME,
+ RES_NAME, RES_PHYSICAL_ID, RES_METADATA,
+ RES_STATUS, RES_STATUS_DATA, RES_TYPE,
+ RES_STACK_ID, RES_STACK_NAME,
+)
+KEYS_RESOURCE = (
+ RES_DESCRIPTION,
+ RES_NAME, RES_PHYSICAL_ID,
+ RES_STATUS, RES_STATUS_DATA, RES_TYPE,
+ RES_STACK_ID, RES_STACK_NAME,
+ RES_TIMESTAMP,
+)
+KEYS_RESOURCE_SUMMARY = (
+ RES_UPDATED_TIME,
+ RES_NAME, RES_PHYSICAL_ID,
+ RES_STATUS, RES_STATUS_DATA, RES_TYPE,
+)
+
+
+def format_stack_resource(resource, keys=None):
+ '''
+ Return a representation of the given resource that matches the API output
+ expectations.
+ '''
+ rs = db_api.resource_get(resource.context, resource.id)
+ last_updated_time = rs.updated_at or rs.created_at
+ attrs = {
+ RES_DESCRIPTION: resource.parsed_template().get('Description', ''),
+ RES_UPDATED_TIME: heat_utils.strtime(last_updated_time),
+ RES_NAME: resource.name,
+ RES_PHYSICAL_ID: resource.instance_id or '',
+ RES_METADATA: rs.rsrc_metadata,
+ RES_STATUS: rs.state,
+ RES_STATUS_DATA: rs.state_description,
+ RES_TYPE: resource.t['Type'],
+ RES_STACK_ID: resource.stack.stack_id(),
+ RES_STACK_NAME: resource.stack.name,
+ RES_TIMESTAMP: heat_utils.strtime(last_updated_time),
+ }
+
+ return _filter_keys(attrs, keys)
+
+
+EVENT_KEYS = (
+ EVENT_ID,
+ EVENT_STACK_ID, EVENT_STACK_NAME,
+ EVENT_TIMESTAMP,
+ EVENT_RES_NAME, EVENT_RES_PHYSICAL_ID,
+ EVENT_RES_STATUS, EVENT_RES_STATUS_DATA, EVENT_RES_TYPE,
+ EVENT_RES_PROPERTIES,
+) = (
+ 'EventId',
+ STACK_ID, STACK_NAME,
+ RES_TIMESTAMP,
+ RES_NAME, RES_PHYSICAL_ID,
+ RES_STATUS, RES_STATUS_DATA, RES_TYPE,
+ 'ResourceProperties',
+)
+
+
+def format_event(event, keys=None):
+ s = event.stack
+ attrs = {
+ EVENT_ID: event.id,
+ EVENT_STACK_ID: event.stack_id,
+ EVENT_STACK_NAME: s.name,
+ EVENT_TIMESTAMP: heat_utils.strtime(event.created_at),
+ EVENT_RES_NAME: event.logical_resource_id,
+ EVENT_RES_PHYSICAL_ID: event.physical_resource_id,
+ EVENT_RES_STATUS: event.name,
+ EVENT_RES_STATUS_DATA: event.resource_status_reason,
+ EVENT_RES_TYPE: event.resource_type,
+ EVENT_RES_PROPERTIES: event.resource_properties,
+ }
+
+ return _filter_keys(attrs, keys)
import webob
import json
import urlparse
-import re
import httplib
import eventlet
from heat.common import config
from heat.common import utils as heat_utils
from heat.common import context as ctxtlib
+from heat.engine import api
from heat.engine import parser
from heat.engine import resources
from heat.engine import watchrule
logger = logging.getLogger('heat.engine.manager')
greenpool = eventlet.GreenPool()
-_param_key = re.compile(r'Parameters\.member\.(.*?)\.ParameterKey$')
-
-
-def _extract_user_params(params):
- def get_param_pairs():
- for k in params:
- keymatch = _param_key.match(k)
- if keymatch:
- key = params[k]
- v = 'Parameters.member.%s.ParameterValue' % keymatch.group(1)
- try:
- value = params[v]
- except KeyError:
- logger.error('Could not apply parameter %s' % key)
-
- yield (key, value)
-
- return dict(get_param_pairs())
-
-
-def _extract_args(params):
- kwargs = {}
- try:
- timeout_mins = int(params.get('TimeoutInMinutes', 0))
- except (ValueError, TypeError):
- logger.exception('create timeout conversion')
- else:
- if timeout_mins > 0:
- kwargs['timeout_in_minutes'] = timeout_mins
- return kwargs
-
class EngineManager(manager.Manager):
"""
auth.authenticate(context)
- res = {'stacks': []}
stacks = db_api.stack_get_by_user(context)
if stacks is None:
- return res
- for s in stacks:
+ stacks = []
+
+ def format_stack_summary(s):
stack = parser.Stack.load(context, s.id)
- mem = {}
- mem['StackId'] = stack.stack_id()
- mem['StackName'] = s.name
- mem['CreationTime'] = heat_utils.strtime(s.created_at)
- mem['TemplateDescription'] = stack.t[parser.DESCRIPTION]
- mem['StackStatus'] = s.status
- res['stacks'].append(mem)
+ return api.format_stack(stack, api.KEYS_STACK_SUMMARY)
- return res
+ return {'stacks': [format_stack_summary(s) for s in stacks]}
def show_stack(self, context, stack_name, params):
"""
"""
auth.authenticate(context)
- res = {'stacks': []}
- stacks = []
- if not stack_name:
- stacks = [s.name for s in db_api.stack_get_by_user(context)]
- logging.debug("No stack name passed, got %s" % stacks)
+ if stack_name is not None:
+ s = db_api.stack_get_by_name(context, stack_name)
+ stacks = [s] if s is not None else []
else:
- stacks = [stack_name]
-
- if not stacks:
- logging.debug("No stacks found to process")
- return res
-
- for stack in stacks:
- logging.debug("Processing show_stack for %s" % stack)
- s = db_api.stack_get_by_name(context, stack)
- if s:
- stack = parser.Stack.load(context, s.id)
- mem = {}
- mem['StackId'] = stack.stack_id()
- mem['StackName'] = s.name
- mem['CreationTime'] = heat_utils.strtime(s.created_at)
- mem['LastUpdatedTimestamp'] = heat_utils.strtime(s.updated_at)
- mem['NotificationARNs'] = 'TODO'
- mem['Parameters'] = stack.t[parser.PARAMETERS]
- mem['Description'] = stack.t[parser.DESCRIPTION]
- mem['StackStatus'] = s.status
- mem['StackStatusReason'] = s.status_reason
-
- # only show the outputs on a completely created stack
- if s.status == stack.CREATE_COMPLETE:
- mem['Outputs'] = stack.get_outputs()
-
- res['stacks'].append(mem)
-
- return res
+ stacks = db_api.stack_get_by_user(context) or []
+
+ def format_stack_detail(s):
+ stack = parser.Stack.load(context, s.id)
+ return api.format_stack(stack, api.KEYS_STACK)
+
+ return {'stacks': [format_stack_detail(s) for s in stacks]}
def create_stack(self, context, stack_name, template, params):
"""
tmpl = parser.Template(template)
user_params = parser.Parameters(stack_name, tmpl,
- _extract_user_params(params))
+ api.extract_user_params(params))
stack = parser.Stack(context, stack_name, tmpl, user_params)
response = stack.validate()
return response
stack_id = stack.store()
- greenpool.spawn_n(stack.create, **_extract_args(params))
+ greenpool.spawn_n(stack.create, **api.extract_args(params))
return {'StackId': stack.stack_id()}
try:
tmpl = parser.Template(template)
user_params = parser.Parameters(stack_name, tmpl,
- _extract_user_params(params))
+ api.extract_user_params(params))
s = parser.Stack(context, stack_name, tmpl, user_params)
except KeyError as ex:
res = ('A Fn::FindInMap operation referenced '
greenpool.spawn_n(stack.delete)
return None
- # Helper for list_events. It's here so we can use it in tests.
- def parse_event(self, event):
- s = event.stack
- return {'EventId': event.id,
- 'StackId': event.stack_id,
- 'StackName': s.name,
- 'Timestamp': heat_utils.strtime(event.created_at),
- 'LogicalResourceId': event.logical_resource_id,
- 'PhysicalResourceId': event.physical_resource_id,
- 'ResourceType': event.resource_type,
- 'ResourceStatusReason': event.resource_status_reason,
- 'ResourceProperties': event.resource_properties,
- 'ResourceStatus': event.name}
-
def list_events(self, context, stack_name, params):
"""
The list_events method lists all events associated with a given stack.
else:
events = db_api.event_get_all_by_user(context)
- return {'events': [self.parse_event(e) for e in events]}
+ return {'events': [api.format_event(e) for e in events]}
def event_create(self, context, event):
if resource.id is None:
raise AttributeError('Resource not created')
- return format_stack_resource(stack[resource_name])
+ return api.format_stack_resource(stack[resource_name],
+ api.KEYS_RESOURCE_DETAIL)
def describe_stack_resources(self, context, stack_name,
physical_resource_id, logical_resource_id):
raise AttributeError("The specified stack doesn't exist")
stack = parser.Stack.load(context, s.id)
- resources = []
- for resource in stack:
- if logical_resource_id and resource.name != logical_resource_id:
- continue
- formatted = format_stack_resource(resource)
- # this API call uses Timestamp instead of LastUpdatedTimestamp
- formatted['Timestamp'] = formatted['LastUpdatedTimestamp']
- del formatted['LastUpdatedTimestamp']
- del formatted['Metadata']
- resources.append(formatted)
- return resources
+ if logical_resource_id is not None:
+ name_match = lambda r: r.name == logical_resource_id
+ else:
+ name_match = lambda r: True
+
+ return [api.format_stack_resource(resource, api.KEYS_RESOURCE)
+ for resource in stack if resource.id is not None and
+ name_match(resource)]
def list_stack_resources(self, context, stack_name):
auth.authenticate(context)
stack = parser.Stack.load(context, s.id)
- resources = []
- response_keys = ('ResourceStatus', 'LogicalResourceId',
- 'LastUpdatedTimestamp', 'PhysicalResourceId',
- 'ResourceType')
- for resource in stack:
- formatted = format_stack_resource(resource)
- for key in formatted.keys():
- if not key in response_keys:
- del formatted[key]
- resources.append(formatted)
- return resources
+ return [api.format_stack_resource(resource, api.KEYS_RESOURCE_SUMMARY)
+ for resource in stack if resource.id is not None]
def metadata_register_address(self, context, url):
config.FLAGS.heat_metadata_server_url = url
self.run_rule(None, wr)
return [None, wd.data]
-
-
-def format_stack_resource(resource):
- """
- Return a representation of the given resource that mathes the API output
- expectations.
- """
- rs = db_api.resource_get(resource.stack.context, resource.id)
- last_updated_time = rs.updated_at or rs.created_at
- return {
- 'StackId': resource.stack.stack_id(),
- 'StackName': resource.stack.name,
- 'LogicalResourceId': resource.name,
- 'PhysicalResourceId': resource.instance_id or '',
- 'ResourceType': resource.t['Type'],
- 'LastUpdatedTimestamp': heat_utils.strtime(last_updated_time),
- 'Metadata': rs.rsrc_metadata,
- 'ResourceStatus': rs.state,
- 'ResourceStatusReason': rs.state_description,
- }
db_api.stack_delete(self.context, self.id)
def output(self, key):
+ '''
+ Get the value of the specified stack output.
+ '''
value = self.outputs[key].get('Value', '')
return self.resolve_runtime_data(value)
- def get_outputs(self):
- def output_dict(k):
- return {'Description': self.outputs[k].get('Description',
- 'No description given'),
- 'OutputKey': k,
- 'OutputValue': self.output(k)}
-
- return [output_dict(key) for key in self.outputs]
-
def restart_resource(self, resource_name):
'''
stop resource_name and all that depend on it
import unittest
from nose.plugins.attrib import attr
-import heat.engine.manager as manager
+import heat.engine.api as api
-@attr(tag=['unit', 'manager'])
+@attr(tag=['unit', 'engine-api'])
@attr(speed='fast')
-class managerTest(unittest.TestCase):
+class EngineApiTest(unittest.TestCase):
def test_params_extract(self):
p = {'Parameters.member.Foo.ParameterKey': 'foo',
'Parameters.member.Foo.ParameterValue': 'bar',
'Parameters.member.Blarg.ParameterKey': 'blarg',
'Parameters.member.Blarg.ParameterValue': 'wibble'}
- params = manager._extract_user_params(p)
+ params = api.extract_user_params(p)
self.assertEqual(len(params), 2)
self.assertTrue('foo' in params)
self.assertEqual(params['foo'], 'bar')
'Parameters.member.Foo.Bar.ParameterValue': 'bar',
'Parameters.member.Foo.Baz.ParameterKey': 'blarg',
'Parameters.member.Foo.Baz.ParameterValue': 'wibble'}
- params = manager._extract_user_params(p)
+ params = api.extract_user_params(p)
self.assertEqual(len(params), 2)
self.assertTrue('foo' in params)
self.assertEqual(params['foo'], 'bar')
'Parameters.member.Foo.Bar.ParameterValue': 'bar',
'Foo.Baz.ParameterKey': 'blarg',
'Foo.Baz.ParameterValue': 'wibble'}
- params = manager._extract_user_params(p)
+ params = api.extract_user_params(p)
self.assertEqual(len(params), 1)
self.assertTrue('foo' in params)
self.assertEqual(params['foo'], 'bar')
def test_params_extract_garbage_prefix(self):
p = {'prefixParameters.member.Foo.Bar.ParameterKey': 'foo',
'Parameters.member.Foo.Bar.ParameterValue': 'bar'}
- params = manager._extract_user_params(p)
+ params = api.extract_user_params(p)
self.assertFalse(params)
def test_params_extract_garbage_suffix(self):
p = {'Parameters.member.Foo.Bar.ParameterKeysuffix': 'foo',
'Parameters.member.Foo.Bar.ParameterValue': 'bar'}
- params = manager._extract_user_params(p)
+ params = api.extract_user_params(p)
self.assertFalse(params)
def test_timeout_extract(self):
p = {'TimeoutInMinutes': '5'}
- args = manager._extract_args(p)
+ args = api.extract_args(p)
self.assertEqual(args['timeout_in_minutes'], 5)
def test_timeout_extract_zero(self):
p = {'TimeoutInMinutes': '0'}
- args = manager._extract_args(p)
+ args = api.extract_args(p)
self.assertTrue('timeout_in_minutes' not in args)
def test_timeout_extract_garbage(self):
p = {'TimeoutInMinutes': 'wibble'}
- args = manager._extract_args(p)
+ args = api.extract_args(p)
self.assertTrue('timeout_in_minutes' not in args)
def test_timeout_extract_none(self):
p = {'TimeoutInMinutes': None}
- args = manager._extract_args(p)
+ args = api.extract_args(p)
self.assertTrue('timeout_in_minutes' not in args)
def test_timeout_extract_not_present(self):
- args = manager._extract_args({})
+ args = api.extract_args({})
self.assertTrue('timeout_in_minutes' not in args)
from heat.engine import auth
-@attr(tag=['unit', 'resource'])
+@attr(tag=['unit', 'engine-api', 'resource'])
@attr(speed='slow')
class stacksTest(unittest.TestCase):
def setUp(self):
self.assertEqual(db_s.status, 'DELETE_COMPLETE')
def test_stack_event_list(self):
- stack = self.get_wordpress_stack('test_event_list_stack')
+ ctx = self.create_context('test_event_list_user')
+ auth.authenticate(ctx).AndReturn(True)
+
+ stack = self.get_wordpress_stack('test_event_list_stack', ctx)
self.m.ReplayAll()
stack.store()
stack.create()
self.assertNotEqual(stack.resources['WebServer'], None)
self.assertTrue(stack.resources['WebServer'].instance_id > 0)
- m = manager.EngineManager()
- events = db_api.event_get_all_by_stack(None, stack.id)
- for ev in [m.parse_event(e) for e in events]:
+ man = manager.EngineManager()
+ el = man.list_events(ctx, stack.name, {})
+
+ self.assertTrue('events' in el)
+ events = el['events']
+
+ self.assertEqual(len(events), 2)
+ for ev in events:
self.assertTrue('EventId' in ev)
self.assertTrue(ev['EventId'] > 0)
self.assertTrue(len(sl['stacks']) > 0)
for s in sl['stacks']:
self.assertTrue('CreationTime' in s)
- #self.assertTrue('LastUpdatedTime' in s)
+ self.assertTrue('LastUpdatedTime' in s)
self.assertTrue('StackId' in s)
self.assertNotEqual(s['StackId'], None)
self.assertTrue('StackName' in s)
self.assertTrue('StackStatus' in s)
- #self.assertTrue('StackStatusReason' in s)
+ self.assertTrue('StackStatusReason' in s)
self.assertTrue('TemplateDescription' in s)
self.assertNotEqual(s['TemplateDescription'].find('WordPress'), -1)
s = sl['stacks'][0]
self.assertTrue('CreationTime' in s)
- #self.assertTrue('LastUpdatedTime' in s)
+ self.assertTrue('LastUpdatedTime' in s)
self.assertTrue('StackId' in s)
self.assertNotEqual(s['StackId'], None)
self.assertTrue('StackName' in s)
r = man.describe_stack_resource(ctx, 'test_stack_res_desc',
'WebServer')
- #self.assertTrue('Description' in r)
+ self.assertTrue('Description' in r)
self.assertTrue('LastUpdatedTimestamp' in r)
self.assertTrue('StackId' in r)
self.assertNotEqual(r['StackId'], None)
self.assertEqual(len(resources), 1)
r = resources[0]
- #self.assertTrue('Description' in r)
+ self.assertTrue('Description' in r)
self.assertTrue('Timestamp' in r)
self.assertTrue('StackId' in r)
self.assertNotEqual(r['StackId'], None)
self.assertTrue('LogicalResourceId' in r)
self.assertEqual(r['LogicalResourceId'], 'WebServer')
self.assertTrue('ResourceStatus' in r)
- #self.assertTrue('ResourceStatusReason' in r)
+ self.assertTrue('ResourceStatusReason' in r)
self.assertTrue('ResourceType' in r)
def test_stack_resources_list_nonexist_stack(self):