summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Options/Parser.py
blob: 307b767d84d7928b7bf3344520574028e85ac9de (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
""" The option parser """

import os
import sys
import argparse
from Bcfg2.version import __version__
from Bcfg2.Compat import ConfigParser
from Bcfg2.Options import Option, PathOption, BooleanOption

__all__ = ["setup", "OptionParserException", "Parser", "get_parser"]


#: The repository option.  This is specified here (and imported into
#: :module:`Bcfg2.Options.Common`) rather than vice-versa due to
#: circular imports.
repository = PathOption(  # pylint: disable=C0103
    '-Q', '--repository', cf=('server', 'repository'),
    default='var/lib/bcfg2', help="Server repository path")


#: A module-level :class:`argparse.Namespace` object that stores all
#: configuration for Bcfg2.
setup = argparse.Namespace(version=__version__,  # pylint: disable=C0103
                           name="Bcfg2",
                           uri='http://trac.mcs.anl.gov/projects/bcfg2')


class OptionParserException(Exception):
    """ Base exception raised for generic option parser errors """


class Parser(argparse.ArgumentParser):
    """ The Bcfg2 option parser.  Most interfaces should not need to
    instantiate a parser, but should instead use
    :func:`Bcfg2.Options.get_parser` to get the parser that already
    exists."""

    #: Option for specifying the path to the Bcfg2 config file
    configfile = PathOption('-C', '--config',
                            help="Path to configuration file",
                            default="/etc/bcfg2.conf")

    #: Builtin options that apply to all commands
    options = [configfile,
               BooleanOption('--version', help="Print the version and exit"),
               Option('-E', '--encoding', metavar='<encoding>',
                      default='UTF-8', help="Encoding of config files",
                      cf=('components', 'encoding'))]

    #: Flag used in unit tests to disable actual config file reads
    unit_test = False

    def __init__(self, **kwargs):
        """ See :class:`argparse.ArgumentParser` for a full list of
        accepted parameters.

        In addition to supporting all arguments and keyword arguments
        from :class:`argparse.ArgumentParser`, several additional
        keyword arguments are allowed.

        :param components: A list of components to add to the parser.
        :type components: list
        :param namespace: The namespace to store options in.  Default
                          is :attr:`Bcfg2.Options.setup`.
        :type namespace: argparse.Namespace
        :param add_base_options: Whether or not to add the options in
                                 :attr:`Bcfg2.Options.Parser.options`
                                 to the parser.  Setting this to False
                                 is default for subparsers. Default is
                                 True.
        :type add_base_options: bool
        """
        self._cfp = ConfigParser.ConfigParser()
        components = kwargs.pop('components', [])

        #: The namespace options will be stored in.
        self.namespace = kwargs.pop('namespace', setup)
        if self.namespace is None:
            self.namespace = setup
        add_base_options = kwargs.pop('add_base_options', True)

        #: Flag to indicate that this is the pre-parsing 'early' run
        #: for important options like database settings that must be
        #: loaded before other components can be.
        self._early = kwargs.pop('early', False)

        if 'add_help' not in kwargs:
            kwargs['add_help'] = add_base_options
        argparse.ArgumentParser.__init__(self, **kwargs)

        #: Whether or not parsing has completed on all current options.
        self.parsed = False

        #: The argument list that was parsed.
        self.argv = None

        #: Components that have been added to the parser
        self.components = []

        #: Options that have been added to the parser
        self.option_list = []
        self._defaults_set = []
        self._config_files = []
        if add_base_options:
            self.add_component(self)
        if components:
            for component in components:
                self.add_component(component)

    def add_options(self, options):
        """ Add an explicit list of options to the parser.  When
        possible, prefer :func:`Bcfg2.Options.Parser.add_component` to
        add a whole component instead."""
        self.parsed = False
        for option in options:
            if option not in self.option_list:
                self.option_list.extend(option.list_options())
                option.add_to_parser(self)

    def add_component(self, component):
        """ Add a component (and all of its options) to the
        parser. """
        if component not in self.components:
            self.components.append(component)
            if hasattr(component, "options"):
                self.add_options(getattr(component, "options"))

    def _set_defaults(self):
        """ Set defaults from the config file for all options that can
        come from the config file, but haven't yet had their default
        set """
        for opt in self.option_list:
            if opt not in self._defaults_set:
                opt.default_from_config(self._cfp)
                self._defaults_set.append(opt)

    def _parse_config_options(self):
        """ populate the namespace with default values for any options
        that aren't already in the namespace (i.e., options without
        CLI arguments) """
        for opt in self.option_list[:]:
            if not opt.args and opt.dest not in self.namespace:
                value = opt.default
                if value:
                    for parser, action in opt.actions.items():
                        if parser is None:
                            action(self, self.namespace, value)
                        else:
                            action(parser, parser.namespace, value)
                else:
                    setattr(self.namespace, opt.dest, value)

    def _finalize(self):
        """ Finalize the value of any options that require that
        additional post-processing step.  (Mostly
        :class:`Bcfg2.Options.Actions.ComponentAction` subclasses.)
        """
        for opt in self.option_list[:]:
            opt.finalize(self.namespace)

    def _reset_namespace(self):
        """ Delete all options from the namespace except for a few
        predefined values and config file options. """
        self.parsed = False
        for attr in dir(self.namespace):
            if (not attr.startswith("_") and
                attr not in ['uri', 'version', 'name'] and
                attr not in self._config_files):
                delattr(self.namespace, attr)

    def add_config_file(self, dest, cfile, reparse=True):
        """ Add a config file, which triggers a full reparse of all
        options. """
        if dest not in self._config_files:
            self._reset_namespace()
            self._cfp.read([cfile])
            self._defaults_set = []
            self._set_defaults()
            if reparse:
                self._parse_config_options()
            self._config_files.append(dest)

    def reparse(self, argv=None):
        """ Reparse options after they have already been parsed.

        :param argv: The argument list to parse.  By default,
                     :attr:`Bcfg2.Options.Parser.argv` is reused.
                     (I.e., the argument list that was initially
                     parsed.)  :type argv: list
        """
        self._reset_namespace()
        self.parse(argv or self.argv)

    def parse(self, argv=None):
        """ Parse options.

        :param argv: The argument list to parse.  By default,
                     ``sys.argv[1:]`` is used.  This is stored in
                     :attr:`Bcfg2.Options.Parser.argv` for reuse by
                     :func:`Bcfg2.Options.Parser.reparse`.
        :type argv: list
        """
        if argv is None:
            argv = sys.argv[1:]
        if self.parsed and self.argv == argv:
            return self.namespace
        self.argv = argv

        # phase 1: get and read config file
        bootstrap_parser = argparse.ArgumentParser(add_help=False)
        self.configfile.add_to_parser(bootstrap_parser)
        bootstrap = bootstrap_parser.parse_known_args(args=self.argv)[0]

        # check whether the specified bcfg2.conf exists
        if not self.unit_test and not os.path.exists(bootstrap.config):
            self.error("Could not read %s" % bootstrap.config)
        self.add_config_file(self.configfile.dest, bootstrap.config,
                             reparse=False)

        # phase 2: re-parse command line for early options; currently,
        # that's database options
        if not self._early:
            early_opts = argparse.Namespace()
            early_parser = Parser(add_help=False, namespace=early_opts,
                                  early=True)
            # add the repo option so we can resolve <repository>
            # macros
            early_parser.add_options([repository])
            early_components = []
            for component in self.components:
                if getattr(component, "parse_first", False):
                    early_components.append(component)
                    early_parser.add_component(component)
            early_parser.parse(self.argv)
            for component in early_components:
                if hasattr(component, "component_parsed_hook"):
                    getattr(component, "component_parsed_hook")(early_opts)

        # phase 3: re-parse command line, loading additional
        # components, until all components have been loaded.  On each
        # iteration, set defaults from config file/environment
        # variables
        self._parse_config_options()
        while not self.parsed:
            self.parsed = True
            self._set_defaults()
            self.parse_known_args(args=self.argv, namespace=self.namespace)
            self._parse_config_options()
        self._parse_config_options()
        self._finalize()

        # phase 4: fix up <repository> macros
        repo = getattr(self.namespace, "repository", repository.default)
        for attr in dir(self.namespace):
            value = getattr(self.namespace, attr)
            if not attr.startswith("_") and hasattr(value, "replace"):
                setattr(self.namespace, attr,
                        value.replace("<repository>", repo, 1))

        # phase 5: call post-parsing hooks
        if not self._early:
            for component in self.components:
                if hasattr(component, "options_parsed_hook"):
                    getattr(component, "options_parsed_hook")()

        return self.namespace


#: A module-level :class:`Bcfg2.Options.Parser` object that is used
#: for all parsing
_parser = Parser()  # pylint: disable=C0103


def get_parser(description=None, components=None, namespace=None):
    """ Get an existing :class:`Bcfg2.Options.Parser` object.  (One is
    created at the module level when :mod:`Bcfg2.Options` is
    imported.)  If no arguments are given, then the existing parser is
    simply fetched.

    If arguments are given, then one of two things happens:

    * If this is the first ``get_parser`` call with arguments, then
      the values given are set accordingly in the parser, and it is
      returned.
    * If this is not the first such call, then
      :class:`Bcfg2.Options.OptionParserException` is raised.

    That is, a ``get_parser`` call with options is considered to
    initialize the parser that already exists, and that can only
    happen once.

    :param description: Set the parser description
    :type description: string
    :param components: Load the given components in the parser
    :type components: list
    :param namespace: Use the given namespace instead of
                      :attr:`Bcfg2.Options.setup`
    :type namespace: argparse.Namespace
    :returns: Bcfg2.Options.Parser object
    """
    if Parser.unit_test:
        return Parser(description=description, components=components,
                      namespace=namespace)
    elif (description or components or namespace):
        if description:
            _parser.description = description
        if components is not None:
            for component in components:
                _parser.add_component(component)
        if namespace:
            _parser.namespace = namespace
    return _parser