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:
Steve Baker 2013-06-20 14:17:20 +12:00
parent 84e0ed42a5
commit 5c5519af26
7 changed files with 135 additions and 4 deletions

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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 = (

View File

@ -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')

View File

@ -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'], [])

View File

@ -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)