1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import os
18 import re
19 import socket
20
21 from logging import getLogger
22 from iniparse import INIConfig
23 from iniparse.config import Undefined
24
25 from gofer import NAME, Singleton
26
27 log = getLogger(__name__)
28
29
30
33
34 Undefined.__getattr__ = _undefined
35
36
38 """
39 Section/property not defined.
40 @param x: A section/property
41 @type x: A section or property object.
42 @return: True if not defined.
43 """
44 return isinstance(x, Undefined)
45
47 """
48 Not define value.
49 @param x: An object to check.
50 @type x: A section/property
51 @return: d if not defined, else x.
52 """
53 if ndef(x):
54 return d
55 else:
56 return x
57
58
59 -class Base(INIConfig):
60 """
61 Base configuration.
62 Uses L{Reader} which provides import.
63 """
64
66 """
67 @param path: The path to an INI file.
68 @type path: str
69 """
70 fp = Reader(path)
71 try:
72 INIConfig.__init__(self, fp)
73 finally:
74 fp.close()
75
76
78 """
79 The gofer agent configuration.
80 @cvar ROOT: The root configuration directory.
81 @type ROOT: str
82 @cvar PATH: The absolute path to the config directory.
83 @type PATH: str
84 @cvar USER: The path to an alternate configuration file
85 within the user's home.
86 @type USER: str
87 @cvar ALT: The environment variable with a path to an alternate
88 configuration file.
89 @type ALT: str
90 """
91 __metaclass__ = Singleton
92
93 ROOT = '/etc/%s' % NAME
94 FILE = 'agent.conf'
95 PATH = os.path.join(ROOT, FILE)
96 USER = os.path.join('~/.%s' % NAME, FILE)
97 CNFD = os.path.join(ROOT, 'conf.d')
98 ALT = '%s_OVERRIDE' % NAME.upper()
99
101 """
102 Open the configuration.
103 Merge (in) alternate configuration file when specified
104 by environment variable.
105 """
106 try:
107 Base.__init__(self, self.PATH)
108 self.__addconfd()
109 altpath = self.__altpath()
110 if altpath:
111 alt = Base(altpath)
112 self.__mergeIn(alt)
113 log.info('merged[in]:%s\n%s', altpath, self)
114 except:
115 log.error(self.PATH, exc_info=1)
116 raise
117
119 """
120 Update with the specified I{other} configuration.
121 @param other: The conf to update with.
122 @type other: Base
123 @return: self
124 @rtype: L{Config}
125 """
126 for section in other:
127 for key in other[section]:
128 self[section][key] = other[section][key]
129 return self
130
132 """
133 Merge (in) the specified I{other} configuration.
134 @param other: The conf to merge in.
135 @type other: Base
136 @return: self
137 @rtype: L{Config}
138 """
139 for section in other:
140 if section not in self:
141 continue
142 for key in other[section]:
143 self[section][key] = other[section][key]
144 return self
145
147 """
148 Merge (out) to the specified I{other} configuration.
149 @param other: The conf to merge out.
150 @type other: Base
151 @return: self
152 @rtype: L{Config}
153 """
154 for section in other:
155 if section not in self:
156 continue
157 for key in other[section]:
158 other[section][key] = self[section][key]
159 return self
160
162 """
163 Write the configuration.
164 """
165 altpath = self.__altpath()
166 if altpath:
167 alt = self.__read(altpath)
168 self.__mergeOut(alt)
169 log.info('merge[out]:%s\n%s', altpath, alt)
170 path = altpath
171 s = str(alt)
172 else:
173 path = self.PATH
174 s = str(self)
175 fp = open(path, 'w')
176 try:
177 fp.write(s)
178 finally:
179 fp.close()
180
182 """
183 Get the I{alternate} configuration path.
184 Resolution order: ALT, USER
185 @return: The path to the alternate configuration file.
186 @rtype: str
187 """
188 path = os.environ.get(self.ALT)
189 if path:
190 return path
191 path = os.path.expanduser(self.USER)
192 if os.path.exists(path):
193 return path
194 else:
195 None
196
198 """
199 Read and merge the conf.d files.
200 """
201 for fn in os.listdir(self.CNFD):
202 path = os.path.join(self.CNFD, fn)
203 cfg = Base(path)
204 self.__update(cfg)
205 log.info('updated with: %s\n%s', path, self)
206
207
209 """
210 Import property specification.
211 @ivar pattern: The regex for property specification.
212 @type pattern: I{regex.pattern}
213 @ivar vdict: The variable dictionary.
214 @type vdict: dict
215 @ivar plain: The list of I{plan} properties to import.
216 @type plain: [str,..]
217 """
218
219 pattern = re.compile('([^(]+)(\()([^)]+)(\))')
220
222 """
223 @param properties: A list of property specifications.
224 @type properties: [str,..]
225 """
226 self.vdict = {}
227 self.plain = []
228 for p in properties:
229 if not p:
230 continue
231 m = self.pattern.match(p)
232 if m:
233 key = m.group(1).strip()
234 value = m.group(3).strip()
235 self.vdict[key] = value
236 else:
237 self.plain.append(p)
238
240 """
241 Get whether a property is I{plain} and is to be imported.
242 @param property: A property name.
243 @type property: str
244 @return: True when property is to be imported.
245 @rtype: bool
246 """
247 return ( property in self.plain )
248
249 - def var(self, property):
250 """
251 Get the property's declared variable name.
252 @param property: A property name.
253 @type property: str
254 @return: The variable name declared for the property
255 or None when not declared.
256 @rtype: str
257 """
258 return self.vdict.get(property)
259
261 """
262 Get whether the object is empty.
263 @return: True no properties defined.
264 @rtype: bool
265 """
266 return ( len(self) == 0 )
267
269 keys = self.vdict.keys()
270 keys += self.plain
271 return iter(keys)
272
274 return ( len(self.vdict)+len(self.plain) )
275
276
278 """
279 Represents an import directive.
280 @@import:<path>:<section>:<property>,
281 where <property> is: <name>|<name>(<variable>).
282 When the <variable> form is used, a variable is assigned the value
283 to be used as $(var) in the conf rather than imported.
284 @cvar allproperties: An (empty) object representing all properties
285 are to be imported.
286 @type allproperties: L{Properties}
287 @ivar path: The path to the imported ini file.
288 @type path: str
289 @ivar section: The name of the section to be imported.
290 @type section: str
291 @ivar properties: The property specification.
292 @type properties: L{Properties}
293 """
294
295 allproperties = Properties()
296
298 """
299 @param imp: An import directive.
300 @type imp: str
301 """
302 part = imp.split(':')
303 self.path = part[1]
304 self.section = None
305 self.properties = self.allproperties
306 if len(part) > 2:
307 self.section = part[2].strip()
308 if len(part) > 3:
309 plist = [s.strip() for s in part[3].split(',')]
310 self.properties = Properties(plist)
311
313 """
314 Execute the import directive.
315 @return: The (imported) lines & declared (vdict) variables.
316 @rtype: tuple(<imported>,<vdict>)
317 """
318 vdict = {}
319 input = Base(self.path)
320 if not self.section:
321 return (input, vdict)
322 imported = INIConfig()
323 S = input[self.section]
324 if ndef(S):
325 raise Exception, '[%s] not found in %s' % (self.section, self.path)
326 for k in S:
327 v = input[self.section][k]
328 if self.properties.empty() or self.properties.isplain(k):
329 ts = getattr(imported, self.section)
330 setattr(ts, k, v)
331 else:
332 var = self.properties.var(k)
333 if var:
334 var = '$(%s)' % var.strip()
335 vdict[var] = v
336 return (imported, vdict)
337
338
340 """
341 configuration macro resolver
342 @return: The resolved configuration value
343 @rtype: str
344 """
345 n = macro[2:-1]
346 cfg = Config()
347 s,p = n.split('.',1)
348 return nvl(cfg[s][p])
349
350
352 """
353 File reader.
354 post-process directives.
355 @ivar idx: The line index.
356 @type idx: int
357 @ivar vdict: The variable dictionary.
358 @type vdict: dict
359 @ivar path: The path to a file to read.
360 @type path: str
361 """
362
363 MACROS = {
364 '%{hostname}':socket.gethostname(),
365 '%{messaging.url}':_cnfvalue,
366 '%{messaging.cacert}':_cnfvalue,
367 '%{messaging.clientcert}':_cnfvalue,
368 }
369
371 self.idx = 0
372 self.vdict = {}
373 self.path = path
374 log.info('reading: %s', path)
375 f = open(path)
376 try:
377 bfr = f.read()
378 self.lines = self.__post(bfr.split('\n'))
379 finally:
380 f.close()
381
383 """
384 read the next line.
385 @return: The line of None on EOF.
386 @rtype: str
387 """
388 if self.idx < len(self.lines):
389 ln = self.lines[self.idx]
390 self.idx += 1
391 return ln+'\n'
392
395
396 - def __post(self, input):
397 """
398 Post-process file content for directives.
399 @param input: The file content (lines).
400 @type input: list
401 @return: The post processed content.
402 @rtype: list
403 """
404 output = []
405 for ln in input:
406 if ln.startswith('@import'):
407 for ln in self.__import(ln):
408 output.append(ln)
409 else:
410 ln = self.__repl(ln)
411 output.append(ln)
412 return output
413
415 """
416 Procecss an i{import} directive and return the result.
417 @param ln: A line containing the directive.
418 @type ln: str
419 @return: The import result (lines).
420 @rtype: [str,..]
421 """
422 log.info('processing: %s', ln)
423 imp = Import(ln)
424 imported, vdict = imp()
425 self.vdict.update(vdict)
426 return str(imported).split('\n')
427
429 """
430 Replace variables contained in the line.
431 @param ln: A file line.
432 @type ln: str
433 @return: The line w/ vars replaced.
434 @rtype: str
435 """
436 if ln.startswith('#'):
437 return ln
438 for k,v in self.MACROS.items()+self.vdict.items():
439 if k in ln:
440 if callable(v):
441 v = v(k)
442 log.info('line "%s" s/%s/%s/', ln, k, v)
443 ln = ln.replace(k, v)
444 return ln
445
448
449
450 if __name__ == '__main__':
451 cfg = Config()
452 print cfg
453