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