2e281df428
When a resource failed, the stack state was set to FAILED and current traversal was set to emoty string. The actual traversal was lost and there was no way to delete the sync points belonging to the actual traversal. This change keeps the current traversal when you do a state set, so that later you can delete the sync points belonging to it. Also, the current traversal is set to empty when the stack has failed and there is no need to rollback. Closes-Bug: #1618155 Change-Id: Iec3922af92b70b0628fb94b7b2d597247e6d42c4
909 lines
41 KiB
Python
909 lines
41 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 mock
|
|
from oslo_config import cfg
|
|
|
|
from heat.common import template_format
|
|
from heat.engine import environment
|
|
from heat.engine import resource as res
|
|
from heat.engine import stack as parser
|
|
from heat.engine import template as templatem
|
|
from heat.objects import raw_template as raw_template_object
|
|
from heat.objects import resource as resource_objects
|
|
from heat.objects import snapshot as snapshot_objects
|
|
from heat.objects import stack as stack_object
|
|
from heat.objects import sync_point as sync_point_object
|
|
from heat.rpc import worker_client
|
|
from heat.tests import common
|
|
from heat.tests.engine import tools
|
|
from heat.tests import utils
|
|
|
|
|
|
@mock.patch.object(worker_client.WorkerClient, 'check_resource')
|
|
class StackConvergenceCreateUpdateDeleteTest(common.HeatTestCase):
|
|
def setUp(self):
|
|
super(StackConvergenceCreateUpdateDeleteTest, self).setUp()
|
|
cfg.CONF.set_override('convergence_engine', True, enforce_type=True)
|
|
self.stack = None
|
|
|
|
@mock.patch.object(parser.Stack, 'mark_complete')
|
|
def test_converge_empty_template(self, mock_mc, mock_cr):
|
|
empty_tmpl = templatem.Template.create_empty_template()
|
|
stack = parser.Stack(utils.dummy_context(), 'empty_tmpl_stack',
|
|
empty_tmpl, convergence=True)
|
|
stack.store()
|
|
stack.converge_stack(template=stack.t, action=stack.CREATE)
|
|
self.assertFalse(mock_cr.called)
|
|
mock_mc.assert_called_once_with()
|
|
|
|
def test_conv_wordpress_single_instance_stack_create(self, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
convergence=True)
|
|
stack.store() # usually, stack is stored before converge is called
|
|
|
|
stack.converge_stack(template=stack.t, action=stack.CREATE)
|
|
self.assertIsNone(stack.ext_rsrcs_db)
|
|
self.assertEqual('Dependencies([((1, True), None)])',
|
|
repr(stack.convergence_dependencies))
|
|
|
|
stack_db = stack_object.Stack.get_by_id(stack.context, stack.id)
|
|
self.assertIsNotNone(stack_db.current_traversal)
|
|
self.assertIsNotNone(stack_db.raw_template_id)
|
|
|
|
self.assertIsNone(stack_db.prev_raw_template_id)
|
|
|
|
self.assertTrue(stack_db.convergence)
|
|
self.assertEqual({'edges': [[[1, True], None]]}, stack_db.current_deps)
|
|
leaves = stack.convergence_dependencies.leaves()
|
|
expected_calls = []
|
|
for rsrc_id, is_update in leaves:
|
|
expected_calls.append(
|
|
mock.call.worker_client.WorkerClient.check_resource(
|
|
stack.context, rsrc_id, stack.current_traversal,
|
|
{'input_data': {}},
|
|
is_update, None))
|
|
self.assertEqual(expected_calls, mock_cr.mock_calls)
|
|
|
|
def test_conv_string_five_instance_stack_create(self, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.store()
|
|
stack.converge_stack(template=stack.t, action=stack.CREATE)
|
|
self.assertIsNone(stack.ext_rsrcs_db)
|
|
self.assertEqual('Dependencies(['
|
|
'((1, True), (3, True)), '
|
|
'((2, True), (3, True)), '
|
|
'((3, True), (4, True)), '
|
|
'((3, True), (5, True))])',
|
|
repr(stack.convergence_dependencies))
|
|
|
|
stack_db = stack_object.Stack.get_by_id(stack.context, stack.id)
|
|
self.assertIsNotNone(stack_db.current_traversal)
|
|
self.assertIsNotNone(stack_db.raw_template_id)
|
|
self.assertIsNone(stack_db.prev_raw_template_id)
|
|
self.assertTrue(stack_db.convergence)
|
|
self.assertEqual(sorted([[[3, True], [5, True]], # C, A
|
|
[[3, True], [4, True]], # C, B
|
|
[[1, True], [3, True]], # E, C
|
|
[[2, True], [3, True]]]), # D, C
|
|
sorted(stack_db.current_deps['edges']))
|
|
|
|
# check if needed_by is stored properly
|
|
expected_needed_by = {'A': [3], 'B': [3],
|
|
'C': [1, 2],
|
|
'D': [], 'E': []}
|
|
rsrcs_db = resource_objects.Resource.get_all_by_stack(
|
|
stack_db._context, stack_db.id
|
|
)
|
|
self.assertEqual(5, len(rsrcs_db))
|
|
for rsrc_name, rsrc_obj in rsrcs_db.items():
|
|
self.assertEqual(sorted(expected_needed_by[rsrc_name]),
|
|
sorted(rsrc_obj.needed_by))
|
|
self.assertEqual(stack_db.raw_template_id,
|
|
rsrc_obj.current_template_id)
|
|
|
|
# check if sync_points were stored
|
|
for entity_id in [5, 4, 3, 2, 1, stack_db.id]:
|
|
sync_point = sync_point_object.SyncPoint.get_by_key(
|
|
stack_db._context, entity_id, stack_db.current_traversal, True
|
|
)
|
|
self.assertIsNotNone(sync_point)
|
|
self.assertEqual(stack_db.id, sync_point.stack_id)
|
|
|
|
leaves = stack.convergence_dependencies.leaves()
|
|
expected_calls = []
|
|
for rsrc_id, is_update in leaves:
|
|
expected_calls.append(
|
|
mock.call.worker_client.WorkerClient.check_resource(
|
|
stack.context, rsrc_id, stack.current_traversal,
|
|
{'input_data': {}},
|
|
is_update, None))
|
|
self.assertEqual(expected_calls, mock_cr.mock_calls)
|
|
|
|
def _mock_convg_db_update_requires(self):
|
|
"""Updates requires column of resources.
|
|
|
|
Required for testing the generation of convergence dependency graph
|
|
on an update.
|
|
"""
|
|
requires = dict()
|
|
for rsrc_id, is_update in self.stack.convergence_dependencies:
|
|
if is_update:
|
|
reqs = self.stack.convergence_dependencies.requires((
|
|
rsrc_id, is_update))
|
|
requires[rsrc_id] = list({id for id, is_update in reqs})
|
|
|
|
rsrcs_db = resource_objects.Resource.get_all_active_by_stack(
|
|
self.stack.context, self.stack.id)
|
|
|
|
for rsrc_id, rsrc in rsrcs_db.items():
|
|
if rsrc.id in requires:
|
|
rsrcs_db[rsrc_id].requires = requires[rsrc.id]
|
|
return rsrcs_db
|
|
|
|
def test_conv_string_five_instance_stack_update(self, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.store()
|
|
# create stack
|
|
stack.converge_stack(template=stack.t, action=stack.CREATE)
|
|
|
|
curr_stack_db = stack_object.Stack.get_by_id(stack.context, stack.id)
|
|
curr_stack = parser.Stack.load(curr_stack_db._context,
|
|
stack=curr_stack_db)
|
|
# update stack with new template
|
|
t2 = template_format.parse(tools.string_template_five_update)
|
|
template2 = templatem.Template(
|
|
t2, env=environment.Environment({'KeyName2': 'test2'}))
|
|
|
|
# on our previous create_complete, worker would have updated the
|
|
# rsrc.requires. Mock the same behavior here.
|
|
self.stack = stack
|
|
with mock.patch.object(
|
|
parser.Stack, 'db_active_resources_get',
|
|
side_effect=self._mock_convg_db_update_requires):
|
|
curr_stack.converge_stack(template=template2, action=stack.UPDATE)
|
|
|
|
self.assertIsNotNone(curr_stack.ext_rsrcs_db)
|
|
self.assertEqual('Dependencies(['
|
|
'((3, False), (1, False)), '
|
|
'((3, False), (2, False)), '
|
|
'((4, False), (3, False)), '
|
|
'((4, False), (4, True)), '
|
|
'((5, False), (3, False)), '
|
|
'((5, False), (5, True)), '
|
|
'((6, True), (8, True)), '
|
|
'((7, True), (8, True)), '
|
|
'((8, True), (4, True)), '
|
|
'((8, True), (5, True))])',
|
|
repr(curr_stack.convergence_dependencies))
|
|
|
|
stack_db = stack_object.Stack.get_by_id(curr_stack.context,
|
|
curr_stack.id)
|
|
self.assertIsNotNone(stack_db.raw_template_id)
|
|
self.assertIsNotNone(stack_db.current_traversal)
|
|
self.assertIsNotNone(stack_db.prev_raw_template_id)
|
|
self.assertTrue(stack_db.convergence)
|
|
self.assertEqual(sorted([[[7, True], [8, True]],
|
|
[[8, True], [5, True]],
|
|
[[8, True], [4, True]],
|
|
[[6, True], [8, True]],
|
|
[[3, False], [2, False]],
|
|
[[3, False], [1, False]],
|
|
[[5, False], [3, False]],
|
|
[[5, False], [5, True]],
|
|
[[4, False], [3, False]],
|
|
[[4, False], [4, True]]]),
|
|
sorted(stack_db.current_deps['edges']))
|
|
'''
|
|
To visualize:
|
|
|
|
G(7, True) H(6, True)
|
|
\ /
|
|
\ / B(4, False) A(5, False)
|
|
\ / / \ / /
|
|
\ / / /
|
|
F(8, True) / / \ /
|
|
/ \ / / C(3, False)
|
|
/ \ / / \
|
|
/ / \ /
|
|
/ / \ / / \
|
|
B(4, True) A(5, True) D(2, False) E(1, False)
|
|
|
|
Leaves are at the bottom
|
|
'''
|
|
|
|
# check if needed_by are stored properly
|
|
# For A & B:
|
|
# needed_by=C, F
|
|
|
|
expected_needed_by = {'A': [3, 8], 'B': [3, 8],
|
|
'C': [1, 2],
|
|
'D': [], 'E': [],
|
|
'F': [6, 7],
|
|
'G': [], 'H': []}
|
|
rsrcs_db = resource_objects.Resource.get_all_by_stack(
|
|
stack_db._context, stack_db.id
|
|
)
|
|
self.assertEqual(8, len(rsrcs_db))
|
|
for rsrc_name, rsrc_obj in rsrcs_db.items():
|
|
self.assertEqual(sorted(expected_needed_by[rsrc_name]),
|
|
sorted(rsrc_obj.needed_by))
|
|
|
|
# check if sync_points are created for forward traversal
|
|
# [F, H, G, A, B, Stack]
|
|
for entity_id in [8, 7, 6, 5, 4, stack_db.id]:
|
|
sync_point = sync_point_object.SyncPoint.get_by_key(
|
|
stack_db._context, entity_id, stack_db.current_traversal, True
|
|
)
|
|
self.assertIsNotNone(sync_point)
|
|
self.assertEqual(stack_db.id, sync_point.stack_id)
|
|
|
|
# check if sync_points are created for cleanup traversal
|
|
# [A, B, C, D, E]
|
|
for entity_id in [5, 4, 3, 2, 1]:
|
|
sync_point = sync_point_object.SyncPoint.get_by_key(
|
|
stack_db._context, entity_id, stack_db.current_traversal, False
|
|
)
|
|
self.assertIsNotNone(sync_point)
|
|
self.assertEqual(stack_db.id, sync_point.stack_id)
|
|
|
|
leaves = stack.convergence_dependencies.leaves()
|
|
expected_calls = []
|
|
for rsrc_id, is_update in leaves:
|
|
expected_calls.append(
|
|
mock.call.worker_client.WorkerClient.check_resource(
|
|
stack.context, rsrc_id, stack.current_traversal,
|
|
{'input_data': {}},
|
|
is_update, None))
|
|
|
|
leaves = curr_stack.convergence_dependencies.leaves()
|
|
for rsrc_id, is_update in leaves:
|
|
expected_calls.append(
|
|
mock.call.worker_client.WorkerClient.check_resource(
|
|
curr_stack.context, rsrc_id, curr_stack.current_traversal,
|
|
{'input_data': {}},
|
|
is_update, None))
|
|
self.assertEqual(expected_calls, mock_cr.mock_calls)
|
|
|
|
def test_conv_empty_template_stack_update_delete(self, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.store()
|
|
# create stack
|
|
stack.converge_stack(template=stack.t, action=stack.CREATE)
|
|
|
|
# update stack with new template
|
|
template2 = templatem.Template.create_empty_template(
|
|
version=stack.t.version)
|
|
|
|
curr_stack_db = stack_object.Stack.get_by_id(stack.context, stack.id)
|
|
curr_stack = parser.Stack.load(curr_stack_db._context,
|
|
stack=curr_stack_db)
|
|
# on our previous create_complete, worker would have updated the
|
|
# rsrc.requires. Mock the same behavior here.
|
|
self.stack = stack
|
|
with mock.patch.object(
|
|
parser.Stack, 'db_active_resources_get',
|
|
side_effect=self._mock_convg_db_update_requires):
|
|
curr_stack.converge_stack(template=template2, action=stack.DELETE)
|
|
|
|
self.assertIsNotNone(curr_stack.ext_rsrcs_db)
|
|
self.assertEqual('Dependencies(['
|
|
'((3, False), (1, False)), '
|
|
'((3, False), (2, False)), '
|
|
'((4, False), (3, False)), '
|
|
'((5, False), (3, False))])',
|
|
repr(curr_stack.convergence_dependencies))
|
|
|
|
stack_db = stack_object.Stack.get_by_id(curr_stack.context,
|
|
curr_stack.id)
|
|
self.assertIsNotNone(stack_db.current_traversal)
|
|
self.assertIsNotNone(stack_db.prev_raw_template_id)
|
|
self.assertEqual(sorted([[[3, False], [2, False]],
|
|
[[3, False], [1, False]],
|
|
[[5, False], [3, False]],
|
|
[[4, False], [3, False]]]),
|
|
sorted(stack_db.current_deps['edges']))
|
|
|
|
expected_needed_by = {'A': [3], 'B': [3],
|
|
'C': [1, 2],
|
|
'D': [], 'E': []}
|
|
rsrcs_db = resource_objects.Resource.get_all_by_stack(
|
|
stack_db._context, stack_db.id
|
|
)
|
|
self.assertEqual(5, len(rsrcs_db))
|
|
for rsrc_name, rsrc_obj in rsrcs_db.items():
|
|
self.assertEqual(sorted(expected_needed_by[rsrc_name]),
|
|
sorted(rsrc_obj.needed_by))
|
|
|
|
# check if sync_points are created for cleanup traversal
|
|
# [A, B, C, D, E, Stack]
|
|
for entity_id in [5, 4, 3, 2, 1, stack_db.id]:
|
|
is_update = False
|
|
if entity_id == stack_db.id:
|
|
is_update = True
|
|
sync_point = sync_point_object.SyncPoint.get_by_key(
|
|
stack_db._context, entity_id, stack_db.current_traversal,
|
|
is_update)
|
|
self.assertIsNotNone(sync_point, 'entity %s' % entity_id)
|
|
self.assertEqual(stack_db.id, sync_point.stack_id)
|
|
|
|
leaves = stack.convergence_dependencies.leaves()
|
|
expected_calls = []
|
|
for rsrc_id, is_update in leaves:
|
|
expected_calls.append(
|
|
mock.call.worker_client.WorkerClient.check_resource(
|
|
stack.context, rsrc_id, stack.current_traversal,
|
|
{'input_data': {}},
|
|
is_update, None))
|
|
|
|
leaves = curr_stack.convergence_dependencies.leaves()
|
|
for rsrc_id, is_update in leaves:
|
|
expected_calls.append(
|
|
mock.call.worker_client.WorkerClient.check_resource(
|
|
curr_stack.context, rsrc_id, curr_stack.current_traversal,
|
|
{'input_data': {}},
|
|
is_update, None))
|
|
self.assertEqual(expected_calls, mock_cr.mock_calls)
|
|
|
|
def test_mark_complete_purges_db(self, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.store()
|
|
stack.purge_db = mock.Mock()
|
|
stack.mark_complete()
|
|
self.assertTrue(stack.purge_db.called)
|
|
|
|
def test_state_set_sets_empty_curr_trvsl_for_failed_stack(
|
|
self, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.status = stack.FAILED
|
|
stack.store()
|
|
stack.purge_db()
|
|
self.assertEqual('', stack.current_traversal)
|
|
|
|
@mock.patch.object(raw_template_object.RawTemplate, 'delete')
|
|
def test_purge_db_deletes_previous_template(self, mock_tmpl_delete,
|
|
mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.prev_raw_template_id = 10
|
|
stack.purge_db()
|
|
self.assertTrue(mock_tmpl_delete.called)
|
|
|
|
@mock.patch.object(parser.Stack, '_delete_credentials')
|
|
@mock.patch.object(stack_object.Stack, 'delete')
|
|
def test_purge_db_deletes_creds(self, mock_delete_stack,
|
|
mock_creds_delete, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
reason = 'stack delete complete'
|
|
mock_creds_delete.return_value = (stack.COMPLETE, reason)
|
|
stack.state_set(stack.DELETE, stack.COMPLETE, reason)
|
|
stack.purge_db()
|
|
self.assertTrue(mock_creds_delete.called)
|
|
self.assertTrue(mock_delete_stack.called)
|
|
|
|
@mock.patch.object(parser.Stack, '_delete_credentials')
|
|
@mock.patch.object(stack_object.Stack, 'delete')
|
|
def test_purge_db_deletes_creds_failed(self, mock_delete_stack,
|
|
mock_creds_delete, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
|
|
reason = 'stack delete complete'
|
|
failed_reason = 'Error deleting trust'
|
|
mock_creds_delete.return_value = (stack.FAILED, failed_reason)
|
|
stack.state_set(stack.DELETE, stack.COMPLETE, reason)
|
|
stack.purge_db()
|
|
self.assertTrue(mock_creds_delete.called)
|
|
self.assertFalse(mock_delete_stack.called)
|
|
self.assertEqual((stack.DELETE, stack.FAILED), stack.state)
|
|
|
|
@mock.patch.object(raw_template_object.RawTemplate, 'delete')
|
|
def test_purge_db_does_not_delete_previous_template_when_stack_fails(
|
|
self, mock_tmpl_delete, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.status = stack.FAILED
|
|
stack.purge_db()
|
|
self.assertFalse(mock_tmpl_delete.called)
|
|
|
|
def test_purge_db_deletes_sync_points(self, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.store()
|
|
stack.purge_db()
|
|
rows = sync_point_object.SyncPoint.delete_all_by_stack_and_traversal(
|
|
stack.context, stack.id, stack.current_traversal)
|
|
self.assertEqual(0, rows)
|
|
|
|
@mock.patch.object(stack_object.Stack, 'delete')
|
|
def test_purge_db_deletes_stack_for_deleted_stack(self, mock_stack_delete,
|
|
mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.store()
|
|
stack.state_set(stack.DELETE, stack.COMPLETE, 'test reason')
|
|
stack.purge_db()
|
|
self.assertTrue(mock_stack_delete.called)
|
|
|
|
@mock.patch.object(resource_objects.Resource, 'purge_deleted')
|
|
def test_purge_db_calls_rsrc_purge_deleted(self, mock_rsrc_purge_delete,
|
|
mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.store()
|
|
stack.purge_db()
|
|
self.assertTrue(mock_rsrc_purge_delete.called)
|
|
|
|
def test_get_best_existing_db_resource(self, mock_cr):
|
|
stack = tools.get_stack('test_stack', utils.dummy_context(),
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
stack.store()
|
|
stack.prev_raw_template_id = 2
|
|
stack.t.id = 3
|
|
dummy_res = stack.resources['A']
|
|
a_res_2 = res.Resource('A', dummy_res.t, stack)
|
|
a_res_2.current_template_id = 2
|
|
a_res_2.id = 2
|
|
a_res_3 = res.Resource('A', dummy_res.t, stack)
|
|
a_res_3.current_template_id = 3
|
|
a_res_3.id = 3
|
|
a_res_1 = res.Resource('A', dummy_res.t, stack)
|
|
a_res_1.current_template_id = 1
|
|
a_res_1.id = 1
|
|
existing_res = {2: a_res_2,
|
|
3: a_res_3,
|
|
1: a_res_1}
|
|
stack.ext_rsrcs_db = existing_res
|
|
best_res = stack._get_best_existing_rsrc_db('A')
|
|
# should return resource with template id 3 which is current template
|
|
self.assertEqual(a_res_3.id, best_res.id)
|
|
|
|
# no resource with current template id as 3
|
|
existing_res = {1: a_res_1,
|
|
2: a_res_2}
|
|
stack.ext_rsrcs_db = existing_res
|
|
best_res = stack._get_best_existing_rsrc_db('A')
|
|
# should return resource with template id 2 which is prev template
|
|
self.assertEqual(a_res_2.id, best_res.id)
|
|
|
|
# no resource with current template id as 3 or 2
|
|
existing_res = {1: a_res_1}
|
|
stack.ext_rsrcs_db = existing_res
|
|
best_res = stack._get_best_existing_rsrc_db('A')
|
|
# should return resource with template id 1 existing in DB
|
|
self.assertEqual(a_res_1.id, best_res.id)
|
|
|
|
@mock.patch.object(parser.Stack, '_converge_create_or_update')
|
|
def test_updated_time_stack_create(self, mock_ccu, mock_cr):
|
|
stack = parser.Stack(utils.dummy_context(), 'convg_updated_time_test',
|
|
templatem.Template.create_empty_template(),
|
|
convergence=True)
|
|
stack.converge_stack(template=stack.t, action=stack.CREATE)
|
|
self.assertIsNone(stack.updated_time)
|
|
self.assertTrue(mock_ccu.called)
|
|
|
|
@mock.patch.object(parser.Stack, '_converge_create_or_update')
|
|
def test_updated_time_stack_update(self, mock_ccu, mock_cr):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'R1': {'Type': 'GenericResourceType'}}}
|
|
stack = parser.Stack(utils.dummy_context(), 'updated_time_test',
|
|
templatem.Template(tmpl), convergence=True)
|
|
stack.converge_stack(template=stack.t, action=stack.UPDATE)
|
|
self.assertIsNotNone(stack.updated_time)
|
|
self.assertTrue(mock_ccu.called)
|
|
|
|
@mock.patch.object(parser.Stack, '_converge_create_or_update')
|
|
@mock.patch.object(sync_point_object.SyncPoint,
|
|
'delete_all_by_stack_and_traversal')
|
|
def test_sync_point_delete_stack_create(self, mock_syncpoint_del,
|
|
mock_ccu, mock_cr):
|
|
stack = parser.Stack(utils.dummy_context(), 'convg_updated_time_test',
|
|
templatem.Template.create_empty_template())
|
|
stack.converge_stack(template=stack.t, action=stack.CREATE)
|
|
self.assertFalse(mock_syncpoint_del.called)
|
|
self.assertTrue(mock_ccu.called)
|
|
|
|
@mock.patch.object(parser.Stack, '_converge_create_or_update')
|
|
@mock.patch.object(sync_point_object.SyncPoint,
|
|
'delete_all_by_stack_and_traversal')
|
|
def test_sync_point_delete_stack_update(self, mock_syncpoint_del,
|
|
mock_ccu, mock_cr):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'R1': {'Type': 'GenericResourceType'}}}
|
|
stack = parser.Stack(utils.dummy_context(), 'updated_time_test',
|
|
templatem.Template(tmpl))
|
|
stack.current_traversal = 'prev_traversal'
|
|
stack.converge_stack(template=stack.t, action=stack.UPDATE)
|
|
self.assertTrue(mock_syncpoint_del.called)
|
|
self.assertTrue(mock_ccu.called)
|
|
|
|
def test_snapshot_delete(self, mock_cr):
|
|
tmpl = {'HeatTemplateFormatVersion': '2012-12-12',
|
|
'Resources': {'R1': {'Type': 'GenericResourceType'}}}
|
|
stack = parser.Stack(utils.dummy_context(), 'updated_time_test',
|
|
templatem.Template(tmpl))
|
|
stack.current_traversal = 'prev_traversal'
|
|
stack.action, stack.status = stack.CREATE, stack.COMPLETE
|
|
stack.store()
|
|
snapshot_values = {
|
|
'stack_id': stack.id,
|
|
'name': 'fake_snapshot',
|
|
'tenant': stack.context.tenant_id,
|
|
'status': 'COMPLETE',
|
|
'data': None
|
|
}
|
|
snapshot_objects.Snapshot.create(stack.context, snapshot_values)
|
|
|
|
# Ensure that snapshot is not deleted on stack update
|
|
stack.converge_stack(template=stack.t, action=stack.UPDATE)
|
|
db_snapshot_obj = snapshot_objects.Snapshot.get_all(
|
|
stack.context, stack.id)
|
|
self.assertEqual('fake_snapshot', db_snapshot_obj[0].name)
|
|
self.assertEqual(stack.id, db_snapshot_obj[0].stack_id)
|
|
|
|
# Ensure that snapshot is deleted on stack delete
|
|
stack.converge_stack(template=stack.t, action=stack.DELETE)
|
|
self.assertEqual([], snapshot_objects.Snapshot.get_all(
|
|
stack.context, stack.id))
|
|
self.assertTrue(mock_cr.called)
|
|
|
|
|
|
@mock.patch.object(parser.Stack, '_persist_state')
|
|
class TestConvgStackStateSet(common.HeatTestCase):
|
|
def setUp(self):
|
|
super(TestConvgStackStateSet, self).setUp()
|
|
cfg.CONF.set_override('convergence_engine', True, enforce_type=True)
|
|
self.stack = tools.get_stack(
|
|
'test_stack', utils.dummy_context(),
|
|
template=tools.wp_template, convergence=True)
|
|
|
|
def test_state_set_create_adopt_update_delete_rollback_complete(self,
|
|
mock_ps):
|
|
mock_ps.return_value = 'updated'
|
|
ret_val = self.stack.state_set(self.stack.CREATE, self.stack.COMPLETE,
|
|
'Create complete')
|
|
self.assertTrue(mock_ps.called)
|
|
# Ensure that state_set returns with value for convergence
|
|
self.assertEqual('updated', ret_val)
|
|
|
|
mock_ps.reset_mock()
|
|
ret_val = self.stack.state_set(self.stack.UPDATE, self.stack.COMPLETE,
|
|
'Update complete')
|
|
self.assertTrue(mock_ps.called)
|
|
self.assertEqual('updated', ret_val)
|
|
|
|
mock_ps.reset_mock()
|
|
ret_val = self.stack.state_set(
|
|
self.stack.ROLLBACK, self.stack.COMPLETE, 'Rollback complete')
|
|
self.assertTrue(mock_ps.called)
|
|
self.assertEqual('updated', ret_val)
|
|
|
|
mock_ps.reset_mock()
|
|
ret_val = self.stack.state_set(self.stack.DELETE, self.stack.COMPLETE,
|
|
'Delete complete')
|
|
self.assertTrue(mock_ps.called)
|
|
self.assertEqual('updated', ret_val)
|
|
|
|
mock_ps.reset_mock()
|
|
ret_val = self.stack.state_set(self.stack.ADOPT, self.stack.COMPLETE,
|
|
'Adopt complete')
|
|
self.assertTrue(mock_ps.called)
|
|
self.assertEqual('updated', ret_val)
|
|
|
|
def test_state_set_stack_suspend(self, mock_ps):
|
|
mock_ps.return_value = 'updated'
|
|
ret_val = self.stack.state_set(
|
|
self.stack.SUSPEND, self.stack.IN_PROGRESS, 'Suspend started')
|
|
self.assertTrue(mock_ps.called)
|
|
# Ensure that state_set returns None for other actions in convergence
|
|
self.assertIsNone(ret_val)
|
|
mock_ps.reset_mock()
|
|
ret_val = self.stack.state_set(
|
|
self.stack.SUSPEND, self.stack.COMPLETE, 'Suspend complete')
|
|
self.assertFalse(mock_ps.called)
|
|
self.assertIsNone(ret_val)
|
|
|
|
def test_state_set_stack_resume(self, mock_ps):
|
|
ret_val = self.stack.state_set(
|
|
self.stack.RESUME, self.stack.IN_PROGRESS, 'Resume started')
|
|
self.assertTrue(mock_ps.called)
|
|
self.assertIsNone(ret_val)
|
|
mock_ps.reset_mock()
|
|
ret_val = self.stack.state_set(self.stack.RESUME, self.stack.COMPLETE,
|
|
'Resume complete')
|
|
self.assertFalse(mock_ps.called)
|
|
self.assertIsNone(ret_val)
|
|
|
|
def test_state_set_stack_snapshot(self, mock_ps):
|
|
ret_val = self.stack.state_set(
|
|
self.stack.SNAPSHOT, self.stack.IN_PROGRESS, 'Snapshot started')
|
|
self.assertTrue(mock_ps.called)
|
|
self.assertIsNone(ret_val)
|
|
mock_ps.reset_mock()
|
|
ret_val = self.stack.state_set(
|
|
self.stack.SNAPSHOT, self.stack.COMPLETE, 'Snapshot complete')
|
|
self.assertFalse(mock_ps.called)
|
|
self.assertIsNone(ret_val)
|
|
|
|
def test_state_set_stack_restore(self, mock_ps):
|
|
ret_val = self.stack.state_set(
|
|
self.stack.RESTORE, self.stack.IN_PROGRESS, 'Restore started')
|
|
self.assertTrue(mock_ps.called)
|
|
self.assertIsNone(ret_val)
|
|
mock_ps.reset_mock()
|
|
ret_val = self.stack.state_set(
|
|
self.stack.RESTORE, self.stack.COMPLETE, 'Restore complete')
|
|
self.assertFalse(mock_ps.called)
|
|
self.assertIsNone(ret_val)
|
|
|
|
|
|
class TestConvgStackRollback(common.HeatTestCase):
|
|
|
|
def setUp(self):
|
|
super(TestConvgStackRollback, self).setUp()
|
|
self.ctx = utils.dummy_context()
|
|
self.stack = tools.get_stack('test_stack_rollback', self.ctx,
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
|
|
def test_trigger_rollback_uses_old_template_if_available(self):
|
|
# create a template and assign to stack as previous template
|
|
t = template_format.parse(tools.wp_template)
|
|
prev_tmpl = templatem.Template(t)
|
|
prev_tmpl.store(context=self.ctx)
|
|
self.stack.prev_raw_template_id = prev_tmpl.id
|
|
# mock failure
|
|
self.stack.action = self.stack.UPDATE
|
|
self.stack.status = self.stack.FAILED
|
|
self.stack.store()
|
|
# mock converge_stack()
|
|
self.stack.converge_stack = mock.Mock()
|
|
# call trigger_rollbac
|
|
self.stack.rollback()
|
|
|
|
# Make sure stack converge is called with previous template
|
|
self.assertTrue(self.stack.converge_stack.called)
|
|
self.assertIsNone(self.stack.prev_raw_template_id)
|
|
call_args, call_kwargs = self.stack.converge_stack.call_args
|
|
template_used_for_rollback = call_args[0]
|
|
self.assertEqual(prev_tmpl.id, template_used_for_rollback.id)
|
|
|
|
def test_trigger_rollback_uses_empty_template_if_prev_tmpl_not_available(
|
|
self):
|
|
# mock create failure with no previous template
|
|
self.stack.prev_raw_template_id = None
|
|
self.stack.action = self.stack.CREATE
|
|
self.stack.status = self.stack.FAILED
|
|
self.stack.store()
|
|
# mock converge_stack()
|
|
self.stack.converge_stack = mock.Mock()
|
|
# call trigger_rollback
|
|
self.stack.rollback()
|
|
|
|
# Make sure stack converge is called with empty template
|
|
self.assertTrue(self.stack.converge_stack.called)
|
|
call_args, call_kwargs = self.stack.converge_stack.call_args
|
|
template_used_for_rollback = call_args[0]
|
|
self.assertEqual({}, template_used_for_rollback['resources'])
|
|
|
|
|
|
class TestConvgComputeDependencies(common.HeatTestCase):
|
|
def setUp(self):
|
|
super(TestConvgComputeDependencies, self).setUp()
|
|
self.ctx = utils.dummy_context()
|
|
self.stack = tools.get_stack('test_stack_convg', self.ctx,
|
|
template=tools.string_template_five,
|
|
convergence=True)
|
|
|
|
def _fake_db_resources(self, stack):
|
|
db_resources = {}
|
|
i = 0
|
|
for rsrc_name in ['E', 'D', 'C', 'B', 'A']:
|
|
i += 1
|
|
rsrc = mock.MagicMock()
|
|
rsrc.id = i
|
|
rsrc.name = rsrc_name
|
|
rsrc.current_template_id = stack.prev_raw_template_id
|
|
db_resources[i] = rsrc
|
|
db_resources[3].requires = [4, 5]
|
|
db_resources[1].requires = [3]
|
|
db_resources[2].requires = [3]
|
|
return db_resources
|
|
|
|
def test_dependencies_create_stack_without_mock(self):
|
|
self.stack.store()
|
|
self.current_resources = self.stack._update_or_store_resources()
|
|
self.stack._compute_convg_dependencies(self.stack.ext_rsrcs_db,
|
|
self.stack.dependencies,
|
|
self.current_resources)
|
|
self.assertEqual('Dependencies(['
|
|
'((1, True), (3, True)), '
|
|
'((2, True), (3, True)), '
|
|
'((3, True), (4, True)), '
|
|
'((3, True), (5, True))])',
|
|
repr(self.stack._convg_deps))
|
|
|
|
def test_dependencies_update_same_template(self):
|
|
t = template_format.parse(tools.string_template_five)
|
|
tmpl = templatem.Template(t)
|
|
self.stack.t = tmpl
|
|
self.stack.t.id = 2
|
|
self.stack.prev_raw_template_id = 1
|
|
db_resources = self._fake_db_resources(self.stack)
|
|
curr_resources = {res.name: res for id, res in db_resources.items()}
|
|
self.stack._compute_convg_dependencies(db_resources,
|
|
self.stack.dependencies,
|
|
curr_resources)
|
|
self.assertEqual('Dependencies(['
|
|
'((1, False), (1, True)), '
|
|
'((1, True), (3, True)), '
|
|
'((2, False), (2, True)), '
|
|
'((2, True), (3, True)), '
|
|
'((3, False), (1, False)), '
|
|
'((3, False), (2, False)), '
|
|
'((3, False), (3, True)), '
|
|
'((3, True), (4, True)), '
|
|
'((3, True), (5, True)), '
|
|
'((4, False), (3, False)), '
|
|
'((4, False), (4, True)), '
|
|
'((5, False), (3, False)), '
|
|
'((5, False), (5, True))])',
|
|
repr(self.stack._convg_deps))
|
|
|
|
def test_dependencies_update_new_template(self):
|
|
t = template_format.parse(tools.string_template_five_update)
|
|
tmpl = templatem.Template(t)
|
|
self.stack.t = tmpl
|
|
self.stack.t.id = 2
|
|
self.stack.prev_raw_template_id = 1
|
|
db_resources = self._fake_db_resources(self.stack)
|
|
|
|
curr_resources = {res.name: res for id, res in db_resources.items()}
|
|
# 'H', 'G', 'F' are part of new template
|
|
i = len(db_resources)
|
|
for new_rsrc in ['H', 'G', 'F']:
|
|
i += 1
|
|
rsrc = mock.MagicMock()
|
|
rsrc.name = new_rsrc
|
|
rsrc.id = i
|
|
curr_resources[new_rsrc] = rsrc
|
|
|
|
self.stack._compute_convg_dependencies(db_resources,
|
|
self.stack.dependencies,
|
|
curr_resources)
|
|
self.assertEqual('Dependencies(['
|
|
'((3, False), (1, False)), '
|
|
'((3, False), (2, False)), '
|
|
'((4, False), (3, False)), '
|
|
'((4, False), (4, True)), '
|
|
'((5, False), (3, False)), '
|
|
'((5, False), (5, True)), '
|
|
'((6, True), (8, True)), '
|
|
'((7, True), (8, True)), '
|
|
'((8, True), (4, True)), '
|
|
'((8, True), (5, True))])',
|
|
repr(self.stack._convg_deps))
|
|
|
|
def test_dependencies_update_replace_rollback(self):
|
|
t = template_format.parse(tools.string_template_five)
|
|
tmpl = templatem.Template(t)
|
|
self.stack.t = tmpl
|
|
self.stack.t.id = 1
|
|
self.stack.prev_raw_template_id = 2
|
|
db_resources = self._fake_db_resources(self.stack)
|
|
|
|
# previous resource E still exists in db.
|
|
db_resources[1].current_template_id = 1
|
|
# resource that replaced E
|
|
res = mock.MagicMock()
|
|
res.id = 6
|
|
res.name = 'E'
|
|
res.requires = [3]
|
|
res.replaces = 1
|
|
res.current_template_id = 2
|
|
db_resources[6] = res
|
|
|
|
curr_resources = {res.name: res for id, res in db_resources.items()}
|
|
# best existing resource
|
|
curr_resources['E'] = db_resources[1]
|
|
self.stack._compute_convg_dependencies(db_resources,
|
|
self.stack.dependencies,
|
|
curr_resources)
|
|
self.assertEqual('Dependencies(['
|
|
'((1, False), (1, True)), '
|
|
'((1, False), (6, False)), '
|
|
'((1, True), (3, True)), '
|
|
'((2, False), (2, True)), '
|
|
'((2, True), (3, True)), '
|
|
'((3, False), (1, False)), '
|
|
'((3, False), (2, False)), '
|
|
'((3, False), (3, True)), '
|
|
'((3, False), (6, False)), '
|
|
'((3, True), (4, True)), '
|
|
'((3, True), (5, True)), '
|
|
'((4, False), (3, False)), '
|
|
'((4, False), (4, True)), '
|
|
'((5, False), (3, False)), '
|
|
'((5, False), (5, True))])',
|
|
repr(self.stack._convg_deps))
|
|
|
|
def test_dependencies_update_delete(self):
|
|
tmpl = templatem.Template.create_empty_template(
|
|
version=self.stack.t.version)
|
|
self.stack.t = tmpl
|
|
self.stack.t.id = 2
|
|
self.stack.prev_raw_template_id = 1
|
|
db_resources = self._fake_db_resources(self.stack)
|
|
curr_resources = {res.name: res for id, res in db_resources.items()}
|
|
self.stack._compute_convg_dependencies(db_resources,
|
|
self.stack.dependencies,
|
|
curr_resources)
|
|
self.assertEqual('Dependencies(['
|
|
'((3, False), (1, False)), '
|
|
'((3, False), (2, False)), '
|
|
'((4, False), (3, False)), '
|
|
'((5, False), (3, False))])',
|
|
repr(self.stack._convg_deps))
|
|
|
|
|
|
class TestConvergenceMigration(common.HeatTestCase):
|
|
def test_migration_to_convergence_engine(self):
|
|
self.ctx = utils.dummy_context()
|
|
self.stack = tools.get_stack('test_stack_convg', self.ctx,
|
|
template=tools.string_template_five)
|
|
self.stack.store()
|
|
for r in self.stack.resources.values():
|
|
r._store()
|
|
self.stack.migrate_to_convergence()
|
|
self.stack = self.stack.load(self.ctx, self.stack.id)
|
|
|
|
self.assertTrue(self.stack.convergence)
|
|
self.assertIsNone(self.stack.prev_raw_template_id)
|
|
exp_required_by = {'A': ['C'], 'B': ['C'], 'C': ['D', 'E'],
|
|
'D': [], 'E': []}
|
|
exp_requires = {'A': [], 'B': [], 'C': ['A', 'B'], 'D': ['C'],
|
|
'E': ['C']}
|
|
exp_tmpl_id = self.stack.t.id
|
|
|
|
def id_to_name(ids):
|
|
names = []
|
|
for r in self.stack.resources.values():
|
|
if r.id in ids:
|
|
names.append(r.name)
|
|
return names
|
|
for r in self.stack.resources.values():
|
|
self.assertEqual(sorted(exp_required_by[r.name]),
|
|
sorted(r.required_by()))
|
|
self.assertEqual(sorted(exp_requires[r.name]),
|
|
sorted(id_to_name(r.requires)))
|
|
self.assertEqual(exp_tmpl_id, r.current_template_id)
|