diff --git a/heat/tests/convergence/__init__.py b/heat/tests/convergence/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/heat/tests/convergence/framework/__init__.py b/heat/tests/convergence/framework/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/heat/tests/convergence/framework/engine_wrapper.py b/heat/tests/convergence/framework/engine_wrapper.py new file mode 100644 index 0000000000..7ca3a4785d --- /dev/null +++ b/heat/tests/convergence/framework/engine_wrapper.py @@ -0,0 +1,103 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.db import api as db_api +from heat.engine import service +from heat.engine import stack +from heat.tests.convergence.framework import message_processor +from heat.tests.convergence.framework import message_queue +from heat.tests.convergence.framework import scenario_template +from heat.tests import utils + + +class Engine(message_processor.MessageProcessor): + ''' + Wrapper to the engine service. Methods of this + class will be called from the scenario tests. + ''' + + queue = message_queue.MessageQueue('engine') + + def __init__(self): + super(Engine, self).__init__('engine') + + def scenario_template_to_hot(self, scenario_tmpl): + ''' + Converts the scenario template into hot template. + ''' + hot_tmpl = {"heat_template_version": "2013-05-23"} + resources = {} + for res_name, res_def in scenario_tmpl.resources.iteritems(): + props = getattr(res_def, 'properties') + depends = getattr(res_def, 'depends_on') + res_defn = {"type": "OS::Heat::TestResource"} + if props: + props_def = {} + for prop_name, prop_value in props.items(): + if type(prop_value) == scenario_template.GetRes: + prop_res = getattr(prop_value, "target_name") + prop_value = {'get_resource': prop_res} + elif type(prop_value) == scenario_template.GetAtt: + prop_res = getattr(prop_value, "target_name") + prop_attr = getattr(prop_value, "attr") + prop_value = {'get_attr': [prop_res, prop_attr]} + props_def[prop_name] = prop_value + res_defn["properties"] = props_def + if depends: + res_defn["depends_on"] = depends + resources[res_name] = res_defn + hot_tmpl['resources'] = resources + return hot_tmpl + + @message_processor.asynchronous + def create_stack(self, stack_name, scenario_tmpl): + cnxt = utils.dummy_context() + srv = service.EngineService("host", "engine") + thread_group_mgr = service.ThreadGroupManager() + srv.thread_group_mgr = thread_group_mgr + hot_tmpl = self.scenario_template_to_hot(scenario_tmpl) + srv.create_stack(cnxt, stack_name, hot_tmpl, + params={}, files={}, args={}) + + @message_processor.asynchronous + def update_stack(self, stack_name, scenario_tmpl): + cnxt = utils.dummy_context() + db_stack = db_api.stack_get_by_name(cnxt, stack_name) + srv = service.EngineService("host", "engine") + thread_group_mgr = service.ThreadGroupManager() + srv.thread_group_mgr = thread_group_mgr + hot_tmpl = self.scenario_template_to_hot(scenario_tmpl) + stack_identity = {'stack_name': stack_name, + 'stack_id': db_stack.id, + 'tenant': db_stack.tenant, + 'path': ''} + srv.update_stack(cnxt, stack_identity, hot_tmpl, + params={}, files={}, args={}) + + @message_processor.asynchronous + def delete_stack(self, stack_name): + cnxt = utils.dummy_context() + db_stack = db_api.stack_get_by_name(cnxt, stack_name) + stack_identity = {'stack_name': stack_name, + 'stack_id': db_stack.id, + 'tenant': db_stack.tenant, + 'path': ''} + srv = service.EngineService("host", "engine") + srv.delete_stack(cnxt, stack_identity) + + @message_processor.asynchronous + def rollback_stack(self, stack_name): + cntxt = utils.dummy_context() + db_stack = db_api.stack_get_by_name(cntxt, stack_name) + stk = stack.Stack.load(cntxt, stack=db_stack) + stk.rollback() diff --git a/heat/tests/convergence/framework/event_loop.py b/heat/tests/convergence/framework/event_loop.py new file mode 100644 index 0000000000..2ca9e19b4d --- /dev/null +++ b/heat/tests/convergence/framework/event_loop.py @@ -0,0 +1,22 @@ +# +# 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. + + +class EventLoop(object): + + def __init__(self, *processors): + self.processors = processors + + def __call__(self): + while any([processor() for processor in self.processors]): + continue diff --git a/heat/tests/convergence/framework/fake_resource.py b/heat/tests/convergence/framework/fake_resource.py new file mode 100644 index 0000000000..b5176e836e --- /dev/null +++ b/heat/tests/convergence/framework/fake_resource.py @@ -0,0 +1,96 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.common import exception +from heat.common.i18n import _ +from heat.engine import attributes +from heat.engine import properties +from heat.engine import resource +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +class TestResource(resource.Resource): + + PROPERTIES = ( + A, C, CA, rA, rB + ) = ( + 'a', 'c', 'ca', '!a', '!b' + ) + + ATTRIBUTES = ( + A, rA + ) = ( + 'a', '!a' + ) + + properties_schema = { + A: properties.Schema( + properties.Schema.STRING, + _('Fake property a.'), + default='a', + update_allowed=True + ), + C: properties.Schema( + properties.Schema.STRING, + _('Fake property c.'), + update_allowed=True, + default='c' + ), + CA: properties.Schema( + properties.Schema.STRING, + _('Fake property ca.'), + update_allowed=True, + default='ca' + ), + rA: properties.Schema( + properties.Schema.STRING, + _('Fake property !a.'), + update_allowed=True, + default='!a' + ), + rB: properties.Schema( + properties.Schema.STRING, + _('Fake property !c.'), + update_allowed=True, + default='!b' + ), + } + + attributes_schema = { + A: attributes.Schema( + _('Fake attribute a.'), + cache_mode=attributes.Schema.CACHE_NONE + ), + rA: attributes.Schema( + _('Fake attribute !a.'), + cache_mode=attributes.Schema.CACHE_NONE + ), + } + + def handle_create(self): + for prop in self.properties.props.keys(): + self.data_set(prop, self.properties.get(prop), redact=False) + + self.resource_id_set(self.physical_resource_name()) + + def handle_update(self, json_snippet=None, tmpl_diff=None, prop_diff=None): + for prop in prop_diff: + if '!' in prop: + raise exception.UpdateReplace(self.name) + self.data_set(prop, prop_diff.get(prop), redact=False) + + def _resolve_attribute(self, name): + if name in self.attributes: + return self.data().get(name) diff --git a/heat/tests/convergence/framework/message_processor.py b/heat/tests/convergence/framework/message_processor.py new file mode 100644 index 0000000000..7aff5353ac --- /dev/null +++ b/heat/tests/convergence/framework/message_processor.py @@ -0,0 +1,109 @@ +# +# 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 functools +import inspect + +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +def asynchronous(function): + '''Decorator for MessageProcessor methods to make them asynchronous. + + To use, simply call the method as usual. Instead of being executed + immediately, it will be placed on the queue for the MessageProcessor and + run on a future iteration of the event loop. + ''' + arg_names = inspect.getargspec(function).args + MessageData = collections.namedtuple(function.__name__, arg_names[1:]) + + @functools.wraps(function) + def call_or_send(processor, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], MessageData): + try: + return function(processor, **args[0]._asdict()) + except Exception as exc: + LOG.exception('[%s] Exception in "%s": %s', + processor.name, function.__name__, exc) + raise + else: + data = inspect.getcallargs(function, processor, *args, **kwargs) + data.pop(arg_names[0]) # lose self + return processor.queue.send(function.__name__, + MessageData(**data)) + + call_or_send.MessageData = MessageData + return call_or_send + + +class MessageProcessor(object): + + queue = None + + def __init__(self, name): + self.name = name + + def __call__(self): + message = self.queue.get() + if message is None: + LOG.debug('[%s] No messages' % self.name) + return False + + try: + method = getattr(self, message.name) + except AttributeError: + LOG.error('[%s] Bad message name "%s"' % (self.name, + message.name)) + raise + else: + LOG.info('[%s] %r' % (self.name, message.data)) + + method(message.data) + return True + + @asynchronous + def noop(self, count=1): + ''' + Insert No-op operations in the message queue. + ''' + assert isinstance(count, int) + if count > 1: + self.queue.send_priority('noop', + self.noop.MessageData(count - 1)) + + @asynchronous + def _execute(self, func): + ''' + Insert a function call in the message queue. + + The function takes no arguments, so use functools.partial to curry the + arguments before passing it here. + ''' + func() + + def call(self, func, *args, **kwargs): + ''' + Insert a function call in the message queue. + ''' + self._execute(functools.partial(func, *args, **kwargs)) + + def clear(self): + ''' + Delete all the messages from the queue. + ''' + self.queue.clear() + +__all__ = ['MessageProcessor', 'asynchronous'] diff --git a/heat/tests/convergence/framework/message_queue.py b/heat/tests/convergence/framework/message_queue.py new file mode 100644 index 0000000000..ea2b7496e3 --- /dev/null +++ b/heat/tests/convergence/framework/message_queue.py @@ -0,0 +1,39 @@ +# +# 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 + + +Message = collections.namedtuple('Message', ['name', 'data']) + + +class MessageQueue(object): + + def __init__(self, name): + self.name = name + self._queue = collections.deque() + + def send(self, name, data=None): + self._queue.append(Message(name, data)) + + def send_priority(self, name, data=None): + self._queue.appendleft(Message(name, data)) + + def get(self): + try: + return self._queue.popleft() + except IndexError: + return None + + def clear(self): + self._queue.clear() diff --git a/heat/tests/convergence/framework/processes.py b/heat/tests/convergence/framework/processes.py new file mode 100644 index 0000000000..b6bca92cbd --- /dev/null +++ b/heat/tests/convergence/framework/processes.py @@ -0,0 +1,44 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.tests.convergence.framework import engine_wrapper +from heat.tests.convergence.framework import event_loop as event_loop_module +from heat.tests.convergence.framework import worker_wrapper + + +engine = None +worker = None +event_loop = None + + +class Processes(object): + + def __init__(self): + global engine + global worker + global event_loop + + engine = engine_wrapper.Engine() + worker = worker_wrapper.Worker() + + event_loop = event_loop_module.EventLoop(engine, worker) + + self.engine = engine + self.worker = worker + self.event_loop = event_loop + + def clear(self): + self.engine.clear() + self.worker.clear() + +Processes() diff --git a/heat/tests/convergence/framework/reality.py b/heat/tests/convergence/framework/reality.py new file mode 100644 index 0000000000..5e8ab956e3 --- /dev/null +++ b/heat/tests/convergence/framework/reality.py @@ -0,0 +1,51 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.common import exception +from heat.db import api as db_api +from heat.tests import utils + + +class RealityStore(object): + + def __init__(self): + self.cntxt = utils.dummy_context() + + def resources_by_logical_name(self, logical_name): + ret = [] + resources = db_api.resource_get_all(self.cntxt) + for res in resources: + if (res.name == logical_name and res.action in ("CREATE", "UPDATE") + and res.status == "COMPLETE"): + ret.append(res) + return ret + + def all_resources(self): + try: + resources = db_api.resource_get_all(self.cntxt) + except exception.NotFound: + return [] + + ret = [] + for res in resources: + if res.action in ("CREATE", "UPDATE") and res.status == "COMPLETE": + ret.append(res) + return ret + + def resource_properties(self, res, prop_name): + res_data = db_api.resource_data_get_by_key(self.cntxt, + res.id, + prop_name) + return res_data.value + +reality = RealityStore() diff --git a/heat/tests/convergence/framework/scenario.py b/heat/tests/convergence/framework/scenario.py new file mode 100644 index 0000000000..bf74480ce5 --- /dev/null +++ b/heat/tests/convergence/framework/scenario.py @@ -0,0 +1,49 @@ +# +# 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 os +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +def list_all(): + scenario_dir = os.path.join(os.path.dirname(__file__), '../scenarios') + if not os.path.isdir(scenario_dir): + LOG.error('Scenario directory "%s" not found', scenario_dir) + return + + for root, dirs, files in os.walk(scenario_dir): + for filename in files: + name, ext = os.path.splitext(filename) + if ext == '.py': + LOG.debug('Found scenario "%s"', name) + yield name, os.path.join(root, filename) + + +class Scenario(object): + + def __init__(self, name, path): + self.name = name + + with open(path) as f: + source = f.read() + + self.code = compile(source, path, 'exec') + LOG.debug('Loaded scenario %s', self.name) + + def __call__(self, _event_loop, **global_env): + LOG.info('*** Beginning scenario "%s"', self.name) + + exec(self.code, global_env, {}) + _event_loop() diff --git a/heat/tests/convergence/framework/scenario_template.py b/heat/tests/convergence/framework/scenario_template.py new file mode 100644 index 0000000000..b19ee66e89 --- /dev/null +++ b/heat/tests/convergence/framework/scenario_template.py @@ -0,0 +1,39 @@ +# +# 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. + + +class GetRes(object): + def __init__(self, target_name): + self.target_name = target_name + + +class GetAtt(GetRes): + def __init__(self, target_name, attr): + super(GetAtt, self).__init__(target_name) + self.attr = attr + + +class RsrcDef(object): + def __init__(self, properties, depends_on): + self.properties = properties + self.depends_on = depends_on + + +class Template(object): + + def __init__(self, resources={}, key=None): + self.key = key + self.resources = resources + + def __repr__(self): + return 'Template(%r)' % self.resources diff --git a/heat/tests/convergence/framework/testutils.py b/heat/tests/convergence/framework/testutils.py new file mode 100644 index 0000000000..43b37687b8 --- /dev/null +++ b/heat/tests/convergence/framework/testutils.py @@ -0,0 +1,70 @@ +# +# 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 functools +from oslo_log import log as logging + +from heat.tests.convergence.framework import reality +from heat.tests.convergence.framework import scenario_template + +LOG = logging.getLogger(__name__) + + +def verify(test, reality, tmpl): + for name in tmpl.resources: + rsrc_count = len(reality.resources_by_logical_name(name)) + test.assertEqual(1, rsrc_count, + 'Found %d copies of resource "%s"' % (rsrc_count, + name)) + + all_rsrcs = reality.all_resources() + + for name, defn in tmpl.resources.items(): + phys_rsrc = reality.resources_by_logical_name(name)[0] + + for prop_name, prop_def in defn.properties.items(): + real_value = reality.resource_properties(phys_rsrc, prop_name) + + if isinstance(prop_def, scenario_template.GetAtt): + targs = reality.resources_by_logical_name(prop_def.target_name) + att_value = targs[0].properties_data[prop_def.attr] + test.assertEqual(att_value, real_value) + + elif isinstance(prop_def, scenario_template.GetRes): + targs = reality.resources_by_logical_name(prop_def.target_name) + test.assertEqual(targs[0].nova_instance, real_value) + + else: + test.assertEqual(prop_def, real_value) + + test.assertEqual(len(defn.properties), len(phys_rsrc.properties_data)) + + test.assertEqual(len(tmpl.resources), len(all_rsrcs)) + + +def scenario_globals(procs, testcase): + return { + 'test': testcase, + 'reality': reality.reality, + 'verify': functools.partial(verify, + testcase, + reality.reality), + + 'Template': scenario_template.Template, + 'RsrcDef': scenario_template.RsrcDef, + 'GetRes': scenario_template.GetRes, + 'GetAtt': scenario_template.GetAtt, + + 'engine': procs.engine, + 'worker': procs.worker, + } diff --git a/heat/tests/convergence/framework/worker_wrapper.py b/heat/tests/convergence/framework/worker_wrapper.py new file mode 100644 index 0000000000..eb52d9463e --- /dev/null +++ b/heat/tests/convergence/framework/worker_wrapper.py @@ -0,0 +1,35 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.engine import worker +from heat.tests.convergence.framework import message_processor +from heat.tests.convergence.framework import message_queue + + +class Worker(message_processor.MessageProcessor): + + queue = message_queue.MessageQueue('worker') + + def __init__(self): + super(Worker, self).__init__('worker') + + @message_processor.asynchronous + def check_resource(self, ctxt, resource_id, + current_traversal, data, + is_update, adopt_stack_data): + worker.WorkerService("fake_host", "fake_topic", + "fake_engine", "tgm").check_resource( + ctxt, resource_id, + current_traversal, + data, is_update, + adopt_stack_data) diff --git a/heat/tests/convergence/scenarios/basic_create.py b/heat/tests/convergence/scenarios/basic_create.py new file mode 100644 index 0000000000..63a76441c3 --- /dev/null +++ b/heat/tests/convergence/scenarios/basic_create.py @@ -0,0 +1,24 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) diff --git a/heat/tests/convergence/scenarios/basic_create_rollback.py b/heat/tests/convergence/scenarios/basic_create_rollback.py new file mode 100644 index 0000000000..dee46ba2e3 --- /dev/null +++ b/heat/tests/convergence/scenarios/basic_create_rollback.py @@ -0,0 +1,26 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(3) +engine.rollback_stack('foo') +engine.noop(6) +engine.call(verify, Template()) diff --git a/heat/tests/convergence/scenarios/basic_update_delete.py b/heat/tests/convergence/scenarios/basic_update_delete.py new file mode 100644 index 0000000000..53b71c2fa7 --- /dev/null +++ b/heat/tests/convergence/scenarios/basic_update_delete.py @@ -0,0 +1,43 @@ +# +# 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. + + +def check_resource_count(expected_count): + test.assertEqual(expected_count, len(reality.all_resources())) + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(2) + +example_template2 = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), + 'F': RsrcDef({}, ['D', 'E']), +}) +engine.update_stack('foo', example_template2) +engine.call(check_resource_count, 3) +engine.noop(11) +engine.call(verify, example_template2) + +engine.delete_stack('foo') +engine.noop(6) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/create_early_delete.py b/heat/tests/convergence/scenarios/create_early_delete.py new file mode 100644 index 0000000000..232c5581ff --- /dev/null +++ b/heat/tests/convergence/scenarios/create_early_delete.py @@ -0,0 +1,27 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(2) + +engine.delete_stack('foo') +engine.noop(6) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/disjoint_create.py b/heat/tests/convergence/scenarios/disjoint_create.py new file mode 100644 index 0000000000..2d2c41541b --- /dev/null +++ b/heat/tests/convergence/scenarios/disjoint_create.py @@ -0,0 +1,24 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) diff --git a/heat/tests/convergence/scenarios/multiple_update.py b/heat/tests/convergence/scenarios/multiple_update.py new file mode 100644 index 0000000000..a969ba7c42 --- /dev/null +++ b/heat/tests/convergence/scenarios/multiple_update.py @@ -0,0 +1,50 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template_shrunk = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), +}) +engine.update_stack('foo', example_template_shrunk) +engine.noop(10) +engine.call(verify, example_template_shrunk) + +example_template_long = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), + 'F': RsrcDef({}, ['D', 'E']), +}) +engine.update_stack('foo', example_template_long) +engine.noop(12) +engine.call(verify, example_template_long) + +engine.delete_stack('foo') +engine.noop(6) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/update_add.py b/heat/tests/convergence/scenarios/update_add.py new file mode 100644 index 0000000000..27e2e6fc24 --- /dev/null +++ b/heat/tests/convergence/scenarios/update_add.py @@ -0,0 +1,36 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template2 = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), + 'F': RsrcDef({}, ['D', 'E']), +}) +engine.update_stack('foo', example_template2) +engine.noop(11) +engine.call(verify, example_template2) diff --git a/heat/tests/convergence/scenarios/update_add_concurrent.py b/heat/tests/convergence/scenarios/update_add_concurrent.py new file mode 100644 index 0000000000..7726269d11 --- /dev/null +++ b/heat/tests/convergence/scenarios/update_add_concurrent.py @@ -0,0 +1,39 @@ +# +# 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. + + +def check_resource_count(expected_count): + test.assertEqual(expected_count, len(reality.all_resources())) + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(2) + +example_template2 = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), + 'F': RsrcDef({}, ['D', 'E']), +}) +engine.update_stack('foo', example_template2) +engine.call(check_resource_count, 3) +engine.noop(11) +engine.call(verify, example_template2) diff --git a/heat/tests/convergence/scenarios/update_add_rollback.py b/heat/tests/convergence/scenarios/update_add_rollback.py new file mode 100644 index 0000000000..b4df19938b --- /dev/null +++ b/heat/tests/convergence/scenarios/update_add_rollback.py @@ -0,0 +1,39 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template2 = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), + 'F': RsrcDef({}, ['A']), +}) +engine.update_stack('foo', example_template2) +engine.noop(4) + +engine.rollback_stack('foo') +engine.noop(8) +engine.call(verify, example_template) diff --git a/heat/tests/convergence/scenarios/update_add_rollback_early.py b/heat/tests/convergence/scenarios/update_add_rollback_early.py new file mode 100644 index 0000000000..01e2e0f3c2 --- /dev/null +++ b/heat/tests/convergence/scenarios/update_add_rollback_early.py @@ -0,0 +1,39 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template2 = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), + 'F': RsrcDef({}, ['D']), +}) +engine.update_stack('foo', example_template2) +engine.noop(4) + +engine.rollback_stack('foo') +engine.noop(8) +engine.call(verify, example_template) diff --git a/heat/tests/convergence/scenarios/update_remove.py b/heat/tests/convergence/scenarios/update_remove.py new file mode 100644 index 0000000000..da52ca8fb6 --- /dev/null +++ b/heat/tests/convergence/scenarios/update_remove.py @@ -0,0 +1,34 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template2 = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), +}) +engine.update_stack('foo', example_template2) +engine.noop(9) +engine.call(verify, example_template2) diff --git a/heat/tests/convergence/scenarios/update_remove_rollback.py b/heat/tests/convergence/scenarios/update_remove_rollback.py new file mode 100644 index 0000000000..2a942330b4 --- /dev/null +++ b/heat/tests/convergence/scenarios/update_remove_rollback.py @@ -0,0 +1,51 @@ +# +# 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. + + +b_uuid = None + +def store_b_uuid(): + global b_uuid + b_uuid = next(iter(reality.resources_by_logical_name('B'))).uuid + +def check_b_not_replaced(): + test.assertEqual(b_uuid, + next(iter(reality.resources_by_logical_name('B'))).uuid) + test.assertIsNot(b_uuid, None) + +example_template = Template({ + 'A': RsrcDef({}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A', 'B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) +engine.call(store_b_uuid) + +example_template2 = Template({ + 'A': RsrcDef({}, []), + 'C': RsrcDef({'a': '4alpha'}, ['A']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', 'a')}, []), +}) +engine.update_stack('foo', example_template2) +engine.noop(2) + +engine.rollback_stack('foo') +engine.noop(10) + +engine.call(verify, example_template) +engine.call(check_b_not_replaced) diff --git a/heat/tests/convergence/scenarios/update_replace.py b/heat/tests/convergence/scenarios/update_replace.py new file mode 100644 index 0000000000..e74b0e3f65 --- /dev/null +++ b/heat/tests/convergence/scenarios/update_replace.py @@ -0,0 +1,65 @@ +# +# 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. + + +c_uuid = None + +def store_c_uuid(): + global c_uuid + c_uuid = next(iter(reality.resources_by_logical_name('C'))).uuid + +def check_c_replaced(): + test.assertNotEqual(c_uuid, + next(iter(reality.resources_by_logical_name('C'))).uuid) + test.assertIsNot(c_uuid, None) + + +example_template = Template({ + 'A': RsrcDef({'a': 'initial'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) +engine.call(store_c_uuid) + +example_template_updated = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.update_stack('foo', example_template_updated) +engine.noop(11) +engine.call(verify, example_template_updated) + +example_template_long = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), + 'F': RsrcDef({}, ['D', 'E']), +}) +engine.update_stack('foo', example_template_long) +engine.noop(12) +engine.call(verify, example_template_long) +engine.call(check_c_replaced) + +engine.delete_stack('foo') +engine.noop(6) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/update_replace_invert_deps.py b/heat/tests/convergence/scenarios/update_replace_invert_deps.py new file mode 100644 index 0000000000..265b72cb50 --- /dev/null +++ b/heat/tests/convergence/scenarios/update_replace_invert_deps.py @@ -0,0 +1,42 @@ +# +# 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. + + +def check_resource_counts(count_map): + for name, count in count_map.items(): + test.assertEqual(count, + len(list(reality.resources_by_logical_name(name)))) + + +example_template = Template({ + 'A': RsrcDef({'!a': 'initial'}, []), + 'B': RsrcDef({'!b': 'first'}, ['A']), +}) +engine.create_stack('foo', example_template) +engine.noop(4) +engine.call(verify, example_template) + +example_template_inverted = Template({ + 'A': RsrcDef({'!a': 'updated'}, ['B']), + 'B': RsrcDef({'!b': 'second'}, []), +}) +engine.update_stack('foo', example_template_inverted) +engine.noop(4) +engine.call(check_resource_counts, {'A': 2, 'B': 1}) +engine.noop(2) +engine.call(verify, example_template_inverted) +engine.call(check_resource_counts, {'A': 1, 'B': 1}) + +engine.delete_stack('foo') +engine.noop(3) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/update_replace_missed_cleanup.py b/heat/tests/convergence/scenarios/update_replace_missed_cleanup.py new file mode 100644 index 0000000000..4c17a35c6c --- /dev/null +++ b/heat/tests/convergence/scenarios/update_replace_missed_cleanup.py @@ -0,0 +1,55 @@ +# +# 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. + + +def check_c_count(expected_count): + test.assertEqual(expected_count, + len(reality.resources_by_logical_name('C'))) + +example_template = Template({ + 'A': RsrcDef({'a': 'initial'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template_shrunk = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.update_stack('foo', example_template_shrunk) +engine.noop(7) + +example_template_long = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), + 'F': RsrcDef({}, ['D', 'E']), +}) +engine.update_stack('foo', example_template_long) +engine.call(check_c_count, 2) +engine.noop(11) +engine.call(verify, example_template_long) + +engine.delete_stack('foo') +engine.noop(12) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/update_replace_missed_cleanup_delete.py b/heat/tests/convergence/scenarios/update_replace_missed_cleanup_delete.py new file mode 100644 index 0000000000..83285bd15c --- /dev/null +++ b/heat/tests/convergence/scenarios/update_replace_missed_cleanup_delete.py @@ -0,0 +1,43 @@ +# +# 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. + + +def check_c_count(expected_count): + test.assertEqual(expected_count, + len(reality.resources_by_logical_name('C'))) + +example_template = Template({ + 'A': RsrcDef({'a': 'initial'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template_shrunk = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.update_stack('foo', example_template_shrunk) +engine.noop(7) + +engine.delete_stack('foo') +engine.call(check_c_count, 2) +engine.noop(11) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/update_replace_rollback.py b/heat/tests/convergence/scenarios/update_replace_rollback.py new file mode 100644 index 0000000000..780057cac4 --- /dev/null +++ b/heat/tests/convergence/scenarios/update_replace_rollback.py @@ -0,0 +1,47 @@ +# +# 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. + + +def check_c_count(expected_count): + test.assertEqual(expected_count, + len(reality.resources_by_logical_name('C'))) + +example_template = Template({ + 'A': RsrcDef({'a': 'initial'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template2 = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.update_stack('foo', example_template2) +engine.noop(4) + +engine.rollback_stack('foo') +engine.call(check_c_count, 2) +engine.noop(11) +engine.call(verify, example_template) + +engine.delete_stack('foo') +engine.noop(12) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/update_user_replace.py b/heat/tests/convergence/scenarios/update_user_replace.py new file mode 100644 index 0000000000..089e005bba --- /dev/null +++ b/heat/tests/convergence/scenarios/update_user_replace.py @@ -0,0 +1,65 @@ +# +# 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. + + +c_uuid = None + +def store_c_uuid(): + global c_uuid + c_uuid = next(iter(reality.resources_by_logical_name('C'))).uuid + +def check_c_replaced(): + test.assertNotEqual(c_uuid, + next(iter(reality.resources_by_logical_name('newC'))).uuid) + test.assertIsNot(c_uuid, None) + + +example_template = Template({ + 'A': RsrcDef({'a': 'initial'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) +engine.call(store_c_uuid) + +example_template_updated = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'newC': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('newC')}, []), + 'E': RsrcDef({'ca': GetAtt('newC', '!a')}, []), +}) +engine.update_stack('foo', example_template_updated) +engine.noop(11) +engine.call(verify, example_template_updated) + +example_template_long = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'newC': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('newC')}, []), + 'E': RsrcDef({'ca': GetAtt('newC', '!a')}, []), + 'F': RsrcDef({}, ['D', 'E']), +}) +engine.update_stack('foo', example_template_long) +engine.noop(12) +engine.call(verify, example_template_long) +engine.call(check_c_replaced) + +engine.delete_stack('foo') +engine.noop(6) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/scenarios/update_user_replace_rollback.py b/heat/tests/convergence/scenarios/update_user_replace_rollback.py new file mode 100644 index 0000000000..45e45452fd --- /dev/null +++ b/heat/tests/convergence/scenarios/update_user_replace_rollback.py @@ -0,0 +1,42 @@ +# +# 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. + + +example_template = Template({ + 'A': RsrcDef({'a': 'initial'}, []), + 'B': RsrcDef({}, []), + 'C': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('C')}, []), + 'E': RsrcDef({'ca': GetAtt('C', '!a')}, []), +}) +engine.create_stack('foo', example_template) +engine.noop(5) +engine.call(verify, example_template) + +example_template_updated = Template({ + 'A': RsrcDef({'a': 'updated'}, []), + 'B': RsrcDef({}, []), + 'newC': RsrcDef({'!a': GetAtt('A', 'a')}, ['B']), + 'D': RsrcDef({'c': GetRes('newC')}, []), + 'E': RsrcDef({'ca': GetAtt('newC', '!a')}, []), +}) +engine.update_stack('foo', example_template_updated) +engine.noop(3) + +engine.rollback_stack('foo') +engine.noop(12) +engine.call(verify, example_template) + +engine.delete_stack('foo') +engine.noop(6) +engine.call(verify, Template({})) diff --git a/heat/tests/convergence/test_converge.py b/heat/tests/convergence/test_converge.py new file mode 100755 index 0000000000..450a233dbc --- /dev/null +++ b/heat/tests/convergence/test_converge.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.engine import resource +from heat.tests import common +from heat.tests.convergence.framework import fake_resource +from heat.tests.convergence.framework import processes +from heat.tests.convergence.framework import scenario +from heat.tests.convergence.framework import testutils +from oslo_config import cfg + + +class ScenarioTest(common.HeatTestCase): + + scenarios = [(name, {'name': name, 'path': path}) + for name, path in scenario.list_all()] + + def setUp(self): + super(ScenarioTest, self).setUp() + resource._register_class('OS::Heat::TestResource', + fake_resource.TestResource) + self.procs = processes.Processes() + po = self.patch("heat.rpc.worker_client.WorkerClient.check_resource") + po.side_effect = self.procs.worker.check_resource + cfg.CONF.set_default('convergence_engine', True) + + def tearDown(self): + super(ScenarioTest, self).tearDown() + + def test_scenario(self): + self.procs.clear() + runner = scenario.Scenario(self.name, self.path) + runner(self.procs.event_loop, + **testutils.scenario_globals(self.procs, self)) diff --git a/tox.ini b/tox.ini index f91bf8d840..db44298605 100644 --- a/tox.ini +++ b/tox.ini @@ -73,7 +73,7 @@ commands = bandit -c bandit.yaml -r heat -n5 -p heat_conservative # H405 multi line docstring summary not separated with an empty line ignore = H404,H405 show-source = true -exclude=.*,dist,*openstack/common*,*lib/python*,*egg,tools,build +exclude=.*,dist,*openstack/common*,*lib/python*,*egg,tools,build,*convergence/scenarios/* max-complexity=20 [hacking]