]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Expose resource dependency required_by to REST API.
authorSteve Baker <sbaker@redhat.com>
Thu, 20 Jun 2013 02:17:20 +0000 (14:17 +1200)
committerSteve Baker <sbaker@redhat.com>
Sun, 30 Jun 2013 23:14:45 +0000 (11:14 +1200)
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

heat/engine/api.py
heat/engine/dependencies.py
heat/engine/resource.py
heat/rpc/api.py
heat/tests/test_dependencies.py
heat/tests/test_engine_api_utils.py
heat/tests/test_parser.py

index f0578a09c191dd688b3c20910cf6d08767ea0b93..33aeeef7c6d9e09f42077cc2bd956c711544eda2 100644 (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:
index f8f6e6272007b7e6deefbd1501c72672e57a7d63..dc5d7d23318615d979369eedac7079287c1f8849 100644 (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
index cf1f5a095cf1c4b8d0b49fe441db561cbc8c2706..6dc8febee8ce652e27fc9e2ec2f3b2e874498e37 100644 (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()
 
index f9dbacecc882f614c8bb3c6568b23037b44ce97c..23adca6b3815d0b73eb880e4f3f52a293acaf4cb 100644 (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 = (
index eba83d093be72d73911c2cae84216a9df94f21e0..9196eae3e1a4f4f6e9c0c70c31f721d3ecb4cc02 100644 (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')
index 9dbb8968b9d138e94b26509e74b23da913f20b1d..f176be2388c4f5ff9a03269081e47bba1796b873 100644 (file)
 #    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'], [])
index 51f8789d7ba3ad422aec54b78e202536e35ceae9..62993033f7b01e0f959f783a8a62031c53e97c5f 100644 (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)