1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
41
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
66 eaters = dict(conf.get('eater', {}))
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
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
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
150
151 while aliases:
152 alias = aliases.pop()
153 if alias in aliases:
154 raise ConfigError("Duplicate alias: %s" % alias)
155
156 return eaters
157
159
160
161
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
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
216
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
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
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
328
329 if onlyOld is None:
330 onlyOld = []
331 onlyNew = []
332 diff = []
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
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
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
438
439
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
461 config['label'] = self.label
462
463 if not config['source']:
464
465 del config['source']
466
467
468 return config
469
472
473 - def getLabel(self):
475
478
479 - def getParent(self):
481
482 - def getConfigDict(self):
484
485 - def getWorker(self):
487
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
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
513 "I represent a <atmosphere> entry in a planet config file"
514 - def __init__(self):
516
518 return len(self.components)
519
521 parserError = ConfigError
522
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
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
550
570
571 parsers = {'plug': (parsePlug, plugs.append)}
572 self.parseFromTable(node, parsers)
573 return plugs
574
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
589
590
591
592
593
594
595
596
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
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
649
654
668
672
685
692
693
694
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
711
713
714
715
716
717
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
732
733
734
735
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
760 parsers = {'component': (parseComponent, components.append)}
761 self.parseFromTable(node, parsers)
762
763
764
765
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
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
816
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
845
856
858
859
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
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
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
932
934 self.doc.unlink()
935 self.doc = None
936
938 """
939 Admin configuration file parser.
940 """
941 logCategory = 'config'
942
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
954 BaseConfigParser.__init__(self, file)
955
957
958
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
985 from flumotion.common.fxml import SXML
986 X = SXML()
987
988 def serialise(propVal):
989 if isinstance(propVal, tuple):
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