summaryrefslogtreecommitdiffstats
path: root/lmi/scripts/common/versioncheck
diff options
context:
space:
mode:
Diffstat (limited to 'lmi/scripts/common/versioncheck')
-rw-r--r--lmi/scripts/common/versioncheck/__init__.py146
-rw-r--r--lmi/scripts/common/versioncheck/parser.py521
2 files changed, 667 insertions, 0 deletions
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