summaryrefslogtreecommitdiffstats
path: root/doc/script-development.rst
diff options
context:
space:
mode:
Diffstat (limited to 'doc/script-development.rst')
-rw-r--r--doc/script-development.rst668
1 files changed, 0 insertions, 668 deletions
diff --git a/doc/script-development.rst b/doc/script-development.rst
deleted file mode 100644
index a4ec801..0000000
--- a/doc/script-development.rst
+++ /dev/null
@@ -1,668 +0,0 @@
-Script Development
-==================
-This provides a general overview on what script is, how is it written
-and is interfaced with.
-
-Prerequisities
---------------
-Reader should be familiar about a CIM_ (Common Information Model). He should
-have a general idea about, what OpenLMI_ is and what it does. He should get
-familiar with ``lmishell``, which is a python binary shipped with
-openlmi-tools_.
-
-Also user should be acquinted with writing help strings for unix command line
-utilities [#]_.
-
-Introduction
-------------
-By a *script* in this document we mean:
-
- * Python library utilizing ``lmishell`` for instrumenting CIM providers
- through a CIMOM broker. It resides in ``lmi.scripts.<profile_name>``
- package, where ``<profile_name>`` corresponds to a DMTF profile or an
- arbitrary set of ``OpenLMI`` providers.
- * Command wrappers for this library as a set of classes inheriting from
- :py:class:`~lmi.scripts.common.command.base.LmiBaseCommand`. These may
- create a hierarchy of nested subcommands. They are the entry points of
- ``lmi`` meta-command to the wrapped functionality of library.
-
-Command wrappers are part of the library usually grouped in a single
-module named after the ``lmi`` subcommand or ``cmd``: ::
-
- lmi.scripts.<profile_name>.cmd
-
-Writing a library
------------------
-Library shall consist of a set of functions taking a namespace or
-connection object as a first argument. There are no special requirements
-on how to divide these functions into submodules. Use common sense. Smaller
-scripts can have all functionality in
-``lmi/scripts/<profile_name>/__init__.py`` module. With wrappers usually
-contained in ``lmi/scripts/<profile_name>/cmd.py``.
-
-Library should be written with an ease of use in mind. Functions should
-represent possible use cases of what can be done with particular
-providers instead of wrapping 1-to-1 a CIM class's methods in python
-functions.
-
-Any function that shall be called by a command wrapper must accect a
-``namespace`` argument, which by a convention is called ``ns``. It's an
-instance of :py:class:`lmi.shell.LMINamespace` providing quick access to
-represented CIM namespace [#]_ and its classes. It's also possible to specify
-that function shall be passed a raw :py:class:`lmi.shell.LMIConnection` object.
-For details see :ref:`function_invocation`.
-
-Service example
-~~~~~~~~~~~~~~~
-Suppose we have a service profile and we want to write a python interface
-for. Real provider implementation can be found at ``src/service`` directory
-in upstream git [#]_. For more information please refer to `service description`_.
-
-As you may see, this implements single CIM class ``LMI_Service`` with a few
-useful methods such as:
-
- * ``StartService()``
- * ``StopService()``
-
-We'd like our users to provide a way how to list system services, get a
-details for one of them and allow to start, stop and restart them.
-
-Simplified [#]_ version of some of these functions may look like this: ::
-
- def list_services(ns, kind='enabled'):
- for service in sorted(ns.LMI_Service.instances(),
- key=lambda i: i.Name):
- if kind == 'disabled' and service.EnabledDefault != \
- ns.LMI_Service.EnabledDefaultValues.Disabled:
- continue
- if kind == 'oneshot' and service.EnabledDefault != \
- ns.LMI_Service.EnabledDefaultValues.NotApplicable:
- continue
- if kind == 'enabled' and service.EnabledDefault != \
- ns.LMI_Service.EnabledDefaultValues.Enabled:
- # list only enabled
- continue
- yield service
-
-It yields instances of ``LMI_Service`` class. There is no need to use
-exclusively ``yield`` statement instead of ``return``. We prefer to use it in
-enumerating functions because of memory usage reduction (which is possible to
-occur in the future, when underlying components will also allow for lazy
-evaluation). Moreover user may limit the number of instances listed, reducing
-the number of instances evaluated.
-
-::
-
- from lmi.shell import LMIInstanceName
- from lmi.scripts.common import get_logger
- from lmi.scripts.common.errors import LmiFailed
-
- LOG = get_logger(__name__)
-
- def start_service(ns, service):
- if isinstance(service, basestring):
- # let's accept service as a string
- inst = ns.LMI_Service.first_instance(key="Name", value=service)
- name = service
- else: # or as LMIInstance or LMIInstanceName
- inst = service
- name = inst.path['Name']
- if inst is None:
- raise LmiFailed('No such service "%s".' % name)
- if isinstance(inst, LMIInstanceName):
- # we need LMIInstance
- inst = inst.to_instance()
- res = inst.StartService()
- if res == 0:
- LOG().debug('Started service "%s" on hostname "%s".',
- name, ns.hostname)
- return res
-
-In similar fashion, ``stop_service``, ``restart_service`` and others could be
-written.
-
-``ns`` argument typically points to ``root/cimv2`` namespace, which is the
-main implementation namespace for ``OpenLMI`` providers. One could also write
-these functions operating upon a connection object like this: ::
-
- def get_instance(c, service):
- inst = c.root.cimv2.LMI_Service.first_instance(
- key="Name", value=service)
- if inst is None:
- raise LmiFailed('No such service "%s".' % service)
- return inst
-
-User can then easily access any other namespace he may need. Command classes
-need to be informed about what wrapped function expects. This will be
-explained later in more detail (see :ref:`function_invocation`).
-
-The ``LOG`` variable provides acces to the logger of this module. Messages
-logged in this way end up in a log file [#]_ and console. Implicitly only
-warnings and higher priority messages are logged into a console. This is
-controllable with ``lmi`` parameters.
-
-Useful information should not be rendered or printed by these functions
-directly. Wrapper commands shall post-process instances or data returned,
-render useful information and print it on standard output stream.
-
-If operation fails due to some not-so-unexpected error, please use
-:py:class:`~lmi.scripts.common.errors.LmiFailed` exception with human readable
-description.
-
-For more *real world* examples, take a look on scripts already present in our
-`upstream git`_.
-
-.. _command_wrappers_overview:
-
-Command wrappers overview
--------------------------
-They are a set of commands wrapping up library's functionality in a set of
-commands creating a tree invocable by ``lmi`` meta-command. All commands are
-subclasses of :py:class:`~lmi.scripts.common.command.base.LmiBaseCommand`.
-
-Behaviour of commands is controlled by class properties such as these: ::
-
- class Show(command.LmiShowInstance):
- CALLABLE = 'lmi.scripts.service:get_instance'
- PROPERTIES = (
- 'Name',
- 'Caption',
- ('Enabled', lambda i: i.EnabledDefault == 2),
- ('Active', 'Started'),
- 'Status')
-
-Example above contains definition of **show** command for instances of
-``LMI_Service``. Its associated function is ``get_instance()`` located in
-``lmi.scripts.service`` module [#]_. Properties used will be described
-in detail later (see :ref:`lmi_instance_commands_properties`). Let's just say,
-that ``PROPERTIES`` specify a way how the instance is rendered.
-
-Top-level commands
-~~~~~~~~~~~~~~~~~~
-Are entry points of a script library. They are direct subcommands of ``lmi``.
-For example: ::
-
- $ lmi help
- $ lmi service list
- $ lmi sw show openlmi-providers
-
-``help``, ``service`` and ``sw`` are top-level commands. One script library
-(such as ``service`` above) can provide one or more of them. They need to be
-listed in a ``setup.py`` script in ``entry_points`` argument of ``setup()``
-function. More details will be noted later in `Writing setup.py`_.
-
-They contain usage string which is a documentation and prescription of
-command-line arguments in one string. This string is printed when user
-requests command's help: ::
-
- $ lmi help
-
-.. _usage_string:
-
-Usage string
-^^^^^^^^^^^^
-looks like this: ::
-
- """
- System service management.
-
- Usage:
- %(cmd)s list [--all | --disabled]
- %(cmd)s start <service>
-
- Options:
- --all List all services available.
- --disabled List only disabled services.
- """
-
-Format of this string is very important, it's parsed by a docopt_ command line
-parser, generating options dictionary for commands. Please refer to its
-documentation for details.
-
-.. note::
-
- There is one deviation to *classical* usage string. It's the use of
- ``%(cmd)s`` formatting mark. This is replaced with full command's name.
- Full name means that all subcommands and binary name prefixing current
- command on command line are part of it. So for example full name of
- command **list** in a following string passed to command line: ::
-
- lmi sw list pkgs
-
- is ``lmi sw list``.
-
- If parsing **sw** usage, it is just ``lmi sw``.
-
- Please use this notation instead of writing your own usages completely.
- Although it may work from command line, it won't work in interactive
- mode without ``%(cmd)s`` being used.
-
-.. _end-point_commands_introduction:
-
-End-point commands
-~~~~~~~~~~~~~~~~~~
-Are associated with one or more function of script library. They handle the
-following:
-
- 1. call docopt_ parser on command line arguments
- 2. make some name pre-processing on them (see
- :ref:`pre_processing_properties`)
- 3. verify them (see :ref:`end-point_commands`)
- 4. transform them (see :ref:`end-point_commands`)
- 5. pass them to associated function
- 6. collect results
- 7. render them and print them
-
-Developper of command wrappers needs to be familiar about each step. We will
-describe them later in details.
-
-There are following end-point commands available for subclassing:
-
- * ``LmiCheckResult`` (see :ref:`lmi_check_result`)
- * ``LmiLister`` (see :ref:`lmi_lister`)
- * ``LmiInstanceLister`` (see :ref:`lmi_instance_lister`)
- * ``LmiShowInstance`` (see :ref:`lmi_show_instance`)
-
-They differ in how they render the result obtained from associated function.
-
-These are listed in depth in :ref:`end-point_commands`.
-
-.. _command_multiplexers_introduction:
-
-Command multiplexers
-~~~~~~~~~~~~~~~~~~~~
-Provide a way how to group multiple commands under one. Suppose you want to
-list packages, repositories and files. All of these use cases need different
-arguments, and render different information so logically they should be
-represented by independent end-point commands. What binds them together is the
-user's wish to *list* something. He may wish for other things like *show*,
-*add*, *remove* etc. Having all combination of these wishes and things would
-generate a lot of commands under the top-level one. Let's instead group them
-under particular *wish* like this:
-
- * ``sw list packages``
- * ``sw list repositories``
- * ``sw list files``
- * ``sw show package``
-
-To reflect it in our commands definition hierarchy, we need to use
-:py:class:`~lmi.scripts.common.command.multiplexer.LmiCommandMultiplexer`
-command.
-
-::
-
- class Lister(command.LmiCommandMultiplexer):
- """ List information about packages, repositories or files. """
- COMMANDS = {
- 'packages' : PkgLister,
- 'repositories' : RepoLister,
- 'files' : FileLister
- }
-
-Where ``COMMANDS`` property maps subcommand classes to their names as will
-be passed on command line. Each command multiplexer consumes one command
-argument from command line, representing the subcommand and passes the rest
-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 a special select command used to specify which command to load
-for particular version on remote broker. It can be written 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:
-
- 1. license header
- 2. usage dostring - parseable by docopt_
- 3. end-point command wrappers
- 4. single top-level command
-
-The top-level command is usally defined like this: ::
-
- Service = command.register_subcommands(
- 'Service', __doc__,
- { 'list' : Lister
- , 'show' : Show
- , 'start' : Start
- , 'stop' : Stop
- , 'restart' : Restart
- },
- )
-
-Where the ``__doc__`` is a usage string (see usage_string_) and module's doc
-string at the same time. It's mentioned in point 2. ``Service`` is a name,
-which will be listed in ``entry_points`` dictionary described in section below
-(entry_points_). The global variable's name we assign to should be the same as
-the value of the first argument to
-:py:func:`~lmi.scripts.common.command.helper.register_subcommands`. The last
-argument here is the dictionary mapping all subcommands of **service** to their
-names [#]_.
-
-Egg structure
-~~~~~~~~~~~~~
-Script library is distributed as an python egg, making it easy to distribute
-and install either to system or user directory.
-
-Following tree shows directory structure of *service* egg residing in
-`upstream git`_:
-
-.. only:: not latex
-
- ::
-
- commands/service
- ├── lmi
- │ ├── __init__.py
- │ └── scripts
- │ ├── __init__.py
- │ └── service
- │ ├── cmd.py
- │ └── __init__.py
- ├── README.md
- └── setup.py
-
-.. raw:: latex
-
- \begin{center}
- \begin{tikzpicture}[dirtree]
- \node {commands/service}
- child { node {lmi}
- child { node {\_\_init\_\_.py} }
- child { node {scripts}
- child { node {\_\_init\_\_.py} }
- child { node {service}
- child { node {cmd.py} }
- child { node {\_\_init\_\_.py} }
- }
- }
- }
- child { node {README.md} }
- child { node {setup.py} };
- \end{tikzpicture} \\
- \end{center}
-
-This library then can be imported with: ::
-
- from lmi.scripts import service
-
-``commands/service/scripts/service`` must be a package (directory with
-``__init__.py``) because ``lmi.scripts`` is a namespace package. It
-can have arbitrary number of modules and subpackages. The care should be taken
-to make the API easy to use and learn though.
-
-Writing ``setup.py``
---------------------
-Follows a minimal example of ``setup.py`` script for service library. ::
-
- from setuptools import setup, find_packages
- setup(
- name="openlmi-scripts-service",
- version="0.1.0",
- description='LMI command for system service administration.',
- url='https://github.com/openlmi/openlmi-scripts',
- platforms=['Any'],
- license="BSD",
- install_requires=['openlmi-scripts'],
- namespace_packages=['lmi', 'lmi.scripts'],
- packages=['lmi', 'lmi.scripts', 'lmi.scripts.service'],
-
- entry_points={
- 'lmi.scripts.cmd': [
- 'service = lmi.scripts.service.cmd:Service',
- ],
- },
- )
-
-.. _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
-``service`` in ``lmi.scripts.cmd`` namespace. This particular namespace is used
-by ``lmi`` meta-command when searching of registered user commands. ``Service``
-is a command multiplexer, created with a call to
-:py:func:`~lmi.scripts.common.command.helper.register_subcommands` grouping
-end-point commands together.
-
-Next example shows setup with more top-level commands
-(of storage scripts library): ::
-
- entry_points={
- 'lmi.scripts.cmd': [
- 'fs = lmi.scripts.storage.fs_cmd:Fs',
- 'partition = lmi.scripts.storage.partition_cmd:Partition',
- 'raid = lmi.scripts.storage.raid_cmd:Raid',
- 'lv = lmi.scripts.storage.lv_cmd:Lv',
- 'vg = lmi.scripts.storage.vg_cmd:Vg',
- 'storage = lmi.scripts.storage.storage_cmd:Storage',
- 'mount = lmi.scripts.storage.mount_cmd:Mount',
- ],
- },
-
-Conventions
------------
-There are several conventions you should try to follow in your shiny scripts.
-
-Logging messages
-~~~~~~~~~~~~~~~~
-In each module where logging facilities are going to be used, define global
-varibale ``LOG`` like this: ::
-
- from lmi.scripts.common import get_logger
-
- LOG = get_logger(__name__)
-
-It's a callable used throughout particular module in this way: ::
-
- LOG().warn('All the data of "%s" will be lost!', partition)
-
-Each message should be a whole sentence. It shall begin with an upper case
-letter and end with a dot or other sentence terminator.
-
-Bad example: ::
-
- LOG().info('processing %s', card)
-
-Exceptions
-~~~~~~~~~~
-Again all the exceptions should be initialized with messages forming
-a whole sentence.
-
-They will be catched and printed on *stderr* by lmi meta-command. If the
-*Trace* option in :ref:`sect_main` is on, traceback will be printed. There is
-just one exception. If the exception inherits from
-:py:class:`~lmi.scripts.common.errors.LmiError`, traceback won't be printed
-unless verbosity level is highest: ::
-
- # self refers to some command
- self.app.config.verbosity == self.app.config.OUTPUT_DEBUG
-
-This is a feature allowing for common error use-cases to be gracefully
-handled. In your scripts you should stick to using
-:py:class:`~lmi.scripts.common.errors.LmiFailed` for such exceptions.
-
-Following is an example of such a common error-case, where printing traceback
-does not add any interesting information: ::
-
- iname = ns.LMI_Service.new_instance_name({
- "Name": service,
- "CreationClassName" : "LMI_Service",
- "SystemName" : cs.Name,
- "SystemCreationClassName" : cs.CreationClassName
- })
- inst = iname.to_instance()
- if inst is None:
- raise errors.LmiFailed('No such service "%s".' % service)
- # process the service instance
-
-``service`` is a name provided by user. If such a service is not found,
-``inst`` will be assigned ``None``. In this case we don't want to continue in
-script's execution thus we raise an exception. We provide very clear message
-that needs no other comment. We don't want any traceback to be printed, thus
-the use of :py:class:`~lmi.scripts.common.errors.LmiFailed`.
-
-Detailed description
---------------------
-These pages provide more details of some aspects:
-
-.. toctree::
- :maxdepth: 2
-
- command-classes
- command-properties
-
-Tutorial
---------
-
-.. toctree::
- :maxdepth: 2
-
- script-tutorial
-
--------------------------------------------------------------------------------
-
-.. seealso::
-
- Docopt_ documentation.
-
- :ref:`command_classes`
-
- :ref:`command_properties`
-
- :ref:`script_tutorial`
-
--------------------------------------------------------------------------------
-
-.. [#] Described by a POSIX.
-.. [#] Default namespace is ``"root/cimv2"``.
-.. [#] view: https://fedorahosted.org/openlmi/browser/openlmi-providers
- git: ``ssh://git.fedorahosted.org/git/openlmi-providers.git/``
-.. [#] Simplified here means that there are no documentation strings
- and no type checking.
-.. [#] If logging to a file is enabled in configuration.
-.. [#] 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
-.. _OpenLMI: http://fedorahosted.org/openlmi/
-.. _openlmi-tools: http://fedorahosted.org/openlmi/wiki/shell
-.. _docopt: http://docopt.org/
-.. _`service description`: https://fedorahosted.org/openlmi/wiki/service
-.. _`upstream git`: https://github.com/openlmi/openlmi-scripts