propset - enables svn keywords
[myplc.git] / plc_config.py
1 #!/usr/bin/python
2 #
3 # Merge PlanetLab Central (PLC) configuration files into a variety of
4 # output formats. These files represent the global configuration for a
5 # PLC installation.
6 #
7 # Mark Huang <mlhuang@cs.princeton.edu>
8 # Copyright (C) 2006 The Trustees of Princeton University
9 #
10 # $Id$
11 #
12
13 import xml.dom.minidom
14 from StringIO import StringIO
15 import time
16 import re
17 import textwrap
18 import codecs
19 import os
20 import types
21
22
23 class PLCConfiguration:
24     """
25     Configuration file store. Optionally instantiate with a file path
26     or object:
27
28     plc = PLCConfiguration()
29     plc = PLCConfiguration(fileobj)
30     plc = PLCConfiguration("/etc/planetlab/plc_config.xml")
31
32     You may load() additional files later, which will be merged into
33     the current configuration:
34
35     plc.load("/etc/planetlab/local.xml")
36
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.
40     
41     plc.save()
42     plc.save("/etc/planetlab/plc_config.xml")
43     """
44
45     def __init__(self, file = None):
46         impl = xml.dom.minidom.getDOMImplementation()
47         self._dom = impl.createDocument(None, "configuration", None)
48         self._variables = {}
49         self._packages = {}
50         self._files = []
51
52         if file is not None:
53             self.load(file)
54
55
56     def _get_text(self, node):
57         """
58         Get the text of a text node.
59         """
60
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
65                 return ""
66             else:
67                 return node.firstChild.data
68
69         return None
70
71
72     def _get_text_of_child(self, parent, name):
73         """
74         Get the text of a (direct) child text node.
75         """
76
77         for node in parent.childNodes:
78             if node.nodeType == node.ELEMENT_NODE and \
79                node.tagName == name:
80                 return self._get_text(node)
81
82         return None
83
84
85     def _set_text(self, node, data):
86         """
87         Set the text of a text node.
88         """
89
90         if node.firstChild and \
91            node.firstChild.nodeType == node.TEXT_NODE:
92             if data is None:
93                 node.removeChild(node.firstChild)
94             else:
95                 node.firstChild.data = data
96         elif data is not None:
97             text = TrimText()
98             text.data = data
99             node.appendChild(text)
100
101
102     def _set_text_of_child(self, parent, name, data):
103         """
104         Set the text of a (direct) child text node.
105         """
106
107         for node in parent.childNodes:
108             if node.nodeType == node.ELEMENT_NODE and \
109                node.tagName == name:
110                 self._set_text(node, data)
111                 return
112
113         child = TrimTextElement(name)
114         self._set_text(child, data)
115         parent.appendChild(child)
116
117
118     def _category_element_to_dict(self, category_element):
119         """
120         Turn a <category> element into a dictionary of its attributes
121         and child text nodes.
122         """
123
124         category = {}
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
131
132         return category
133
134
135     def _variable_element_to_dict(self, variable_element):
136         """
137         Turn a <variable> element into a dictionary of its attributes
138         and child text nodes.
139         """
140
141         variable = {}
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
150
151         return variable
152
153
154     def _group_element_to_dict(self, group_element):
155         """
156         Turn a <group> element into a dictionary of its attributes
157         and child text nodes.
158         """
159
160         group = {}
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
166
167         return group
168
169
170     def _packagereq_element_to_dict(self, packagereq_element):
171         """
172         Turns a <packagereq> element into a dictionary of its attributes
173         and child text nodes.
174         """
175
176         package = {}
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
181
182         return package
183
184
185     def load(self, file = "/etc/planetlab/plc_config.xml"):
186         """
187         Merge file into configuration store.
188         """
189
190         dom = xml.dom.minidom.parse(file)
191         if type(file) in types.StringTypes:
192             self._files.append(os.path.abspath(file))
193
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)
199
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)
204
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)
210
211                 for packagereq_element in group_element.getElementsByTagName('packagereq'):
212                     package = self._packagereq_element_to_dict(packagereq_element)
213                     self.add_package(group, package)
214
215
216     def save(self, file = None):
217         """
218         Write configuration store to file.
219         """
220
221         if file is None:
222             if self._files:
223                 file = self._files[0]
224             else:
225                 file = "/etc/planetlab/plc_config.xml"
226
227         if type(file) in types.StringTypes:
228             fileobj = open(file, 'w')
229         else:
230             fileobj = file
231
232         fileobj.seek(0)
233         fileobj.write(self.output_xml())
234         fileobj.truncate()
235
236         fileobj.close()
237
238
239     def get(self, category_id, variable_id):
240         """
241         Get the specified variable in the specified category.
242
243         Arguments:
244
245         category_id = unique category identifier (e.g., 'plc_www')
246         variable_id = unique variable identifier (e.g., 'port')
247
248         Returns:
249
250         variable = { 'id': "variable_identifier",
251                      'type': "variable_type",
252                      'value': "variable_value",
253                      'name': "Variable name",
254                      'description': "Variable description" }
255         """
256
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]
261             else:
262                 variable = None
263         else:
264             category = None
265             variable = None
266
267         return (category, variable)
268
269
270     def delete(self, category_id, variable_id):
271         """
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.
275
276         Arguments:
277
278         category_id = unique category identifier (e.g., 'plc_www')
279         variable_id = unique variable identifier (e.g., 'port')
280         """
281
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]
291
292
293     def set(self, category, variable):
294         """
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
299         category.
300
301         Arguments:
302
303         category = { 'id': "category_identifier",
304                      'name': "Category name",
305                      'description': "Category description" }
306
307         variable = { 'id': "variable_identifier",
308                      'type': "variable_type",
309                      'value': "variable_value",
310                      'name': "Variable name",
311                      'description': "Variable description" }
312         """
313
314         if not category.has_key('id') or type(category['id']) not in types.StringTypes:
315             return
316         
317         category_id = category['id'].lower()
318
319         if self._variables.has_key(category_id):
320             # Existing category
321             (old_category, variables) = self._variables[category_id]
322
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])
328
329             category_element = old_category['element']
330         else:
331             # Merge into DOM
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])
337
338             if self._dom.documentElement.getElementsByTagName('variables'):
339                 variables_element = self._dom.documentElement.getElementsByTagName('variables')[0]
340             else:
341                 variables_element = self._dom.createElement('variables')
342                 self._dom.documentElement.appendChild(variables_element)
343             variables_element.appendChild(category_element)
344
345             # Cache it
346             category['element'] = category_element
347             variables = {}
348             self._variables[category_id] = (category, variables)
349
350         if variable is None or not variable.has_key('id') or type(variable['id']) not in types.StringTypes:
351             return
352
353         variable_id = variable['id'].lower()
354
355         if variables.has_key(variable_id):
356             # Existing variable
357             old_variable = variables[variable_id]
358
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])
368         else:
369             # Merge into DOM
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])
378                 
379             if category_element.getElementsByTagName('variablelist'):
380                 variablelist_element = category_element.getElementsByTagName('variablelist')[0]
381             else:
382                 variablelist_element = self._dom.createElement('variablelist')
383                 category_element.appendChild(variablelist_element)
384             variablelist_element.appendChild(variable_element)
385
386             # Cache it
387             variable['element'] = variable_element
388             variables[variable_id] = variable
389
390
391     def locate_varname (self, varname):
392         """
393         Locates category and variable from a variable's (shell) name
394
395         Returns:
396         (variable, category) when found
397         (None, None) otherwise
398         """
399         
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)
403                 if (id == varname):
404                     return (category,variable)
405         return (None,None)
406
407     def get_package(self, group_id, package_name):
408         """
409         Get the specified package in the specified package group.
410
411         Arguments:
412
413         group_id - unique group id (e.g., 'plc')
414         package_name - unique package name (e.g., 'postgresql')
415
416         Returns:
417
418         package = { 'name': "package_name",
419                     'type': "mandatory|optional" }
420         """
421
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]
426             else:
427                 package = None
428         else:
429             group = None
430             package = None
431
432         return (group, package)
433
434
435     def delete_package(self, group_id, package_name):
436         """
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.
440
441         Arguments:
442
443         group_id - unique group id (e.g., 'plc')
444         package_name - unique package name (e.g., 'postgresql')
445         """
446
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]
456
457
458     def add_package(self, group, package):
459         """
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
464         specified group.
465
466         Arguments:
467
468         group = { 'id': "group_identifier",
469                   'name': "Group name",
470                   'default': "true|false",
471                   'description': "Group description",
472                   'uservisible': "true|false" }
473
474         package = { 'name': "package_name",
475                     'type': "mandatory|optional" }
476         """
477
478         if not group.has_key('id'):
479             return
480
481         group_id = group['id']
482
483         if self._packages.has_key(group_id):
484             # Existing group
485             (old_group, packages) = self._packages[group_id]
486
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])
492
493             group_element = old_group['element']
494         else:
495             # Merge into DOM
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])
500
501             if self._dom.documentElement.getElementsByTagName('comps'):
502                 comps_element = self._dom.documentElement.getElementsByTagName('comps')[0]
503             else:
504                 comps_element = self._dom.createElement('comps')
505                 self._dom.documentElement.appendChild(comps_element)
506             comps_element.appendChild(group_element)
507
508             # Cache it
509             group['element'] = group_element
510             packages = {}
511             self._packages[group_id] = (group, packages)
512
513         if package is None or not package.has_key('name'):
514             return
515
516         package_name = package['name']
517         if packages.has_key(package_name):
518             # Existing package
519             old_package = packages[package_name]
520
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])
526         else:
527             # Merge into DOM
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])
533                 
534             if group_element.getElementsByTagName('packagelist'):
535                 packagelist_element = group_element.getElementsByTagName('packagelist')[0]
536             else:
537                 packagelist_element = self._dom.createElement('packagelist')
538                 group_element.appendChild(packagelist_element)
539             packagelist_element.appendChild(packagereq_element)
540
541             # Cache it
542             package['element'] = packagereq_element
543             packages[package_name] = package
544
545
546     def variables(self):
547         """
548         Return all variables.
549
550         Returns:
551
552         variables = { 'category_id': (category, variablelist) }
553
554         category = { 'id': "category_identifier",
555                      'name': "Category name",
556                      'description': "Category description" }
557
558         variablelist = { 'variable_id': variable }
559
560         variable = { 'id': "variable_identifier",
561                      'type': "variable_type",
562                      'value': "variable_value",
563                      'name': "Variable name",
564                      'description': "Variable description" }
565         """
566
567         return self._variables
568
569
570     def packages(self):
571         """
572         Return all packages.
573
574         Returns:
575
576         packages = { 'group_id': (group, packagelist) }
577
578         group = { 'id': "group_identifier",
579                   'name': "Group name",
580                   'default': "true|false",
581                   'description': "Group description",
582                   'uservisible': "true|false" }
583
584         packagelist = { 'package_name': package }
585
586         package = { 'name': "package_name",
587                     'type': "mandatory|optional" }
588         """
589
590         return self._packages
591
592
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']
597         # And uppercase it
598         id = id.upper()
599
600         if variable.has_key('type'):
601             type = variable['type']
602         else:
603             type = None
604
605         if variable.has_key('name'):
606             name = variable['name']
607         else:
608             name = None
609
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
614                 pass
615             elif type == "boolean":
616                 # bash, Python, and PHP can all agree on 0 and 1
617                 if value == "true":
618                     value = "1"
619                 else:
620                     value = "0"
621             else:
622                 # bash, Python, and PHP all support strong single quoting
623                 value = "'" + value.replace("'", "\\'") + "'"
624         else:
625             value = None
626
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)
634         else:
635             comments = None
636
637         return (id, name, value, comments)
638
639
640     def _header(self):
641         header = """
642 DO NOT EDIT. This file was automatically generated at
643 %s from:
644
645 %s
646 """ % (time.asctime(), os.linesep.join(self._files))
647
648         # Get rid of the surrounding newlines
649         return header.strip().split(os.linesep)
650
651
652     def output_shell(self, show_comments = True, encoding = "utf-8"):
653         """
654         Return variables as a shell script.
655         """
656
657         buf = codecs.lookup(encoding)[3](StringIO())
658         buf.writelines(["# " + line + os.linesep for line in self._header()])
659
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)
663                 if show_comments:
664                     buf.write(os.linesep)
665                     if name is not None:
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)
672
673         return buf.getvalue()
674
675
676     def output_php(self, encoding = "utf-8"):
677         """
678         Return variables as a PHP script.
679         """
680
681         buf = codecs.lookup(encoding)[3](StringIO())
682         buf.write("<?php" + os.linesep)
683         buf.writelines(["// " + line + os.linesep for line in self._header()])
684
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)
689                 if name is not None:
690                     buf.write("// " + name + os.linesep)
691                 if comments is not None:
692                     buf.writelines(["// " + line + os.linesep for line in comments])
693                 if value is None:
694                     value = 'NULL'
695                 buf.write("define('%s', %s);" % (id, value) + os.linesep)
696
697         buf.write("?>" + os.linesep)
698
699         return buf.getvalue()
700
701
702     def output_xml(self, encoding = "utf-8"):
703         """
704         Return variables in original XML format.
705         """
706
707         buf = codecs.lookup(encoding)[3](StringIO())
708         self._dom.writexml(buf, addindent = "  ", indent = "", newl = "\n", encoding = encoding)
709
710         return buf.getvalue()
711
712
713     def output_variables(self, encoding = "utf-8"):
714         """
715         Return list of all variable names.
716         """
717
718         buf = codecs.lookup(encoding)[3](StringIO())
719
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)
724
725         return buf.getvalue()
726
727
728     def output_packages(self, encoding = "utf-8"):
729         """
730         Return list of all packages.
731         """
732
733         buf = codecs.lookup(encoding)[3](StringIO())
734
735         for (group, packages) in self._packages.values():
736             buf.write(os.linesep.join(packages.keys()))
737
738         if buf.tell():
739             buf.write(os.linesep)
740
741         return buf.getvalue()
742
743
744     def output_groups(self, encoding = "utf-8"):
745         """
746         Return list of all package group names.
747         """
748
749         buf = codecs.lookup(encoding)[3](StringIO())
750
751         for (group, packages) in self._packages.values():
752             buf.write(group['name'] + os.linesep)
753
754         return buf.getvalue()
755
756
757     def output_comps(self, encoding = "utf-8"):
758         """
759         Return <comps> section of configuration.
760         """
761
762         if self._dom is None or \
763            not self._dom.getElementsByTagName("comps"):
764             return
765         comps = self._dom.getElementsByTagName("comps")[0]
766
767         impl = xml.dom.minidom.getDOMImplementation()
768         doc = impl.createDocument(None, "comps", None)
769
770         buf = codecs.lookup(encoding)[3](StringIO())
771
772         # Pop it off the DOM temporarily
773         parent = comps.parentNode
774         parent.removeChild(comps)
775
776         doc.replaceChild(comps, doc.documentElement)
777         doc.writexml(buf, encoding = encoding)
778
779         # Put it back
780         parent.appendChild(comps)
781
782         return buf.getvalue()
783
784
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, "", "", "")
790
791
792 class TrimTextElement(xml.dom.minidom.Element):
793     def writexml(self, writer, indent="", addindent="", newl=""):
794         writer.write(indent)
795         xml.dom.minidom.Element.writexml(self, writer, "", "", "")
796         writer.write(newl)
797
798
799 if __name__ == '__main__':
800     import sys
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"])