summaryrefslogtreecommitdiffstats
path: root/doc/command-properties.rst
blob: d45f8d9e4892c9ad30982cb6625ff000228c8720 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
.. _command_properties:

Command properties
==================

As noted before in :ref:`end-point_commands`, command at first tries to
process input arguments, calls an associated function and then renders its
result. We'll now introduce properties affecting this process.

Command class properties are written in their bodies and handled by their
metaclasses. After being processed, they are removed from class. So they are
not accessible as class attributes or from their instances.

.. _pre_processing_properties:

Options pre-processing
----------------------
Influencing properties:

    * ``OPT_NO_UNDERSCORES`` (opt_no_underscores_)
    * ``ARG_ARRAY_SUFFIX``   (arg_array_suffix_)
    * ``OWN_USAGE``          (own_usage_)

docopt_ will make a dictionary of options based on usage string such
as the one above (:ref:`usage_string`). Options dictionary matching this
example looks like this: ::

    { 'list'       : bool
    , '--all'      : bool
    , '--disabled' : bool
    , 'start'      : bool
    , '<service>'  : str
    }

Values of this dictionary are passed to an associated function as arguments
with names created out of matching keys. Since argument names can not contain
characters such as `'<'`, `'>'`, `'-'`, etc., these need to be replaced.
Process of renaming of these options can be described by the following pseudo
algorithm:

.. _options_transform_algorithm:

    1. arguments enclosed in brackets are un-surrounded -- brackets get
       removed ::

        "<service>" -> "service"

    2. arguments written in upper case are made lower cased ::

        "FILE" -> "file"

    3. prefix of short and long options made of dashes shall be replaced with
       single underscore ::

        "-a"    -> "_a"
        "--all" -> "_all"

    4. any non-empty sequence of characters not allowed in python's identitier
       shall be replaced with a single underscore ::

        "_long-option"     -> "_long_option"
        "special--cmd-#2"  -> "special_cmd_2"

Points 3 and 4 could be merged into one. But we separate them due to effects
of ``OPT_NO_UNDERSCORES`` property described below.

.. seealso::
    Notes in :ref:`end-point_commands` for method
    :py:meth`lmi.scripts.common.command.endpoint.LmiEndPointCommand.transform_options`
    which is issued before the above algorithm is run.

Treating dashes
~~~~~~~~~~~~~~~
Single dash and double dash are special cases of commands.

Double dash in usage string allows to pass option-like argument to a script
e.g.: ::

    lmi file show -- --file-prefix-with-double-dash

Without the ``'--'`` argument prefixing the file, docopt_ would throw an error
beacause of ``--file-prefix-with-double-dash`` being treated as an unknown
option. This way it's correctly treated as an argument ``<file>`` given the
usage string: ::

    Usage: %(cmd)s file show [--] <file>

Double dash isn't be passed to an associated function.

Single dash on a command line is commonly used to specify stdout or stding. For
example in the following snippet: ::

    Usage: %(cmd)s file copy (- | <file>) <dest>

``'-'`` stands for standard input which will be read instead of a file if the
user wishes to.

Property descriptions
~~~~~~~~~~~~~~~~~~~~~
.. _opt_no_underscores:

``OPT_NO_UNDERSCORES`` : ``bool`` (defaults to ``False``)
    Modifies point 3 of options pre-processing. It causes the prefix of dashes
    to be completely removed with no replacement: ::

        "--long-options" -> "long-option"

    This may not be save if there is a command with the same name as the
    option being removed. Setting this property to ``True`` will cause
    overwriting the command with the value of option. A warning shall be
    echoed if such a case occurs.

.. _arg_array_suffix:

