Package flumotion :: Package common :: Module config
[hide private]

Source Code for Module flumotion.common.config

   1  # -*- Mode: Python; test-case-name: flumotion.test.test_config -*- 
   2  # vi:si:et:sw=4:sts=4:ts=4 
   3  # 
   4  # Flumotion - a streaming media server 
   5  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
   6  # All rights reserved. 
   7   
   8  # This file may be distributed and/or modified under the terms of 
   9  # the GNU General Public License version 2 as published by 
  10  # the Free Software Foundation. 
  11  # This file is distributed without any warranty; without even the implied 
  12  # warranty of merchantability or fitness for a particular purpose. 
  13  # See "LICENSE.GPL" in the source distribution for more information. 
  14   
  15  # Licensees having purchased or holding a valid Flumotion Advanced 
  16  # Streaming Server license may use this file in accordance with the 
  17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
  18  # See "LICENSE.Flumotion" in the source distribution for more information. 
  19   
  20  # Headers in this file shall remain intact. 
  21   
  22  """ 
  23  parsing of configuration files 
  24  """ 
  25   
  26  import os 
  27  import locale 
  28  import sys 
  29  from xml.dom import minidom, Node 
  30  from xml.parsers import expat 
  31   
  32  from twisted.python import reflect 
  33   
  34  from flumotion.common import log, errors, common, registry, fxml 
  35  from flumotion.configure import configure 
  36   
  37  from errors import ConfigError 
  38   
