Stack resource search

heat resource-list -f <filters>

imlements blueprint heat-stack-resource-search

APIImpact

Change-Id: Iaae88f3b32bc2ba7b41a4078ef3aa8ffc07079b7
This commit is contained in:
Kanagaraj Manickam 2015-12-15 08:26:24 +05:30
parent f37991c079
commit 6c4709696b
14 changed files with 137 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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