diff --git a/heat/engine/api.py b/heat/engine/api.py index f0578a09c1..33aeeef7c6 100644 --- a/heat/engine/api.py +++ b/heat/engine/api.py @@ -109,6 +109,7 @@ def format_stack_resource(resource, detail=True): api.RES_ID: dict(resource.identifier()), api.RES_STACK_ID: dict(resource.stack.identifier()), api.RES_STACK_NAME: resource.stack.name, + api.RES_REQUIRED_BY: resource.required_by(), } if detail: diff --git a/heat/engine/dependencies.py b/heat/engine/dependencies.py index f8f6e62720..dc5d7d2331 100644 --- a/heat/engine/dependencies.py +++ b/heat/engine/dependencies.py @@ -168,6 +168,15 @@ class Dependencies(object): return self + def required_by(self, last): + ''' + List the keys that require the specified node. + ''' + if last not in self._graph: + raise KeyError + + return self._graph[last].required_by() + def __getitem__(self, last): ''' Return a partial dependency graph consisting of the specified node and diff --git a/heat/engine/resource.py b/heat/engine/resource.py index cf1f5a095c..6dc8febee8 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -301,6 +301,14 @@ class Resource(object): self._add_dependencies(deps, None, self.t) deps += (self, None) + def required_by(self): + ''' + Returns a list of names of resources which directly require this + resource as a dependency. + ''' + return list( + [r.name for r in self.stack.dependencies.required_by(self)]) + def keystone(self): return self.stack.clients.keystone() diff --git a/heat/rpc/api.py b/heat/rpc/api.py index f9dbacecc8..23adca6b38 100644 --- a/heat/rpc/api.py +++ b/heat/rpc/api.py @@ -50,12 +50,12 @@ RES_KEYS = ( RES_DESCRIPTION, RES_UPDATED_TIME, RES_NAME, RES_PHYSICAL_ID, RES_METADATA, RES_ACTION, RES_STATUS, RES_STATUS_DATA, RES_TYPE, - RES_ID, RES_STACK_ID, RES_STACK_NAME, + RES_ID, RES_STACK_ID, RES_STACK_NAME, RES_REQUIRED_BY, ) = ( 'description', 'updated_time', 'logical_resource_id', 'physical_resource_id', 'metadata', 'resource_action', 'resource_status', 'resource_status_reason', - 'resource_type', 'resource_identity', STACK_ID, STACK_NAME, + 'resource_type', 'resource_identity', STACK_ID, STACK_NAME, 'required_by', ) EVENT_KEYS = ( diff --git a/heat/tests/test_dependencies.py b/heat/tests/test_dependencies.py index eba83d093b..9196eae3e1 100644 --- a/heat/tests/test_dependencies.py +++ b/heat/tests/test_dependencies.py @@ -194,3 +194,24 @@ class dependenciesTest(testtools.TestCase): for n in ('last', 'mid1', 'mid2', 'mid3'): self.assertTrue(n in order, "'%s' not found in dependency order" % n) + + def test_required_by(self): + d = Dependencies([('last', 'e1'), ('last', 'mid1'), ('last', 'mid2'), + ('mid1', 'e2'), ('mid1', 'mid3'), + ('mid2', 'mid3'), + ('mid3', 'e3')]) + + self.assertEqual(0, len(list(d.required_by('last')))) + + required_by = list(d.required_by('mid3')) + self.assertEqual(len(required_by), 2) + for n in ('mid1', 'mid2'): + self.assertTrue(n in required_by, + "'%s' not found in required_by" % n) + + required_by = list(d.required_by('e2')) + self.assertEqual(len(required_by), 1) + self.assertTrue('mid1' in required_by, + "'%s' not found in required_by" % n) + + self.assertRaises(KeyError, d.required_by, 'foo') diff --git a/heat/tests/test_engine_api_utils.py b/heat/tests/test_engine_api_utils.py index 9dbb8968b9..f176be2388 100644 --- a/heat/tests/test_engine_api_utils.py +++ b/heat/tests/test_engine_api_utils.py @@ -12,9 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. - -from heat.tests.common import HeatTestCase +from heat.common import context import heat.engine.api as api +from heat.engine import parser +from heat.engine import resource +from heat.openstack.common import uuidutils +from heat.rpc import api as rpc_api +from heat.tests.common import HeatTestCase +from heat.tests import generic_resource as generic_rsrc +from heat.tests.utils import setup_dummy_db class EngineApiTest(HeatTestCase): @@ -71,3 +77,59 @@ class EngineApiTest(HeatTestCase): def test_disable_rollback_extract_bad(self): self.assertRaises(ValueError, api.extract_args, {'disable_rollback': 'bad'}) + + +class FormatTest(HeatTestCase): + + def setUp(self): + super(FormatTest, self).setUp() + setup_dummy_db() + ctx = context.get_admin_context() + self.m.StubOutWithMock(ctx, 'user') + ctx.user = 'test_user' + ctx.tenant_id = 'test_tenant' + + template = parser.Template({ + 'Resources': { + 'generic1': {'Type': 'GenericResourceType'}, + 'generic2': { + 'Type': 'GenericResourceType', + 'DependsOn': 'generic1'} + } + }) + resource._register_class('GenericResourceType', + generic_rsrc.GenericResource) + self.stack = parser.Stack(ctx, 'test_stack', template, + stack_id=uuidutils.generate_uuid()) + + def test_format_stack_resource(self): + res = self.stack['generic1'] + + resource_keys = set(( + rpc_api.RES_UPDATED_TIME, + rpc_api.RES_NAME, + rpc_api.RES_PHYSICAL_ID, + rpc_api.RES_METADATA, + rpc_api.RES_ACTION, + rpc_api.RES_STATUS, + rpc_api.RES_STATUS_DATA, + rpc_api.RES_TYPE, + rpc_api.RES_ID, + rpc_api.RES_STACK_ID, + rpc_api.RES_STACK_NAME, + rpc_api.RES_REQUIRED_BY)) + + resource_details_keys = resource_keys.union(set( + (rpc_api.RES_DESCRIPTION, rpc_api.RES_METADATA))) + + formatted = api.format_stack_resource(res, True) + self.assertEqual(resource_details_keys, set(formatted.keys())) + + formatted = api.format_stack_resource(res, False) + self.assertEqual(resource_keys, set(formatted.keys())) + + def test_format_stack_resource_required_by(self): + res1 = api.format_stack_resource(self.stack['generic1']) + res2 = api.format_stack_resource(self.stack['generic2']) + self.assertEqual(res1['required_by'], ['generic2']) + self.assertEqual(res2['required_by'], []) diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 51f8789d7b..62993033f7 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -1479,3 +1479,33 @@ class StackTest(HeatTestCase): (rsrc.UPDATE, rsrc.FAILED)): rsrc.state_set(action, status) self.assertEqual(None, self.stack.output('TestOutput')) + + @stack_delete_after + def test_resource_required_by(self): + tmpl = {'Resources': {'AResource': {'Type': 'GenericResourceType'}, + 'BResource': {'Type': 'GenericResourceType', + 'DependsOn': 'AResource'}, + 'CResource': {'Type': 'GenericResourceType', + 'DependsOn': 'BResource'}, + 'DResource': {'Type': 'GenericResourceType', + 'DependsOn': 'BResource'}}} + + self.m.StubOutWithMock(scheduler.TaskRunner, '_sleep') + scheduler.TaskRunner._sleep(mox.IsA(int)).MultipleTimes() + mox.Replay(scheduler.TaskRunner._sleep) + + self.stack = parser.Stack(self.ctx, 'depends_test_stack', + template.Template(tmpl)) + self.stack.store() + self.stack.create() + self.assertEqual(self.stack.state, + (parser.Stack.CREATE, parser.Stack.COMPLETE)) + + 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)