summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--heat/api/openstack/v1/resources.py15
-rw-r--r--heat/db/api.py4
-rw-r--r--heat/db/sqlalchemy/api.py9
-rw-r--r--heat/engine/service.py8
-rw-r--r--heat/engine/stack.py29
-rw-r--r--heat/objects/resource.py5
-rw-r--r--heat/rpc/client.py10
-rw-r--r--heat/tests/api/cfn/test_api_cfn_v1.py5
-rw-r--r--heat/tests/api/openstack_v1/test_resources.py18
-rw-r--r--heat/tests/db/test_sqlalchemy_api.py23
-rw-r--r--heat/tests/engine/service/test_service_engine.py2
-rw-r--r--heat/tests/engine/service/test_stack_resources.py6
-rw-r--r--heat/tests/test_rpc_client.py4
-rw-r--r--heat/tests/test_stack.py33
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',