diff options
Diffstat (limited to 'lmi')
-rw-r--r-- | lmi/scripts/_metacommand/__init__.py | 2 | ||||
-rw-r--r-- | lmi/scripts/_metacommand/exit.py | 1 | ||||
-rw-r--r-- | lmi/scripts/_metacommand/help.py | 7 | ||||
-rw-r--r-- | lmi/scripts/_metacommand/interactive.py | 4 | ||||
-rw-r--r-- | lmi/scripts/common/command/__init__.py | 1 | ||||
-rw-r--r-- | lmi/scripts/common/command/base.py | 45 | ||||
-rw-r--r-- | lmi/scripts/common/command/meta.py | 113 | ||||
-rw-r--r-- | lmi/scripts/common/command/select.py | 195 | ||||
-rw-r--r-- | lmi/scripts/common/errors.py | 19 | ||||
-rw-r--r-- | lmi/scripts/common/util.py | 146 | ||||
-rw-r--r-- | lmi/scripts/common/versioncheck/__init__.py | 146 | ||||
-rw-r--r-- | lmi/scripts/common/versioncheck/parser.py | 521 |
12 files changed, 1192 insertions, 8 deletions
diff --git a/lmi/scripts/_metacommand/__init__.py b/lmi/scripts/_metacommand/__init__.py index 92bfab2..affea7b 100644 --- a/lmi/scripts/_metacommand/__init__.py +++ b/lmi/scripts/_metacommand/__init__.py @@ -221,6 +221,8 @@ class MetaCommand(object): try: retval = cmd.run(argv) except Exception as exc: + if isinstance(exc, errors.LmiUnsatisfiedDependencies): + retval = exit.EXIT_CODE_UNSATISFIED_DEPENDENCIES LOG().exception(str(exc)) if isinstance(retval, bool) or not isinstance(retval, (int, long)): return ( exit.EXIT_CODE_SUCCESS if bool(retval) or retval is None diff --git a/lmi/scripts/_metacommand/exit.py b/lmi/scripts/_metacommand/exit.py index 5b568a5..e876073 100644 --- a/lmi/scripts/_metacommand/exit.py +++ b/lmi/scripts/_metacommand/exit.py @@ -42,6 +42,7 @@ EXIT_CODE_FAILURE = 1 EXIT_CODE_KEYBOARD_INTERRUPT = 2 EXIT_CODE_COMMAND_NOT_FOUND = 3 EXIT_CODE_INVALID_SYNTAX = 4 +EXIT_CODE_UNSATISFIED_DEPENDENCIES = 5 def _execute_exit(exit_code): """ Associated function with ``Exit`` command. """ diff --git a/lmi/scripts/_metacommand/help.py b/lmi/scripts/_metacommand/help.py index 97a70bf..b88f6b5 100644 --- a/lmi/scripts/_metacommand/help.py +++ b/lmi/scripts/_metacommand/help.py @@ -64,6 +64,13 @@ class Help(LmiEndPointCommand): index = 0 try: while index < len(subcommand) and not node.is_end_point(): + while node.is_selector(): + cmd_factory, _ = node.select_cmds().next() + node = cmd_factory(self.app, node.cmd_name, + node.parent) + # selector may return either multiplexer or end-point + if node.is_end_point(): + break cmd_factory = cmdutil.get_subcommand_factory(node, subcommand[index]) node = cmd_factory(self.app, subcommand[index], node) diff --git a/lmi/scripts/_metacommand/interactive.py b/lmi/scripts/_metacommand/interactive.py index 17eb353..f2b327c 100644 --- a/lmi/scripts/_metacommand/interactive.py +++ b/lmi/scripts/_metacommand/interactive.py @@ -292,6 +292,10 @@ class Interactive(cmd.Cmd): LOG().error(str(err)) self._last_exit_code = exit.EXIT_CODE_COMMAND_NOT_FOUND + except errors.LmiUnsatisfiedDependencies as err: + LOG().error(str(err)) + self._last_exit_code = exit.EXIT_CODE_UNSATISFIED_DEPENDENCIES + except errors.LmiError as err: LOG().error(str(err)) self._last_exit_code = exit.EXIT_CODE_FAILURE diff --git a/lmi/scripts/common/command/__init__.py b/lmi/scripts/common/command/__init__.py index 045d952..2062577 100644 --- a/lmi/scripts/common/command/__init__.py +++ b/lmi/scripts/common/command/__init__.py @@ -42,6 +42,7 @@ from lmi.scripts.common.command.lister import LmiInstanceLister from lmi.scripts.common.command.lister import LmiLister from lmi.scripts.common.command.multiplexer import LmiCommandMultiplexer from lmi.scripts.common.command.session import LmiSessionCommand +from lmi.scripts.common.command.select import LmiSelectCommand from lmi.scripts.common.command.show import LmiShowInstance from lmi.scripts.common.command.helper import make_list_command diff --git a/lmi/scripts/common/command/base.py b/lmi/scripts/common/command/base.py index d1f6cc7..cebf422 100644 --- a/lmi/scripts/common/command/base.py +++ b/lmi/scripts/common/command/base.py @@ -49,18 +49,23 @@ DEFAULT_FORMATTER_OPTIONS = { class LmiBaseCommand(object): """ - Abstract base class for all commands handling command line arguemtns. + Abstract base class for all commands handling command line arguments. Instances of this class are organized in a tree with root element being the ``lmi`` meta-command (if not running in interactive mode). Each such instance can have more child commands if its - :py:meth:`LmiBaseCommand.is_end_point` method return ``False``. Each has + :py:meth:`LmiBaseCommand.is_multiplexer` method return ``True``. Each has one parent command except for the top level one, whose :py:attr:`parent` property returns ``None``. - Set of commands is organized in a tree, where each command - (except for the root) has its own parent. :py:meth:`is_end_point` method - distinguish leaves from nodes. The path from root command to the - leaf is a sequence of commands passed to command line. + Set of commands is organized in a tree, where each command (except for the + root) has its own parent. :py:meth:`is_end_point` method distinguishes + leaves from nodes. The path from root command to the leaf is a sequence of + commands passed to command line. + + There is also a special command called selector. Its :py:meth:`is_selector` + method returns ``True``. It selects proper command that shall be passed all + the arguments based on expression with profile requirements. It shares its + name and parent with selected child. If the :py:meth:`LmiBaseCommand.has_own_usage` returns ``True``, the parent command won't process the whole command line and the remainder will be @@ -96,6 +101,32 @@ class LmiBaseCommand(object): return True @classmethod + def is_multiplexer(cls): + """ + Is this command a multiplexer? Note that only one of + :py:meth:`is_end_point`, :py:meth:`is_selector` and this method can + evaluate to``True``. + + :returns: ``True`` if this command is not an end-point command and it's + a multiplexer. It contains one or more subcommands. It consumes the + first argument from command-line arguments and passes the rest to + one of its subcommands. + :rtype: boolean + """ + return not cls.is_end_point() + + @classmethod + def is_selector(cls): + """ + Is this command a selector? + + :returns: ``True`` if this command is a subclass of + :py:class:`lmi.scripts.common.command.select.LmiSelectCommand`. + :rtype: boolean + """ + return not cls.is_end_point() and not cls.is_multiplexer() + + @classmethod def has_own_usage(cls): """ :returns: ``True``, if this command has its own usage string, which is @@ -300,7 +331,7 @@ class LmiBaseCommand(object): """ Allows to override session object. This is useful for especially for conditional commands (subclasses of - :py:class:`~lmi.scripts.common.command.LmiSelectCommand`) that devide + :py:class:`~lmi.scripts.common.command.select.LmiSelectCommand`) that devide connections to groups satisfying particular expression. These groups are turned into session proxies containing just a subset of connections in global session object. diff --git a/lmi/scripts/common/command/meta.py b/lmi/scripts/common/command/meta.py index ecd4b49..7d8213e 100644 --- a/lmi/scripts/common/command/meta.py +++ b/lmi/scripts/common/command/meta.py @@ -54,6 +54,7 @@ RE_CALLABLE = re.compile( re.IGNORECASE) RE_ARRAY_SUFFIX = re.compile(r'^(?:[a-z_]+[a-z0-9_]*)?$', re.IGNORECASE) RE_OPTION = re.compile(r'^-+(?P<name>[^-+].*)$') +RE_MODULE_PATH = re.compile(r'([a-zA-z_]\w+\.)+[a-zA-z_]\w+') FORMAT_OPTIONS = ('no_headings', 'human_friendly') @@ -486,6 +487,85 @@ def _handle_format_options(name, bases, dcl): for key in format_options: dcl.pop('FMT_' + key.upper()) +def _handle_select(name, dcl): + """ + Process properties of :py:class:`.select.LmiSelectCommand`. + Currently handled properties are: + + ``SELECT`` : ``list`` + Is a list of pairs ``(condition, command)`` where ``condition`` is + an expression in *LMIReSpL* language. And ``command`` is either a + string with absolute path to command that shall be loaded or the + command class itself. + + Small example: :: + + SELECT = [ + ( 'OpenLMI-Hardware < 0.4.2' + , 'lmi.scripts.hardware.pre042.PreCmd' + ) + , ('OpenLMI-Hardware >= 0.4.2 & class LMI_Chassis == 0.3.0' + , HwCmd + ) + ] + + It says: Let the ``PreHwCmd`` command do the job on brokers having + ``openlmi-hardware`` package older than ``0.4.2``. Use the + ``HwCmd`` anywhere else where also the ``LMI_Chassis`` CIM class in + version ``0.3.0`` is available. + + First matching condition wins and assigned command will be passed + all the arguments. + + ``DEFAULT`` : ``str`` or :py:class:`~.base.LmiBaseCommand` + Defines fallback command used in case no condition can be + satisfied. + + They will be turned into ``get_conditionals()`` method. + """ + module_name = dcl.get('__module__', name) + if not 'SELECT' in dcl: + raise errors.LmiCommandError(module_name, name, + "Missing SELECT property.") + def inv_prop(msg, *args): + return errors.LmiCommandInvalidProperty(module_name, name, msg % args) + expressions = dcl.pop('SELECT') + if not isinstance(expressions, (list, tuple)): + raise inv_prop('SELECT must be list or tuple.') + if len(expressions) < 1: + raise inv_prop('SELECT must contain at least one condition!') + for index, item in enumerate(expressions): + if not isinstance(item, tuple): + raise inv_prop('Items of SELECT must be tuples, not %s!' % + getattr(type(item), '__name__', 'UNKNOWN')) + if len(item) != 2: + raise inv_prop('Expected pair in SELECT on index %d!' % index) + expr, cmd = item + if not isinstance(expr, basestring): + raise inv_prop('Expected expression string on index %d' + ' in SELECT!' % index) + if isinstance(cmd, basestring) and not RE_MODULE_PATH.match(cmd): + raise inv_prop('Second item of conditional pair on index %d' + ' in SELECT does not look as an importable path!' % cmd) + if ( not isinstance(cmd, basestring) + and not issubclass(cmd, (basestring, base.LmiBaseCommand))): + raise inv_prop('Expected subclass of LmiBaseCommand (or its import' + ' path) as a second item of a pair on index %d in SELECT!' + % index) + + default = dcl.pop('DEFAULT', None) + if isinstance(default, basestring) and not RE_MODULE_PATH.match(default): + raise inv_prop('DEFAULT "%s" does not look as an importable path!' + % default) + if ( default is not None and not isinstance(default, basestring) + and not issubclass(default, (basestring, base.LmiBaseCommand))): + raise inv_prop('Expected subclass of LmiBaseCommand' + ' (or its import path) as a value of DEFAULT!') + def _new_get_conditionals(self): + return expressions, default + + dcl['get_conditionals'] = _new_get_conditionals + class EndPointCommandMetaClass(abc.ABCMeta): """ End point command does not have any subcommands. It's a leaf of @@ -711,7 +791,7 @@ class MultiplexerMetaClass(abc.ABCMeta): 'COMMANDS dictionary must be composed of' ' LmiBaseCommand subclasses, failed class: "%s"' % cmd.__name__) - if not cmd.is_end_point() and not cmd.has_own_usage(): + if cmd.is_multiplexer() and not cmd.has_own_usage(): LOG().warn('Command "%s.%s" is missing usage string.' ' It will be inherited from parent command.', cmd.__module__, cmd.__name__) @@ -726,3 +806,34 @@ class MultiplexerMetaClass(abc.ABCMeta): _handle_format_options(name, bases, dcl) return super(MultiplexerMetaClass, mcs).__new__(mcs, name, bases, dcl) + +class SelectMetaClass(abc.ABCMeta): + """ + Meta class for select commands with guarded commands. Additional handled + properties: + + ``SELECT`` : ``list`` + List of commands guarded with expressions representing requirements + on server's side that need to be met. + ``DEFAULT`` : ``str`` or :py:class:`~.base.LmiBaseCommand` + Defines fallback command used in case no condition can is + satisfied. + """ + + def __new__(mcs, name, bases, dcl): + if dcl.get('__metaclass__', None) is not SelectMetaClass: + module_name = dcl.get('__module__', name) + if not '__doc__' in dcl: + LOG().warn('Command selector "%s.%s" is missing short' + ' description string (__doc__).', + module_name, name) + default = dcl.get('DEFAULT', None) + if ( default is not None + and issubclass(default, base.LmiBaseCommand) + and getattr(dcl['DEFAULT'], '__doc__', None)): + LOG().warn('Using __doc__ string from default command for' + ' selector "%s.%s".', module_name, name) + dcl['__doc__'] = dcl['DEFAULT'].__doc__ + _handle_select(name, dcl) + return super(SelectMetaClass, mcs).__new__(mcs, name, bases, dcl) + diff --git a/lmi/scripts/common/command/select.py b/lmi/scripts/common/command/select.py new file mode 100644 index 0000000..5f89994 --- /dev/null +++ b/lmi/scripts/common/command/select.py @@ -0,0 +1,195 @@ +# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are +# those of the authors and should not be interpreted as representing official +# policies, either expressed or implied, of the FreeBSD Project. +# +# Authors: Michal Minar <miminar@redhat.com> +# +""" +Defines command class used to choose other commands depending on +profile and class requirements. +""" + +from docopt import docopt +from pyparsing import ParseException + +from lmi.scripts.common import get_logger +from lmi.scripts.common import errors +from lmi.scripts.common.command import base +from lmi.scripts.common.command import meta +from lmi.scripts.common.session import SessionProxy +from lmi.scripts.common.versioncheck import eval_respl + +class LmiSelectCommand(base.LmiBaseCommand): + """ + Base class for command selectors. It does not process command line + arguments. Thery are passed unchanged to selected command whose + requirements are met. Its doc string is not interpreted in any way. + + If there are more hosts, conditions are evaluated per each. They are then + split into groups, each fulfilling particular condition. Associated + commands are then invoked on these groups separately. + + Example usage: :: + + class MySelect(LmiSelectCommand): + SELECT = [ + ( 'OpenLMI-Hardware >= 0.4.2' + , 'lmi.scripts.hardware.current.Cmd'), + ('OpenLMI-Hardware', 'lmi.scripts.hardware.pre042.Cmd') + ] + DEFAULT = MissingHwProviderCmd + + Using metaclass: :py:class:`.meta.SelectMetaClass`. + """ + __metaclass__ = meta.SelectMetaClass + + @classmethod + def is_end_point(cls): + return False + + @classmethod + def is_multiplexer(cls): + return False + + @classmethod + def get_conditionals(cls): + """ + Get the expressions with associated commands. This shall be overriden + by a subclass. + + :returns: Pair of ``(expressions, default)``. + Where ``expressions`` is a list of pairs ``(condition, command)``. + And ``default`` is the same as ``command`` used in case no + ``condition`` is satisfied. + :rtype: list + """ + raise NotImplementedError( + "get_conditionals needs to be defined in subclass") + + def eval_expr(self, expr, hosts, cache=None): + """ + Evaluate expression on group of hosts. + + :param string expr: Expression to evaluate. + :param list hosts: Group of hosts that shall be checked. + :param dictionary cache: Optional cache object speeding up evaluation + by reducing number of queries to broker. + :returns: Subset of hosts satisfying *expr*. + :rtype: list + """ + if cache is None: + cache = dict() + session = self.session + satisfied = [] + try: + for host in hosts: # TODO: could be done concurrently + conn = session[host] + if not conn: + continue + if eval_respl(expr, conn, cache=cache): + satisfied.append(host) + except ParseException: + raise errors.LmiBadSelectExpression(self.__class__.__module__, + self.__class__.__name__, "Bad select expression: %s" % expr) + return satisfied + + def select_cmds(self, cache=None): + """ + Generator of command factories with associated groups of hosts. It + evaluates given expressions on session. In this process all expressions + from :py:meth:`get_conditionals` are checked in a row. Host satisfying + some expression is added to group associated with it and is excluded + from processing following expressions. + + :param dictionary cache: Optional cache object speeding up the evaluation + by reducing number of queries to broker. + :returns: Pairs in form ``(command_factory, session_proxy)``. + :rtype: generator + :raises: + * :py:class:`~lmi.scripts.common.errors.LmiUnsatisfiedDependencies` + if no condition is satisfied for at least one host. Note that + this exception is raised at the end of evaluation. This lets + you choose whether you want to process satisfied hosts - by + processing the generator at once. Or whether you want to be + sure it is satisfied by all of them - you turn the generator + into a list. + * :py:class:`~lmi.scripts.common.errors.LmiNoConnections` + if no successful connection was done. + """ + if cache is None: + cache = dict() + conds, default = self.get_conditionals() + def get_cmd_factory(cmd): + if isinstance(cmd, basestring): + i = cmd.rindex('.') + module = __import__(cmd[:i], fromlist=cmd[i+1:]) + return getattr(module, cmd[i+1:]) + else: + return cmd + + session = self.session + unsatisfied = set(session.hostnames) + + for expr, cmd in conds: + hosts = self.eval_expr(expr, unsatisfied, cache) + if hosts: + yield get_cmd_factory(cmd), SessionProxy(session, hosts) + hosts = set(hosts).union(set(session.get_unconnected())) + unsatisfied.difference_update(hosts) + if not unsatisfied: + break + if default is not None and unsatisfied: + yield get_cmd_factory(default), SessionProxy(session, unsatisfied) + unsatisfied.clear() + if len(unsatisfied): + raise errors.LmiUnsatisfiedDependencies(unsatisfied) + if len(session) == len(session.get_unconnected()): + raise errors.LmiNoConnections("No successful connection!") + + def get_usage(self, proper=False): + """ + Try to get usage of any command satisfying some expression. + + :raises: Same exceptions as :py:meth:`select_cmds`. + """ + for cmd_cls, _ in self.select_cmds(): + cmd = cmd_cls(self.app, self.cmd_name, self.parent) + return cmd.get_usage(proper) + + def run(self, args): + """ + Iterate over command factories with associated sessions and + execute them with unchanged *args*. + """ + result = 0 + for cmd_cls, session in self.select_cmds(): + cmd = cmd_cls(self.app, self.cmd_name, self.parent) + cmd.set_session_proxy(session) + ret = cmd.run(args) + if result == 0: + result = ret + return result + diff --git a/lmi/scripts/common/errors.py b/lmi/scripts/common/errors.py index 615fbf8..5162cf8 100644 --- a/lmi/scripts/common/errors.py +++ b/lmi/scripts/common/errors.py @@ -45,6 +45,16 @@ class LmiFailed(LmiError): """ pass +class LmiUnsatisfiedDependencies(LmiFailed): + """ + Raised when no guarded command in + :py:class:`~.command.select.LmiSelectCommand` can be loaded due to + unsatisfied requirements. + """ + def __init__(self, uris): + LmiFailed.__init__(self, "Profile and class dependencies were not" + " satisfied for this session (%s)." % ', '.join(uris)) + class LmiUnexpectedResult(LmiError): """ Raised, when command's associated function returns something unexpected. @@ -113,6 +123,15 @@ class LmiCommandInvalidCallable(LmiCommandInvalidProperty): def __init__(self, module_name, class_name, msg): LmiCommandInvalidProperty.__init__(self, module_name, class_name, msg) +class LmiBadSelectExpression(LmiCommandError): + """ + Raised, when expression of :py:class:`~.command.select.LmiSelectCommand` + could not be evaluated. + """ + def __init__(self, module_name, class_name, expr): + LmiCommandError.__init__(self, module_name, class_name, + "Bad select expression: %s" % expr) + class LmiTerminate(Exception): """ Raised to cleanly terminate interavtive shell. diff --git a/lmi/scripts/common/util.py b/lmi/scripts/common/util.py new file mode 100644 index 0000000..4ee2161 --- /dev/null +++ b/lmi/scripts/common/util.py @@ -0,0 +1,146 @@ +# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are +# those of the authors and should not be interpreted as representing official +# policies, either expressed or implied, of the FreeBSD Project. +# +# Authors: Michal Minar <miminar@redhat.com> +# +""" +Various utilities for LMI Scripts. +""" + +class FilteredDict(dict): + """ + Dictionary-like collection that wraps some other dictionary and provides + limited access to its keys and values. It permits to get, delete and set + items specified in advance. + + .. note:: + Please use only the methods overriden. This class does not guarantee + 100% API compliance. Not overriden methods won't work properly. + + :param list key_filter: Set of keys that can be get, set or deleted. + For other keys, :py:class:`KeyError` will be raised. + :param dictionary original: Original dictionary containing not only + keys in *key_filter* but others as well. All modifying operations + operate also on this dictionary. But only those keys in *key_filter* + can be affected by them. + """ + + def __init__(self, key_filter, original=None): + dict.__init__(self) + if original is not None and not isinstance(original, dict): + raise TypeError("original needs to be a dictionary") + if original is None: + original = dict() + self._original = original + self._keys = frozenset(key_filter) + + def __contains__(self, key): + return key in self._keys and key in self._original + + def __delitem__(self, key): + if not key in self._keys: + raise KeyError(repr(key)) + del self._original[key] + + def clear(self): + for key in self._keys: + self._original.pop(key, None) + + def copy(self): + return FilteredDict(self._keys, self._original) + + def iterkeys(self): + for key in self._keys: + yield key + + def __getitem__(self, key): + if not key in self._keys: + raise KeyError(repr(key)) + return self._original[key] + + def __iter__(self): + for key in self._keys: + if key in self._original: + yield key + + def __eq__(self, other): + return ( isinstance(other, FilteredDict) + and self._original == other._original + and self._keys == other._keys) + + def __lt__(self, other): + if isinstance(other, dict): + return { k: v for k, v in self._original.items() + if k in self._keys} < other + if not isinstance(other, FilteredDict): + raise TypeError("Can not compare FilteredDict to objects" + " of other types!") + return self._original <= other._original and self._keys <= other._keys + + def __len__(self): + return len(self.keys()) + + def __setitem__(self, key, value): + if not key in self._keys: + raise KeyError(repr(key)) + self._original[key] = value + + def keys(self): + return [k for k in self._keys if k in self._original] + + def values(self): + return [self._original[k] for k in self.keys() if k in self._original] + + def items(self): + return [(k, self._original[k]) for k in self.keys()] + + def iteritems(self): + return iter(self.items()) + + def pop(self, key, *args): + ret = self[key] + del self[key] + return ret + + def popitem(self): + for key in self._keys: + if key in self._original: + return self.pop(key) + raise KeyError("FilterDict is empty!") + + def update(self, *args, **kwargs): + if len(args) > 1: + raise TypeError('Expected just one positional argument!') + if args and callable(getattr(args[0], 'keys', None)): + for key in args[0].keys(): + self[key] = args[0][key] + elif args: + for key, value in args[0]: + self[key] = value + for key, value in kwargs.items(): + self[key] = value + diff --git a/lmi/scripts/common/versioncheck/__init__.py b/lmi/scripts/common/versioncheck/__init__.py new file mode 100644 index 0000000..c7e595f --- /dev/null +++ b/lmi/scripts/common/versioncheck/__init__.py @@ -0,0 +1,146 @@ +# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are +# those of the authors and should not be interpreted as representing official +# policies, either expressed or implied, of the FreeBSD Project. +# +# Authors: Michal Minar <miminar@redhat.com> +# +""" +Package with utilities for checking availability of profiles or CIM classes. +Version requirements can also be specified. +""" + +import functools +from pyparsing import ParseException + +from lmi.scripts.common import Configuration +from lmi.scripts.common import errors +from lmi.scripts.common.versioncheck import parser + +def cmp_profiles(fst, snd): + """ + Compare two profiles by their version. + + :returns: + * -1 if the *fst* profile has lower version than *snd* + * 0 if their versions are equal + * 1 otherwise + :rtype: int + """ + fstver = fst.RegisteredVersion + sndver = snd.RegisteredVersion + if fstver == sndver: + return 0 + return -1 if parser.cmp_version(fstver, sndver) else 1 + +def get_profile_version(conn, name, cache=None): + """ + Get version of registered profile on particular broker. Queries + ``CIM_RegisteredProfile`` and ``CIM_RegisteredSubProfile``. The latter + comes in question only when ``CIM_RegisteredProfile`` does not yield any + matching result. + + :param conn: Connection object. + :param string name: Name of the profile which must match value of *RegisteredName* + property. + :param dictionary cache: Optional cache where the result will be stored for + later use. This greatly speeds up evaluation of several expressions refering + to same profiles or classes. + :returns: Version of matching profile found. If there were more of them, + the highest version will be returned. ``None`` will be returned when no matching + profile or subprofile is found. + :rtype: string + """ + if cache and name in cache: + return cache[(conn.uri, name)] + insts = conn.root.interop.wql('SELECT * FROM CIM_RegisteredProfile' + ' WHERE RegisteredName=\"%s\"' % name) + regular = set(i for i in insts if i.classname.endswith('RegisteredProfile')) + if regular: # select instances of PG_RegisteredProfile if available + insts = regular + else: # otherwise fallback to PG_RegisteredSubProfile instances + insts = set(i for i in insts if i not in regular) + if not insts: + ret = None + else: + ret = sorted(insts, cmp=cmp_profiles)[-1].RegisteredVersion + if cache is not None: + cache[(conn.uri, name)] = ret + return ret + +def get_class_version(conn, name, namespace=None, cache=None): + """ + Query broker for version of particular CIM class. Version is stored in + ``Version`` qualifier of particular CIM class. + + :param conn: Connection object. + :param string name: Name of class to query. + :param string namespace: Optional CIM namespace. Defaults to configured namespace. + :param dictionary cache: Optional cache used to speed up expression prrocessing. + :returns: Version of CIM matching class. Empty string if class is registered but + is missing ``Version`` qualifier and ``None`` if it is not registered. + :rtype: string + """ + if namespace is None: + namespace = Configuration.get_instance().namespace + if cache and (namespace, name) in cache: + return cache[(conn.uri, namespace, name)] + ns = conn.get_namespace(namespace) + cls = getattr(ns, name, None) + if not cls: + ret = None + else: + quals = cls.wrapped_object.qualifiers + if 'Version' not in quals: + ret = '' + else: + ret = quals['Version'].value + if cache is not None: + cache[(conn.uri, namespace, name)] = ret + return ret + +def eval_respl(expr, conn, namespace=None, cache=None): + """ + Evaluate LMIReSpL expression on particular broker. + + :param string expr: Expression to evaluate. + :param conn: Connection object. + :param string namespace: Optional CIM namespace where CIM classes will be + searched. + :param dictionary cache: Optional cache speeding up evaluation. + :returns: ``True`` if requirements in expression are satisfied. + :rtype: boolean + """ + if namespace is None: + namespace = Configuration.get_instance().namespace + stack = [] + pvget = functools.partial(get_profile_version, conn, cache=cache) + cvget = functools.partial(get_class_version, conn, + namespace=namespace, cache=cache) + pr = parser.bnf_parser(stack, pvget, cvget) + pr.parseString(expr, parseAll=True) + # Now evaluate starting non-terminal created on stack. + return stack[0]() + diff --git a/lmi/scripts/common/versioncheck/parser.py b/lmi/scripts/common/versioncheck/parser.py new file mode 100644 index 0000000..b5f9116 --- /dev/null +++ b/lmi/scripts/common/versioncheck/parser.py @@ -0,0 +1,521 @@ +# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are +# those of the authors and should not be interpreted as representing official +# policies, either expressed or implied, of the FreeBSD Project. +# +# Authors: Michal Minar <miminar@redhat.com> +# +""" +Parser for mini-language specifying profile and class requirements. We call +the language LMIReSpL (openLMI Requirement Specification Language). + +The only thing designed for use outside this module is :py:func:`bnf_parser`. + +Language is generated by BNF grammer which served as a model for parser. + +Formal representation of BNF grammer is following: :: + + expr ::= term [ op expr ]* + term ::= '!'? req + req ::= profile_cond | clsreq_cond | '(' expr ')' + profile_cond ::= 'profile'? [ profile | profile_quot ] cond? + clsreq_cond ::= 'class' [ clsname | clsname_quot] cond? + profile_quot ::= '"' /\w+[ +.a-zA-Z0-9_-]*/ '"' + profile ::= /\w+[+.a-zA-Z_-]*/ + clsname_quot ::= '"' clsname '"' + clsname ::= /[a-zA-Z]+_[a-zA-Z][a-zA-Z0-9_]*/ + cond ::= cmpop version + cmpop ::= /(<|=|>|!)=|<|>/ + version ::= /[0-9]+(\.[0-9]+)*/ + op ::= '&' | '|' + +String surrounded by quotes is a literal. String enclosed with slashes is a +regular expression. Square brackets encloses a group of words and limit +the scope of some operation (like iteration). +""" +import abc +import operator +from pyparsing import Literal, Combine, Optional, ZeroOrMore, \ + Forward, Regex, Keyword, FollowedBy, LineEnd, ParseException + +#: Dictionary mapping supported comparison operators to a pair. First item is a +#: function making the comparison and the second can be of two values (``all`` +#: or ``any``). Former sayes that each part of first version string must be in +#: relation to corresponding part of second version string in order to satisfy +#: the condition. The latter causes the comparison to end on first satisfied +#: part. +OP_MAP = { + '==' : (operator.eq, all), + '<=' : (operator.le, all), + '>=' : (operator.ge, all), + '!=' : (operator.ne, any), + '>' : (operator.gt, any), + '<' : (operator.lt, any) +} + +def cmp_version(fst, snd, opsign='<'): + """ + Compare two version specifications. Each version string shall contain + digits delimited with dots. Empty string is also valid version. It will be + replaced with -1. + + :param str fst: First version string. + :param str snd: Second version string. + :param str opsign: Sign denoting operation to be used. Supported signs + are present in :py:attr:`OP_MAP`. + :returns: ``True`` if the relation denoted by particular operation exists + between two operands. + :rtype: boolean + """ + def splitver(ver): + """ Converts version string to a tuple of integers. """ + return tuple(int(p) if p else -1 for p in ver.split('.')) + aparts = splitver(fst) + bparts = splitver(snd) + op, which = OP_MAP[opsign] + if which is all: + for ap, bp in zip(aparts, bparts): + if not op(ap, bp): + return False + else: + for ap, bp in zip(aparts, bparts): + if op(ap, bp): + return True + if ap != bp: + return False + return op(len(aparts), len(bparts)) + +class SemanticGroup(object): + """ + Base class for non-terminals. Just a minimal set of non-terminals is + represented by objects the rest is represented by strings. + + All subclasses need to define their own :py:meth:`evaluate` method. The + parser builds a tree of these non-terminals with single non-terminal being + a root node. This node's *evaluate* method returns a boolean saying whether + the condition is satisfied. Root node is always an object of + :py:class:`Expr`. + """ + + __metaclass__ = abc.ABCMeta + + def __call__(self): + return self.evaluate() + + @abc.abstractmethod + def evaluate(self): + """ + :returns: ``True`` if the sub-condition represented by this non-terminal + is satisfied. + :rtype: boolean + """ + pass + +class Expr(SemanticGroup): + """ + Initial non-terminal. Object of this class (or one of its subclasses) is a + result of parsing. + + :param term: An object of :py:class:`Term` non-terminal. + """ + + def __init__(self, term): + assert isinstance(term, Term) + self.fst = term + + def evaluate(self): + return self.fst() + + def __str__(self): + return str(self.fst) + +class And(Expr): + """ + Represents logical *AND* of two expressions. Short-circuit evaluation is + being exploited here. + + :param fst: An object of :py:class:`Term` non-terminal. + :param snd: An object of :py:class:`Term` non-terminal. + """ + + def __init__(self, fst, snd): + assert isinstance(snd, (Term, Expr)) + Expr.__init__(self, fst) + self.snd = snd + + def evaluate(self): + if self.fst(): + return self.snd() + return False + + def __str__(self): + return "%s & %s" % (self.fst, self.snd) + +class Or(Expr): + """ + Represents logical *OR* of two expressions. Short-circuit evaluation is being + exploited here. + + :param fst: An object of :py:class:`Term` non-terminal. + :param snd: An object of :py:class:`Term` non-terminal. + """ + + def __init__(self, fst, snd): + assert isinstance(snd, (Term, Expr)) + Expr.__init__(self, fst) + self.snd = snd + + def evaluate(self): + if self.fst(): + return True + return self.snd() + + def __str__(self): + return "%s | %s" % (self.fst, self.snd) + +class Term(SemanticGroup): + """ + Represents possible negation of expression. + + :param req: An object of :py:class:`Req`. + :param boolean negate: Whether the result of children shall be negated. + """ + + def __init__(self, req, negate): + assert isinstance(req, Req) + self.req = req + self.negate = negate + + def evaluate(self): + res = self.req() + return not res if self.negate else res + + def __str__(self): + if self.negate: + return '!' + str(self.req) + return str(self.req) + +class Req(SemanticGroup): + """ + Represents one of following subexpressions: + + * single requirement on particular profile + * single requirement on particular class + * a subexpression + """ + pass + +class ReqCond(Req): + """ + Represents single requirement on particular class or profile. + + :param str kind: Name identifying kind of thing this belongs. For example + ``'class'`` or ``'profile'``. + :param callable version_getter: Is a function called to get version of + either profile or CIM class. It must return corresponding version string + if the profile or class is registered and ``None`` otherwise. + Version string is read from ``RegisteredVersion`` property of + ``CIM_RegisteredProfile``. If a class is being queried, version + shall be taken from ``Version`` qualifier of given class. + :param str name: Name of profile or CIM class to check for. In case + of a profile, it is compared to ``RegisteredName`` property of + ``CIM_RegisteredProfile``. If any instance of this class has matching + name, it's version will be checked. If no matching instance is found, + instances of ``CIM_RegisteredSubProfile`` are queried the same way. + Failing to find it results in ``False``. + :param str cond: Is a version requirement. Check the grammer above for + ``cond`` non-terminal. + """ + + def __init__(self, kind, version_getter, name, cond=None): + assert isinstance(kind, basestring) + assert callable(version_getter) + assert isinstance(name, basestring) + assert cond is None or (isinstance(cond, tuple) and len(cond) == 2) + self.kind = kind + self.version_getter = version_getter + self.name = name + self.cond = cond + + def evaluate(self): + version = self.version_getter(self.name) + return version and (not self.cond or self._check_version(version)) + + def _check_version(self, version): + """ + Checks whether the version of profile or class satisfies the + requirement. Version strings are first split into a list of integers + (that were delimited with a dot) and then they are compared in + descending order from the most signigicant down. + + :param str version: Version of profile or class to check. + """ + opsign, cmpver = self.cond + return cmp_version(version, cmpver, opsign) + + def __str__(self): + return '{%s "%s"%s}' % ( + self.kind, self.name, ' %s %s' % self.cond if self.cond else '') + +class Subexpr(Req): + """ + Represents a subexpression originally enclosed in brackets. + """ + + def __init__(self, expr): + assert isinstance(expr, Expr) + self.expr = expr + + def evaluate(self): + return self.expr() + + def __str__(self): + return "(%s)" % self.expr + +class TreeBuilder(object): + """ + A stack interface for parser. It defines methods modifying the stack with + additional checks. + """ + + def __init__(self, stack, profile_version_getter, class_version_getter): + if not isinstance(stack, list): + raise TypeError("stack needs to be empty!") + if stack: + stack[:] = [] + self.stack = stack + self.profile_version_getter = profile_version_getter + self.class_version_getter = class_version_getter + + def expr(self, strg, loc, toks): + """ + Operates upon a stack. It takes either one or two *terms* there + and makes an expression object out of them. Terms need to be delimited + with logical operator. + """ + assert len(self.stack) > 0 + if not isinstance(self.stack[-1], (Term, Expr)): + raise ParseException("Invalid expression (stopped at char %d)." + % loc) + if len(self.stack) >= 3 and self.stack[-2] in ('&', '|'): + assert isinstance(self.stack[-3], Term) + if self.stack[-2] == '&': + expr = And(self.stack[-3], self.stack[-1]) + else: + expr = Or(self.stack[-3], self.stack[-1]) + self.stack.pop() + self.stack.pop() + elif not isinstance(self.stack[-1], Expr): + expr = Expr(self.stack[-1]) + else: + expr = self.stack[-1] + self.stack[-1] = expr + + def term(self, strg, loc, toks): + """ + Creates a ``term`` out of requirement (``req`` non-terminal). + """ + assert len(self.stack) > 0 + assert isinstance(self.stack[-1], Req) + self.stack[-1] = Term(self.stack[-1], toks[0] == '!') + + def subexpr(self, strg, loc, toks): + """ + Operates upon a stack. It creates an instance of :py:class:`Subexpr` + out of :py:class:`Expr` which is enclosed in brackets. + """ + assert len(self.stack) > 1 + assert self.stack[-2] == '(' + assert isinstance(self.stack[-1], Expr) + assert len(toks) > 0 and toks[-1] == ')' + self.stack[-2] = Subexpr(self.stack[-1]) + self.stack.pop() + + def push_class(self, strg, loc, toks): + """ + Handles ``clsreq_cond`` non-terminal in one go. It extracts + corresponding tokens and pushes an object of :py:class:`ReqCond` to a + stack. + """ + assert toks[0] == 'class' + assert len(toks) >= 2 + name = toks[1] + condition = None + if len(toks) > 2 and toks[2] in OP_MAP: + assert len(toks) >= 4 + condition = toks[2], toks[3] + self.stack.append(ReqCond('class', self.class_version_getter, + name, condition)) + + def push_profile(self, strg, loc, toks): + """ + Handles ``profile_cond`` non-terminal in one go. It behaves in the same + way as :py:meth:`push_profile`. + """ + index = 0 + if toks[0] == 'profile': + index = 1 + assert len(toks) > index + name = toks[index] + index += 1 + condition = None + if len(toks) > index and toks[index] in OP_MAP: + assert len(toks) >= index + 2 + condition = toks[index], toks[index + 1] + self.stack.append(ReqCond('profile', self.profile_version_getter, + name, condition)) + + def push_literal(self, strg, loc, toks): + """ + Pushes operators to a stack. + """ + assert toks[0] in ('&', '|', '(') + if toks[0] == '(': + assert not self.stack or self.stack[-1] in ('&', '|') + else: + assert len(self.stack) > 0 + assert isinstance(self.stack[-1], Term) + self.stack.append(toks[0]) + +def bnf_parser(stack, profile_version_getter, class_version_getter): + """ + Builds a parser operating on provided stack. + + :param list stack: Stack to operate on. It will contain the resulting + :py:class:`Expr` object when the parsing is successfully over - + it will be the only item in the list. It needs to be initially empty. + :param callable profile_version_getter: Function returning version + of registered profile or ``None`` if not present. + :param callable class_version_getter: Fucntion returning version + of registered class or ``None`` if not present. + :returns: Parser object. + :rtype: :py:class:`pyparsing,ParserElement` + """ + if not isinstance(stack, list): + raise TypeError("stack must be a list!") + builder = TreeBuilder(stack, profile_version_getter, class_version_getter) + + ntop = ((Literal('&') | Literal('|')) + FollowedBy(Regex('["a-zA-Z\(!]'))) \ + .setName('op').setParseAction(builder.push_literal) + ntversion = Regex(r'[0-9]+(\.[0-9]+)*').setName('version') + ntcmpop = Regex(r'(<|=|>|!)=|<|>(?=\s*\d)').setName('cmpop') + ntcond = (ntcmpop + ntversion).setName('cond') + ntclsname = Regex(r'[a-zA-Z]+_[a-zA-Z][a-zA-Z0-9_]*').setName('clsname') + ntclsname_quot = Combine( + Literal('"').suppress() + + ntclsname + + Literal('"').suppress()).setName('clsname_quot') + ntprofile_quot = Combine( + Literal('"').suppress() + + Regex(r'\w+[ +.a-zA-Z0-9_-]*') + + Literal('"').suppress()).setName('profile_quot') + ntprofile = Regex(r'\w+[+.a-zA-Z0-9_-]*').setName('profile') + ntclsreq_cond = ( + Keyword('class') + + (ntclsname_quot | ntclsname) + + Optional(ntcond)).setName('clsreq_cond').setParseAction( + builder.push_class) + ntprofile_cond = ( + Optional(Keyword('profile')) + + (ntprofile_quot | ntprofile) + + Optional(ntcond)).setName('profile_cond').setParseAction( + builder.push_profile) + ntexpr = Forward().setName('expr') + bracedexpr = ( + Literal('(').setParseAction(builder.push_literal) + + ntexpr + + Literal(')')).setParseAction(builder.subexpr) + ntreq = (bracedexpr | ntclsreq_cond | ntprofile_cond).setName('req') + ntterm = (Optional(Literal("!")) + ntreq + FollowedBy(Regex('[\)&\|]') | LineEnd()))\ + .setParseAction(builder.term) + ntexpr << ntterm + ZeroOrMore(ntop + ntexpr).setParseAction(builder.expr) + + return ntexpr + +if __name__ == '__main__': + def get_class_version(class_name): + try: + version = { 'lmi_logicalfile' : '0.1.2' + , 'lmi_softwareidentity' : '3.2.1' + , 'pg_computersystem' : '1.1.1' + }[class_name.lower()] + except KeyError: + version = None + return version + + def get_profile_version(profile_name): + try: + version = { 'openlmi software' : '0.1.2' + , 'openlmi-software' : '1.3.4' + , 'openlmi hardware' : '1.1.1' + , 'openlmi-hardware' : '0.2.3' + }[profile_name.lower()] + except KeyError: + version = None + return version + + def test(s, expected): + stack = [] + parser = bnf_parser(stack, get_profile_version, get_class_version) + results = parser.parseString(s, parseAll=True) + if len(stack) == 1: + evalresult = stack[0]() + if expected == evalresult: + print "%s\t=>\tOK" % s + else: + print "%s\t=>\tFAILED" % s + else: + print "%s\t=>\tFAILED" % s + print " stack: [%s]" % ', '.join(str(i) for i in stack) + + test( 'class LMI_SoftwareIdentity == 0.2.0', False) + test( '"OpenLMI-Software" == 0.1.2 & "OpenLMI-Hardware" < 0.1.3', False) + test( 'OpenLMI-Software<1|OpenLMI-Hardware!=1.2.4', True) + test( '"OpenLMI Software" & profile "OpenLMI Hardware"' + ' | ! class LMI_LogicalFile', True) + test( 'profile OpenLMI-Software > 0.1.2 & !(class "PG_ComputerSystem"' + ' == 2.3.4 | "OpenLMI Hardware")', False) + test( 'OpenLMI-Software > 1.3 & OpenLMI-Software >= 1.3.4' + ' & OpenLMI-Software < 1.3.4.1 & OpenLMI-Software <= 1.3.4' + ' & OpenLMI-Software == 1.3.4', True) + test( 'OpenLMI-Software < 1.3.4 | OpenLMI-Software > 1.3.4' + ' | OpenLMI-Software != 1.3.4', False) + test( '(! OpenLMI-Software == 1.3.4 | OpenLMI-Software <= 1.3.4.1)' + ' & !(openlmi-software > 1.3.4 | Openlmi-software != 1.3.4)', True) + for badexpr in ( + 'OpenLMI-Software > & OpenLMI-Hardware', + 'classs LMI_SoftwareIdentity', + 'OpenLMI-Software > 1.2.3 | == 5.4.3', + '', + '"OpenLMI-Software', + 'OpenLMI-Software < > OpenLMI-Hardware', + 'OpenlmiSoftware & (openLMI-Hardware ', + 'OpenLMISoftware & ) OpenLMI-Hardare (', + 'OpenLMISoftware | OpenlmiSoftware > "1.2.3"' + ): + try: + test(badexpr, None) + except ParseException: + print "%s\t=>\tOK" % badexpr |