``ARG_ARRAY_SUFFIX`` : ``str`` (defaults to ``""``)
    Adds additional point (5) to `options_transform_algorithm`_. All
    repeatable arguments, resulting in a ``list`` of items, are renamed to
    ``<original_name><suffix>`` [#]_. Repeatable argument in usage string
    looks like this: ::

        """
        Usage: %(cmd)s start <service> ...
        """

    Causing all of the ``<service>`` arguments being loaded into a ``list``
    object.

.. _own_usage:

``OWN_USAGE`` : ``bool`` (defaults to ``False``)
    Says whether the documentation string of this class is a usage string.
    Each command in hierarchy can have its own usage string.

    This can also be assigned a usage string directly: ::

        class MySubcommand(LmiCheckResult):
            """
            Class doc string.
            """
            OWN_USAGE = "Usage: %(cmd)s --opt1 --opt1 <file> <args> ..."
            EXPECT = 0

    But using a boolean value is more readable: ::

        class MySubcommand(LmiCheckResult):
            """
            Usage: %(cmd)s --opt1 --opt1 <file> <args> ...
            """
            OWN_USAGE = True
            EXPECT = 0

    .. note::

        Using own usage strings in subcommands of top-level commands is not
        recommended. It brings a lot of redundancy and may prove problematic
        to modify while keeping consistency among hierarchically nested
        usages.

        Therefore try to have just one usage string in a top-level command.
        And one top-level command in a single module. Resulting in one usage
        string per one command wrappers module. This makes it easier to read
        and modify.

.. _associating_a_function:

Associating a function
----------------------
Influencing properties:

    * ``CALLABLE`` (callable_)

When command is invoked, its method
:py:meth:`~lmi.scripts.common.command.endpoint.LmiEndPointCommand.execute` will
get verified and transformed options as positional and keyword arguments.
This method shall pass them to an associated function residing in script
library and return its result on completition.

One way to associate a function is to use ``CALLABLE`` property. The other
is to define very own ``execute()`` method like this: ::

    class Lister(command.LmiInstanceLister):
        PROPERTIES = ('Name', "Started", 'Status')

        def execute(self, ns, _all, _disabled, _oneshot):
            kind = 'enabled'
            if _all:
                kind = 'all'
            elif _disabled:
                kind = 'disabled'
            elif _oneshot:
                kind = 'oneshot'
            for service_inst in service.list_services(ns, kind):
                yield service_inst

This may come handy if the application object [#]_ needs to be accessed or
if we need to decide which function to call based on command line options.

.. _property_descriptions:

Property descriptions
~~~~~~~~~~~~~~~~~~~~~
.. _callable:

``CALLABLE`` : ``str`` (defaults to ``None``)
    This is a mandatory option if
    :py:meth:`~lmi.scripts.common.command.endpoint.LmiEndPointCommand.execute`
    method is not overriden. It may be a string composed of a full path of
    module and its callable delimited with ``':'``: ::

        CALLABLE = 'lmi.scripts.service:start'

    Causes function ``start()`` of ``'lmi.scripts.service'`` module to be
    associated with command.

    Callable may also be assigned directly like this: ::

        from lmi.scripts import service
        class Start(command.LmiCheckResult):
            CALLABLE = service.start
            EXPECT = 0

    The first variant (by assigning string) comes handy if the particular
    module of associated function is not yet imported. Thus delaying the
    import until the point of function's invocation - if the execution comes
    to this point at all. In short it speeds up execution of ``lmi``
    meta-command by reducing number of module imports that are not needed.

.. _function_invocation:

Function invocation
-------------------
Influencing properties:

    * ``NAMESPACE`` (namespace_)

Property descriptions
~~~~~~~~~~~~~~~~~~~~~

.. _namespace:

``NAMESPACE`` : ``str`` (defaults to ``None``)
    This property affects the first argument passed to an associated function.
    Various values have different impact:

    +-----------+---------------------------------------+-------------------------------------+
    | Value     | Value of first argument.              | Its type                            |
    +===========+=======================================+=====================================+
    | ``None``  | Same impact as value ``"root/cimv2"`` | :py:class:`lmi.shell.LMINamespace`  |
    +-----------+---------------------------------------+-------------------------------------+
    | ``False`` | Raw connection object                 | :py:class:`lmi.shell.LMIConnection` |
    +-----------+---------------------------------------+-------------------------------------+
    | any path  | Namespace object with given path      | :py:class:`lmi.shell.LMINamespace`  |
    +-----------+---------------------------------------+-------------------------------------+

    This usually won't need any modification. Sometimes perhaps associated
    function might want to access more than one namespace, in that case an
    instance of :py:class:`lmi.shell.LMIConnection` might provide more useful.

    Namespace can also be overriden globally in a configuration file or with
    an option on command line.


Output rendering
----------------
All these options begin with ``FMT_`` which is a shortcut for *formatter* as
they become options to formatter objects. These can be defined not only in
end-point commands but also in multiplexers. In the latter case they set the
defaults for all their direct and indirect child commands.

.. note::
    These options override configuration settings and command line options.
    Therefor use them with care.

They are:

.. _fmt_no_headings:

``FMT_NO_HEADINGS`` : ``bool`` (defaults to ``False``)
    Allows to suppress headings (column or row names) in the output.

    .. note::
        With :ref:`lmi_lister` command it's preferable to set the *COLUMNS*
        property to empty list instead. Otherwise associated function is
        expected to return column headers as a first row in its result.

.. _fmt_human_friendly:

``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
them.

``LmiCommandMultiplexer``
~~~~~~~~~~~~~~~~~~~~~~~~~
.. _commands:

``COMMANDS`` : ``dict`` (mandatory)
    Dictionary assigning subcommands to their names listed in usage string.
    Example follows: ::

        class MyCommand(LmiCommandMultiplexer):
            '''
            My command description.

            Usage: %(cmd)s mycommand (subcmd1 | subcmd2)
            '''
            COMMANDS = {'subcmd1' : Subcmd1, 'subcmd2' : Subcmd2}
            OWN_USAGE = True

    Where ``Subcmd1`` and ``Subcmd2`` are some other ``LmiBaseCommand``
    subclasses. Documentation string must be parseable with docopt_.

    ``COMMANDS`` property will be translated to
    :py:meth:`~lmi.scripts.common.command.multiplexer.LmiCommandMultiplexer.child_commands`
    class method by
    :py:class:`~lmi.scripts.common.command.meta.MultiplexerMetaClass`.

``FALLBACK_COMMAND`` : :py:class:`lmi.scripts.common.command.endpoint.LmiEndPointCommand`
    Command class used when no command defined in ``COMMANDS`` dictionary is
    passed on command line.

    Take for example this usage string: ::

        """
        Display hardware information.

        Usage:
            %(cmd)s [all]
            %(cmd)s system
            %(cmd)s chassis
        """

    This suggests there are tree commands defined taking care of listing
    hardware informations. Entry point definition could look like this: ::

        class Hardware(command.LmiCommandMultiplexer):
            OWN_USAGE = __doc__     # usage string from above
            COMMANDS  = { 'all'     : All
                        , 'system'  : System
                        , 'chassis' : Chassis
                        }
            FALLBACK_COMMAND = All

    Without the ``FALLBACK_COMMAND`` property, the multiplexer would not
    handle the case when ``'all'`` argument is omitted as is suggested in
    the usage string. Adding it to command properties causes this multiplexer
    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
~~~~~~~~~~~~~~~~~~~~~~~~
.. _columns:

``COLUMNS`` : ``tuple``
    Column names. It's a tuple with name for each column. Each row of data
    shall then contain the same number of items as this tuple. If omitted,
    associated function is expected to provide them in the first row of
    returned list. It's translated to
    :py:meth:`~lmi.scripts.common.command.lister.LmiBaseListerCommand.get_columns`
    class method.

    If set to empty list, no column headers will be printed. Every item of
    returned list of associated function will be treated as data. Note that
    setting this to empty list makes the *FMT_NO_HEADINGS* property
    redundant.

.. _lmi_instance_commands_properties:
.. _lmi_show_instance_properties:
.. _lmi_instance_lister_properties:

``LmiShowInstance`` and ``LmiInstanceLister`` properties
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These two classes expect, as a result of their associated function, an instance
or a list of instances of some CIM class. They take care of rendering them to
standard output. Thus their properties affect the way how their properties
are rendered.

.. _properties:

``PROPERTIES`` : ``tuple``
    Property names in the same order as the properties shall be listed. Items
    of this tuple can take multiple forms:

    Property Name : ``str``
        Will be used for the name of column/property in output table and the
        same name will be used when obtaining the value from instance. Thus
        this form may be used only if the property name of instance can appear
        as a name of column.

    (Column Name, Property Name) : ``(str, str)``
        This pair allows to render value of property under different name
        (*Column Name*).

    (Column Name, getter) : ``(str, callable)``
        This way allows the value to be arbitrarily computed. The second
        item is a callable taking one and only argument -- the instance of
        class to be rendered.

    Example below shows different ways of rendering attributes for instances
    of ``LMI_Service`` CIM class: ::

        class Show(command.LmiShowInstance):
            CALLABLE = 'lmi.scripts.service:get_instance'
            PROPERTIES = (
                    'Name',
                    ('Enabled', lambda i: i.EnabledDefault == 2),
                    ('Active', 'Started'))

    First property will be shown with the same label as the name of property.
    Second one modifies the value of ``EnabledDefault`` from ``int`` to
    ``bool`` representing enabled state. The last one uses different label for
    property name ``Started``.

.. _dynamic_properties:

``DYNAMIC_PROPERTIES`` : ``bool`` (defaults to ``False``)
    Whether the associated function is expected to return the properties tuple
    itself. If ``True``, the result of associated function must be in form: ::

        (properties, data)

    Where ``properties`` have the same inscription and meaning as a
    ``PROPERTIES`` property of class.

    Otherwise, only the ``data`` is expected.

    .. note::
        Both :py:class:`~lmi.scripts.common.command.show.LmiShowInstance`
        and :py:class:`~lmi.scripts.common.command.lister.LmiInstanceLister`
        expect different ``data`` to be returned. See :ref:`lmi_show_instance`
        and :ref:`lmi_instance_lister` for more information.

.. note::

    Omitting both ``PROPERTIES`` and ``DYNAMIC_PROPERTIES`` makes the
    ``LmiShowInstance`` render all attributes of instance. For
    ``LmiInstanceLister`` this is not allowed, either ``DYNAMIC_PROPERTIES``
    must be ``True`` or ``PROPERTIES`` must be filled.


.. _lmi_check_result_properties:

``LmiCheckResult`` properties
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This command typically does not produce any output if operation succeeds. The
operation succeeds if the result of associated function is expected. There are
more ways how to say what is an expected result. One way is to use ``EXPECT``
property. The other is to provide very own implementation of
:py:class:`~lmi.scripts.common.command.checkresult.LmiCheckResult.check_result`
method.

.. _expect:

``EXPECT``: (mandatory)
    Any value can be assigned to this property. This value is then expected
    to be returned by associated function. Unexpected result is treated
    as an error.

    A callable object assigned here has special meaning. This object must
    accept exactly two parameters:

        1. options - Dictionary with parsed command line options returned by
           docopt_ after being processed by
           :py:meth:`~lmi.scripts.common.command.endpoint.LmiEndPointCommand.transform_options`.
        2. result - Return value of associated function.

    If the associated function does not return an expected result, an error
    such as: ::

        There was 1 error:
        host kvm-fedora-20
            0 != 1

    will be presented to user which is not much helpful. To improve user
    experience, the
    :py:class:`~lmi.scripts.common.command.checkresult.LmiCheckResult.check_result`
    method could be implemented instead. Note the example: ::

        class Update(command.LmiCheckResult):
            ARG_ARRAY_SUFFIX = '_array'

            def check_result(self, options, result):
                """
                :param list result: List of packages successfuly installed
                    that were passed as an ``<package_array>`` arguments.
                """
                if options['<package_array>'] != result:
                    return (False, ('failed to update packages: %s' %
                            ", ".join( set(options['<package_array>'])
                                     - set(result))))
                return True

    The ``execute()`` method is not listed to make the listing shorter. This
    command could be used with usage string such as: ::

        %(cmd)s update [--force] [--repoid <repository>] <package> ...

    In case of a failure, this would produce output like this one: ::

        $ lmi sw update wt wt-doc unknownpackage
        There was 1 error:
        host kvm-fedora-20
            failed to update packages: unknownpackage

.. seealso::

    Docopt_ home page and its git: http://github.org/docopt/docopt.

-------------------------------------------------------------------------------

.. [#] Angle brackets here just mark the boundaries of name components. They
       have nothing to do with arguments.
.. [#] Application object is accessible through ``app`` property of each
       command instance.

.. ****************************************************************************

.. _CIM:            http://dmtf.org/standards/cim
.. _OpenLMI:        http://fedorahosted.org/openlmi/
.. _openlmi-tools:  http://fedorahosted.org/openlmi/wiki/shell
.. _docopt:         http://docopt.org/
.. _docopt-git:     http://github.org/docopt