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 (category,maint_user) = self.get('plc_api', 'maintenance_user')
258 if maint_user == None:
259 (category, maint_user) = read.get('plc_api', 'maintenance_user')
260 if maint_user == None:
261 (category,maint_user) = default.get('plc_api', 'maintenance_user')
262 if maint_user == None:
263 raise ConfigurationException("Cannot find PLC_API_MAINTENANCE_USER")
265 (category,root_user) = self.get('plc', 'root_user')
266 if root_user == None:
267 (category,root_user) = read.get('plc', 'root_user')
268 if root_user == None:
269 root_user = default.get('plc', 'root_user')
270 if root_user == None:
271 raise ConfigurationException("Cannot find PLC_ROOT_USER")
273 muser= maint_user['value']
274 ruser= root_user['value']
277 raise ConfigurationException("The Maintenance Account email address cannot be the same as the Root User email address")
281 def get(self, category_id, variable_id):
283 Get the specified variable in the specified category.
287 category_id = unique category identifier (e.g., 'plc_www')
288 variable_id = unique variable identifier (e.g., 'port')
292 variable = { 'id': "variable_identifier",
293 'type': "variable_type",
294 'value': "variable_value",
295 'name': "Variable name",
296 'description': "Variable description" }
299 if self._variables.has_key(category_id.lower()):
300 (category, variables) = self._variables[category_id]
301 if variables.has_key(variable_id.lower()):
302 variable = variables[variable_id]
309 return (category, variable)
312 def delete(self, category_id, variable_id):
314 Delete the specified variable from the specified category. If
315 variable_id is None, deletes all variables from the specified
316 category as well as the category itself.
320 category_id = unique category identifier (e.g., 'plc_www')
321 variable_id = unique variable identifier (e.g., 'port')
324 if self._variables.has_key(category_id.lower()):
325 (category, variables) = self._variables[category_id]
326 if variable_id is None:
327 category['element'].parentNode.removeChild(category['element'])
328 del self._variables[category_id]
329 elif variables.has_key(variable_id.lower()):
330 variable = variables[variable_id]
331 variable['element'].parentNode.removeChild(variable['element'])
332 del variables[variable_id]
335 def set(self, category, variable):
337 Add and/or update the specified variable. The 'id' fields are
338 mandatory. If a field is not specified and the category and/or
339 variable already exists, the field will not be updated. If
340 'variable' is None, only adds and/or updates the specified
345 category = { 'id': "category_identifier",
346 'name': "Category name",
347 'description': "Category description" }
349 variable = { 'id': "variable_identifier",
350 'type': "variable_type",
351 'value': "variable_value",
352 'name': "Variable name",
353 'description': "Variable description" }
356 if not category.has_key('id') or type(category['id']) not in types.StringTypes:
359 category_id = category['id'].lower()
361 if self._variables.has_key(category_id):
363 (old_category, variables) = self._variables[category_id]
365 # Merge category attributes
366 for tag in ['name', 'description']:
367 if category.has_key(tag):
368 old_category[tag] = category[tag]
369 self._set_text_of_child(old_category['element'], tag, category[tag])
371 category_element = old_category['element']
374 category_element = self._dom.createElement('category')
375 category_element.setAttribute('id', category_id)
376 for tag in ['name', 'description']:
377 if category.has_key(tag):
378 self._set_text_of_child(category_element, tag, category[tag])
380 if self._dom.documentElement.getElementsByTagName('variables'):
381 variables_element = self._dom.documentElement.getElementsByTagName('variables')[0]
383 variables_element = self._dom.createElement('variables')
384 self._dom.documentElement.appendChild(variables_element)
385 variables_element.appendChild(category_element)
388 category['element'] = category_element
390 self._variables[category_id] = (category, variables)
392 if variable is None or not variable.has_key('id') or type(variable['id']) not in types.StringTypes:
395 variable_id = variable['id'].lower()
397 if variables.has_key(variable_id):
399 old_variable = variables[variable_id]
401 # Merge variable attributes
402 for attribute in ['type']:
403 if variable.has_key(attribute):
404 old_variable[attribute] = variable[attribute]
405 old_variable['element'].setAttribute(attribute, variable[attribute])
406 for tag in ['name', 'value', 'description']:
407 if variable.has_key(tag):
408 old_variable[tag] = variable[tag]
409 self._set_text_of_child(old_variable['element'], tag, variable[tag])
412 variable_element = self._dom.createElement('variable')
413 variable_element.setAttribute('id', variable_id)
414 for attribute in ['type']:
415 if variable.has_key(attribute):
416 variable_element.setAttribute(attribute, variable[attribute])
417 for tag in ['name', 'value', 'description']:
418 if variable.has_key(tag):
419 self._set_text_of_child(variable_element, tag, variable[tag])
421 if category_element.getElementsByTagName('variablelist'):
422 variablelist_element = category_element.getElementsByTagName('variablelist')[0]
424 variablelist_element = self._dom.createElement('variablelist')
425 category_element.appendChild(variablelist_element)
426 variablelist_element.appendChild(variable_element)
429 variable['element'] = variable_element
430 variables[variable_id] = variable
433 def locate_varname (self, varname):
435 Locates category and variable from a variable's (shell) name
438 (variable, category) when found
439 (None, None) otherwise
442 for (category_id, (category, variables)) in self._variables.iteritems():
443 for variable in variables.values():
444 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
446 return (category,variable)
449 def get_package(self, group_id, package_name):
451 Get the specified package in the specified package group.
455 group_id - unique group id (e.g., 'plc')
456 package_name - unique package name (e.g., 'postgresql')
460 package = { 'name': "package_name",
461 'type': "mandatory|optional" }
464 if self._packages.has_key(group_id.lower()):
465 (group, packages) = self._packages[group_id]
466 if packages.has_key(package_name):
467 package = packages[package_name]
474 return (group, package)
477 def delete_package(self, group_id, package_name):
479 Deletes the specified variable from the specified category. If
480 variable_id is None, deletes all variables from the specified
481 category as well as the category itself.
485 group_id - unique group id (e.g., 'plc')
486 package_name - unique package name (e.g., 'postgresql')
489 if self._packages.has_key(group_id):
490 (group, packages) = self._packages[group_id]
491 if package_name is None:
492 group['element'].parentNode.removeChild(group['element'])
493 del self._packages[group_id]
494 elif packages.has_key(package_name.lower()):
495 package = packages[package_name]
496 package['element'].parentNode.removeChild(package['element'])
497 del packages[package_name]
500 def add_package(self, group, package):
502 Add and/or update the specified package. The 'id' and 'name'
503 fields are mandatory. If a field is not specified and the
504 package or group already exists, the field will not be
505 updated. If package is None, only adds/or updates the
510 group = { 'id': "group_identifier",
511 'name': "Group name",
512 'default': "true|false",
513 'description': "Group description",
514 'uservisible': "true|false" }
516 package = { 'name': "package_name",
517 'type': "mandatory|optional" }
520 if not group.has_key('id'):
523 group_id = group['id']
525 if self._packages.has_key(group_id):
527 (old_group, packages) = self._packages[group_id]
529 # Merge group attributes
530 for tag in ['id', 'name', 'default', 'description', 'uservisible']:
531 if group.has_key(tag):
532 old_group[tag] = group[tag]
533 self._set_text_of_child(old_group['element'], tag, group[tag])
535 group_element = old_group['element']
538 group_element = self._dom.createElement('group')
539 for tag in ['id', 'name', 'default', 'description', 'uservisible']:
540 if group.has_key(tag):
541 self._set_text_of_child(group_element, tag, group[tag])
543 if self._dom.documentElement.getElementsByTagName('comps'):
544 comps_element = self._dom.documentElement.getElementsByTagName('comps')[0]
546 comps_element = self._dom.createElement('comps')
547 self._dom.documentElement.appendChild(comps_element)
548 comps_element.appendChild(group_element)
551 group['element'] = group_element
553 self._packages[group_id] = (group, packages)
555 if package is None or not package.has_key('name'):
558 package_name = package['name']
559 if packages.has_key(package_name):
561 old_package = packages[package_name]
563 # Merge variable attributes
564 for attribute in ['type']:
565 if package.has_key(attribute):
566 old_package[attribute] = package[attribute]
567 old_package['element'].setAttribute(attribute, package[attribute])
570 packagereq_element = TrimTextElement('packagereq')
571 self._set_text(packagereq_element, package_name)
572 for attribute in ['type']:
573 if package.has_key(attribute):
574 packagereq_element.setAttribute(attribute, package[attribute])
576 if group_element.getElementsByTagName('packagelist'):
577 packagelist_element = group_element.getElementsByTagName('packagelist')[0]
579 packagelist_element = self._dom.createElement('packagelist')
580 group_element.appendChild(packagelist_element)
581 packagelist_element.appendChild(packagereq_element)
584 package['element'] = packagereq_element
585 packages[package_name] = package
590 Return all variables.
594 variables = { 'category_id': (category, variablelist) }
596 category = { 'id': "category_identifier",
597 'name': "Category name",
598 'description': "Category description" }
600 variablelist = { 'variable_id': variable }
602 variable = { 'id': "variable_identifier",
603 'type': "variable_type",
604 'value': "variable_value",
605 'name': "Variable name",
606 'description': "Variable description" }
609 return self._variables
618 packages = { 'group_id': (group, packagelist) }
620 group = { 'id': "group_identifier",
621 'name': "Group name",
622 'default': "true|false",
623 'description': "Group description",
624 'uservisible': "true|false" }
626 packagelist = { 'package_name': package }
628 package = { 'name': "package_name",
629 'type': "mandatory|optional" }
632 return self._packages
635 def _sanitize_variable(self, category_id, variable):
636 assert variable.has_key('id')
637 # Prepend variable name with category label
638 id = category_id + "_" + variable['id']
642 if variable.has_key('type'):
643 type = variable['type']
647 if variable.has_key('name'):
648 name = variable['name']
652 if variable.has_key('value') and variable['value'] is not None:
653 value = variable['value']
654 if type == "int" or type == "double":
655 # bash, Python, and PHP do not require that numbers be quoted
657 elif type == "boolean":
658 # bash, Python, and PHP can all agree on 0 and 1
664 # bash, Python, and PHP all support strong single quoting
665 value = "'" + value.replace("'", "\\'") + "'"
669 if variable.has_key('description') and variable['description'] is not None:
670 description = variable['description']
671 # Collapse consecutive whitespace
672 description = re.sub(r'\s+', ' ', description)
673 # Wrap comments at 70 columns
674 wrapper = textwrap.TextWrapper()
675 comments = wrapper.wrap(description)
679 return (id, name, value, comments)
684 DO NOT EDIT. This file was automatically generated at
688 """ % (time.asctime(), os.linesep.join(self._files))
690 # Get rid of the surrounding newlines
691 return header.strip().split(os.linesep)
694 def output_shell(self, show_comments = True, encoding = "utf-8"):
696 Return variables as a shell script.
699 buf = codecs.lookup(encoding)[3](StringIO())
700 buf.writelines(["# " + line + os.linesep for line in self._header()])
702 for (category_id, (category, variables)) in self._variables.iteritems():
703 for variable in variables.values():
704 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
706 buf.write(os.linesep)
708 buf.write("# " + name + os.linesep)
709 if comments is not None:
710 buf.writelines(["# " + line + os.linesep for line in comments])
711 # bash does not have the concept of NULL
712 if value is not None:
713 buf.write(id + "=" + value + os.linesep)
715 return buf.getvalue()
718 def output_php(self, encoding = "utf-8"):
720 Return variables as a PHP script.
723 buf = codecs.lookup(encoding)[3](StringIO())
724 buf.write("<?php" + os.linesep)
725 buf.writelines(["// " + line + os.linesep for line in self._header()])
727 for (category_id, (category, variables)) in self._variables.iteritems():
728 for variable in variables.values():
729 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
730 buf.write(os.linesep)
732 buf.write("// " + name + os.linesep)
733 if comments is not None:
734 buf.writelines(["// " + line + os.linesep for line in comments])
737 buf.write("define('%s', %s);" % (id, value) + os.linesep)
739 buf.write("?>" + os.linesep)
741 return buf.getvalue()
744 def output_xml(self, encoding = "utf-8"):
746 Return variables in original XML format.
749 buf = codecs.lookup(encoding)[3](StringIO())
750 self._dom.writexml(buf, addindent = " ", indent = "", newl = "\n", encoding = encoding)
752 return buf.getvalue()
755 def output_variables(self, encoding = "utf-8"):
757 Return list of all variable names.
760 buf = codecs.lookup(encoding)[3](StringIO())
762 for (category_id, (category, variables)) in self._variables.iteritems():
763 for variable in variables.values():
764 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
765 buf.write(id + os.linesep)
767 return buf.getvalue()
770 def output_packages(self, encoding = "utf-8"):
772 Return list of all packages.
775 buf = codecs.lookup(encoding)[3](StringIO())
777 for (group, packages) in self._packages.values():
778 buf.write(os.linesep.join(packages.keys()))
781 buf.write(os.linesep)
783 return buf.getvalue()
786 def output_groups(self, encoding = "utf-8"):
788 Return list of all package group names.
791 buf = codecs.lookup(encoding)[3](StringIO())
793 for (group, packages) in self._packages.values():
794 buf.write(group['name'] + os.linesep)
796 return buf.getvalue()
799 def output_comps(self, encoding = "utf-8"):
801 Return <comps> section of configuration.
804 if self._dom is None or \
805 not self._dom.getElementsByTagName("comps"):
807 comps = self._dom.getElementsByTagName("comps")[0]
809 impl = xml.dom.minidom.getDOMImplementation()
810 doc = impl.createDocument(None, "comps", None)
812 buf = codecs.lookup(encoding)[3](StringIO())
814 # Pop it off the DOM temporarily
815 parent = comps.parentNode
816 parent.removeChild(comps)
818 doc.replaceChild(comps, doc.documentElement)
819 doc.writexml(buf, encoding = encoding)
822 parent.appendChild(comps)
824 return buf.getvalue()
827 # xml.dom.minidom.Text.writexml adds surrounding whitespace to textual
828 # data when pretty-printing. Override this behavior.
829 class TrimText(xml.dom.minidom.Text):
830 def writexml(self, writer, indent="", addindent="", newl=""):
831 xml.dom.minidom.Text.writexml(self, writer, "", "", "")
834 class TrimTextElement(xml.dom.minidom.Element):
835 def writexml(self, writer, indent="", addindent="", newl=""):
837 xml.dom.minidom.Element.writexml(self, writer, "", "", "")
841 if __name__ == '__main__':
843 if len(sys.argv) > 1 and sys.argv[1] in ['build', 'install', 'uninstall']:
844 from distutils.core import setup
845 setup(py_modules=["plc_config"])