diff options
71 files changed, 2553 insertions, 381 deletions
@@ -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 @@ -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" @@ -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 @@ -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() |