Merge "Parser for expressions for config (python part)"

This commit is contained in:
Jenkins 2014-06-17 15:12:55 +00:00 committed by Gerrit Code Review
commit bc614a0122
5 changed files with 240 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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