
We add it to all the top-level modules. We will tackle imageutils separately since it's quite large in and of itself. Change-Id: I26b16a471c19a59cf8b084be01ca7c57dbba530e Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
224 lines
7.9 KiB
Python
224 lines
7.9 KiB
Python
# Copyright (c) 2011 OpenStack Foundation
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 ast
|
|
from collections.abc import Callable
|
|
import operator
|
|
from typing import Any
|
|
|
|
import pyparsing
|
|
|
|
|
|
def _all_in(x: str, *y: str) -> bool:
|
|
x = ast.literal_eval(x)
|
|
if not isinstance(x, list):
|
|
raise TypeError(
|
|
"<all-in> must compare with a list literal"
|
|
" string, EG \"{}\"".format(['aes', 'mmx'])
|
|
)
|
|
return all(val in x for val in y)
|
|
|
|
|
|
def _range_in(x: str, *y: str) -> bool:
|
|
x = ast.literal_eval(x)
|
|
if len(y) != 4:
|
|
raise TypeError(
|
|
"<range-in> operator has to be followed by 2 "
|
|
"space separated numeric value surrounded by "
|
|
"brackets \"range_in [ 10 20 ] \""
|
|
)
|
|
num_x = float(x)
|
|
num_y = float(y[1])
|
|
num_z = float(y[2])
|
|
if num_y > num_z:
|
|
raise TypeError(
|
|
"<range-in> operator's first argument has to be "
|
|
"smaller or equal to the second argument EG"
|
|
"\"range_in ( 10 20 ] \""
|
|
)
|
|
|
|
if y[0] == '[':
|
|
lower = num_x >= num_y
|
|
elif y[0] == '(':
|
|
lower = num_x > num_y
|
|
else:
|
|
raise TypeError(
|
|
"The first element should be an opening bracket (\"(\" or \"[\")"
|
|
)
|
|
|
|
if y[3] == ']':
|
|
upper = num_x <= num_z
|
|
elif y[3] == ')':
|
|
upper = num_x < num_z
|
|
else:
|
|
raise TypeError(
|
|
"The last element should be a closing bracket (\")\" or \"]\")"
|
|
)
|
|
return lower and upper
|
|
|
|
|
|
op_methods: dict[str, Callable[..., bool]] = {
|
|
# This one is special/odd,
|
|
# TODO(harlowja): fix it so that it's not greater than or
|
|
# equal, see here for the original @ https://review.openstack.org/#/c/8089/
|
|
'=': lambda x, y: float(x) >= float(y),
|
|
# More sane ops/methods
|
|
# Numerical methods
|
|
'!=': lambda x, y: float(x) != float(y),
|
|
'<=': lambda x, y: float(x) <= float(y),
|
|
'<': lambda x, y: float(x) < float(y),
|
|
'==': lambda x, y: float(x) == float(y),
|
|
'>=': lambda x, y: float(x) >= float(y),
|
|
'>': lambda x, y: float(x) > float(y),
|
|
# String methods
|
|
's!=': operator.ne,
|
|
's<': operator.lt,
|
|
's<=': operator.le,
|
|
's==': operator.eq,
|
|
's>': operator.gt,
|
|
's>=': operator.ge,
|
|
# Other
|
|
'<all-in>': _all_in,
|
|
'<in>': lambda x, y: y in x,
|
|
'<or>': lambda x, *y: any(x == a for a in y),
|
|
'<range-in>': _range_in,
|
|
}
|
|
|
|
|
|
def make_grammar() -> pyparsing.ParserElement:
|
|
"""Creates the grammar to be used by a spec matcher.
|
|
|
|
The grammar created supports the following operations.
|
|
|
|
Numerical values:
|
|
* ``= :`` equal to or greater than. This is equivalent to ``>=`` and is
|
|
supported for `legacy reasons
|
|
<http://docs.openstack.org/developer/nova/filter_scheduler.html#ComputeCapabilitiesFilter>`_
|
|
* ``!= :`` Float/integer value not equal
|
|
* ``<= :`` Float/integer value less than or equal
|
|
* ``< :`` Float/integer value less than
|
|
* ``== :`` Float/integer value equal
|
|
* ``>= :`` Float/integer value greater than or equal
|
|
* ``> :`` Float/integer value greater
|
|
|
|
String operations:
|
|
* ``s!= :`` Not equal
|
|
* ``s< :`` Less than
|
|
* ``s<= :`` Less than or equal
|
|
* ``s== :`` Equal
|
|
* ``s> :`` Greater than
|
|
* ``s>= :`` Greater than or equal
|
|
|
|
Other operations:
|
|
* ``<all-in> :`` All items 'in' value
|
|
* ``<in> :`` Item 'in' value, like a substring in a string.
|
|
* ``<or> :`` Logical 'or'
|
|
* ``<range-in>:`` Range tester with customizable boundary conditions, tests
|
|
whether value is in the range, boundary condition could be
|
|
inclusve \'[\' or exclusive \'(\'.
|
|
|
|
If no operator is specified the default is ``s==`` (string equality comparison)
|
|
|
|
Example operations:
|
|
* ``">= 60"`` Is the numerical value greater than or equal to 60
|
|
* ``"<or> spam <or> eggs"`` Does the value contain ``spam`` or ``eggs``
|
|
* ``"s== 2.1.0"`` Is the string value equal to ``2.1.0``
|
|
* ``"<in> gcc"`` Is the string ``gcc`` contained in the value string
|
|
* ``"<all-in> aes mmx"`` Are both ``aes`` and ``mmx`` in the value
|
|
* ``"<range-in> [ 10 20 ]"`` float(value) >= 10 and float(value) <= 20
|
|
* ``"<range-in> ( 10 20 ]"`` float(value) > 10 and float(value) <= 20
|
|
* ``"<range-in> ( 10 20 )"`` float(value) > 10 and float(value) < 20
|
|
|
|
:returns: A pyparsing.MatchFirst object. See
|
|
https://pythonhosted.org/pyparsing/ for details on pyparsing.
|
|
"""
|
|
# This is apparently how pyparsing recommends to be used,
|
|
# as http://pyparsing.wikispaces.com/share/view/644825 states that
|
|
# it is not thread-safe to use a parser across threads.
|
|
|
|
unary_ops = (
|
|
# Order matters here (so that '=' doesn't match before '==')
|
|
pyparsing.Literal("==")
|
|
| pyparsing.Literal("=")
|
|
| pyparsing.Literal("!=")
|
|
| pyparsing.Literal("<in>")
|
|
| pyparsing.Literal(">=")
|
|
| pyparsing.Literal("<=")
|
|
| pyparsing.Literal(">")
|
|
| pyparsing.Literal("<")
|
|
| pyparsing.Literal("s==")
|
|
| pyparsing.Literal("s!=")
|
|
|
|
|
# Order matters here (so that '<' doesn't match before '<=')
|
|
pyparsing.Literal("s<=")
|
|
| pyparsing.Literal("s<")
|
|
|
|
|
# Order matters here (so that '>' doesn't match before '>=')
|
|
pyparsing.Literal("s>=")
|
|
| pyparsing.Literal("s>")
|
|
)
|
|
|
|
all_in_nary_op = pyparsing.Literal("<all-in>")
|
|
or_ = pyparsing.Literal("<or>")
|
|
range_in_binary_op = pyparsing.Literal("<range-in>")
|
|
|
|
# An atom is anything not an keyword followed by anything but whitespace
|
|
atom = ~(
|
|
unary_ops | all_in_nary_op | or_ | range_in_binary_op
|
|
) + pyparsing.Regex(r"\S+")
|
|
|
|
unary = unary_ops + atom
|
|
range_op = range_in_binary_op + atom + atom + atom + atom
|
|
nary = all_in_nary_op + pyparsing.OneOrMore(atom)
|
|
disjunction = pyparsing.OneOrMore(or_ + atom)
|
|
|
|
# Even-numbered tokens will be '<or>', so we drop them
|
|
disjunction.setParseAction(lambda _s, _l, t: ["<or>"] + t[1::2])
|
|
|
|
expr = disjunction | nary | range_op | unary | atom
|
|
return expr
|
|
|
|
|
|
def match(cmp_value: Any, spec: str) -> bool:
|
|
"""Match a given value to a given spec DSL.
|
|
|
|
This uses the grammar defined by make_grammar()
|
|
|
|
:param cmp_value: Value to be checked for match.
|
|
:param spec: The comparison specification string, for example ``">= 70"``
|
|
or ``"s== string_value"``. See ``make_grammar()`` for examples
|
|
of a specification string.
|
|
:returns: True if cmp_value is a match for spec. False otherwise.
|
|
"""
|
|
expr = make_grammar()
|
|
tree: pyparsing.ParseResults | list[str]
|
|
try:
|
|
# As of 2018-01-29 documentation on parseString()
|
|
# https://pythonhosted.org/pyparsing/pyparsing.ParserElement-class.html#parseString
|
|
#
|
|
# parseString() will take our specification string, for example "< 6"
|
|
# and convert it into a list of ['<', "6"]
|
|
tree = expr.parseString(spec)
|
|
except pyparsing.ParseException:
|
|
# If an exception then we will just check if the value matches the spec
|
|
tree = [spec]
|
|
if len(tree) == 1:
|
|
return bool(tree[0] == cmp_value)
|
|
|
|
# tree[0] will contain a string representation of a comparison operation
|
|
# such as '>=', we then convert that string to a comparison function
|
|
compare_func = op_methods[tree[0]]
|
|
return compare_func(cmp_value, *tree[1:])
|