Merge "Parser for expressions for config (python part)"
This commit is contained in:
commit
bc614a0122
|
@ -69,6 +69,11 @@ default_messages = {
|
|||
# RPC errors
|
||||
"CannotFindTask": "Cannot find task",
|
||||
|
||||
# expression parser errors
|
||||
"LexError": "Illegal character",
|
||||
"ParseError": "Synxtax error",
|
||||
"UnknownModel": "Unknown model",
|
||||
|
||||
# unknown
|
||||
"UnknownError": "Unknown error"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# 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 inspect
|
||||
|
||||
from nailgun.errors import errors
|
||||
from nailgun.test.base import BaseTestCase
|
||||
from nailgun.utils import evaluate_expression
|
||||
|
||||
|
||||
class TestExpressionParser(BaseTestCase):
|
||||
|
||||
def test_expression_parser(self):
|
||||
cluster = self.env.create_cluster(api=False, mode='ha_compact')
|
||||
models = {
|
||||
'cluster': cluster,
|
||||
'settings': cluster.attributes.editable,
|
||||
'release': cluster.release
|
||||
}
|
||||
hypervisor = models['settings']['common']['libvirt_type']['value']
|
||||
|
||||
test_cases = (
|
||||
# test scalars
|
||||
('true', True),
|
||||
('false', False),
|
||||
('123', 123),
|
||||
('"123"', '123'),
|
||||
("'123'", '123'),
|
||||
# test boolean operators
|
||||
('true or false', True),
|
||||
('true and false', False),
|
||||
('not true', False),
|
||||
# test precedence
|
||||
('true or true and false or false', True),
|
||||
('true == true and false == false', True),
|
||||
# test comparison
|
||||
('123 == 123', True),
|
||||
('123 == 321', False),
|
||||
('123 != 321', True),
|
||||
('123 != "123"', True),
|
||||
# test grouping
|
||||
('(true or true) and not (false or false)', True),
|
||||
# test errors
|
||||
('(true', errors.ParseError),
|
||||
('false and', errors.ParseError),
|
||||
('== 123', errors.ParseError),
|
||||
('#^@$*()#@!', errors.ParseError),
|
||||
# test modelpaths
|
||||
('cluster:mode', 'ha_compact'),
|
||||
('cluster:mode == "ha_compact"', True),
|
||||
('cluster:mode != "multinode"', True),
|
||||
('"controller" in release:roles', True),
|
||||
('"unknown-role" in release:roles', False),
|
||||
('settings:common.libvirt_type.value', hypervisor),
|
||||
('settings:common.libvirt_type.value == "{0}"'.format(hypervisor),
|
||||
True),
|
||||
('cluster:mode == "ha_compact" and not ('
|
||||
'settings:common.libvirt_type.value '
|
||||
'!= "{0}")'.format(hypervisor), True),
|
||||
)
|
||||
|
||||
for test_case in test_cases:
|
||||
expression, result = test_case
|
||||
if inspect.isclass(result) and issubclass(result, Exception):
|
||||
self.assertRaises(result, evaluate_expression,
|
||||
expression, models)
|
||||
else:
|
||||
self.assertEqual(evaluate_expression(expression, models),
|
||||
result)
|
|
@ -19,6 +19,7 @@ from random import choice
|
|||
|
||||
from nailgun.logger import logger
|
||||
from nailgun.settings import settings
|
||||
from nailgun.utils import expression_parser
|
||||
|
||||
|
||||
def dict_merge(a, b):
|
||||
|
@ -59,6 +60,10 @@ def traverse(cdict, generator_class):
|
|||
return new_dict
|
||||
|
||||
|
||||
def evaluate_expression(expression, models=None):
|
||||
return expression_parser.evaluate(expression, models)
|
||||
|
||||
|
||||
class AttributesGenerator(object):
|
||||
@classmethod
|
||||
def password(cls, arg=None):
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# 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 ply.lex
|
||||
import ply.yacc
|
||||
|
||||
from nailgun.errors import errors
|
||||
|
||||
tokens = (
|
||||
'NUMBER', 'STRING', 'TRUE', 'FALSE', 'AND', 'OR', 'NOT', 'IN',
|
||||
'EQUALS', 'NOT_EQUALS', 'LPAREN', 'RPAREN',
|
||||
'MODELPATH',
|
||||
)
|
||||
|
||||
|
||||
def t_NUMBER(t):
|
||||
r'-?\d+'
|
||||
t.value = int(t.value)
|
||||
return t
|
||||
|
||||
|
||||
def t_STRING(t):
|
||||
r'(?P<openingquote>["\']).*?(?P=openingquote)'
|
||||
t.value = t.value[1:-1]
|
||||
return t
|
||||
|
||||
|
||||
def t_TRUE(t):
|
||||
r'true'
|
||||
t.value = True
|
||||
return t
|
||||
|
||||
|
||||
def t_FALSE(t):
|
||||
r'false'
|
||||
t.value = False
|
||||
return t
|
||||
|
||||
|
||||
t_AND = r'and'
|
||||
t_OR = r'or'
|
||||
t_NOT = r'not'
|
||||
t_IN = r'in'
|
||||
t_MODELPATH = r'\w*?\:[\w\.\-]+'
|
||||
t_EQUALS = r'=='
|
||||
t_NOT_EQUALS = r'!='
|
||||
t_LPAREN = r'\('
|
||||
t_RPAREN = r'\)'
|
||||
|
||||
t_ignore = ' \t\r\n'
|
||||
|
||||
|
||||
def t_error(t):
|
||||
errors.LexError("Illegal character '%s'" % t.value[0])
|
||||
t.lexer.skip(1)
|
||||
|
||||
|
||||
ply.lex.lex()
|
||||
|
||||
context = {
|
||||
'models': {}
|
||||
}
|
||||
|
||||
precedence = (
|
||||
('left', 'OR'),
|
||||
('left', 'AND'),
|
||||
('left', 'EQUALS', 'NOT_EQUALS'),
|
||||
('left', 'IN', 'NOT'),
|
||||
)
|
||||
|
||||
|
||||
def p_expression_binop(p):
|
||||
"""expression : expression EQUALS expression
|
||||
| expression NOT_EQUALS expression
|
||||
| expression OR expression
|
||||
| expression AND expression
|
||||
| expression IN expression
|
||||
"""
|
||||
if p[2] == '==':
|
||||
p[0] = p[1] == p[3]
|
||||
elif p[2] == '!=':
|
||||
p[0] = p[1] != p[3]
|
||||
elif p[2] == 'or':
|
||||
p[0] = p[1] or p[3]
|
||||
elif p[2] == 'and':
|
||||
p[0] = p[1] and p[3]
|
||||
elif p[2] == 'in':
|
||||
p[0] = p[1] in p[3]
|
||||
|
||||
|
||||
def p_not_expression(p):
|
||||
"""expression : NOT expression
|
||||
"""
|
||||
p[0] = not p[2]
|
||||
|
||||
|
||||
def p_expression_group(p):
|
||||
"""expression : LPAREN expression RPAREN
|
||||
"""
|
||||
p[0] = p[2]
|
||||
|
||||
|
||||
def p_expression_scalar(p):
|
||||
"""expression : NUMBER
|
||||
| STRING
|
||||
| TRUE
|
||||
| FALSE
|
||||
"""
|
||||
p[0] = p[1]
|
||||
|
||||
|
||||
def p_expression_modelpath(p):
|
||||
"""expression : MODELPATH
|
||||
"""
|
||||
model_name, attribute = p[1].split(':', 1)
|
||||
try:
|
||||
model = context['models'][model_name]
|
||||
except KeyError:
|
||||
raise errors.UnknownModel("Unknown model '%s'" % model_name)
|
||||
|
||||
def get_attribute_value(model, path):
|
||||
value = model[path.pop(0)]
|
||||
return get_attribute_value(value, path) if len(path) else value
|
||||
p[0] = get_attribute_value(model, attribute.split('.'))
|
||||
|
||||
|
||||
def p_error(p):
|
||||
raise errors.ParseError("Syntax error at '%s'" % getattr(p, 'value', ''))
|
||||
|
||||
|
||||
parser = ply.yacc.yacc(debug=False, write_tables=False)
|
||||
|
||||
|
||||
def evaluate(expression, models=None):
|
||||
context['models'] = models if models is not None else {}
|
||||
return parser.parse(expression)
|
|
@ -17,6 +17,7 @@ jsonschema==2.3.0
|
|||
kombu==3.0.16
|
||||
netaddr==0.7.10
|
||||
oslo.config==1.2.1
|
||||
ply==3.4
|
||||
psycopg2==2.5.1
|
||||
pycrypto==2.6.1
|
||||
simplejson==3.3.0
|
||||
|
|
Loading…
Reference in New Issue