summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Options/Subcommands.py
blob: 8972bde00e865b153947c27a8aed8a11c55be6b7 (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
""" Classes to make it easier to create commands with large numbers of
subcommands (e.g., bcfg2-admin, bcfg2-info). """

import re
import cmd
import sys
import copy
import shlex
import logging

from Bcfg2.Compat import StringIO
from Bcfg2.Options import PositionalArgument, _debug
from Bcfg2.Options.OptionGroups import Subparser
from Bcfg2.Options.Parser import Parser, setup as master_setup

__all__ = ["Subcommand", "CommandRegistry"]


class Subcommand(object):
    """ Base class for subcommands.  This must be subclassed to create
    commands.

    Specifically, you must override
    :func:`Bcfg2.Options.Subcommand.run`.  You may want to override:

    * The docstring, which will be used as the short help.
    * :attr:`Bcfg2.Options.Subcommand.options`
    * :attr:`Bcfg2.Options.Subcommand.help`
    * :attr:`Bcfg2.Options.Subcommand.interactive`
    *
    * :func:`Bcfg2.Options.Subcommand.shutdown`

    You should not need to override
    :func:`Bcfg2.Options.Subcommand.__call__` or
    :func:`Bcfg2.Options.Subcommand.usage`.

    A ``Subcommand`` subclass constructor must not take any arguments.
    """

    #: Options this command takes
    options = []

    #: Longer help message
    help = None

    #: Whether or not to expose this command in an interactive
    #: :class:`cmd.Cmd` shell, if one is used.  (``bcfg2-info`` uses
    #: one, ``bcfg2-admin`` does not.)
    interactive = True

    _ws_re = re.compile(r'\s+', flags=re.MULTILINE)

    def __init__(self):
        self.core = None
        description = "%s: %s" % (self.__class__.__name__.lower(),
                                  self.__class__.__doc__)

        #: The :class:`Bcfg2.Options.Parser` that will be used to
        #: parse options if this subcommand is called from an
        #: interactive :class:`cmd.Cmd` shell.
        self.parser = Parser(
            prog=self.__class__.__name__.lower(),
            description=description,
            components=[self],
            add_base_options=False,
            epilog=self.help)
        self._usage = None

        #: A :class:`logging.Logger` that can be used to produce
        #: logging output for this command.
        self.logger = logging.getLogger(self.__class__.__name__.lower())

    def __call__(self, args=None):
        """ Perform option parsing and other tasks necessary to
        support running ``Subcommand`` objects as part of a
        :class:`cmd.Cmd` shell.  You should not need to override
        ``__call__``.

        :param args: Arguments given in the interactive shell
        :type args: list of strings
        :returns: The return value of :func:`Bcfg2.Options.Subcommand.run`
        """
        if args is not None:
            self.parser.namespace = copy.copy(master_setup)
            alist = shlex.split(args)
            try:
                setup = self.parser.parse(alist)
            except SystemExit:
                return sys.exc_info()[1].code
            return self.run(setup)
        else:
            return self.run(master_setup)

    def usage(self):
        """ Get the short usage message. """
        if self._usage is None:
            sio = StringIO()
            self.parser.print_usage(file=sio)
            usage = self._ws_re.sub(' ', sio.getvalue()).strip()[7:]
            doc = self._ws_re.sub(' ', getattr(self, "__doc__") or "").strip()
            if not doc:
                self._usage = usage
            else:
                self._usage = "%s - %s" % (usage, doc)
        return self._usage

    def run(self, setup):
        """ Run the command.

        :param setup: A namespace giving the options for this command.
                      This must be used instead of
                      :attr:`Bcfg2.Options.setup` because this command
                      may have been called from an interactive
                      :class:`cmd.Cmd` shell, and thus has its own
                      option parser and its own (private) namespace.
                      ``setup`` is guaranteed to contain all of the
                      options in the global
                      :attr:`Bcfg2.Options.setup` namespace, in
                      addition to any local options given to this
                      command from the interactive shell.
        :type setup: argparse.Namespace
        """
        raise NotImplementedError  # pragma: nocover

    def shutdown(self):
        """ Perform any necessary shutdown tasks for this command This
        is called to when the program exits (*not* when this command
        is finished executing). """
        pass  # pragma: nocover


class Help(Subcommand):
    """List subcommands and usage, or get help on a specific subcommand."""
    options = [PositionalArgument("command", nargs='?')]

    # the interactive shell has its own help
    interactive = False

    def __init__(self, registry):
        Subcommand.__init__(self)
        self._registry = registry

    def run(self, setup):
        commands = self._registry.commands
        if setup.command:
            try:
                commands[setup.command].parser.print_help()
                return 0
            except KeyError:
                print("No such command: %s" % setup.command)
                return 1
        for command in sorted(commands.keys()):
            print(commands[command].usage())


class CommandRegistry(object):
    """A ``CommandRegistry`` is used to register subcommands and provides
    a single interface to run them.  It's also used by
    :class:`Bcfg2.Options.Subcommands.Help` to produce help messages
    for all available commands.
    """

    def __init__(self):
        #: A dict of registered commands.  Keys are the class names,
        #: lowercased (i.e., the command names), and values are instances
        #: of the command objects.
        self.commands = dict()

        #: A list of options that should be added to the option parser
        #: in order to handle registered subcommands.
        self.subcommand_options = []

        #: the help command
        self.help = Help(self)
        self.register_command(self.help)

    def runcommand(self):
        """ Run the single command named in
        ``Bcfg2.Options.setup.subcommand``, which is where
        :class:`Bcfg2.Options.Subparser` groups store the
        subcommand. """
        _debug("Running subcommand %s" % master_setup.subcommand)
        try:
            return self.commands[master_setup.subcommand].run(master_setup)
        finally:
            self.shutdown()

    def shutdown(self):
        """Perform shutdown tasks.

        This calls the ``shutdown`` method of the subcommand that was
        run.
        """
        _debug("Shutting down subcommand %s" % master_setup.subcommand)
        self.commands[master_setup.subcommand].shutdown()

    def register_command(self, cls_or_obj):
        """ Register a single command.

        :param cls_or_obj: The command class or object to register
        :type cls_or_obj: type or Subcommand
        :returns: An instance of ``cmdcls``
        """
        if isinstance(cls_or_obj, type):
            cmdcls = cls_or_obj
            cmd_obj = cmdcls()
        else:
            cmd_obj = cls_or_obj
            cmdcls = cmd_obj.__class__
        name = cmdcls.__name__.lower()
        self.commands[name] = cmd_obj
        # py2.5 can't mix *magic and non-magical keyword args, thus
        # the **dict(...)
        self.subcommand_options.append(
            Subparser(*cmdcls.options, **dict(name=name, help=cmdcls.__doc__)))
        if issubclass(self.__class__, cmd.Cmd) and cmdcls.interactive:
            setattr(self, "do_%s" % name, cmd_obj)
            setattr(self, "help_%s" % name, cmd_obj.parser.print_help)
        return cmd_obj

    def register_commands(self, candidates, parent=Subcommand):
        """ Register all subcommands in ``candidates`` against the
        :class:`Bcfg2.Options.CommandRegistry` subclass given in
        ``registry``.  A command is registered if and only if:

        * It is a subclass of the given ``parent`` (by default,
          :class:`Bcfg2.Options.Subcommand`);
        * It is not the parent class itself; and
        * Its name does not start with an underscore.

        :param registry: The :class:`Bcfg2.Options.CommandRegistry`
                         subclass against which commands will be
                         registered.
        :type registry: Bcfg2.Options.CommandRegistry
        :param candidates: A list of objects that will be considered for
                           registration.  Only objects that meet the
                           criteria listed above will be registered.
        :type candidates: list
        :param parent: Specify a parent class other than
                       :class:`Bcfg2.Options.Subcommand` that all
                       registered commands must subclass.
        :type parent: type
        """
        for attr in candidates:
            if (isinstance(attr, type) and
                    issubclass(attr, parent) and
                    attr != parent and
                    not attr.__name__.startswith("_")):
                self.register_command(attr)