Expose resource dependency required_by to REST API.
Each resource can generate a list of names of resources which require this resource as a direct (non transitive) dependency. This information is returned in the list as well as the show REST calls so that a diagram of the running stack can be built with a single request. Other uses of exposing this information is: - template authors debugging their own template dependency issues - integration tests validating template dependencies Change-Id: Ibe62345afa87e49c4e2152a5fcb74e5ee003124e
This commit is contained in:
parent
84e0ed42a5
commit
5c5519af26
|
@ -109,6 +109,7 @@ def format_stack_resource(resource, detail=True):
|
||||||
api.RES_ID: dict(resource.identifier()),
|
api.RES_ID: dict(resource.identifier()),
|
||||||
api.RES_STACK_ID: dict(resource.stack.identifier()),
|
api.RES_STACK_ID: dict(resource.stack.identifier()),
|
||||||
api.RES_STACK_NAME: resource.stack.name,
|
api.RES_STACK_NAME: resource.stack.name,
|
||||||
|
api.RES_REQUIRED_BY: resource.required_by(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if detail:
|
if detail:
|
||||||
|
|
|
@ -168,6 +168,15 @@ class Dependencies(object):
|
||||||
|
|
||||||
return self
|
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):
|
def __getitem__(self, last):
|
||||||
'''
|
'''
|
||||||
Return a partial dependency graph consisting of the specified node and
|
Return a partial dependency graph consisting of the specified node and
|
||||||
|
|
|
@ -301,6 +301,14 @@ class Resource(object):
|
||||||
self._add_dependencies(deps, None, self.t)
|
self._add_dependencies(deps, None, self.t)
|
||||||
deps += (self, None)
|
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):
|
def keystone(self):
|
||||||
return self.stack.clients.keystone()
|
return self.stack.clients.keystone()
|
||||||
|
|
||||||
|
|
|
@ -50,12 +50,12 @@ RES_KEYS = (
|
||||||
RES_DESCRIPTION, RES_UPDATED_TIME,
|
RES_DESCRIPTION, RES_UPDATED_TIME,
|
||||||
RES_NAME, RES_PHYSICAL_ID, RES_METADATA, RES_ACTION,
|
RES_NAME, RES_PHYSICAL_ID, RES_METADATA, RES_ACTION,
|
||||||
RES_STATUS, RES_STATUS_DATA, RES_TYPE,
|
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',
|
'description', 'updated_time',
|
||||||
'logical_resource_id', 'physical_resource_id', 'metadata',
|
'logical_resource_id', 'physical_resource_id', 'metadata',
|
||||||
'resource_action', 'resource_status', 'resource_status_reason',
|
'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 = (
|
EVENT_KEYS = (
|
||||||
|
|
|
@ -194,3 +194,24 @@ class dependenciesTest(testtools.TestCase):
|
||||||
for n in ('last', 'mid1', 'mid2', 'mid3'):
|
for n in ('last', 'mid1', 'mid2', 'mid3'):
|
||||||
self.assertTrue(n in order,
|
self.assertTrue(n in order,
|
||||||
"'%s' not found in dependency order" % n)
|
"'%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')
|
||||||
|
|
|
@ -12,9 +12,15 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from heat.common import context
|
||||||
from heat.tests.common import HeatTestCase
|
|
||||||
import heat.engine.api as api
|
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):
|
class EngineApiTest(HeatTestCase):
|
||||||
|
@ -71,3 +77,59 @@ class EngineApiTest(HeatTestCase):
|
||||||
def test_disable_rollback_extract_bad(self):
|
def test_disable_rollback_extract_bad(self):
|
||||||
self.assertRaises(ValueError, api.extract_args,
|
self.assertRaises(ValueError, api.extract_args,
|
||||||
{'disable_rollback': 'bad'})
|
{'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'], [])
|
||||||
|
|
|
@ -1479,3 +1479,33 @@ class StackTest(HeatTestCase):
|
||||||
(rsrc.UPDATE, rsrc.FAILED)):
|
(rsrc.UPDATE, rsrc.FAILED)):
|
||||||
rsrc.state_set(action, status)
|
rsrc.state_set(action, status)
|
||||||
self.assertEqual(None, self.stack.output('TestOutput'))
|
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)
|
||||||
|
|
Loading…
Reference in New Issue