- use preferred lowercase define() in PHP generation
[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: plc_config.py,v 1.1.1.1 2006/03/27 17:36:46 mlhuang Exp $
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, 'r+')
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 get_package(self, group_id, package_name):
392         """
393         Get the specified package in the specified package group.
394
395         Arguments:
396
397         group_id - unique group id (e.g., 'plc')
398         package_name - unique package name (e.g., 'postgresql')
399
400         Returns:
401
402         package = { 'name': "package_name",
403                     'type': "mandatory|optional" }
404         """
405
406         if self._packages.has_key(group_id.lower()):
407             (group, packages) = self._packages[group_id]
408             if packages.has_key(package_name):
409                 package = packages[package_name]
410             else:
411                 package = None
412         else:
413             group = None
414             package = None
415
416         return (group, package)
417
418
419     def delete_package(self, group_id, package_name):
420         """
421         Deletes the specified variable from the specified category. If
422         variable_id is None, deletes all variables from the specified
423         category as well as the category itself.
424
425         Arguments:
426
427         group_id - unique group id (e.g., 'plc')
428         package_name - unique package name (e.g., 'postgresql')
429         """
430
431         if self._packages.has_key(group_id):
432             (group, packages) = self._packages[group_id]
433             if package_name is None:
434                 group['element'].parentNode.removeChild(group['element'])
435                 del self._packages[group_id]
436             elif packages.has_key(package_name.lower()):
437                 package = packages[package_name]
438                 package['element'].parentNode.removeChild(package['element'])
439                 del packages[package_name]
440
441
442     def add_package(self, group, package):
443         """
444         Add and/or update the specified package. The 'id' and 'name'
445         fields are mandatory. If a field is not specified and the
446         package or group already exists, the field will not be
447         updated. If package is None, only adds/or updates the
448         specified group.
449
450         Arguments:
451
452         group = { 'id': "group_identifier",
453                   'name': "Group name",
454                   'default': "true|false",
455                   'description': "Group description",
456                   'uservisible': "true|false" }
457
458         package = { 'name': "package_name",
459                     'type': "mandatory|optional" }
460         """
461
462         if not group.has_key('id'):
463             return
464
465         group_id = group['id']
466
467         if self._packages.has_key(group_id):
468             # Existing group
469             (old_group, packages) = self._packages[group_id]
470
471             # Merge group attributes
472             for tag in ['id', 'name', 'default', 'description', 'uservisible']:
473                 if group.has_key(tag):
474                     old_group[tag] = group[tag]
475                     self._set_text_of_child(old_group['element'], tag, group[tag])
476
477             group_element = old_group['element']
478         else:
479             # Merge into DOM
480             group_element = self._dom.createElement('group')
481             for tag in ['id', 'name', 'default', 'description', 'uservisible']:
482                 if group.has_key(tag):
483                     self._set_text_of_child(group_element, tag, group[tag])
484
485             if self._dom.documentElement.getElementsByTagName('comps'):
486                 comps_element = self._dom.documentElement.getElementsByTagName('comps')[0]
487             else:
488                 comps_element = self._dom.createElement('comps')
489                 self._dom.documentElement.appendChild(comps_element)
490             comps_element.appendChild(group_element)
491
492             # Cache it
493             group['element'] = group_element
494             packages = {}
495             self._packages[group_id] = (group, packages)
496
497         if package is None or not package.has_key('name'):
498             return
499
500         package_name = package['name']
501         if packages.has_key(package_name):
502             # Existing package
503             old_package = packages[package_name]
504
505             # Merge variable attributes
506             for attribute in ['type']:
507                 if package.has_key(attribute):
508                     old_package[attribute] = package[attribute]
509                     old_package['element'].setAttribute(attribute, package[attribute])
510         else:
511             # Merge into DOM
512             packagereq_element = TrimTextElement('packagereq')
513             self._set_text(packagereq_element, package_name)
514             for attribute in ['type']:
515                 if package.has_key(attribute):
516                     packagereq_element.setAttribute(attribute, package[attribute])
517                 
518             if group_element.getElementsByTagName('packagelist'):
519                 packagelist_element = group_element.getElementsByTagName('packagelist')[0]
520             else:
521                 packagelist_element = self._dom.createElement('packagelist')
522                 group_element.appendChild(packagelist_element)
523             packagelist_element.appendChild(packagereq_element)
524
525             # Cache it
526             package['element'] = packagereq_element
527             packages[package_name] = package
528
529
530     def variables(self):
531         """
532         Return all variables.
533
534         Returns:
535
536         variables = { 'category_id': (category, variablelist) }
537
538         category = { 'id': "category_identifier",
539                      'name': "Category name",
540                      'description': "Category description" }
541
542         variablelist = { 'variable_id': variable }
543
544         variable = { 'id': "variable_identifier",
545                      'type': "variable_type",
546                      'value': "variable_value",
547                      'name': "Variable name",
548                      'description': "Variable description" }
549         """
550
551         return self._variables
552
553
554     def packages(self):
555         """
556         Return all packages.
557
558         Returns:
559
560         packages = { 'group_id': (group, packagelist) }
561
562         group = { 'id': "group_identifier",
563                   'name': "Group name",
564                   'default': "true|false",
565                   'description': "Group description",
566                   'uservisible': "true|false" }
567
568         packagelist = { 'package_name': package }
569
570         package = { 'name': "package_name",
571                     'type': "mandatory|optional" }
572         """
573
574         return self._packages
575
576
577     def _sanitize_variable(self, category_id, variable):
578         assert variable.has_key('id')
579         # Prepend variable name with category label
580         id = category_id + "_" + variable['id']
581         # And uppercase it
582         id = id.upper()
583
584         if variable.has_key('type'):
585             type = variable['type']
586         else:
587             type = None
588
589         if variable.has_key('name'):
590             name = variable['name']
591         else:
592             name = None
593
594         if variable.has_key('value') and variable['value'] is not None:
595             value = variable['value']
596             if type == "int" or type == "double":
597                 # bash, Python, and PHP do not require that numbers be quoted
598                 pass
599             elif type == "boolean":
600                 # bash, Python, and PHP can all agree on 0 and 1
601                 if value == "true":
602                     value = "1"
603                 else:
604                     value = "0"
605             else:
606                 # bash, Python, and PHP all support strong single quoting
607                 value = "'" + value.replace("'", "\\'") + "'"
608         else:
609             value = None
610
611         if variable.has_key('description') and variable['description'] is not None:
612             description = variable['description']
613             # Collapse consecutive whitespace
614             description = re.sub(r'\s+', ' ', description)
615             # Wrap comments at 70 columns
616             wrapper = textwrap.TextWrapper()
617             comments = wrapper.wrap(description)
618         else:
619             comments = None
620
621         return (id, name, value, comments)
622
623
624     def _header(self):
625         header = """
626 DO NOT EDIT. This file was automatically generated at
627 %s from:
628
629 %s
630 """ % (time.asctime(), os.linesep.join(self._files))
631
632         # Get rid of the surrounding newlines
633         return header.strip().split(os.linesep)
634
635
636     def output_shell(self, encoding = "utf-8"):
637         """
638         Return variables as a shell script.
639         """
640
641         buf = codecs.lookup(encoding)[3](StringIO())
642         buf.writelines(["# " + line + os.linesep for line in self._header()])
643
644         for (category_id, (category, variables)) in self._variables.iteritems():
645             for variable in variables.values():
646                 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
647                 buf.write(os.linesep)
648                 if name is not None:
649                     buf.write("# " + name + os.linesep)
650                 if comments is not None:
651                     buf.writelines(["# " + line + os.linesep for line in comments])
652                 # bash does not have the concept of NULL
653                 if value is not None:
654                     buf.write(id + "=" + value + os.linesep)
655
656         return buf.getvalue()
657
658
659     def output_php(self, encoding = "utf-8"):
660         """
661         Return variables as a PHP script.
662         """
663
664         buf = codecs.lookup(encoding)[3](StringIO())
665         buf.write("<?php" + os.linesep)
666         buf.writelines(["// " + line + os.linesep for line in self._header()])
667
668         for (category_id, (category, variables)) in self._variables.iteritems():
669             for variable in variables.values():
670                 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
671                 buf.write(os.linesep)
672                 if name is not None:
673                     buf.write("// " + name + os.linesep)
674                 if comments is not None:
675                     buf.writelines(["// " + line + os.linesep for line in comments])
676                 if value is None:
677                     value = 'NULL'
678                 buf.write("define('%s', %s);" % (id, value) + os.linesep)
679
680         buf.write("?>" + os.linesep)
681
682         return buf.getvalue()
683
684
685     def output_xml(self, encoding = "utf-8"):
686         """
687         Return variables in original XML format.
688         """
689
690         buf = codecs.lookup(encoding)[3](StringIO())
691         self._dom.writexml(buf, addindent = "  ", indent = "", newl = "\n", encoding = encoding)
692
693         return buf.getvalue()
694
695
696     def output_variables(self, encoding = "utf-8"):
697         """
698         Return list of all variable names.
699         """
700
701         buf = codecs.lookup(encoding)[3](StringIO())
702
703         for (category_id, (category, variables)) in self._variables.iteritems():
704             for variable in variables.values():
705                 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
706                 buf.write(id + os.linesep)
707
708         return buf.getvalue()
709
710
711     def output_packages(self, encoding = "utf-8"):
712         """
713         Return list of all packages.
714         """
715
716         buf = codecs.lookup(encoding)[3](StringIO())
717
718         for (group, packages) in self._packages.values():
719             buf.write(os.linesep.join(packages.keys()))
720
721         if buf.tell():
722             buf.write(os.linesep)
723
724         return buf.getvalue()
725
726
727     def output_comps(self, encoding = "utf-8"):
728         """
729         Return <comps> section of configuration.
730         """
731
732         if self._dom is None or \
733            not self._dom.getElementsByTagName("comps"):
734             return
735         comps = self._dom.getElementsByTagName("comps")[0]
736
737         impl = xml.dom.minidom.getDOMImplementation()
738         doc = impl.createDocument(None, "comps", None)
739
740         buf = codecs.lookup(encoding)[3](StringIO())
741
742         # Pop it off the DOM temporarily
743         parent = comps.parentNode
744         parent.removeChild(comps)
745
746         doc.replaceChild(comps, doc.documentElement)
747         doc.writexml(buf, encoding = encoding)
748
749         # Put it back
750         parent.appendChild(comps)
751
752         return buf.getvalue()
753
754
755 # xml.dom.minidom.Text.writexml adds surrounding whitespace to textual
756 # data when pretty-printing. Override this behavior.
757 class TrimText(xml.dom.minidom.Text):
758     def writexml(self, writer, indent="", addindent="", newl=""):
759         xml.dom.minidom.Text.writexml(self, writer, "", "", "")
760
761
762 class TrimTextElement(xml.dom.minidom.Element):
763     def writexml(self, writer, indent="", addindent="", newl=""):
764         writer.write(indent)
765         xml.dom.minidom.Element.writexml(self, writer, "", "", "")
766         writer.write(newl)
767
768
769 if __name__ == '__main__':
770     import sys
771     if len(sys.argv) > 1 and sys.argv[1] in ['build', 'install']:
772         from distutils.core import setup
773         setup(py_modules=["plc_config"])