summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichal Minar <miminar@redhat.com>2014-03-17 08:11:34 +0100
committerMichal Minar <miminar@redhat.com>2014-03-17 16:26:36 +0100
commiteab5df929f59827a7493651b305661a611bc25fa (patch)
tree6b4934ce6964736a382ee0082bc5b52f765ac419
parente67c9462f426df7f3d1e40952c2f97f51714fc56 (diff)
downloadopenlmi-scripts-eab5df929f59827a7493651b305661a611bc25fa.tar.gz
openlmi-scripts-eab5df929f59827a7493651b305661a611bc25fa.tar.xz
openlmi-scripts-eab5df929f59827a7493651b305661a611bc25fa.zip
added suppport for profile and class requirements
Added special command LmiSelectCommand taking set of conditions with associated commands to load. Conditions must be met for command to be loaded. They contain profile and class requirements on remote broker.
-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
-rw-r--r--test/README.md7
-rw-r--r--test/cmdver/README.md1
-rw-r--r--test/cmdver/lmi/__init__.py27
-rw-r--r--test/cmdver/lmi/scripts/__init__.py27
-rw-r--r--test/cmdver/lmi/scripts/cmdver/__init__.py135
-rw-r--r--test/cmdver/lmi/scripts/cmdver/devel.py32
-rw-r--r--test/cmdver/lmi/scripts/cmdver/pre042.py32
-rw-r--r--test/cmdver/lmi/scripts/cmdver/swbase.py51
-rw-r--r--test/cmdver/lmi/scripts/cmdver/ver042.py32
-rw-r--r--test/cmdver/setup.py48
-rw-r--r--test/test_unit.sh69
-rw-r--r--test/test_versioning.sh236
-rw-r--r--test/unit/test_common.py79
25 files changed, 1968 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
diff --git a/test/README.md b/test/README.md
index 03219b3..cfcbbf7 100644
--- a/test/README.md
+++ b/test/README.md
@@ -11,6 +11,10 @@ Dependencies
* openlmi-tools
Openlmi scripts need not be installed.
+
+Remote host shall have `openlmi-software` and `openlmi-hardware` installed. If
+not, `LMI_SOFTWARE_PROVIDER_VERSION` and `LMI_HARDWARE_PROVIDER_VERSION` needs
+to be set to `none`.
Run
---
@@ -20,6 +24,9 @@ can connect to it. Export these variables:
* LMI_CIMOM_URL
* LMI_CIMOM_USERNAME
* LMI_CIMOM_PASSWORD
+ * LMI_SOFTWARE_PROVIDER_VERSION - version of software provider registered
+ with CIMOM
+ * LMI_HARDWARE_PROVIDER_VERSION
Execute:
$ ./run.sh
diff --git a/test/cmdver/README.md b/test/cmdver/README.md
new file mode 100644
index 0000000..14c401e
--- /dev/null
+++ b/test/cmdver/README.md
@@ -0,0 +1 @@
+Subcommand just for testing purposes.
diff --git a/test/cmdver/lmi/__init__.py b/test/cmdver/lmi/__init__.py
new file mode 100644
index 0000000..b1a2ff0
--- /dev/null
+++ b/test/cmdver/lmi/__init__.py
@@ -0,0 +1,27 @@
+# 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.
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/test/cmdver/lmi/scripts/__init__.py b/test/cmdver/lmi/scripts/__init__.py
new file mode 100644
index 0000000..b1a2ff0
--- /dev/null
+++ b/test/cmdver/lmi/scripts/__init__.py
@@ -0,0 +1,27 @@
+# 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.
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/test/cmdver/lmi/scripts/cmdver/__init__.py b/test/cmdver/lmi/scripts/cmdver/__init__.py
new file mode 100644
index 0000000..f9e0c8b
--- /dev/null
+++ b/test/cmdver/lmi/scripts/cmdver/__init__.py
@@ -0,0 +1,135 @@
+# 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.
+
+import pywbem
+from lmi.scripts.common import command
+from lmi.scripts.common import get_computer_system
+
+# 1st entry point
+class CmdverSw(command.LmiSelectCommand):
+ """
+ This is a short description for CmdverSw.
+ """
+ SELECT = (
+ ('OpenLMI-Software < 0.4.2', 'lmi.scripts.cmdver.pre042.Cmd'),
+ ('OpenLMI-Software == 0.4.2', 'lmi.scripts.cmdver.ver042.Cmd'),
+ ('OpenLMI-Software > 0.4.2', 'lmi.scripts.cmdver.devel.Cmd'),
+ )
+
+def get_hw_profile_version(ns):
+ try:
+ return ns.connection.root.interop.wql('SELECT * FROM PG_RegisteredProfile'
+ ' WHERE RegisteredName="OpenLMI-Hardware"')[0].RegisteredVersion
+ except pywbem.CIMError, IndexError:
+ return None
+
+class SystemInfo(command.LmiLister):
+ COLUMNS = []
+ PRE042 = False
+
+ def execute(self, ns):
+ cls = ns.LMI_Chassis
+ inst = cls.first_instance()
+ verstr = get_hw_profile_version(ns)
+ if self.PRE042:
+ verstr += ' (PRE 0.4.2)'
+ return [('Prov version', verstr),
+ ('Chassis Type', cls.ChassisPackageTypeValues.value_name(
+ inst.ChassisPackageType))]
+
+class HostnameInfo(command.LmiLister):
+ COLUMNS = []
+ PRE042 = False
+
+ def execute(self, ns):
+ verstr = get_hw_profile_version(ns)
+ if self.PRE042:
+ verstr += ' (PRE 0.4.2)'
+ return [('Prov version', verstr),
+ ('Hostname', get_computer_system(ns).Name)]
+
+class PreSystemInfo(SystemInfo):
+ PRE042 = True
+
+class PreHostnameInfo(HostnameInfo):
+ PRE042 = True
+
+class HwCmd(command.LmiCommandMultiplexer):
+ """
+ Hardware testing command.
+
+ Usage:
+ %(cmd)s system
+ %(cmd)s hostname
+ """
+ COMMANDS = {
+ 'system' : SystemInfo,
+ 'hostname' : HostnameInfo
+ }
+ OWN_USAGE = True
+
+class PreHwCmd(HwCmd):
+ COMMANDS = {
+ 'system' : PreSystemInfo,
+ 'hostname' : PreHostnameInfo
+ }
+
+class NoHwRegistered(command.LmiLister):
+ """
+ Hardware testing command.
+
+ Usage: %(cmd)s <cmd>
+ """
+ COLUMNS = []
+ OWN_USAGE = True
+ def execute(self, ns, cmd):
+ return [('Given command', cmd), ('Prov version', 'N/A')]
+
+# 2nd entry point
+class CmdverHw(command.LmiSelectCommand):
+ """
+ This is a short description for CmdverHw.
+ """
+ SELECT = (
+ ('OpenLMI-Hardware < 0.4.2', PreHwCmd),
+ ('OpenLMI-Hardware >= 0.4.2 & class LMI_Chassis == 0.3.0', HwCmd)
+ )
+ DEFAULT = NoHwRegistered
+
+# 3rd entry point
+class Cmdver(command.LmiCommandMultiplexer):
+ """
+ Command for testing version dependencies.
+
+ Usage:
+ %(cmd)s (sw|hw) [<args>...]
+ """
+ COMMANDS = {
+ 'sw' : CmdverSw,
+ 'hw' : CmdverHw
+ }
+ OWN_USAGE = True
diff --git a/test/cmdver/lmi/scripts/cmdver/devel.py b/test/cmdver/lmi/scripts/cmdver/devel.py
new file mode 100644
index 0000000..624428e
--- /dev/null
+++ b/test/cmdver/lmi/scripts/cmdver/devel.py
@@ -0,0 +1,32 @@
+# 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.
+
+from lmi.scripts.common import command
+from lmi.scripts.cmdver import swbase
+
+class Cmd(swbase.SwCmdBase):
+ ADDITIONAL_VERSION_INFO = ' (DEVEL)'
diff --git a/test/cmdver/lmi/scripts/cmdver/pre042.py b/test/cmdver/lmi/scripts/cmdver/pre042.py
new file mode 100644
index 0000000..e7565ae
--- /dev/null
+++ b/test/cmdver/lmi/scripts/cmdver/pre042.py
@@ -0,0 +1,32 @@
+# 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.
+
+from lmi.scripts.common import command
+from lmi.scripts.cmdver import swbase
+
+class Cmd(swbase.SwCmdBase):
+ ADDITIONAL_VERSION_INFO = ' (PRE 0.4.2)'
diff --git a/test/cmdver/lmi/scripts/cmdver/swbase.py b/test/cmdver/lmi/scripts/cmdver/swbase.py
new file mode 100644
index 0000000..eed9dc2
--- /dev/null
+++ b/test/cmdver/lmi/scripts/cmdver/swbase.py
@@ -0,0 +1,51 @@
+# 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.
+"""
+Software testing command.
+
+Usage: %(cmd)s
+"""
+
+import pywbem
+from lmi.scripts.common import command
+
+def get_sw_profile_version(ns):
+ try:
+ return ns.connection.root.interop.wql('SELECT * FROM PG_RegisteredProfile'
+ ' WHERE RegisteredName="OpenLMI-Software"')[0].RegisteredVersion
+ except pywbem.CIMError, IndexError:
+ return None
+
+class SwCmdBase(command.LmiLister):
+ OWN_USAGE = __doc__
+ COLUMNS = []
+ ADDITIONAL_VERSION_INFO = ''
+
+ def execute(self, ns):
+ return [('Prov version',
+ get_sw_profile_version(ns) + self.ADDITIONAL_VERSION_INFO)]
+
diff --git a/test/cmdver/lmi/scripts/cmdver/ver042.py b/test/cmdver/lmi/scripts/cmdver/ver042.py
new file mode 100644
index 0000000..433b539
--- /dev/null
+++ b/test/cmdver/lmi/scripts/cmdver/ver042.py
@@ -0,0 +1,32 @@
+# 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.
+
+from lmi.scripts.common import command
+from lmi.scripts.cmdver import swbase
+
+class Cmd(swbase.SwCmdBase):
+ ADDITIONAL_VERSION_INFO = ' (VER 0.4.2)'
diff --git a/test/cmdver/setup.py b/test/cmdver/setup.py
new file mode 100644
index 0000000..58ea463
--- /dev/null
+++ b/test/cmdver/setup.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+from setuptools import setup
+
+try:
+ long_description = open('README.md', 'rt').read()
+except IOError:
+ long_description = ''
+
+setup(
+ name='openlmi-scripts-cmdver',
+ version='0.1.2',
+ description='Test command for versioning.',
+ long_description=long_description,
+ author=u'Michal Minar',
+ author_email='miminar@redhat.com',
+ url='https://github.com/openlmi/openlmi-cmdver',
+ download_url='https://github.com/openlmi/openlmi-cmdver/tarball/master',
+ platforms=['Any'],
+ license="BSD",
+ classifiers=[
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: POSIX :: Linux',
+ 'Topic :: System :: Systems Administration',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Intended Audience :: Developers',
+ 'Environment :: Console',
+ ],
+
+ install_requires=['openlmi-scripts'],
+
+ namespace_packages=['lmi', 'lmi.scripts'],
+ packages=['lmi', 'lmi.scripts', 'lmi.scripts.cmdver'],
+ include_package_data=True,
+
+ entry_points={
+ 'lmi.scripts.cmd': [
+ # All subcommands of lmi command should go here.
+ # See http://pythonhosted.org/openlmi-scripts/script-development.html#writing-setup-py
+ 'ver-sw = lmi.scripts.cmdver:CmdverSw',
+ 'ver-hw = lmi.scripts.cmdver:CmdverHw',
+ 'ver = lmi.scripts.cmdver:Cmdver',
+ ],
+ },
+ )
diff --git a/test/test_unit.sh b/test/test_unit.sh
new file mode 100644
index 0000000..04fe17b
--- /dev/null
+++ b/test/test_unit.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+#
+# Copyright (c) 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>
+
+. ./base.sh
+
+# Set the full test name
+TEST="openlmi-scripts/test/test_cmd.sh"
+
+PACKAGE="openlmi-scripts"
+
+rlJournalStart
+
+rlPhaseStartSetup
+ rlLogInfo "Creating temporary python sandbox"
+ sandbox=`mktemp -d`
+ export PYTHONPATH="$sandbox"
+ pushd ..
+ rlLogInfo "Installing lmi meta-command"
+ rlRun "python setup.py develop --install-dir=$sandbox"
+ popd
+ export "$sandbox:$PATH"
+rlPhaseEnd
+
+rlPhaseStartTest
+ rlLogInfo "Running unittests"
+
+ pushd unit
+ for i in test_*.py; do
+ rlRun "python $i"
+ done
+ popd # unit
+
+rlPhaseEnd
+
+rlPhaseStartCleanup
+ rlLogInfo "Removing temporary python sandbox"
+ rm -rf "$sandbox"
+rlPhaseEnd
+
+rlJournalPrintText
+rlJournalEnd
diff --git a/test/test_versioning.sh b/test/test_versioning.sh
new file mode 100644
index 0000000..0f7f5b6
--- /dev/null
+++ b/test/test_versioning.sh
@@ -0,0 +1,236 @@
+#!/bin/bash
+#
+# Copyright (c) 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>
+
+. ./base.sh
+
+EXIT_CODE_UNSATISFIED=5
+DEFAULT_VERSION='0.4.2'
+
+# Set the full test name
+TEST="openlmi-scripts/test/test_versioning"
+
+# Package being tested
+PACKAGE="openlmi-scripts"
+
+function cmp2int() {
+ digits=( `echo $1 | tr '.' ' '` )
+ result=0
+ for i in `seq 0 $((${#digits[@]} - 1))`; do
+ result=$((result*100))
+ result=$((result + ${digits[$i]}))
+ done
+ echo $result
+}
+
+rlJournalStart
+
+if [[ -z "${LMI_SOFTWARE_PROVIDER_VERSION}" ]]; then
+ msg="No version specified for OpenLMI-Software. Defaulting to "
+ msg+="{$DEFAULT_VERSION}."
+ rlLogInfo "$msg"
+ LMI_SOFTWARE_PROVIDER_VERSION="${DEFAULT_VERSION}"
+elif [[ "$LMI_SOFTWARE_PROVIDER_VERSION" == none ]]; then
+ LMI_SOFTWARE_PROVIDER_VERSION=''
+fi
+if [[ -z "${LMI_HARDWARE_PROVIDER_VERSION}" ]]; then
+ msg="No version specified for OpenLMI-Hardware Defaulting to "
+ msg+="{$DEFAULT_VERSION}."
+ rlLogInfo "$msg"
+ LMI_HARDWARE_PROVIDER_VERSION="${DEFAULT_VERSION}"
+elif [[ "$LMI_SOFTWARE_PROVIDER_VERSION" == none ]]; then
+ LMI_HARDWARE_PROVIDER_VERSION=''
+fi
+
+rlPhaseStartSetup
+ rlLogInfo "Creating temporary python sandbox"
+ sandbox=`mktemp -d`
+ export PYTHONPATH="$sandbox"
+ pushd ..
+ rlLogInfo "Installing lmi meta-command"
+ rlRun "python setup.py develop --install-dir=$sandbox"
+ popd
+ rlLogInfo "Installing testing command"
+ pushd cmdver
+ rlRun "python setup.py develop --install-dir=$sandbox"
+ popd
+ export "$sandbox:$PATH"
+rlPhaseEnd
+
+rlPhaseStartTest
+ rlLogInfo "Test help on select command"
+
+ rlRun -s "$LMI_ help"
+ rlAssertEquals "Check the number of subcommands available." \
+ `grep '^\s\+[[:alnum:]-]\+\s\+-\s\+' $rlRun_LOG | wc -l` 4
+ rlAssertGrep '\<ver\>\s\+-\s\+Command for testing version dependencies\.$' \
+ $rlRun_LOG
+ rlAssertGrep '\<ver-hw\s\+-\s\+This is a short description for CmdverHw\.$' \
+ $rlRun_LOG
+ rlAssertGrep '\<ver-sw\s\+-\s\+This is a short description for CmdverSw\.$' \
+ $rlRun_LOG
+ rm $rlRun_LOG
+
+ rlRun -s "$LMI help ver"
+ rlAssertGrep "^Command for testing version dependencies.$" $rlRun_LOG
+ rlAssertGrep "^Usage:$" $rlRun_LOG
+ rlAssertGrep "^\s\+lmi ver (sw\\|hw) \[<args>\.\.\.\]$'" $rlRun_LOG
+ rm $rlRun_LOG
+
+ if [[ -z "$LMI_SOFTWARE_PROVIDER_VERSION" ]]; then
+ rlRun -s "$LMI help ver-sw" $EXIT_CODE_UNSATISFIED
+ rlAssertGrep "error\s*:\s\+Profile and class dependencies were not satisfied for" \
+ $rlRun_LOG
+ rm $rlRun_LOG
+
+ rlRun -s "$LMI help ver sw" $EXIT_CODE_UNSATISFIED
+ rlAssertGrep "error\s*:\s\+Profile and class dependencies were not satisfied for" \
+ $rlRun_LOG
+ rm $rlRun_LOG
+
+ else
+ rlRun -s "$LMI help ver-sw"
+ rlAssertGrep "^Software testing command.$" $rlRun_LOG
+ rlAssertGrep "Usage: lmi ver-sw" $rlRun_LOG
+ rm $rlRun_LOG
+
+ rlRun -s "$LMI help ver sw" 0
+ rlAssertGrep "^Software testing command.$" $rlRun_LOG
+ rlAssertGrep "Usage: lmi ver sw" $rlRun_LOG
+ rm $rlRun_LOG
+ fi
+
+ if [[ -z "$LMI_HARDWARE_PROVIDER_VERSION" ]]; then
+ rlRun -s "$LMI help ver-hw"
+ rlAssertGrep "^Hardware testing command\.$" $rlRun_LOG
+ rlAssertGrep "^Usage: lmi ver-hw <cmd>$" $rlRun_LOG
+ rm $rlRun_LOG
+
+ rlRun -s "$LMI help ver hw"
+ rlAssertGrep "^Hardware testing command\.$" $rlRun_LOG
+ rlAssertGrep "^Usage: lmi ver hw <cmd>$" $rlRun_LOG
+ rm $rlRun_LOG
+
+ else
+ rlRun -s "$LMI help ver-hw"
+ rlAssertGrep "^Hardware testing command\.$" $rlRun_LOG
+ rlAssertGrep "^Usage:$" $rlRun_LOG
+ rlAssertGrep "^\s\+lmi ver-hw system$" $rlRun_LOG
+ rlAssertGrep "^\s\+lmi ver-hw hostname$" $rlRun_LOG
+ rm $rlRun_LOG
+
+ rlRun -s "$LMI help ver hw" 0
+ rlAssertGrep "^Hardware testing command.$" $rlRun_LOG
+ rlAssertGrep "^Usage:$" $rlRun_LOG
+ rlAssertGrep "^\s\+lmi ver hw system$" $rlRun_LOG
+ rlAssertGrep "^\s\+lmi ver hw hostname$" $rlRun_LOG
+ rm $rlRun_LOG
+
+ rlRun -s "$LMI help ver hw system" 0
+ rlAssertGrep "^Hardware testing command.$" $rlRun_LOG
+ rlAssertGrep "^Usage:$" $rlRun_LOG
+ rlAssertGrep "^\s\+lmi ver hw system$" $rlRun_LOG
+ rlAssertGrep "^\s\+lmi ver hw hostname$" $rlRun_LOG
+ rm $rlRun_LOG
+ fi
+
+rlPhaseEnd
+
+rlPhaseStartTest
+ rlLogInfo "Test software testing command"
+
+ if [[ -z "$LMI_SOFTWARE_PROVIDER_VERSION" ]]; then
+ rlRun -s "$LMI ver-sw" $EXIT_CODE_UNSATISFIED
+ rlAssertGrep "Profile and class dependencies were not satisfied" \
+ $rlRun_LOG
+ rm $rlRun_LOG
+
+ elif [[ `cmp2int $LMI_SOFTWARE_PROVIDER_VERSION` -lt `cmp2int 0.4.2` ]]; then
+
+ rlRun -s "$LMI ver-sw"
+ rlAssertGrep "Prov version.*${LMI_SOFTWARE_PROVIDER_VERSION} (PRE 0.4.2)" $rlRun_LOG
+ rm $rlRun_LOG
+
+ elif [[ `cmp2int $LMI_SOFTWARE_PROVIDER_VERSION` == `cmp2int 0.4.2` ]]; then
+ rlRun -s "$LMI ver-sw"
+ rlAssertGrep "Prov version.*${LMI_SOFTWARE_PROVIDER_VERSION} (VER 0.4.2)" $rlRun_LOG
+ rm $rlRun_LOG
+
+ else
+ rlRun -s "$LMI ver-sw"
+ rlAssertGrep "Prov version.*${LMI_SOFTWARE_PROVIDER_VERSION} (DEVEL)" $rlRun_LOG
+ rm $rlRun_LOG
+ fi
+
+rlPhaseEnd
+
+rlPhaseStartTest
+ rlLogInfo "Test hardware testing command"
+
+ if [[ -z "$LMI_HARDWARE_PROVIDER_VERSION" ]]; then
+ for cmd in "system" "hostname"; do
+ rlRun -s "$LMI ver-hw $cmd"
+ rlAssertEquals "Printed table has just 2 rows" \
+ `cat $rlRun_LOG | wc -l` 2
+ rlAssertGrep "^Given command\s\+$cmd$" $rlRun_LOG
+ rlAssertGrep "^Prov version\s\+N/A" $rlRun_LOG
+ rm $rlRun_LOG
+ done
+
+ else
+ if [[ `cmp2int $LMI_HARDWARE_PROVIDER_VERSION` -lt `cmp2int 0.4.2` ]]; then
+ ver_suffix=' (PRE 0.4.2)'
+ else
+ ver_suffix=''
+ fi
+ for cmd in "system" "hostname"; do
+ rlRun -s "$LMI ver-hw $cmd"
+ rlAssertEquals "Printed table has just 2 rows" \
+ `cat $rlRun_LOG | wc -l` 2
+ rlAssertGrep "^Prov version\s\+$LMI_SOFTWARE_PROVIDER_VERSION$ver_suffix\$" \
+ $rlRun_LOG
+ if [[ $cmd == system ]]; then
+ reg="^Chassis Type\s\+.*"
+ else
+ reg="^Hostname\s\+$HOSTNAME" $rlRun_LOG
+ fi
+ rm $rlRun_LOG
+ done
+ fi
+
+rlPhaseEnd
+
+rlPhaseStartCleanup
+ rlLogInfo "Removing temporary python sandbox"
+ rm -rf "$sandbox"
+rlPhaseEnd
+
+rlJournalPrintText
+rlJournalEnd
diff --git a/test/unit/test_common.py b/test/unit/test_common.py
new file mode 100644
index 0000000..955d964
--- /dev/null
+++ b/test/unit/test_common.py
@@ -0,0 +1,79 @@
+import unittest
+
+from lmi.scripts.common.util import FilteredDict
+
+class FilteredDictTest(unittest.TestCase):
+
+ def test_empty(self):
+ d = FilteredDict(tuple(), {})
+ self.assertEqual(0, len(d))
+ self.assertNotIn('key', d)
+ self.assertEqual(0, len(d.keys()))
+ self.assertEqual(0, len(d.values()))
+ self.assertEqual(0, len(d.items()))
+ self.assertRaises(KeyError, d.__getitem__, 'key')
+ self.assertRaises(KeyError, d.__setitem__, 'key', 'value')
+
+ def test_empty_keys(self):
+ d = FilteredDict(tuple(), {'a': 1})
+ self.assertEqual(0, len(d))
+ self.assertNotIn('a', d)
+ self.assertEqual(0, len(d.keys()))
+ self.assertEqual(0, len(d.values()))
+ self.assertEqual(0, len(d.items()))
+ self.assertRaises(KeyError, d.__getitem__, 'a')
+ self.assertRaises(KeyError, d.__setitem__, 'a', 2)
+
+ def test_empty_origin(self):
+ d = FilteredDict(tuple('a'), {})
+ self.assertEqual(0, len(d))
+ self.assertNotIn('a', d)
+ self.assertEqual(0, len(d.keys()))
+ self.assertEqual(0, len(d.values()))
+ self.assertEqual(0, len(d.items()))
+ self.assertRaises(KeyError, d.__getitem__, 'a')
+ d['a'] = 1
+ self.assertEqual(1, len(d))
+ self.assertEqual(1, d['a'])
+ self.assertIn('a', d)
+ self.assertEqual(['a',], d.keys())
+ self.assertEqual([1], d.values())
+ self.assertEqual([('a', 1)], d.items())
+ di = d.iteritems()
+ self.assertEqual(('a', 1), di.next())
+ self.assertRaises(StopIteration, di.next)
+ d['a'] = 2
+ self.assertEqual(2, d['a'])
+ del d['a']
+ self.assertEqual(0, len(d))
+
+ def test_filled(self):
+ original = {'b': 2, 'c': 3}
+ d = FilteredDict(('a', 'b'), original)
+ self.assertEqual(1, len(d))
+ self.assertNotIn('a', d)
+ self.assertIn('b', d)
+ self.assertNotIn('c', d)
+ self.assertEqual(1, len(d.keys()))
+ self.assertEqual(1, len(d.values()))
+ self.assertEqual(1, len(d.items()))
+ self.assertRaises(KeyError, d.__getitem__, 'a')
+ self.assertEqual(2, d['b'])
+ di = d.iteritems()
+ self.assertEqual(('b', 2), di.next())
+ self.assertRaises(StopIteration, di.next)
+ self.assertEqual(2, d.pop('b'))
+ self.assertEqual(0, len(d))
+ self.assertEqual({'c': 3}, original)
+ d.update({'a': 1, 'b': 4})
+ self.assertEqual({'a': 1, 'b': 4, 'c': 3}, original)
+ self.assertEqual(2, len(d))
+ self.assertEqual(set((('a', 1), ('b', 4))), set(d.items()))
+ d.clear()
+ self.assertEqual(0, len(d))
+ self.assertEqual({'c': 3}, original)
+ self.assertRaises(KeyError, d.__setitem__, 'c', 5)
+ self.assertRaises(KeyError, d.update, {'b': 2, 'c': 3})
+
+if __name__ == '__main__':
+ unittest.main()