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
|
""" 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'))]
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)
add_base_options = kwargs.pop('add_base_options', True)
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)
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):
""" 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()
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 os.path.exists(bootstrap.config):
self.error("Could not read %s" % bootstrap.config)
self.add_config_file(self.configfile.dest, bootstrap.config)
# phase 2: re-parse command line, loading additional
# components, until all components have been loaded. On each
# iteration, set defaults from config file/environment
# variables
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._finalize()
self._parse_config_options()
# phase 3: 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 4: call post-parsing hooks
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
#: Track whether or not the module-level parser has been initialized
#: yet. We track this separately because some things (e.g., modules
#: that add components on import) will use the parser before it has
#: been initialized, so we can't just set
#: :attr:`Bcfg2.Options._parser` to None and wait for
#: :func:`Bcfg2.Options.get_parser` to be called.
_parser_initialized = False # 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_initialized and (description or components or namespace):
raise OptionParserException("Parser has already been initialized")
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
|