deb-heat/heat/tests/test_nested_stack.py
Liang Chen a2d04996ae Tidy up urlfetch.py exception handling
IOError doesn't seem appropiate for things like invalid URL scheme
and template exceeding max size. And requests.RequestException means
an ambiguous exception occurred while handling requests, not
necessarily an IO error.

Change-Id: Ib8108a5b9bf6d6c99545a1c29735521720d3a75a
2014-04-11 20:47:15 +08:00

674 lines
22 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.
import copy
import json
import mock
from requests import exceptions
from oslo.config import cfg
cfg.CONF.import_opt('max_resources_per_stack', 'heat.common.config')
from heat.common import exception
from heat.common import template_format
from heat.common import urlfetch
from heat.db import api as db_api
from heat.engine import parser
from heat.engine import resource
from heat.engine import scheduler
from heat.tests.common import HeatTestCase
from heat.tests import generic_resource as generic_rsrc
from heat.tests import utils
class NestedStackTest(HeatTestCase):
test_template = '''
HeatTemplateFormatVersion: '2012-12-12'
Resources:
the_nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: https://server.test/the.template
Parameters:
KeyName: foo
'''
nested_template = '''
HeatTemplateFormatVersion: '2012-12-12'
Parameters:
KeyName:
Type: String
Outputs:
Foo:
Value: bar
'''
update_template = '''
HeatTemplateFormatVersion: '2012-12-12'
Parameters:
KeyName:
Type: String
Outputs:
Bar:
Value: foo
'''
def setUp(self):
super(NestedStackTest, self).setUp()
self.m.StubOutWithMock(urlfetch, 'get')
utils.setup_dummy_db()
def create_stack(self, template):
t = template_format.parse(template)
stack = self.parse_stack(t)
stack.create()
self.assertEqual((stack.CREATE, stack.COMPLETE), stack.state)
return stack
def adopt_stack(self, template, adopt_data):
t = template_format.parse(template)
stack = self.parse_stack(t, adopt_data)
stack.adopt()
self.assertEqual((stack.ADOPT, stack.COMPLETE), stack.state)
return stack
def parse_stack(self, t, data=None):
ctx = utils.dummy_context('test_username', 'aaaa', 'password')
stack_name = 'test_stack'
tmpl = parser.Template(t)
stack = parser.Stack(ctx, stack_name, tmpl, adopt_stack_data=data)
stack.store()
return stack
def test_nested_stack_create(self):
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn(self.nested_template)
self.m.ReplayAll()
stack = self.create_stack(self.test_template)
rsrc = stack['the_nested']
nested_name = utils.PhysName(stack.name, 'the_nested')
self.assertEqual(rsrc.physical_resource_name(), nested_name)
arn_prefix = ('arn:openstack:heat::aaaa:stacks/%s/' %
rsrc.physical_resource_name())
self.assertTrue(rsrc.FnGetRefId().startswith(arn_prefix))
self.assertEqual('bar', rsrc.FnGetAtt('Outputs.Foo'))
self.assertRaises(
exception.InvalidTemplateAttribute, rsrc.FnGetAtt, 'Foo')
self.assertRaises(
exception.InvalidTemplateAttribute, rsrc.FnGetAtt, 'Outputs.Bar')
self.assertRaises(
exception.InvalidTemplateAttribute, rsrc.FnGetAtt, 'Bar')
rsrc.delete()
self.assertTrue(rsrc.FnGetRefId().startswith(arn_prefix))
self.m.VerifyAll()
def test_nested_stack_adopt(self):
resource._register_class('GenericResource',
generic_rsrc.GenericResource)
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn('''
HeatTemplateFormatVersion: '2012-12-12'
Parameters:
KeyName:
Type: String
Resources:
NestedResource:
Type: GenericResource
Outputs:
Foo:
Value: bar
''')
self.m.ReplayAll()
adopt_data = {
"resources": {
"the_nested": {
"resource_id": "test-res-id",
"resources": {
"NestedResource": {
"resource_id": "test-nested-res-id"
}
}
}
}
}
stack = self.adopt_stack(self.test_template, adopt_data)
self.assertEqual((stack.ADOPT, stack.COMPLETE), stack.state)
rsrc = stack['the_nested']
self.assertEqual((rsrc.ADOPT, rsrc.COMPLETE), rsrc.state)
nested_name = utils.PhysName(stack.name, 'the_nested')
self.assertEqual(nested_name, rsrc.physical_resource_name())
self.assertEqual('test-nested-res-id',
rsrc.nested()['NestedResource'].resource_id)
rsrc.delete()
self.m.VerifyAll()
def test_nested_stack_adopt_fail(self):
resource._register_class('GenericResource',
generic_rsrc.GenericResource)
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn('''
HeatTemplateFormatVersion: '2012-12-12'
Parameters:
KeyName:
Type: String
Resources:
NestedResource:
Type: GenericResource
Outputs:
Foo:
Value: bar
''')
self.m.ReplayAll()
adopt_data = {
"resources": {
"the_nested": {
"resource_id": "test-res-id",
"resources": {
}
}
}
}
stack = self.adopt_stack(self.test_template, adopt_data)
rsrc = stack['the_nested']
self.assertEqual((rsrc.ADOPT, rsrc.FAILED), rsrc.nested().state)
nested_name = utils.PhysName(stack.name, 'the_nested')
self.assertEqual(nested_name, rsrc.physical_resource_name())
rsrc.delete()
self.m.VerifyAll()
def test_nested_stack_create_with_timeout(self):
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn(self.nested_template)
self.m.ReplayAll()
timeout_template = template_format.parse(
copy.deepcopy(self.test_template))
props = timeout_template['Resources']['the_nested']['Properties']
props['TimeoutInMinutes'] = '50'
stack = self.create_stack(json.dumps(timeout_template))
self.assertEqual((stack.CREATE, stack.COMPLETE), stack.state)
self.m.VerifyAll()
def test_nested_stack_create_exceeds_resource_limit(self):
cfg.CONF.set_override('max_resources_per_stack', 1)
resource._register_class('GenericResource',
generic_rsrc.GenericResource)
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn('''
HeatTemplateFormatVersion: '2012-12-12'
Parameters:
KeyName:
Type: String
Resources:
NestedResource:
Type: GenericResource
Outputs:
Foo:
Value: bar
''')
self.m.ReplayAll()
t = template_format.parse(self.test_template)
stack = self.parse_stack(t)
stack.create()
self.assertEqual((stack.CREATE, stack.FAILED), stack.state)
self.assertIn('Maximum resources per stack exceeded',
stack.status_reason)
self.m.VerifyAll()
def test_nested_stack_create_equals_resource_limit(self):
cfg.CONF.set_override('max_resources_per_stack', 2)
resource._register_class('GenericResource',
generic_rsrc.GenericResource)
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn('''
HeatTemplateFormatVersion: '2012-12-12'
Parameters:
KeyName:
Type: String
Resources:
NestedResource:
Type: GenericResource
Outputs:
Foo:
Value: bar
''')
self.m.ReplayAll()
t = template_format.parse(self.test_template)
stack = self.parse_stack(t)
stack.create()
self.assertEqual((stack.CREATE, stack.COMPLETE), stack.state)
self.assertIn('NestedResource',
stack['the_nested'].nested())
self.m.VerifyAll()
def test_nested_stack_update(self):
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn(self.nested_template)
urlfetch.get('https://server.test/new.template').MultipleTimes().\
AndReturn(self.update_template)
self.m.ReplayAll()
stack = self.create_stack(self.test_template)
rsrc = stack['the_nested']
original_nested_id = rsrc.resource_id
t = template_format.parse(self.test_template)
new_res = copy.deepcopy(t['Resources']['the_nested'])
new_res['Properties']['TemplateURL'] = (
'https://server.test/new.template')
prop_diff = {'TemplateURL': 'https://server.test/new.template'}
updater = rsrc.handle_update(new_res, {}, prop_diff)
updater.run_to_completion()
self.assertIs(True, rsrc.check_update_complete(updater))
# Expect the physical resource name staying the same after update,
# so that the nested was actually updated instead of replaced.
self.assertEqual(original_nested_id, rsrc.resource_id)
db_nested = db_api.stack_get(stack.context,
rsrc.resource_id)
# Owner_id should be preserved during the update process.
self.assertEqual(stack.id, db_nested.owner_id)
self.assertEqual('foo', rsrc.FnGetAtt('Outputs.Bar'))
self.assertRaises(
exception.InvalidTemplateAttribute, rsrc.FnGetAtt, 'Foo')
self.assertRaises(
exception.InvalidTemplateAttribute, rsrc.FnGetAtt, 'Outputs.Foo')
self.assertRaises(
exception.InvalidTemplateAttribute, rsrc.FnGetAtt, 'Bar')
rsrc.delete()
self.m.VerifyAll()
def test_nested_stack_update_equals_resource_limit(self):
resource._register_class('GenericResource',
generic_rsrc.GenericResource)
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn(self.nested_template)
urlfetch.get('https://server.test/new.template').MultipleTimes().\
AndReturn('''
HeatTemplateFormatVersion: '2012-12-12'
Parameters:
KeyName:
Type: String
Resources:
NestedResource:
Type: GenericResource
Outputs:
Bar:
Value: foo
''')
self.m.ReplayAll()
stack = self.create_stack(self.test_template)
cfg.CONF.set_override('max_resources_per_stack', 2)
rsrc = stack['the_nested']
t = template_format.parse(self.test_template)
new_res = copy.deepcopy(t['Resources']['the_nested'])
new_res['Properties']['TemplateURL'] = (
'https://server.test/new.template')
prop_diff = {'TemplateURL': 'https://server.test/new.template'}
updater = rsrc.handle_update(new_res, {}, prop_diff)
updater.run_to_completion()
self.assertIs(True, rsrc.check_update_complete(updater))
self.assertEqual('foo', rsrc.FnGetAtt('Outputs.Bar'))
rsrc.delete()
self.m.VerifyAll()
def test_nested_stack_update_exceeds_limit(self):
resource._register_class('GenericResource',
generic_rsrc.GenericResource)
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn(self.nested_template)
urlfetch.get('https://server.test/new.template').MultipleTimes().\
AndReturn('''
HeatTemplateFormatVersion: '2012-12-12'
Parameters:
KeyName:
Type: String
Resources:
NestedResource:
Type: GenericResource
Outputs:
Bar:
Value: foo
''')
self.m.ReplayAll()
stack = self.create_stack(self.test_template)
cfg.CONF.set_override('max_resources_per_stack', 1)
rsrc = stack['the_nested']
t = template_format.parse(self.test_template)
new_res = copy.deepcopy(t['Resources']['the_nested'])
new_res['Properties']['TemplateURL'] = (
'https://server.test/new.template')
prop_diff = {'TemplateURL': 'https://server.test/new.template'}
ex = self.assertRaises(exception.RequestLimitExceeded,
rsrc.handle_update, new_res, {}, prop_diff)
self.assertIn(exception.StackResourceLimitExceeded.msg_fmt,
str(ex))
rsrc.delete()
self.m.VerifyAll()
def test_nested_stack_suspend_resume(self):
urlfetch.get('https://server.test/the.template').AndReturn(
self.nested_template)
self.m.ReplayAll()
stack = self.create_stack(self.test_template)
rsrc = stack['the_nested']
scheduler.TaskRunner(rsrc.suspend)()
self.assertEqual((rsrc.SUSPEND, rsrc.COMPLETE), rsrc.state)
scheduler.TaskRunner(rsrc.resume)()
self.assertEqual((rsrc.RESUME, rsrc.COMPLETE), rsrc.state)
rsrc.delete()
self.m.VerifyAll()
def test_nested_stack_three_deep(self):
root_template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth1.template'
'''
depth1_template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth2.template'
'''
depth2_template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth3.template'
Parameters:
KeyName: foo
'''
urlfetch.get(
'https://server.test/depth1.template').AndReturn(
depth1_template)
urlfetch.get(
'https://server.test/depth2.template').AndReturn(
depth2_template)
urlfetch.get(
'https://server.test/depth3.template').AndReturn(
self.nested_template)
self.m.ReplayAll()
self.create_stack(root_template)
self.m.VerifyAll()
def test_nested_stack_four_deep(self):
root_template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth1.template'
'''
depth1_template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth2.template'
'''
depth2_template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth3.template'
'''
depth3_template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth4.template'
Parameters:
KeyName: foo
'''
urlfetch.get(
'https://server.test/depth1.template').AndReturn(
depth1_template)
urlfetch.get(
'https://server.test/depth2.template').AndReturn(
depth2_template)
urlfetch.get(
'https://server.test/depth3.template').AndReturn(
depth3_template)
urlfetch.get(
'https://server.test/depth4.template').AndReturn(
self.nested_template)
self.m.ReplayAll()
t = template_format.parse(root_template)
stack = self.parse_stack(t)
stack.create()
self.assertEqual((stack.CREATE, stack.FAILED), stack.state)
self.assertIn('Recursion depth exceeds', stack.status_reason)
self.m.VerifyAll()
def test_nested_stack_four_wide(self):
root_template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth1.template'
Parameters:
KeyName: foo
Nested2:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth2.template'
Parameters:
KeyName: foo
Nested3:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth3.template'
Parameters:
KeyName: foo
Nested4:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/depth4.template'
Parameters:
KeyName: foo
'''
urlfetch.get(
'https://server.test/depth1.template').InAnyOrder().AndReturn(
self.nested_template)
urlfetch.get(
'https://server.test/depth2.template').InAnyOrder().AndReturn(
self.nested_template)
urlfetch.get(
'https://server.test/depth3.template').InAnyOrder().AndReturn(
self.nested_template)
urlfetch.get(
'https://server.test/depth4.template').InAnyOrder().AndReturn(
self.nested_template)
self.m.ReplayAll()
self.create_stack(root_template)
self.m.VerifyAll()
def test_nested_stack_infinite_recursion(self):
template = '''
HeatTemplateFormatVersion: 2012-12-12
Resources:
Nested:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: 'https://server.test/the.template'
'''
urlfetch.get(
'https://server.test/the.template').MultipleTimes().AndReturn(
template)
self.m.ReplayAll()
t = template_format.parse(template)
stack = self.parse_stack(t)
stack.create()
self.assertEqual((stack.CREATE, stack.FAILED), stack.state)
self.assertIn('Recursion depth exceeds', stack.status_reason)
self.m.VerifyAll()
def test_nested_stack_delete(self):
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn(self.nested_template)
self.m.ReplayAll()
stack = self.create_stack(self.test_template)
rsrc = stack['the_nested']
scheduler.TaskRunner(rsrc.delete)()
self.assertEqual((stack.DELETE, stack.COMPLETE), rsrc.state)
nested_stack = parser.Stack.load(utils.dummy_context(
'test_username', 'aaaa', 'password'), rsrc.resource_id)
self.assertEqual((stack.DELETE, stack.COMPLETE), nested_stack.state)
self.m.VerifyAll()
def test_nested_stack_delete_then_delete_parent_stack(self):
urlfetch.get('https://server.test/the.template').MultipleTimes().\
AndReturn(self.nested_template)
self.m.ReplayAll()
stack = self.create_stack(self.test_template)
rsrc = stack['the_nested']
nested_stack = parser.Stack.load(utils.dummy_context(
'test_username', 'aaaa', 'password'), rsrc.resource_id)
nested_stack.delete()
stack = parser.Stack.load(utils.dummy_context(
'test_username', 'aaaa', 'password'), stack.id)
stack.delete()
self.assertEqual((stack.DELETE, stack.COMPLETE), stack.state)
self.m.VerifyAll()
def test_child_params(self):
t = template_format.parse(self.test_template)
stack = self.parse_stack(t)
nested_stack = stack['the_nested']
nested_stack.properties.data[nested_stack.PARAMETERS] = {'foo': 'bar'}
self.assertEqual({'foo': 'bar'}, nested_stack.child_params())
@mock.patch.object(urlfetch, 'get')
def test_child_template_when_file_is_fetched(self, mock_get):
mock_get.return_value = 'template_file'
t = template_format.parse(self.test_template)
stack = self.parse_stack(t)
nested_stack = stack['the_nested']
with mock.patch('heat.common.template_format.parse') as mock_parse:
mock_parse.return_value = 'child_template'
self.assertEqual('child_template', nested_stack.child_template())
mock_parse.assert_called_once_with('template_file')
@mock.patch.object(urlfetch, 'get')
def test_child_template_when_fetching_file_fails(self, mock_get):
mock_get.side_effect = exceptions.RequestException()
t = template_format.parse(self.test_template)
stack = self.parse_stack(t)
nested_stack = stack['the_nested']
self.assertRaises(ValueError, nested_stack.child_template)
@mock.patch.object(urlfetch, 'get')
def test_child_template_when_io_error(self, mock_get):
msg = 'Failed to retrieve template'
mock_get.side_effect = urlfetch.URLFetchError(msg)
t = template_format.parse(self.test_template)
stack = self.parse_stack(t)
nested_stack = stack['the_nested']
self.assertRaises(ValueError, nested_stack.child_template)
class ResDataResource(generic_rsrc.GenericResource):
def handle_create(self):
db_api.resource_data_set(self, "test", 'A secret value', True)
class ResDataNestedStackTest(NestedStackTest):
nested_template = '''
HeatTemplateFormatVersion: "2012-12-12"
Parameters:
KeyName:
Type: String
Resources:
nested_res:
Type: "res.data.resource"
Outputs:
Foo:
Value: bar
'''
def setUp(self):
super(ResDataNestedStackTest, self).setUp()
resource._register_class("res.data.resource", ResDataResource)
def test_res_data_delete(self):
urlfetch.get('https://server.test/the.template').AndReturn(
self.nested_template)
self.m.ReplayAll()
stack = self.create_stack(self.test_template)
res = stack['the_nested'].nested()['nested_res']
stack.delete()
self.assertEqual((stack.DELETE, stack.COMPLETE), stack.state)
self.assertRaises(exception.NotFound, db_api.resource_data_get, res,
'test')