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_STACK_ID: dict(resource.stack.identifier()),
|
||||
api.RES_STACK_NAME: resource.stack.name,
|
||||
api.RES_REQUIRED_BY: resource.required_by(),
|
||||
}
|
||||
|
||||
if detail:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'], [])
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue