3 # Merge PlanetLab Central (PLC) configuration files into a variety of
4 # output formats. These files represent the global configuration for a
7 # Mark Huang <mlhuang@cs.princeton.edu>
8 # Copyright (C) 2006 The Trustees of Princeton University
13 import xml.dom.minidom
14 from StringIO import StringIO
23 class PLCConfiguration:
25 Configuration file store. Optionally instantiate with a file path
28 plc = PLCConfiguration()
29 plc = PLCConfiguration(fileobj)
30 plc = PLCConfiguration("/etc/planetlab/plc_config.xml")
32 You may load() additional files later, which will be merged into
33 the current configuration:
35 plc.load("/etc/planetlab/local.xml")
37 You may also save() the configuration. If a file path or object is
38 not specified, the configuration will be written to the file path
39 or object that was first loaded.
42 plc.save("/etc/planetlab/plc_config.xml")
45 def __init__(self, file = None):
46 impl = xml.dom.minidom.getDOMImplementation()
47 self._dom = impl.createDocument(None, "configuration", None)
56 def _get_text(self, node):
58 Get the text of a text node.
61 if node.firstChild and \
62 node.firstChild.nodeType == node.TEXT_NODE:
63 if node.firstChild.data is None:
64 # Interpret simple presence of node as "", not NULL
67 return node.firstChild.data
72 def _get_text_of_child(self, parent, name):
74 Get the text of a (direct) child text node.
77 for node in parent.childNodes:
78 if node.nodeType == node.ELEMENT_NODE and \
80 return self._get_text(node)
85 def _set_text(self, node, data):
87 Set the text of a text node.
90 if node.firstChild and \
91 node.firstChild.nodeType == node.TEXT_NODE:
93 node.removeChild(node.firstChild)
95 node.firstChild.data = data
96 elif data is not None:
99 node.appendChild(text)
102 def _set_text_of_child(self, parent, name, data):
104 Set the text of a (direct) child text node.
107 for node in parent.childNodes:
108 if node.nodeType == node.ELEMENT_NODE and \
109 node.tagName == name:
110 self._set_text(node, data)
113 child = TrimTextElement(name)
114 self._set_text(child, data)
115 parent.appendChild(child)
118 def _category_element_to_dict(self, category_element):
120 Turn a <category> element into a dictionary of its attributes
121 and child text nodes.
125 category['id'] = category_element.getAttribute('id').lower()
126 for node in category_element.childNodes:
127 if node.nodeType == node.ELEMENT_NODE and \
128 node.tagName in ['name', 'description']:
129 category[node.tagName] = self._get_text_of_child(category_element, node.tagName)
130 category['element'] = category_element
135 def _variable_element_to_dict(self, variable_element):
137 Turn a <variable> element into a dictionary of its attributes
138 and child text nodes.
142 variable['id'] = variable_element.getAttribute('id').lower()
143 if variable_element.hasAttribute('type'):
144 variable['type'] = variable_element.getAttribute('type')
145 for node in variable_element.childNodes:
146 if node.nodeType == node.ELEMENT_NODE and \
147 node.tagName in ['name', 'value', 'description']:
148 variable[node.tagName] = self._get_text_of_child(variable_element, node.tagName)
149 variable['element'] = variable_element
154 def _group_element_to_dict(self, group_element):
156 Turn a <group> element into a dictionary of its attributes
157 and child text nodes.
161 for node in group_element.childNodes:
162 if node.nodeType == node.ELEMENT_NODE and \
163 node.tagName in ['id', 'name', 'default', 'description', 'uservisible']:
164 group[node.tagName] = self._get_text_of_child(group_element, node.tagName)
165 group['element'] = group_element
170 def _packagereq_element_to_dict(self, packagereq_element):
172 Turns a <packagereq> element into a dictionary of its attributes
173 and child text nodes.
177 if packagereq_element.hasAttribute('type'):
178 package['type'] = packagereq_element.getAttribute('type')
179 package['name'] = self._get_text(packagereq_element)
180 package['element'] = packagereq_element
185 def load(self, file = "/etc/planetlab/plc_config.xml"):
187 Merge file into configuration store.
190 dom = xml.dom.minidom.parse(file)
191 if type(file) in types.StringTypes:
192 self._files.append(os.path.abspath(file))
194 # Parse <variables> section
195 for variables_element in dom.getElementsByTagName('variables'):
196 for category_element in variables_element.getElementsByTagName('category'):
197 category = self._category_element_to_dict(category_element)
198 self.set(category, None)
200 for variablelist_element in category_element.getElementsByTagName('variablelist'):
201 for variable_element in variablelist_element.getElementsByTagName('variable'):
202 variable = self._variable_element_to_dict(variable_element)
203 self.set(category, variable)
205 # Parse <comps> section
206 for comps_element in dom.getElementsByTagName('comps'):
207 for group_element in comps_element.getElementsByTagName('group'):
208 group = self._group_element_to_dict(group_element)
209 self.add_package(group, None)
211 for packagereq_element in group_element.getElementsByTagName('packagereq'):
212 package = self._packagereq_element_to_dict(packagereq_element)
213 self.add_package(group, package)
216 def save(self, file = None):
218 Write configuration store to file.
223 file = self._files[0]
225 file = "/etc/planetlab/plc_config.xml"
227 if type(file) in types.StringTypes:
228 fileobj = open(file, 'w')
233 fileobj.write(self.output_xml())
239 def get(self, category_id, variable_id):
241 Get the specified variable in the specified category.
245 category_id = unique category identifier (e.g., 'plc_www')
246 variable_id = unique variable identifier (e.g., 'port')
250 variable = { 'id': "variable_identifier",
251 'type': "variable_type",
252 'value': "variable_value",
253 'name': "Variable name",
254 'description': "Variable description" }
257 if self._variables.has_key(category_id.lower()):
258 (category, variables) = self._variables[category_id]
259 if variables.has_key(variable_id.lower()):
260 variable = variables[variable_id]
267 return (category, variable)
270 def delete(self, category_id, variable_id):
272 Delete the specified variable from the specified category. If
273 variable_id is None, deletes all variables from the specified
274 category as well as the category itself.
278 category_id = unique category identifier (e.g., 'plc_www')
279 variable_id = unique variable identifier (e.g., 'port')
282 if self._variables.has_key(category_id.lower()):
283 (category, variables) = self._variables[category_id]
284 if variable_id is None:
285 category['element'].parentNode.removeChild(category['element'])
286 del self._variables[category_id]
287 elif variables.has_key(variable_id.lower()):
288 variable = variables[variable_id]
289 variable['element'].parentNode.removeChild(variable['element'])
290 del variables[variable_id]
293 def set(self, category, variable):
295 Add and/or update the specified variable. The 'id' fields are
296 mandatory. If a field is not specified and the category and/or
297 variable already exists, the field will not be updated. If
298 'variable' is None, only adds and/or updates the specified
303 category = { 'id': "category_identifier",
304 'name': "Category name",
305 'description': "Category description" }
307 variable = { 'id': "variable_identifier",
308 'type': "variable_type",
309 'value': "variable_value",
310 'name': "Variable name",
311 'description': "Variable description" }
314 if not category.has_key('id') or type(category['id']) not in types.StringTypes:
317 category_id = category['id'].lower()
319 if self._variables.has_key(category_id):
321 (old_category, variables) = self._variables[category_id]
323 # Merge category attributes
324 for tag in ['name', 'description']:
325 if category.has_key(tag):
326 old_category[tag] = category[tag]
327 self._set_text_of_child(old_category['element'], tag, category[tag])
329 category_element = old_category['element']
332 category_element = self._dom.createElement('category')
333 category_element.setAttribute('id', category_id)
334 for tag in ['name', 'description']:
335 if category.has_key(tag):
336 self._set_text_of_child(category_element, tag, category[tag])
338 if self._dom.documentElement.getElementsByTagName('variables'):
339 variables_element = self._dom.documentElement.getElementsByTagName('variables')[0]
341 variables_element = self._dom.createElement('variables')
342 self._dom.documentElement.appendChild(variables_element)
343 variables_element.appendChild(category_element)
346 category['element'] = category_element
348 self._variables[category_id] = (category, variables)
350 if variable is None or not variable.has_key('id') or type(variable['id']) not in types.StringTypes:
353 variable_id = variable['id'].lower()
355 if variables.has_key(variable_id):
357 old_variable = variables[variable_id]
359 # Merge variable attributes
360 for attribute in ['type']:
361 if variable.has_key(attribute):
362 old_variable[attribute] = variable[attribute]
363 old_variable['element'].setAttribute(attribute, variable[attribute])
364 for tag in ['name', 'value', 'description']:
365 if variable.has_key(tag):
366 old_variable[tag] = variable[tag]
367 self._set_text_of_child(old_variable['element'], tag, variable[tag])
370 variable_element = self._dom.createElement('variable')
371 variable_element.setAttribute('id', variable_id)
372 for attribute in ['type']:
373 if variable.has_key(attribute):
374 variable_element.setAttribute(attribute, variable[attribute])
375 for tag in ['name', 'value', 'description']:
376 if variable.has_key(tag):
377 self._set_text_of_child(variable_element, tag, variable[tag])
379 if category_element.getElementsByTagName('variablelist'):
380 variablelist_element = category_element.getElementsByTagName('variablelist')[0]
382 variablelist_element = self._dom.createElement('variablelist')
383 category_element.appendChild(variablelist_element)
384 variablelist_element.appendChild(variable_element)
387 variable['element'] = variable_element
388 variables[variable_id] = variable
391 def locate_varname (self, varname):
393 Locates category and variable from a variable's (shell) name
396 (variable, category) when found
397 (None, None) otherwise
400 for (category_id, (category, variables)) in self._variables.iteritems():
401 for variable in variables.values():
402 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
404 return (category,variable)
407 def get_package(self, group_id, package_name):
409 Get the specified package in the specified package group.
413 group_id - unique group id (e.g., 'plc')
414 package_name - unique package name (e.g., 'postgresql')
418 package = { 'name': "package_name",
419 'type': "mandatory|optional" }
422 if self._packages.has_key(group_id.lower()):
423 (group, packages) = self._packages[group_id]
424 if packages.has_key(package_name):
425 package = packages[package_name]
432 return (group, package)
435 def delete_package(self, group_id, package_name):
437 Deletes the specified variable from the specified category. If
438 variable_id is None, deletes all variables from the specified
439 category as well as the category itself.
443 group_id - unique group id (e.g., 'plc')
444 package_name - unique package name (e.g., 'postgresql')
447 if self._packages.has_key(group_id):
448 (group, packages) = self._packages[group_id]
449 if package_name is None:
450 group['element'].parentNode.removeChild(group['element'])
451 del self._packages[group_id]
452 elif packages.has_key(package_name.lower()):
453 package = packages[package_name]
454 package['element'].parentNode.removeChild(package['element'])
455 del packages[package_name]
458 def add_package(self, group, package):
460 Add and/or update the specified package. The 'id' and 'name'
461 fields are mandatory. If a field is not specified and the
462 package or group already exists, the field will not be
463 updated. If package is None, only adds/or updates the
468 group = { 'id': "group_identifier",
469 'name': "Group name",
470 'default': "true|false",
471 'description': "Group description",
472 'uservisible': "true|false" }
474 package = { 'name': "package_name",
475 'type': "mandatory|optional" }
478 if not group.has_key('id'):
481 group_id = group['id']
483 if self._packages.has_key(group_id):
485 (old_group, packages) = self._packages[group_id]
487 # Merge group attributes
488 for tag in ['id', 'name', 'default', 'description', 'uservisible']:
489 if group.has_key(tag):
490 old_group[tag] = group[tag]
491 self._set_text_of_child(old_group['element'], tag, group[tag])
493 group_element = old_group['element']
496 group_element = self._dom.createElement('group')
497 for tag in ['id', 'name', 'default', 'description', 'uservisible']:
498 if group.has_key(tag):
499 self._set_text_of_child(group_element, tag, group[tag])
501 if self._dom.documentElement.getElementsByTagName('comps'):
502 comps_element = self._dom.documentElement.getElementsByTagName('comps')[0]
504 comps_element = self._dom.createElement('comps')
505 self._dom.documentElement.appendChild(comps_element)
506 comps_element.appendChild(group_element)
509 group['element'] = group_element
511 self._packages[group_id] = (group, packages)
513 if package is None or not package.has_key('name'):
516 package_name = package['name']
517 if packages.has_key(package_name):
519 old_package = packages[package_name]
521 # Merge variable attributes
522 for attribute in ['type']:
523 if package.has_key(attribute):
524 old_package[attribute] = package[attribute]
525 old_package['element'].setAttribute(attribute, package[attribute])
528 packagereq_element = TrimTextElement('packagereq')
529 self._set_text(packagereq_element, package_name)
530 for attribute in ['type']:
531 if package.has_key(attribute):
532 packagereq_element.setAttribute(attribute, package[attribute])
534 if group_element.getElementsByTagName('packagelist'):
535 packagelist_element = group_element.getElementsByTagName('packagelist')[0]
537 packagelist_element = self._dom.createElement('packagelist')
538 group_element.appendChild(packagelist_element)
539 packagelist_element.appendChild(packagereq_element)
542 package['element'] = packagereq_element
543 packages[package_name] = package
548 Return all variables.
552 variables = { 'category_id': (category, variablelist) }
554 category = { 'id': "category_identifier",
555 'name': "Category name",
556 'description': "Category description" }
558 variablelist = { 'variable_id': variable }
560 variable = { 'id': "variable_identifier",
561 'type': "variable_type",
562 'value': "variable_value",
563 'name': "Variable name",
564 'description': "Variable description" }
567 return self._variables
576 packages = { 'group_id': (group, packagelist) }
578 group = { 'id': "group_identifier",
579 'name': "Group name",
580 'default': "true|false",
581 'description': "Group description",
582 'uservisible': "true|false" }
584 packagelist = { 'package_name': package }
586 package = { 'name': "package_name",
587 'type': "mandatory|optional" }
590 return self._packages
593 def _sanitize_variable(self, category_id, variable):
594 assert variable.has_key('id')
595 # Prepend variable name with category label
596 id = category_id + "_" + variable['id']
600 if variable.has_key('type'):
601 type = variable['type']
605 if variable.has_key('name'):
606 name = variable['name']
610 if variable.has_key('value') and variable['value'] is not None:
611 value = variable['value']
612 if type == "int" or type == "double":
613 # bash, Python, and PHP do not require that numbers be quoted
615 elif type == "boolean":
616 # bash, Python, and PHP can all agree on 0 and 1
622 # bash, Python, and PHP all support strong single quoting
623 value = "'" + value.replace("'", "\\'") + "'"
627 if variable.has_key('description') and variable['description'] is not None:
628 description = variable['description']
629 # Collapse consecutive whitespace
630 description = re.sub(r'\s+', ' ', description)
631 # Wrap comments at 70 columns
632 wrapper = textwrap.TextWrapper()
633 comments = wrapper.wrap(description)
637 return (id, name, value, comments)
642 DO NOT EDIT. This file was automatically generated at
646 """ % (time.asctime(), os.linesep.join(self._files))
648 # Get rid of the surrounding newlines
649 return header.strip().split(os.linesep)
652 def output_shell(self, show_comments = True, encoding = "utf-8"):
654 Return variables as a shell script.
657 buf = codecs.lookup(encoding)[3](StringIO())
658 buf.writelines(["# " + line + os.linesep for line in self._header()])
660 for (category_id, (category, variables)) in self._variables.iteritems():
661 for variable in variables.values():
662 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
664 buf.write(os.linesep)
666 buf.write("# " + name + os.linesep)
667 if comments is not None:
668 buf.writelines(["# " + line + os.linesep for line in comments])
669 # bash does not have the concept of NULL
670 if value is not None:
671 buf.write(id + "=" + value + os.linesep)
673 return buf.getvalue()
676 def output_php(self, encoding = "utf-8"):
678 Return variables as a PHP script.
681 buf = codecs.lookup(encoding)[3](StringIO())
682 buf.write("<?php" + os.linesep)
683 buf.writelines(["// " + line + os.linesep for line in self._header()])
685 for (category_id, (category, variables)) in self._variables.iteritems():
686 for variable in variables.values():
687 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
688 buf.write(os.linesep)
690 buf.write("// " + name + os.linesep)
691 if comments is not None:
692 buf.writelines(["// " + line + os.linesep for line in comments])
695 buf.write("define('%s', %s);" % (id, value) + os.linesep)
697 buf.write("?>" + os.linesep)
699 return buf.getvalue()
702 def output_xml(self, encoding = "utf-8"):
704 Return variables in original XML format.
707 buf = codecs.lookup(encoding)[3](StringIO())
708 self._dom.writexml(buf, addindent = " ", indent = "", newl = "\n", encoding = encoding)
710 return buf.getvalue()
713 def output_variables(self, encoding = "utf-8"):
715 Return list of all variable names.
718 buf = codecs.lookup(encoding)[3](StringIO())
720 for (category_id, (category, variables)) in self._variables.iteritems():
721 for variable in variables.values():
722 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
723 buf.write(id + os.linesep)
725 return buf.getvalue()
728 def output_packages(self, encoding = "utf-8"):
730 Return list of all packages.
733 buf = codecs.lookup(encoding)[3](StringIO())
735 for (group, packages) in self._packages.values():
736 buf.write(os.linesep.join(packages.keys()))
739 buf.write(os.linesep)
741 return buf.getvalue()
744 def output_groups(self, encoding = "utf-8"):
746 Return list of all package group names.
749 buf = codecs.lookup(encoding)[3](StringIO())
751 for (group, packages) in self._packages.values():
752 buf.write(group['name'] + os.linesep)
754 return buf.getvalue()
757 def output_comps(self, encoding = "utf-8"):
759 Return <comps> section of configuration.
762 if self._dom is None or \
763 not self._dom.getElementsByTagName("comps"):
765 comps = self._dom.getElementsByTagName("comps")[0]
767 impl = xml.dom.minidom.getDOMImplementation()
768 doc = impl.createDocument(None, "comps", None)
770 buf = codecs.lookup(encoding)[3](StringIO())
772 # Pop it off the DOM temporarily
773 parent = comps.parentNode
774 parent.removeChild(comps)
776 doc.replaceChild(comps, doc.documentElement)
777 doc.writexml(buf, encoding = encoding)
780 parent.appendChild(comps)
782 return buf.getvalue()
785 # xml.dom.minidom.Text.writexml adds surrounding whitespace to textual
786 # data when pretty-printing. Override this behavior.
787 class TrimText(xml.dom.minidom.Text):
788 def writexml(self, writer, indent="", addindent="", newl=""):
789 xml.dom.minidom.Text.writexml(self, writer, "", "", "")
792 class TrimTextElement(xml.dom.minidom.Element):
793 def writexml(self, writer, indent="", addindent="", newl=""):
795 xml.dom.minidom.Element.writexml(self, writer, "", "", "")
799 if __name__ == '__main__':
801 if len(sys.argv) > 1 and sys.argv[1] in ['build', 'install', 'uninstall']:
802 from distutils.core import setup
803 setup(py_modules=["plc_config"])