7f7e62310b50ebc4879cf142ef865c53041b3db4
[certmaster.git] / certmaster / config.py
1 # This program is free software; you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation; either version 2 of the License, or
4 # (at your option) any later version.
5 #
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU Library General Public License for more details.
10 #
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
14 # Copyright 2002 Duke University
15 # filched from yum - menno smits wrote this - he rocks
16
17
18 import os
19 import sys
20 import warnings
21 import copy
22 import urlparse
23 from ConfigParser import NoSectionError, NoOptionError, ConfigParser
24 from ConfigParser import ParsingError
25 import exceptions
26
27 CONFIG_FILE = "/etc/certmaster/certmaster.conf"
28
29 class ConfigError(exceptions.Exception):
30 def __init__(self, value=None):
31 exceptions.Exception.__init__(self)
32 self.value = value
33 def __str__(self):
34 return "%s" %(self.value,)
35
36
37 class Option(object):
38 '''
39 This class handles a single Yum configuration file option. Create
40 subclasses for each type of supported configuration option.
41
42 Python descriptor foo (__get__ and __set__) is used to make option
43 definition easy and consise.
44 '''
45
46 def __init__(self, default=None):
47 self._setattrname()
48 self.inherit = False
49 self.default = default
50
51 def _setattrname(self):
52 '''Calculate the internal attribute name used to store option state in
53 configuration instances.
54 '''
55 self._attrname = '__opt%d' % id(self)
56
57 def __get__(self, obj, objtype):
58 '''Called when the option is read (via the descriptor protocol).
59
60 @param obj: The configuration instance to modify.
61 @param objtype: The type of the config instance (not used).
62 @return: The parsed option value or the default value if the value
63 was not set in the configuration file.
64 '''
65 if obj is None:
66 return self
67
68 return getattr(obj, self._attrname, None)
69
70 def __set__(self, obj, value):
71 '''Called when the option is set (via the descriptor protocol).
72
73 @param obj: The configuration instance to modify.
74 @param value: The value to set the option to.
75 @return: Nothing.
76 '''
77 # Only try to parse if its a string
78 if isinstance(value, basestring):
79 try:
80 value = self.parse(value)
81 except ValueError, e:
82 # Add the field name onto the error
83 raise ValueError('Error parsing %r: %s' % (value, str(e)))
84
85 setattr(obj, self._attrname, value)
86
87 def setup(self, obj, name):
88 '''Initialise the option for a config instance.
89 This must be called before the option can be set or retrieved.
90
91 @param obj: BaseConfig (or subclass) instance.
92 @param name: Name of the option.
93 '''
94 setattr(obj, self._attrname, copy.copy(self.default))
95
96 def clone(self):
97 '''Return a safe copy of this Option instance
98 '''
99 new = copy.copy(self)
100 new._setattrname()
101 return new
102
103 def parse(self, s):
104 '''Parse the string value to the Option's native value.
105
106 @param s: Raw string value to parse.
107 @return: Validated native value.
108
109 Will raise ValueError if there was a problem parsing the string.
110 Subclasses should override this.
111 '''
112 return s
113
114 def tostring(self, value):
115 '''Convert the Option's native value to a string value.
116
117 @param value: Native option value.
118 @return: String representation of input.
119
120 This does the opposite of the parse() method above.
121 Subclasses should override this.
122 '''
123 return str(value)
124
125
126 def Inherit(option_obj):
127 '''Clone an Option instance for the purposes of inheritance. The returned
128 instance has all the same properties as the input Option and shares items
129 such as the default value. Use this to avoid redefinition of reused
130 options.
131
132 @param option_obj: Option instance to inherit.
133 @return: New Option instance inherited from the input.
134 '''
135 new_option = option_obj.clone()
136 new_option.inherit = True
137 return new_option
138
139
140 class ListOption(Option):
141
142 def __init__(self, default=None):
143 if default is None:
144 default = []
145 super(ListOption, self).__init__(default)
146
147 def parse(self, s):
148 """Converts a string from the config file to a workable list
149
150 Commas and spaces are used as separators for the list
151 """
152 # we need to allow for the '\n[whitespace]' continuation - easier
153 # to sub the \n with a space and then read the lines
154 s = s.replace('\n', ' ')
155 s = s.replace(',', ' ')
156 return s.split()
157
158 def tostring(self, value):
159 return '\n '.join(value)
160
161
162 class UrlOption(Option):
163 '''
164 This option handles lists of URLs with validation of the URL scheme.
165 '''
166
167 def __init__(self, default=None, schemes=('http', 'ftp', 'file', 'https'),
168 allow_none=False):
169 super(UrlOption, self).__init__(default)
170 self.schemes = schemes
171 self.allow_none = allow_none
172
173 def parse(self, url):
174 url = url.strip()
175
176 # Handle the "_none_" special case
177 if url.lower() == '_none_':
178 if self.allow_none:
179 return None
180 else:
181 raise ValueError('"_none_" is not a valid value')
182
183 # Check that scheme is valid
184 (s,b,p,q,f,o) = urlparse.urlparse(url)
185 if s not in self.schemes:
186 raise ValueError('URL must be %s not "%s"' % (self._schemelist(), s))
187
188 return url
189
190 def _schemelist(self):
191 '''Return a user friendly list of the allowed schemes
192 '''
193 if len(self.schemes) < 1:
194 return 'empty'
195 elif len(self.schemes) == 1:
196 return self.schemes[0]
197 else:
198 return '%s or %s' % (', '.join(self.schemes[:-1]), self.schemes[-1])
199
200
201 class UrlListOption(ListOption):
202 '''
203 Option for handling lists of URLs with validation of the URL scheme.
204 '''
205
206 def __init__(self, default=None, schemes=('http', 'ftp', 'file', 'https')):
207 super(UrlListOption, self).__init__(default)
208
209 # Hold a UrlOption instance to assist with parsing
210 self._urloption = UrlOption(schemes=schemes)
211
212 def parse(self, s):
213 out = []
214 for url in super(UrlListOption, self).parse(s):
215 out.append(self._urloption.parse(url))
216 return out
217
218
219 class IntOption(Option):
220 def parse(self, s):
221 try:
222 return int(s)
223 except (ValueError, TypeError), e:
224 raise ValueError('invalid integer value')
225
226
227 class BoolOption(Option):
228 def parse(self, s):
229 s = s.lower()
230 if s in ('0', 'no', 'false'):
231 return False
232 elif s in ('1', 'yes', 'true'):
233 return True
234 else:
235 raise ValueError('invalid boolean value')
236
237 def tostring(self, value):
238 if value:
239 return "1"
240 else:
241 return "0"
242
243
244 class FloatOption(Option):
245 def parse(self, s):
246 try:
247 return float(s.strip())
248 except (ValueError, TypeError):
249 raise ValueError('invalid float value')
250
251
252 class SelectionOption(Option):
253 '''Handles string values where only specific values are allowed
254 '''
255 def __init__(self, default=None, allowed=()):
256 super(SelectionOption, self).__init__(default)
257 self._allowed = allowed
258
259 def parse(self, s):
260 if s not in self._allowed:
261 raise ValueError('"%s" is not an allowed value' % s)
262 return s
263
264 class BytesOption(Option):
265
266 # Multipliers for unit symbols
267 MULTS = {
268 'k': 1024,
269 'm': 1024*1024,
270 'g': 1024*1024*1024,
271 }
272
273 def parse(self, s):
274 """Parse a friendly bandwidth option to bytes
275
276 The input should be a string containing a (possibly floating point)
277 number followed by an optional single character unit. Valid units are
278 'k', 'M', 'G'. Case is ignored.
279
280 Valid inputs: 100, 123M, 45.6k, 12.4G, 100K, 786.3, 0
281 Invalid inputs: -10, -0.1, 45.6L, 123Mb
282
283 Return value will always be an integer
284
285 1k = 1024 bytes.
286
287 ValueError will be raised if the option couldn't be parsed.
288 """
289 if len(s) < 1:
290 raise ValueError("no value specified")
291
292 if s[-1].isalpha():
293 n = s[:-1]
294 unit = s[-1].lower()
295 mult = self.MULTS.get(unit, None)
296 if not mult:
297 raise ValueError("unknown unit '%s'" % unit)
298 else:
299 n = s
300 mult = 1
301
302 try:
303 n = float(n)
304 except ValueError:
305 raise ValueError("couldn't convert '%s' to number" % n)
306
307 if n < 0:
308 raise ValueError("bytes value may not be negative")
309
310 return int(n * mult)
311
312
313 class ThrottleOption(BytesOption):
314
315 def parse(self, s):
316 """Get a throttle option.
317
318 Input may either be a percentage or a "friendly bandwidth value" as
319 accepted by the BytesOption.
320
321 Valid inputs: 100, 50%, 80.5%, 123M, 45.6k, 12.4G, 100K, 786.0, 0
322 Invalid inputs: 100.1%, -4%, -500
323
324 Return value will be a int if a bandwidth value was specified or a
325 float if a percentage was given.
326
327 ValueError will be raised if input couldn't be parsed.
328 """
329 if len(s) < 1:
330 raise ValueError("no value specified")
331
332 if s[-1] == '%':
333 n = s[:-1]
334 try:
335 n = float(n)
336 except ValueError:
337 raise ValueError("couldn't convert '%s' to number" % n)
338 if n < 0 or n > 100:
339 raise ValueError("percentage is out of range")
340 return n / 100.0
341 else:
342 return BytesOption.parse(self, s)
343
344
345 class BaseConfig(object):
346 '''
347 Base class for storing configuration definitions. Subclass when creating
348 your own definitons.
349 '''
350
351 def __init__(self):
352 self._section = None
353
354 for name in self.iterkeys():
355 option = self.optionobj(name)
356 option.setup(self, name)
357
358 def __str__(self):
359 out = []
360 out.append('[%s]' % self._section)
361 for name, value in self.iteritems():
362 out.append('%s: %r' % (name, value))
363 return '\n'.join(out)
364
365 def populate(self, parser, section, parent=None):
366 '''Set option values from a INI file section.
367
368 @param parser: ConfParser instance (or subclass)
369 @param section: INI file section to read use.
370 @param parent: Optional parent BaseConfig (or subclass) instance to use
371 when doing option value inheritance.
372 '''
373 self.cfg = parser
374 self._section = section
375
376 for name in self.iterkeys():
377 option = self.optionobj(name)
378 value = None
379 try:
380 value = parser.get(section, name)
381 except (NoSectionError, NoOptionError):
382 # No matching option in this section, try inheriting
383 if parent and option.inherit:
384 value = getattr(parent, name)
385
386 if value is not None:
387 setattr(self, name, value)
388
389 def optionobj(cls, name):
390 '''Return the Option instance for the given name
391 '''
392 obj = getattr(cls, name, None)
393 if isinstance(obj, Option):
394 return obj
395 else:
396 raise KeyError
397 optionobj = classmethod(optionobj)
398
399 def isoption(cls, name):
400 '''Return True if the given name refers to a defined option
401 '''
402 try:
403 cls.optionobj(name)
404 return True
405 except KeyError:
406 return False
407 isoption = classmethod(isoption)
408
409 def iterkeys(self):
410 '''Yield the names of all defined options in the instance.
411 '''
412 for name, item in self.iteritems():
413 yield name
414
415 def iteritems(self):
416 '''Yield (name, value) pairs for every option in the instance.
417
418 The value returned is the parsed, validated option value.
419 '''
420 # Use dir() so that we see inherited options too
421 for name in dir(self):
422 if self.isoption(name):
423 yield (name, getattr(self, name))
424
425 def write(self, fileobj, section=None, always=()):
426 '''Write out the configuration to a file-like object
427
428 @param fileobj: File-like object to write to
429 @param section: Section name to use. If not-specified the section name
430 used during parsing will be used.
431 @param always: A sequence of option names to always write out.
432 Options not listed here will only be written out if they are at
433 non-default values. Set to None to dump out all options.
434 '''
435 # Write section heading
436 if section is None:
437 if self._section is None:
438 raise ValueError("not populated, don't know section")
439 section = self._section
440
441 # Updated the ConfigParser with the changed values
442 cfgOptions = self.cfg.options(section)
443 for name,value in self.iteritems():
444 option = self.optionobj(name)
445 if always is None or name in always or option.default != value or name in cfgOptions :
446 self.cfg.set(section,name, option.tostring(value))
447 # write the updated ConfigParser to the fileobj.
448 self.cfg.write(fileobj)
449
450 def getConfigOption(self, option, default=None):
451 warnings.warn('getConfigOption() will go away in a future version of Yum.\n'
452 'Please access option values as attributes or using getattr().',
453 DeprecationWarning)
454 if hasattr(self, option):
455 return getattr(self, option)
456 return default
457
458 def setConfigOption(self, option, value):
459 warnings.warn('setConfigOption() will go away in a future version of Yum.\n'
460 'Please set option values as attributes or using setattr().',
461 DeprecationWarning)
462 if hasattr(self, option):
463 setattr(self, option, value)
464 else:
465 raise ConfigError, 'No such option %s' % option
466
467
468 def read_config(config_file, BaseConfigDerived):
469 confparser = ConfigParser()
470 opts = BaseConfigDerived()
471 if os.path.exists(config_file):
472 try:
473 confparser.read(config_file)
474 except ParsingError, e:
475 print >> sys.stderr, "Error reading config file: %s" % e
476 sys.exit(1)
477 opts.populate(confparser, 'main')
478
479 ## build up the cas structure
480 opts.ca = {}
481 # opts.ca[''] = {}
482
483 ## Add the default items when just using a single ca
484 # main_items = confparser.items('main')
485 # for (key,value) in main_items:
486 # if key in ['autosign','cadir','cert_dir','certroot','csrroot']:
487 # print "main ca: key: %s, value: %s" % (key,value)
488 # opts.ca[''][key] = value
489 opts.ca[''] = BaseConfigDerived()
490 opts.ca[''].populate(confparser,'main')
491
492 ## Add additonal ca sections
493 sections = confparser.sections()
494 for a_section in sections:
495 if a_section.startswith('ca:'):
496 ca_name = a_section[3:]
497 # items = confparser.items(a_section)
498 # opts.ca[ca_name] = {}
499 # for (key,value) in items:
500 # opts.ca[ca_name][key] = value
501 opts.ca[ca_name] = BaseConfigDerived()
502 opts.ca[ca_name].populate(confparser,a_section)
503 opts.ca[ca_name].cakey = None
504 opts.ca[ca_name].cacert = None
505
506 return opts