diff --git a/heat/api/openstack/v1/resources.py b/heat/api/openstack/v1/resources.py index 7847942e3..241bfdbae 100644 --- a/heat/api/openstack/v1/resources.py +++ b/heat/api/openstack/v1/resources.py @@ -30,11 +30,19 @@ def format_resource(req, res, keys=[]): if key == engine_api.RES_ID: identity = identifier.ResourceIdentifier(**value) - yield ('links', [util.make_link(req, identity), - util.make_link(req, identity.stack(), 'stack')]) + links = [util.make_link(req, identity), + util.make_link(req, identity.stack(), 'stack')] + + nested_id = res.get(engine_api.RES_NESTED_STACK_ID) + if nested_id: + nested_identity = identifier.HeatIdentifier(**nested_id) + links.append(util.make_link(req, nested_identity, 'nested')) + + yield ('links', links) elif (key == engine_api.RES_STACK_NAME or key == engine_api.RES_STACK_ID or - key == engine_api.RES_ACTION): + key == engine_api.RES_ACTION or + key == engine_api.RES_NESTED_STACK_ID): return elif (key == engine_api.RES_METADATA): return diff --git a/heat/engine/api.py b/heat/engine/api.py index 4fb980c6a..ae0cc718b 100644 --- a/heat/engine/api.py +++ b/heat/engine/api.py @@ -130,6 +130,9 @@ def format_stack_resource(resource, detail=True): api.RES_REQUIRED_BY: resource.required_by(), } + if hasattr(resource, 'nested') and callable(resource.nested): + res[api.RES_NESTED_STACK_ID] = dict(resource.nested().identifier()) + if detail: res[api.RES_DESCRIPTION] = resource.parsed_template('Description', '') res[api.RES_METADATA] = resource.metadata diff --git a/heat/rpc/api.py b/heat/rpc/api.py index 3361b6ac4..a7e19b760 100644 --- a/heat/rpc/api.py +++ b/heat/rpc/api.py @@ -50,13 +50,13 @@ RES_KEYS = ( 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_REQUIRED_BY, + RES_REQUIRED_BY, RES_NESTED_STACK_ID, ) = ( 'description', 'updated_time', 'resource_name', 'physical_resource_id', 'metadata', 'resource_action', 'resource_status', 'resource_status_reason', 'resource_type', 'resource_identity', STACK_ID, STACK_NAME, - 'required_by', + 'required_by', 'nested_stack_id', ) RES_SCHEMA_KEYS = ( diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py index c57deaf5c..742bce4fa 100644 --- a/heat/tests/test_api_openstack_v1.py +++ b/heat/tests/test_api_openstack_v1.py @@ -1851,6 +1851,58 @@ class ResourceControllerTest(ControllerTest, HeatTestCase): self.assertEqual(expected, result) self.m.VerifyAll() + def test_show_with_nested_stack(self, mock_enforce): + self._mock_enforce_setup(mock_enforce, 'show', True) + res_name = 'WikiDatabase' + stack_identity = identifier.HeatIdentifier(self.tenant, + 'wordpress', '6') + res_identity = identifier.ResourceIdentifier(resource_name=res_name, + **stack_identity) + nested_stack_identity = identifier.HeatIdentifier(self.tenant, + 'nested', 'some_id') + + req = self._get(stack_identity._tenant_path()) + + engine_resp = { + u'description': u'', + u'resource_identity': dict(res_identity), + u'stack_name': stack_identity.stack_name, + u'resource_name': res_name, + u'resource_status_reason': None, + u'updated_time': u'2012-07-23T13:06:00Z', + u'stack_identity': dict(stack_identity), + u'resource_action': u'CREATE', + u'resource_status': u'COMPLETE', + u'physical_resource_id': + u'a3455d8c-9f88-404d-a85b-5315293e67de', + u'resource_type': u'AWS::EC2::Instance', + u'metadata': {u'ensureRunning': u'true'}, + u'nested_stack_id': dict(nested_stack_identity) + } + self.m.StubOutWithMock(rpc, 'call') + rpc.call(req.context, self.topic, + {'namespace': None, + 'method': 'describe_stack_resource', + 'args': {'stack_identity': stack_identity, + 'resource_name': res_name}, + 'version': self.api_version}, + None).AndReturn(engine_resp) + self.m.ReplayAll() + + result = self.controller.show(req, tenant_id=self.tenant, + stack_name=stack_identity.stack_name, + stack_id=stack_identity.stack_id, + resource_name=res_name) + + expected = [{'href': self._url(res_identity), 'rel': 'self'}, + {'href': self._url(stack_identity), 'rel': 'stack'}, + {'href': self._url(nested_stack_identity), 'rel': 'nested'} + ] + + self.assertEqual(expected, result['resource']['links']) + self.assertIsNone(result.get(rpc_api.RES_NESTED_STACK_ID)) + self.m.VerifyAll() + def test_show_nonexist(self, mock_enforce): self._mock_enforce_setup(mock_enforce, 'show', True) res_name = 'WikiDatabase' diff --git a/heat/tests/test_engine_api_utils.py b/heat/tests/test_engine_api_utils.py index caabcc8ac..a900cd8a3 100644 --- a/heat/tests/test_engine_api_utils.py +++ b/heat/tests/test_engine_api_utils.py @@ -159,6 +159,15 @@ class FormatTest(HeatTestCase): formatted = api.format_stack_resource(res, False) self.assertEqual(resource_keys, set(formatted.keys())) + def test_format_stack_resource_with_nested_stack(self): + res = self.stack['generic1'] + nested_id = {'foo': 'bar'} + res.nested = mock.Mock() + res.nested.return_value.identifier.return_value = nested_id + + formatted = api.format_stack_resource(res, False) + self.assertEqual(nested_id, formatted[rpc_api.RES_NESTED_STACK_ID]) + def test_format_stack_resource_required_by(self): res1 = api.format_stack_resource(self.stack['generic1']) res2 = api.format_stack_resource(self.stack['generic2'])