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 ConfigurationException(Exception): pass
25 class PLCConfiguration:
27 Configuration file store. Optionally instantiate with a file path
30 plc = PLCConfiguration()
31 plc = PLCConfiguration(fileobj)
32 plc = PLCConfiguration("/etc/planetlab/plc_config.xml")
34 You may load() additional files later, which will be merged into
35 the current configuration:
37 plc.load("/etc/planetlab/local.xml")
39 You may also save() the configuration. If a file path or object is
40 not specified, the configuration will be written to the file path
41 or object that was first loaded.
44 plc.save("/etc/planetlab/plc_config.xml")
47 def __init__(self, file = None):
48 impl = xml.dom.minidom.getDOMImplementation()
49 self._dom = impl.createDocument(None, "configuration", None)
58 def _get_text(self, node):
60 Get the text of a text node.
63 if node.firstChild and \
64 node.firstChild.nodeType == node.TEXT_NODE:
65 if node.firstChild.data is None:
66 # Interpret simple presence of node as "", not NULL
69 return node.firstChild.data
74 def _get_text_of_child(self, parent, name):
76 Get the text of a (direct) child text node.
79 for node in parent.childNodes:
80 if node.nodeType == node.ELEMENT_NODE and \
82 return self._get_text(node)
87 def _set_text(self, node, data):
89 Set the text of a text node.
92 if node.firstChild and \
93 node.firstChild.nodeType == node.TEXT_NODE:
95 node.removeChild(node.firstChild)
97 node.firstChild.data = data
98 elif data is not None:
101 node.appendChild(text)
104 def _set_text_of_child(self, parent, name, data):
106 Set the text of a (direct) child text node.
109 for node in parent.childNodes:
110 if node.nodeType == node.ELEMENT_NODE and \
111 node.tagName == name:
112 self._set_text(node, data)
115 child = TrimTextElement(name)
116 self._set_text(child, data)
117 parent.appendChild(child)
120 def _category_element_to_dict(self, category_element):
122 Turn a <category> element into a dictionary of its attributes
123 and child text nodes.
127 category['id'] = category_element.getAttribute('id').lower()
128 for node in category_element.childNodes:
129 if node.nodeType == node.ELEMENT_NODE and \
130 node.tagName in ['name', 'description']:
131 category[node.tagName] = self._get_text_of_child(category_element, node.tagName)
132 category['element'] = category_element
137 def _variable_element_to_dict(self, variable_element):
139 Turn a <variable> element into a dictionary of its attributes
140 and child text nodes.
144 variable['id'] = variable_element.getAttribute('id').lower()
145 if variable_element.hasAttribute('type'):
146 variable['type'] = variable_element.getAttribute('type')
147 for node in variable_element.childNodes:
148 if node.nodeType == node.ELEMENT_NODE and \
149 node.tagName in ['name', 'value', 'description']:
150 variable[node.tagName] = self._get_text_of_child(variable_element, node.tagName)
151 variable['element'] = variable_element
156 def _group_element_to_dict(self, group_element):
158 Turn a <group> element into a dictionary of its attributes
159 and child text nodes.
163 for node in group_element.childNodes:
164 if node.nodeType == node.ELEMENT_NODE and \
165 node.tagName in ['id', 'name', 'default', 'description', 'uservisible']:
166 group[node.tagName] = self._get_text_of_child(group_element, node.tagName)
167 group['element'] = group_element
172 def _packagereq_element_to_dict(self, packagereq_element):
174 Turns a <packagereq> element into a dictionary of its attributes
175 and child text nodes.
179 if packagereq_element.hasAttribute('type'):
180 package['type'] = packagereq_element.getAttribute('type')
181 package['name'] = self._get_text(packagereq_element)
182 package['element'] = packagereq_element
187 def load(self, file = "/etc/planetlab/plc_config.xml"):
189 Merge file into configuration store.
192 dom = xml.dom.minidom.parse(file)
193 if type(file) in types.StringTypes:
194 self._files.append(os.path.abspath(file))
196 # Parse <variables> section
197 for variables_element in dom.getElementsByTagName('variables'):
198 for category_element in variables_element.getElementsByTagName('category'):
199 category = self._category_element_to_dict(category_element)
200 self.set(category, None)
202 for variablelist_element in category_element.getElementsByTagName('variablelist'):
203 for variable_element in variablelist_element.getElementsByTagName('variable'):
204 variable = self._variable_element_to_dict(variable_element)
205 self.set(category, variable)
207 # Parse <comps> section
208 for comps_element in dom.getElementsByTagName('comps'):
209 for group_element in comps_element.getElementsByTagName('group'):
210 group = self._group_element_to_dict(group_element)
211 self.add_package(group, None)
213 for packagereq_element in group_element.getElementsByTagName('packagereq'):
214 package = self._packagereq_element_to_dict(packagereq_element)
215 self.add_package(group, package)
218 def save(self, file = None):
220 Write configuration store to file.
225 file = self._files[0]
227 file = "/etc/planetlab/plc_config.xml"
229 if type(file) in types.StringTypes:
230 fileobj = open(file, 'w')
235 fileobj.write(self.output_xml())
240 def verify(self, default, read):
241 """ Confirm that the existing configuration is consistent according to
244 It looks for filled-in values in the order of, local object (self),
245 followed by cread (read values), and finally default values.
253 None. If an exception is found, ConfigurationException is raised.
257 maint_user = self.get('plc_api', 'maintenance_user')
258 if maint_user == (None, None):
259 maint_user = read.get('plc_api', 'maintenance_user')
260 if maint_user == (None, None):
261 maint_user = default.get('plc_api', 'maintenance_user')
263 root_user = self.get('plc', 'root_user')
264 if root_user == (None, None):
265 root_user = read.get('plc', 'root_user')
266 if root_user == (None, None):
267 root_user = default.get('plc', 'root_user')
269 muser= maint_user[1]['value']
270 ruser= root_user[1]['value']
273 raise ConfigurationException("The Maintenance Account email address cannot be the same as the Root User email address")
277 def get(self, category_id, variable_id):
279 Get the specified variable in the specified category.
283 category_id = unique category identifier (e.g., 'plc_www')
284 variable_id = unique variable identifier (e.g., 'port')
288 variable = { 'id': "variable_identifier",
289 'type': "variable_type",
290 'value': "variable_value",
291 'name': "Variable name",
292 'description': "Variable description" }
295 if self._variables.has_key(category_id.lower()):
296 (category, variables) = self._variables[category_id]
297 if variables.has_key(variable_id.lower()):
298 variable = variables[variable_id]
305 return (category, variable)
308 def delete(self, category_id, variable_id):
310 Delete the specified variable from the specified category. If
311 variable_id is None, deletes all variables from the specified
312 category as well as the category itself.
316 category_id = unique category identifier (e.g., 'plc_www')
317 variable_id = unique variable identifier (e.g., 'port')
320 if self._variables.has_key(category_id.lower()):
321 (category, variables) = self._variables[category_id]
322 if variable_id is None:
323 category['element'].parentNode.removeChild(category['element'])
324 del self._variables[category_id]
325 elif variables.has_key(variable_id.lower()):
326 variable = variables[variable_id]
327 variable['element'].parentNode.removeChild(variable['element'])
328 del variables[variable_id]
331 def set(self, category, variable):
333 Add and/or update the specified variable. The 'id' fields are
334 mandatory. If a field is not specified and the category and/or
335 variable already exists, the field will not be updated. If
336 'variable' is None, only adds and/or updates the specified
341 category = { 'id': "category_identifier",
342 'name': "Category name",
343 'description': "Category description" }
345 variable = { 'id': "variable_identifier",
346 'type': "variable_type",
347 'value': "variable_value",
348 'name': "Variable name",
349 'description': "Variable description" }
352 if not category.has_key('id') or type(category['id']) not in types.StringTypes:
355 category_id = category['id'].lower()
357 if self._variables.has_key(category_id):
359 (old_category, variables) = self._variables[category_id]
361 # Merge category attributes
362 for tag in ['name', 'description']:
363 if category.has_key(tag):
364 old_category[tag] = category[tag]
365 self._set_text_of_child(old_category['element'], tag, category[tag])
367 category_element = old_category['element']
370 category_element = self._dom.createElement('category')
371 category_element.setAttribute('id', category_id)
372 for tag in ['name', 'description']:
373 if category.has_key(tag):
374 self._set_text_of_child(category_element, tag, category[tag])
376 if self._dom.documentElement.getElementsByTagName('variables'):
377 variables_element = self._dom.documentElement.getElementsByTagName('variables')[0]
379 variables_element = self._dom.createElement('variables')
380 self._dom.documentElement.appendChild(variables_element)
381 variables_element.appendChild(category_element)
384 category['element'] = category_element
386 self._variables[category_id] = (category, variables)
388 if variable is None or not variable.has_key('id') or type(variable['id']) not in types.StringTypes:
391 variable_id = variable['id'].lower()
393 if variables.has_key(variable_id):
395 old_variable = variables[variable_id]
397 # Merge variable attributes
398 for attribute in ['type']:
399 if variable.has_key(attribute):
400 old_variable[attribute] = variable[attribute]
401 old_variable['element'].setAttribute(attribute, variable[attribute])
402 for tag in ['name', 'value', 'description']:
403 if variable.has_key(tag):
404 old_variable[tag] = variable[tag]
405 self._set_text_of_child(old_variable['element'], tag, variable[tag])
408 variable_element = self._dom.createElement('variable')
409 variable_element.setAttribute('id', variable_id)
410 for attribute in ['type']:
411 if variable.has_key(attribute):
412 variable_element.setAttribute(attribute, variable[attribute])
413 for tag in ['name', 'value', 'description']:
414 if variable.has_key(tag):
415 self._set_text_of_child(variable_element, tag, variable[tag])
417 if category_element.getElementsByTagName('variablelist'):
418 variablelist_element = category_element.getElementsByTagName('variablelist')[0]
420 variablelist_element = self._dom.createElement('variablelist')
421 category_element.appendChild(variablelist_element)
422 variablelist_element.appendChild(variable_element)
425 variable['element'] = variable_element
426 variables[variable_id] = variable
429 def locate_varname (self, varname):
431 Locates category and variable from a variable's (shell) name
434 (variable, category) when found
435 (None, None) otherwise
438 for (category_id, (category, variables)) in self._variables.iteritems():
439 for variable in variables.values():
440 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
442 return (category,variable)
445 def get_package(self, group_id, package_name):
447 Get the specified package in the specified package group.
451 group_id - unique group id (e.g., 'plc')
452 package_name - unique package name (e.g., 'postgresql')
456 package = { 'name': "package_name",
457 'type': "mandatory|optional" }
460 if self._packages.has_key(group_id.lower()):
461 (group, packages) = self._packages[group_id]
462 if packages.has_key(package_name):
463 package = packages[package_name]
470 return (group, package)
473 def delete_package(self, group_id, package_name):
475 Deletes the specified variable from the specified category. If
476 variable_id is None, deletes all variables from the specified
477 category as well as the category itself.
481 group_id - unique group id (e.g., 'plc')
482 package_name - unique package name (e.g., 'postgresql')
485 if self._packages.has_key(group_id):
486 (group, packages) = self._packages[group_id]
487 if package_name is None:
488 group['element'].parentNode.removeChild(group['element'])
489 del self._packages[group_id]
490 elif packages.has_key(package_name.lower()):
491 package = packages[package_name]
492 package['element'].parentNode.removeChild(package['element'])
493 del packages[package_name]
496 def add_package(self, group, package):
498 Add and/or update the specified package. The 'id' and 'name'
499 fields are mandatory. If a field is not specified and the
500 package or group already exists, the field will not be
501 updated. If package is None, only adds/or updates the
506 group = { 'id': "group_identifier",
507 'name': "Group name",
508 'default': "true|false",
509 'description': "Group description",
510 'uservisible': "true|false" }
512 package = { 'name': "package_name",
513 'type': "mandatory|optional" }
516 if not group.has_key('id'):
519 group_id = group['id']
521 if self._packages.has_key(group_id):
523 (old_group, packages) = self._packages[group_id]
525 # Merge group attributes
526 for tag in ['id', 'name', 'default', 'description', 'uservisible']:
527 if group.has_key(tag):
528 old_group[tag] = group[tag]
529 self._set_text_of_child(old_group['element'], tag, group[tag])
531 group_element = old_group['element']
534 group_element = self._dom.createElement('group')
535 for tag in ['id', 'name', 'default', 'description', 'uservisible']:
536 if group.has_key(tag):
537 self._set_text_of_child(group_element, tag, group[tag])
539 if self._dom.documentElement.getElementsByTagName('comps'):
540 comps_element = self._dom.documentElement.getElementsByTagName('comps')[0]
542 comps_element = self._dom.createElement('comps')
543 self._dom.documentElement.appendChild(comps_element)
544 comps_element.appendChild(group_element)
547 group['element'] = group_element
549 self._packages[group_id] = (group, packages)
551 if package is None or not package.has_key('name'):
554 package_name = package['name']
555 if packages.has_key(package_name):
557 old_package = packages[package_name]
559 # Merge variable attributes
560 for attribute in ['type']:
561 if package.has_key(attribute):
562 old_package[attribute] = package[attribute]
563 old_package['element'].setAttribute(attribute, package[attribute])
566 packagereq_element = TrimTextElement('packagereq')
567 self._set_text(packagereq_element, package_name)
568 for attribute in ['type']:
569 if package.has_key(attribute):
570 packagereq_element.setAttribute(attribute, package[attribute])
572 if group_element.getElementsByTagName('packagelist'):
573 packagelist_element = group_element.getElementsByTagName('packagelist')[0]
575 packagelist_element = self._dom.createElement('packagelist')
576 group_element.appendChild(packagelist_element)
577 packagelist_element.appendChild(packagereq_element)
580 package['element'] = packagereq_element
581 packages[package_name] = package
586 Return all variables.
590 variables = { 'category_id': (category, variablelist) }
592 category = { 'id': "category_identifier",
593 'name': "Category name",
594 'description': "Category description" }
596 variablelist = { 'variable_id': variable }
598 variable = { 'id': "variable_identifier",
599 'type': "variable_type",
600 'value': "variable_value",
601 'name': "Variable name",
602 'description': "Variable description" }
605 return self._variables
614 packages = { 'group_id': (group, packagelist) }
616 group = { 'id': "group_identifier",
617 'name': "Group name",
618 'default': "true|false",
619 'description': "Group description",
620 'uservisible': "true|false" }
622 packagelist = { 'package_name': package }
624 package = { 'name': "package_name",
625 'type': "mandatory|optional" }
628 return self._packages
631 def _sanitize_variable(self, category_id, variable):
632 assert variable.has_key('id')
633 # Prepend variable name with category label
634 id = category_id + "_" + variable['id']
638 if variable.has_key('type'):
639 type = variable['type']
643 if variable.has_key('name'):
644 name = variable['name']
648 if variable.has_key('value') and variable['value'] is not None:
649 value = variable['value']
650 if type == "int" or type == "double":
651 # bash, Python, and PHP do not require that numbers be quoted
653 elif type == "boolean":
654 # bash, Python, and PHP can all agree on 0 and 1
660 # bash, Python, and PHP all support strong single quoting
661 value = "'" + value.replace("'", "\\'") + "'"
665 if variable.has_key('description') and variable['description'] is not None:
666 description = variable['description']
667 # Collapse consecutive whitespace
668 description = re.sub(r'\s+', ' ', description)
669 # Wrap comments at 70 columns
670 wrapper = textwrap.TextWrapper()
671 comments = wrapper.wrap(description)
675 return (id, name, value, comments)
680 DO NOT EDIT. This file was automatically generated at
684 """ % (time.asctime(), os.linesep.join(self._files))
686 # Get rid of the surrounding newlines
687 return header.strip().split(os.linesep)
690 def output_shell(self, show_comments = True, encoding = "utf-8"):
692 Return variables as a shell script.
695 buf = codecs.lookup(encoding)[3](StringIO())
696 buf.writelines(["# " + line + os.linesep for line in self._header()])
698 for (category_id, (category, variables)) in self._variables.iteritems():
699 for variable in variables.values():
700 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
702 buf.write(os.linesep)
704 buf.write("# " + name + os.linesep)
705 if comments is not None:
706 buf.writelines(["# " + line + os.linesep for line in comments])
707 # bash does not have the concept of NULL
708 if value is not None:
709 buf.write(id + "=" + value + os.linesep)
711 return buf.getvalue()
714 def output_php(self, encoding = "utf-8"):
716 Return variables as a PHP script.
719 buf = codecs.lookup(encoding)[3](StringIO())
720 buf.write("<?php" + os.linesep)
721 buf.writelines(["// " + line + os.linesep for line in self._header()])
723 for (category_id, (category, variables)) in self._variables.iteritems():
724 for variable in variables.values():
725 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
726 buf.write(os.linesep)
728 buf.write("// " + name + os.linesep)
729 if comments is not None:
730 buf.writelines(["// " + line + os.linesep for line in comments])
733 buf.write("define('%s', %s);" % (id, value) + os.linesep)
735 buf.write("?>" + os.linesep)
737 return buf.getvalue()
740 def output_xml(self, encoding = "utf-8"):
742 Return variables in original XML format.
745 buf = codecs.lookup(encoding)[3](StringIO())
746 self._dom.writexml(buf, addindent = " ", indent = "", newl = "\n", encoding = encoding)
748 return buf.getvalue()
751 def output_variables(self, encoding = "utf-8"):
753 Return list of all variable names.
756 buf = codecs.lookup(encoding)[3](StringIO())
758 for (category_id, (category, variables)) in self._variables.iteritems():
759 for variable in variables.values():
760 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
761 buf.write(id + os.linesep)
763 return buf.getvalue()
766 def output_packages(self, encoding = "utf-8"):
768 Return list of all packages.
771 buf = codecs.lookup(encoding)[3](StringIO())
773 for (group, packages) in self._packages.values():
774 buf.write(os.linesep.join(packages.keys()))
777 buf.write(os.linesep)
779 return buf.getvalue()
782 def output_groups(self, encoding = "utf-8"):
784 Return list of all package group names.
787 buf = codecs.lookup(encoding)[3](StringIO())
789 for (group, packages) in self._packages.values():
790 buf.write(group['name'] + os.linesep)
792 return buf.getvalue()
795 def output_comps(self, encoding = "utf-8"):
797 Return <comps> section of configuration.
800 if self._dom is None or \
801 not self._dom.getElementsByTagName("comps"):
803 comps = self._dom.getElementsByTagName("comps")[0]
805 impl = xml.dom.minidom.getDOMImplementation()
806 doc = impl.createDocument(None, "comps", None)
808 buf = codecs.lookup(encoding)[3](StringIO())
810 # Pop it off the DOM temporarily
811 parent = comps.parentNode
812 parent.removeChild(comps)
814 doc.replaceChild(comps, doc.documentElement)
815 doc.writexml(buf, encoding = encoding)
818 parent.appendChild(comps)
820 return buf.getvalue()
823 # xml.dom.minidom.Text.writexml adds surrounding whitespace to textual
824 # data when pretty-printing. Override this behavior.
825 class TrimText(xml.dom.minidom.Text):
826 def writexml(self, writer, indent="", addindent="", newl=""):
827 xml.dom.minidom.Text.writexml(self, writer, "", "", "")
830 class TrimTextElement(xml.dom.minidom.Element):
831 def writexml(self, writer, indent="", addindent="", newl=""):
833 xml.dom.minidom.Element.writexml(self, writer, "", "", "")
837 if __name__ == '__main__':
839 if len(sys.argv) > 1 and sys.argv[1] in ['build', 'install', 'uninstall']:
840 from distutils.core import setup
841 setup(py_modules=["plc_config"])