Stack resource search
heat resource-list -f <filters> imlements blueprint heat-stack-resource-search APIImpact Change-Id: Iaae88f3b32bc2ba7b41a4078ef3aa8ffc07079b7
This commit is contained in:
parent
f37991c079
commit
6c4709696b
@ -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]}
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 '
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user