diff --git a/heat/api/v1/stacks.py b/heat/api/v1/stacks.py index 1f90105337..84c0a60b93 100644 --- a/heat/api/v1/stacks.py +++ b/heat/api/v1/stacks.py @@ -31,6 +31,7 @@ from heat.common import context from heat import utils from heat import rpc import heat.rpc.common as rpc_common +import heat.engine.api as engine_api from heat.openstack.common import log as logging @@ -79,22 +80,103 @@ class StackController(object): # FIXME : further investigation into engine errors required return exception.HeatInternalFailureError(detail=ex.value) + @staticmethod + def _extract_user_params(params): + """ + Extract a dictionary of user input parameters for the stack + + In the AWS API parameters, each user parameter appears as two key-value + pairs with keys of the form below: + + Parameters.member.1.ParameterKey + Parameters.member.1.ParameterValue + + We reformat this into a normal dict here to match the heat + engine API expected format + + Note this implemented outside of "create" as it will also be + used by update (and EstimateTemplateCost if appropriate..) + """ + # Define the AWS key format to extract + PARAM_KEYS = ( + PARAM_USER_KEY_re, + PARAM_USER_VALUE_fmt, + ) = ( + re.compile(r'Parameters\.member\.(.*?)\.ParameterKey$'), + 'Parameters.member.%s.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()) + + @staticmethod + def _reformat_dict_keys(keymap={}, inputdict={}): + ''' + Utility function for mapping one dict format to another + ''' + result = {} + for key in keymap: + result[keymap[key]] = inputdict[key] + return result + def list(self, req): """ Implements ListStacks API action Lists summary information for all stacks """ + + def format_stack_summary(s): + """ + Reformat engine output into the AWS "StackSummary" format + """ + # Map the engine-api format to the AWS StackSummary datatype + keymap = { + engine_api.STACK_CREATION_TIME: 'CreationTime', + engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime', + engine_api.STACK_ID: 'StackId', + engine_api.STACK_NAME: 'StackName', + engine_api.STACK_STATUS: 'StackStatus', + engine_api.STACK_STATUS_DATA: 'StackStatusReason', + engine_api.STACK_TMPL_DESCRIPTION: 'TemplateDescription', + } + + result = self._reformat_dict_keys(keymap, s) + + # AWS docs indicate DeletionTime is ommitted for current stacks + # This is still TODO in the engine, we don't keep data for + # stacks after they are deleted + if engine_api.STACK_DELETION_TIME in s: + result['DeletionTime'] = s[engine_api.STACK_DELETION_TIME] + + return self._stackid_addprefix(result) + con = req.context parms = dict(req.params) - stack_list = rpc.call(con, 'engine', - {'method': 'list_stacks', - 'args': {'params': parms}}) + try: + # Note show_stack returns details for all stacks when called with + # no stack_name, we only use a subset of the result here though + stack_list = rpc.call(con, 'engine', + {'method': 'show_stack', + 'args': {'stack_name': None, + 'params': parms}}) + except rpc_common.RemoteError as ex: + return self._remote_error(ex) - res = {'StackSummaries': []} - if stack_list is not None: - for s in stack_list['stacks']: - res['StackSummaries'].append(self._stackid_addprefix(s)) + res = {'StackSummaries': [format_stack_summary(s) + for s in stack_list['stacks']]} return self._format_response('ListStacks', res) @@ -103,6 +185,53 @@ class StackController(object): Implements DescribeStacks API action Gets detailed information for a stack (or all stacks) """ + def format_stack_outputs(o): + keymap = { + engine_api.OUTPUT_DESCRIPTION: 'Description', + engine_api.OUTPUT_KEY: 'OutputKey', + engine_api.OUTPUT_VALUE: 'OutputValue', + } + + return self._reformat_dict_keys(keymap, o) + + def format_stack(s): + """ + Reformat engine output into the AWS "StackSummary" format + """ + keymap = { + engine_api.STACK_CAPABILITIES: 'Capabilities', + engine_api.STACK_CREATION_TIME: 'CreationTime', + engine_api.STACK_DESCRIPTION: 'Description', + engine_api.STACK_DISABLE_ROLLBACK: 'DisableRollback', + engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime', + engine_api.STACK_NOTIFICATION_TOPICS: 'NotificationARNs', + engine_api.STACK_PARAMETERS: 'Parameters', + engine_api.STACK_ID: 'StackId', + engine_api.STACK_NAME: 'StackName', + engine_api.STACK_STATUS: 'StackStatus', + engine_api.STACK_STATUS_DATA: 'StackStatusReason', + engine_api.STACK_TIMEOUT: 'TimeoutInMinutes', + } + + result = self._reformat_dict_keys(keymap, s) + + # Reformat outputs, these are handled separately as they are + # only present in the engine output for a completely created + # stack + result['Outputs'] = [] + if engine_api.STACK_OUTPUTS in s: + for o in s[engine_api.STACK_OUTPUTS]: + result['Outputs'].append(format_stack_outputs(o)) + + # Reformat Parameters dict-of-dict into AWS API format + # This is a list-of-dict with nasty "ParameterKey" : key + # "ParameterValue" : value format. + result['Parameters'] = [{'ParameterKey':k, + 'ParameterValue':v.get('Default')} + for (k, v) in result['Parameters'].items()] + + return self._stackid_addprefix(result) + con = req.context parms = dict(req.params) @@ -122,15 +251,7 @@ class StackController(object): except rpc_common.RemoteError as ex: return self._remote_error(ex) - res = {'Stacks': []} - for s in stack_list['stacks']: - # Reformat Parameters dict-of-dict into AWS API format - # This is a list-of-dict with nasty "ParameterKey" : key - # "ParameterValue" : value format. - s['Parameters'] = [{'ParameterKey':k, - 'ParameterValue':v.get('Default')} - for (k, v) in s['Parameters'].items()] - res['Stacks'].append(self._stackid_addprefix(s)) + res = {'Stacks': [format_stack(s) for s in stack_list['stacks']]} return self._format_response('DescribeStacks', res) @@ -165,8 +286,29 @@ class StackController(object): Implements CreateStack API action Create stack as defined in template file """ + def extract_args(params): + """ + Extract request parameters/arguments and reformat them to match + the engine API. FIXME: we currently only support a subset of + the AWS defined parameters (both here and in the engine) + """ + # TODO : Capabilities, DisableRollback, NotificationARNs + keymap = {'TimeoutInMinutes': engine_api.PARAM_TIMEOUT, } + + result = {} + for k in keymap: + if k in req.params: + result[keymap[k]] = params[k] + + return result + con = req.context - parms = dict(req.params) + + # Extract the stack input parameters + stack_parms = self._extract_user_params(req.params) + + # Extract any additional arguments ("Request Parameters") + create_args = extract_args(req.params) try: templ = self._get_template(req) @@ -189,7 +331,8 @@ class StackController(object): {'method': 'create_stack', 'args': {'stack_name': req.params['StackName'], 'template': stack, - 'params': parms}}) + 'params': stack_parms, + 'args': create_args}}) except rpc_common.RemoteError as ex: return self._remote_error(ex) @@ -288,6 +431,27 @@ class StackController(object): Implements the DescribeStackEvents API action Returns events related to a specified stack (or all stacks) """ + def format_stack_event(e): + """ + Reformat engine output into the AWS "StackEvent" format + """ + keymap = { + engine_api.EVENT_ID: 'EventId', + engine_api.EVENT_RES_NAME: 'LogicalResourceId', + engine_api.EVENT_RES_PHYSICAL_ID: 'PhysicalResourceId', + engine_api.EVENT_RES_PROPERTIES: 'ResourceProperties', + engine_api.EVENT_RES_STATUS: 'ResourceStatus', + engine_api.EVENT_RES_STATUS_DATA: 'ResourceStatusData', + engine_api.EVENT_RES_TYPE: 'ResourceType', + engine_api.EVENT_STACK_ID: 'StackId', + engine_api.EVENT_STACK_NAME: 'StackName', + engine_api.EVENT_TIMESTAMP: 'Timestamp', + } + + result = self._reformat_dict_keys(keymap, e) + + return self._stackid_addprefix(result) + con = req.context parms = dict(req.params) @@ -302,9 +466,7 @@ class StackController(object): events = 'Error' not in event_res and event_res['events'] or [] - result = [] - for e in events: - result.append(self._stackid_addprefix(e)) + result = [format_stack_event(e) for e in events] return self._format_response('DescribeStackEvents', {'StackEvents': result}) @@ -314,6 +476,28 @@ class StackController(object): Implements the DescribeStackResource API action Return the details of the given resource belonging to the given stack. """ + + def format_resource_detail(r): + """ + Reformat engine output into the AWS "StackResourceDetail" format + """ + keymap = { + engine_api.RES_DESCRIPTION: 'Description', + engine_api.RES_UPDATED_TIME: 'LastUpdatedTimestamp', + engine_api.RES_NAME: 'LogicalResourceId', + engine_api.RES_METADATA: 'Metadata', + engine_api.RES_PHYSICAL_ID: 'PhysicalResourceId', + engine_api.RES_STATUS: 'ResourceStatus', + engine_api.RES_STATUS_DATA: 'ResourceStatusReason', + engine_api.RES_TYPE: 'ResourceType', + engine_api.RES_STACK_ID: 'StackId', + engine_api.RES_STACK_NAME: 'StackName', + } + + result = self._reformat_dict_keys(keymap, r) + + return self._stackid_addprefix(result) + con = req.context args = { 'stack_name': req.params.get('StackName'), @@ -328,8 +512,10 @@ class StackController(object): except rpc_common.RemoteError as ex: return self._remote_error(ex) + result = format_resource_detail(resource_details) + return self._format_response('DescribeStackResource', - {'StackResourceDetail': resource_details}) + {'StackResourceDetail': result}) def describe_stack_resources(self, req): """ @@ -347,6 +533,27 @@ class StackController(object): `LogicalResourceId`: filter the resources list by the logical resource id. """ + + def format_stack_resource(r): + """ + Reformat engine output into the AWS "StackResource" format + """ + keymap = { + engine_api.RES_DESCRIPTION: 'Description', + engine_api.RES_NAME: 'LogicalResourceId', + engine_api.RES_PHYSICAL_ID: 'PhysicalResourceId', + engine_api.RES_STATUS: 'ResourceStatus', + engine_api.RES_STATUS_DATA: 'ResourceStatusReason', + engine_api.RES_TYPE: 'ResourceType', + engine_api.RES_STACK_ID: 'StackId', + engine_api.RES_STACK_NAME: 'StackName', + engine_api.RES_UPDATED_TIME: 'Timestamp', + } + + result = self._reformat_dict_keys(keymap, r) + + return self._stackid_addprefix(result) + con = req.context stack_name = req.params.get('StackName') physical_resource_id = req.params.get('PhysicalResourceId') @@ -368,9 +575,7 @@ class StackController(object): except rpc_common.RemoteError as ex: return self._remote_error(ex) - result = [] - for r in resources: - result.append(self._stackid_addprefix(r)) + result = [format_stack_resource(r) for r in resources] return self._format_response('DescribeStackResources', {'StackResources': result}) @@ -380,6 +585,21 @@ class StackController(object): Implements the ListStackResources API action Return summary of the resources belonging to the specified stack. """ + def format_resource_summary(r): + """ + Reformat engine output into the AWS "StackResourceSummary" format + """ + keymap = { + engine_api.RES_UPDATED_TIME: 'LastUpdatedTimestamp', + engine_api.RES_NAME: 'LogicalResourceId', + engine_api.RES_PHYSICAL_ID: 'PhysicalResourceId', + engine_api.RES_STATUS: 'ResourceStatus', + engine_api.RES_STATUS_DATA: 'ResourceStatusReason', + engine_api.RES_TYPE: 'ResourceType', + } + + return self._reformat_dict_keys(keymap, r) + con = req.context try: @@ -390,8 +610,10 @@ class StackController(object): except rpc_common.RemoteError as ex: return self._remote_error(ex) + summaries = [format_resource_summary(r) for r in resources] + return self._format_response('ListStackResources', - {'StackResourceSummaries': resources}) + {'StackResourceSummaries': summaries}) def create_resource(options): diff --git a/heat/engine/api.py b/heat/engine/api.py index a87a3fb805..447363eadc 100644 --- a/heat/engine/api.py +++ b/heat/engine/api.py @@ -20,48 +20,14 @@ from heat.openstack.common import log as logging 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()) +PARAM_KEYS = (PARAM_TIMEOUT, ) = ('timeout_mins', ) def extract_args(params): ''' Extract any arguments passed as parameters through the API and return them - as a dictionary. + as a dictionary. This allows us to filter the passed args and do type + conversion where appropriate ''' kwargs = {} try: @@ -70,63 +36,34 @@ def extract_args(params): logger.exception('create timeout conversion') else: if timeout_mins > 0: - kwargs['timeout_mins'] = timeout_mins + kwargs[PARAM_TIMEOUT] = 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, + STACK_STATUS, STACK_STATUS_DATA, STACK_CAPABILITIES, + STACK_DISABLE_ROLLBACK, STACK_TIMEOUT, ) = ( - 'StackName', 'StackId', - 'CreationTime', 'LastUpdatedTime', 'DeletionTime', - 'NotificationARNs', - 'Description', 'TemplateDescription', - 'Parameters', 'Outputs', - 'StackStatus', 'StackStatusReason', - PARAM_TIMEOUT, + 'stack_name', 'stack_id', + 'creation_time', 'updated_time', 'deletion_time', + 'notification_topics', + 'description', 'template_description', + 'parameters', 'outputs', + 'stack_status', 'stack_status_reason', 'capabilities', + 'disable_rollback', 'timeout_mins' ) -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', + 'description', + 'output_key', 'output_value', ) @@ -144,7 +81,7 @@ def format_stack_outputs(stack, outputs): return [format_stack_output(key) for key in outputs] -def format_stack(stack, keys=None): +def format_stack(stack): ''' Return a representation of the given stack that matches the API output expectations. @@ -160,6 +97,8 @@ def format_stack(stack, keys=None): STACK_TMPL_DESCRIPTION: stack.t[parser.DESCRIPTION], STACK_STATUS: stack.state, STACK_STATUS_DATA: stack.state_description, + STACK_CAPABILITIES: [], # TODO Not implemented yet + STACK_DISABLE_ROLLBACK: True, # TODO Not implemented yet STACK_TIMEOUT: stack.timeout_mins, } @@ -167,7 +106,7 @@ def format_stack(stack, keys=None): if stack.state == stack.CREATE_COMPLETE: info[STACK_OUTPUTS] = format_stack_outputs(stack, stack.outputs) - return _filter_keys(info, keys) + return info RES_KEYS = ( @@ -175,42 +114,21 @@ RES_KEYS = ( 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', + 'description', 'updated_time', + 'logical_resource_id', 'physical_resource_id', 'metadata', + 'resource_status', 'resource_status_reason', 'resource_type', 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): +def format_stack_resource(resource): ''' Return a representation of the given resource that matches the API output expectations. ''' last_updated_time = resource.updated_time or resource.created_time - attrs = { + res = { RES_DESCRIPTION: resource.parsed_template().get('Description', ''), RES_UPDATED_TIME: heat_utils.strtime(last_updated_time), RES_NAME: resource.name, @@ -221,10 +139,9 @@ def format_stack_resource(resource, keys=None): RES_TYPE: resource.t['Type'], RES_STACK_ID: resource.stack.id, RES_STACK_NAME: resource.stack.name, - RES_TIMESTAMP: heat_utils.strtime(last_updated_time), } - return _filter_keys(attrs, keys) + return res EVENT_KEYS = ( @@ -235,18 +152,18 @@ EVENT_KEYS = ( EVENT_RES_STATUS, EVENT_RES_STATUS_DATA, EVENT_RES_TYPE, EVENT_RES_PROPERTIES, ) = ( - 'EventId', + 'event_id', STACK_ID, STACK_NAME, - RES_TIMESTAMP, + "event_time", RES_NAME, RES_PHYSICAL_ID, RES_STATUS, RES_STATUS_DATA, RES_TYPE, - 'ResourceProperties', + 'resource_properties', ) -def format_event(event, keys=None): +def format_event(event): s = event.stack - attrs = { + event = { EVENT_ID: event.id, EVENT_STACK_ID: s.id, EVENT_STACK_NAME: s.name, @@ -259,4 +176,4 @@ def format_event(event, keys=None): EVENT_RES_PROPERTIES: event.resource_properties, } - return _filter_keys(attrs, keys) + return event diff --git a/heat/engine/manager.py b/heat/engine/manager.py index d0eb2b5cd6..dcc3bca945 100644 --- a/heat/engine/manager.py +++ b/heat/engine/manager.py @@ -60,26 +60,6 @@ class EngineManager(manager.Manager): """Load configuration options and connect to the hypervisor.""" pass - def list_stacks(self, context, params): - """ - The list_stacks method is the end point that actually implements - the 'list' command of the heat API. - arg1 -> RPC context. - arg2 -> Dict of http request parameters passed in from API side. - """ - - auth.authenticate(context) - - stacks = db_api.stack_get_by_user(context) - if stacks is None: - stacks = [] - - def format_stack_summary(s): - stack = parser.Stack.load(context, s.id) - return api.format_stack(stack, api.KEYS_STACK_SUMMARY) - - return {'stacks': [format_stack_summary(s) for s in stacks]} - def show_stack(self, context, stack_name, params): """ The show_stack method returns the attributes of one stack. @@ -100,11 +80,11 @@ class EngineManager(manager.Manager): def format_stack_detail(s): stack = parser.Stack.load(context, s.id) - return api.format_stack(stack, api.KEYS_STACK) + return api.format_stack(stack) return {'stacks': [format_stack_detail(s) for s in stacks]} - def create_stack(self, context, stack_name, template, params): + def create_stack(self, context, stack_name, template, params, args): """ The create_stack method creates a new stack using the template provided. @@ -113,7 +93,8 @@ class EngineManager(manager.Manager): arg1 -> RPC context. arg2 -> Name of the stack you want to create. arg3 -> Template of stack you want to create. - arg4 -> Params passed from API. + arg4 -> Stack Input Params + arg4 -> Request parameters/args passed from API """ logger.info('template is %s' % template) @@ -123,10 +104,11 @@ class EngineManager(manager.Manager): return {'Error': 'Stack already exists with that name.'} tmpl = parser.Template(template) + # Extract the template parameters, and any common query parameters - template_params = parser.Parameters(stack_name, tmpl, - api.extract_user_params(params)) - common_params = api.extract_args(params) + template_params = parser.Parameters(stack_name, tmpl, params) + common_params = api.extract_args(args) + stack = parser.Stack(context, stack_name, tmpl, template_params, **common_params) @@ -159,8 +141,7 @@ class EngineManager(manager.Manager): stack_name = 'validate' try: tmpl = parser.Template(template) - user_params = parser.Parameters(stack_name, tmpl, - api.extract_user_params(params)) + user_params = parser.Parameters(stack_name, tmpl, params) s = parser.Stack(context, stack_name, tmpl, user_params) except KeyError as ex: res = ('A Fn::FindInMap operation referenced ' @@ -272,8 +253,7 @@ class EngineManager(manager.Manager): if resource.id is None: raise AttributeError('Resource not created') - return api.format_stack_resource(stack[resource_name], - api.KEYS_RESOURCE_DETAIL) + return api.format_stack_resource(stack[resource_name]) def describe_stack_resources(self, context, stack_name, physical_resource_id, logical_resource_id): @@ -299,7 +279,7 @@ class EngineManager(manager.Manager): else: name_match = lambda r: True - return [api.format_stack_resource(resource, api.KEYS_RESOURCE) + return [api.format_stack_resource(resource) for resource in stack if resource.id is not None and name_match(resource)] @@ -312,7 +292,7 @@ class EngineManager(manager.Manager): stack = parser.Stack.load(context, s.id) - return [api.format_stack_resource(resource, api.KEYS_RESOURCE_SUMMARY) + return [api.format_stack_resource(resource) for resource in stack if resource.id is not None] def metadata_register_address(self, context, url): diff --git a/heat/tests/test_api_v1.py b/heat/tests/test_api_v1.py new file mode 100644 index 0000000000..03ac9ab0a4 --- /dev/null +++ b/heat/tests/test_api_v1.py @@ -0,0 +1,91 @@ +# 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 nose +import unittest +from nose.plugins.attrib import attr + +from heat.common.config import HeatConfigOpts +import heat.api.v1.stacks as stacks + + +@attr(tag=['unit', 'api-v1-stacks', 'StackController']) +@attr(speed='fast') +class StackControllerTest(unittest.TestCase): + ''' + Tests the API class which acts as the WSGI controller, + the endpoint processing API requests after they are routed + ''' + 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 = self.controller._extract_user_params(p) + self.assertEqual(len(params), 2) + self.assertTrue('foo' in params) + self.assertEqual(params['foo'], 'bar') + self.assertTrue('blarg' in params) + self.assertEqual(params['blarg'], 'wibble') + + def test_params_extract_dots(self): + p = {'Parameters.member.Foo.Bar.ParameterKey': 'foo', + 'Parameters.member.Foo.Bar.ParameterValue': 'bar', + 'Parameters.member.Foo.Baz.ParameterKey': 'blarg', + 'Parameters.member.Foo.Baz.ParameterValue': 'wibble'} + params = self.controller._extract_user_params(p) + self.assertEqual(len(params), 2) + self.assertTrue('foo' in params) + self.assertEqual(params['foo'], 'bar') + self.assertTrue('blarg' in params) + self.assertEqual(params['blarg'], 'wibble') + + def test_params_extract_garbage(self): + p = {'Parameters.member.Foo.Bar.ParameterKey': 'foo', + 'Parameters.member.Foo.Bar.ParameterValue': 'bar', + 'Foo.Baz.ParameterKey': 'blarg', + 'Foo.Baz.ParameterValue': 'wibble'} + params = self.controller._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 = self.controller._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 = self.controller._extract_user_params(p) + self.assertFalse(params) + + # TODO : lots more StackController tests.. + + def setUp(self): + # Create WSGI controller instance + options = HeatConfigOpts() + self.controller = stacks.StackController(options) + print "setup complete" + + def tearDown(self): + print "teardown complete" + + +if __name__ == '__main__': + sys.argv.append(__file__) + nose.main() diff --git a/heat/tests/test_engine_api_utils.py b/heat/tests/test_engine_api_utils.py index 9dc589cbf8..8650a6bc39 100644 --- a/heat/tests/test_engine_api_utils.py +++ b/heat/tests/test_engine_api_utils.py @@ -23,69 +23,23 @@ import heat.engine.api as api @attr(tag=['unit', 'engine-api']) @attr(speed='fast') 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 = api.extract_user_params(p) - self.assertEqual(len(params), 2) - self.assertTrue('foo' in params) - self.assertEqual(params['foo'], 'bar') - self.assertTrue('blarg' in params) - self.assertEqual(params['blarg'], 'wibble') - - def test_params_extract_dots(self): - p = {'Parameters.member.Foo.Bar.ParameterKey': 'foo', - 'Parameters.member.Foo.Bar.ParameterValue': 'bar', - 'Parameters.member.Foo.Baz.ParameterKey': 'blarg', - 'Parameters.member.Foo.Baz.ParameterValue': 'wibble'} - params = api.extract_user_params(p) - self.assertEqual(len(params), 2) - self.assertTrue('foo' in params) - self.assertEqual(params['foo'], 'bar') - self.assertTrue('blarg' in params) - self.assertEqual(params['blarg'], 'wibble') - - def test_params_extract_garbage(self): - p = {'Parameters.member.Foo.Bar.ParameterKey': 'foo', - 'Parameters.member.Foo.Bar.ParameterValue': 'bar', - 'Foo.Baz.ParameterKey': 'blarg', - 'Foo.Baz.ParameterValue': 'wibble'} - 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 = 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 = api.extract_user_params(p) - self.assertFalse(params) - def test_timeout_extract(self): - p = {'TimeoutInMinutes': '5'} + p = {'timeout_mins': '5'} args = api.extract_args(p) self.assertEqual(args['timeout_mins'], 5) def test_timeout_extract_zero(self): - p = {'TimeoutInMinutes': '0'} + p = {'timeout_mins': '0'} args = api.extract_args(p) self.assertTrue('timeout_mins' not in args) def test_timeout_extract_garbage(self): - p = {'TimeoutInMinutes': 'wibble'} + p = {'timeout_mins': 'wibble'} args = api.extract_args(p) self.assertTrue('timeout_mins' not in args) def test_timeout_extract_none(self): - p = {'TimeoutInMinutes': None} + p = {'timeout_mins': None} args = api.extract_args(p) self.assertTrue('timeout_mins' not in args) diff --git a/heat/tests/test_stacks.py b/heat/tests/test_stacks.py index 40cbfb24eb..ec813cb737 100644 --- a/heat/tests/test_stacks.py +++ b/heat/tests/test_stacks.py @@ -172,63 +172,48 @@ class stackManagerTest(unittest.TestCase): self.assertEqual(len(events), 2) for ev in events: - self.assertTrue('EventId' in ev) - self.assertTrue(ev['EventId'] > 0) + self.assertTrue('event_id' in ev) + self.assertTrue(ev['event_id'] > 0) - self.assertTrue('LogicalResourceId' in ev) - self.assertEqual(ev['LogicalResourceId'], 'WebServer') + self.assertTrue('logical_resource_id' in ev) + self.assertEqual(ev['logical_resource_id'], 'WebServer') - self.assertTrue('PhysicalResourceId' in ev) + self.assertTrue('physical_resource_id' in ev) - self.assertTrue('ResourceProperties' in ev) + self.assertTrue('resource_properties' in ev) # Big long user data field.. it mentions 'wordpress' # a few times so this should work. - user_data = ev['ResourceProperties']['UserData'] + user_data = ev['resource_properties']['UserData'] self.assertNotEqual(user_data.find('wordpress'), -1) - self.assertEqual(ev['ResourceProperties']['ImageId'], + self.assertEqual(ev['resource_properties']['ImageId'], 'F16-x86_64-gold') - self.assertEqual(ev['ResourceProperties']['InstanceType'], + self.assertEqual(ev['resource_properties']['InstanceType'], 'm1.large') - self.assertTrue('ResourceStatus' in ev) - self.assertTrue(ev['ResourceStatus'] in ('IN_PROGRESS', + self.assertTrue('resource_status' in ev) + self.assertTrue(ev['resource_status'] in ('IN_PROGRESS', 'CREATE_COMPLETE')) - self.assertTrue('ResourceStatusReason' in ev) - self.assertEqual(ev['ResourceStatusReason'], 'state changed') + self.assertTrue('resource_status_reason' in ev) + self.assertEqual(ev['resource_status_reason'], 'state changed') - self.assertTrue('ResourceType' in ev) - self.assertEqual(ev['ResourceType'], 'AWS::EC2::Instance') + self.assertTrue('resource_type' in ev) + self.assertEqual(ev['resource_type'], 'AWS::EC2::Instance') - self.assertTrue('StackId' in ev) + self.assertTrue('stack_id' in ev) - self.assertTrue('StackName' in ev) - self.assertEqual(ev['StackName'], self.stack_name) + self.assertTrue('stack_name' in ev) + self.assertEqual(ev['stack_name'], self.stack_name) - self.assertTrue('Timestamp' in ev) - - def test_stack_list(self): - sl = self.man.list_stacks(self.ctx, {}) - - self.assertTrue(len(sl['stacks']) > 0) - for s in sl['stacks']: - self.assertTrue('CreationTime' 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('TemplateDescription' in s) - self.assertNotEqual(s['TemplateDescription'].find('WordPress'), -1) + self.assertTrue('event_time' in ev) def test_stack_describe_all(self): sl = self.man.show_stack(self.ctx, None, {}) self.assertEqual(len(sl['stacks']), 1) for s in sl['stacks']: - self.assertNotEqual(s['StackId'], None) - self.assertNotEqual(s['Description'].find('WordPress'), -1) + self.assertNotEqual(s['stack_id'], None) + self.assertNotEqual(s['description'].find('WordPress'), -1) def test_stack_describe_all_empty(self): self.tearDown() @@ -250,35 +235,35 @@ class stackManagerTest(unittest.TestCase): self.assertEqual(len(sl['stacks']), 1) s = sl['stacks'][0] - self.assertTrue('CreationTime' in s) - self.assertTrue('LastUpdatedTime' in s) - self.assertTrue('StackId' in s) - self.assertNotEqual(s['StackId'], None) - self.assertTrue('StackName' in s) - self.assertEqual(s['StackName'], self.stack_name) - self.assertTrue('StackStatus' in s) - self.assertTrue('StackStatusReason' in s) - self.assertTrue('Description' in s) - self.assertNotEqual(s['Description'].find('WordPress'), -1) - self.assertTrue('Parameters' in s) + self.assertTrue('creation_time' in s) + self.assertTrue('updated_time' in s) + self.assertTrue('stack_id' in s) + self.assertNotEqual(s['stack_id'], None) + self.assertTrue('stack_name' in s) + self.assertEqual(s['stack_name'], self.stack_name) + self.assertTrue('stack_status' in s) + self.assertTrue('stack_status_reason' in s) + self.assertTrue('description' in s) + self.assertNotEqual(s['description'].find('WordPress'), -1) + self.assertTrue('parameters' in s) def test_stack_resource_describe(self): r = self.man.describe_stack_resource(self.ctx, self.stack_name, 'WebServer') - self.assertTrue('Description' in r) - self.assertTrue('LastUpdatedTimestamp' in r) - self.assertTrue('StackId' in r) - self.assertNotEqual(r['StackId'], None) - self.assertTrue('StackName' in r) - self.assertEqual(r['StackName'], self.stack_name) - self.assertTrue('Metadata' in r) - self.assertTrue('ResourceStatus' in r) - self.assertTrue('ResourceStatusReason' in r) - self.assertTrue('ResourceType' in r) - self.assertTrue('PhysicalResourceId' in r) - self.assertTrue('LogicalResourceId' in r) - self.assertEqual(r['LogicalResourceId'], 'WebServer') + self.assertTrue('description' in r) + self.assertTrue('updated_time' in r) + self.assertTrue('stack_id' in r) + self.assertNotEqual(r['stack_id'], None) + self.assertTrue('stack_name' in r) + self.assertEqual(r['stack_name'], self.stack_name) + self.assertTrue('metadata' in r) + self.assertTrue('resource_status' in r) + self.assertTrue('resource_status_reason' in r) + self.assertTrue('resource_type' in r) + self.assertTrue('physical_resource_id' in r) + self.assertTrue('logical_resource_id' in r) + self.assertEqual(r['logical_resource_id'], 'WebServer') def test_stack_resource_describe_nonexist_stack(self): self.assertRaises(AttributeError, @@ -297,18 +282,18 @@ class stackManagerTest(unittest.TestCase): self.assertEqual(len(resources), 1) r = resources[0] - self.assertTrue('Description' in r) - self.assertTrue('Timestamp' in r) - self.assertTrue('StackId' in r) - self.assertNotEqual(r['StackId'], None) - self.assertTrue('StackName' in r) - self.assertEqual(r['StackName'], self.stack_name) - self.assertTrue('ResourceStatus' in r) - self.assertTrue('ResourceStatusReason' in r) - self.assertTrue('ResourceType' in r) - self.assertTrue('PhysicalResourceId' in r) - self.assertTrue('LogicalResourceId' in r) - self.assertEqual(r['LogicalResourceId'], 'WebServer') + self.assertTrue('description' in r) + self.assertTrue('updated_time' in r) + self.assertTrue('stack_id' in r) + self.assertNotEqual(r['stack_id'], None) + self.assertTrue('stack_name' in r) + self.assertEqual(r['stack_name'], self.stack_name) + self.assertTrue('resource_status' in r) + self.assertTrue('resource_status_reason' in r) + self.assertTrue('resource_type' in r) + self.assertTrue('physical_resource_id' in r) + self.assertTrue('logical_resource_id' in r) + self.assertEqual(r['logical_resource_id'], 'WebServer') def test_stack_resources_describe_no_filter(self): resources = self.man.describe_stack_resources(self.ctx, @@ -317,8 +302,8 @@ class stackManagerTest(unittest.TestCase): self.assertEqual(len(resources), 1) r = resources[0] - self.assertTrue('LogicalResourceId' in r) - self.assertEqual(r['LogicalResourceId'], 'WebServer') + self.assertTrue('logical_resource_id' in r) + self.assertEqual(r['logical_resource_id'], 'WebServer') def test_stack_resources_describe_bad_lookup(self): self.assertRaises(AttributeError, @@ -340,13 +325,13 @@ class stackManagerTest(unittest.TestCase): self.assertEqual(len(resources), 1) r = resources[0] - self.assertTrue('LastUpdatedTimestamp' in r) - self.assertTrue('PhysicalResourceId' in r) - self.assertTrue('LogicalResourceId' in r) - self.assertEqual(r['LogicalResourceId'], 'WebServer') - self.assertTrue('ResourceStatus' in r) - self.assertTrue('ResourceStatusReason' in r) - self.assertTrue('ResourceType' in r) + self.assertTrue('updated_time' in r) + self.assertTrue('physical_resource_id' in r) + self.assertTrue('logical_resource_id' in r) + self.assertEqual(r['logical_resource_id'], 'WebServer') + self.assertTrue('resource_status' in r) + self.assertTrue('resource_status_reason' in r) + self.assertTrue('resource_type' in r) def test_stack_resources_list_nonexist_stack(self): self.assertRaises(AttributeError,