Add filter support to stack API
Accepts multiple request params ATTR=VALUE to filter the listed stacks. Stacks will be matched against all whitelisted filters passed in the request. Whitelisted filters for stacks are NAME and STATUS. If the same ATTR is passed multiple times, stacks matching ANY of the values will be returned. blueprint: filter-stacks Change-Id: Ic9421a827e99793448c23a841006fb8c03819030
This commit is contained in:
parent
36c4a30df7
commit
ab8ab995fa
@ -155,6 +155,10 @@ class StackController(object):
|
||||
"""
|
||||
Lists summary information for all stacks
|
||||
"""
|
||||
filter_whitelist = {
|
||||
'status': 'mixed',
|
||||
'name': 'mixed',
|
||||
}
|
||||
whitelist = {
|
||||
'limit': 'single',
|
||||
'marker': 'single',
|
||||
@ -162,7 +166,9 @@ class StackController(object):
|
||||
'sort_keys': 'multi',
|
||||
}
|
||||
params = util.get_allowed_params(req.params, whitelist)
|
||||
stacks = self.engine.list_stacks(req.context, **params)
|
||||
filter_params = util.get_allowed_params(req.params, filter_whitelist)
|
||||
stacks = self.engine.list_stacks(req.context, filters=filter_params,
|
||||
**params)
|
||||
|
||||
return stacks_view.collection(req, stacks)
|
||||
|
||||
|
@ -85,6 +85,10 @@ def get_allowed_params(params, whitelist):
|
||||
value = params.get(key)
|
||||
elif get_type == 'multi':
|
||||
value = params.getall(key)
|
||||
elif get_type == 'mixed':
|
||||
value = params.getall(key)
|
||||
if isinstance(value, list) and len(value) == 1:
|
||||
value = value.pop()
|
||||
|
||||
if value:
|
||||
allowed_params[key] = value
|
||||
|
@ -197,11 +197,20 @@ class EngineService(service.Service):
|
||||
return [format_stack_detail(s) for s in stacks]
|
||||
|
||||
@request_context
|
||||
def list_stacks(self, cnxt, limit=None, sort_keys=None, marker=None,
|
||||
sort_dir=None):
|
||||
def list_stacks(self, cnxt, limit=None, marker=None, sort_keys=None,
|
||||
sort_dir=None, filters=None):
|
||||
"""
|
||||
The list_stacks method returns attributes of all stacks.
|
||||
arg1 -> RPC cnxt.
|
||||
The list_stacks method returns attributes of all stacks. It supports
|
||||
pagination (``limit`` and ``marker``), sorting (``sort_keys`` and
|
||||
``sort_dir``) and filtering (``filters``) of the results.
|
||||
|
||||
:param cnxt: RPC context
|
||||
:param limit: the number of stacks to list (integer or string)
|
||||
:param marker: the ID of the last item in the previous page
|
||||
:param sort_keys: an array of fields used to sort the list
|
||||
:param sort_dir: the direction of the sort ('asc' or 'desc')
|
||||
:param filters: a dict with attribute:value to filter the list
|
||||
:returns: a list of formatted stacks
|
||||
"""
|
||||
|
||||
def format_stack_details(stacks):
|
||||
@ -217,7 +226,7 @@ class EngineService(service.Service):
|
||||
yield api.format_stack(stack)
|
||||
|
||||
stacks = db_api.stack_get_all_by_tenant(cnxt, limit, sort_keys, marker,
|
||||
sort_dir) or []
|
||||
sort_dir, filters) or []
|
||||
return list(format_stack_details(stacks))
|
||||
|
||||
def _validate_deferred_auth_context(self, cnxt, stack):
|
||||
|
@ -50,16 +50,24 @@ class EngineClient(heat.openstack.common.rpc.proxy.RpcProxy):
|
||||
return self.call(ctxt, self.make_msg('identify_stack',
|
||||
stack_name=stack_name))
|
||||
|
||||
def list_stacks(self, ctxt, limit=None, sort_keys=None, marker=None,
|
||||
sort_dir=None):
|
||||
def list_stacks(self, ctxt, limit=None, marker=None, sort_keys=None,
|
||||
sort_dir=None, filters=None):
|
||||
"""
|
||||
The list_stacks method returns the attributes of all stacks.
|
||||
The list_stacks method returns attributes of all stacks. It supports
|
||||
pagination (``limit`` and ``marker``), sorting (``sort_keys`` and
|
||||
``sort_dir``) and filtering (``filters``) of the results.
|
||||
|
||||
:param ctxt: RPC context.
|
||||
:param limit: the number of stacks to list (integer or string)
|
||||
:param marker: the ID of the last item in the previous page
|
||||
:param sort_keys: an array of fields used to sort the list
|
||||
:param sort_dir: the direction of the sort ('asc' or 'desc')
|
||||
:param filters: a dict with attribute:value to filter the list
|
||||
:returns: a list of stacks
|
||||
"""
|
||||
return self.call(ctxt, self.make_msg('list_stacks', limit=limit,
|
||||
sort_keys=sort_keys, marker=marker,
|
||||
sort_dir=sort_dir))
|
||||
sort_dir=sort_dir, filters=filters))
|
||||
|
||||
def show_stack(self, ctxt, stack_identity):
|
||||
"""
|
||||
|
@ -133,7 +133,7 @@ class CfnStackControllerTest(HeatTestCase):
|
||||
u'StackStatus': u'CREATE_COMPLETE'}]}}}
|
||||
self.assertEqual(result, expected)
|
||||
default_args = {'limit': None, 'sort_keys': None, 'marker': None,
|
||||
'sort_dir': None}
|
||||
'sort_dir': None, 'filters': None}
|
||||
mock_call.assert_called_once_with(dummy_req.context, self.topic,
|
||||
{'namespace': None,
|
||||
'method': 'list_stacks',
|
||||
|
@ -341,7 +341,7 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
||||
}
|
||||
self.assertEqual(result, expected)
|
||||
default_args = {'limit': None, 'sort_keys': None, 'marker': None,
|
||||
'sort_dir': None}
|
||||
'sort_dir': None, 'filters': {}}
|
||||
mock_call.assert_called_once_with(req.context, self.topic,
|
||||
{'namespace': None,
|
||||
'method': 'list_stacks',
|
||||
@ -350,7 +350,7 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
||||
None)
|
||||
|
||||
@mock.patch.object(rpc, 'call')
|
||||
def test_index_whitelists_request_params(self, mock_call):
|
||||
def test_index_whitelists_pagination_params(self, mock_call):
|
||||
params = {
|
||||
'limit': 'fake limit',
|
||||
'sort_keys': 'fake sort keys',
|
||||
@ -365,13 +365,35 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
||||
|
||||
rpc_call_args, _ = mock_call.call_args
|
||||
engine_args = rpc_call_args[2]['args']
|
||||
self.assertEqual(4, len(engine_args))
|
||||
self.assertEqual(5, len(engine_args))
|
||||
self.assertIn('limit', engine_args)
|
||||
self.assertIn('sort_keys', engine_args)
|
||||
self.assertIn('marker', engine_args)
|
||||
self.assertIn('sort_dir', engine_args)
|
||||
self.assertNotIn('balrog', engine_args)
|
||||
|
||||
@mock.patch.object(rpc, 'call')
|
||||
def test_index_whitelist_filter_params(self, mock_call):
|
||||
params = {
|
||||
'status': 'fake status',
|
||||
'name': 'fake name',
|
||||
'balrog': 'you shall not pass!'
|
||||
}
|
||||
req = self._get('/stacks', params=params)
|
||||
mock_call.return_value = []
|
||||
|
||||
result = self.controller.index(req, tenant_id='fake_tenant_id')
|
||||
|
||||
rpc_call_args, _ = mock_call.call_args
|
||||
engine_args = rpc_call_args[2]['args']
|
||||
self.assertIn('filters', engine_args)
|
||||
|
||||
filters = engine_args['filters']
|
||||
self.assertEqual(2, len(filters))
|
||||
self.assertIn('status', filters)
|
||||
self.assertIn('name', filters)
|
||||
self.assertNotIn('balrog', filters)
|
||||
|
||||
@mock.patch.object(rpc, 'call')
|
||||
def test_detail(self, mock_call):
|
||||
req = self._get('/stacks/detail')
|
||||
@ -426,7 +448,7 @@ class StackControllerTest(ControllerTest, HeatTestCase):
|
||||
|
||||
self.assertEqual(result, expected)
|
||||
default_args = {'limit': None, 'sort_keys': None, 'marker': None,
|
||||
'sort_dir': None}
|
||||
'sort_dir': None, 'filters': None}
|
||||
mock_call.assert_called_once_with(req.context, self.topic,
|
||||
{'namespace': None,
|
||||
'method': 'list_stacks',
|
||||
|
@ -58,6 +58,21 @@ class TestGetAllowedParams(HeatTestCase):
|
||||
self.assertIn('foo value', result['foo'])
|
||||
self.assertIn('foo value 2', result['foo'])
|
||||
|
||||
def test_handles_mixed_value_param_with_multiple_entries(self):
|
||||
self.whitelist = {'foo': 'mixed'}
|
||||
self.params.add('foo', 'foo value 2')
|
||||
|
||||
result = util.get_allowed_params(self.params, self.whitelist)
|
||||
self.assertEqual(2, len(result['foo']))
|
||||
self.assertIn('foo value', result['foo'])
|
||||
self.assertIn('foo value 2', result['foo'])
|
||||
|
||||
def test_handles_mixed_value_param_with_single_entry(self):
|
||||
self.whitelist = {'foo': 'mixed'}
|
||||
|
||||
result = util.get_allowed_params(self.params, self.whitelist)
|
||||
self.assertEqual('foo value', result['foo'])
|
||||
|
||||
def test_ignores_bogus_whitelist_items(self):
|
||||
self.whitelist = {'foo': 'blah'}
|
||||
result = util.get_allowed_params(self.params, self.whitelist)
|
||||
|
@ -17,6 +17,7 @@ import functools
|
||||
import json
|
||||
import sys
|
||||
|
||||
import mock
|
||||
import mox
|
||||
from testtools import matchers
|
||||
import testscenarios
|
||||
@ -1141,6 +1142,19 @@ class StackServiceTest(HeatTestCase):
|
||||
|
||||
self.m.VerifyAll()
|
||||
|
||||
@mock.patch.object(db_api, 'stack_get_all_by_tenant')
|
||||
def test_stack_list_passes_filtering_info(self, mock_stack_get_all_by_t):
|
||||
|
||||
filters = {'foo': 'bar'}
|
||||
self.eng.list_stacks(self.ctx, filters=filters)
|
||||
mock_stack_get_all_by_t.assert_called_once_with(mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
filters
|
||||
)
|
||||
|
||||
def test_stack_describe_nonexistent(self):
|
||||
non_exist_identifier = identifier.HeatIdentifier(
|
||||
self.ctx.tenant_id, 'wibble',
|
||||
|
@ -87,7 +87,8 @@ class EngineRpcAPITestCase(testtools.TestCase):
|
||||
'limit': mock.ANY,
|
||||
'sort_keys': mock.ANY,
|
||||
'marker': mock.ANY,
|
||||
'sort_dir': mock.ANY
|
||||
'sort_dir': mock.ANY,
|
||||
'filters': mock.ANY
|
||||
}
|
||||
self._test_engine_api('list_stacks', 'call', **default_args)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user