Merge "Fix filter name inconsistency in stack_list"
This commit is contained in:
commit
24b1fbce28
@ -192,6 +192,8 @@ class StackController(object):
|
||||
|
||||
def _index(self, req, tenant_safe=True):
|
||||
filter_whitelist = {
|
||||
# usage of keys in this list are not encouraged, please use
|
||||
# rpc_api.STACK_KEYS instead
|
||||
'id': 'mixed',
|
||||
'status': 'mixed',
|
||||
'name': 'mixed',
|
||||
@ -214,7 +216,25 @@ class StackController(object):
|
||||
'not_tags_any': 'single',
|
||||
}
|
||||
params = util.get_allowed_params(req.params, whitelist)
|
||||
filter_params = util.get_allowed_params(req.params, filter_whitelist)
|
||||
stack_keys = dict.fromkeys(rpc_api.STACK_KEYS, 'mixed')
|
||||
unsupported = (
|
||||
rpc_api.STACK_ID, # not user visible
|
||||
rpc_api.STACK_CAPABILITIES, # not supported
|
||||
rpc_api.STACK_CREATION_TIME, # don't support timestamp
|
||||
rpc_api.STACK_DELETION_TIME, # don't support timestamp
|
||||
rpc_api.STACK_DESCRIPTION, # not supported
|
||||
rpc_api.STACK_NOTIFICATION_TOPICS, # not supported
|
||||
rpc_api.STACK_OUTPUTS, # not in database
|
||||
rpc_api.STACK_PARAMETERS, # not in this table
|
||||
rpc_api.STACK_TAGS, # tags query following a specific guideline
|
||||
rpc_api.STACK_TMPL_DESCRIPTION, # not supported
|
||||
rpc_api.STACK_UPDATED_TIME, # don't support timestamp
|
||||
)
|
||||
for key in unsupported:
|
||||
stack_keys.pop(key)
|
||||
# downward compatibility
|
||||
stack_keys.update(filter_whitelist)
|
||||
filter_params = util.get_allowed_params(req.params, stack_keys)
|
||||
|
||||
show_deleted = False
|
||||
p_name = rpc_api.PARAM_SHOW_DELETED
|
||||
|
@ -85,6 +85,90 @@ def extract_args(params):
|
||||
return kwargs
|
||||
|
||||
|
||||
def _parse_object_status(status):
|
||||
"""Parse input status into action and status if possible.
|
||||
|
||||
This function parses a given string (or list of strings) and see if it
|
||||
contains the action part. The action part is exacted if found.
|
||||
|
||||
:param status: A string or a list of strings where each string contains
|
||||
a status to be checked.
|
||||
:returns: (actions, statuses) tuple, where actions is a set of actions
|
||||
extracted from the input status and statuses is a set of pure
|
||||
object status.
|
||||
"""
|
||||
|
||||
if not isinstance(status, list):
|
||||
status = [status]
|
||||
|
||||
status_set = set()
|
||||
action_set = set()
|
||||
for val in status:
|
||||
# Note: cannot reference Stack.STATUSES due to circular reference issue
|
||||
for s in ('COMPLETE', 'FAILED', 'IN_PROGRESS'):
|
||||
index = val.rfind(s)
|
||||
if index != -1:
|
||||
status_set.add(val[index:])
|
||||
if index > 1:
|
||||
action_set.add(val[:index - 1])
|
||||
break
|
||||
|
||||
return action_set, status_set
|
||||
|
||||
|
||||
def translate_filters(params):
|
||||
"""Translate filter names to their corresponding DB field names.
|
||||
|
||||
:param params: A dictionary containing keys from engine.api.STACK_KEYS
|
||||
and other keys previously leaked to users.
|
||||
:returns: A dict containing only valid DB filed names.
|
||||
"""
|
||||
key_map = {
|
||||
rpc_api.STACK_NAME: 'name',
|
||||
rpc_api.STACK_ACTION: 'action',
|
||||
rpc_api.STACK_STATUS: 'status',
|
||||
rpc_api.STACK_STATUS_DATA: 'status_reason',
|
||||
rpc_api.STACK_DISABLE_ROLLBACK: 'disable_rollback',
|
||||
rpc_api.STACK_TIMEOUT: 'timeout',
|
||||
rpc_api.STACK_OWNER: 'username',
|
||||
rpc_api.STACK_PARENT: 'owner_id',
|
||||
rpc_api.STACK_USER_PROJECT_ID: 'stack_user_project_id',
|
||||
}
|
||||
|
||||
for key, field in key_map.items():
|
||||
value = params.pop(key, None)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
fld_value = params.get(field, None)
|
||||
if fld_value:
|
||||
if not isinstance(fld_value, list):
|
||||
fld_value = [fld_value]
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
value.extend(fld_value)
|
||||
|
||||
params[field] = value
|
||||
|
||||
# Deal with status which might be of form <ACTION>_<STATUS>, e.g.
|
||||
# "CREATE_FAILED". Note this logic is still not ideal due to the fact
|
||||
# that action and status are stored separately.
|
||||
if 'status' in params:
|
||||
a_set, s_set = _parse_object_status(params['status'])
|
||||
statuses = sorted(s_set)
|
||||
params['status'] = statuses[0] if len(statuses) == 1 else statuses
|
||||
|
||||
if a_set:
|
||||
a = params.get('action', [])
|
||||
action_set = set(a) if isinstance(a, list) else set([a])
|
||||
actions = sorted(action_set.union(a_set))
|
||||
|
||||
params['action'] = actions[0] if len(actions) == 1 else actions
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def format_stack_outputs(stack, outputs):
|
||||
'''
|
||||
Return a representation of the given output template for the given stack
|
||||
|
@ -498,6 +498,9 @@ class EngineService(service.Service):
|
||||
multiple tags using the boolean OR expression
|
||||
:returns: a list of formatted stacks
|
||||
"""
|
||||
if filters is not None:
|
||||
filters = api.translate_filters(filters)
|
||||
|
||||
stacks = parser.Stack.load_all(cnxt, limit, marker, sort_keys,
|
||||
sort_dir, filters, tenant_safe,
|
||||
show_deleted, resolve_data=False,
|
||||
|
@ -321,7 +321,27 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase):
|
||||
'username': 'fake username',
|
||||
'tenant': 'fake tenant',
|
||||
'owner_id': 'fake owner-id',
|
||||
'balrog': 'you shall not pass!'
|
||||
'stack_name': 'fake stack name',
|
||||
'stack_identity': 'fake identity',
|
||||
'creation_time': 'create timestamp',
|
||||
'updated_time': 'update timestamp',
|
||||
'deletion_time': 'deletion timestamp',
|
||||
'notification_topics': 'fake topic',
|
||||
'description': 'fake description',
|
||||
'template_description': 'fake description',
|
||||
'parameters': 'fake params',
|
||||
'outputs': 'fake outputs',
|
||||
'stack_action': 'fake action',
|
||||
'stack_status': 'fake status',
|
||||
'stack_status_reason': 'fake status reason',
|
||||
'capabilities': 'fake capabilities',
|
||||
'disable_rollback': 'fake value',
|
||||
'timeout_mins': 'fake timeout',
|
||||
'stack_owner': 'fake owner',
|
||||
'parent': 'fake parent',
|
||||
'stack_user_project_id': 'fake project id',
|
||||
'tags': 'fake tags',
|
||||
'barlog': 'you shall not pass!'
|
||||
}
|
||||
req = self._get('/stacks', params=params)
|
||||
mock_call.return_value = []
|
||||
@ -333,15 +353,18 @@ class StackControllerTest(tools.ControllerTest, common.HeatTestCase):
|
||||
self.assertIn('filters', engine_args)
|
||||
|
||||
filters = engine_args['filters']
|
||||
self.assertEqual(7, len(filters))
|
||||
self.assertIn('id', filters)
|
||||
self.assertIn('status', filters)
|
||||
self.assertIn('name', filters)
|
||||
self.assertIn('action', filters)
|
||||
self.assertIn('username', filters)
|
||||
self.assertIn('tenant', filters)
|
||||
self.assertIn('owner_id', filters)
|
||||
self.assertNotIn('balrog', filters)
|
||||
self.assertEqual(16, len(filters))
|
||||
for key in ('id', 'status', 'name', 'action', 'username', 'tenant',
|
||||
'owner_id', 'stack_name', 'stack_action', 'stack_status',
|
||||
'stack_status_reason', 'disable_rollback', 'timeout_mins',
|
||||
'stack_owner', 'parent', 'stack_user_project_id'):
|
||||
self.assertIn(key, filters)
|
||||
|
||||
for key in ('stack_identity', 'creation_time', 'updated_time',
|
||||
'deletion_time', 'notification_topics', 'description',
|
||||
'template_description', 'parameters', 'outputs',
|
||||
'capabilities', 'tags', 'barlog'):
|
||||
self.assertNotIn(key, filters)
|
||||
|
||||
def test_index_returns_stack_count_if_with_count_is_true(
|
||||
self, mock_enforce):
|
||||
|
@ -1138,3 +1138,126 @@ class TestExtractArgs(common.HeatTestCase):
|
||||
exc = self.assertRaises(ValueError, api.extract_args, p)
|
||||
self.assertIn('Invalid tag, "tag2," contains a comma',
|
||||
six.text_type(exc))
|
||||
|
||||
|
||||
class TranslateFilterTest(common.HeatTestCase):
|
||||
scenarios = [
|
||||
(
|
||||
'single+single',
|
||||
dict(inputs={'stack_status': 'COMPLETE', 'status': 'FAILED'},
|
||||
expected={'status': ['COMPLETE', 'FAILED']})
|
||||
), (
|
||||
'none+single',
|
||||
dict(inputs={'name': 'n1'},
|
||||
expected={'name': 'n1'})
|
||||
), (
|
||||
'single+none',
|
||||
dict(inputs={'stack_name': 'n1'},
|
||||
expected={'name': 'n1'})
|
||||
), (
|
||||
'none+list',
|
||||
dict(inputs={'action': ['a1', 'a2']},
|
||||
expected={'action': ['a1', 'a2']})
|
||||
), (
|
||||
'list+none',
|
||||
dict(inputs={'stack_action': ['a1', 'a2']},
|
||||
expected={'action': ['a1', 'a2']})
|
||||
), (
|
||||
'single+list',
|
||||
dict(inputs={'stack_owner': 'u1', 'username': ['u2', 'u3']},
|
||||
expected={'username': ['u1', 'u2', 'u3']})
|
||||
), (
|
||||
'list+single',
|
||||
dict(inputs={'parent': ['s1', 's2'], 'owner_id': 's3'},
|
||||
expected={'owner_id': ['s1', 's2', 's3']})
|
||||
), (
|
||||
'list+list',
|
||||
dict(inputs={'stack_name': ['n1', 'n2'], 'name': ['n3', 'n4']},
|
||||
expected={'name': ['n1', 'n2', 'n3', 'n4']})
|
||||
), (
|
||||
'full_status_split',
|
||||
dict(inputs={'stack_status': 'CREATE_COMPLETE'},
|
||||
expected={'action': 'CREATE', 'status': 'COMPLETE'})
|
||||
), (
|
||||
'full_status_split_merge',
|
||||
dict(inputs={'stack_status': 'CREATE_COMPLETE',
|
||||
'status': 'CREATE_FAILED'},
|
||||
expected={'action': 'CREATE',
|
||||
'status': ['COMPLETE', 'FAILED']})
|
||||
), (
|
||||
'action_status_merge',
|
||||
dict(inputs={'action': ['UPDATE', 'CREATE'],
|
||||
'status': 'CREATE_FAILED'},
|
||||
expected={'action': ['CREATE', 'UPDATE'],
|
||||
'status': 'FAILED'})
|
||||
)
|
||||
]
|
||||
|
||||
def test_stack_filter_translate(self):
|
||||
actual = api.translate_filters(self.inputs)
|
||||
self.assertEqual(self.expected, actual)
|
||||
|
||||
|
||||
class ParseStatusTest(common.HeatTestCase):
|
||||
scenarios = [
|
||||
(
|
||||
'single_bogus',
|
||||
dict(inputs='bogus status',
|
||||
expected=(set(), set()))
|
||||
), (
|
||||
'list_bogus',
|
||||
dict(inputs=['foo', 'bar'],
|
||||
expected=(set(), set()))
|
||||
), (
|
||||
'single_partial',
|
||||
dict(inputs='COMPLETE',
|
||||
expected=(set(), set(['COMPLETE'])))
|
||||
), (
|
||||
'multi_partial',
|
||||
dict(inputs=['FAILED', 'COMPLETE'],
|
||||
expected=(set(), set(['FAILED', 'COMPLETE'])))
|
||||
), (
|
||||
'multi_partial_dup',
|
||||
dict(inputs=['FAILED', 'FAILED'],
|
||||
expected=(set(), set(['FAILED'])))
|
||||
), (
|
||||
'single_full',
|
||||
dict(inputs=['DELETE_FAILED'],
|
||||
expected=(set(['DELETE']), set(['FAILED'])))
|
||||
), (
|
||||
'multi_full',
|
||||
dict(inputs=['DELETE_FAILED', 'CREATE_COMPLETE'],
|
||||
expected=(set(['CREATE', 'DELETE']),
|
||||
set(['COMPLETE', 'FAILED'])))
|
||||
), (
|
||||
'mix_bogus_partial',
|
||||
dict(inputs=['delete_failed', 'COMPLETE'],
|
||||
expected=(set(), set(['COMPLETE'])))
|
||||
), (
|
||||
'mix_bogus_full',
|
||||
dict(inputs=['delete_failed', 'action_COMPLETE'],
|
||||
expected=(set(['action']), set(['COMPLETE'])))
|
||||
), (
|
||||
'mix_bogus_full_incomplete',
|
||||
dict(inputs=['delete_failed', '_COMPLETE'],
|
||||
expected=(set(), set(['COMPLETE'])))
|
||||
), (
|
||||
'mix_partial_full',
|
||||
dict(inputs=['FAILED', 'b_COMPLETE'],
|
||||
expected=(set(['b']),
|
||||
set(['COMPLETE', 'FAILED'])))
|
||||
), (
|
||||
'mix_full_dup',
|
||||
dict(inputs=['a_FAILED', 'a_COMPLETE'],
|
||||
expected=(set(['a']),
|
||||
set(['COMPLETE', 'FAILED'])))
|
||||
), (
|
||||
'mix_full_dup_2',
|
||||
dict(inputs=['a_FAILED', 'b_FAILED'],
|
||||
expected=(set(['a', 'b']), set(['FAILED'])))
|
||||
)
|
||||
]
|
||||
|
||||
def test_stack_parse_status(self):
|
||||
actual = api._parse_object_status(self.inputs)
|
||||
self.assertEqual(self.expected, actual)
|
||||
|
@ -563,6 +563,27 @@ class StackServiceTest(common.HeatTestCase):
|
||||
mock.ANY,
|
||||
)
|
||||
|
||||
@mock.patch.object(stack_object.Stack, 'get_all')
|
||||
def test_stack_list_passes_filter_translated(self, mock_stack_get_all):
|
||||
filters = {'stack_name': 'bar'}
|
||||
self.eng.list_stacks(self.ctx, filters=filters)
|
||||
translated = {'name': 'bar'}
|
||||
mock_stack_get_all.assert_called_once_with(mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
translated,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
mock.ANY,
|
||||
)
|
||||
|
||||
@mock.patch.object(stack_object.Stack, 'get_all')
|
||||
def test_stack_list_tenant_safe_defaults_to_true(self, mock_stack_get_all):
|
||||
self.eng.list_stacks(self.ctx)
|
||||
|
Loading…
x
Reference in New Issue
Block a user