Package backend :: Package common :: Module rhnConfig
[hide private]
[frames] | no frames]

Source Code for Module backend.common.rhnConfig

  1  # 
  2  # Copyright (c) 2008--2017 Red Hat, Inc. 
  3  # 
  4  # This software is licensed to you under the GNU General Public License, 
  5  # version 2 (GPLv2). There is NO WARRANTY for this software, express or 
  6  # implied, including the implied warranties of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 
  8  # along with this software; if not, see 
  9  # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. 
 10  # 
 11  # Red Hat trademarks are not licensed under GPLv2. No permission is 
 12  # granted to use or replicate Red Hat trademarks that are incorporated 
 13  # in this software or its documentation. 
 14  # 
 15   
 16  import os 
 17  import sys 
 18  import glob 
 19  import stat 
 20  import re 
 21   
 22  from rhn.UserDictCase import UserDictCase 
 23  from spacewalk.common.usix import raise_with_tb 
 24   
 25  # bare-except and broad-except 
 26  # pylint: disable=W0702,W0703 
 27   
 28  _CONFIG_ROOT = '/etc/rhn' 
 29  _CONFIG_FILE = '%s/rhn.conf' % _CONFIG_ROOT 
 30  _CONFIG_DEFAULTS_ROOT = '/usr/share/rhn/config-defaults' 
 31   
 32   
