87ff54290a
Allow resource to check whether it is in an expected state and update status and action accordingly. This adds a check action to the stack that will perform checks in all their resources and nested resources as well. If a resource doesn't have a handle_check() method, check for that resource will just pass. If any resource at the end of a Stack.check() doesn't have a handle_check() method, then a message will be added to the status_reason of both the resource and the stack saying that CHECK is not (fully) supported. This allows for the other resources to implement their own checks for their expected states. Co-Authored-By: Richard Lee <rblee88@gmail.com> Implements: blueprint stack-check (partial) Change-Id: I18985476696faeaa47149c233a067f9494c2effa
3626 lines
150 KiB
Python
3626 lines
150 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 collections
|
|
import copy
|
|
import json
|
|
import time
|
|
|
|
from keystoneclient import exceptions as kc_exceptions
|
|
import mock
|
|
from mox import IgnoreArg
|
|
from oslo.config import cfg
|
|
import six
|
|
import warnings
|
|
|
|
from heat.common import context
|
|
from heat.common import exception
|
|
from heat.common import template_format
|
|
from heat.common import urlfetch
|
|
import heat.db.api as db_api
|
|
import heat.engine.cfn.functions
|
|
from heat.engine.cfn import functions as cfn_funcs
|
|
from heat.engine.cfn import template as cfn_t
|
|
from heat.engine.clients.os import keystone
|
|
from heat.engine.clients.os import nova
|
|
from heat.engine import environment
|
|
from heat.engine import function
|
|
from heat.engine.hot import template as hot_t
|
|
from heat.engine import parameters
|
|
from heat.engine import parser
|
|
from heat.engine import resource
|
|
from heat.engine import rsrc_defn
|
|
from heat.engine import scheduler
|
|
from heat.engine import template
|
|
from heat.tests.common import HeatTestCase
|
|
from heat.tests.fakes import FakeKeystoneClient
|
|
from heat.tests import generic_resource as generic_rsrc
|
|
from heat.tests import utils
|
|
from heat.tests.v1_1 import fakes
|
|
|
|
|
|
def join(raw):
|
|
tmpl = parser.Template(mapping_template)
|
|
return function.resolve(tmpl.parse(None, raw))
|
|
|
|
|
|
class ParserTest(HeatTestCase):
|
|
|
|
def test_list(self):
|
|
raw = ['foo', 'bar', 'baz']
|
|
parsed = join(raw)
|
|
for i in six.moves.xrange(len(raw)):
|
|
self.assertEqual(raw[i], parsed[i])
|
|
self.assertIsNot(raw, parsed)
|
|
|
|
def test_dict(self):
|
|
raw = {'foo': 'bar', 'blarg': 'wibble'}
|
|
parsed = join(raw)
|
|
for k in raw:
|
|
self.assertEqual(raw[k], parsed[k])
|
|
self.assertIsNot(raw, parsed)
|
|
|
|
def test_dict_list(self):
|
|
raw = {'foo': ['bar', 'baz'], 'blarg': 'wibble'}
|
|
parsed = join(raw)
|
|
self.assertEqual(raw['blarg'], parsed['blarg'])
|
|
for i in six.moves.xrange(len(raw['foo'])):
|
|
self.assertEqual(raw['foo'][i], parsed['foo'][i])
|
|
self.assertIsNot(raw, parsed)
|
|
self.assertIsNot(raw['foo'], parsed['foo'])
|
|
|
|
def test_list_dict(self):
|
|
raw = [{'foo': 'bar', 'blarg': 'wibble'}, 'baz', 'quux']
|
|
parsed = join(raw)
|
|
for i in six.moves.xrange(1, len(raw)):
|
|
self.assertEqual(raw[i], parsed[i])
|
|
for k in raw[0]:
|
|
self.assertEqual(raw[0][k], parsed[0][k])
|
|
self.assertIsNot(raw, parsed)
|
|
self.assertIsNot(raw[0], parsed[0])
|
|
|
|
def test_join(self):
|
|
raw = {'Fn::Join': [' ', ['foo', 'bar', 'baz']]}
|
|
self.assertEqual('foo bar baz', join(raw))
|
|
|
|
def test_join_none(self):
|
|
raw = {'Fn::Join': [' ', ['foo', None, 'baz']]}
|
|
self.assertEqual('foo baz', join(raw))
|
|
|
|
def test_join_list(self):
|
|
raw = [{'Fn::Join': [' ', ['foo', 'bar', 'baz']]}, 'blarg', 'wibble']
|
|
parsed = join(raw)
|
|
self.assertEqual('foo bar baz', parsed[0])
|
|
for i in six.moves.xrange(1, len(raw)):
|
|
self.assertEqual(raw[i], parsed[i])
|
|
self.assertIsNot(raw, parsed)
|
|
|
|
def test_join_dict_val(self):
|
|
raw = {'quux': {'Fn::Join': [' ', ['foo', 'bar', 'baz']]},
|
|
'blarg': 'wibble'}
|
|
parsed = join(raw)
|
|
self.assertEqual('foo bar baz', parsed['quux'])
|
|
self.assertEqual(raw['blarg'], parsed['blarg'])
|
|
self.assertIsNot(raw, parsed)
|
|
|
|
|
|
mapping_template = template_format.parse('''{
|
|
"AWSTemplateFormatVersion" : "2010-09-09",
|
|
"Mappings" : {
|
|
"ValidMapping" : {
|
|
"TestKey" : { "TestValue" : "wibble" }
|
|
},
|
|
"InvalidMapping" : {
|
|
"ValueList" : [ "foo", "bar" ],
|
|
"ValueString" : "baz"
|
|
},
|
|
"MapList": [ "foo", { "bar" : "baz" } ],
|
|
"MapString": "foobar"
|
|
}
|
|
}''')
|
|
|
|
empty_template = template_format.parse('''{
|
|
"HeatTemplateFormatVersion" : "2012-12-12",
|
|
}''')
|
|
|
|
parameter_template = template_format.parse('''{
|
|
"HeatTemplateFormatVersion" : "2012-12-12",
|
|
"Parameters" : {
|
|
"foo" : { "Type" : "String" },
|
|
"blarg" : { "Type" : "String", "Default": "quux" }
|
|
}
|
|
}''')
|
|
|
|
|
|
resource_template = template_format.parse('''{
|
|
"HeatTemplateFormatVersion" : "2012-12-12",
|
|
"Resources" : {
|
|
"foo" : { "Type" : "GenericResourceType" },
|
|
"blarg" : { "Type" : "GenericResourceType" }
|
|
}
|
|
}''')
|
|
|
|
|
|
class DummyClass(object):
|
|
metadata = None
|
|
|
|
def metadata_get(self):
|
|
return self.metadata
|
|
|
|
def metadata_set(self, metadata):
|
|
self.metadata = metadata
|
|
|
|
|
|
class TemplateTest(HeatTestCase):
|
|
|
|
def setUp(self):
|
|
super(TemplateTest, self).setUp()
|
|
self.ctx = utils.dummy_context()
|
|
|
|
resource._register_class('GenericResourceType',
|
|
generic_rsrc.GenericResource)
|
|
|
|
@staticmethod
|
|
def resolve(snippet, template, stack=None):
|
|
return function.resolve(template.parse(stack, snippet))
|
|
|
|
def test_defaults(self):
|
|
empty = parser.Template(empty_template)
|
|
self.assertNotIn('AWSTemplateFormatVersion', empty)
|
|
self.assertEqual('No description', empty['Description'])
|
|
self.assertEqual({}, empty['Mappings'])
|
|
self.assertEqual({}, empty['Resources'])
|
|
self.assertEqual({}, empty['Outputs'])
|
|
|
|
def test_aws_version(self):
|
|
tmpl = parser.Template(mapping_template)
|
|
self.assertEqual(('AWSTemplateFormatVersion', '2010-09-09'),
|
|
tmpl.version)
|
|
|
|
def test_heat_version(self):
|
|
tmpl = parser.Template(resource_template)
|
|
self.assertEqual(('HeatTemplateFormatVersion', '2012-12-12'),
|
|
tmpl.version)
|
|
|
|
def test_invalid_hot_version(self):
|
|
invalid_hot_version_tmp = template_format.parse(
|
|
'''{
|
|
"heat_template_version" : "2012-12-12",
|
|
}''')
|
|
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
|
|
parser.Template, invalid_hot_version_tmp)
|
|
valid_versions = ['2014-10-16', '2013-05-23']
|
|
ex_error_msg = ('The template version is invalid: '
|
|
'"heat_template_version: 2012-12-12". '
|
|
'"heat_template_version" should be one of: %s'
|
|
% ', '.join(valid_versions))
|
|
self.assertEqual(ex_error_msg, six.text_type(init_ex))
|
|
|
|
def test_invalid_version_not_in_hot_versions(self):
|
|
invalid_hot_version_tmp = template_format.parse(
|
|
'''{
|
|
"heat_template_version" : "2012-12-12",
|
|
}''')
|
|
versions = {
|
|
('heat_template_version', '2013-05-23'): hot_t.HOTemplate,
|
|
('heat_template_version', '2013-06-23'): hot_t.HOTemplate
|
|
}
|
|
|
|
temp_copy = copy.deepcopy(template._template_classes)
|
|
template._template_classes = versions
|
|
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
|
|
parser.Template, invalid_hot_version_tmp)
|
|
ex_error_msg = ('The template version is invalid: '
|
|
'"heat_template_version: 2012-12-12". '
|
|
'"heat_template_version" should be '
|
|
'one of: 2013-05-23, 2013-06-23')
|
|
self.assertEqual(ex_error_msg, six.text_type(init_ex))
|
|
template._template_classes = temp_copy
|
|
|
|
def test_invalid_aws_version(self):
|
|
invalid_aws_version_tmp = template_format.parse(
|
|
'''{
|
|
"AWSTemplateFormatVersion" : "2012-12-12",
|
|
}''')
|
|
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
|
|
parser.Template, invalid_aws_version_tmp)
|
|
ex_error_msg = ('The template version is invalid: '
|
|
'"AWSTemplateFormatVersion: 2012-12-12". '
|
|
'"AWSTemplateFormatVersion" should be: 2010-09-09')
|
|
self.assertEqual(ex_error_msg, six.text_type(init_ex))
|
|
|
|
def test_invalid_version_not_in_aws_versions(self):
|
|
invalid_aws_version_tmp = template_format.parse(
|
|
'''{
|
|
"AWSTemplateFormatVersion" : "2012-12-12",
|
|
}''')
|
|
versions = {
|
|
('AWSTemplateFormatVersion', '2010-09-09'): cfn_t.CfnTemplate,
|
|
('AWSTemplateFormatVersion', '2011-06-23'): cfn_t.CfnTemplate
|
|
}
|
|
temp_copy = copy.deepcopy(template._template_classes)
|
|
template._template_classes = versions
|
|
|
|
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
|
|
parser.Template, invalid_aws_version_tmp)
|
|
ex_error_msg = ('The template version is invalid: '
|
|
'"AWSTemplateFormatVersion: 2012-12-12". '
|
|
'"AWSTemplateFormatVersion" should be '
|
|
'one of: 2010-09-09, 2011-06-23')
|
|
self.assertEqual(ex_error_msg, six.text_type(init_ex))
|
|
template._template_classes = temp_copy
|
|
|
|
def test_invalid_heat_version(self):
|
|
invalid_heat_version_tmp = template_format.parse(
|
|
'''{
|
|
"HeatTemplateFormatVersion" : "2010-09-09",
|
|
}''')
|
|
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
|
|
parser.Template, invalid_heat_version_tmp)
|
|
ex_error_msg = ('The template version is invalid: '
|
|
'"HeatTemplateFormatVersion: 2010-09-09". '
|
|
'"HeatTemplateFormatVersion" should be: 2012-12-12')
|
|
self.assertEqual(ex_error_msg, six.text_type(init_ex))
|
|
|
|
def test_invalid_version_not_in_heat_versions(self):
|
|
invalid_heat_version_tmp = template_format.parse(
|
|
'''{
|
|
"HeatTemplateFormatVersion" : "2010-09-09",
|
|
}''')
|
|
versions = {
|
|
('HeatTemplateFormatVersion', '2012-12-12'): cfn_t.CfnTemplate,
|
|
('HeatTemplateFormatVersion', '2014-12-12'): cfn_t.CfnTemplate
|
|
}
|
|
temp_copy = copy.deepcopy(template._template_classes)
|
|
template._template_classes = versions
|
|
|
|
init_ex = self.assertRaises(exception.InvalidTemplateVersion,
|
|
parser.Template, invalid_heat_version_tmp)
|
|
ex_error_msg = ('The template version is invalid: '
|
|
'"HeatTemplateFormatVersion: 2010-09-09". '
|
|
'"HeatTemplateFormatVersion" should be '
|
|
'one of: 2012-12-12, 2014-12-12')
|
|
self.assertEqual(ex_error_msg, six.text_type(init_ex))
|
|
|
|
template._template_classes = temp_copy
|
|
|
|
def test_invalid_template(self):
|
|
scanner_error = '''
|
|
1
|
|
Mappings:
|
|
ValidMapping:
|
|
TestKey: TestValue
|
|
'''
|
|
parser_error = '''
|
|
Mappings:
|
|
ValidMapping:
|
|
TestKey: {TestKey1: "Value1" TestKey2: "Value2"}
|
|
'''
|
|
|
|
self.assertRaises(ValueError, template_format.parse, scanner_error)
|
|
self.assertRaises(ValueError, template_format.parse, parser_error)
|
|
|
|
def test_invalid_section(self):
|
|
tmpl = parser.Template({'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Foo': ['Bar']})
|
|
self.assertNotIn('Foo', tmpl)
|
|
|
|
def test_find_in_map(self):
|
|
tmpl = parser.Template(mapping_template)
|
|
stack = parser.Stack(self.ctx, 'test', tmpl)
|
|
find = {'Fn::FindInMap': ["ValidMapping", "TestKey", "TestValue"]}
|
|
self.assertEqual("wibble", self.resolve(find, tmpl, stack))
|
|
|
|
def test_find_in_invalid_map(self):
|
|
tmpl = parser.Template(mapping_template)
|
|
stack = parser.Stack(self.ctx, 'test', tmpl)
|
|
finds = ({'Fn::FindInMap': ["InvalidMapping", "ValueList", "foo"]},
|
|
{'Fn::FindInMap': ["InvalidMapping", "ValueString", "baz"]},
|
|
{'Fn::FindInMap': ["MapList", "foo", "bar"]},
|
|
{'Fn::FindInMap': ["MapString", "foo", "bar"]})
|
|
|
|
for find in finds:
|
|
self.assertRaises((KeyError, TypeError), self.resolve,
|
|
find, tmpl, stack)
|
|
|
|
def test_bad_find_in_map(self):
|
|
tmpl = parser.Template(mapping_template)
|
|
stack = parser.Stack(self.ctx, 'test', tmpl)
|
|
finds = ({'Fn::FindInMap': "String"},
|
|
{'Fn::FindInMap': {"Dict": "String"}},
|
|
{'Fn::FindInMap': ["ShortList", "foo"]},
|
|
{'Fn::FindInMap': ["ReallyShortList"]})
|
|
|
|
for find in finds:
|
|
self.assertRaises(KeyError, self.resolve, find, tmpl, stack)
|
|
|
|
def test_param_refs(self):
|
|
tmpl = parser.Template(parameter_template)
|
|
env = environment.Environment({'foo': 'bar', 'blarg': 'wibble'})
|
|
stack = parser.Stack(self.ctx, 'test', tmpl, env)
|
|
p_snippet = {"Ref": "foo"}
|
|
self.assertEqual("bar", self.resolve(p_snippet, tmpl, stack))
|
|
|
|
def test_param_ref_missing(self):
|
|
tmpl = parser.Template(parameter_template)
|
|
env = environment.Environment({'foo': 'bar'})
|
|
stack = parser.Stack(self.ctx, 'test', tmpl, env)
|
|
stack.env = environment.Environment({})
|
|
stack.parameters = parameters.Parameters(stack.identifier(), tmpl)
|
|
snippet = {"Ref": "foo"}
|
|
self.assertRaises(exception.UserParameterMissing,
|
|
self.resolve,
|
|
snippet, tmpl, stack)
|
|
|
|
def test_resource_refs(self):
|
|
tmpl = parser.Template(resource_template)
|
|
stack = parser.Stack(self.ctx, 'test', tmpl)
|
|
|
|
self.m.StubOutWithMock(stack['foo'], 'FnGetRefId')
|
|
stack['foo'].FnGetRefId().MultipleTimes().AndReturn('bar')
|
|
self.m.ReplayAll()
|
|
|
|
r_snippet = {"Ref": "foo"}
|
|
self.assertEqual("bar", self.resolve(r_snippet, tmpl, stack))
|
|
self.m.VerifyAll()
|
|
|
|
def test_resource_refs_param(self):
|
|
tmpl = parser.Template(resource_template)
|
|
stack = parser.Stack(self.ctx, 'test', tmpl)
|
|
|
|
p_snippet = {"Ref": "baz"}
|
|
parsed = tmpl.parse(stack, p_snippet)
|
|
self.assertTrue(isinstance(parsed, heat.engine.cfn.functions.ParamRef))
|
|
|
|
def test_select_from_list(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Select": ["1", ["foo", "bar"]]}
|
|
self.assertEqual("bar", self.resolve(data, tmpl))
|
|
|
|
def test_select_from_list_integer_index(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Select": [1, ["foo", "bar"]]}
|
|
self.assertEqual("bar", self.resolve(data, tmpl))
|
|
|
|
def test_select_from_list_out_of_bound(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Select": ["0", ["foo", "bar"]]}
|
|
self.assertEqual("foo", self.resolve(data, tmpl))
|
|
data = {"Fn::Select": ["1", ["foo", "bar"]]}
|
|
self.assertEqual("bar", self.resolve(data, tmpl))
|
|
data = {"Fn::Select": ["2", ["foo", "bar"]]}
|
|
self.assertEqual("", self.resolve(data, tmpl))
|
|
|
|
def test_select_from_dict(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Select": ["red", {"red": "robin", "re": "foo"}]}
|
|
self.assertEqual("robin", self.resolve(data, tmpl))
|
|
|
|
def test_select_from_none(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Select": ["red", None]}
|
|
self.assertEqual("", self.resolve(data, tmpl))
|
|
|
|
def test_select_from_dict_not_existing(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Select": ["green", {"red": "robin", "re": "foo"}]}
|
|
self.assertEqual("", self.resolve(data, tmpl))
|
|
|
|
def test_select_from_serialized_json_map(self):
|
|
tmpl = parser.Template(empty_template)
|
|
js = json.dumps({"red": "robin", "re": "foo"})
|
|
data = {"Fn::Select": ["re", js]}
|
|
self.assertEqual("foo", self.resolve(data, tmpl))
|
|
|
|
def test_select_from_serialized_json_list(self):
|
|
tmpl = parser.Template(empty_template)
|
|
js = json.dumps(["foo", "fee", "fum"])
|
|
data = {"Fn::Select": ["0", js]}
|
|
self.assertEqual("foo", self.resolve(data, tmpl))
|
|
|
|
def test_select_empty_string(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Select": ["0", '']}
|
|
self.assertEqual("", self.resolve(data, tmpl))
|
|
data = {"Fn::Select": ["1", '']}
|
|
self.assertEqual("", self.resolve(data, tmpl))
|
|
data = {"Fn::Select": ["one", '']}
|
|
self.assertEqual("", self.resolve(data, tmpl))
|
|
|
|
def test_join(self):
|
|
tmpl = parser.Template(empty_template)
|
|
join = {"Fn::Join": [" ", ["foo", "bar"]]}
|
|
self.assertEqual("foo bar", self.resolve(join, tmpl))
|
|
|
|
def test_split_ok(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Split": [";", "foo; bar; achoo"]}
|
|
self.assertEqual(['foo', ' bar', ' achoo'], self.resolve(data, tmpl))
|
|
|
|
def test_split_no_delim_in_str(self):
|
|
tmpl = parser.Template(empty_template)
|
|
data = {"Fn::Split": [";", "foo, bar, achoo"]}
|
|
self.assertEqual(['foo, bar, achoo'], self.resolve(data, tmpl))
|
|
|
|
def test_base64(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::Base64": "foobar"}
|
|
# For now, the Base64 function just returns the original text, and
|
|
# does not convert to base64 (see issue #133)
|
|
self.assertEqual("foobar", self.resolve(snippet, tmpl))
|
|
|
|
def test_get_azs(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::GetAZs": ""}
|
|
self.assertEqual(["nova"], self.resolve(snippet, tmpl))
|
|
|
|
def test_get_azs_with_stack(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::GetAZs": ""}
|
|
stack = parser.Stack(self.ctx, 'test_stack',
|
|
parser.Template(empty_template))
|
|
self.m.StubOutWithMock(nova.NovaClientPlugin, '_create')
|
|
fc = fakes.FakeClient()
|
|
nova.NovaClientPlugin._create().AndReturn(fc)
|
|
self.m.ReplayAll()
|
|
self.assertEqual(["nova1"], self.resolve(snippet, tmpl, stack))
|
|
|
|
def test_replace_string_values(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::Replace": [
|
|
{'$var1': 'foo', '%var2%': 'bar'},
|
|
'$var1 is %var2%'
|
|
]}
|
|
self.assertEqual('foo is bar', self.resolve(snippet, tmpl))
|
|
|
|
def test_replace_number_values(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::Replace": [
|
|
{'$var1': 1, '%var2%': 2},
|
|
'$var1 is not %var2%'
|
|
]}
|
|
self.assertEqual('1 is not 2', self.resolve(snippet, tmpl))
|
|
|
|
snippet = {"Fn::Replace": [
|
|
{'$var1': 1.3, '%var2%': 2.5},
|
|
'$var1 is not %var2%'
|
|
]}
|
|
self.assertEqual('1.3 is not 2.5', self.resolve(snippet, tmpl))
|
|
|
|
def test_replace_none_values(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::Replace": [
|
|
{'$var1': None, '${var2}': None},
|
|
'"$var1" is "${var2}"'
|
|
]}
|
|
self.assertEqual('"" is ""', self.resolve(snippet, tmpl))
|
|
|
|
def test_replace_missing_key(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::Replace": [
|
|
{'$var1': 'foo', 'var2': 'bar'},
|
|
'"$var1" is "${var3}"'
|
|
]}
|
|
self.assertEqual('"foo" is "${var3}"', self.resolve(snippet, tmpl))
|
|
|
|
def test_replace_param_values(self):
|
|
tmpl = parser.Template(parameter_template)
|
|
env = environment.Environment({'foo': 'wibble'})
|
|
stack = parser.Stack(self.ctx, 'test_stack', tmpl, env)
|
|
snippet = {"Fn::Replace": [
|
|
{'$var1': {'Ref': 'foo'}, '%var2%': {'Ref': 'blarg'}},
|
|
'$var1 is %var2%'
|
|
]}
|
|
self.assertEqual('wibble is quux', self.resolve(snippet, tmpl, stack))
|
|
|
|
def test_member_list2map_good(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::MemberListToMap": [
|
|
'Name', 'Value', ['.member.0.Name=metric',
|
|
'.member.0.Value=cpu',
|
|
'.member.1.Name=size',
|
|
'.member.1.Value=56']]}
|
|
self.assertEqual({'metric': 'cpu', 'size': '56'},
|
|
self.resolve(snippet, tmpl))
|
|
|
|
def test_member_list2map_good2(self):
|
|
tmpl = parser.Template(empty_template)
|
|
snippet = {"Fn::MemberListToMap": [
|
|
'Key', 'Value', ['.member.2.Key=metric',
|
|
'.member.2.Value=cpu',
|
|
'.member.5.Key=size',
|
|
'.member.5.Value=56']]}
|
|
self.assertEqual({'metric': 'cpu', 'size': '56'},
|
|
self.resolve(snippet, tmpl))
|
|
|
|
def test_resource_facade(self):
|
|
metadata_snippet = {'Fn::ResourceFacade': 'Metadata'}
|
|
deletion_policy_snippet = {'Fn::ResourceFacade': 'DeletionPolicy'}
|
|
update_policy_snippet = {'Fn::ResourceFacade': 'UpdatePolicy'}
|
|
|
|
parent_resource = DummyClass()
|
|
parent_resource.metadata_set({"foo": "bar"})
|
|
|
|
parent_resource.t = rsrc_defn.ResourceDefinition(
|
|
'parent', 'SomeType',
|
|
deletion_policy=rsrc_defn.ResourceDefinition.RETAIN,
|
|
update_policy={"blarg": "wibble"})
|
|
|
|
parent_resource.stack = parser.Stack(self.ctx, 'toplevel_stack',
|
|
parser.Template(empty_template))
|
|
stack = parser.Stack(self.ctx, 'test_stack',
|
|
parser.Template(empty_template),
|
|
parent_resource=parent_resource)
|
|
self.assertEqual({"foo": "bar"},
|
|
self.resolve(metadata_snippet, stack.t, stack))
|
|
self.assertEqual('Retain',
|
|
self.resolve(deletion_policy_snippet, stack.t, stack))
|
|
self.assertEqual({"blarg": "wibble"},
|
|
self.resolve(update_policy_snippet, stack.t, stack))
|
|
|
|
def test_resource_facade_function(self):
|
|
deletion_policy_snippet = {'Fn::ResourceFacade': 'DeletionPolicy'}
|
|
|
|
parent_resource = DummyClass()
|
|
parent_resource.metadata_set({"foo": "bar"})
|
|
parent_resource.stack = parser.Stack(self.ctx, 'toplevel_stack',
|
|
parser.Template(empty_template))
|
|
del_policy = cfn_funcs.Join(parent_resource.stack,
|
|
'Fn::Join', ['eta', ['R', 'in']])
|
|
parent_resource.t = rsrc_defn.ResourceDefinition(
|
|
'parent', 'SomeType',
|
|
deletion_policy=del_policy)
|
|
|
|
stack = parser.Stack(self.ctx, 'test_stack',
|
|
parser.Template(empty_template),
|
|
parent_resource=parent_resource)
|
|
self.assertEqual('Retain',
|
|
self.resolve(deletion_policy_snippet, stack.t, stack))
|
|
|
|
def test_resource_facade_invalid_arg(self):
|
|
snippet = {'Fn::ResourceFacade': 'wibble'}
|
|
stack = parser.Stack(self.ctx, 'test_stack',
|
|
parser.Template(empty_template))
|
|
error = self.assertRaises(ValueError,
|
|
self.resolve,
|
|
snippet,
|
|
stack.t, stack)
|
|
self.assertIn(snippet.keys()[0], six.text_type(error))
|
|
|
|
def test_resource_facade_missing_deletion_policy(self):
|
|
snippet = {'Fn::ResourceFacade': 'DeletionPolicy'}
|
|
|
|
parent_resource = DummyClass()
|
|
parent_resource.metadata_set({"foo": "bar"})
|
|
parent_resource.t = rsrc_defn.ResourceDefinition('parent', 'SomeType')
|
|
|
|
parent_resource.stack = parser.Stack(self.ctx, 'toplevel_stack',
|
|
parser.Template(empty_template))
|
|
stack = parser.Stack(self.ctx, 'test_stack',
|
|
parser.Template(empty_template),
|
|
parent_resource=parent_resource)
|
|
self.assertEqual('Delete', self.resolve(snippet, stack.t, stack))
|
|
|
|
def test_prevent_parameters_access(self):
|
|
expected_description = "This can be accessed"
|
|
tmpl = parser.Template({'AWSTemplateFormatVersion': '2010-09-09',
|
|
'Description': expected_description,
|
|
'Parameters':
|
|
{'foo': {'Type': 'String', 'Required': True}}})
|
|
self.assertEqual(expected_description, tmpl['Description'])
|
|
keyError = self.assertRaises(KeyError, tmpl.__getitem__, 'Parameters')
|
|
self.assertIn("can not be accessed directly", six.text_type(keyError))
|
|
|
|
def test_parameters_section_not_iterable(self):
|
|
expected_description = "This can be accessed"
|
|
tmpl = parser.Template({'AWSTemplateFormatVersion': '2010-09-09',
|
|
'Description': expected_description,
|
|
'Parameters':
|
|
{'foo': {'Type': 'String', 'Required': True}}})
|
|
self.assertEqual(expected_description, tmpl['Description'])
|
|
self.assertNotIn('Parameters', tmpl.keys())
|
|
|
|
def test_add_resource(self):
|
|
cfn_tpl = template_format.parse('''
|
|
AWSTemplateFormatVersion: 2010-09-09
|
|
Resources:
|
|
resource1:
|
|
Type: AWS::EC2::Instance
|
|
Properties:
|
|
property1: value1
|
|
Metadata:
|
|
foo: bar
|
|
DependsOn: dummy
|
|
DeletionPolicy: Retain
|
|
UpdatePolicy:
|
|
foo: bar
|
|
''')
|
|
source = parser.Template(cfn_tpl)
|
|
empty = parser.Template(copy.deepcopy(empty_template))
|
|
stack = parser.Stack(self.ctx, 'test_stack', source)
|
|
|
|
for defn in source.resource_definitions(stack).values():
|
|
empty.add_resource(defn)
|
|
|
|
self.assertEqual(cfn_tpl['Resources'], empty.t['Resources'])
|
|
|
|
|
|
class TemplateFnErrorTest(HeatTestCase):
|
|
scenarios = [
|
|
('select_from_list_not_int',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Select": ["one", ["foo", "bar"]]})),
|
|
('select_from_dict_not_str',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Select": ["1", {"red": "robin", "re": "foo"}]})),
|
|
('select_from_serialized_json_wrong',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Select": ["not", "no json"]})),
|
|
('select_wrong_num_args_1',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Select": []})),
|
|
('select_wrong_num_args_2',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Select": ["4"]})),
|
|
('select_wrong_num_args_3',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Select": ["foo", {"foo": "bar"}, ""]})),
|
|
('select_wrong_num_args_4',
|
|
dict(expect=TypeError,
|
|
snippet={'Fn::Select': [['f'], {'f': 'food'}]})),
|
|
('split_no_delim',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Split": ["foo, bar, achoo"]})),
|
|
('split_no_list',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Split": "foo, bar, achoo"})),
|
|
('base64_list',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Base64": ["foobar"]})),
|
|
('base64_dict',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Base64": {"foo": "bar"}})),
|
|
('replace_list_value',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Replace": [
|
|
{'$var1': 'foo', '%var2%': ['bar']},
|
|
'$var1 is %var2%']})),
|
|
('replace_list_mapping',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Replace": [
|
|
['var1', 'foo', 'var2', 'bar'],
|
|
'$var1 is ${var2}']})),
|
|
('replace_dict',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Replace": {}})),
|
|
('replace_missing_template',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Replace": [['var1', 'foo', 'var2', 'bar']]})),
|
|
('replace_none_template',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Replace": [['var2', 'bar'], None]})),
|
|
('replace_list_string',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Replace": [
|
|
{'var1': 'foo', 'var2': 'bar'},
|
|
['$var1 is ${var2}']]})),
|
|
('join_string',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Join": [" ", "foo"]})),
|
|
('join_dict',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Join": [" ", {"foo": "bar"}]})),
|
|
('join_wrong_num_args_1',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Join": []})),
|
|
('join_wrong_num_args_2',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Join": [" "]})),
|
|
('join_wrong_num_args_3',
|
|
dict(expect=ValueError,
|
|
snippet={"Fn::Join": [" ", {"foo": "bar"}, ""]})),
|
|
('join_string_nodelim',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Join": "o"})),
|
|
('join_string_nodelim_1',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Join": "oh"})),
|
|
('join_string_nodelim_2',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Join": "ohh"})),
|
|
('join_dict_nodelim1',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Join": {"foo": "bar"}})),
|
|
('join_dict_nodelim2',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Join": {"foo": "bar", "blarg": "wibble"}})),
|
|
('join_dict_nodelim3',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::Join": {"foo": "bar", "blarg": "wibble",
|
|
"baz": "quux"}})),
|
|
('member_list2map_no_key_or_val',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::MemberListToMap": [
|
|
'Key', ['.member.2.Key=metric',
|
|
'.member.2.Value=cpu',
|
|
'.member.5.Key=size',
|
|
'.member.5.Value=56']]})),
|
|
('member_list2map_no_list',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::MemberListToMap": [
|
|
'Key', '.member.2.Key=metric']})),
|
|
('member_list2map_not_string',
|
|
dict(expect=TypeError,
|
|
snippet={"Fn::MemberListToMap": [
|
|
'Name', ['Value'], ['.member.0.Name=metric',
|
|
'.member.0.Value=cpu',
|
|
'.member.1.Name=size',
|
|
'.member.1.Value=56']]})),
|
|
]
|
|
|
|
def test_bad_input(self):
|
|
tmpl = parser.Template(empty_template)
|
|
resolve = lambda s: TemplateTest.resolve(s, tmpl)
|
|
error = self.assertRaises(self.expect,
|
|
resolve,
|
|
self.snippet)
|
|
self.assertIn(self.snippet.keys()[0], six.text_type(error))
|
|
|
|
|
|
class ResolveDataTest(HeatTestCase):
|
|
|
|
def setUp(self):
|
|
super(ResolveDataTest, self).setUp()
|
|
self.username = 'parser_stack_test_user'
|
|
|
|
self.ctx = utils.dummy_context()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'resolve_test_stack',
|
|
template.Template(empty_template),
|
|
environment.Environment({}))
|
|
|
|
def resolve(self, snippet):
|
|
return function.resolve(self.stack.t.parse(self.stack, snippet))
|
|
|
|
def test_stack_resolve_runtime_data_deprecated(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack',
|
|
parser.Template(empty_template),
|
|
tenant_id='bar')
|
|
|
|
with warnings.catch_warnings(record=True) as ws:
|
|
warnings.filterwarnings('always')
|
|
|
|
# Work around http://bugs.python.org/issue4180
|
|
getattr(parser, '__warningregistry__', {}).clear()
|
|
|
|
test_data = {'foo': 'bar'}
|
|
resolved = stack.resolve_runtime_data(test_data)
|
|
|
|
self.assertTrue(ws)
|
|
self.assertTrue(issubclass(ws[0].category, DeprecationWarning))
|
|
|
|
self.assertEqual(test_data, resolved)
|
|
|
|
def test_join_split(self):
|
|
# join
|
|
snippet = {'Fn::Join': [';', ['one', 'two', 'three']]}
|
|
self.assertEqual('one;two;three',
|
|
self.resolve(snippet))
|
|
|
|
# join then split
|
|
snippet = {'Fn::Split': [';', snippet]}
|
|
self.assertEqual(['one', 'two', 'three'],
|
|
self.resolve(snippet))
|
|
|
|
def test_split_join_split_join(self):
|
|
# each snippet in this test encapsulates
|
|
# the snippet from the previous step, leading
|
|
# to increasingly nested function calls
|
|
|
|
# split
|
|
snippet = {'Fn::Split': [',', 'one,two,three']}
|
|
self.assertEqual(['one', 'two', 'three'],
|
|
self.resolve(snippet))
|
|
|
|
# split then join
|
|
snippet = {'Fn::Join': [';', snippet]}
|
|
self.assertEqual('one;two;three',
|
|
self.resolve(snippet))
|
|
|
|
# split then join then split
|
|
snippet = {'Fn::Split': [';', snippet]}
|
|
self.assertEqual(['one', 'two', 'three'],
|
|
self.resolve(snippet))
|
|
|
|
# split then join then split then join
|
|
snippet = {'Fn::Join': ['-', snippet]}
|
|
self.assertEqual('one-two-three',
|
|
self.resolve(snippet))
|
|
|
|
def test_join_recursive(self):
|
|
raw = {'Fn::Join': ['\n', [{'Fn::Join':
|
|
[' ', ['foo', 'bar']]}, 'baz']]}
|
|
self.assertEqual('foo bar\nbaz', self.resolve(raw))
|
|
|
|
def test_base64_replace(self):
|
|
raw = {'Fn::Base64': {'Fn::Replace': [
|
|
{'foo': 'bar'}, 'Meet at the foo']}}
|
|
self.assertEqual('Meet at the bar',
|
|
self.resolve(raw))
|
|
|
|
def test_replace_base64(self):
|
|
raw = {'Fn::Replace': [{'foo': 'bar'}, {
|
|
'Fn::Base64': 'Meet at the foo'}]}
|
|
self.assertEqual('Meet at the bar',
|
|
self.resolve(raw))
|
|
|
|
def test_nested_selects(self):
|
|
data = {
|
|
'a': ['one', 'two', 'three'],
|
|
'b': ['een', 'twee', {'d': 'D', 'e': 'E'}]
|
|
}
|
|
raw = {'Fn::Select': ['a', data]}
|
|
self.assertEqual(data['a'],
|
|
self.resolve(raw))
|
|
|
|
raw = {'Fn::Select': ['b', data]}
|
|
self.assertEqual(data['b'],
|
|
self.resolve(raw))
|
|
|
|
raw = {
|
|
'Fn::Select': ['1', {
|
|
'Fn::Select': ['b', data]}]}
|
|
self.assertEqual('twee',
|
|
self.resolve(raw))
|
|
|
|
raw = {
|
|
'Fn::Select': ['e', {
|
|
'Fn::Select': ['2', {
|
|
'Fn::Select': ['b', data]}]}]}
|
|
self.assertEqual('E',
|
|
self.resolve(raw))
|
|
|
|
def test_member_list_select(self):
|
|
snippet = {'Fn::Select': ['metric', {"Fn::MemberListToMap": [
|
|
'Name', 'Value', ['.member.0.Name=metric',
|
|
'.member.0.Value=cpu',
|
|
'.member.1.Name=size',
|
|
'.member.1.Value=56']]}]}
|
|
self.assertEqual('cpu',
|
|
self.resolve(snippet))
|
|
|
|
|
|
class StackTest(HeatTestCase):
|
|
def setUp(self):
|
|
super(StackTest, self).setUp()
|
|
|
|
self.username = 'parser_stack_test_user'
|
|
self.tmpl = parser.Template(copy.deepcopy(empty_template))
|
|
|
|
self.ctx = utils.dummy_context()
|
|
|
|
resource._register_class('GenericResourceType',
|
|
generic_rsrc.GenericResource)
|
|
resource._register_class('ResourceWithPropsType',
|
|
generic_rsrc.ResourceWithProps)
|
|
resource._register_class('ResourceWithComplexAttributesType',
|
|
generic_rsrc.ResourceWithComplexAttributes)
|
|
|
|
self.m.ReplayAll()
|
|
|
|
def test_stack_reads_tenant(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
tenant_id='bar')
|
|
self.assertEqual('bar', stack.tenant_id)
|
|
|
|
def test_stack_reads_tenant_from_context_if_empty(self):
|
|
self.ctx.tenant_id = 'foo'
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
tenant_id=None)
|
|
self.assertEqual('foo', stack.tenant_id)
|
|
|
|
def test_stack_string_repr(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl)
|
|
expected = 'Stack "%s" [%s]' % (stack.name, stack.id)
|
|
observed = str(stack)
|
|
self.assertEqual(expected, observed)
|
|
|
|
def test_state_defaults(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl)
|
|
self.assertEqual((None, None), stack.state)
|
|
self.assertEqual('', stack.status_reason)
|
|
|
|
def test_timeout_secs_default(self):
|
|
cfg.CONF.set_override('stack_action_timeout', 1000)
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl)
|
|
self.assertIsNone(stack.timeout_mins)
|
|
self.assertEqual(1000, stack.timeout_secs())
|
|
|
|
def test_timeout_secs(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
timeout_mins=10)
|
|
self.assertEqual(600, stack.timeout_secs())
|
|
|
|
def test_no_auth_token(self):
|
|
ctx = utils.dummy_context()
|
|
ctx.auth_token = None
|
|
self.stub_keystoneclient()
|
|
|
|
self.m.ReplayAll()
|
|
stack = parser.Stack(ctx, 'test_stack', self.tmpl)
|
|
self.assertEqual('abcd1234',
|
|
stack.clients.client('keystone').auth_token)
|
|
|
|
self.m.VerifyAll()
|
|
|
|
def test_state(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
action=parser.Stack.CREATE,
|
|
status=parser.Stack.IN_PROGRESS)
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.IN_PROGRESS),
|
|
stack.state)
|
|
stack.state_set(parser.Stack.CREATE, parser.Stack.COMPLETE, 'test')
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
stack.state)
|
|
stack.state_set(parser.Stack.DELETE, parser.Stack.COMPLETE, 'test')
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
stack.state)
|
|
|
|
def test_state_deleted(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
action=parser.Stack.CREATE,
|
|
status=parser.Stack.IN_PROGRESS)
|
|
stack.id = '1234'
|
|
|
|
# Simulate a deleted stack
|
|
self.m.StubOutWithMock(db_api, 'stack_get')
|
|
db_api.stack_get(stack.context, stack.id).AndReturn(None)
|
|
|
|
self.m.ReplayAll()
|
|
|
|
self.assertIsNone(stack.state_set(parser.Stack.CREATE,
|
|
parser.Stack.COMPLETE, 'test'))
|
|
self.m.VerifyAll()
|
|
|
|
def test_state_bad(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
action=parser.Stack.CREATE,
|
|
status=parser.Stack.IN_PROGRESS)
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.IN_PROGRESS),
|
|
stack.state)
|
|
self.assertRaises(ValueError, stack.state_set,
|
|
'baad', parser.Stack.COMPLETE, 'test')
|
|
self.assertRaises(ValueError, stack.state_set,
|
|
parser.Stack.CREATE, 'oops', 'test')
|
|
|
|
def test_status_reason(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
status_reason='quux')
|
|
self.assertEqual('quux', stack.status_reason)
|
|
stack.state_set(parser.Stack.CREATE, parser.Stack.IN_PROGRESS,
|
|
'wibble')
|
|
self.assertEqual('wibble', stack.status_reason)
|
|
|
|
def test_load_nonexistant_id(self):
|
|
self.assertRaises(exception.NotFound, parser.Stack.load,
|
|
None, -1)
|
|
|
|
def test_total_resources_empty(self):
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
status_reason='flimflam')
|
|
self.assertEqual(0, stack.total_resources())
|
|
|
|
def test_total_resources_generic(self):
|
|
tpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources':
|
|
{'A': {'Type': 'GenericResourceType'}}}
|
|
stack = parser.Stack(self.ctx, 'test_stack', parser.Template(tpl),
|
|
status_reason='blarg')
|
|
self.assertEqual(1, stack.total_resources())
|
|
|
|
def _setup_nested(self, name):
|
|
nested_tpl = ('{"HeatTemplateFormatVersion" : "2012-12-12",'
|
|
'"Resources":{'
|
|
'"A": {"Type": "GenericResourceType"},'
|
|
'"B": {"Type": "GenericResourceType"}}}')
|
|
tpl = {'HeatTemplateFormatVersion': "2012-12-12",
|
|
'Resources':
|
|
{'A': {'Type': 'AWS::CloudFormation::Stack',
|
|
'Properties':
|
|
{'TemplateURL': 'http://server.test/nested.json'}},
|
|
'B': {'Type': 'GenericResourceType'}}}
|
|
self.m.StubOutWithMock(urlfetch, 'get')
|
|
urlfetch.get('http://server.test/nested.json').AndReturn(nested_tpl)
|
|
self.m.ReplayAll()
|
|
self.stack = parser.Stack(self.ctx, 'test_stack', parser.Template(tpl),
|
|
status_reason=name)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
|
|
def test_total_resources_nested(self):
|
|
self._setup_nested('zyzzyx')
|
|
self.assertEqual(4, self.stack.total_resources())
|
|
self.assertIsNotNone(self.stack['A'].nested())
|
|
self.assertEqual(
|
|
2, self.stack['A'].nested().total_resources())
|
|
self.assertEqual(
|
|
4,
|
|
self.stack['A'].nested().root_stack.total_resources())
|
|
|
|
def test_iter_resources(self):
|
|
self._setup_nested('iter_resources')
|
|
nested_stack = self.stack['A'].nested()
|
|
resource_generator = self.stack.iter_resources()
|
|
self.assertIsNot(resource_generator, list)
|
|
|
|
first_level_resources = list(resource_generator)
|
|
self.assertEqual(2, len(first_level_resources))
|
|
self.assertIn(self.stack['A'], first_level_resources)
|
|
self.assertIn(self.stack['B'], first_level_resources)
|
|
|
|
all_resources = list(self.stack.iter_resources(1))
|
|
self.assertIn(self.stack['A'], first_level_resources)
|
|
self.assertIn(self.stack['B'], first_level_resources)
|
|
self.assertIn(nested_stack['A'], all_resources)
|
|
self.assertIn(nested_stack['B'], all_resources)
|
|
|
|
def test_root_stack(self):
|
|
self._setup_nested('toor')
|
|
self.assertEqual(self.stack, self.stack.root_stack)
|
|
self.assertIsNotNone(self.stack['A'].nested())
|
|
self.assertEqual(
|
|
self.stack, self.stack['A'].nested().root_stack)
|
|
|
|
def test_load_parent_resource(self):
|
|
self.stack = parser.Stack(self.ctx, 'load_parent_resource',
|
|
self.tmpl)
|
|
self.stack.store()
|
|
stack = db_api.stack_get(self.ctx, self.stack.id)
|
|
|
|
t = template.Template.load(self.ctx, stack.raw_template_id)
|
|
self.m.StubOutWithMock(template.Template, 'load')
|
|
template.Template.load(
|
|
self.ctx, stack.raw_template_id, stack.raw_template
|
|
).AndReturn(t)
|
|
|
|
env = environment.Environment(stack.parameters)
|
|
self.m.StubOutWithMock(environment, 'Environment')
|
|
environment.Environment(stack.parameters).AndReturn(env)
|
|
|
|
self.m.StubOutWithMock(parser.Stack, '__init__')
|
|
parser.Stack.__init__(self.ctx, stack.name, t, env, stack.id,
|
|
stack.action, stack.status, stack.status_reason,
|
|
stack.timeout, True, stack.disable_rollback,
|
|
'parent', owner_id=None,
|
|
stack_user_project_id=None,
|
|
created_time=IgnoreArg(),
|
|
updated_time=None,
|
|
user_creds_id=stack.user_creds_id,
|
|
tenant_id='test_tenant_id',
|
|
use_stored_context=False)
|
|
|
|
self.m.ReplayAll()
|
|
parser.Stack.load(self.ctx, stack_id=self.stack.id,
|
|
parent_resource='parent')
|
|
|
|
self.m.VerifyAll()
|
|
|
|
def test_identifier(self):
|
|
self.stack = parser.Stack(self.ctx, 'identifier_test',
|
|
self.tmpl)
|
|
self.stack.store()
|
|
identifier = self.stack.identifier()
|
|
self.assertEqual(self.stack.tenant_id, identifier.tenant)
|
|
self.assertEqual('identifier_test', identifier.stack_name)
|
|
self.assertTrue(identifier.stack_id)
|
|
self.assertFalse(identifier.path)
|
|
|
|
def test_get_stack_abandon_data(self):
|
|
tpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources':
|
|
{'A': {'Type': 'GenericResourceType'},
|
|
'B': {'Type': 'GenericResourceType'}}}
|
|
resources = '''{"A": {"status": "COMPLETE", "name": "A",
|
|
"resource_data": {}, "resource_id": null, "action": "INIT",
|
|
"type": "GenericResourceType", "metadata": {}},
|
|
"B": {"status": "COMPLETE", "name": "B", "resource_data": {},
|
|
"resource_id": null, "action": "INIT", "type": "GenericResourceType",
|
|
"metadata": {}}}'''
|
|
self.stack = parser.Stack(self.ctx, 'stack_details_test',
|
|
parser.Template(tpl))
|
|
self.stack.store()
|
|
info = self.stack.prepare_abandon()
|
|
self.assertIsNone(info['action'])
|
|
self.assertIn('id', info)
|
|
self.assertEqual('stack_details_test', info['name'])
|
|
self.assertEqual(json.loads(resources), info['resources'])
|
|
self.assertIsNone(info['status'])
|
|
self.assertEqual(tpl, info['template'])
|
|
|
|
def test_set_param_id(self):
|
|
self.stack = parser.Stack(self.ctx, 'param_arn_test',
|
|
self.tmpl)
|
|
exp_prefix = ('arn:openstack:heat::test_tenant_id'
|
|
':stacks/param_arn_test/')
|
|
self.assertEqual(self.stack.parameters['AWS::StackId'],
|
|
exp_prefix + 'None')
|
|
self.stack.store()
|
|
identifier = self.stack.identifier()
|
|
self.assertEqual(self.stack.parameters['AWS::StackId'],
|
|
exp_prefix + self.stack.id)
|
|
self.assertEqual(self.stack.parameters['AWS::StackId'],
|
|
identifier.arn())
|
|
self.m.VerifyAll()
|
|
|
|
def test_set_param_id_update(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Metadata': {'Bar': {'Ref': 'AWS::StackId'}},
|
|
'Properties': {'Foo': 'abc'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_stack_arn_test',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
stack_arn = self.stack.parameters['AWS::StackId']
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Metadata': {'Bar': {'Ref': 'AWS::StackId'}},
|
|
'Properties': {'Foo': 'xyz'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('xyz', self.stack['AResource'].properties['Foo'])
|
|
|
|
self.assertEqual(
|
|
stack_arn, self.stack['AResource'].metadata_get()['Bar'])
|
|
|
|
def test_load_param_id(self):
|
|
self.stack = parser.Stack(self.ctx, 'param_load_arn_test',
|
|
self.tmpl)
|
|
self.stack.store()
|
|
identifier = self.stack.identifier()
|
|
self.assertEqual(self.stack.parameters['AWS::StackId'],
|
|
identifier.arn())
|
|
|
|
newstack = parser.Stack.load(self.ctx, stack_id=self.stack.id)
|
|
self.assertEqual(identifier.arn(), newstack.parameters['AWS::StackId'])
|
|
|
|
def test_load_reads_tenant_id(self):
|
|
self.ctx.tenant_id = 'foobar'
|
|
self.stack = parser.Stack(self.ctx, 'stack_name', self.tmpl)
|
|
self.stack.store()
|
|
stack_id = self.stack.id
|
|
self.ctx.tenant_id = None
|
|
stack = parser.Stack.load(self.ctx, stack_id=stack_id)
|
|
self.assertEqual('foobar', stack.tenant_id)
|
|
|
|
def test_created_time(self):
|
|
self.stack = parser.Stack(self.ctx, 'creation_time_test',
|
|
self.tmpl)
|
|
self.assertIsNone(self.stack.created_time)
|
|
self.stack.store()
|
|
self.assertIsNotNone(self.stack.created_time)
|
|
|
|
def test_updated_time(self):
|
|
self.stack = parser.Stack(self.ctx, 'updated_time_test',
|
|
self.tmpl)
|
|
self.assertIsNone(self.stack.updated_time)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'R1': {'Type': 'GenericResourceType'}}}
|
|
newstack = parser.Stack(self.ctx, 'updated_time_test',
|
|
parser.Template(tmpl))
|
|
self.stack.update(newstack)
|
|
self.assertIsNotNone(self.stack.updated_time)
|
|
|
|
def test_access_policy_update(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'R1': {'Type': 'GenericResourceType'},
|
|
'Policy': {
|
|
'Type': 'OS::Heat::AccessPolicy',
|
|
'Properties': {
|
|
'AllowedResources': ['R1'],
|
|
},
|
|
}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_stack_access_policy_test',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'R1': {'Type': 'GenericResourceType'},
|
|
'R2': {'Type': 'GenericResourceType'},
|
|
'Policy': {
|
|
'Type': 'OS::Heat::AccessPolicy',
|
|
'Properties': {
|
|
'AllowedResources': ['R1', 'R2'],
|
|
},
|
|
}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_delete(self):
|
|
self.stack = parser.Stack(self.ctx, 'delete_test',
|
|
self.tmpl)
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
|
|
self.stack.delete()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNone(db_s)
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_delete_user_creds(self):
|
|
self.stack = parser.Stack(self.ctx, 'delete_test',
|
|
self.tmpl)
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
self.assertIsNotNone(db_s.user_creds_id)
|
|
user_creds_id = db_s.user_creds_id
|
|
db_creds = db_api.user_creds_get(db_s.user_creds_id)
|
|
self.assertIsNotNone(db_creds)
|
|
|
|
self.stack.delete()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNone(db_s)
|
|
db_creds = db_api.user_creds_get(user_creds_id)
|
|
self.assertIsNone(db_creds)
|
|
del_db_s = db_api.stack_get(self.ctx, stack_id, show_deleted=True)
|
|
self.assertIsNone(del_db_s.user_creds_id)
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_delete_user_creds_gone_missing(self):
|
|
'''It may happen that user_creds were deleted when a delete
|
|
operation was stopped. We should be resilient to this and still
|
|
complete the delete operation.
|
|
'''
|
|
self.stack = parser.Stack(self.ctx, 'delete_test',
|
|
self.tmpl)
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
self.assertIsNotNone(db_s.user_creds_id)
|
|
user_creds_id = db_s.user_creds_id
|
|
db_creds = db_api.user_creds_get(db_s.user_creds_id)
|
|
self.assertIsNotNone(db_creds)
|
|
|
|
db_api.user_creds_delete(self.ctx, user_creds_id)
|
|
|
|
self.stack.delete()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNone(db_s)
|
|
db_creds = db_api.user_creds_get(user_creds_id)
|
|
self.assertIsNone(db_creds)
|
|
del_db_s = db_api.stack_get(self.ctx, stack_id, show_deleted=True)
|
|
self.assertIsNone(del_db_s.user_creds_id)
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_delete_trust(self):
|
|
cfg.CONF.set_override('deferred_auth_method', 'trusts')
|
|
self.stub_keystoneclient()
|
|
|
|
self.stack = parser.Stack(
|
|
self.ctx, 'delete_trust', self.tmpl)
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
|
|
self.stack.delete()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNone(db_s)
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_delete_trust_backup(self):
|
|
cfg.CONF.set_override('deferred_auth_method', 'trusts')
|
|
|
|
class FakeKeystoneClientFail(FakeKeystoneClient):
|
|
def delete_trust(self, trust_id):
|
|
raise Exception("Shouldn't delete")
|
|
|
|
self.m.StubOutWithMock(keystone.KeystoneClientPlugin, '_create')
|
|
keystone.KeystoneClientPlugin._create().AndReturn(
|
|
FakeKeystoneClientFail())
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(
|
|
self.ctx, 'delete_trust', self.tmpl)
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
|
|
self.stack.delete(backup=True)
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNone(db_s)
|
|
self.assertEqual(self.stack.state,
|
|
(parser.Stack.DELETE, parser.Stack.COMPLETE))
|
|
|
|
def test_delete_trust_nested(self):
|
|
cfg.CONF.set_override('deferred_auth_method', 'trusts')
|
|
|
|
class FakeKeystoneClientFail(FakeKeystoneClient):
|
|
def delete_trust(self, trust_id):
|
|
raise Exception("Shouldn't delete")
|
|
|
|
self.stub_keystoneclient(fake_client=FakeKeystoneClientFail())
|
|
|
|
self.stack = parser.Stack(
|
|
self.ctx, 'delete_trust_nested', self.tmpl,
|
|
owner_id='owner123')
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
user_creds_id = db_s.user_creds_id
|
|
self.assertIsNotNone(user_creds_id)
|
|
user_creds = db_api.user_creds_get(user_creds_id)
|
|
self.assertIsNotNone(user_creds)
|
|
|
|
self.stack.delete()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNone(db_s)
|
|
user_creds = db_api.user_creds_get(user_creds_id)
|
|
self.assertIsNotNone(user_creds)
|
|
self.assertEqual(self.stack.state,
|
|
(parser.Stack.DELETE, parser.Stack.COMPLETE))
|
|
|
|
def test_delete_trust_fail(self):
|
|
cfg.CONF.set_override('deferred_auth_method', 'trusts')
|
|
|
|
class FakeKeystoneClientFail(FakeKeystoneClient):
|
|
def delete_trust(self, trust_id):
|
|
raise kc_exceptions.Forbidden("Denied!")
|
|
|
|
self.m.StubOutWithMock(keystone.KeystoneClientPlugin, '_create')
|
|
keystone.KeystoneClientPlugin._create().AndReturn(
|
|
FakeKeystoneClientFail())
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(
|
|
self.ctx, 'delete_trust', self.tmpl)
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
|
|
self.stack.delete()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.assertIn('Error deleting trust', self.stack.status_reason)
|
|
|
|
def test_suspend_resume(self):
|
|
self.m.ReplayAll()
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.stack = parser.Stack(self.ctx, 'suspend_test',
|
|
parser.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((self.stack.CREATE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.suspend()
|
|
|
|
self.assertEqual((self.stack.SUSPEND, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.resume()
|
|
|
|
self.assertEqual((self.stack.RESUME, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.m.VerifyAll()
|
|
|
|
def test_suspend_stack_suspended_ok(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.stack = parser.Stack(self.ctx, 'suspend_test',
|
|
parser.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((self.stack.CREATE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.suspend()
|
|
self.assertEqual((self.stack.SUSPEND, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
# unexpected to call Resource.suspend
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'suspend')
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.suspend()
|
|
self.assertEqual((self.stack.SUSPEND, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_resume_stack_resumeed_ok(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.stack = parser.Stack(self.ctx, 'suspend_test',
|
|
parser.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((self.stack.CREATE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.suspend()
|
|
self.assertEqual((self.stack.SUSPEND, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.resume()
|
|
self.assertEqual((self.stack.RESUME, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
# unexpected to call Resource.resume
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'resume')
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.resume()
|
|
self.assertEqual((self.stack.RESUME, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_suspend_fail(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_suspend')
|
|
exc = Exception('foo')
|
|
generic_rsrc.GenericResource.handle_suspend().AndRaise(exc)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'suspend_test_fail',
|
|
parser.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((self.stack.CREATE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.suspend()
|
|
|
|
self.assertEqual((self.stack.SUSPEND, self.stack.FAILED),
|
|
self.stack.state)
|
|
self.assertEqual('Resource SUSPEND failed: Exception: foo',
|
|
self.stack.status_reason)
|
|
self.m.VerifyAll()
|
|
|
|
def test_resume_fail(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_resume')
|
|
generic_rsrc.GenericResource.handle_resume().AndRaise(Exception('foo'))
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'resume_test_fail',
|
|
parser.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((self.stack.CREATE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.suspend()
|
|
|
|
self.assertEqual((self.stack.SUSPEND, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.resume()
|
|
|
|
self.assertEqual((self.stack.RESUME, self.stack.FAILED),
|
|
self.stack.state)
|
|
self.assertEqual('Resource RESUME failed: Exception: foo',
|
|
self.stack.status_reason)
|
|
self.m.VerifyAll()
|
|
|
|
def test_suspend_timeout(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_suspend')
|
|
exc = scheduler.Timeout('foo', 0)
|
|
generic_rsrc.GenericResource.handle_suspend().AndRaise(exc)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'suspend_test_fail_timeout',
|
|
parser.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((self.stack.CREATE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.suspend()
|
|
|
|
self.assertEqual((self.stack.SUSPEND, self.stack.FAILED),
|
|
self.stack.state)
|
|
self.assertEqual('Suspend timed out', self.stack.status_reason)
|
|
self.m.VerifyAll()
|
|
|
|
def test_resume_timeout(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_resume')
|
|
exc = scheduler.Timeout('foo', 0)
|
|
generic_rsrc.GenericResource.handle_resume().AndRaise(exc)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'resume_test_fail_timeout',
|
|
parser.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((self.stack.CREATE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.suspend()
|
|
|
|
self.assertEqual((self.stack.SUSPEND, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.resume()
|
|
|
|
self.assertEqual((self.stack.RESUME, self.stack.FAILED),
|
|
self.stack.state)
|
|
|
|
self.assertEqual('Resume timed out', self.stack.status_reason)
|
|
self.m.VerifyAll()
|
|
|
|
def _get_stack_to_check(self, name):
|
|
def _mock_check(res):
|
|
res.handle_check = mock.Mock()
|
|
if hasattr(res, 'nested'):
|
|
[_mock_check(r) for r in res.nested().resources.values()]
|
|
|
|
self._setup_nested(name)
|
|
[_mock_check(res) for res in self.stack.resources.values()]
|
|
return self.stack
|
|
|
|
def test_check_supported(self):
|
|
stack = self._get_stack_to_check('check-supported')
|
|
stack.check()
|
|
|
|
self.assertEqual(stack.COMPLETE, stack.status)
|
|
self.assertEqual(stack.CHECK, stack.action)
|
|
[self.assertTrue(res.handle_check.called)
|
|
for res in stack.resources.values()]
|
|
self.assertNotIn('not fully supported', stack.status_reason)
|
|
|
|
def test_check_nested_stack(self):
|
|
def _mock_check(res):
|
|
res.handle_check = mock.Mock()
|
|
|
|
self._setup_nested('check-nested-stack')
|
|
nested = self.stack['A'].nested()
|
|
[_mock_check(res) for res in nested.resources.values()]
|
|
self.stack.check()
|
|
|
|
[self.assertTrue(res.handle_check.called)
|
|
for res in nested.resources.values()]
|
|
|
|
def test_check_not_supported(self):
|
|
stack = self._get_stack_to_check('check-not-supported')
|
|
del stack['B'].handle_check
|
|
stack.check()
|
|
|
|
self.assertEqual(stack.COMPLETE, stack.status)
|
|
self.assertEqual(stack.CHECK, stack.action)
|
|
self.assertTrue(stack['A'].handle_check.called)
|
|
self.assertIn('not fully supported', stack.status_reason)
|
|
|
|
def test_check_fail(self):
|
|
stack = self._get_stack_to_check('check-fail')
|
|
stack['A'].handle_check.side_effect = Exception('fail-A')
|
|
stack['B'].handle_check.side_effect = Exception('fail-B')
|
|
stack.check()
|
|
|
|
self.assertEqual(stack.FAILED, stack.status)
|
|
self.assertEqual(stack.CHECK, stack.action)
|
|
self.assertTrue(stack['A'].handle_check.called)
|
|
self.assertTrue(stack['B'].handle_check.called)
|
|
self.assertIn('fail-A', stack.status_reason)
|
|
self.assertIn('fail-B', stack.status_reason)
|
|
|
|
def test_delete_rollback(self):
|
|
self.stack = parser.Stack(self.ctx, 'delete_rollback_test',
|
|
self.tmpl, disable_rollback=False)
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
|
|
self.stack.delete(action=self.stack.ROLLBACK)
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNone(db_s)
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_delete_badaction(self):
|
|
self.stack = parser.Stack(self.ctx, 'delete_badaction_test',
|
|
self.tmpl)
|
|
stack_id = self.stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
|
|
self.stack.delete(action="wibble")
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
|
|
def test_adopt_stack(self):
|
|
adopt_data = '''{
|
|
"action": "CREATE",
|
|
"status": "COMPLETE",
|
|
"name": "my-test-stack-name",
|
|
"resources": {
|
|
"AResource": {
|
|
"status": "COMPLETE",
|
|
"name": "AResource",
|
|
"resource_data": {},
|
|
"metadata": {},
|
|
"resource_id": "test-res-id",
|
|
"action": "CREATE",
|
|
"type": "GenericResourceType"
|
|
}
|
|
}
|
|
}'''
|
|
|
|
tmpl = {
|
|
'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}},
|
|
'Outputs': {'TestOutput': {'Value': {
|
|
'Fn::GetAtt': ['AResource', 'Foo']}}
|
|
}
|
|
}
|
|
|
|
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
|
|
template.Template(tmpl),
|
|
adopt_stack_data=json.loads(adopt_data))
|
|
self.stack.store()
|
|
self.stack.adopt()
|
|
res = self.stack['AResource']
|
|
self.assertEqual(u'test-res-id', res.resource_id)
|
|
self.assertEqual('AResource', res.name)
|
|
self.assertEqual('COMPLETE', res.status)
|
|
self.assertEqual('ADOPT', res.action)
|
|
self.assertEqual((self.stack.ADOPT, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('AResource', self.stack.output('TestOutput'))
|
|
|
|
def test_adopt_stack_fails(self):
|
|
adopt_data = '''{
|
|
"action": "CREATE",
|
|
"status": "COMPLETE",
|
|
"name": "my-test-stack-name",
|
|
"resources": {}
|
|
}'''
|
|
|
|
tmpl = template.Template({
|
|
'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'foo': {'Type': 'GenericResourceType'},
|
|
|
|
}
|
|
})
|
|
self.stack = parser.Stack(utils.dummy_context(), 'test_stack',
|
|
tmpl,
|
|
adopt_stack_data=json.loads(adopt_data))
|
|
self.stack.store()
|
|
self.stack.adopt()
|
|
self.assertEqual((self.stack.ADOPT, self.stack.FAILED),
|
|
self.stack.state)
|
|
expected = ('Resource ADOPT failed: Exception: Resource ID was not'
|
|
' provided.')
|
|
self.assertEqual(expected, self.stack.status_reason)
|
|
|
|
def test_adopt_stack_rollback(self):
|
|
adopt_data = '''{
|
|
"name": "my-test-stack-name",
|
|
"resources": {}
|
|
}'''
|
|
|
|
tmpl = template.Template({
|
|
'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'foo': {'Type': 'GenericResourceType'},
|
|
|
|
}
|
|
})
|
|
self.stack = parser.Stack(utils.dummy_context(),
|
|
'test_stack',
|
|
tmpl,
|
|
disable_rollback=False,
|
|
adopt_stack_data=json.loads(adopt_data))
|
|
self.stack.store()
|
|
self.stack.adopt()
|
|
self.assertEqual((self.stack.ROLLBACK, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_update_badstate(self):
|
|
self.stack = parser.Stack(self.ctx, 'test_stack', self.tmpl,
|
|
action=parser.Stack.CREATE,
|
|
status=parser.Stack.FAILED)
|
|
self.stack.store()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.stack.update(mock.Mock())
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
|
|
def test_resource_by_refid(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'resource_by_refid_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertIn('AResource', self.stack)
|
|
rsrc = self.stack['AResource']
|
|
rsrc.resource_id_set('aaaa')
|
|
self.assertIsNotNone(resource)
|
|
|
|
for action, status in (
|
|
(rsrc.INIT, rsrc.COMPLETE),
|
|
(rsrc.CREATE, rsrc.IN_PROGRESS),
|
|
(rsrc.CREATE, rsrc.COMPLETE),
|
|
(rsrc.RESUME, rsrc.IN_PROGRESS),
|
|
(rsrc.RESUME, rsrc.COMPLETE),
|
|
(rsrc.UPDATE, rsrc.IN_PROGRESS),
|
|
(rsrc.UPDATE, rsrc.COMPLETE)):
|
|
rsrc.state_set(action, status)
|
|
self.assertEqual(rsrc, self.stack.resource_by_refid('aaaa'))
|
|
|
|
rsrc.state_set(rsrc.DELETE, rsrc.IN_PROGRESS)
|
|
try:
|
|
self.assertIsNone(self.stack.resource_by_refid('aaaa'))
|
|
self.assertIsNone(self.stack.resource_by_refid('bbbb'))
|
|
finally:
|
|
rsrc.state_set(rsrc.CREATE, rsrc.COMPLETE)
|
|
|
|
def test_update_add(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType'}}}
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertIn('BResource', self.stack)
|
|
|
|
def test_update_remove(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertNotIn('BResource', self.stack)
|
|
|
|
def test_update_description(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Description': 'ATemplate',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Description': 'BTemplate',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('BTemplate',
|
|
self.stack.t[self.stack.t.DESCRIPTION])
|
|
|
|
def test_update_timeout(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Description': 'ATemplate',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl), timeout_mins=60)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Description': 'ATemplate',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2), timeout_mins=30)
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual(30, self.stack.timeout_mins)
|
|
|
|
def test_update_disable_rollback(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Description': 'ATemplate',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Description': 'ATemplate',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=True)
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual(True, self.stack.disable_rollback)
|
|
|
|
def test_update_modify_ok_replace(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'xyz'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('xyz', self.stack['AResource'].properties['Foo'])
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_modify_param_ok_replace(self):
|
|
tmpl = {
|
|
'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Parameters': {
|
|
'foo': {'Type': 'String'}
|
|
},
|
|
'Resources': {
|
|
'AResource': {
|
|
'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': {'Ref': 'foo'}}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps,
|
|
'update_template_diff')
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
environment.Environment({'foo': 'abc'}))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl),
|
|
environment.Environment({'foo': 'xyz'}))
|
|
|
|
def check_props(*args):
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
|
|
generic_rsrc.ResourceWithProps.update_template_diff(
|
|
{'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'xyz'}},
|
|
{'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}).WithSideEffects(check_props) \
|
|
.AndRaise(resource.UpdateReplace)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('xyz', self.stack['AResource'].properties['Foo'])
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_modify_update_failed(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=True)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
res = self.stack['AResource']
|
|
res.update_allowed_properties = ('Foo',)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'xyz'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
# patch in a dummy handle_update
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_update')
|
|
tmpl_diff = {'Properties': {'Foo': 'xyz'}}
|
|
prop_diff = {'Foo': 'xyz'}
|
|
generic_rsrc.ResourceWithProps.handle_update(
|
|
tmpl2['Resources']['AResource'], tmpl_diff,
|
|
prop_diff).AndRaise(Exception("Foo"))
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_modify_replace_failed_delete(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=True)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'xyz'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
# make the update fail deleting the existing resource
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_delete')
|
|
generic_rsrc.ResourceWithProps.handle_delete().AndRaise(Exception)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
# Unset here so destroy() is not stubbed for stack.delete cleanup
|
|
self.m.UnsetStubs()
|
|
|
|
def test_update_modify_replace_failed_create(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=True)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'xyz'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
# patch in a dummy handle_create making the replace fail creating
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_create')
|
|
generic_rsrc.ResourceWithProps.handle_create().AndRaise(Exception)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_modify_replace_failed_create_and_delete_1(self):
|
|
resource._register_class('ResourceWithResourceIDType',
|
|
generic_rsrc.ResourceWithResourceID)
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'a_res'}},
|
|
'BResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'b_res'},
|
|
'DependsOn': 'AResource'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=True)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'xyz'}},
|
|
'BResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'b_res'},
|
|
'DependsOn': 'AResource'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
# patch in a dummy handle_create making the replace fail creating
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID,
|
|
'handle_create')
|
|
generic_rsrc.ResourceWithResourceID.handle_create().AndRaise(Exception)
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID,
|
|
'mox_resource_id')
|
|
# First, attempts to delete backup_stack. The create (xyz) has been
|
|
# failed, so it has no resource_id.
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
None).AndReturn(None)
|
|
# There are dependency AResource and BResource, so we must delete
|
|
# BResource, then delete AResource.
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'b_res').AndReturn(None)
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'a_res').AndReturn(None)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.stack.delete()
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_modify_replace_failed_create_and_delete_2(self):
|
|
resource._register_class('ResourceWithResourceIDType',
|
|
generic_rsrc.ResourceWithResourceID)
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'a_res'}},
|
|
'BResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'b_res'},
|
|
'DependsOn': 'AResource'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=True)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'c_res'}},
|
|
'BResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'xyz'},
|
|
'DependsOn': 'AResource'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
# patch in a dummy handle_create making the replace fail creating
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID,
|
|
'handle_create')
|
|
generic_rsrc.ResourceWithResourceID.handle_create()
|
|
generic_rsrc.ResourceWithResourceID.handle_create().AndRaise(Exception)
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID,
|
|
'mox_resource_id')
|
|
# First, attempts to delete backup_stack. The create (xyz) has been
|
|
# failed, so it has no resource_id.
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
None).AndReturn(None)
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'c_res').AndReturn(None)
|
|
# There are dependency AResource and BResource, so we must delete
|
|
# BResource, then delete AResource.
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'b_res').AndReturn(None)
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'a_res').AndReturn(None)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
# set resource_id for AResource because handle_create() is overwritten
|
|
# by the mox.
|
|
self.stack.resources['AResource'].resource_id_set('c_res')
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.stack.delete()
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_modify_replace_create_in_progress_and_delete_1(self):
|
|
resource._register_class('ResourceWithResourceIDType',
|
|
generic_rsrc.ResourceWithResourceID)
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'a_res'}},
|
|
'BResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'b_res'},
|
|
'DependsOn': 'AResource'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=True)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'xyz'}},
|
|
'BResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'b_res'},
|
|
'DependsOn': 'AResource'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
# patch in a dummy handle_create making the replace fail creating
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID,
|
|
'handle_create')
|
|
generic_rsrc.ResourceWithResourceID.handle_create().AndRaise(Exception)
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID,
|
|
'mox_resource_id')
|
|
# First, attempts to delete backup_stack. The create (xyz) has been
|
|
# failed, so it has no resource_id.
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
None).AndReturn(None)
|
|
# There are dependency AResource and BResource, so we must delete
|
|
# BResource, then delete AResource.
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'b_res').AndReturn(None)
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'a_res').AndReturn(None)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
# Override stack status and resources status for emulating
|
|
# IN_PROGRESS situation
|
|
self.stack.state_set(
|
|
parser.Stack.UPDATE, parser.Stack.IN_PROGRESS, None)
|
|
self.stack.resources['AResource'].state_set(
|
|
resource.Resource.CREATE, resource.Resource.IN_PROGRESS, None)
|
|
self.stack.delete()
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_modify_replace_create_in_progress_and_delete_2(self):
|
|
resource._register_class('ResourceWithResourceIDType',
|
|
generic_rsrc.ResourceWithResourceID)
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'a_res'}},
|
|
'BResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'b_res'},
|
|
'DependsOn': 'AResource'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=True)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'c_res'}},
|
|
'BResource': {'Type':
|
|
'ResourceWithResourceIDType',
|
|
'Properties': {'ID': 'xyz'},
|
|
'DependsOn': 'AResource'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
# patch in a dummy handle_create making the replace fail creating
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID,
|
|
'handle_create')
|
|
generic_rsrc.ResourceWithResourceID.handle_create()
|
|
generic_rsrc.ResourceWithResourceID.handle_create().AndRaise(Exception)
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithResourceID,
|
|
'mox_resource_id')
|
|
# First, attempts to delete backup_stack. The create (xyz) has been
|
|
# failed, so it has no resource_id.
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
None).AndReturn(None)
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'c_res').AndReturn(None)
|
|
# There are dependency AResource and BResource, so we must delete
|
|
# BResource, then delete AResource.
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'b_res').AndReturn(None)
|
|
generic_rsrc.ResourceWithResourceID.mox_resource_id(
|
|
'a_res').AndReturn(None)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
# set resource_id for AResource because handle_create() is overwritten
|
|
# by the mox.
|
|
self.stack.resources['AResource'].resource_id_set('c_res')
|
|
# Override stack status and resources status for emulating
|
|
# IN_PROGRESS situation
|
|
self.stack.state_set(
|
|
parser.Stack.UPDATE, parser.Stack.IN_PROGRESS, None)
|
|
self.stack.resources['BResource'].state_set(
|
|
resource.Resource.CREATE, resource.Resource.IN_PROGRESS, None)
|
|
self.stack.delete()
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_add_signal(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType'}}}
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
updater = scheduler.TaskRunner(self.stack.update_task, updated_stack)
|
|
updater.start()
|
|
while 'BResource' not in self.stack:
|
|
self.assertFalse(updater.step())
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.IN_PROGRESS),
|
|
self.stack.state)
|
|
|
|
# Reload the stack from the DB and prove that it contains the new
|
|
# resource already
|
|
re_stack = parser.Stack.load(utils.dummy_context(), self.stack.id)
|
|
self.assertIn('BResource', re_stack)
|
|
|
|
updater.run_to_completion()
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertIn('BResource', self.stack)
|
|
|
|
def test_update_add_failed_create(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType'}}}
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
|
|
# patch in a dummy handle_create making BResource fail creating
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_create')
|
|
generic_rsrc.GenericResource.handle_create().AndRaise(Exception)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.assertIn('BResource', self.stack)
|
|
|
|
# Reload the stack from the DB and prove that it contains the failed
|
|
# resource (to ensure it will be deleted on stack delete)
|
|
re_stack = parser.Stack.load(utils.dummy_context(), self.stack.id)
|
|
self.assertIn('BResource', re_stack)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_add_failed_create_rollback_failed(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType'}}}
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=False)
|
|
|
|
# patch in a dummy handle_create making BResource fail creating
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_create')
|
|
generic_rsrc.GenericResource.handle_create().AndRaise(Exception)
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_delete')
|
|
generic_rsrc.GenericResource.handle_delete().AndRaise(Exception)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.assertIn('BResource', self.stack)
|
|
|
|
# Reload the stack from the DB and prove that it contains the failed
|
|
# resource (to ensure it will be deleted on stack delete)
|
|
re_stack = parser.Stack.load(utils.dummy_context(), self.stack.id)
|
|
self.assertIn('BResource', re_stack)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_rollback(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'xyz'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=False)
|
|
|
|
# patch in a dummy handle_create making the replace fail when creating
|
|
# the replacement rsrc
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_create')
|
|
generic_rsrc.ResourceWithProps.handle_create().AndRaise(Exception)
|
|
self.m.ReplayAll()
|
|
|
|
with mock.patch.object(self.stack, 'state_set',
|
|
side_effect=self.stack.state_set) as mock_state:
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
self.assertEqual(2, mock_state.call_count)
|
|
self.assertEqual(('UPDATE', 'IN_PROGRESS'),
|
|
mock_state.call_args_list[0][0][:2])
|
|
self.assertEqual(('ROLLBACK', 'IN_PROGRESS'),
|
|
mock_state.call_args_list[1][0][:2])
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_rollback_fail(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Parameters': {'AParam': {'Type': 'String'}},
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}}}
|
|
|
|
env1 = {'parameters': {'AParam': 'abc'}}
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False,
|
|
env=environment.Environment(env1))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Parameters': {'BParam': {'Type': 'String'}},
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'xyz'}}}}
|
|
|
|
env2 = {'parameters': {'BParam': 'smelly'}}
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=False,
|
|
env=environment.Environment(env2))
|
|
|
|
# patch in a dummy handle_create making the replace fail when creating
|
|
# the replacement rsrc, and again on the second call (rollback)
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_create')
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_delete')
|
|
generic_rsrc.ResourceWithProps.handle_create().AndRaise(Exception)
|
|
generic_rsrc.ResourceWithProps.handle_delete().AndRaise(Exception)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_rollback_add(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=False)
|
|
|
|
# patch in a dummy handle_create making the replace fail when creating
|
|
# the replacement rsrc, and succeed on the second call (rollback)
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_create')
|
|
generic_rsrc.GenericResource.handle_create().AndRaise(Exception)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertNotIn('BResource', self.stack)
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_rollback_remove(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'ResourceWithPropsType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=False)
|
|
|
|
# patch in a dummy delete making the destroy fail
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_delete')
|
|
generic_rsrc.ResourceWithProps.handle_delete().AndRaise(Exception)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertIn('BResource', self.stack)
|
|
self.m.VerifyAll()
|
|
# Unset here so delete() is not stubbed for stack.delete cleanup
|
|
self.m.UnsetStubs()
|
|
|
|
def test_update_rollback_replace(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'foo'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False)
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'bar'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=False)
|
|
|
|
# patch in a dummy delete making the destroy fail
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_delete')
|
|
generic_rsrc.ResourceWithProps.handle_delete().AndRaise(Exception)
|
|
generic_rsrc.ResourceWithProps.handle_delete().AndReturn(None)
|
|
generic_rsrc.ResourceWithProps.handle_delete().AndReturn(None)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
# Unset here so delete() is not stubbed for stack.delete cleanup
|
|
self.m.UnsetStubs()
|
|
|
|
def test_update_replace_by_reference(self):
|
|
'''
|
|
assertion:
|
|
changes in dynamic attributes, due to other resources been updated
|
|
are not ignored and can cause dependent resources to be updated.
|
|
'''
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}},
|
|
'BResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {
|
|
'Foo': {'Ref': 'AResource'}}}}}
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'smelly'}},
|
|
'BResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {
|
|
'Foo': {'Ref': 'AResource'}}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.m.VerifyAll()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
self.assertEqual('AResource',
|
|
self.stack['BResource'].properties['Foo'])
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'FnGetRefId')
|
|
generic_rsrc.ResourceWithProps.FnGetRefId().AndReturn(
|
|
'AResource')
|
|
generic_rsrc.ResourceWithProps.FnGetRefId().MultipleTimes().AndReturn(
|
|
'inst-007')
|
|
self.m.ReplayAll()
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('smelly', self.stack['AResource'].properties['Foo'])
|
|
self.assertEqual('inst-007', self.stack['BResource'].properties['Foo'])
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_with_new_resources_with_reference(self):
|
|
'''
|
|
assertion:
|
|
check, that during update with new resources which one has
|
|
reference on second, reference will be correct resolved.
|
|
'''
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'CResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}}}
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'CResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}},
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'smelly'}},
|
|
'BResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {
|
|
'Foo': {'Ref': 'AResource'}}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.m.VerifyAll()
|
|
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['CResource'].properties['Foo'])
|
|
self.assertEqual(1, len(self.stack.resources))
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_create')
|
|
|
|
generic_rsrc.ResourceWithProps.handle_create().MultipleTimes().\
|
|
AndReturn(None)
|
|
|
|
self.m.ReplayAll()
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2))
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('smelly', self.stack['AResource'].properties['Foo'])
|
|
self.assertEqual('AResource',
|
|
self.stack['BResource'].properties['Foo'])
|
|
|
|
self.assertEqual(3, len(self.stack.resources))
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_by_reference_and_rollback_1(self):
|
|
'''
|
|
assertion:
|
|
check that rollback still works with dynamic metadata
|
|
this test fails the first instance
|
|
'''
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}},
|
|
'BResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {
|
|
'Foo': {'Ref': 'AResource'}}}}}
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'smelly'}},
|
|
'BResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {
|
|
'Foo': {'Ref': 'AResource'}}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False)
|
|
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.m.VerifyAll()
|
|
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
self.assertEqual('AResource',
|
|
self.stack['BResource'].properties['Foo'])
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'FnGetRefId')
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_create')
|
|
|
|
generic_rsrc.ResourceWithProps.FnGetRefId().MultipleTimes().AndReturn(
|
|
'AResource')
|
|
|
|
# mock to make the replace fail when creating the replacement resource
|
|
generic_rsrc.ResourceWithProps.handle_create().AndRaise(Exception)
|
|
|
|
self.m.ReplayAll()
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=False)
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_by_reference_and_rollback_2(self):
|
|
'''
|
|
assertion:
|
|
check that rollback still works with dynamic metadata
|
|
this test fails the second instance
|
|
'''
|
|
|
|
class ResourceTypeA(generic_rsrc.ResourceWithProps):
|
|
count = 0
|
|
|
|
def handle_create(self):
|
|
ResourceTypeA.count += 1
|
|
self.resource_id_set('%s%d' % (self.name, self.count))
|
|
|
|
resource._register_class('ResourceTypeA', ResourceTypeA)
|
|
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceTypeA',
|
|
'Properties': {'Foo': 'abc'}},
|
|
'BResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {
|
|
'Foo': {'Ref': 'AResource'}}}}}
|
|
tmpl2 = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceTypeA',
|
|
'Properties': {'Foo': 'smelly'}},
|
|
'BResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {
|
|
'Foo': {'Ref': 'AResource'}}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False)
|
|
|
|
self.m.ReplayAll()
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.m.VerifyAll()
|
|
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
self.assertEqual('AResource1',
|
|
self.stack['BResource'].properties['Foo'])
|
|
|
|
self.m.StubOutWithMock(generic_rsrc.ResourceWithProps, 'handle_create')
|
|
|
|
# mock to make the replace fail when creating the second
|
|
# replacement resource
|
|
generic_rsrc.ResourceWithProps.handle_create().AndRaise(Exception)
|
|
|
|
self.m.ReplayAll()
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl2),
|
|
disable_rollback=False)
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.ROLLBACK, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
self.assertEqual('AResource1',
|
|
self.stack['BResource'].properties['Foo'])
|
|
|
|
self.m.VerifyAll()
|
|
|
|
def test_update_replace_parameters(self):
|
|
'''
|
|
assertion:
|
|
changes in static environment parameters
|
|
are not ignored and can cause dependent resources to be updated.
|
|
'''
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Parameters': {'AParam': {'Type': 'String'}},
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': {'Ref': 'AParam'}}}}}
|
|
|
|
env1 = {'parameters': {'AParam': 'abc'}}
|
|
env2 = {'parameters': {'AParam': 'smelly'}}
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
environment.Environment(env1))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(tmpl),
|
|
environment.Environment(env2))
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('smelly', self.stack['AResource'].properties['Foo'])
|
|
|
|
def test_update_deletion_policy(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'Bar'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
resource_id = self.stack['AResource'].id
|
|
|
|
new_tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'DeletionPolicy': 'Retain',
|
|
'Properties': {'Foo': 'Bar'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(new_tmpl))
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.assertEqual(resource_id, self.stack['AResource'].id)
|
|
|
|
def test_update_deletion_policy_no_handle_update(self):
|
|
|
|
class ResourceWithNoUpdate(resource.Resource):
|
|
properties_schema = {'Foo': {'Type': 'String'}}
|
|
|
|
resource._register_class('ResourceWithNoUpdate',
|
|
ResourceWithNoUpdate)
|
|
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithNoUpdate',
|
|
'Properties': {'Foo': 'Bar'}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
resource_id = self.stack['AResource'].id
|
|
|
|
new_tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithNoUpdate',
|
|
'DeletionPolicy': 'Retain',
|
|
'Properties': {'Foo': 'Bar'}}}}
|
|
|
|
updated_stack = parser.Stack(self.ctx, 'updated_stack',
|
|
template.Template(new_tmpl))
|
|
self.stack.update(updated_stack)
|
|
self.assertEqual((parser.Stack.UPDATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.assertEqual(resource_id, self.stack['AResource'].id)
|
|
|
|
def test_stack_create_timeout(self):
|
|
self.m.StubOutWithMock(scheduler.DependencyTaskGroup, '__call__')
|
|
self.m.StubOutWithMock(scheduler, 'wallclock')
|
|
|
|
stack = parser.Stack(self.ctx, 's', self.tmpl)
|
|
|
|
def dummy_task():
|
|
while True:
|
|
yield
|
|
|
|
start_time = time.time()
|
|
scheduler.wallclock().AndReturn(start_time)
|
|
scheduler.wallclock().AndReturn(start_time + 1)
|
|
scheduler.DependencyTaskGroup.__call__().AndReturn(dummy_task())
|
|
scheduler.wallclock().AndReturn(start_time + stack.timeout_secs() + 1)
|
|
|
|
self.m.ReplayAll()
|
|
|
|
stack.create()
|
|
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.FAILED),
|
|
stack.state)
|
|
self.assertEqual('Create timed out', stack.status_reason)
|
|
|
|
self.m.VerifyAll()
|
|
|
|
def test_stack_delete_timeout(self):
|
|
stack = parser.Stack(self.ctx, 'delete_test',
|
|
self.tmpl)
|
|
stack_id = stack.store()
|
|
|
|
db_s = db_api.stack_get(self.ctx, stack_id)
|
|
self.assertIsNotNone(db_s)
|
|
|
|
self.m.StubOutWithMock(scheduler.DependencyTaskGroup, '__call__')
|
|
self.m.StubOutWithMock(scheduler, 'wallclock')
|
|
|
|
def dummy_task():
|
|
while True:
|
|
yield
|
|
|
|
start_time = time.time()
|
|
scheduler.wallclock().AndReturn(start_time)
|
|
scheduler.wallclock().AndReturn(start_time + 1)
|
|
scheduler.DependencyTaskGroup.__call__().AndReturn(dummy_task())
|
|
scheduler.wallclock().AndReturn(start_time + stack.timeout_secs() + 1)
|
|
self.m.ReplayAll()
|
|
stack.delete()
|
|
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.FAILED),
|
|
stack.state)
|
|
self.assertEqual('Delete timed out', stack.status_reason)
|
|
|
|
self.m.VerifyAll()
|
|
|
|
def test_stack_delete_resourcefailure(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.m.StubOutWithMock(generic_rsrc.GenericResource, 'handle_delete')
|
|
exc = Exception('foo')
|
|
generic_rsrc.GenericResource.handle_delete().AndRaise(exc)
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'delete_test_fail',
|
|
parser.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((self.stack.CREATE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.stack.delete()
|
|
|
|
self.assertEqual((self.stack.DELETE, self.stack.FAILED),
|
|
self.stack.state)
|
|
self.assertEqual('Resource DELETE failed: Exception: foo',
|
|
self.stack.status_reason)
|
|
self.m.VerifyAll()
|
|
|
|
def test_stack_name_valid(self):
|
|
stack = parser.Stack(self.ctx, 's', self.tmpl)
|
|
self.assertIsInstance(stack, parser.Stack)
|
|
stack = parser.Stack(self.ctx, 'stack123', self.tmpl)
|
|
self.assertIsInstance(stack, parser.Stack)
|
|
stack = parser.Stack(self.ctx, 'test.stack', self.tmpl)
|
|
self.assertIsInstance(stack, parser.Stack)
|
|
stack = parser.Stack(self.ctx, 'test_stack', self.tmpl)
|
|
self.assertIsInstance(stack, parser.Stack)
|
|
stack = parser.Stack(self.ctx, 'TEST', self.tmpl)
|
|
self.assertIsInstance(stack, parser.Stack)
|
|
stack = parser.Stack(self.ctx, 'test-stack', self.tmpl)
|
|
self.assertIsInstance(stack, parser.Stack)
|
|
|
|
def test_stack_name_invalid(self):
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, '_foo',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, '1bad',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, '.kcats',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, 'test stack',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, ' teststack',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, '^-^',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, '\"stack\"',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, '1234',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, 'cat|dog',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, '$(foo)',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, 'test/stack',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, 'test\stack',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, 'test::stack',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, 'test;stack',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, 'test~stack',
|
|
self.tmpl)
|
|
self.assertRaises(ValueError, parser.Stack, self.ctx, '#test',
|
|
self.tmpl)
|
|
|
|
def test_resource_state_get_att(self):
|
|
tmpl = {
|
|
'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}},
|
|
'Outputs': {'TestOutput': {'Value': {
|
|
'Fn::GetAtt': ['AResource', 'Foo']}}
|
|
}
|
|
}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'resource_state_get_att',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertIn('AResource', self.stack)
|
|
rsrc = self.stack['AResource']
|
|
rsrc.resource_id_set('aaaa')
|
|
self.assertEqual('AResource', rsrc.FnGetAtt('Foo'))
|
|
|
|
for action, status in (
|
|
(rsrc.CREATE, rsrc.IN_PROGRESS),
|
|
(rsrc.CREATE, rsrc.COMPLETE),
|
|
(rsrc.CREATE, rsrc.FAILED),
|
|
(rsrc.SUSPEND, rsrc.IN_PROGRESS),
|
|
(rsrc.SUSPEND, rsrc.COMPLETE),
|
|
(rsrc.RESUME, rsrc.IN_PROGRESS),
|
|
(rsrc.RESUME, rsrc.COMPLETE),
|
|
(rsrc.UPDATE, rsrc.IN_PROGRESS),
|
|
(rsrc.UPDATE, rsrc.FAILED),
|
|
(rsrc.UPDATE, rsrc.COMPLETE)):
|
|
rsrc.state_set(action, status)
|
|
self.assertEqual('AResource', self.stack.output('TestOutput'))
|
|
for action, status in (
|
|
(rsrc.DELETE, rsrc.IN_PROGRESS),
|
|
(rsrc.DELETE, rsrc.FAILED),
|
|
(rsrc.DELETE, rsrc.COMPLETE)):
|
|
rsrc.state_set(action, status)
|
|
self.assertIsNone(self.stack.output('TestOutput'))
|
|
|
|
def test_resource_required_by(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType',
|
|
'DependsOn': 'AResource'},
|
|
'CResource': {'Type': 'GenericResourceType',
|
|
'DependsOn': 'BResource'},
|
|
'DResource': {'Type': 'GenericResourceType',
|
|
'DependsOn': 'BResource'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'depends_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.assertEqual(['BResource'],
|
|
self.stack['AResource'].required_by())
|
|
self.assertEqual([],
|
|
self.stack['CResource'].required_by())
|
|
required_by = self.stack['BResource'].required_by()
|
|
self.assertEqual(2, len(required_by))
|
|
for r in ['CResource', 'DResource']:
|
|
self.assertIn(r, required_by)
|
|
|
|
def test_resource_multi_required_by(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType'},
|
|
'CResource': {'Type': 'GenericResourceType'},
|
|
'DResource': {'Type': 'GenericResourceType',
|
|
'DependsOn': ['AResource',
|
|
'BResource',
|
|
'CResource']}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'depends_test_stack',
|
|
template.Template(tmpl))
|
|
self.stack.store()
|
|
self.stack.create()
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
for r in ['AResource', 'BResource', 'CResource']:
|
|
self.assertEqual(['DResource'],
|
|
self.stack[r].required_by())
|
|
|
|
def test_store_saves_owner(self):
|
|
"""
|
|
The owner_id attribute of Store is saved to the database when stored.
|
|
"""
|
|
self.stack = parser.Stack(
|
|
self.ctx, 'owner_stack', self.tmpl)
|
|
stack_ownee = parser.Stack(
|
|
self.ctx, 'ownee_stack', self.tmpl,
|
|
owner_id=self.stack.id)
|
|
stack_ownee.store()
|
|
db_stack = db_api.stack_get(self.ctx, stack_ownee.id)
|
|
self.assertEqual(self.stack.id, db_stack.owner_id)
|
|
|
|
def test_init_user_creds_id(self):
|
|
ctx_init = utils.dummy_context(user='my_user',
|
|
password='my_pass')
|
|
ctx_init.request_id = self.ctx.request_id
|
|
creds = db_api.user_creds_create(ctx_init)
|
|
self.stack = parser.Stack(self.ctx, 'creds_init', self.tmpl,
|
|
user_creds_id=creds.id)
|
|
self.stack.store()
|
|
self.assertEqual(creds.id, self.stack.user_creds_id)
|
|
ctx_expected = ctx_init.to_dict()
|
|
ctx_expected['auth_token'] = None
|
|
self.assertEqual(ctx_expected, self.stack.stored_context().to_dict())
|
|
|
|
def test_store_saves_creds(self):
|
|
"""
|
|
A user_creds entry is created on first stack store
|
|
"""
|
|
self.stack = parser.Stack(
|
|
self.ctx, 'creds_stack', self.tmpl)
|
|
self.stack.store()
|
|
|
|
# The store should've created a user_creds row and set user_creds_id
|
|
db_stack = db_api.stack_get(self.ctx, self.stack.id)
|
|
user_creds_id = db_stack.user_creds_id
|
|
self.assertIsNotNone(user_creds_id)
|
|
|
|
# should've stored the username/password in the context
|
|
user_creds = db_api.user_creds_get(user_creds_id)
|
|
self.assertEqual(self.ctx.username, user_creds.get('username'))
|
|
self.assertEqual(self.ctx.password, user_creds.get('password'))
|
|
self.assertIsNone(user_creds.get('trust_id'))
|
|
self.assertIsNone(user_creds.get('trustor_user_id'))
|
|
|
|
# Check the stored_context is as expected
|
|
expected_context = context.RequestContext.from_dict(self.ctx.to_dict())
|
|
expected_context.auth_token = None
|
|
stored_context = self.stack.stored_context().to_dict()
|
|
self.assertEqual(expected_context.to_dict(), stored_context)
|
|
|
|
# Store again, ID should not change
|
|
self.stack.store()
|
|
self.assertEqual(user_creds_id, db_stack.user_creds_id)
|
|
|
|
def test_store_saves_creds_trust(self):
|
|
"""
|
|
A user_creds entry is created on first stack store
|
|
"""
|
|
cfg.CONF.set_override('deferred_auth_method', 'trusts')
|
|
|
|
self.m.StubOutWithMock(keystone.KeystoneClientPlugin, '_create')
|
|
keystone.KeystoneClientPlugin._create().AndReturn(
|
|
FakeKeystoneClient())
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(
|
|
self.ctx, 'creds_stack', self.tmpl)
|
|
self.stack.store()
|
|
|
|
# The store should've created a user_creds row and set user_creds_id
|
|
db_stack = db_api.stack_get(self.ctx, self.stack.id)
|
|
user_creds_id = db_stack.user_creds_id
|
|
self.assertIsNotNone(user_creds_id)
|
|
|
|
# should've stored the trust_id and trustor_user_id returned from
|
|
# FakeKeystoneClient.create_trust_context, username/password should
|
|
# not have been stored
|
|
user_creds = db_api.user_creds_get(user_creds_id)
|
|
self.assertIsNone(user_creds.get('username'))
|
|
self.assertIsNone(user_creds.get('password'))
|
|
self.assertEqual('atrust', user_creds.get('trust_id'))
|
|
self.assertEqual('auser123', user_creds.get('trustor_user_id'))
|
|
|
|
# Check the stored_context is as expected
|
|
expected_context = context.RequestContext(
|
|
trust_id='atrust', trustor_user_id='auser123',
|
|
request_id=self.ctx.request_id, is_admin=False).to_dict()
|
|
stored_context = self.stack.stored_context().to_dict()
|
|
self.assertEqual(expected_context, stored_context)
|
|
|
|
# Store again, ID should not change
|
|
self.stack.store()
|
|
self.assertEqual(user_creds_id, db_stack.user_creds_id)
|
|
|
|
def test_backup_copies_user_creds_id(self):
|
|
ctx_init = utils.dummy_context(user='my_user',
|
|
password='my_pass')
|
|
ctx_init.request_id = self.ctx.request_id
|
|
creds = db_api.user_creds_create(ctx_init)
|
|
self.stack = parser.Stack(self.ctx, 'creds_init', self.tmpl,
|
|
user_creds_id=creds.id)
|
|
self.stack.store()
|
|
self.assertEqual(creds.id, self.stack.user_creds_id)
|
|
backup = self.stack._backup_stack()
|
|
self.assertEqual(creds.id, backup.user_creds_id)
|
|
|
|
def test_stored_context_err(self):
|
|
"""
|
|
Test stored_context error path.
|
|
"""
|
|
self.stack = parser.Stack(self.ctx, 'creds_stack', self.tmpl)
|
|
ex = self.assertRaises(exception.Error, self.stack.stored_context)
|
|
expected_err = 'Attempt to use stored_context with no user_creds'
|
|
self.assertEqual(expected_err, six.text_type(ex))
|
|
|
|
def test_init_stored_context_false(self):
|
|
ctx_init = utils.dummy_context(user='mystored_user',
|
|
password='mystored_pass')
|
|
ctx_init.request_id = self.ctx.request_id
|
|
creds = db_api.user_creds_create(ctx_init)
|
|
self.stack = parser.Stack(self.ctx, 'creds_store1', self.tmpl,
|
|
user_creds_id=creds.id,
|
|
use_stored_context=False)
|
|
ctx_expected = self.ctx.to_dict()
|
|
self.assertEqual(ctx_expected, self.stack.context.to_dict())
|
|
self.stack.store()
|
|
self.assertEqual(ctx_expected, self.stack.context.to_dict())
|
|
|
|
def test_init_stored_context_true(self):
|
|
ctx_init = utils.dummy_context(user='mystored_user',
|
|
password='mystored_pass')
|
|
ctx_init.request_id = self.ctx.request_id
|
|
creds = db_api.user_creds_create(ctx_init)
|
|
self.stack = parser.Stack(self.ctx, 'creds_store2', self.tmpl,
|
|
user_creds_id=creds.id,
|
|
use_stored_context=True)
|
|
ctx_expected = ctx_init.to_dict()
|
|
ctx_expected['auth_token'] = None
|
|
self.assertEqual(ctx_expected, self.stack.context.to_dict())
|
|
self.stack.store()
|
|
self.assertEqual(ctx_expected, self.stack.context.to_dict())
|
|
|
|
def test_load_stored_context_false(self):
|
|
ctx_init = utils.dummy_context(user='mystored_user',
|
|
password='mystored_pass')
|
|
ctx_init.request_id = self.ctx.request_id
|
|
creds = db_api.user_creds_create(ctx_init)
|
|
self.stack = parser.Stack(self.ctx, 'creds_store3', self.tmpl,
|
|
user_creds_id=creds.id)
|
|
self.stack.store()
|
|
|
|
load_stack = parser.Stack.load(self.ctx, stack_id=self.stack.id,
|
|
use_stored_context=False)
|
|
self.assertEqual(self.ctx.to_dict(), load_stack.context.to_dict())
|
|
|
|
def test_load_stored_context_true(self):
|
|
ctx_init = utils.dummy_context(user='mystored_user',
|
|
password='mystored_pass')
|
|
ctx_init.request_id = self.ctx.request_id
|
|
creds = db_api.user_creds_create(ctx_init)
|
|
self.stack = parser.Stack(self.ctx, 'creds_store4', self.tmpl,
|
|
user_creds_id=creds.id)
|
|
self.stack.store()
|
|
ctx_expected = ctx_init.to_dict()
|
|
ctx_expected['auth_token'] = None
|
|
|
|
load_stack = parser.Stack.load(self.ctx, stack_id=self.stack.id,
|
|
use_stored_context=True)
|
|
self.assertEqual(ctx_expected, load_stack.context.to_dict())
|
|
|
|
def test_load_honors_owner(self):
|
|
"""
|
|
Loading a stack from the database will set the owner_id of the
|
|
resultant stack appropriately.
|
|
"""
|
|
self.stack = parser.Stack(
|
|
self.ctx, 'owner_stack', self.tmpl)
|
|
stack_ownee = parser.Stack(
|
|
self.ctx, 'ownee_stack', self.tmpl,
|
|
owner_id=self.stack.id)
|
|
stack_ownee.store()
|
|
|
|
saved_stack = parser.Stack.load(self.ctx, stack_id=stack_ownee.id)
|
|
self.assertEqual(self.stack.id, saved_stack.owner_id)
|
|
|
|
def test_requires_deferred_auth(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'},
|
|
'BResource': {'Type': 'GenericResourceType'},
|
|
'CResource': {'Type': 'GenericResourceType'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'update_test_stack',
|
|
template.Template(tmpl),
|
|
disable_rollback=False)
|
|
|
|
self.assertFalse(self.stack.requires_deferred_auth())
|
|
|
|
self.stack['CResource'].requires_deferred_auth = True
|
|
self.assertTrue(self.stack.requires_deferred_auth())
|
|
|
|
def test_stack_user_project_id_default(self):
|
|
self.stack = parser.Stack(self.ctx, 'user_project_none',
|
|
self.tmpl)
|
|
self.stack.store()
|
|
self.assertIsNone(self.stack.stack_user_project_id)
|
|
db_stack = db_api.stack_get(self.ctx, self.stack.id)
|
|
self.assertIsNone(db_stack.stack_user_project_id)
|
|
|
|
def test_stack_user_project_id_constructor(self):
|
|
self.stub_keystoneclient()
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'user_project_init',
|
|
self.tmpl,
|
|
stack_user_project_id='aproject1234')
|
|
self.stack.store()
|
|
self.assertEqual('aproject1234', self.stack.stack_user_project_id)
|
|
db_stack = db_api.stack_get(self.ctx, self.stack.id)
|
|
self.assertEqual('aproject1234', db_stack.stack_user_project_id)
|
|
|
|
self.stack.delete()
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_stack_user_project_id_delete_fail(self):
|
|
|
|
class FakeKeystoneClientFail(FakeKeystoneClient):
|
|
def delete_stack_domain_project(self, project_id):
|
|
raise kc_exceptions.Forbidden("Denied!")
|
|
|
|
self.m.StubOutWithMock(keystone.KeystoneClientPlugin, '_create')
|
|
keystone.KeystoneClientPlugin._create().AndReturn(
|
|
FakeKeystoneClientFail())
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'user_project_init',
|
|
self.tmpl,
|
|
stack_user_project_id='aproject1234')
|
|
self.stack.store()
|
|
self.assertEqual('aproject1234', self.stack.stack_user_project_id)
|
|
db_stack = db_api.stack_get(self.ctx, self.stack.id)
|
|
self.assertEqual('aproject1234', db_stack.stack_user_project_id)
|
|
|
|
self.stack.delete()
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.FAILED),
|
|
self.stack.state)
|
|
self.assertIn('Error deleting project', self.stack.status_reason)
|
|
self.m.VerifyAll()
|
|
|
|
def test_stack_user_project_id_setter(self):
|
|
self.stub_keystoneclient()
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'user_project_init',
|
|
self.tmpl)
|
|
self.stack.store()
|
|
self.assertIsNone(self.stack.stack_user_project_id)
|
|
self.stack.set_stack_user_project_id(project_id='aproject456')
|
|
self.assertEqual('aproject456', self.stack.stack_user_project_id)
|
|
db_stack = db_api.stack_get(self.ctx, self.stack.id)
|
|
self.assertEqual('aproject456', db_stack.stack_user_project_id)
|
|
|
|
self.stack.delete()
|
|
self.assertEqual((parser.Stack.DELETE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.m.VerifyAll()
|
|
|
|
def test_preview_resources_returns_list_of_resource_previews(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'GenericResourceType'}}}
|
|
self.stack = parser.Stack(self.ctx, 'preview_stack',
|
|
template.Template(tmpl))
|
|
res = mock.Mock()
|
|
res.preview.return_value = 'foo'
|
|
self.stack._resources = {'r1': res}
|
|
|
|
resources = self.stack.preview_resources()
|
|
self.assertEqual(['foo'], resources)
|
|
|
|
def test_correct_outputs(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}},
|
|
'BResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'def'}}},
|
|
'Outputs': {
|
|
'Resource_attr': {
|
|
'Value': {
|
|
'Fn::GetAtt': ['AResource', 'Foo']}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'stack_with_correct_outputs',
|
|
template.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
self.assertEqual('abc', self.stack['AResource'].properties['Foo'])
|
|
# According _resolve_attribute method in GenericResource output
|
|
# value will be equal with name AResource.
|
|
self.assertEqual('AResource', self.stack.output('Resource_attr'))
|
|
|
|
self.stack.delete()
|
|
|
|
self.assertEqual((self.stack.DELETE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_incorrect_outputs(self):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {
|
|
'AResource': {'Type': 'ResourceWithPropsType',
|
|
'Properties': {'Foo': 'abc'}}},
|
|
'Outputs': {
|
|
'Resource_attr': {
|
|
'Value': {
|
|
'Fn::GetAtt': ['AResource', 'Bar']}}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'stack_with_incorrect_outputs',
|
|
template.Template(tmpl))
|
|
|
|
self.stack.store()
|
|
self.stack.create()
|
|
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
self.assertIsNone(self.stack.output('Resource_attr'))
|
|
self.assertEqual('The Referenced Attribute (AResource Bar) is '
|
|
'incorrect.',
|
|
self.stack.outputs['Resource_attr']['error_msg'])
|
|
|
|
self.stack.delete()
|
|
|
|
self.assertEqual((self.stack.DELETE, self.stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
def test_stack_load_no_param_value_validation(self):
|
|
'''
|
|
Test stack loading with disabled parameter value validation.
|
|
'''
|
|
tmpl = template_format.parse('''
|
|
heat_template_version: 2013-05-23
|
|
parameters:
|
|
flavor:
|
|
type: string
|
|
description: A flavor.
|
|
constraints:
|
|
- custom_constraint: nova.flavor
|
|
resources:
|
|
a_resource:
|
|
type: GenericResourceType
|
|
''')
|
|
|
|
# Mock objects so the query for flavors in server.FlavorConstraint
|
|
# works for stack creation
|
|
fc = fakes.FakeClient()
|
|
self.m.StubOutWithMock(nova.NovaClientPlugin, '_create')
|
|
nova.NovaClientPlugin._create().AndReturn(fc)
|
|
|
|
fc.flavors = self.m.CreateMockAnything()
|
|
flavor = collections.namedtuple("Flavor", ["id", "name"])
|
|
flavor.id = "1234"
|
|
flavor.name = "dummy"
|
|
fc.flavors.list().AndReturn([flavor])
|
|
|
|
self.m.ReplayAll()
|
|
|
|
self.stack = parser.Stack(self.ctx, 'stack_with_custom_constraint',
|
|
template.Template(tmpl),
|
|
environment.Environment({'flavor': 'dummy'}))
|
|
|
|
self.stack.validate()
|
|
self.stack.store()
|
|
self.stack.create()
|
|
stack_id = self.stack.id
|
|
|
|
self.m.VerifyAll()
|
|
|
|
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
|
|
self.stack.state)
|
|
|
|
loaded_stack = parser.Stack.load(self.ctx, stack_id=self.stack.id)
|
|
self.assertEqual(stack_id, loaded_stack.parameters['OS::stack_id'])
|
|
|
|
# verify that fc.flavors.list() has not been called, i.e. verify that
|
|
# parameter value validation did not happen and FlavorConstraint was
|
|
# not invoked
|
|
self.m.VerifyAll()
|
|
|
|
def test_snapshot_delete(self):
|
|
snapshots = []
|
|
|
|
class ResourceDeleteSnapshot(generic_rsrc.ResourceWithProps):
|
|
|
|
def handle_delete_snapshot(self, data):
|
|
snapshots.append(data)
|
|
|
|
resource._register_class(
|
|
'ResourceDeleteSnapshot', ResourceDeleteSnapshot)
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'AResource': {'Type': 'ResourceDeleteSnapshot'}}}
|
|
|
|
self.stack = parser.Stack(self.ctx, 'snapshot_stack',
|
|
template.Template(tmpl))
|
|
data = self.stack.prepare_abandon()
|
|
fake_snapshot = collections.namedtuple('Snapshot', ('data',))(data)
|
|
self.stack.delete_snapshot(fake_snapshot)
|
|
self.assertEqual([data['resources']['AResource']], snapshots)
|