diff options
-rw-r--r-- | heat/api/openstack/v1/resources.py | 15 | ||||
-rw-r--r-- | heat/db/api.py | 4 | ||||
-rw-r--r-- | heat/db/sqlalchemy/api.py | 9 | ||||
-rw-r--r-- | heat/engine/service.py | 8 | ||||
-rw-r--r-- | heat/engine/stack.py | 29 | ||||
-rw-r--r-- | heat/objects/resource.py | 5 | ||||
-rw-r--r-- | heat/rpc/client.py | 10 | ||||
-rw-r--r-- | heat/tests/api/cfn/test_api_cfn_v1.py | 5 | ||||
-rw-r--r-- | heat/tests/api/openstack_v1/test_resources.py | 18 | ||||
-rw-r--r-- | heat/tests/db/test_sqlalchemy_api.py | 23 | ||||
-rw-r--r-- | heat/tests/engine/service/test_service_engine.py | 2 | ||||
-rw-r--r-- | heat/tests/engine/service/test_stack_resources.py | 6 | ||||
-rw-r--r-- | heat/tests/test_rpc_client.py | 4 | ||||
-rw-r--r-- | heat/tests/test_stack.py | 33 |
14 files changed, 137 insertions, 34 deletions
diff --git a/heat/api/openstack/v1/resources.py b/heat/api/openstack/v1/resources.py index db2230a53..87ffee39b 100644 --- a/heat/api/openstack/v1/resources.py +++ b/heat/api/openstack/v1/resources.py @@ -94,6 +94,16 @@ class ResourceController(object): @util.identified_stack def index(self, req, identity): """Lists information for all resources.""" + + whitelist = { + 'type': 'mixed', + 'status': 'mixed', + 'name': 'mixed', + 'action': 'mixed', + 'id': 'mixed', + 'physical_resource_id': 'mixed' + } + nested_depth = self._extract_to_param(req, rpc_api.PARAM_NESTED_DEPTH, param_utils.extract_int, @@ -103,10 +113,13 @@ class ResourceController(object): param_utils.extract_bool, default=False) + params = util.get_allowed_params(req.params, whitelist) + res_list = self.rpc_client.list_stack_resources(req.context, identity, nested_depth, - with_detail) + with_detail, + filters=params) return {'resources': [format_resource(req, res) for res in res_list]} diff --git a/heat/db/api.py b/heat/db/api.py index 11c84c1c5..f5240511a 100644 --- a/heat/db/api.py +++ b/heat/db/api.py @@ -113,8 +113,8 @@ def resource_exchange_stacks(context, resource_id1, resource_id2): return IMPL.resource_exchange_stacks(context, resource_id1, resource_id2) -def resource_get_all_by_stack(context, stack_id, key_id=False): - return IMPL.resource_get_all_by_stack(context, stack_id, key_id) +def resource_get_all_by_stack(context, stack_id, key_id=False, filters=None): + return IMPL.resource_get_all_by_stack(context, stack_id, key_id, filters) def resource_get_by_name_and_stack(context, resource_name, stack_id): diff --git a/heat/db/sqlalchemy/api.py b/heat/db/sqlalchemy/api.py index 28cc223fb..275dd3268 100644 --- a/heat/db/sqlalchemy/api.py +++ b/heat/db/sqlalchemy/api.py @@ -305,12 +305,15 @@ def resource_create(context, values): return resource_ref -def resource_get_all_by_stack(context, stack_id, key_id=False): - results = model_query( +def resource_get_all_by_stack(context, stack_id, key_id=False, filters=None): + query = model_query( context, models.Resource ).filter_by( stack_id=stack_id - ).options(orm.joinedload("data")).all() + ).options(orm.joinedload("data")) + + query = db_filters.exact_filter(query, models.Resource, filters) + results = query.all() if not results: raise exception.NotFound(_("no resources for stack_id %s were found") diff --git a/heat/engine/service.py b/heat/engine/service.py index 1dd3085dc..15d20c1ac 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -292,7 +292,7 @@ class EngineService(service.Service): by the RPC caller. """ - RPC_API_VERSION = '1.24' + RPC_API_VERSION = '1.25' def __init__(self, host, topic): super(EngineService, self).__init__() @@ -1546,13 +1546,15 @@ class EngineService(service.Service): @context.request_context def list_stack_resources(self, cnxt, stack_identity, - nested_depth=0, with_detail=False): + nested_depth=0, with_detail=False, + filters=None): s = self._get_stack(cnxt, stack_identity, show_deleted=True) stack = parser.Stack.load(cnxt, stack=s) depth = min(nested_depth, cfg.CONF.max_nested_stack_depth) return [api.format_stack_resource(resource, detail=with_detail) - for resource in stack.iter_resources(depth)] + for resource in stack.iter_resources(depth, + filters=filters)] @context.request_context def stack_suspend(self, cnxt, stack_identity): diff --git a/heat/engine/stack.py b/heat/engine/stack.py index ac78008f9..f2b2507c2 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -274,21 +274,40 @@ class Stack(collections.Mapping): @property def resources(self): + return self._find_resources() + + def _find_resources(self, filters=None): if self._resources is None: - self._resources = dict((name, resource.Resource(name, data, self)) - for (name, data) in - self.t.resource_definitions(self).items()) + res_defns = self.t.resource_definitions(self) + + if not filters: + self._resources = dict((name, + resource.Resource(name, data, self)) + for (name, data) in res_defns.items()) + else: + self._resources = dict() + self._db_resources = dict() + for rsc in six.itervalues( + resource_objects.Resource.get_all_by_stack( + self.context, self.id, True, filters)): + self._db_resources[rsc.name] = rsc + res = resource.Resource(rsc.name, + res_defns[rsc.name], + self) + self._resources[rsc.name] = res + # There is no need to continue storing the db resources # after resource creation self._db_resources = None + return self._resources - def iter_resources(self, nested_depth=0): + def iter_resources(self, nested_depth=0, filters=None): """Iterates over all the resources in a stack. Iterating includes nested stacks up to `nested_depth` levels below. """ - for res in six.itervalues(self): + for res in six.itervalues(self._find_resources(filters)): yield res if not res.has_nested() or nested_depth == 0: diff --git a/heat/objects/resource.py b/heat/objects/resource.py index 718c85e00..5565dbcd7 100644 --- a/heat/objects/resource.py +++ b/heat/objects/resource.py @@ -124,9 +124,10 @@ class Resource( resource_id2) @classmethod - def get_all_by_stack(cls, context, stack_id, key_id=False): + def get_all_by_stack(cls, context, stack_id, key_id=False, filters=None): resources_db = db_api.resource_get_all_by_stack(context, - stack_id, key_id) + stack_id, key_id, + filters) resources = [ ( resource_key, diff --git a/heat/rpc/client.py b/heat/rpc/client.py index 3b37f9c88..397f975f8 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -43,6 +43,7 @@ class EngineClient(object): 1.22 - Add support for stack export 1.23 - Add environment_files to create/update/preview/validate 1.24 - Adds ignorable_errors to validate_template + 1.25 - list_stack_resoure filter update """ BASE_RPC_API_VERSION = '1.0' @@ -503,20 +504,23 @@ class EngineClient(object): resource_name=resource_name)) def list_stack_resources(self, ctxt, stack_identity, - nested_depth=0, with_detail=False): + nested_depth=0, with_detail=False, + filters=None): """List the resources belonging to a stack. :param ctxt: RPC context. :param stack_identity: Name of the stack. :param nested_depth: Levels of nested stacks of which list resources. :param with_detail: show detail for resoruces in list. + :param filters: a dict with attribute:value to search the resources """ return self.call(ctxt, self.make_msg('list_stack_resources', stack_identity=stack_identity, nested_depth=nested_depth, - with_detail=with_detail), - version='1.12') + with_detail=with_detail, + filters=filters), + version='1.25') def stack_suspend(self, ctxt, stack_identity): return self.call(ctxt, self.make_msg('stack_suspend', diff --git a/heat/tests/api/cfn/test_api_cfn_v1.py b/heat/tests/api/cfn/test_api_cfn_v1.py index d341c00c6..492339e45 100644 --- a/heat/tests/api/cfn/test_api_cfn_v1.py +++ b/heat/tests/api/cfn/test_api_cfn_v1.py @@ -1635,8 +1635,9 @@ class CfnStackControllerTest(common.HeatTestCase): dummy_req.context, ('list_stack_resources', {'stack_identity': identity, 'nested_depth': 0, - 'with_detail': False}), - version='1.12' + 'with_detail': False, + 'filters': None}), + version='1.25' ).AndReturn(engine_resp) self.m.ReplayAll() diff --git a/heat/tests/api/openstack_v1/test_resources.py b/heat/tests/api/openstack_v1/test_resources.py index 97e741b33..015acd734 100644 --- a/heat/tests/api/openstack_v1/test_resources.py +++ b/heat/tests/api/openstack_v1/test_resources.py @@ -75,8 +75,9 @@ class ResourceControllerTest(tools.ControllerTest, common.HeatTestCase): ('list_stack_resources', {'stack_identity': stack_identity, 'nested_depth': 0, 'with_detail': False, + 'filters': {} }), - version='1.12' + version='1.25' ).AndReturn(engine_resp) self.m.ReplayAll() @@ -114,8 +115,9 @@ class ResourceControllerTest(tools.ControllerTest, common.HeatTestCase): req.context, ('list_stack_resources', {'stack_identity': stack_identity, 'nested_depth': 0, - 'with_detail': False}), - version='1.12' + 'with_detail': False, + 'filters': {}}), + version='1.25' ).AndRaise(tools.to_remote_error(error)) self.m.ReplayAll() @@ -143,8 +145,9 @@ class ResourceControllerTest(tools.ControllerTest, common.HeatTestCase): req.context, ('list_stack_resources', {'stack_identity': stack_identity, 'nested_depth': 99, - 'with_detail': False}), - version='1.12' + 'with_detail': False, + 'filters': {}}), + version='1.25' ).AndReturn([]) self.m.ReplayAll() @@ -238,8 +241,9 @@ class ResourceControllerTest(tools.ControllerTest, common.HeatTestCase): req.context, ('list_stack_resources', {'stack_identity': stack_identity, 'nested_depth': 0, - 'with_detail': True}), - version='1.12' + 'with_detail': True, + 'filters': {}}), + version='1.25' ).AndReturn(engine_resp) self.m.ReplayAll() diff --git a/heat/tests/db/test_sqlalchemy_api.py b/heat/tests/db/test_sqlalchemy_api.py index ea85497f2..ac54b2ba0 100644 --- a/heat/tests/db/test_sqlalchemy_api.py +++ b/heat/tests/db/test_sqlalchemy_api.py @@ -2076,11 +2076,32 @@ class DBAPIResourceTest(common.HeatTestCase): values = [ {'name': 'res1', 'stack_id': self.stack.id}, {'name': 'res2', 'stack_id': self.stack.id}, - {'name': 'res3', 'stack_id': self.stack1.id}, + {'name': 'res3', 'stack_id': self.stack.id}, + {'name': 'res4', 'stack_id': self.stack1.id}, ] [create_resource(self.ctx, self.stack, **val) for val in values] + # Test for all resources in a stack resources = db_api.resource_get_all_by_stack(self.ctx, self.stack.id) + self.assertEqual(3, len(resources)) + self.assertEqual('res1', resources.get('res1').name) + self.assertEqual('res2', resources.get('res2').name) + self.assertEqual('res3', resources.get('res3').name) + + # Test for resources matching single entry + resources = db_api.resource_get_all_by_stack(self.ctx, + self.stack.id, + filters=dict(name='res1')) + self.assertEqual(1, len(resources)) + self.assertEqual('res1', resources.get('res1').name) + + # Test for resources matching multi entry + resources = db_api.resource_get_all_by_stack(self.ctx, + self.stack.id, + filters=dict(name=[ + 'res1', + 'res2' + ])) self.assertEqual(2, len(resources)) self.assertEqual('res1', resources.get('res1').name) self.assertEqual('res2', resources.get('res2').name) diff --git a/heat/tests/engine/service/test_service_engine.py b/heat/tests/engine/service/test_service_engine.py index 908a1cb92..2bbecf305 100644 --- a/heat/tests/engine/service/test_service_engine.py +++ b/heat/tests/engine/service/test_service_engine.py @@ -40,7 +40,7 @@ class ServiceEngineTest(common.HeatTestCase): def test_make_sure_rpc_version(self): self.assertEqual( - '1.24', + '1.25', service.EngineService.RPC_API_VERSION, ('RPC version is changed, please update this test to new version ' 'and make sure additional test cases are added for RPC APIs ' diff --git a/heat/tests/engine/service/test_stack_resources.py b/heat/tests/engine/service/test_stack_resources.py index 2be3f6b3f..4b1a843d1 100644 --- a/heat/tests/engine/service/test_stack_resources.py +++ b/heat/tests/engine/service/test_stack_resources.py @@ -246,7 +246,8 @@ class StackResourcesServiceTest(common.HeatTestCase): resources = self.eng.list_stack_resources(self.ctx, self.stack.identifier(), 2) - self.stack.iter_resources.assert_called_once_with(2) + self.stack.iter_resources.assert_called_once_with(2, + filters=None) @mock.patch.object(stack.Stack, 'load') @tools.stack_context('service_resources_list_test_stack_with_max_depth') @@ -258,7 +259,8 @@ class StackResourcesServiceTest(common.HeatTestCase): self.stack.identifier(), 99) max_depth = cfg.CONF.max_nested_stack_depth - self.stack.iter_resources.assert_called_once_with(max_depth) + self.stack.iter_resources.assert_called_once_with(max_depth, + filters=None) @mock.patch.object(stack.Stack, 'load') def test_stack_resources_list_deleted_stack(self, mock_load): diff --git a/heat/tests/test_rpc_client.py b/heat/tests/test_rpc_client.py index f3bd7da60..78d603890 100644 --- a/heat/tests/test_rpc_client.py +++ b/heat/tests/test_rpc_client.py @@ -251,7 +251,9 @@ class EngineRpcAPITestCase(common.HeatTestCase): self._test_engine_api('list_stack_resources', 'call', stack_identity=self.identity, nested_depth=0, - with_detail=False) + with_detail=False, + filters=None, + version=1.25) def test_stack_suspend(self): self._test_engine_api('stack_suspend', 'call', diff --git a/heat/tests/test_stack.py b/heat/tests/test_stack.py index efb64fe9b..d06402c21 100644 --- a/heat/tests/test_stack.py +++ b/heat/tests/test_stack.py @@ -38,6 +38,7 @@ from heat.engine import service from heat.engine import stack from heat.engine import template from heat.objects import raw_template as raw_template_object +from heat.objects import resource as resource_objects from heat.objects import stack as stack_object from heat.objects import stack_tag as stack_tag_object from heat.objects import user_creds as ucreds_object @@ -219,7 +220,7 @@ class StackTest(common.HeatTestCase): self.assertEqual(1, self.stack.total_resources(self.stack.id)) self.assertEqual(1, self.stack.total_resources()) - def test_iter_resources(self): + def test_iter_resources_with_nested(self): tpl = {'HeatTemplateFormatVersion': '2012-12-12', 'Resources': {'A': {'Type': 'StackResourceType'}, @@ -245,6 +246,36 @@ class StackTest(common.HeatTestCase): all_resources = list(self.stack.iter_resources(1)) self.assertEqual(5, len(all_resources)) + @mock.patch.object(resource_objects.Resource, 'get_all_by_stack') + @mock.patch('heat.engine.resource.Resource') + def test_iter_resources_with_filters(self, mock_resource, mock_db_call): + mock_rsc = mock.MagicMock() + mock_rsc.name = 'A' + mock_db_call.return_value = {'A': mock_rsc} + mock_resource.return_value = mock_rsc + tpl = {'HeatTemplateFormatVersion': '2012-12-12', + 'Resources': + {'A': {'Type': 'StackResourceType'}, + 'B': {'Type': 'GenericResourceType'}}} + self.stack = stack.Stack(self.ctx, 'test_stack', + template.Template(tpl), + status_reason='blarg') + + all_resources = list(self.stack.iter_resources( + filters=dict(name=['A']) + )) + + # Verify, the db query is called with expected filter + mock_db_call.assert_called_once_with(self.ctx, + self.stack.id, + True, + dict(name=['A'])) + # Make sure it returns only one resource. + self.assertEqual(1, len(all_resources)) + + # And returns the resource A + self.assertEqual('A', all_resources[0].name) + @mock.patch.object(stack.Stack, 'db_resource_get') def test_iter_resources_cached(self, mock_drg): tpl = {'HeatTemplateFormatVersion': '2012-12-12', |