33 -def warn(*args):
34 """ 35 Function used for debugging purposes 36 """ 37 sys.stderr.write("CONFIG PARSE WARNING: %s\n" % " ".join(map(str, args)))
38 39
40 -class ConfigParserError(Exception):
41 42 """ 43 Exception class we're using to expose fatal errors 44 """ 45 pass
46 47 48 # TODO: need to be able to specify "" component and parse all files in 49 # the directory and form a _complete_ mapping structure. 50 # Or, if that is too difficult, take in a list of components...
51 -class RHNOptions:
52 53 """ Main options class 54 The basic idea is to share the important pieces of information - the 55 component and the configuration tree - across all instances of this 56 class. 57 """ 58
59 - def __init__(self, component=None, root=None, filename=None):
60 self.__component = None 61 # Defaults for each option, keyed on tuples 62 self.__defaults = {} 63 # Parsed config file, keyed on tuples 64 self.__parsedConfig = {} 65 # Dictionary used as a cache (to avoid looking up options all over the 66 # place). Keyed on strings (component names) 67 self.__configs = {} 68 # Last modification date for the config file 69 self.__timestamp = 0 70 # NOTE: root: root directory location of config files. 71 self.root = None 72 self.filename = None 73 self.init(component, root, filename)
74
75 - def init(self, component, root=None, filename=None):
76 """ 77 Visible function, so that we can re-init the object without 78 losing the reference to it 79 """ 80 if root is None: 81 root = _CONFIG_ROOT 82 self.filename = filename 83 if self.filename is None: 84 self.filename = _CONFIG_FILE 85 self.setComponent(component) 86 self.root = root
87
88 - def setComponent(self, comp):
89 if not comp: 90 comp = () 91 self.__component = comp
92
93 - def getComponent(self):
94 return self.__component
95
96 - def is_initialized(self):
97 return (self.__component is not None) and \ 98 self.__component in self.__configs
99
100 - def modifiedYN(self):
101 """returns last modified time diff if rhn.conf has changed.""" 102 103 try: 104 si = os.stat(self.filename) 105 except OSError: 106 e = sys.exc_info()[1] 107 if e[0] == 13: #Error code 13 - Permission denied 108 sys.stderr.write("ERROR: must be root to execute\n") 109 else: 110 sys.stderr.write("ERROR: " + self.filename + " is not accesible\n") 111 sys.exit(e[0]) 112 lm = si[stat.ST_MTIME] 113 # should always be positive, but a non-zero result is still 114 # indication that the file has changed. 115 return lm - self.__timestamp
116
117 - def updateLastModified(self, timeDiff=None):
118 """ update the last modified time of the rhn.conf file. """ 119 if timeDiff is None: 120 timeDiff = self.modifiedYN() 121 self.__timestamp = self.__timestamp + timeDiff
122
123 - def parse(self):
124 """ 125 This function parses the config file, if needed, and populates 126 the configuration cache self.__configs 127 """ 128 # Speed up the most common case 129 timeDiff = self.modifiedYN() 130 if not timeDiff and self.is_initialized(): 131 # Nothing to do: the config file did not change and we already 132 # have the config cached 133 return 134 else: 135 # if the timestamp changed, clear the list of cached configs 136 # and retain the new timestamp 137 self.updateLastModified(timeDiff) 138 self.__configs.clear() # cache cleared 139 140 # parse the defaults. 141 self._parseDefaults(allCompsYN=0) 142 143 # Now that we parsed the defaults, we parse the multi-key 144 # self.filename configuration (ie, /etc/rhn/rhn.conf) 145 self.__parsedConfig = parse_file(self.filename) 146 147 # And now generate and cache the current component 148 self.__merge()
149
150 - def _parseDefaults(self, allCompsYN=0):
151 """ Parsing of the /usr/share/rhn/config-defaults/*.conf (or equivalent) 152 Make sure we have all the needed default config files loaded 153 We store the defaults in a dictionary, keyed on the component tuple 154 """ 155 comps = parse_comps(self.__component) 156 if allCompsYN: 157 comps = getAllComponents_tuples() 158 for comp in comps: 159 if comp in self.__defaults: 160 # We already have it loaded 161 # XXX: Should we do timestamp checking for this one too? 162 continue 163 # Create the config file name 164 conffile = "%s/rhn.conf" % (_CONFIG_DEFAULTS_ROOT) 165 if comp: 166 conffile = "%s/rhn_%s.conf" % (_CONFIG_DEFAULTS_ROOT, 167 '_'.join(comp)) 168 # if the file is not there (or can't be read), skip 169 if not os.access(conffile, os.R_OK): 170 warn("File not found or can't be read", conffile) 171 continue 172 # store this default set of values 173 _dict = parse_file(conffile, single_key=1) 174 # the parsed file is keyed by component, but for a config 175 # file containing only single keys we know the component 176 # is going to be () and we need to override it with 177 # whatever we're parsing now in the self.__defaults table 178 def_dict = {} 179 for k in _dict[()].keys(): 180 # we extract just the values and dump the line number 181 # from the (values,linno) tuples which is the hash 182 # value for _dict[()][k] 183 def_dict[k] = _dict[()][k][0] 184 self.__defaults[comp] = def_dict
185
186 - def keys(self):
187 self.__check() 188 return list(self.__configs[self.__component].keys())
189
190 - def has_key(self, key):
191 self.__check() 192 return key in self.__configs[self.__component]
193
194 - def values(self):
195 self.__check() 196 return list(self.__configs[self.__component].values())
197
198 - def items(self):
199 self.__check() 200 return list(self.__configs[self.__component].items())
201
202 - def set(self, key, value):
203 self.__check() 204 self.__configs[self.__component][key] = value
205 __setitem__ = set 206
207 - def show(self):
208 self.__check() 209 # display the configuration read from the file(s) and exit 210 vals = list(self.__configs[self.__component].items()) 211 vals.sort(key=lambda a: a[0]) 212 for k, v in vals: 213 if v is None: 214 v = "" 215 print("%-20s = %s" % (k, v))
216 217 # polymorphic methods 218
219 - def __getattr__(self, key):
220 """fetch option you want in a self.DEBUG kind of syntax 221 (can force component selection) 222 223 e.g.: say for example we have an option proxy.debug = 5 224 stored in the dictionary. proxy just says that only proxy 225 can access this option. So for this exmple, 226 self.__component is proxy. 227 cfg = RHNOptions("proxy") 228 print cfg.DEBUG ---> yields 5 229 """ 230 self.__check() 231 if key not in self.__configs[self.__component]: 232 raise AttributeError(key) 233 return self.__configs[self.__component][key]
234 __getitem__ = __getattr__ 235
236 - def get(self, key, default=None):
237 ret = default 238 if key in self.__configs[self.__component]: 239 ret = self.__configs[self.__component][key] 240 return ret
241
242 - def __str__(self):
243 s = "Uninitialized" 244 if self.__component and self.__component in self.__configs: 245 s = str(self.__configs[self.__component]) 246 return "<RHNOptions instance at %s: %s>" % (id(self), s)
247 __repr__ = __str__ 248 249 # private methods 250
251 - def __check(self):
252 if not self.is_initialized(): 253 raise ConfigParserError("Uninitialized config for component", 254 self.__component)
255
256 - def __merge(self, component=None):
257 """ 258 merge the config options between the default comp dictionaries 259 and the file we're parsing now 260 """ 261 # Caches this component's configuration options 262 if component is None: 263 component = self.__component 264 265 opts = UserDictCase() 266 comps = parse_comps(component) 267 for comp in comps: 268 if comp not in self.__defaults: 269 warn('key not found in config default dict', comp) 270 continue 271 opts.update(self.__defaults[comp]) 272 273 # Now load the specific stuff, and perform syntax checking too 274 for comp in comps: 275 if comp not in self.__parsedConfig: 276 # No such entry in the config file 277 continue 278 for key, (values, _lineno_) in self.__parsedConfig[comp].items(): 279 # we don't really want to force every item in the 280 # config file to have a default value first. If we do, 281 # uncomment this section 282 # if not opts.has_key(key): # Unknown keyword 283 # warn("Warning: in file %s, line %s: unknown " 284 # "option name `%s'" % (self.filename, lineno, key)) 285 # continue 286 opts[key] = values 287 # and now save it 288 self.__configs[component] = opts
289 290 # protected/test methods 291
292 - def getDefaults(self):
293 """returns the __defaults dict (dictionary of parsed defaults). 294 """ 295 self.__check() 296 return self.__defaults
297
298 - def _getParsedConfig(self):
299 """returns the __parsedConfig dict (dictionary of parsed 300 /etc/rhn/rhn.conf file). 301 """ 302 self.__check() 303 return self.__parsedConfig
304
305 - def _getConfigs(self):
306 """returns the __configs dict (dictionary of the merged options 307 keyed by component. 308 """ 309 self.__check() 310 return self.__configs
311
312 - def showall(self):
313 from pprint import pprint 314 print("__defaults: dictionary of parsed defaults.") 315 pprint(self.__defaults) 316 print("") 317 print("__parsedConfig: dictionary of parsed /etc/rhn/rhn.conf file.") 318 pprint(self.__parsedConfig) 319 print("") 320 print("__configs: dictionary of the merged options keyed by component.") 321 pprint(self.__configs)
322 323
324 -def parse_comps(component):
325 """ 326 Splits a component name (a.b.c) into a list of tuples that can be 327 joined together to determine a config file name 328 Eg. a.b.c --> [(), ('a',), ('a','b'), ('a','b','c')] 329 """ 330 # Split the component name on '.' 331 if not component: 332 return [()] 333 comps = [c.lower() for c in component.split('.')] 334 # Now generate the prefixes for this component 335 return [tuple(comps[:i]) for i in range(len(comps) + 1)]
336 337
338 -def parse_line(line):
339 """ 340 Parse a config line... 341 Returns a tuple (keys, values), or (None, None) if we don't care 342 about this line 343 """ 344 varSeparator = '.' 345 optSeparator = ',' 346 347 def sanitize_value(key, val): 348 """ 349 attempt to convert a string value to the proper type 350 """ 351 converTable = {'proxy.http_proxy_username': str, 352 'proxy.http_proxy_password': str, 353 'server.satellite.http_proxy_username': str, 354 'server.satellite.http_proxy_password': str, 355 'server.satellite.rhn_parent': str, 356 'db_name': str, 357 'db_user': str, 358 'db_password': str, 359 'db_host': str} 360 val = val.strip() 361 362 if converTable.get(key): 363 try: 364 val = converTable.get(key)(val) 365 except ValueError: 366 pass 367 else: 368 try: 369 val = int(val) # make int if can. 370 except ValueError: 371 try: 372 val = float(val) # make float if can. 373 except ValueError: 374 pass 375 if val == '': # Empty strings treated as None 376 val = None 377 return val
378 379 # Skip empty and comment-only lines 380 if re.match(r'[ \t]*(#|$)', line): 381 return (None, None) 382 383 # now split it into keys and values. We allow for max one 384 # split/cut (the first one) 385 (keys, vals) = [c.strip() for c in line.split('=', 1)] 386 387 # extract the keys, convert to lowercase 388 keys = keys.lower() 389 if not keys: 390 raise ConfigParserError("Missing Key = expression") 391 392 # extract the values, preserving case 393 if not vals: 394 keys = keys.split(varSeparator) 395 return (keys, None) 396 # split and sanitize 397 vals = list(map(sanitize_value, [keys] * len(vals.split(optSeparator)), 398 vals.split(optSeparator))) 399 if len(vals) == 1: 400 # Single value 401 vals = vals[0] 402 keys = keys.split(varSeparator) 403 # and now return our findings 404 return (keys, vals) 405 406
407 -def parse_file(filename, single_key=0):
408 """ 409 parse a config file (read it in, parse its lines) 410 """ 411 lines = read_file(filename) 412 # the base case, an empty tuple component, is always present. 413 ret = {(): {}} 414 lineno = 0 415 # okay, read the file, parse the lines one by one 416 for line in lines: 417 # lineno is 1-based 418 lineno = lineno + 1 419 try: 420 (keys, values) = parse_line(line) 421 except: 422 raise_with_tb(ConfigParserError("Parse Error: <%s:%s>: '%s'" % ( 423 filename, lineno, line)), sys.exc_info()[2]) 424 if keys is None: # We don't care about this line 425 continue 426 # now process the parsed line 427 if single_key and len(keys) > 1: 428 # Error, we should not have more than one key in the this 429 # config file 430 # raise ConfigParserError("Parse Error: <%s:%s>: too many keys" 431 # % (filename, lineno)) 432 # let's fix the faulty config=file setup... 433 # XXX: needs more testing!!! (2003-04-17) 434 del keys[:-1] 435 # Store this line in a dictionary filled by component 436 comp = tuple(keys[:-1]) 437 key = keys[-1] 438 if comp not in ret: 439 # Don't make it a UserDictCase since we know exactly we 440 # already used string.lower 441 ret[comp] = {} 442 ret[comp][key] = (values, lineno) 443 return ret
444 445
446 -def read_file(filename):
447 """ 448 reads a text config file and returns its lines in a list 449 """ 450 try: 451 lines = open(filename, 'rb').readlines() 452 new_lines = [] 453 combined = '' 454 for line in lines: 455 # if the line isn't part of a multiline, lets add it 456 if line.find('\\\n') < 0: 457 combined = combined + line 458 new_lines.append(combined) 459 combined = '' 460 else: 461 combined = combined + line.replace('\\\n', ' ') 462 return new_lines 463 except (IOError, OSError): 464 e = sys.exc_info()[1] 465 raise_with_tb(ConfigParserError("Can not read config file", filename, e.args[1]), sys.exc_info()[2])
466 467
468 -def getAllComponents_tree(defaultDir=None):
469 """Figure out all components and return them in a tree-like structure 470 471 {'server', {'server.app':{}, 472 'server.satellite':{}, 473 'server.applet':{}, 'server.bugzilla':{}, 474 'server.iss':{}, 'server.xmlrpc':{}, 'server.xp':{}}, 475 'web': {}, 476 'tools': {}} 477 478 NOTE: this was begging for recursion... I avoided that like the plague 479 """ 480 481 if defaultDir is None: 482 defaultDir = _CONFIG_DEFAULTS_ROOT 483 comps = glob.glob('%s/*.conf' % defaultDir) 484 compTree = {} 485 for comp in comps: 486 comp = os.path.basename(comp) 487 comp = comp[:comp.find('.')] # left of .conf 488 parts = comp.split('_')[1:] # strip off that rhn_ 489 if not parts: 490 continue 491 d = compTree 492 for i in range(len(parts)): 493 key = '.'.join(parts[:i + 1]) 494 if key not in d: 495 d[key] = {} 496 d = d[key] 497 return compTree
498 499
500 -def getAllComponents(defaultDir=None, compsTree=None):
501 """recursively flattens the results of getAllComponents_tree returning 502 a list of all components""" 503 504 if compsTree is None: 505 compsTree = getAllComponents_tree(defaultDir) 506 l = [] 507 for k, v in compsTree.items(): 508 l.extend(getAllComponents(None, v)) 509 l.append(k) 510 return l
511 512
513 -def getAllComponents_tuples(defaultDir=None):
514 """returns a list of ALL components in the tuple-ified format: 515 E.g., [(), ('a',), ('a','b'), ('a','b','c'), ...] 516 """ 517 comps = getAllComponents(defaultDir) 518 d = {} 519 for comp in comps: 520 for c in parse_comps(comp): 521 d[c] = None 522 return list(d.keys())
523 524 525 CFG = RHNOptions() 526 527
528 -def initCFG(component=None, root=None, filename=None):
529 """ 530 Main entry point here 531 """ 532 # NOTE: root: root directory location of config files. 533 CFG.init(component, root, filename) 534 CFG.parse()
535 536 ALL_CFG = RHNOptions('') 537 ALL_CFG.parse() 538 PRODUCT_NAME = ALL_CFG.PRODUCT_NAME 539 540
541 -def runTest():
542 print("Test script:") 543 import pprint 544 print("Component tree of all installed components:") 545 pprint.pprint(getAllComponents_tree()) 546 print("") 547 test_cfg = RHNOptions(sys.argv[1]) 548 # test_cfg = RHNOptions('server.app') 549 # test_cfg = RHNOptions('proxy.broker') 550 # test_cfg = RHNOptions('proxy.redirect', _CONFIG_ROOT) 551 # test_cfg = RHNOptions('proxy.redirect', '/tmp') 552 # test_cfg.filename = 'empty.conf' 553 test_cfg.parse() 554 print("=============== the object's repr ================================") 555 print(test_cfg) 556 print("=============== the object's defaults ============================") 557 pprint.pprint(test_cfg.getDefaults()) 558 print("=============== an erronous lookup example =======================") 559 print("testing __getattr__") 560 try: 561 print(test_cfg.lkasjdfxxxxxxxxxxxxxx) 562 except AttributeError: 563 e = sys.exc_info()[1] 564 print('Testing: "AttributeError: %s"' % e) 565 print("") 566 print("=============== the object's merged settings ======================") 567 test_cfg.show() 568 print("=============== dump of all relevant dictionaries =================") 569 test_cfg.showall() 570 print("===================================================================")
571 572 573 #------------------------------------------------------------------------------ 574 # Usage: rhnConfig.py [ { get | list } component [ key ] ] 575 # No args assumes test mode. 576 577 578 if __name__ == "__main__": 579 do_list = 0 580 comp_arg = None 581 key_arg = None 582 583 if len(sys.argv) == 4 and sys.argv[1] == "get": 584 comp_arg = sys.argv[2] 585 key_arg = sys.argv[3] 586 elif len(sys.argv) == 3 and sys.argv[1] == "list": 587 comp_arg = sys.argv[2] 588 do_list = 1 589 else: 590 # Assume test mode. 591 runTest() 592 sys.exit(1) 593 594 cfg = RHNOptions(comp_arg) 595 cfg.parse() 596 597 if do_list: 598 cfg.show() 599 else: 600 print(cfg.get(key_arg)) 601