summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile33
-rw-r--r--Makefile.inc53
-rw-r--r--README.md74
-rw-r--r--VERSION1
-rw-r--r--commands/README.md124
-rw-r--r--commands/account/Makefile20
-rw-r--r--commands/account/doc/conf.py.skel (renamed from commands/account/doc/conf.py)4
-rw-r--r--commands/account/setup.cfg2
-rw-r--r--commands/account/setup.py.skel (renamed from commands/account/setup.py)7
-rw-r--r--commands/hardware/Makefile1
-rw-r--r--commands/hardware/doc/conf.py.skel (renamed from commands/hardware/doc/conf.py)4
-rw-r--r--commands/hardware/setup.cfg2
-rw-r--r--commands/hardware/setup.py.skel (renamed from commands/hardware/setup.py)2
-rw-r--r--commands/logicalfile/Makefile20
-rw-r--r--commands/logicalfile/doc/conf.py.skel (renamed from commands/logicalfile/doc/conf.py)4
-rw-r--r--commands/logicalfile/setup.py.skel (renamed from commands/logicalfile/setup.py)7
-rwxr-xr-xcommands/make_new.py49
-rw-r--r--commands/networking/Makefile20
-rw-r--r--commands/networking/doc/conf.py.skel (renamed from commands/powermanagement/doc/conf.py)4
-rw-r--r--commands/networking/setup.py.skel (renamed from commands/networking/setup.py)2
-rw-r--r--commands/powermanagement/Makefile1
-rw-r--r--commands/powermanagement/doc/conf.py.skel (renamed from commands/networking/doc/conf.py)4
-rw-r--r--commands/powermanagement/setup.cfg2
-rw-r--r--commands/powermanagement/setup.py.skel (renamed from commands/powermanagement/setup.py)2
-rw-r--r--commands/service/Makefile20
-rw-r--r--commands/service/doc/conf.py.skel (renamed from commands/service/doc/conf.py)4
-rw-r--r--commands/service/setup.py.skel (renamed from commands/service/setup.py)7
-rw-r--r--commands/software/Makefile20
-rw-r--r--commands/software/doc/conf.py.skel (renamed from commands/software/doc/conf.py)4
-rw-r--r--commands/software/setup.py.skel (renamed from commands/software/setup.py)7
-rw-r--r--commands/storage/Makefile20
-rw-r--r--commands/storage/doc/conf.py.skel (renamed from commands/storage/doc/conf.py)4
-rw-r--r--commands/storage/setup.py.skel (renamed from commands/storage/setup.py)7
-rw-r--r--commands/system/Makefile20
-rw-r--r--commands/system/doc/conf.py.skel (renamed from commands/system/doc/conf.py)4
-rw-r--r--commands/system/lmi/scripts/system/__init__.py45
-rw-r--r--commands/system/setup.py.skel (renamed from commands/system/setup.py)2
-rw-r--r--doc/command-properties.rst46
-rw-r--r--doc/conf.py.skel (renamed from doc/conf.py)4
-rw-r--r--doc/script-development.rst117
-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__.py2
-rw-r--r--lmi/scripts/common/command/base.py70
-rw-r--r--lmi/scripts/common/command/checkresult.py2
-rw-r--r--lmi/scripts/common/command/helper.py44
-rw-r--r--lmi/scripts/common/command/meta.py137
-rw-r--r--lmi/scripts/common/command/select.py195
-rw-r--r--lmi/scripts/common/command/session.py6
-rw-r--r--lmi/scripts/common/command/util.py28
-rw-r--r--lmi/scripts/common/errors.py19
-rw-r--r--lmi/scripts/common/session.py48
-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--setup.py.skel (renamed from setup.py)7
-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
71 files changed, 2553 insertions, 381 deletions
diff --git a/Makefile b/Makefile
index ca812ac..fbfc67e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,25 +1,14 @@
-PYTHONPATH?=$(HOME)/workspace/python_sandbox
-DEVELOPDIR?=$(shell echo $(PYTHONPATH) | cut -d : -f 1)
+include Makefile.inc
-.PHONY: readme sdist develop upload_docs clean all
+COMMANDS ?= $(shell find commands -mindepth 1 -maxdepth 1 -type d)
+# all rules executable on meta-command and commands
+RULES := setup upload upload_docs clean develop
+MASSRULES := $(foreach rule,$(RULES),$(rule)-all)
-all: sdist
+.PHONY: $(MASSRULES)
-sdist:
- python setup.py sdist
-
-develop:
- python setup.py develop --install-dir=$(DEVELOPDIR)
-
-readme: README.txt
-
-%.txt: %.md
- pandoc --from=markdown --to=rst -o $@ $?
-
-upload_docs:
- make -C doc html
- python setup.py upload_docs
-
-clean:
- -rm README.txt
- make -C doc clean
+$(MASSRULES): %-all: %
+ # executes rule for metacommand and for all commands found
+ for cmd in $(COMMANDS); do \
+ make -C $$cmd $*; \
+ done
diff --git a/Makefile.inc b/Makefile.inc
new file mode 100644
index 0000000..d3b604f
--- /dev/null
+++ b/Makefile.inc
@@ -0,0 +1,53 @@
+SELFDIR := $(dir $(lastword $(MAKEFILE_LIST)))
+PYTHONPATH ?= $(HOME)/workspace/python_sandbox
+DEVELOPDIR ?= $(shell echo $(PYTHONPATH) | cut -d : -f 1)
+VERSION ?= $(shell cat $(SELFDIR)/VERSION)
+# these can be used only after the setup.py is created
+EGGNAME = $(shell python setup.py --name)
+SRCDIST = $(EGGNAME)-$(VERSION).tar.gz
+
+.PHONY: readme sdist develop upload_docs clean all doc setup
+
+all: sdist
+
+setup: setup.py doc/conf.py
+
+%.py: %.py.skel $(SELFDIR)/VERSION
+ sed 's/@@VERSION@@/$(VERSION)/g' <$*.py.skel >$@
+
+sdist: setup .$(VERSION).sdist
+
+.$(VERSION).sdist:
+ python setup.py sdist
+ touch $@
+
+develop: setup
+ python setup.py develop --install-dir=$(DEVELOPDIR)
+
+readme: README.txt
+
+%.txt: %.md
+ pandoc --from=markdown --to=rst -o $@ $?
+
+doc: setup
+ make -C doc
+
+upload: setup .$(VERSION).upload
+
+.$(VERSION).upload:
+ python setup.py upload
+ touch $@
+
+upload_docs: setup .$(VERSION).upload_docs
+
+.$(VERSION).upload_docs:
+ make -C doc html
+ python setup.py upload_docs
+ touch $@
+
+clean:
+ -rm README.txt setup.py .$(VERSION).*
+ -rm -rf dist/
+ make -C doc clean
+
+# ex: ft=make noet sw=8 ts=8
diff --git a/README.md b/README.md
index 8179987..fc5b7e6 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ Each subdirectory of `commands/` contains library for interfacing with
particular set of OpenLMI providers. Each contains its own `setup.py` file,
that handles its installation and registration of command. They have one
command thing. Each such `setup.py` must pass `entry_points` dictionary to
-the `setup()` function, wich associates commands defined in command library
+the `setup()` function, which associates commands defined in command library
with its name under `lmi` meta-command.
Dependencies
@@ -48,19 +48,12 @@ There are following python dependencies:
* openlmi-tools ([PyPI][])
* python-docopt
-### Uploading to PyPI
-Since *PyPI* expects README file to be in a *reStructuredText* markup
-language and the one present is written in *markdown*, it needs to be
-converted to it. So please make sure you have `pandoc` installed before
-running:
-
- $ python setup.py sdist upload
-
Installation
------------
Use standard `setuptools` script for installation:
$ cd openlmi-scripts
+ $ make setup
$ python setup.py install --user
This installs just the *lmi meta-command* and client-side library. To install
@@ -87,7 +80,7 @@ To issue single command on a host, run:
$ lmi --host ${hostname} service list
-To start the app in interactive mode:
+To start it in interactive mode:
$ lmi --host ${hostname}
> service list --disabled
@@ -101,7 +94,7 @@ Developing lmi scripts.
This documents how to quickly develop lmi scripts without the need to
reinstall python eggs, when anything is changed. This presumes, that the
-development process takes place in a git repotory checked out from [git][].
+development process takes place in a git repository checked out from [git][].
It can be located anywhere on system.
Before we start with setting up an environment, please double check, that you
@@ -144,15 +137,58 @@ Let's setup an environment:
openlmi-scripts repository.
5. Install them and any commands you want -- possibly your own
- $ python setup.py develop --install-dir=$WSP
- $ for cmd in service storage; do
- > pushd commands/$cmd
- > python setup.py develop --install-dir=$WSP
- > popd
- > done
+ $ DEVELOPDIR=$WSP make develop-all
+
+Now any change made to openlmi-scripts is immediately reflected in LMI
+Meta-command.
+
+### Uploading to PyPI
+Since *PyPI* expects README file to be in a *reStructuredText* markup
+language and the one present is written in *markdown*, it needs to be
+converted to it. So please make sure you have `pandoc` installed before
+running:
+
+ $ make upload
+
+### Versioning
+All the scripts share the same version. Version string resides in `VERSION`
+file in root directory. When changed, all `setup.py` scripts need to be
+regenerated. This is done with:
+
+ $ make setup-all
+
+### Makefile rules
+There are various rules provided to ease the development. Most of them may
+be applied to all commands/libraries at once. They are:
+
+ * `clean` - remove temporary and generated files
+ * `develop` - install library in developing mode
+ * `doc` - build documentation
+ * `readme` - create `README.txt` file out of `README.md`
+ * `sdist` - creates source tarball in `dist` directory
+ * `setup` - writes a `setup.py` and `doc/conf.py` files from their skeletons
+ * `upload` - upload to *PyPI*
+ * `upload_docs` - upload documentation to [pythonhosted]
+
+Each script's `Makefile` has the same interface. The root `Makefile` is an
+exception. It takes care of LMI Meta-command and its library. It defines all
+the rules above but also contains few more. They are all variations of above
+commands, have the same name but end with `-all` suffix. They operate on LMI
+Meta-command and all subcommands at once. Such rules are:
+
+ * `clean-all`
+ * `develop-all`
+ * `setup-all`
+ * `upload-all`
+ * `upload\_docs-all`
+
+To limit the set of commands they shall operate on, the `COMMANDS` environment
+variable may be used. For example following command:
+
+ $ COMMANDS='storage software networking' make clean-all
-Now any change made to openlmi-scripts is immediately reflected in `lmi`
-meta-command.
+Will clean storage, software and networking directories and LMI Meta-command as
+well.
------------------------------------------------------------------------------
[git]: https://github.com/openlmi/openlmi-scripts "openlmi-scripts"
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..b003284
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.2.7
diff --git a/commands/README.md b/commands/README.md
index 4ff3359..7221422 100644
--- a/commands/README.md
+++ b/commands/README.md
@@ -1,13 +1,12 @@
Subcommands
===========
-
Each provider has a set of subcommands represented by entry points in
`setup.py` script. They are installed as python eggs. Each such command is
located in its own subdirectory under `commands/`.
Structure
---------
-This directory contains all subcommands of lmi metacommand.
+This directory contains all subcommands of LMI Meta-command.
It has the following structrure:
commands
@@ -22,27 +21,28 @@ It has the following structrure:
│   ├── doc # usage and developer documentation
│   │   ├── cmdline.generated
│   │   ├── cmdline.rst # source in reStructured text [rst]
- │   │   ├── conf.py # documentation configuration
+ │   │   ├── conf.py.skel # documentation configuration
│   │   ├── index.rst # top-level documentation source
│   │   ├── Makefile
│   │   └── python.rst # library reference source
│   ├── README.md
- │   └── setup.py
+ │   ├── Makefile
+ │   └── setup.py.skel
│ ... ...
-Single library under `commands/<provider_prefix>/lmi/scripts` may implement
-one or more commands for `lmi` meta-command. To let the meta-command know of
-them, they must be listed in `entry_points` in setup file (see below).
+Single library under `commands/<provider_prefix>/lmi/scripts` may implement one
+or more commands for LMI Meta-command. To let it know of them, they must be
+listed in `entry_points` in setup file (see below).
-There is no limit for complexity of particular command library, it should
-provide easy to use interface for any third party python application for
-system management of remote hosts. Part of the interface is exported via
-commands to `lmi` meta-command. These, by a convention, are defined in
+There is no limit for complexity of particular script, it should provide an
+easy to use interface for any third party python application for system
+management of remote hosts. Part of the interface is exported via commands to
+LMI Meta-command. These, by a convention, are defined in
`lmi.scripts.<provider_prefix>.cmd` module.
### Documentation directory
Please stick to the structure presented above. It makes it easy to include
-library documents into the global documentation.
+library documents in a global documentation.
* `cmdline.generated` is a file in [*reStructuredText*][see rst] format generated
with `tools/help2rst` script from command's usage string.
@@ -51,6 +51,8 @@ library documents into the global documentation.
* `python.rst` just lists the modules from `lmi.scripts.<provider_prefix>` to
document for python reference.
* `index.rst` binds all the other files together.
+ * `conf.py.skel` contains a configuration template with macros replaced
+ with correct values when the `Makefile` is being processed.
### Generating it
You may use `make_new.py` script to generate whole directory structure with
@@ -61,13 +63,13 @@ regard to provided data. See its help message:
Setup file
----------
Allows to install command python egg and register their exported commands to
-particular namespace for `lmi` meta-command. The minimal script can look like
+particular namespace for LMI Meta-command. The minimal script can look like
this:
from setuptools import setup
setup(
name="<PROJECT>",
- version="<VERSION>",
+ version="@@VERSION@@",
install_requires=['openlmi-scripts'],
namespace_packages=['lmi', 'lmi.scripts'],
packages=['lmi', 'lmi.scripts', 'lmi.scripts.<provider_prefix>'],
@@ -78,6 +80,10 @@ this:
},
)
+This text needs to be store in `setup.py.skel` file. Regular `setup.py` will be
+created out of it when `make setup` is run. `@@VERSION@@` string will be
+replaced during this operation with correct version common to all the scripts.
+
It's advisable to fill more information about your command package like
*description* and *author*. Please refer to python documentation for
setuptools and see already created scripts under `commands/` directory for
@@ -92,93 +98,7 @@ description of *Command module* below.
[a-z]+(-[a-z]+)*(\.py)?
-Command module
---------------
-Is a python module named (by a convention) `.cmd` under your command library.
-It has a documentation string passeable to the `docopt` command line parser
-(see `http://docopt.org/`). The structure of this file is following:
-
- """
- Usage:
- %(cmd)s cmd1 [options]
- %(cmd)s cmd2 [options]
-
- """
- from lmi.scripts.common import LmiLister, register_subcommands
-
- class Cmd1(LmiLister):
- CALLABLE = "lmi.scripts.<provider_prefix>:cmd1_func"
-
- class Cmd2(LmiLister):
- CALLABLE = "lmi.scripts.<provider_prefix>:cmd2_func"
-
- Entry = register_subcommands(
- 'Entry', __doc__,
- { 'cmd1' : Cmd1,
- , 'cmd2' : Cmd2
- },
- requires=["<requirement_string>", ...])
-
-### Usage string
-Is very important here. The command line is parsed by
-`docopt` according to it. It must conform to POSIX standard for program help.
-Please refer to http://docopt.org/ for more information and examples.
-
-One notable detail here is the `%(cmd)s` string for command name. This
-is replaced with `lmi CMD_NAME` when running from command line or just
-`CMD_NAME` while in interactive mode. It's important to prefix your usage
-strings exactly this way. This also means that every `%` character must
-be doubled in usage string to avoid collisions in formatting.
-
-### Command classes
-Are defined in `lmi.scripts.common.commands` module. They affect the way how
-output is rendered to user. Please refer to this module for more information.
-
-### Top level command
-Function `register_subcommands` creates top-level command (refered to as
-`<CMD>` in section *Setup script* above), which passes control to one
-particular subcommand. To let the top-level command know, which command class
-instruments which command a mapping must be passed to this function. The
-global variable, which holds the result of this call must be listed in
-`entry_points` dictionary in `setup.py` script, otherwise it won't be
-available in `lmi` meta-command.
-
-### <requirement\_string>
-**This is not yet supported feature**
-
-Allows to specify, which profiles and their specific versions are neccessary
-to run this command. It's format can be described by following grammar:
-
- <requirement_string> :
- <instance_id> <op> <version>
- | <instance_id> ;
-
- <op> : < | > | = | <= | >= | != ;
-
-Where string enclosed in `<>` is a non-terminal symbol. `:` separates
-non-terminal symbol from the sequence of possible transcriptions
-(right-hand-sides of rules). `|` is a separator of individual transcriptions
-and `;` is symbol denoting the end of rules for single symbol.
-
-`<instance_id>` is a value of `LMI_RegisteredProfile::InstanceID` property.
-
-`<version>` is a version that is compared to given comparison operator to the
-version of installed profile with matching `InstanceID` on target system. It
-must match the following regular expression:
-
- [0-9]+\.[0-9]+\.[0-9]+
-
-First sequence of digits will be compared with `MajorVersion`, second to
-`MinorVersion` and third to the `RevisionNumber` of particular
-`LMI_RegisteredProfile` instance.
-
-`class_requirement_string` has equvalent grammar:
-
- <class_requirement_string> :
- <class_name> <op> <version>
- | <class_name> ;
-
-where `<class_name>` denotes the name of CIM class. And `<version>` the value
-of its `Version` qualifier in the mof file.
+For more information please refer to documentation at [script-development][].
+[script-development]: http://pythonhosted.org/openlmi-scripts/script-development.html# "Script Development"
[rst]: http://sphinx-doc.org/rest.html "reStructuredText"
diff --git a/commands/account/Makefile b/commands/account/Makefile
index 8c02686..ee4552b 100644
--- a/commands/account/Makefile
+++ b/commands/account/Makefile
@@ -1,19 +1 @@
-PYTHONPATH?=$(HOME)/workspace/python_sandbox
-DEVELOPDIR?=$(shell echo $(PYTHONPATH) | cut -d : -f 1)
-
-.PHONY: sdist develop upload_docs clean all
-
-all: sdist
-
-sdist:
- python setup.py sdist
-
-develop:
- python setup.py develop --install-dir=$(DEVELOPDIR)
-
-upload_docs:
- make -C doc html
- python setup.py upload_docs
-
-clean:
- make -C doc clean
+include ../../Makefile.inc
diff --git a/commands/account/doc/conf.py b/commands/account/doc/conf.py.skel
index 1f6e78f..9aee71a 100644
--- a/commands/account/doc/conf.py
+++ b/commands/account/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2014, Red Hat, Inc.'
# built documents.
#
# The short X.Y version.
-version = '0.0.1'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.0.1'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/account/setup.cfg b/commands/account/setup.cfg
new file mode 100644
index 0000000..2ddce25
--- /dev/null
+++ b/commands/account/setup.cfg
@@ -0,0 +1,2 @@
+[upload_docs]
+upload-dir = doc/_build/html
diff --git a/commands/account/setup.py b/commands/account/setup.py.skel
index 8d81d71..86df908 100644
--- a/commands/account/setup.py
+++ b/commands/account/setup.py.skel
@@ -1,8 +1,5 @@
#!/usr/bin/env python
-PROJECT = 'openlmi-scripts-account'
-VERSION = '0.0.1'
-
from setuptools import setup, find_packages
try:
@@ -11,8 +8,8 @@ except IOError:
long_description = ''
setup(
- name=PROJECT,
- version=VERSION,
+ name='openlmi-scripts-account',
+ version='@@VERSION@@',
description='LMI command for system account administration.',
long_description=long_description,
author='Roman Rakus',
diff --git a/commands/hardware/Makefile b/commands/hardware/Makefile
new file mode 100644
index 0000000..ee4552b
--- /dev/null
+++ b/commands/hardware/Makefile
@@ -0,0 +1 @@
+include ../../Makefile.inc
diff --git a/commands/hardware/doc/conf.py b/commands/hardware/doc/conf.py.skel
index 79fe291..08b460a 100644
--- a/commands/hardware/doc/conf.py
+++ b/commands/hardware/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2013-2014, Red Hat, Inc.'
# built documents.
#
# The short X.Y version.
-version = '0.0.1'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.0.1'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/hardware/setup.cfg b/commands/hardware/setup.cfg
new file mode 100644
index 0000000..2ddce25
--- /dev/null
+++ b/commands/hardware/setup.cfg
@@ -0,0 +1,2 @@
+[upload_docs]
+upload-dir = doc/_build/html
diff --git a/commands/hardware/setup.py b/commands/hardware/setup.py.skel
index 441612c..7f85956 100644
--- a/commands/hardware/setup.py
+++ b/commands/hardware/setup.py.skel
@@ -10,7 +10,7 @@ except IOError:
setup(
name='openlmi-scripts-hardware',
- version='0.0.3',
+ version='@@VERSION@@',
description='Hardware information available in OpenLMI hardware providers',
long_description=long_description,
author=u'Peter Schiffer',
diff --git a/commands/logicalfile/Makefile b/commands/logicalfile/Makefile
index 7d0a592..ee4552b 100644
--- a/commands/logicalfile/Makefile
+++ b/commands/logicalfile/Makefile
@@ -1,19 +1 @@
-PYTHONPATH?=$(HOME)/workspace/python_sandbox
-DEVELOPDIR?=$(shell echo $(PYTHONPATH) | cut -d : -f 1)
-
-.PHONY: sdist develop upload_docs clean
-
-all: sdist
-
-sdist:
- python setup.py sdist
-
-develop:
- python setup.py develop --install-dir=$(DEVELOPDIR)
-
-upload_docs:
- make -C doc html
- python setup.py upload_docs
-
-clean:
- make -C doc clean
+include ../../Makefile.inc
diff --git a/commands/logicalfile/doc/conf.py b/commands/logicalfile/doc/conf.py.skel
index 841f14b..eba7932 100644
--- a/commands/logicalfile/doc/conf.py
+++ b/commands/logicalfile/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2013-2014, Red Hat, Inc.'
# built documents.
#
# The short X.Y version.
-version = '0.0.3'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.0.3'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/logicalfile/setup.py b/commands/logicalfile/setup.py.skel
index f3f32c8..466c6af 100644
--- a/commands/logicalfile/setup.py
+++ b/commands/logicalfile/setup.py.skel
@@ -1,8 +1,5 @@
#!/usr/bin/env python
-PROJECT = 'openlmi-scripts-logicalfile'
-VERSION = '0.0.3'
-
from setuptools import setup, find_packages
try:
@@ -11,8 +8,8 @@ except IOError:
long_description = ''
setup(
- name=PROJECT,
- version=VERSION,
+ name='openlmi-scripts-logicalfile',
+ version='@@VERSION@@',
description='LMI command for system logical file administration.',
long_description=long_description,
author='Jan Synacek',
diff --git a/commands/make_new.py b/commands/make_new.py
index 96821fc..a2a8111 100755
--- a/commands/make_new.py
+++ b/commands/make_new.py
@@ -7,8 +7,6 @@ Usage:
{cmd} [options] <command>
Options:
- -v --version <version> Version of script library.
- Defaults to {default_version}.
-a --author <author> Full name of library's author.
-e --email <email> Author's email address.
-d --description <description>
@@ -27,8 +25,6 @@ from sphinx import quickstart
RE_COMMAND_NAME = re.compile(r'^([a-z]+(_[a-z]+)*)$')
RE_RST_STATEMENT = re.compile(r'^\s*(:[^:]+:.*)')
-DEFAULT_VERSION = '0.0.1'
-
SETUP_TEMPLATE = \
u"""#!/usr/bin/env python
# -*- encoding: utf-8 -*-
@@ -42,7 +38,7 @@ except IOError:
setup(
name='openlmi-scripts-{name}',
- version={version!r},
+ version='@@VERSION@@',
{_description}description={description!r},
long_description=long_description,
{_author}author=u'{author}',
@@ -78,6 +74,11 @@ setup(
)
"""
+SETUP_CFG_TEMPLATE = """\
+[upload_docs]
+upload-dir = doc/_build/html
+"""
+
BSD_LICENSE_HEADER = \
"""# Copyright (C) 2013-2014 Red Hat, Inc. All rights reserved.
#
@@ -185,6 +186,10 @@ def write_setup(config, output_path):
with open(output_path, 'w') as setup_file:
setup_file.write(SETUP_TEMPLATE.format(**values).encode('utf-8'))
+def write_setup_cfg(config, output_path):
+ with open(output_path, 'w') as setup_file:
+ setup_file.write(SETUP_CFG_TEMPLATE)
+
def write_empty(config, output_path):
with open(output_path, 'w'):
pass
@@ -193,6 +198,10 @@ def write_cmdline(config, output_path):
with open(output_path, 'w') as cmdline_file:
cmdline_file.write(DOC_CMDLINE)
+def write_makefile(config, output_path):
+ with open(output_path, 'w') as cmdline_file:
+ cmdline_file.write('include ../../Makefile.inc')
+
def modify_doc_makefile(config, path):
s_add_cmd, s_add_phony, s_add_cmdregen, s_wait_help_end, s_done = range(5)
state = s_add_cmd
@@ -230,7 +239,8 @@ def modify_doc_index(config, path):
if state == s_wait_toc and line.startswith('.. toctree::'):
state = s_wait_empty_line
new.write(line)
- elif state == s_wait_empty_line and RE_RST_STATEMENT.match(line):
+ elif ( state == s_wait_empty_line
+ and RE_RST_STATEMENT.match(line)):
new.write(' ' +
RE_RST_STATEMENT.match(line).group(1) + '\n')
elif state == s_wait_empty_line and line == '\n':
@@ -248,8 +258,8 @@ def make_doc_directory(config, path):
'dot' : '_',
'project' : config['project_name'],
'author' : config['author'],
- 'version' : config['version'],
- 'release' : config['version'],
+ 'version' : '@@VERSION@@',
+ 'release' : '@@VERSION@@',
'suffix' : '.rst',
'master' : 'index',
'epub' : True,
@@ -265,6 +275,7 @@ def make_doc_directory(config, path):
'makefile' : True,
'batchfile' : True}
quickstart.generate(sphinx_conf)
+ os.rename(os.path.join(path, 'conf.py'), os.path.join(path, 'conf.py.skel'))
write_cmdline(config, os.path.join(path, 'cmdline.rst'))
modify_doc_makefile(config, os.path.join(path, 'Makefile'))
modify_doc_index(config, os.path.join(path, 'index.rst'))
@@ -272,7 +283,7 @@ def make_doc_directory(config, path):
STRUCTURE = {
'doc' : make_doc_directory,
# {
- # 'conf.py' : ...,
+ # 'conf.py.skel': ...,
# 'cmdline.rst' : ...,
# 'python.rst' : ...,
# 'index.rst' : ...,
@@ -287,8 +298,10 @@ STRUCTURE = {
}
}
},
- 'setup.py' : write_setup,
- 'README.md' : write_empty
+ 'setup.py.skel' : write_setup,
+ 'setup.cfg' : write_setup_cfg,
+ 'README.md' : write_empty,
+ 'Makefile' : write_makefile,
}
def make_file(config, filepath, prescription):
@@ -309,29 +322,25 @@ def make_command(config, base_dir):
def parse_command_line(args=None):
if args is None:
args = sys.argv[1:]
- usage = __doc__.format(
- cmd=os.path.basename(sys.argv[0]),
- default_version=DEFAULT_VERSION)
+ usage = __doc__.format(cmd=os.path.basename(sys.argv[0]))
options = docopt(usage, args)
- for opt_name in ('version', 'author', 'email', 'description'):
+ for opt_name in ('author', 'email', 'description'):
options[opt_name] = options.pop('--' + opt_name)
options['command'] = options.pop('<command>')
options['project_name'] = options.pop('--project-name')
if not RE_COMMAND_NAME.match(options['command']):
- die("command name must match regular expression {!r}", RE_COMMAND_NAME.pattern)
+ die("command name must match regular expression {!r}",
+ RE_COMMAND_NAME.pattern)
for info, question, default_value in (
- ('version', 'Library version [{default_version}]: ',
- DEFAULT_VERSION),
('author', 'Your name: ', None),
('email', 'Your email: ', None),
('description', 'Description: ', None)):
if options.get(info, None):
continue
try:
- options[info] = ask(
- question.format(default_version=DEFAULT_VERSION))
+ options[info] = ask(question)
except EOFError:
pass
if not options[info]:
diff --git a/commands/networking/Makefile b/commands/networking/Makefile
index 7d0a592..ee4552b 100644
--- a/commands/networking/Makefile
+++ b/commands/networking/Makefile
@@ -1,19 +1 @@
-PYTHONPATH?=$(HOME)/workspace/python_sandbox
-DEVELOPDIR?=$(shell echo $(PYTHONPATH) | cut -d : -f 1)
-
-.PHONY: sdist develop upload_docs clean
-
-all: sdist
-
-sdist:
- python setup.py sdist
-
-develop:
- python setup.py develop --install-dir=$(DEVELOPDIR)
-
-upload_docs:
- make -C doc html
- python setup.py upload_docs
-
-clean:
- make -C doc clean
+include ../../Makefile.inc
diff --git a/commands/powermanagement/doc/conf.py b/commands/networking/doc/conf.py.skel
index 41ffdb4..f1446f9 100644
--- a/commands/powermanagement/doc/conf.py
+++ b/commands/networking/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2013, Radek Novacek'
# built documents.
#
# The short X.Y version.
-version = '0.0.1'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.0.1'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/networking/setup.py b/commands/networking/setup.py.skel
index b18c030..6dabfdd 100644
--- a/commands/networking/setup.py
+++ b/commands/networking/setup.py.skel
@@ -10,7 +10,7 @@ except IOError:
setup(
name='openlmi-scripts-networking',
- version='0.0.2',
+ version='@@VERSION@@',
description='LMI command for network administration.',
long_description=long_description,
author=u'Radek Novacek',
diff --git a/commands/powermanagement/Makefile b/commands/powermanagement/Makefile
new file mode 100644
index 0000000..ee4552b
--- /dev/null
+++ b/commands/powermanagement/Makefile
@@ -0,0 +1 @@
+include ../../Makefile.inc
diff --git a/commands/networking/doc/conf.py b/commands/powermanagement/doc/conf.py.skel
index 5138a89..f1446f9 100644
--- a/commands/networking/doc/conf.py
+++ b/commands/powermanagement/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2013, Radek Novacek'
# built documents.
#
# The short X.Y version.
-version = '0.0.2'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.0.2'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/powermanagement/setup.cfg b/commands/powermanagement/setup.cfg
new file mode 100644
index 0000000..2ddce25
--- /dev/null
+++ b/commands/powermanagement/setup.cfg
@@ -0,0 +1,2 @@
+[upload_docs]
+upload-dir = doc/_build/html
diff --git a/commands/powermanagement/setup.py b/commands/powermanagement/setup.py.skel
index 6613348..5fe420d 100644
--- a/commands/powermanagement/setup.py
+++ b/commands/powermanagement/setup.py.skel
@@ -10,7 +10,7 @@ except IOError:
setup(
name='openlmi-scripts-powermanagement',
- version='0.0.1',
+ version='@@VERSION@@',
description='LMI command for power management.',
long_description=long_description,
author=u'Radek Novacek',
diff --git a/commands/service/Makefile b/commands/service/Makefile
index 7d0a592..ee4552b 100644
--- a/commands/service/Makefile
+++ b/commands/service/Makefile
@@ -1,19 +1 @@
-PYTHONPATH?=$(HOME)/workspace/python_sandbox
-DEVELOPDIR?=$(shell echo $(PYTHONPATH) | cut -d : -f 1)
-
-.PHONY: sdist develop upload_docs clean
-
-all: sdist
-
-sdist:
- python setup.py sdist
-
-develop:
- python setup.py develop --install-dir=$(DEVELOPDIR)
-
-upload_docs:
- make -C doc html
- python setup.py upload_docs
-
-clean:
- make -C doc clean
+include ../../Makefile.inc
diff --git a/commands/service/doc/conf.py b/commands/service/doc/conf.py.skel
index 8244025..29770eb 100644
--- a/commands/service/doc/conf.py
+++ b/commands/service/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2013-2014, Red Hat, Inc.'
# built documents.
#
# The short X.Y version.
-version = '0.1.2'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.1.2'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/service/setup.py b/commands/service/setup.py.skel
index cb994e3..105379e 100644
--- a/commands/service/setup.py
+++ b/commands/service/setup.py.skel
@@ -1,8 +1,5 @@
#!/usr/bin/env python
-PROJECT = 'openlmi-scripts-service'
-VERSION = '0.1.2'
-
from setuptools import setup, find_packages
try:
@@ -11,8 +8,8 @@ except IOError:
long_description = ''
setup(
- name=PROJECT,
- version=VERSION,
+ name='openlmi-scripts-service',
+ version='@@VERSION@@',
description='LMI command for system service administration.',
long_description=long_description,
author='Michal Minar',
diff --git a/commands/software/Makefile b/commands/software/Makefile
index 7d0a592..ee4552b 100644
--- a/commands/software/Makefile
+++ b/commands/software/Makefile
@@ -1,19 +1 @@
-PYTHONPATH?=$(HOME)/workspace/python_sandbox
-DEVELOPDIR?=$(shell echo $(PYTHONPATH) | cut -d : -f 1)
-
-.PHONY: sdist develop upload_docs clean
-
-all: sdist
-
-sdist:
- python setup.py sdist
-
-develop:
- python setup.py develop --install-dir=$(DEVELOPDIR)
-
-upload_docs:
- make -C doc html
- python setup.py upload_docs
-
-clean:
- make -C doc clean
+include ../../Makefile.inc
diff --git a/commands/software/doc/conf.py b/commands/software/doc/conf.py.skel
index a48fb0f..b5d0634 100644
--- a/commands/software/doc/conf.py
+++ b/commands/software/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2013-2014, Red Hat, Inc.'
# built documents.
#
# The short X.Y version.
-version = '0.2.5'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.2.5'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/software/setup.py b/commands/software/setup.py.skel
index 49522cd..834d05b 100644
--- a/commands/software/setup.py
+++ b/commands/software/setup.py.skel
@@ -1,8 +1,5 @@
#!/usr/bin/env python
-PROJECT = 'openlmi-scripts-software'
-VERSION = '0.2.5'
-
from setuptools import setup, find_packages
try:
@@ -11,8 +8,8 @@ except IOError:
long_description = ''
setup(
- name=PROJECT,
- version=VERSION,
+ name='openlmi-scripts-software',
+ version='@@VERSION@@',
description='LMI command for system software administration.',
long_description=long_description,
author='Michal Minar',
diff --git a/commands/storage/Makefile b/commands/storage/Makefile
index 8c02686..ee4552b 100644
--- a/commands/storage/Makefile
+++ b/commands/storage/Makefile
@@ -1,19 +1 @@
-PYTHONPATH?=$(HOME)/workspace/python_sandbox
-DEVELOPDIR?=$(shell echo $(PYTHONPATH) | cut -d : -f 1)
-
-.PHONY: sdist develop upload_docs clean all
-
-all: sdist
-
-sdist:
- python setup.py sdist
-
-develop:
- python setup.py develop --install-dir=$(DEVELOPDIR)
-
-upload_docs:
- make -C doc html
- python setup.py upload_docs
-
-clean:
- make -C doc clean
+include ../../Makefile.inc
diff --git a/commands/storage/doc/conf.py b/commands/storage/doc/conf.py.skel
index f8c6d91..91e0905 100644
--- a/commands/storage/doc/conf.py
+++ b/commands/storage/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2013-2014, Red Hat, Inc.'
# built documents.
#
# The short X.Y version.
-version = '0.0.5'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.0.5'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/storage/setup.py b/commands/storage/setup.py.skel
index a90b751..93ddf3f 100644
--- a/commands/storage/setup.py
+++ b/commands/storage/setup.py.skel
@@ -1,8 +1,5 @@
#!/usr/bin/env python
-PROJECT = 'openlmi-scripts-storage'
-VERSION = '0.0.5'
-
from setuptools import setup, find_packages
try:
@@ -11,8 +8,8 @@ except IOError:
long_description = ''
setup(
- name=PROJECT,
- version=VERSION,
+ name='openlmi-scripts-storage',
+ version='@@VERSION@@',
description='LMI command for system storage administration.',
long_description=long_description,
author='Jan Safranek',
diff --git a/commands/system/Makefile b/commands/system/Makefile
index 7d0a592..ee4552b 100644
--- a/commands/system/Makefile
+++ b/commands/system/Makefile
@@ -1,19 +1 @@
-PYTHONPATH?=$(HOME)/workspace/python_sandbox
-DEVELOPDIR?=$(shell echo $(PYTHONPATH) | cut -d : -f 1)
-
-.PHONY: sdist develop upload_docs clean
-
-all: sdist
-
-sdist:
- python setup.py sdist
-
-develop:
- python setup.py develop --install-dir=$(DEVELOPDIR)
-
-upload_docs:
- make -C doc html
- python setup.py upload_docs
-
-clean:
- make -C doc clean
+include ../../Makefile.inc
diff --git a/commands/system/doc/conf.py b/commands/system/doc/conf.py.skel
index 4d1fbe7..29c1048 100644
--- a/commands/system/doc/conf.py
+++ b/commands/system/doc/conf.py.skel
@@ -48,9 +48,9 @@ copyright = u'2014, Peter Schiffer'
# built documents.
#
# The short X.Y version.
-version = '0.0.2'
+version = "@@VERSION@@"
# The full version, including alpha/beta/rc tags.
-release = '0.0.2'
+release = "@@VERSION@@"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/commands/system/lmi/scripts/system/__init__.py b/commands/system/lmi/scripts/system/__init__.py
index 9588ec3..8617138 100644
--- a/commands/system/lmi/scripts/system/__init__.py
+++ b/commands/system/lmi/scripts/system/__init__.py
@@ -110,6 +110,7 @@ def get_system_info(ns):
result += get_hwinfo(ns)
result += get_osinfo(ns)
result += get_servicesinfo(ns)
+ result += get_networkinfo(ns)
return result
def get_hostname(ns):
@@ -231,3 +232,47 @@ def get_servicesinfo(ns):
('Firewall:', fw),
('Logging:', logging)]
return result
+
+def get_networkinfo(ns):
+ """
+ :returns: Tabular data of networking status.
+ :rtype: List of tuples
+ """
+ result = [('', ''), ('Networking', '')]
+ try:
+ lan_endpoints = get_all_instances(ns, 'LMI_LANEndpoint')
+ except LMIClassNotFound:
+ result.append((' N/A', ''))
+ return result
+ nic = 1
+ for lan_endpoint in lan_endpoints:
+ if lan_endpoint.Name == 'lo':
+ continue
+ result += [
+ (' NIC %d' % nic, ''),
+ (' Name:', lan_endpoint.Name)]
+ try:
+ ip_net_con = lan_endpoint.associators(
+ ResultClass='LMI_IPNetworkConnection')[0]
+ result.append((' Status:',
+ ns.LMI_IPNetworkConnection.OperatingStatusValues.value_name(
+ ip_net_con.OperatingStatus)))
+ except LMIClassNotFound:
+ pass
+ try:
+ for ip_protocol_endpoint in lan_endpoint.associators(
+ ResultClass='LMI_IPProtocolEndpoint'):
+ if ip_protocol_endpoint.ProtocolIFType == \
+ ns.LMI_IPProtocolEndpoint.ProtocolIFTypeValues.IPv4:
+ result.append((' IPv4 Address:',
+ ip_protocol_endpoint.IPv4Address))
+ elif ip_protocol_endpoint.ProtocolIFType == \
+ ns.LMI_IPProtocolEndpoint.ProtocolIFTypeValues.IPv6:
+ result.append((' IPv6 Address:',
+ ip_protocol_endpoint.IPv6Address))
+ except LMIClassNotFound:
+ pass
+ result += [
+ (' MAC Address:', lan_endpoint.MACAddress)]
+ nic += 1
+ return result
diff --git a/commands/system/setup.py b/commands/system/setup.py.skel
index 57b07a7..71c3bfe 100644
--- a/commands/system/setup.py
+++ b/commands/system/setup.py.skel
@@ -10,7 +10,7 @@ except IOError:
setup(
name='openlmi-scripts-system',
- version='0.0.2',
+ version='@@VERSION@@',
description='Display general system information',
long_description=long_description,
author=u'Peter Schiffer',
diff --git a/doc/command-properties.rst b/doc/command-properties.rst
index cfba558..d45f8d9 100644
--- a/doc/command-properties.rst
+++ b/doc/command-properties.rst
@@ -288,6 +288,8 @@ They are:
``FMT_HUMAN_FRIENDLY`` : ``bool`` (defaults to ``False``)
Forces the output to be more pleasant to read by human beings.
+.. _specifying_requirements:
+
Command specific properties
---------------------------
Each command class can have its own specific properties. Let's take a look on
@@ -350,6 +352,50 @@ them.
to behave exactly as ``All`` subcommand in case that no command
is given on command line.
+.. _lmi_select_command_properties:
+
+``LmiSelectCommand`` properties
+-------------------------------
+Following properties allow to define profile and class requirements for
+commands.
+
+.. _select:
+
+``SELECT`` : ``list`` (mandatory)
+ 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. If no condition can be satisfied and no default command is set,
+ an exception will be raised.
+
+ .. seealso::
+ Definition of *LMIReSpL* mini-language:
+ :py:mod:`~lmi.scripts.common.versioncheck.parser`
+
+.. _default:
+
+``DEFAULT`` : ``string`` or reference to command class
+ Defines fallback command used in case no condition in ``SELECT`` can be
+ satisfied.
+
.. _lmi_lister_properties:
``LmiLister`` properties
diff --git a/doc/conf.py b/doc/conf.py.skel
index f5a0693..af4caa9 100644
--- a/doc/conf.py
+++ b/doc/conf.py.skel
@@ -58,9 +58,9 @@ copyright = u'2013-2014, Red Hat, Inc.'
# built documents.
#
# The short X.Y version.
-version = '0.2.7'
+version = '@@VERSION@@'
# The full version, including alpha/beta/rc tags.
-release = '0.2.7'
+release = '@@VERSION@@'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/doc/script-development.rst b/doc/script-development.rst
index 5871aa1..040895f 100644
--- a/doc/script-development.rst
+++ b/doc/script-development.rst
@@ -306,6 +306,116 @@ of options to it. In this way we can create arbitrarily tall command trees.
Top-level command is nothing else than a subclass of ``LmiCommandMultiplexer``.
+Specifying profile and class requirements
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Most commands require some provider installed on managed machine to work
+properly. Each such provider should be represented by an instance of
+``CIM_RegisteredProfile`` on remote broker. This instance looks like
+this (in MOF syntax): ::
+
+ instance of CIM_RegisteredProfile {
+ InstanceID = "OpenLMI+OpenLMI-Software+0.4.2";
+ RegisteredOrganization = 1;
+ OtherRegisteredOrganization = "OpenLMI";
+ RegisteredVersion = "0.4.2";
+ AdvertiseTypes = [2];
+ RegisteredName = "OpenLMI-Software";
+ };
+
+We are interested just in ``RegisteredName`` and ``RegisteredVersion``
+properties that we'll use for requirement specification.
+
+Requirement is written in *LMIReSpL* language. For its formal definition
+refer to documentation of :py:mod:`~lmi.scripts.common.versioncheck.parser`.
+Since the language is quite simple, few examples should suffice:
+
+ ``'OpenLMI-Software < 0.4.2'``
+ Requires OpenLMI Software provider to be installed in version lower
+ than ``0.4.2``.
+ ``'OpenLMI-Hardware == 0.4.2 & Openlmi-Software >= 0.4.2'``
+ Requires both hardware and software providers to be installed in
+ particular version. Short-circuit evaluation is utilized here. It
+ means that in this example OpenLMI Software won't be queried unless
+ OpenLMI Hardware is installed and having desired version.
+ ``'profile "OpenLMI-Logical File" > 0.4.2'``
+ If you have spaces in the name of profile, surround it in double
+ quotes. ``profile`` keyword is optional. It could be also present in
+ previous examples.
+
+Version requirements are not limited to profiles only. CIM classes may be
+specified as well:
+
+ ``'class LMI_SoftwareIdentity >= 0.3.0 & OpenLMI-LogicalFile'``
+ In case of class requirements the ``class`` keyword is mandatory. As
+ you can see, version requirement is optional.
+ ``'! (class LMI_SoftwareIdentity | class LMI_UnixFile)'``
+ Complex expressions can be created with the use of brackets and other
+ operators.
+
+One requirement is evaluated in these steps:
+
+ Profile requirement
+ 1. Query ``CIM_RegisteredProfile`` for instances with
+ ``RegisteredName`` matching given name. If found, go to 2. Otherwise
+ query ``CIM_RegisteredSubProfile`` [#subprof]_ for instances with
+ ``RegisteredName`` matching given name. If not found return
+ ``False``.
+ 2. Select the (sub)profile with highest version and go to 3.
+ 3. If the requirement has version specification then compare it to the
+ value of ``RegisteredVersion`` using given operator. If the relation
+ does not apply, return ``False``.
+ 4. Return ``True``.
+
+ Class requirement
+ 1. Get specified class. If not found, return ``False``.
+ 2. If the requirement has version specification then compare it to the
+ value of ``Version`` [#missing_version]_ qualifier of
+ obtained class using given operator. And if the relation
+ does not apply, return ``False``.
+ 3. Return ``True``.
+
+Now let's take a look, where these requirements can be specified.
+There is special select command used to specify which command to load
+for particular version on remote broker. It's defined like this: ::
+
+ from lmi.scripts.common.command import LmiSelectCommand
+
+ class SoftwareCMD(LmiSelectCommand):
+
+ SELECT = [
+ ( 'OpenLMI-Software >= 0.4.2' & 'OpenLMI-LogicalFile'
+ , 'lmi.scripts.software.current.SwLFCmd')
+ , ( 'OpenLMI-Software >= 0.4.2'
+ , 'lmi.scripts.software.current.SwCmd')
+ , ('OpenLMI-Software', 'lmi.scripts.software.pre042.SwCmd')
+ ]
+
+It says to load ``SwLFCmd`` command in case both OpenLMI Software and
+OpenLMI LogicalFile providers are installed. If not, load the ``SwCMD`` from
+``current`` module for OpenLMI Software with recent version and fallback to
+``SwCmd`` for anything else. If the OpenLMI Software provider is not available
+at all, no command will be loaded and exception will be raised.
+
+Previous command could be used as an entry point in your ``setup.py`` script
+(see the :ref:`entry_points`). There is also a utility that makes it look
+better: ::
+
+ from lmi.scripts.common.command import select_command
+
+ SoftwareCMD = select_command('SoftwareCMD',
+ ( 'OpenLMI-Software >= 0.4.2' & 'OpenLMI-LogicalFile'
+ , 'lmi.scripts.software.current.SwLFCmd'),
+ ( 'OpenLMI-Software >= 0.4.2', 'lmi.scripts.software.current.SwCmd'),
+ ('OpenLMI-Software', 'lmi.scripts.software.pre042.SwCmd')
+ )
+
+.. seealso::
+ Documentation of
+ :py:class:`~lmi.scripts.common.command.select.LmiSelectCommand` and
+ :py:class:`~lmi.scripts.common.command.helper.select_command`.
+
+ And also notes on related :ref:`lmi_select_command_properties`.
+
Command wrappers module
~~~~~~~~~~~~~~~~~~~~~~~
Usually consists of:
@@ -413,6 +523,8 @@ Follows a minimal example of ``setup.py`` script for service library. ::
.. _entry_points:
+Entry points
+~~~~~~~~~~~~
The most notable argument here is ``entry_points`` which is a dictionary
containing python namespaces where plugins are registered. In this case, we
register single top-level command (see `Top-level commands`_) called
@@ -541,6 +653,11 @@ Tutorial
.. [#] Precisely in an ``__init__.py`` module of this package.
.. [#] These names must exactly match the names in usage strings.
+.. [#subprof] This is a subclass of ``CIM_RegisteredProfile`` thus it has the
+ same properties.
+.. [#missing_version] If the Version qualifier is missing, -1 will be used
+ for comparison instead of empty string.
+
.. ****************************************************************************
.. _CIM: http://dmtf.org/standards/cim
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..a7c2df4 100644
--- a/lmi/scripts/common/command/__init__.py
+++ b/lmi/scripts/common/command/__init__.py
@@ -42,7 +42,9 @@ 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
from lmi.scripts.common.command.helper import register_subcommands
+from lmi.scripts.common.command.helper import select_command
diff --git a/lmi/scripts/common/command/base.py b/lmi/scripts/common/command/base.py
index 2998f63..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
@@ -173,6 +204,20 @@ class LmiBaseCommand(object):
options = self.parent.format_options
return options
+ @property
+ def session(self):
+ """
+ :returns: Session object. Session for command and all of its children
+ may be overriden with a call to :py:meth:`set_session_proxy`.
+ :rtype: :py:class:`lmi.scripts.common.session.Session`
+ """
+ proxy = getattr(self, '_session_proxy', None)
+ if proxy:
+ return proxy
+ if self.parent is not None:
+ return self.parent.session
+ return self.app.session
+
def get_cmd_name_parts(self, all_parts=False, demand_own_usage=True,
for_docopt=False):
"""
@@ -281,3 +326,16 @@ class LmiBaseCommand(object):
:rtype: integer
"""
raise NotImplementedError("run method must be overriden in subclass")
+
+ def set_session_proxy(self, session):
+ """
+ Allows to override session object. This is useful for especially for
+ conditional commands (subclasses of
+ :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.
+
+ :param session: Session object.
+ """
+ self._session_proxy = session
diff --git a/lmi/scripts/common/command/checkresult.py b/lmi/scripts/common/command/checkresult.py
index 08b3b63..27e9549 100644
--- a/lmi/scripts/common/command/checkresult.py
+++ b/lmi/scripts/common/command/checkresult.py
@@ -134,7 +134,7 @@ class LmiCheckResult(LmiSessionCommand):
pass
def process_session_results(self, session, results):
- if len(self.app.session) > 1:
+ if len(self.session) > 1:
LOG().debug('Successful runs: %d\n',
len([r for r in results.values() if r[0]]))
LmiSessionCommand.process_session_results(self, session, results)
diff --git a/lmi/scripts/common/command/helper.py b/lmi/scripts/common/command/helper.py
index 319442b..578fa8b 100644
--- a/lmi/scripts/common/command/helper.py
+++ b/lmi/scripts/common/command/helper.py
@@ -33,6 +33,8 @@ Module with convenient function for defining user commands.
from lmi.scripts.common.command import LmiLister
from lmi.scripts.common.command import LmiCommandMultiplexer
+from lmi.scripts.common.command import LmiSelectCommand
+from lmi.scripts.common.command import util
def make_list_command(func,
name=None,
@@ -62,7 +64,9 @@ def make_list_command(func,
name = func.__name__
if not name.startswith('_'):
name = '_' + name.capitalize()
- props = { 'COLUMNS' : columns, 'CALLABLE' : func }
+ props = { 'COLUMNS' : columns
+ , 'CALLABLE' : func
+ , '__module__' : util.get_module_name() }
if verify_func:
props['verify_options'] = verify_func
if transform_func:
@@ -89,7 +93,45 @@ def register_subcommands(command_name, usage, command_map,
props = { 'COMMANDS' : command_map
, 'OWN_USAGE' : True
, '__doc__' : usage
+ , '__module__' : util.get_module_name()
, 'FALLBACK_COMMAND' : fallback_command }
return LmiCommandMultiplexer.__metaclass__(command_name,
(LmiCommandMultiplexer, ), props)
+def select_command(command_name, *args, **kwargs):
+ """
+ Create command selector that loads command whose requirements are met.
+
+ Example of invocation: ::
+
+ Hardware = select_command('Hardware',
+ ("Openlmi-Hardware >= 0.4.2", "lmi.scripts.hardware.current.Cmd"),
+ ("Openlmi-Hardware < 0.4.2" , "lmi.scripts.hardware.pre042.Cmd"),
+ default=HwMissing
+ )
+
+ Above example checks remote broker for OpenLMI-Hardware provider. If it is
+ installed and its version is equal or higher than 0.4.2, command from
+ ``current`` module will be used. For older registered versions command
+ contained in ``pre042`` module will be loaded. If hardware provider is not
+ available, HwMissing command will be loaded instead.
+
+ .. seealso::
+ Check out the grammer describing language used in these conditions at
+ :py:mod:`lmi.scripts.common.versioncheck.parser`.
+
+ :param args: List of pairs ``(condition, command)`` that are inspected in
+ given order until single condition is satisfied. Associated command is
+ then loaded. Command is either a reference to command class or path to
+ it given as string. In latter case last dot divides module's import
+ path and command name.
+ :param default: This command will be loaded when no condition from *args*
+ is satisfied.
+ """
+ props = { 'SELECT' : args
+ , 'DEFAULT' : kwargs.get('default', None)
+ , '__module__' : util.get_module_name()
+ }
+ return LmiSelectCommand.__metaclass__(command_name,
+ (LmiSelectCommand, ), props)
+
diff --git a/lmi/scripts/common/command/meta.py b/lmi/scripts/common/command/meta.py
index df1225f..7d8213e 100644
--- a/lmi/scripts/common/command/meta.py
+++ b/lmi/scripts/common/command/meta.py
@@ -54,12 +54,13 @@ 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')
LOG = get_logger(__name__)
-def _handle_usage(name, dcl):
+def _handle_usage(name, bases, dcl):
"""
Take care of ``OWN_USAGE`` property. Supported values:
@@ -96,6 +97,12 @@ def _handle_usage(name, dcl):
return hlp
dcl['get_usage'] = _new_get_usage
has_own_usage = True
+ elif ( dcl.get('__node__', None) is None
+ and any(getattr(b, 'has_own_usage', lambda: False)() for b in bases)):
+ # inherit doc string of base class
+ dcl['__doc__'] = ( b.__doc__ for b in bases
+ if getattr(b, 'has_own_usage', lambda: False)()).next()
+ has_own_usage = True
if has_own_usage:
if not 'has_own_usage' in dcl:
dcl['has_own_usage'] = classmethod(lambda _cls: True)
@@ -436,7 +443,7 @@ def _handle_fallback_command(name, bases, dcl):
usage_string = cmd.__doc__
if not usage_string:
errors.LmiCommandError(dcl['__module__'], name,
- "missing usage string")
+ "Missing usage string.")
fallback.__doc__ = usage_string
fallback.has_own_usage = lambda cls: True
dcl['fallback_command'] = staticmethod(lambda: fallback)
@@ -455,7 +462,7 @@ def _handle_format_options(name, bases, dcl):
if opt_name not in FORMAT_OPTIONS:
raise errors.LmiCommandInvalidProperty(
dcl['__module__'], name,
- 'formatting option "%s" is not supported' %
+ 'Formatting option "%s" is not supported.' %
opt_name)
if ( opt_name in ('no_headings', 'human_friendly')
and not isinstance(value, bool)):
@@ -480,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
@@ -503,7 +589,7 @@ class EndPointCommandMetaClass(abc.ABCMeta):
"""
def __new__(mcs, name, bases, dcl):
- _handle_usage(name, dcl)
+ _handle_usage(name, bases, dcl)
_handle_callable(name, bases, dcl)
_handle_opt_preprocess(name, dcl)
_handle_format_options(name, bases, dcl)
@@ -538,7 +624,7 @@ class SessionCommandMetaClass(EndPointCommandMetaClass):
associated function.
"""
def __new__(mcs, name, bases, dcl):
- _handle_usage(name, dcl)
+ _handle_usage(name, bases, dcl)
_handle_namespace(dcl)
_handle_callable(name, bases, dcl)
@@ -687,15 +773,15 @@ class MultiplexerMetaClass(abc.ABCMeta):
# check COMMANDS property and make it a classmethod
if not 'COMMANDS' in dcl:
raise errors.LmiCommandError(module_name, name,
- 'missing COMMANDS property')
+ 'Missing COMMANDS property.')
cmds = dcl.pop('COMMANDS')
if not isinstance(cmds, dict):
raise errors.LmiCommandInvalidProperty(module_name, name,
'COMMANDS must be a dictionary')
if not all(isinstance(c, basestring) for c in cmds.keys()):
raise errors.LmiCommandInvalidProperty(module_name, name,
- 'keys of COMMANDS dictionary must contain command'
- ' names as strings')
+ 'Keys of COMMANDS dictionary must contain command'
+ ' names as strings.')
for cmd_name, cmd in cmds.items():
if not util.RE_COMMAND_NAME.match(cmd_name):
raise errors.LmiCommandInvalidName(
@@ -705,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__)
@@ -715,8 +801,39 @@ class MultiplexerMetaClass(abc.ABCMeta):
return cmds
dcl['child_commands'] = classmethod(_new_child_commands)
- _handle_usage(name, dcl)
+ _handle_usage(name, bases, dcl)
_handle_fallback_command(name, bases, dcl)
_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/command/session.py b/lmi/scripts/common/command/session.py
index a617539..9d88bbe 100644
--- a/lmi/scripts/common/command/session.py
+++ b/lmi/scripts/common/command/session.py
@@ -127,7 +127,7 @@ class LmiSessionCommand(LmiEndPointCommand):
successful invocation or an exception.
"""
if success:
- if len(self.app.session) > 1:
+ if len(self.session) > 1:
self.formatter.print_host(hostname)
self.produce_output(result)
@@ -187,7 +187,7 @@ class LmiSessionCommand(LmiEndPointCommand):
desired and modifies connection accordingly.
:param connection: Connection to single host.
- :type connection: :py:class:`lmi.shell.LMIConnection`
+ :type connection: :py:class:`lmi.shell.LMIConnection`
:param list args: Arguments handed over to associated function.
:param dictionary kwargs: Keyword arguments handed over to associated
function.
@@ -202,5 +202,5 @@ class LmiSessionCommand(LmiEndPointCommand):
return self.execute(connection, *args, **kwargs)
def run_with_args(self, args, kwargs):
- return self.process_session(self.app.session, args, kwargs)
+ return self.process_session(self.session, args, kwargs)
diff --git a/lmi/scripts/common/command/util.py b/lmi/scripts/common/command/util.py
index eb2c9af..914850a 100644
--- a/lmi/scripts/common/command/util.py
+++ b/lmi/scripts/common/command/util.py
@@ -31,6 +31,8 @@
Utility functions used in command sub-package.
"""
+import inspect
+import os
import re
#: Regular expression matching bracket argument such as ``<arg_name>``.
@@ -79,3 +81,29 @@ def is_abstract_method(clss, method, missing_is_abstract=False):
return False
return missing_is_abstract
+def get_module_name(frame_level=2):
+ """
+ Get a module name of caller from particular outer frame.
+
+ :param integer frame_level: Number of nested frames to skip when searching
+ for called function scope by inspecting stack upwards. When the result
+ of this function is applied directly on the definition of function,
+ it's value should be 1. When used from inside of some other factory, it
+ must be increased by 1.
+
+ Level 0 returns name of this module. Level 1 returns module name of
+ caller. Level 2 returns module name of caller's caller.
+ :returns: Module name.
+ :rtype: string
+ """
+ frame = inspect.currentframe()
+ while frame_level > 0 and frame.f_back:
+ frame = frame.f_back
+ frame_level -= 1
+ module = getattr(frame, 'f_globals', {}).get('__name__', None)
+ if module is None:
+ if hasattr(frame, 'f_code'):
+ module = os.path.basename(frame.f_code.co_filename.splitext())[0]
+ else:
+ module = '_unknown_'
+ return module
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/session.py b/lmi/scripts/common/session.py
index 381bae5..b063d0a 100644
--- a/lmi/scripts/common/session.py
+++ b/lmi/scripts/common/session.py
@@ -35,6 +35,7 @@ from collections import defaultdict
from lmi.scripts.common import errors
from lmi.scripts.common import get_logger
+from lmi.scripts.common.util import FilteredDict
from lmi.shell.LMIConnection import connect
LOG = get_logger(__name__)
@@ -76,26 +77,29 @@ class Session(object):
``None`` if connection can not be made.
"""
if self._connections[hostname] is None:
- self._connections[hostname] = self._connect(
- hostname, interactive=True)
+ try:
+ self._connections[hostname] = self._connect(
+ hostname, interactive=True)
+ except Exception as exc:
+ LOG().error('Failed to make a connection to "%s": %s',
+ hostname, exc)
return self._connections[hostname]
def __len__(self):
""" Get the number of hostnames in session. """
return len(self._connections)
+ def __contains__(self, uri):
+ return uri in self._connections
+
def __iter__(self):
""" Yields connection objects. """
successful_connections = 0
for hostname in self._connections:
- try:
- connection = self[hostname]
- if connection is not None:
- yield connection
- successful_connections += 1
- except Exception as exc:
- LOG().error('Failed to make a connection to "%s": %s',
- hostname, exc)
+ connection = self[hostname]
+ if connection is not None:
+ yield connection
+ successful_connections += 1
if successful_connections == 0:
raise errors.LmiNoConnections('No successful connection made.')
@@ -160,3 +164,27 @@ class Session(object):
"""
return [h for h, c in self._connections.items() if c is None]
+class SessionProxy(Session):
+ """
+ Behaves like a session. But it just encapsulates other session object and
+ provides access to a subset of its items.
+
+ :param session: Session object or even another session proxy.
+ :param list uris: Subset of uris in encapsulated session object.
+ """
+
+ def __init__(self, session, uris):
+ uris = set(uris)
+ if not all(isinstance(uri, basestring) for uri in uris):
+ raise ValueError("uris must be iterable of uris")
+ for uri in uris:
+ if not uri in session:
+ raise ValueError('uri "%s" needs to belong to given session'
+ % uri)
+ Session.__init__(self, session._app, uris, session._credentials,
+ session._same_credentials)
+ self._origin = session
+ self._connections = FilteredDict(uris, session._connections)
+ # let the credentials propagage to original session
+ self._credentials = session._credentials
+
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/setup.py b/setup.py.skel
index b3765cf..0d69f8c 100644
--- a/setup.py
+++ b/setup.py.skel
@@ -5,9 +5,6 @@ import subprocess
import sys
from setuptools import setup, find_packages
-PROJECT = 'openlmi-scripts'
-VERSION = '0.2.7'
-
long_description = ''
try:
try:
@@ -29,8 +26,8 @@ except IOError:
pass
setup(
- name=PROJECT,
- version=VERSION,
+ name='openlmi-scripts',
+ version='@@VERSION@@',
description='Client-side library and command-line client',
long_description=long_description,
author='Michal Minar',
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()