From e27bfdf89d4dd441ed4e66ca7b9b6409c47057a0 Mon Sep 17 00:00:00 2001 From: liusheng Date: Tue, 8 Mar 2016 14:15:09 +0800 Subject: [PATCH] Fix the -q/--query in threshold alarm creation Currently, when creating an threshold with aodhclient, we can use -q/--query parameter to specify query conditions for alarm rule. but the parameter is unavailable now. * The -q parameter is unsupported by cliff, because -q/--quiet is internally used by cliff. so we drop it. * The --query cannot be used according to the help string because wrong implementation. * The --query usages of threshold/event type alarms and gnocchi_aggregation_by_resources_threshold type alarms are different. Closes-Bug: #1550094 Change-Id: I0a1235d12cd6aac80e81aa1c583bc79e2e50667f --- aodhclient/tests/unit/test_alarm_cli.py | 27 ++++++++----- aodhclient/tests/unit/test_utils.py | 10 +++++ aodhclient/utils.py | 53 ++++++++++++++++++++++++- aodhclient/v2/alarm_cli.py | 14 +++++-- 4 files changed, 90 insertions(+), 14 deletions(-) diff --git a/aodhclient/tests/unit/test_alarm_cli.py b/aodhclient/tests/unit/test_alarm_cli.py index 370aa1a..c3d2936 100644 --- a/aodhclient/tests/unit/test_alarm_cli.py +++ b/aodhclient/tests/unit/test_alarm_cli.py @@ -124,7 +124,7 @@ class CliAlarmCreateTest(testtools.TestCase): '--comparison-operator', 'le', '--threshold', '80', '--event-type', 'event', - '--query', '{}', + '--query', 'resource=fake-resource-id', '--granularity', '60', '--aggregation-method', 'last', '--metric', 'cpu', @@ -154,12 +154,18 @@ class CliAlarmCreateTest(testtools.TestCase): 'statistic': 'max', 'comparison_operator': 'le', 'threshold': 80.0, - 'query': '{}' - }, + 'query': [{'field': 'resource', + 'op': 'eq', + 'type': '', + 'value': 'fake-resource-id'}] + }, 'event_rule': { 'event_type': 'event', - 'query': '{}' - }, + 'query': [{'field': 'resource', + 'op': 'eq', + 'type': '', + 'value': 'fake-resource-id'}] + }, 'gnocchi_resources_threshold_rule': { 'granularity': '60', 'metric': 'cpu', @@ -169,14 +175,14 @@ class CliAlarmCreateTest(testtools.TestCase): 'comparison_operator': 'le', 'threshold': 80.0, 'resource_type': 'generic' - }, + }, 'gnocchi_aggregation_by_metrics_threshold_rule': { 'granularity': '60', 'aggregation_method': 'last', 'evaluation_periods': 60, 'comparison_operator': 'le', 'threshold': 80.0 - }, + }, 'gnocchi_aggregation_by_resources_threshold_rule': { 'granularity': '60', 'metric': 'cpu', @@ -184,9 +190,12 @@ class CliAlarmCreateTest(testtools.TestCase): 'evaluation_periods': 60, 'comparison_operator': 'le', 'threshold': 80.0, - 'query': '{}', + 'query': [{'field': 'resource', + 'op': 'eq', + 'type': '', + 'value': 'fake-resource-id'}], 'resource_type': 'generic' - }, + }, 'composite_rule': None, 'type': 'threshold' } diff --git a/aodhclient/tests/unit/test_utils.py b/aodhclient/tests/unit/test_utils.py index 56a060c..5037eb4 100644 --- a/aodhclient/tests/unit/test_utils.py +++ b/aodhclient/tests/unit/test_utils.py @@ -81,3 +81,13 @@ class SearchQueryBuilderTest(base.BaseTestCase): ]}, {"=": {"foo": "quote"}}, ]}) + + +class CliQueryToArray(base.BaseTestCase): + def test_cli_query_to_arrary(self): + cli_query = "this<=34;that=string::foo" + ret_array = utils.cli_to_array(cli_query) + expected_query = [ + {"field": "this", "type": "", "value": "34", "op": "le"}, + {"field": "that", "type": "string", "value": "foo", "op": "eq"}] + self.assertEqual(expected_query, ret_array) diff --git a/aodhclient/utils.py b/aodhclient/utils.py index 84e22f3..f15aa1e 100644 --- a/aodhclient/utils.py +++ b/aodhclient/utils.py @@ -11,7 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - +import re import pyparsing as pp @@ -48,6 +48,16 @@ expr = pp.operatorPrecedence(condition, [ ("∨", 2, pp.opAssoc.LEFT, ), ]) +OP_LOOKUP = {'!=': 'ne', + '>=': 'ge', + '<=': 'le', + '>': 'gt', + '<': 'lt', + '=': 'eq'} + +OP_LOOKUP_KEYS = '|'.join(sorted(OP_LOOKUP.keys(), key=len, reverse=True)) +OP_SPLIT_RE = re.compile(r'(%s)' % OP_LOOKUP_KEYS) + def _parsed_query2dict(parsed_query): result = None @@ -125,3 +135,44 @@ def dict_to_querystring(objs): return "&".join(["%s=%s" % (k, v) for k, v in objs.items() if v is not None]) + + +def cli_to_array(cli_query): + """Convert CLI list of queries to the Python API format. + + This will convert the following: + "this<=34;that=string::foo" + to + "[{field=this,op=le,value=34,type=''}, + {field=that,op=eq,value=foo,type=string}]" + + """ + + opts = [] + queries = cli_query.split(';') + for q in queries: + try: + field, q_operator, type_value = OP_SPLIT_RE.split(q, maxsplit=1) + except ValueError: + raise ValueError('Invalid or missing operator in query %(q)s,' + 'the supported operators are: %(k)s' % + {'q': q, 'k': OP_LOOKUP.keys()}) + if not field: + raise ValueError('Missing field in query %s' % q) + if not type_value: + raise ValueError('Missing value in query %s' % q) + opt = dict(field=field, op=OP_LOOKUP[q_operator]) + + if '::' not in type_value: + opt['type'], opt['value'] = '', type_value + else: + opt['type'], _, opt['value'] = type_value.partition('::') + + if opt['type'] and opt['type'] not in ( + 'string', 'integer', 'float', 'datetime', 'boolean'): + err = ('Invalid value type %(type)s, the type of value' + 'should be one of: integer, string, float, datetime,' + ' boolean.' % opt['type']) + raise ValueError(err) + opts.append(opt) + return opts diff --git a/aodhclient/v2/alarm_cli.py b/aodhclient/v2/alarm_cli.py index 538e3a1..2d8d598 100644 --- a/aodhclient/v2/alarm_cli.py +++ b/aodhclient/v2/alarm_cli.py @@ -152,10 +152,14 @@ class CliAlarmCreate(show.ShowOne): common_group = parser.add_argument_group('common alarm rules') common_group.add_argument( - '-q', '--query', metavar='', dest='query', - help='key[op]data_type::value; list. data_type is optional, ' - 'but if supplied must be string, integer, float, or boolean. ' - 'Used by threshold and event alarms') + '--query', metavar='', dest='query', + help="For alarms of type threshold or event: " + "key[op]data_type::value; list. data_type is optional, " + "but if supplied must be string, integer, float, or boolean. " + 'For alarms of ' + 'type gnocchi_aggregation_by_resources_threshold: ' + 'need to specify a complex query json string, like:' + ' {"and": [{"=": {"ended_at": null}}, ...]}.') common_group.add_argument( '--comparison-operator', metavar='', dest='comparison_operator', choices=ALARM_OPERATORS, @@ -268,6 +272,8 @@ class CliAlarmCreate(show.ShowOne): 'state', 'severity', 'enabled', 'alarm_actions', 'ok_actions', 'insufficient_data_actions', 'time_constraints', 'repeat_actions']) + if parsed_args.type in ('threshold', 'event') and parsed_args.query: + parsed_args.query = utils.cli_to_array(parsed_args.query) alarm['threshold_rule'] = utils.dict_from_parsed_args( parsed_args, ['meter_name', 'period', 'evaluation_periods', 'statistic', 'comparison_operator', 'threshold',