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:
Anderson Mesquita 2013-11-19 13:13:01 -06:00
parent 36c4a30df7
commit ab8ab995fa
9 changed files with 95 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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