95fe521a18
A stacks status is empty when originally entered into the database. This leads to returning empty status responses back if the engine is pre-occupied with other tasks. This patch sets a stack's status to CREATE_IN_PROGRESS by default when the stack object is created. The stack object is only ever created during create and preview and is loaded otherwise from the database in all the other scenarios. Change-Id: Iab0892896b398530423bbe6c6a623376bf86590e Closes-Bug: #1365399
986 lines
35 KiB
Python
986 lines
35 KiB
Python
#
|
|
# 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.
|
|
|
|
from datetime import datetime
|
|
import uuid
|
|
|
|
import json
|
|
import mock
|
|
import six
|
|
|
|
from heat.common.identifier import EventIdentifier
|
|
from heat.common import template_format
|
|
from heat.engine import api
|
|
from heat.engine.event import Event
|
|
from heat.engine import parameters
|
|
from heat.engine import parser
|
|
from heat.engine import resource
|
|
from heat.rpc import api as rpc_api
|
|
from heat.tests.common import HeatTestCase
|
|
from heat.tests import generic_resource as generic_rsrc
|
|
from heat.tests import utils
|
|
|
|
|
|
class FormatTest(HeatTestCase):
|
|
def setUp(self):
|
|
super(FormatTest, self).setUp()
|
|
|
|
template = parser.Template({
|
|
'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'generic1': {'Type': 'GenericResourceType'},
|
|
'generic2': {
|
|
'Type': 'GenericResourceType',
|
|
'DependsOn': 'generic1'}
|
|
}
|
|
})
|
|
resource._register_class('GenericResourceType',
|
|
generic_rsrc.GenericResource)
|
|
resource._register_class('ResWithComplexPropsAndAttrs',
|
|
generic_rsrc.ResWithComplexPropsAndAttrs)
|
|
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
|
|
template, stack_id=str(uuid.uuid4()))
|
|
|
|
def _dummy_event(self, event_id):
|
|
resource = self.stack['generic1']
|
|
return Event(utils.dummy_context(), self.stack, 'CREATE', 'COMPLETE',
|
|
'state changed', 'z3455xyc-9f88-404d-a85b-5315293e67de',
|
|
resource.properties, resource.name, resource.type(),
|
|
uuid='abc123yc-9f88-404d-a85b-531529456xyz',
|
|
id=event_id)
|
|
|
|
def test_format_stack_resource(self):
|
|
res = self.stack['generic1']
|
|
|
|
resource_keys = set((
|
|
rpc_api.RES_UPDATED_TIME,
|
|
rpc_api.RES_NAME,
|
|
rpc_api.RES_PHYSICAL_ID,
|
|
rpc_api.RES_ACTION,
|
|
rpc_api.RES_STATUS,
|
|
rpc_api.RES_STATUS_DATA,
|
|
rpc_api.RES_TYPE,
|
|
rpc_api.RES_ID,
|
|
rpc_api.RES_STACK_ID,
|
|
rpc_api.RES_STACK_NAME,
|
|
rpc_api.RES_REQUIRED_BY))
|
|
|
|
resource_details_keys = resource_keys.union(set(
|
|
(rpc_api.RES_DESCRIPTION, rpc_api.RES_METADATA)))
|
|
|
|
formatted = api.format_stack_resource(res, True)
|
|
self.assertEqual(resource_details_keys, set(formatted.keys()))
|
|
|
|
formatted = api.format_stack_resource(res, False)
|
|
self.assertEqual(resource_keys, set(formatted.keys()))
|
|
|
|
@mock.patch.object(api, 'format_resource_properties')
|
|
def test_format_stack_resource_with_props(self, mock_format_props):
|
|
mock_format_props.return_value = 'formatted_res_props'
|
|
res = self.stack['generic1']
|
|
|
|
formatted = api.format_stack_resource(res, True, with_props=True)
|
|
formatted_props = formatted[rpc_api.RES_SCHEMA_PROPERTIES]
|
|
self.assertEqual('formatted_res_props', formatted_props)
|
|
|
|
def _get_formatted_resource_properties(self, res_name):
|
|
tmpl = parser.Template(template_format.parse('''
|
|
heat_template_version: 2013-05-23
|
|
resources:
|
|
resource1:
|
|
type: ResWithComplexPropsAndAttrs
|
|
resource2:
|
|
type: ResWithComplexPropsAndAttrs
|
|
properties:
|
|
a_string: foobar
|
|
resource3:
|
|
type: ResWithComplexPropsAndAttrs
|
|
properties:
|
|
a_string: { get_attr: [ resource2, string] }
|
|
'''))
|
|
stack = parser.Stack(utils.dummy_context(), 'test_stack_for_preview',
|
|
tmpl, stack_id=str(uuid.uuid4()))
|
|
res = stack[res_name]
|
|
return api.format_resource_properties(res)
|
|
|
|
def test_format_resource_properties_empty(self):
|
|
props = self._get_formatted_resource_properties('resource1')
|
|
self.assertIsNone(props['a_string'])
|
|
self.assertIsNone(props['a_list'])
|
|
self.assertIsNone(props['a_map'])
|
|
|
|
def test_format_resource_properties_direct_props(self):
|
|
props = self._get_formatted_resource_properties('resource2')
|
|
self.assertEqual('foobar', props['a_string'])
|
|
|
|
def test_format_resource_properties_get_attr(self):
|
|
props = self._get_formatted_resource_properties('resource3')
|
|
self.assertEqual('', props['a_string'])
|
|
|
|
def test_format_stack_resource_with_nested_stack(self):
|
|
res = self.stack['generic1']
|
|
nested_id = {'foo': 'bar'}
|
|
res.nested = mock.Mock()
|
|
res.nested.return_value.identifier.return_value = nested_id
|
|
|
|
formatted = api.format_stack_resource(res, False)
|
|
self.assertEqual(nested_id, formatted[rpc_api.RES_NESTED_STACK_ID])
|
|
|
|
def test_format_stack_resource_with_nested_stack_none(self):
|
|
res = self.stack['generic1']
|
|
res.nested = mock.Mock()
|
|
res.nested.return_value = None
|
|
|
|
resource_keys = set((
|
|
rpc_api.RES_UPDATED_TIME,
|
|
rpc_api.RES_NAME,
|
|
rpc_api.RES_PHYSICAL_ID,
|
|
rpc_api.RES_ACTION,
|
|
rpc_api.RES_STATUS,
|
|
rpc_api.RES_STATUS_DATA,
|
|
rpc_api.RES_TYPE,
|
|
rpc_api.RES_ID,
|
|
rpc_api.RES_STACK_ID,
|
|
rpc_api.RES_STACK_NAME,
|
|
rpc_api.RES_REQUIRED_BY))
|
|
|
|
formatted = api.format_stack_resource(res, False)
|
|
self.assertEqual(resource_keys, set(formatted.keys()))
|
|
|
|
def test_format_stack_resource_required_by(self):
|
|
res1 = api.format_stack_resource(self.stack['generic1'])
|
|
res2 = api.format_stack_resource(self.stack['generic2'])
|
|
self.assertEqual(['generic2'], res1['required_by'])
|
|
self.assertEqual([], res2['required_by'])
|
|
|
|
def test_format_stack_resource_with_parent_stack(self):
|
|
res = self.stack['generic1']
|
|
res.stack.parent_resource = mock.Mock()
|
|
res.stack.parent_resource.name = 'foobar'
|
|
|
|
formatted = api.format_stack_resource(res, False)
|
|
self.assertEqual('foobar', formatted[rpc_api.RES_PARENT_RESOURCE])
|
|
|
|
def test_format_event_identifier_uuid(self):
|
|
self._test_format_event('abc123yc-9f88-404d-a85b-531529456xyz')
|
|
|
|
def _test_format_event(self, event_id):
|
|
event = self._dummy_event(event_id)
|
|
|
|
event_keys = set((
|
|
rpc_api.EVENT_ID,
|
|
rpc_api.EVENT_STACK_ID,
|
|
rpc_api.EVENT_STACK_NAME,
|
|
rpc_api.EVENT_TIMESTAMP,
|
|
rpc_api.EVENT_RES_NAME,
|
|
rpc_api.EVENT_RES_PHYSICAL_ID,
|
|
rpc_api.EVENT_RES_ACTION,
|
|
rpc_api.EVENT_RES_STATUS,
|
|
rpc_api.EVENT_RES_STATUS_DATA,
|
|
rpc_api.EVENT_RES_TYPE,
|
|
rpc_api.EVENT_RES_PROPERTIES))
|
|
|
|
formatted = api.format_event(event)
|
|
self.assertEqual(event_keys, set(formatted.keys()))
|
|
|
|
event_id_formatted = formatted[rpc_api.EVENT_ID]
|
|
event_identifier = EventIdentifier(event_id_formatted['tenant'],
|
|
event_id_formatted['stack_name'],
|
|
event_id_formatted['stack_id'],
|
|
event_id_formatted['path'])
|
|
self.assertEqual(event_id, event_identifier.event_id)
|
|
|
|
@mock.patch.object(api, 'format_stack_resource')
|
|
def test_format_stack_preview(self, mock_fmt_resource):
|
|
def mock_format_resources(res, **kwargs):
|
|
return 'fmt%s' % res
|
|
|
|
mock_fmt_resource.side_effect = mock_format_resources
|
|
resources = [1, [2, [3]]]
|
|
self.stack.preview_resources = mock.Mock(return_value=resources)
|
|
|
|
stack = api.format_stack_preview(self.stack)
|
|
self.assertIsInstance(stack, dict)
|
|
self.assertIsNone(stack.get('status'))
|
|
self.assertIsNone(stack.get('action'))
|
|
self.assertIsNone(stack.get('status_reason'))
|
|
self.assertEqual('test_stack', stack['stack_name'])
|
|
self.assertIn('resources', stack)
|
|
self.assertEqual(['fmt1', ['fmt2', ['fmt3']]], stack['resources'])
|
|
|
|
kwargs = mock_fmt_resource.call_args[1]
|
|
self.assertTrue(kwargs['with_props'])
|
|
|
|
def test_format_stack(self):
|
|
self.stack.created_time = datetime(1970, 1, 1)
|
|
info = api.format_stack(self.stack)
|
|
|
|
aws_id = ('arn:openstack:heat::test_tenant_id:'
|
|
'stacks/test_stack/' + self.stack.id)
|
|
expected_stack_info = {
|
|
'capabilities': [],
|
|
'creation_time': '1970-01-01T00:00:00Z',
|
|
'description': 'No description',
|
|
'disable_rollback': True,
|
|
'notification_topics': [],
|
|
'stack_action': 'CREATE',
|
|
'stack_name': 'test_stack',
|
|
'stack_owner': 'test_username',
|
|
'stack_status': 'IN_PROGRESS',
|
|
'stack_status_reason': '',
|
|
'template_description': 'No description',
|
|
'timeout_mins': None,
|
|
'parameters': {
|
|
'AWS::Region': 'ap-southeast-1',
|
|
'AWS::StackId': aws_id,
|
|
'AWS::StackName': 'test_stack'},
|
|
'stack_identity': {
|
|
'path': '',
|
|
'stack_id': self.stack.id,
|
|
'stack_name': 'test_stack',
|
|
'tenant': 'test_tenant_id'},
|
|
'updated_time': None,
|
|
'parent': None}
|
|
self.assertEqual(expected_stack_info, info)
|
|
|
|
def test_format_stack_created_time(self):
|
|
self.stack.created_time = None
|
|
info = api.format_stack(self.stack)
|
|
self.assertIsNotNone(info['creation_time'])
|
|
|
|
def test_format_stack_updated_time(self):
|
|
self.stack.updated_time = None
|
|
info = api.format_stack(self.stack)
|
|
self.assertIsNone(info['updated_time'])
|
|
|
|
self.stack.updated_time = datetime(1970, 1, 1)
|
|
info = api.format_stack(self.stack)
|
|
self.assertEqual('1970-01-01T00:00:00Z', info['updated_time'])
|
|
|
|
@mock.patch.object(api, 'format_stack_outputs')
|
|
def test_format_stack_adds_outputs(self, mock_fmt_outputs):
|
|
mock_fmt_outputs.return_value = 'foobar'
|
|
self.stack.action = 'CREATE'
|
|
self.stack.status = 'COMPLETE'
|
|
info = api.format_stack(self.stack)
|
|
self.assertEqual('foobar', info[rpc_api.STACK_OUTPUTS])
|
|
|
|
def test_format_stack_outputs(self):
|
|
template = parser.Template({
|
|
'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'generic': {'Type': 'GenericResourceType'}
|
|
},
|
|
'Outputs': {
|
|
'correct_output': {
|
|
'Description': 'Good output',
|
|
'Value': {'Fn::GetAtt': ['generic', 'Foo']}
|
|
},
|
|
'incorrect_output': {
|
|
'Value': {'Fn::GetAtt': ['generic', 'Bar']}
|
|
}
|
|
}
|
|
})
|
|
stack = parser.Stack(utils.dummy_context(), 'test_stack',
|
|
template, stack_id=str(uuid.uuid4()))
|
|
stack.action = 'CREATE'
|
|
stack.status = 'COMPLETE'
|
|
stack['generic'].action = 'CREATE'
|
|
stack['generic'].status = 'COMPLETE'
|
|
info = api.format_stack_outputs(stack, stack.outputs)
|
|
expected = [{'description': 'No description given',
|
|
'output_error': 'The Referenced Attribute (generic Bar) '
|
|
'is incorrect.',
|
|
'output_key': 'incorrect_output',
|
|
'output_value': None},
|
|
{'description': 'Good output',
|
|
'output_key': 'correct_output',
|
|
'output_value': 'generic'}]
|
|
|
|
self.assertEqual(expected, info)
|
|
|
|
|
|
class FormatValidateParameterTest(HeatTestCase):
|
|
|
|
base_template = '''
|
|
{
|
|
"AWSTemplateFormatVersion" : "2010-09-09",
|
|
"Description" : "test",
|
|
"Parameters" : {
|
|
%s
|
|
}
|
|
}
|
|
'''
|
|
|
|
base_template_hot = '''
|
|
{
|
|
"heat_template_version" : "2013-05-23",
|
|
"description" : "test",
|
|
"parameters" : {
|
|
%s
|
|
}
|
|
}
|
|
'''
|
|
|
|
scenarios = [
|
|
('simple',
|
|
dict(template=base_template,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"Type": "String",
|
|
"Description": "Name of SSH key pair"
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('default',
|
|
dict(template=base_template,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"Type": "String",
|
|
"Description": "Name of SSH key pair",
|
|
"Default": "dummy"
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'Default': 'dummy',
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('min_length_constraint',
|
|
dict(template=base_template,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"Type": "String",
|
|
"Description": "Name of SSH key pair",
|
|
"MinLength": 4
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MinLength': 4,
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('max_length_constraint',
|
|
dict(template=base_template,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"Type": "String",
|
|
"Description": "Name of SSH key pair",
|
|
"MaxLength": 10
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MaxLength': 10,
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('min_max_length_constraint',
|
|
dict(template=base_template,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"Type": "String",
|
|
"Description": "Name of SSH key pair",
|
|
"MinLength": 4,
|
|
"MaxLength": 10
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MinLength': 4,
|
|
'MaxLength': 10,
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('min_value_constraint',
|
|
dict(template=base_template,
|
|
param_name='MyNumber',
|
|
param='''
|
|
"MyNumber": {
|
|
"Type": "Number",
|
|
"Description": "A number",
|
|
"MinValue": 4
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'Number',
|
|
'Description': 'A number',
|
|
'MinValue': 4,
|
|
'NoEcho': 'false',
|
|
'Label': 'MyNumber'
|
|
})
|
|
),
|
|
('max_value_constraint',
|
|
dict(template=base_template,
|
|
param_name='MyNumber',
|
|
param='''
|
|
"MyNumber": {
|
|
"Type": "Number",
|
|
"Description": "A number",
|
|
"MaxValue": 10
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'Number',
|
|
'Description': 'A number',
|
|
'MaxValue': 10,
|
|
'NoEcho': 'false',
|
|
'Label': 'MyNumber'
|
|
})
|
|
),
|
|
('min_max_value_constraint',
|
|
dict(template=base_template,
|
|
param_name='MyNumber',
|
|
param='''
|
|
"MyNumber": {
|
|
"Type": "Number",
|
|
"Description": "A number",
|
|
"MinValue": 4,
|
|
"MaxValue": 10
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'Number',
|
|
'Description': 'A number',
|
|
'MinValue': 4,
|
|
'MaxValue': 10,
|
|
'NoEcho': 'false',
|
|
'Label': 'MyNumber'
|
|
})
|
|
),
|
|
('allowed_values_constraint',
|
|
dict(template=base_template,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"Type": "String",
|
|
"Description": "Name of SSH key pair",
|
|
"AllowedValues": [ "foo", "bar", "blub" ]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'AllowedValues': ['foo', 'bar', 'blub'],
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('allowed_pattern_constraint',
|
|
dict(template=base_template,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"Type": "String",
|
|
"Description": "Name of SSH key pair",
|
|
"AllowedPattern": "[a-zA-Z0-9]+"
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'AllowedPattern': "[a-zA-Z0-9]+",
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('multiple_constraints',
|
|
dict(template=base_template,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"Type": "String",
|
|
"Description": "Name of SSH key pair",
|
|
"MinLength": 4,
|
|
"MaxLength": 10,
|
|
"AllowedValues": [
|
|
"foo", "bar", "blub"
|
|
],
|
|
"AllowedPattern": "[a-zA-Z0-9]+"
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MinLength': 4,
|
|
'MaxLength': 10,
|
|
'AllowedValues': ['foo', 'bar', 'blub'],
|
|
'AllowedPattern': "[a-zA-Z0-9]+",
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('simple_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair"
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('default_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"default": "dummy"
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'Default': 'dummy',
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('min_length_constraint_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"constraints": [
|
|
{ "length": { "min": 4} }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MinLength': 4,
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('max_length_constraint_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"constraints": [
|
|
{ "length": { "max": 10} }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MaxLength': 10,
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('min_max_length_constraint_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"constraints": [
|
|
{ "length": { "min":4, "max": 10} }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MinLength': 4,
|
|
'MaxLength': 10,
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('min_value_constraint_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='MyNumber',
|
|
param='''
|
|
"MyNumber": {
|
|
"type": "number",
|
|
"description": "A number",
|
|
"constraints": [
|
|
{ "range": { "min": 4} }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'Number',
|
|
'Description': 'A number',
|
|
'MinValue': 4,
|
|
'NoEcho': 'false',
|
|
'Label': 'MyNumber'
|
|
})
|
|
),
|
|
('max_value_constraint_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='MyNumber',
|
|
param='''
|
|
"MyNumber": {
|
|
"type": "number",
|
|
"description": "A number",
|
|
"constraints": [
|
|
{ "range": { "max": 10} }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'Number',
|
|
'Description': 'A number',
|
|
'MaxValue': 10,
|
|
'NoEcho': 'false',
|
|
'Label': 'MyNumber'
|
|
})
|
|
),
|
|
('min_max_value_constraint_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='MyNumber',
|
|
param='''
|
|
"MyNumber": {
|
|
"type": "number",
|
|
"description": "A number",
|
|
"constraints": [
|
|
{ "range": { "min": 4, "max": 10} }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'Number',
|
|
'Description': 'A number',
|
|
'MinValue': 4,
|
|
'MaxValue': 10,
|
|
'NoEcho': 'false',
|
|
'Label': 'MyNumber'
|
|
})
|
|
),
|
|
('allowed_values_constraint_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"constraints": [
|
|
{ "allowed_values": [
|
|
"foo", "bar", "blub"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'AllowedValues': ['foo', 'bar', 'blub'],
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('allowed_pattern_constraint_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"constraints": [
|
|
{ "allowed_pattern": "[a-zA-Z0-9]+" }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'AllowedPattern': "[a-zA-Z0-9]+",
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('multiple_constraints_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"constraints": [
|
|
{ "length": { "min": 4, "max": 10} },
|
|
{ "allowed_values": [
|
|
"foo", "bar", "blub"
|
|
]
|
|
},
|
|
{ "allowed_pattern": "[a-zA-Z0-9]+" }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MinLength': 4,
|
|
'MaxLength': 10,
|
|
'AllowedValues': ['foo', 'bar', 'blub'],
|
|
'AllowedPattern': "[a-zA-Z0-9]+",
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('constraint_description_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"constraints": [
|
|
{ "length": { "min": 4},
|
|
"description": "Big enough" }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MinLength': 4,
|
|
'ConstraintDescription': 'Big enough',
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('constraint_multiple_descriptions_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Name of SSH key pair",
|
|
"constraints": [
|
|
{ "length": { "min": 4},
|
|
"description": "Big enough." },
|
|
{ "allowed_pattern": "[a-zA-Z0-9]+",
|
|
"description": "Only letters." }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Name of SSH key pair',
|
|
'MinLength': 4,
|
|
'AllowedPattern': "[a-zA-Z0-9]+",
|
|
'ConstraintDescription': 'Big enough. Only letters.',
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName'
|
|
})
|
|
),
|
|
('constraint_custom_hot',
|
|
dict(template=base_template_hot,
|
|
param_name='KeyName',
|
|
param='''
|
|
"KeyName": {
|
|
"type": "string",
|
|
"description": "Public Network",
|
|
"constraints": [
|
|
{ "custom_constraint": "neutron.network" }
|
|
]
|
|
}
|
|
''',
|
|
expected={
|
|
'Type': 'String',
|
|
'Description': 'Public Network',
|
|
'NoEcho': 'false',
|
|
'Label': 'KeyName',
|
|
'CustomConstraint': 'neutron.network'
|
|
})
|
|
)
|
|
]
|
|
|
|
def test_format_validate_parameter(self):
|
|
"""
|
|
Test format of a parameter.
|
|
"""
|
|
|
|
t = template_format.parse(self.template % self.param)
|
|
tmpl = parser.Template(t)
|
|
|
|
tmpl_params = parameters.Parameters(None, tmpl)
|
|
tmpl_params.validate(validate_value=False)
|
|
param = tmpl_params.params[self.param_name]
|
|
param_formated = api.format_validate_parameter(param)
|
|
self.assertEqual(self.expected, param_formated)
|
|
|
|
|
|
class FormatSoftwareConfigDeploymentTest(HeatTestCase):
|
|
|
|
def _dummy_software_config(self):
|
|
config = mock.Mock()
|
|
config.name = 'config_mysql'
|
|
config.group = 'Heat::Shell'
|
|
config.id = str(uuid.uuid4())
|
|
config.config = {
|
|
'inputs': [{'name': 'bar'}],
|
|
'outputs': [{'name': 'result'}],
|
|
'options': {},
|
|
'config': '#!/bin/bash\n'
|
|
}
|
|
return config
|
|
|
|
def _dummy_software_deployment(self):
|
|
config = self._dummy_software_config()
|
|
deployment = mock.Mock()
|
|
deployment.config = config
|
|
deployment.id = str(uuid.uuid4())
|
|
deployment.server_id = str(uuid.uuid4())
|
|
deployment.input_values = {'bar': 'baaaaa'}
|
|
deployment.output_values = {'result': '0'}
|
|
deployment.action = 'INIT'
|
|
deployment.status = 'COMPLETE'
|
|
deployment.status_reason = 'Because'
|
|
return deployment
|
|
|
|
def test_format_software_config(self):
|
|
config = self._dummy_software_config()
|
|
result = api.format_software_config(config)
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual([{'name': 'bar'}], result['inputs'])
|
|
self.assertEqual([{'name': 'result'}], result['outputs'])
|
|
self.assertEqual({}, result['options'])
|
|
|
|
def test_format_software_config_none(self):
|
|
self.assertIsNone(api.format_software_config(None))
|
|
|
|
def test_format_software_deployment(self):
|
|
deployment = self._dummy_software_deployment()
|
|
result = api.format_software_deployment(deployment)
|
|
self.assertIsNotNone(result)
|
|
self.assertEqual(deployment.id, result['id'])
|
|
self.assertEqual(deployment.config.id, result['config_id'])
|
|
self.assertEqual(deployment.server_id, result['server_id'])
|
|
self.assertEqual(deployment.input_values, result['input_values'])
|
|
self.assertEqual(deployment.output_values, result['output_values'])
|
|
self.assertEqual(deployment.action, result['action'])
|
|
self.assertEqual(deployment.status, result['status'])
|
|
self.assertEqual(deployment.status_reason, result['status_reason'])
|
|
|
|
def test_format_software_deployment_none(self):
|
|
self.assertIsNone(api.format_software_deployment(None))
|
|
|
|
|
|
class TestExtractArgs(HeatTestCase):
|
|
def test_timeout_extract(self):
|
|
p = {'timeout_mins': '5'}
|
|
args = api.extract_args(p)
|
|
self.assertEqual(5, args['timeout_mins'])
|
|
|
|
def test_timeout_extract_zero(self):
|
|
p = {'timeout_mins': '0'}
|
|
args = api.extract_args(p)
|
|
self.assertNotIn('timeout_mins', args)
|
|
|
|
def test_timeout_extract_garbage(self):
|
|
p = {'timeout_mins': 'wibble'}
|
|
args = api.extract_args(p)
|
|
self.assertNotIn('timeout_mins', args)
|
|
|
|
def test_timeout_extract_none(self):
|
|
p = {'timeout_mins': None}
|
|
args = api.extract_args(p)
|
|
self.assertNotIn('timeout_mins', args)
|
|
|
|
def test_timeout_extract_negative(self):
|
|
p = {'timeout_mins': '-100'}
|
|
error = self.assertRaises(ValueError, api.extract_args, p)
|
|
self.assertIn('Invalid timeout value', six.text_type(error))
|
|
|
|
def test_timeout_extract_not_present(self):
|
|
args = api.extract_args({})
|
|
self.assertNotIn('timeout_mins', args)
|
|
|
|
def test_adopt_stack_data_extract_present(self):
|
|
p = {'adopt_stack_data': json.dumps({'Resources': {}})}
|
|
args = api.extract_args(p)
|
|
self.assertTrue(args.get('adopt_stack_data'))
|
|
|
|
def test_invalid_adopt_stack_data(self):
|
|
p = {'adopt_stack_data': json.dumps("foo")}
|
|
error = self.assertRaises(ValueError, api.extract_args, p)
|
|
self.assertEqual(
|
|
'Unexpected adopt data "foo". Adopt data must be a dict.',
|
|
six.text_type(error))
|
|
|
|
def test_adopt_stack_data_extract_not_present(self):
|
|
args = api.extract_args({})
|
|
self.assertNotIn('adopt_stack_data', args)
|
|
|
|
def test_disable_rollback_extract_true(self):
|
|
args = api.extract_args({'disable_rollback': True})
|
|
self.assertIn('disable_rollback', args)
|
|
self.assertTrue(args.get('disable_rollback'))
|
|
|
|
args = api.extract_args({'disable_rollback': 'True'})
|
|
self.assertIn('disable_rollback', args)
|
|
self.assertTrue(args.get('disable_rollback'))
|
|
|
|
args = api.extract_args({'disable_rollback': 'true'})
|
|
self.assertIn('disable_rollback', args)
|
|
self.assertTrue(args.get('disable_rollback'))
|
|
|
|
def test_disable_rollback_extract_false(self):
|
|
args = api.extract_args({'disable_rollback': False})
|
|
self.assertIn('disable_rollback', args)
|
|
self.assertFalse(args.get('disable_rollback'))
|
|
|
|
args = api.extract_args({'disable_rollback': 'False'})
|
|
self.assertIn('disable_rollback', args)
|
|
self.assertFalse(args.get('disable_rollback'))
|
|
|
|
args = api.extract_args({'disable_rollback': 'false'})
|
|
self.assertIn('disable_rollback', args)
|
|
self.assertFalse(args.get('disable_rollback'))
|
|
|
|
def test_disable_rollback_extract_bad(self):
|
|
self.assertRaises(ValueError, api.extract_args,
|
|
{'disable_rollback': 'bad'})
|