39 -def _ignore(*args):
40 pass
41
42 -def upgradeEaters(conf):
43 def parseFeedId(feedId): 44 if feedId.find(':') == -1: 45 return "%s:default" % feedId 46 else: 47 return feedId
48 49 eaterConfig = conf.get('eater', {}) 50 sourceConfig = conf.get('source', []) 51 if eaterConfig == {} and sourceConfig != []: 52 eaters = registry.getRegistry().getComponent( 53 conf.get('type')).getEaters() 54 eatersDict = {} 55 eatersTuple = [(None, parseFeedId(s)) for s in sourceConfig] 56 eatersDict = buildEatersDict(eatersTuple, eaters) 57 conf['eater'] = eatersDict 58 59 if sourceConfig: 60 sources = [] 61 for s in sourceConfig: 62 sources.append(parseFeedId(s)) 63 conf['source'] = sources 64
65 -def upgradeAliases(conf):
66 eaters = dict(conf.get('eater', {})) # a copy 67 concat = lambda lists: reduce(list.__add__, lists, []) 68 if not reduce(lambda x,y: y and isinstance(x, tuple), 69 concat(eaters.values()), 70 True): 71 for eater in eaters: 72 aliases = [] 73 feeders = eaters[eater] 74 for i in range(len(feeders)): 75 val = feeders[i] 76 if isinstance(val, tuple): 77 feedId, alias = val 78 aliases.append(val[1]) 79 else: 80 feedId = val 81 alias = eater 82 while alias in aliases: 83 log.warning('config', "Duplicate alias %s for " 84 "eater %s, uniquifying", alias, eater) 85 alias += '-bis' 86 aliases.append(alias) 87 feeders[i] = (feedId, val) 88 conf['eater'] = eaters
89 90 UPGRADERS = [upgradeEaters, upgradeAliases] 91 CURRENT_VERSION = len(UPGRADERS) 92
93 -def buildEatersDict(eatersList, eaterDefs):
94 """Build a eaters dict suitable for forming part of a component 95 config. 96 97 @param eatersList: List of eaters. For example, 98 [('default', 'othercomp:feeder', 'foo')] says 99 that our eater 'default' will be fed by the feed 100 identified by the feedId 'othercomp:feeder', and 101 that it has the alias 'foo'. Alias is optional. 102 @type eatersList: List of (eaterName, feedId, eaterAlias?) 103 @param eaterDefs: The set of allowed and required eaters 104 @type eaterDefs: List of 105 L{flumotion.common.registry.RegistryEntryEater} 106 @returns: Dict of eaterName => [(feedId, eaterAlias)] 107 """ 108 def parseEaterTuple(tup): 109 def parse(eaterName, feedId, eaterAlias=None): 110 if eaterAlias is None: 111 eaterAlias = eaterName 112 return (eaterName, feedId, eaterAlias)
113 return parse(*tup) 114 115 eaters = {} 116 for eater, feedId, alias in [parseEaterTuple(t) for t in eatersList]: 117 if eater is None: 118 if not eaterDefs: 119 raise ConfigError("Feed %r cannot be connected, component has " 120 "no eaters" % (feedId,)) 121 # cope with old <source> entries 122 eater = eaterDefs[0].getName() 123 if alias is None: 124 alias = eater 125 feeders = eaters.get(eater, []) 126 if feedId in feeders: 127 raise ConfigError("Already have a feedId %s eating " 128 "from %s", feedId, eater) 129 while alias in [a for f, a in feeders]: 130 log.warning('config', "Duplicate alias %s for eater %s, " 131 "uniquifying", alias, eater) 132 alias += '-bis' 133 134 feeders.append((feedId, alias)) 135 eaters[eater] = feeders 136 for e in eaterDefs: 137 eater = e.getName() 138 if e.getRequired() and not eater in eaters: 139 raise ConfigError("Component wants to eat on %s," 140 " but no feeders specified." 141 % (e.getName(),)) 142 if not e.getMultiple() and len(eaters.get(eater, [])) > 1: 143 raise ConfigError("Component does not support multiple " 144 "sources feeding %s (%r)" 145 % (eater, eaters[eater])) 146 aliases = reduce(list.__add__, 147 [[x[1] for x in tups] for tups in eaters.values()], 148 []) 149 # FIXME: Python 2.3 has no sets 150 # if len(aliases) != len(set(aliases): 151 while aliases: 152 alias = aliases.pop() 153 if alias in aliases: 154 raise ConfigError("Duplicate alias: %s" % alias) 155 156 return eaters 157
158 -def parsePropertyValue(propName, type, value):
159 # XXX: We might end up calling float(), which breaks 160 # when using LC_NUMERIC when it is not C -- only in python 161 # 2.3 though, no prob in 2.4. See PEP 331 162 if sys.version_info < (2, 4): 163 locale.setlocale(locale.LC_NUMERIC, "C") 164 def tryStr(s): 165 try: 166 return str(s) 167 except UnicodeEncodeError: 168 return s
169 def strWithoutNewlines(s): 170 return tryStr(' '.join([x.strip() for x in s.split('\n')])) 171 def fraction(v): 172 def frac(num, denom=1): 173 return int(num), int(denom) 174 if isinstance(v, basestring): 175 return frac(*v.split('/')) 176 else: 177 return frac(*v) 178 def boolean(v): 179 if isinstance(v, bool): 180 return v 181 return common.strToBool(v) 182 183 try: 184 # yay! 185 return {'string': strWithoutNewlines, 186 'rawstring': tryStr, 187 'int': int, 188 'long': long, 189 'bool': boolean, 190 'float': float, 191 'fraction': fraction}[type](value) 192 except KeyError: 193 raise ConfigError("unknown type '%s' for property %s" 194 % (type, propName)) 195 except Exception, e: 196 raise ConfigError("Error parsing property '%s': '%s' does not " 197 "appear to be a valid %s.\nDebug: %s" 198 % (propName, value, type, 199 log.getExceptionMessage(e))) 200
201 -def parseCompoundPropertyValue(name, definition, value):
202 if isinstance(value, (list, tuple)): 203 try: 204 parsed = buildPropertyDict(value, definition.getProperties()) 205 except ConfigError, ce: 206 m = ('(inside compound-property %r) %s' % 207 (name, ce.args[0])) 208 raise ConfigError(m) 209 # elif isinstance(value, basestring): 210 # FIXME: parse the string representation of the compound property? 211 # pass 212 else: 213 raise ConfigError('simple value specified where compound property' 214 ' (name=%r) expected' % (name,)) 215 return parsed
216
217 -def buildPropertyDict(propertyList, propertySpecList):
218 """Build a property dict suitable for forming part of a component 219 config. 220 221 @param propertyList: List of property name-value pairs. For example, 222 [('foo', 'bar'), ('baz', 3)] defines two 223 property-value pairs. The values will be parsed 224 into the appropriate types, this it is allowed 225 to pass the string '3' for an int value. 226 @type propertyList: List of (name, value) 227 @param propertySpecList: The set of allowed and required properties 228 @type propertySpecList: List of 229 L{flumotion.common.registry.RegistryEntryProperty} 230 """ 231 ret = {} 232 prop_specs = dict([(x.name, x) for x in propertySpecList]) 233 for name, value in propertyList: 234 if not name in prop_specs: 235 raise ConfigError('unknown property %s' % (name,)) 236 definition = prop_specs[name] 237 238 if isinstance(definition, registry.RegistryEntryCompoundProperty): 239 parsed = parseCompoundPropertyValue(name, definition, value) 240 else: 241 if isinstance(value, (list, tuple)): 242 raise ConfigError('compound value specified where simple' 243 ' property (name=%r) expected' % (name,)) 244 parsed = parsePropertyValue(name, definition.type, value) 245 if definition.multiple: 246 vals = ret.get(name, []) 247 vals.append(parsed) 248 ret[name] = vals 249 else: 250 if name in ret: 251 raise ConfigError("multiple value specified but not " 252 "allowed for property %s" % (name,)) 253 ret[name] = parsed 254 255 for name, definition in prop_specs.items(): 256 if definition.isRequired() and not name in ret: 257 raise ConfigError("required but unspecified property %s" 258 % (name,)) 259 return ret
260
261 -def buildPlugsSet(plugsList, sockets):
262 """Build a plugs dict suitable for forming part of a component 263 config. 264 265 @param plugsList: List of plugs, as type-propertyList pairs. For 266 example, [('frag', [('foo', 'bar')])] defines a plug 267 of type 'frag', and the propertyList representing 268 that plug's properties. The properties will be 269 validated against the plug's properties as defined 270 in the registry. 271 @type plugsList: List of (type, propertyList) 272 @param sockets: The set of allowed sockets 273 @type sockets: List of str 274 """ 275 ret = {} 276 for socket in sockets: 277 ret[socket] = [] 278 for type, propertyList in plugsList: 279 plug = ConfigEntryPlug(type, propertyList) 280 if plug.socket not in ret: 281 raise ConfigError("Unsupported socket type: %s" 282 % (plug.socket,)) 283 ret[plug.socket].append(plug.config) 284 return ret
285
286 -def buildVirtualFeeds(feedPairs, feeders):
287 """Build a virtual feeds dict suitable for forming part of a 288 component config. 289 290 @param feedPairs: List of virtual feeds, as name-feederName pairs. For 291 example, [('bar:baz', 'qux')] defines one 292 virtual feed 'bar:baz', which is provided by 293 the component's 'qux' feed. 294 @type feedPairs: List of (feedId, feedName) -- both strings. 295 @param feeders: The feeders exported by this component, from the 296 registry. 297 @type feeders: List of str. 298 """ 299 ret = {} 300 for virtual, real in feedPairs: 301 if real not in feeders: 302 raise ConfigError('virtual feed maps to unknown feeder: ' 303 '%s -> %s' % (virtual, real)) 304 try: 305 common.parseFeedId(virtual) 306 except: 307 raise ConfigError('virtual feed name not a valid feedId: %s' 308 % (virtual,)) 309 ret[virtual] = real 310 return ret
311
312 -def dictDiff(old, new, onlyOld=None, onlyNew=None, diff=None, 313 keyBase=None):
314 """Compute the difference between two config dicts. 315 316 @returns: 3 tuple: (onlyOld, onlyNew, diff) where: 317 onlyOld is a list of (key, value), representing key-value 318 pairs that are only in old; 319 onlyNew is a list of (key, value), representing key-value 320 pairs that are only in new; 321 diff is a list of (key, oldValue, newValue), representing 322 keys with different values in old and new; and 323 key is a tuple of strings representing the recursive key 324 to get to a value. For example, ('foo', 'bar') represents 325 the value d['foo']['bar'] on a dict d. 326 """ 327 # key := tuple of strings 328 329 if onlyOld is None: 330 onlyOld = [] # key, value 331 onlyNew = [] # key, value 332 diff = [] # key, oldvalue, newvalue 333 keyBase = () 334 335 for k in old: 336 key = (keyBase + (k,)) 337 if k not in new: 338 onlyOld.append((key, old[k])) 339 elif old[k] != new[k]: 340 if isinstance(old[k], dict) and isinstance(new[k], dict): 341 dictDiff(old[k], new[k], onlyOld, onlyNew, diff, key) 342 else: 343 diff.append((key, old[k], new[k])) 344 345 for k in new: 346 key = (keyBase + (k,)) 347 if k not in old: 348 onlyNew.append((key, new[k])) 349 350 return onlyOld, onlyNew, diff
351
352 -def dictDiffMessageString((old, new, diff), oldLabel='old', 353 newLabel='new'):
354 def ref(label, k): 355 return "%s%s: '%s'" % (label, 356 ''.join(["[%r]" % (subk,) 357 for subk in k[:-1]]), 358 k[-1])
359 360 out = [] 361 for k, v in old: 362 out.append('Only in %s = %r' % (ref(oldLabel, k), v)) 363 for k, v in new: 364 out.append('Only in %s = %r' % (ref(newLabel, k), v)) 365 for k, oldv, newv in diff: 366 out.append('Value mismatch:') 367 out.append(' %s = %r' % (ref(oldLabel, k), oldv)) 368 out.append(' %s = %r' % (ref(newLabel, k), newv)) 369 return '\n'.join(out) 370
371 -class ConfigEntryPlug(log.Loggable):
372 "I represent a <plug> entry in a planet config file"
373 - def __init__(self, type, propertyList):
374 try: 375 defs = registry.getRegistry().getPlug(type) 376 except KeyError: 377 raise ConfigError("unknown plug type: %s" % type) 378 379 self.type = type 380 self.socket = defs.getSocket() 381 self.properties = buildPropertyDict(propertyList, 382 defs.getProperties()) 383 self.config = {'type': self.type, 384 'socket': self.socket, 385 'module-name': defs.entry.getModuleName(), 386 'function-name': defs.entry.getFunction(), 387 'properties': self.properties}
388
389 -class ConfigEntryComponent(log.Loggable):
390 "I represent a <component> entry in a planet config file" 391 nice = 0 392 logCategory = 'config' 393 394 __pychecker__ = 'maxargs=13' 395
396 - def __init__(self, name, parent, type, label, propertyList, plugList, 397 worker, eatersList, isClockMaster, project, version, 398 virtualFeeds=None):
399 self.name = name 400 self.parent = parent 401 self.type = type 402 self.label = label 403 self.worker = worker 404 self.defs = registry.getRegistry().getComponent(self.type) 405 try: 406 self.config = self._buildConfig(propertyList, plugList, 407 eatersList, isClockMaster, 408 project, version, 409 virtualFeeds) 410 except ConfigError, e: 411 # reuse the original exception? 412 e.args = ("While parsing component %s: %s" 413 % (name, log.getExceptionMessage(e)),) 414 raise e
415
416 - def _buildVersionTuple(self, version):
417 if version is None: 418 return configure.versionTuple 419 elif isinstance(version, tuple): 420 assert len(version) == 4 421 return version 422 elif isinstance(version, str): 423 try: 424 def parse(maj, min, mic, nan=0): 425 return maj, min, mic, nan
426 return parse(*map(int, version.split('.'))) 427 except: 428 raise ConfigError("<component> version not " 429 "parseable") 430 raise ConfigError("<component> version not parseable")
431
432 - def _buildConfig(self, propertyList, plugsList, eatersList, 433 isClockMaster, project, version, virtualFeeds):
434 """ 435 Build a component configuration dictionary. 436 """ 437 # clock-master should be either an avatar id or None. 438 # It can temporarily be set to True, and the flow parsing 439 # code will change it to the avatar id or None. 440 config = {'name': self.name, 441 'parent': self.parent, 442 'type': self.type, 443 'config-version': CURRENT_VERSION, 444 'avatarId': common.componentId(self.parent, self.name), 445 'project': project or 'flumotion', 446 'version': self._buildVersionTuple(version), 447 'clock-master': isClockMaster or None, 448 'feed': self.defs.getFeeders(), 449 'properties': buildPropertyDict(propertyList, 450 self.defs.getProperties()), 451 'plugs': buildPlugsSet(plugsList, 452 self.defs.getSockets()), 453 'eater': buildEatersDict(eatersList, 454 self.defs.getEaters()), 455 'source': [tup[1] for tup in eatersList], 456 'virtual-feeds': buildVirtualFeeds(virtualFeeds or [], 457 self.defs.getFeeders())} 458 459 if self.label: 460 # only add a label attribute if it was specified 461 config['label'] = self.label 462 463 if not config['source']: 464 # preserve old behavior 465 del config['source'] 466 # FIXME: verify that config['project'] matches the defs 467 468 return config
469
470 - def getType(self):
471 return self.type
472
473 - def getLabel(self):
474 return self.label
475
476 - def getName(self):
477 return self.name
478
479 - def getParent(self):
480 return self.parent
481
482 - def getConfigDict(self):
483 return self.config
484
485 - def getWorker(self):
486 return self.worker
487
488 -class ConfigEntryFlow:
489 "I represent a <flow> entry in a planet config file"
490 - def __init__(self, name, components):
491 self.name = name 492 self.components = {} 493 for c in components: 494 if c.name in self.components: 495 raise ConfigError('flow %s already has component named %s' 496 % (name, c.name)) 497 self.components[c.name] = c
498
499 -class ConfigEntryManager:
500 "I represent a <manager> entry in a planet config file"
501 - def __init__(self, name, host, port, transport, certificate, bouncer, 502 fludebug, plugs):
503 self.name = name 504 self.host = host 505 self.port = port 506 self.transport = transport 507 self.certificate = certificate 508 self.bouncer = bouncer 509 self.fludebug = fludebug 510 self.plugs = plugs
511
512 -class ConfigEntryAtmosphere:
513 "I represent a <atmosphere> entry in a planet config file"
514 - def __init__(self):
515 self.components = {}
516
517 - def __len__(self):
518 return len(self.components)
519
520 -class BaseConfigParser(fxml.Parser):
521 parserError = ConfigError 522
523 - def __init__(self, file):
524 """ 525 @param file: The file to parse, either as an open file object, 526 or as the name of a file to open. 527 @type file: str or file. 528 """ 529 self.add(file)
530
531 - def add(self, file):
532 """ 533 @param file: The file to parse, either as an open file object, 534 or as the name of a file to open. 535 @type file: str or file. 536 """ 537 try: 538 self.path = os.path.split(file.name)[0] 539 except AttributeError: 540 # for file objects without the name attribute, e.g. StringIO 541 self.path = None 542 543 try: 544 self.doc = self.getRoot(file) 545 except fxml.ParserError, e: 546 raise ConfigError(e.args[0])
547
548 - def getPath(self):
549 return self.path
550
551 - def parsePlugs(self, node):
552 # <plugs> 553 # <plug> 554 # returns: list of (socket, type, properties) 555 self.checkAttributes(node) 556 557 plugs = [] 558 def parsePlug(node): 559 # <plug socket=... type=...> 560 # <property> 561 # FIXME: is it even necessary to have socket specified? 562 # seems not 563 socket, type = self.parseAttributes(node, ('socket', 'type')) 564 properties = [] 565 parsers = {'property': (self._parseProperty, properties.append), 566 'compound-property': (self._parseCompoundProperty, 567 properties.append)} 568 self.parseFromTable(node, parsers) 569 return type, properties
570 571 parsers = {'plug': (parsePlug, plugs.append)} 572 self.parseFromTable(node, parsers) 573 return plugs
574
575 - def _parseFeedId(self, feedId):
576 if feedId.find(':') == -1: 577 return "%s:default" % feedId 578 else: 579 return feedId
580
581 - def parseComponent(self, node, parent, isFeedComponent, 582 needsWorker):
583 """ 584 Parse a <component></component> block. 585 586 @rtype: L{ConfigEntryComponent} 587 """ 588 # <component name="..." type="..." label="..."? worker="..."? 589 # project="..."? version="..."?> 590 # <source>...</source>* 591 # <eater name="...">...</eater>* 592 # <property name="name">value</property>* 593 # <clock-master>...</clock-master>? 594 # <plugs>...</plugs>* 595 # <virtual-feed name="foo" real="bar"/>* 596 # </component> 597 598 attrs = self.parseAttributes(node, ('name', 'type'), 599 ('label', 'worker', 'project', 'version',)) 600 name, type, label, worker, project, version = attrs 601 if needsWorker and not worker: 602 raise ConfigError('component %s does not specify the worker ' 603 'that it is to run on' % (name,)) 604 elif worker and not needsWorker: 605 raise ConfigError('component %s specifies a worker to run ' 606 'on, but does not need a worker' % (name,)) 607 608 properties = [] 609 plugs = [] 610 eaters = [] 611 clockmasters = [] 612 sources = [] 613 virtual_feeds = [] 614 615 def parseBool(node): 616 return self.parseTextNode(node, common.strToBool)
617 parsers = {'property': (self._parseProperty, properties.append), 618 'compound-property': (self._parseCompoundProperty, 619 properties.append), 620 'plugs': (self.parsePlugs, plugs.extend)} 621 622 if isFeedComponent: 623 parsers.update({'eater': (self._parseEater, eaters.extend), 624 'clock-master': (parseBool, clockmasters.append), 625 'source': (self._parseSource, sources.append), 626 'virtual-feed': (self._parseVirtualFeed, 627 virtual_feeds.append)}) 628 629 self.parseFromTable(node, parsers) 630 631 if len(clockmasters) == 0: 632 isClockMaster = None 633 elif len(clockmasters) == 1: 634 isClockMaster = clockmasters[0] 635 else: 636 raise ConfigError("Only one <clock-master> node allowed") 637 638 for feedId in sources: 639 # map old <source> nodes to new <eater> nodes 640 eaters.append((None, feedId)) 641 642 return ConfigEntryComponent(name, parent, type, label, properties, 643 plugs, worker, eaters, 644 isClockMaster, project, version, 645 virtual_feeds) 646
647 - def _parseSource(self, node):
648 return self._parseFeedId(self.parseTextNode(node))
649
650 - def _parseFeed(self, node):
651 alias, = self.parseAttributes(node, (), ('alias',)) 652 feedId = self._parseFeedId(self.parseTextNode(node)) 653 return feedId, alias
654
655 - def _parseEater(self, node):
656 # <eater name="eater-name"> 657 # <feed alias="foo"?>feeding-component:feed-name</feed>* 658 # </eater> 659 name, = self.parseAttributes(node, ('name',)) 660 feeds = [] 661 parsers = {'feed': (self._parseFeed, feeds.append)} 662 self.parseFromTable(node, parsers) 663 if len(feeds) == 0: 664 # we have an eater node with no feeds 665 raise ConfigError( 666 "Eater node %s with no <feed> nodes, is not allowed" % name) 667 return [(name, feedId, alias) for feedId, alias in feeds]
668
669 - def _parseProperty(self, node):
670 name, = self.parseAttributes(node, ('name',)) 671 return name, self.parseTextNode(node, lambda x: x)
672
673 - def _parseCompoundProperty(self, node):
674 # <compound-property name="name"> 675 # <property name="name">value</property>* 676 # <compound-property name="name">...</compound-property>* 677 # </compound-property> 678 name, = self.parseAttributes(node, ('name',)) 679 properties = [] 680 parsers = {'property': (self._parseProperty, properties.append), 681 'compound-property': (self._parseCompoundProperty, 682 properties.append)} 683 self.parseFromTable(node, parsers) 684 return name, properties
685
686 - def _parseVirtualFeed(self, node):
687 # <virtual-feed name="foo" real="bar"/> 688 name, real = self.parseAttributes(node, ('name', 'real')) 689 # assert no content 690 self.parseFromTable(node, {}) 691 return name, real
692 693 # FIXME: rename to PlanetConfigParser or something (should include the 694 # word 'planet' in the name)
695 -class FlumotionConfigXML(BaseConfigParser):
696 """ 697 I represent a planet configuration file for Flumotion. 698 699 @ivar atmosphere: A L{ConfigEntryAtmosphere}, filled in when parse() is 700 called. 701 @ivar flows: A list of L{ConfigEntryFlow}, filled in when parse() is 702 called. 703 """ 704 logCategory = 'config' 705
706 - def __init__(self, file):
707 BaseConfigParser.__init__(self, file) 708 709 self.flows = [] 710 self.atmosphere = ConfigEntryAtmosphere()
711
712 - def parse(self):
713 # <planet> 714 # <manager>? 715 # <atmosphere>* 716 # <flow>* 717 # </planet> 718 root = self.doc.documentElement 719 if root.nodeName != 'planet': 720 raise ConfigError("unexpected root node': %s" % root.nodeName) 721 722 parsers = {'atmosphere': (self._parseAtmosphere, 723 self.atmosphere.components.update), 724 'flow': (self._parseFlow, 725 self.flows.append), 726 'manager': (_ignore, _ignore)} 727 self.parseFromTable(root, parsers) 728 self.doc.unlink() 729 self.doc = None
730
731 - def _parseAtmosphere(self, node):
732 # <atmosphere> 733 # <component> 734 # ... 735 # </atmosphere> 736 ret = {} 737 def parseComponent(node): 738 return self.parseComponent(node, 'atmosphere', False, True)
739 def gotComponent(comp): 740 ret[comp.name] = comp
741 parsers = {'component': (parseComponent, gotComponent)} 742 self.parseFromTable(node, parsers) 743 return ret 744
745 - def _parseFlow(self, node):
746 # <flow name="..."> 747 # <component> 748 # ... 749 # </flow> 750 # "name" cannot be atmosphere or manager 751 name, = self.parseAttributes(node, ('name',)) 752 if name == 'atmosphere': 753 raise ConfigError("<flow> cannot have 'atmosphere' as name") 754 if name == 'manager': 755 raise ConfigError("<flow> cannot have 'manager' as name") 756 757 components = [] 758 def parseComponent(node): 759 return self.parseComponent(node, name, True, True)
760 parsers = {'component': (parseComponent, components.append)} 761 self.parseFromTable(node, parsers) 762 763 # handle master clock selection; probably should be done in the 764 # manager in persistent "flow" objects rather than here in the 765 # config 766 masters = [x for x in components if x.config['clock-master']] 767 if len(masters) > 1: 768 raise ConfigError("Multiple clock masters in flow %s: %r" 769 % (name, masters)) 770 771 need_sync = [(x.defs.getClockPriority(), x) for x in components 772 if x.defs.getNeedsSynchronization()] 773 need_sync.sort() 774 need_sync = [x[1] for x in need_sync] 775 776 if need_sync: 777 if masters: 778 master = masters[0] 779 else: 780 master = need_sync[-1] 781 782 masterAvatarId = master.config['avatarId'] 783 self.info("Setting %s as clock master" % masterAvatarId) 784 785 for c in need_sync: 786 c.config['clock-master'] = masterAvatarId 787 elif masters: 788 self.info('master clock specified, but no synchronization ' 789 'necessary -- ignoring') 790 masters[0].config['clock-master'] = None 791 792 return ConfigEntryFlow(name, components) 793 794 # FIXME: remove, this is only used by the tests
795 - def getComponentEntries(self):
796 """ 797 Get all component entries from both atmosphere and all flows 798 from the configuration. 799 800 @rtype: dictionary of /parent/name -> L{ConfigEntryComponent} 801 """ 802 entries = {} 803 if self.atmosphere and self.atmosphere.components: 804 for c in self.atmosphere.components.values(): 805 path = common.componentId('atmosphere', c.name) 806 entries[path] = c 807 808 for flowEntry in self.flows: 809 for c in flowEntry.components.values(): 810 path = common.componentId(c.parent, c.name) 811 entries[path] = c 812 813 return entries
814 815 # FIXME: manager config and flow configs are currently conflated in the 816 # planet config files; need to separate.
817 -class ManagerConfigParser(BaseConfigParser):
818 """ 819 I parse manager configuration out of a planet configuration file. 820 821 @ivar manager: A L{ConfigEntryManager} containing options for the manager 822 section, filled in at construction time. 823 """ 824 logCategory = 'config' 825 826 MANAGER_SOCKETS = \ 827 ['flumotion.component.plugs.adminaction.AdminAction', 828 'flumotion.component.plugs.lifecycle.ManagerLifecycle', 829 'flumotion.component.plugs.identity.IdentityProvider'] 830
831 - def __init__(self, file):
832 BaseConfigParser.__init__(self, file) 833 834 # the base config: host, port, etc 835 self.manager = None 836 837 # the bouncer ConfigEntryComponent 838 self.bouncer = None 839 840 self.plugs = {} 841 for socket in self.MANAGER_SOCKETS: 842 self.plugs[socket] = [] 843 844 self._parseParameters()
845
846 - def _parseParameters(self):
847 root = self.doc.documentElement 848 if not root.nodeName == 'planet': 849 raise ConfigError("unexpected root node': %s" % root.nodeName) 850 851 parsers = {'atmosphere': (_ignore, _ignore), 852 'flow': (_ignore, _ignore), 853 'manager': (lambda n: self._parseManagerWithoutRegistry(n), 854 lambda v: setattr(self, 'manager', v))} 855 self.parseFromTable(root, parsers)
856
857 - def _parseManagerWithoutRegistry(self, node):
858 # We parse without asking for a registry so the registry doesn't 859 # verify before knowing the debug level 860 name, = self.parseAttributes(node, (), ('name',)) 861 ret = ConfigEntryManager(name, None, None, None, None, None, 862 None, self.plugs) 863 864 def simpleparse(proc): 865 return lambda node: self.parseTextNode(node, proc)
866 def recordval(k): 867 def record(v): 868 if getattr(ret, k): 869 raise ConfigError('duplicate %s: %s' 870 % (k, getattr(ret, k))) 871 setattr(ret, k, v)
872 return record 873 def enum(*allowed): 874 def eparse(v): 875 v = str(v) 876 if v not in allowed: 877 raise ConfigError('unknown value %s (should be ' 878 'one of %r)' % (v, allowed)) 879 return v 880 return eparse 881 882 parsers = {'host': (simpleparse(str), recordval('host')), 883 'port': (simpleparse(int), recordval('port')), 884 'transport': (simpleparse(enum('tcp', 'ssl')), 885 recordval('transport')), 886 'certificate': (simpleparse(str), recordval('certificate')), 887 'component': (_ignore, _ignore), 888 'plugs': (_ignore, _ignore), 889 'debug': (simpleparse(str), recordval('fludebug'))} 890 self.parseFromTable(node, parsers) 891 return ret 892
893 - def _parseManagerWithRegistry(self, node):
894 def parsecomponent(node): 895 return self.parseComponent(node, 'manager', False, False)
896 def gotcomponent(val): 897 if self.bouncer is not None: 898 raise ConfigError('can only have one bouncer ' 899 '(%s is superfluous)' % val.name) 900 # FIXME: assert that it is a bouncer ! 901 self.bouncer = val 902 def parseplugs(node): 903 return buildPlugsSet(self.parsePlugs(node), 904 self.MANAGER_SOCKETS) 905 def gotplugs(newplugs): 906 for socket in self.plugs: 907 self.plugs[socket].extend(newplugs[socket]) 908 909 parsers = {'host': (_ignore, _ignore), 910 'port': (_ignore, _ignore), 911 'transport': (_ignore, _ignore), 912 'certificate': (_ignore, _ignore), 913 'component': (parsecomponent, gotcomponent), 914 'plugs': (parseplugs, gotplugs), 915 'debug': (_ignore, _ignore)} 916 self.parseFromTable(node, parsers) 917
918 - def parseBouncerAndPlugs(self):
919 # <planet> 920 # <manager>? 921 # <atmosphere>* 922 # <flow>* 923 # </planet> 924 root = self.doc.documentElement 925 if not root.nodeName == 'planet': 926 raise ConfigError("unexpected root node': %s" % root.nodeName) 927 928 parsers = {'atmosphere': (_ignore, _ignore), 929 'flow': (_ignore, _ignore), 930 'manager': (self._parseManagerWithRegistry, _ignore)} 931 self.parseFromTable(root, parsers)
932 936
937 -class AdminConfigParser(BaseConfigParser):
938 """ 939 Admin configuration file parser. 940 """ 941 logCategory = 'config' 942
943 - def __init__(self, sockets, file):
944 """ 945 @param file: The file to parse, either as an open file object, 946 or as the name of a file to open. 947 @type file: str or file. 948 """ 949 self.plugs = {} 950 for socket in sockets: 951 self.plugs[socket] = [] 952 953 # will start the parse via self.add() 954 BaseConfigParser.__init__(self, file)
955
956 - def _parse(self):
957 # <admin> 958 # <plugs> 959 root = self.doc.documentElement 960 if not root.nodeName == 'admin': 961 raise ConfigError("unexpected root node': %s" % root.nodeName) 962 963 def parseplugs(node): 964 return buildPlugsSet(self.parsePlugs(node), 965 self.plugs.keys())
966 def addplugs(plugs): 967 for socket in plugs: 968 self.plugs[socket].extend(plugs[socket])
969 parsers = {'plugs': (parseplugs, addplugs)} 970 971 self.parseFromTable(root, parsers) 972 self.doc.unlink() 973 self.doc = None 974
975 - def add(self, file):
976 """ 977 @param file: The file to parse, either as an open file object, 978 or as the name of a file to open. 979 @type file: str or file. 980 """ 981 BaseConfigParser.add(self, file) 982 self._parse()
983
984 -def exportPlanetXml(p):
985 from flumotion.common.fxml import SXML 986 X = SXML() 987 988 def serialise(propVal): 989 if isinstance(propVal, tuple): # fractions are our only tuple type 990 return "%d/%d" % propVal 991 return propVal
992 993 def component(c): 994 concat = lambda lists: reduce(list.__add__, lists, []) 995 C = c.get('config') 996 return ([X.component(name=c.get('name'), 997 type=c.get('type'), 998 label=C.get('label', c.get('name')), 999 worker=c.get('workerRequested'), 1000 project=C['project'], 1001 version=common.versionTupleToString(C['version']))] 1002 + [[X.eater(name=name)] 1003 + [[X.feed(alias=alias), feedId] 1004 for feedId, alias in feeders] 1005 for name, feeders in C['eater'].items()] 1006 + [[X.property(name=name), serialise(value)] 1007 for name, value in C['properties'].items()] 1008 + [[X.clock_master(), 1009 C['clock-master'] == C['avatarId'] and 'true' or 'false']] 1010 + [[X.plugs()] 1011 + concat([[[X.plug(socket=socket, type=plug['type'])] 1012 + [[X.property(name=name), value] 1013 for name, value in plug['properties'].items()] 1014 for plug in plugs] 1015 for socket, plugs in C['plugs'].items()])] 1016 + [[X.virtual_feed(name=name, real=real)] 1017 for name, real in C['virtual-feeds'].items()]) 1018 1019 def flow(f): 1020 return ([X.flow(name=f.get('name'))] 1021 + [component(c) for c in f.get('components')]) 1022 1023 def atmosphere(a): 1024 return ([X.atmosphere()] 1025 + [component(c) for c in a.get('components')]) 1026 1027 def planet(p): 1028 return ([X.planet(name=p.get('name')), 1029 atmosphere(p.get('atmosphere'))] 1030 + [flow(f) for f in p.get('flows')]) 1031 return fxml.sxml2unicode(planet(p)) 1032