summaryrefslogtreecommitdiffstats
path: root/lmi
diff options
context:
space:
mode:
Diffstat (limited to 'lmi')
-rw-r--r--lmi/scripts/_metacommand/__init__.py2
-rw-r--r--lmi/scripts/_metacommand/exit.py1
-rw-r--r--lmi/scripts/_metacommand/help.py7
-rw-r--r--lmi/scripts/_metacommand/interactive.py4
-rw-r--r--lmi/scripts/common/command/__init__.py1
-rw-r--r--lmi/scripts/common/command/base.py45
-rw-r--r--lmi/scripts/common/command/meta.py113
-rw-r--r--lmi/scripts/common/command/select.py195
-rw-r--r--lmi/scripts/common/errors.py19
-rw-r--r--lmi/scripts/common/util.py146
-rw-r--r--lmi/scripts/common/versioncheck/__init__.py146
-rw-r--r--lmi/scripts/common/versioncheck/parser.py521
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