verify phase fixed and re-enabled
[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 ConfigurationException(Exception): pass
24
25 class PLCConfiguration:
26     """
27     Configuration file store. Optionally instantiate with a file path
28     or object:
29
30     plc = PLCConfiguration()
31     plc = PLCConfiguration(fileobj)
32     plc = PLCConfiguration("/etc/planetlab/plc_config.xml")
33
34     You may load() additional files later, which will be merged into
35     the current configuration:
36
37     plc.load("/etc/planetlab/local.xml")
38
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.
42     
43     plc.save()
44     plc.save("/etc/planetlab/plc_config.xml")
45     """
46
47     def __init__(self, file = None):
48         impl = xml.dom.minidom.getDOMImplementation()
49         self._dom = impl.createDocument(None, "configuration", None)
50         self._variables = {}
51         self._packages = {}
52         self._files = []
53
54         if file is not None:
55             self.load(file)
56
57
58     def _get_text(self, node):
59         """
60         Get the text of a text node.
61         """
62
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
67                 return ""
68             else:
69                 return node.firstChild.data
70
71         return None
72
73
74     def _get_text_of_child(self, parent, name):
75         """
76         Get the text of a (direct) child text node.
77         """
78
79         for node in parent.childNodes:
80             if node.nodeType == node.ELEMENT_NODE and \
81                node.tagName == name:
82                 return self._get_text(node)
83
84         return None
85
86
87     def _set_text(self, node, data):
88         """
89         Set the text of a text node.
90         """
91
92         if node.firstChild and \
93            node.firstChild.nodeType == node.TEXT_NODE:
94             if data is None:
95                 node.removeChild(node.firstChild)
96             else:
97                 node.firstChild.data = data
98         elif data is not None:
99             text = TrimText()
100             text.data = data
101             node.appendChild(text)
102
103
104     def _set_text_of_child(self, parent, name, data):
105         """
106         Set the text of a (direct) child text node.
107         """
108
109         for node in parent.childNodes:
110             if node.nodeType == node.ELEMENT_NODE and \
111                node.tagName == name:
112                 self._set_text(node, data)
113                 return
114
115         child = TrimTextElement(name)
116         self._set_text(child, data)
117         parent.appendChild(child)
118
119
120     def _category_element_to_dict(self, category_element):
121         """
122         Turn a <category> element into a dictionary of its attributes
123         and child text nodes.
124         """
125
126         category = {}
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
133
134         return category
135
136
137     def _variable_element_to_dict(self, variable_element):
138         """
139         Turn a <variable> element into a dictionary of its attributes
140         and child text nodes.
141         """
142
143         variable = {}
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
152
153         return variable
154
155
156     def _group_element_to_dict(self, group_element):
157         """
158         Turn a <group> element into a dictionary of its attributes
159         and child text nodes.
160         """
161
162         group = {}
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
168
169         return group
170
171
172     def _packagereq_element_to_dict(self, packagereq_element):
173         """
174         Turns a <packagereq> element into a dictionary of its attributes
175         and child text nodes.
176         """
177
178         package = {}
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
183
184         return package
185
186
187     def load(self, file = "/etc/planetlab/plc_config.xml"):
188         """
189         Merge file into configuration store.
190         """
191
192         dom = xml.dom.minidom.parse(file)
193         if type(file) in types.StringTypes:
194             self._files.append(os.path.abspath(file))
195
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)
201
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)
206
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)
212
213                 for packagereq_element in group_element.getElementsByTagName('packagereq'):
214                     package = self._packagereq_element_to_dict(packagereq_element)
215                     self.add_package(group, package)
216
217
218     def save(self, file = None):
219         """
220         Write configuration store to file.
221         """
222
223         if file is None:
224             if self._files:
225                 file = self._files[0]
226             else:
227                 file = "/etc/planetlab/plc_config.xml"
228
229         if type(file) in types.StringTypes:
230             fileobj = open(file, 'w')
231         else:
232             fileobj = file
233
234         fileobj.seek(0)
235         fileobj.write(self.output_xml())
236         fileobj.truncate()
237
238         fileobj.close()
239
240     def verify(self, default, read):
241         """ Confirm that the existing configuration is consistent according to
242         the checks below.
243
244             It looks for filled-in values in the order of, local object (self),
245             followed by cread (read values), and finally default values.
246
247         Arguments: 
248
249             None
250
251         Returns:
252
253             None.  If an exception is found, ConfigurationException is raised.
254
255         """
256
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")
264
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")
272
273         muser= maint_user['value']
274         ruser= root_user['value']
275
276         if muser == ruser:
277             raise ConfigurationException("The Maintenance Account email address cannot be the same as the Root User email address")
278         return
279
280
281     def get(self, category_id, variable_id):
282         """
283         Get the specified variable in the specified category.
284
285         Arguments:
286
287         category_id = unique category identifier (e.g., 'plc_www')
288         variable_id = unique variable identifier (e.g., 'port')
289
290         Returns:
291
292         variable = { 'id': "variable_identifier",
293                      'type': "variable_type",
294                      'value': "variable_value",
295                      'name': "Variable name",
296                      'description': "Variable description" }
297         """
298
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]
303             else:
304                 variable = None
305         else:
306             category = None
307             variable = None
308
309         return (category, variable)
310
311
312     def delete(self, category_id, variable_id):
313         """
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.
317
318         Arguments:
319
320         category_id = unique category identifier (e.g., 'plc_www')
321         variable_id = unique variable identifier (e.g., 'port')
322         """
323
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]
333
334
335     def set(self, category, variable):
336         """
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
341         category.
342
343         Arguments:
344
345         category = { 'id': "category_identifier",
346                      'name': "Category name",
347                      'description': "Category description" }
348
349         variable = { 'id': "variable_identifier",
350                      'type': "variable_type",
351                      'value': "variable_value",
352                      'name': "Variable name",
353                      'description': "Variable description" }
354         """
355
356         if not category.has_key('id') or type(category['id']) not in types.StringTypes:
357             return
358         
359         category_id = category['id'].lower()
360
361         if self._variables.has_key(category_id):
362             # Existing category
363             (old_category, variables) = self._variables[category_id]
364
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])
370
371             category_element = old_category['element']
372         else:
373             # Merge into DOM
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])
379
380             if self._dom.documentElement.getElementsByTagName('variables'):
381                 variables_element = self._dom.documentElement.getElementsByTagName('variables')[0]
382             else:
383                 variables_element = self._dom.createElement('variables')
384                 self._dom.documentElement.appendChild(variables_element)
385             variables_element.appendChild(category_element)
386
387             # Cache it
388             category['element'] = category_element
389             variables = {}
390             self._variables[category_id] = (category, variables)
391
392         if variable is None or not variable.has_key('id') or type(variable['id']) not in types.StringTypes:
393             return
394
395         variable_id = variable['id'].lower()
396
397         if variables.has_key(variable_id):
398             # Existing variable
399             old_variable = variables[variable_id]
400
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])
410         else:
411             # Merge into DOM
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])
420                 
421             if category_element.getElementsByTagName('variablelist'):
422                 variablelist_element = category_element.getElementsByTagName('variablelist')[0]
423             else:
424                 variablelist_element = self._dom.createElement('variablelist')
425                 category_element.appendChild(variablelist_element)
426             variablelist_element.appendChild(variable_element)
427
428             # Cache it
429             variable['element'] = variable_element
430             variables[variable_id] = variable
431
432
433     def locate_varname (self, varname):
434         """
435         Locates category and variable from a variable's (shell) name
436
437         Returns:
438         (variable, category) when found
439         (None, None) otherwise
440         """
441         
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)
445                 if (id == varname):
446                     return (category,variable)
447         return (None,None)
448
449     def get_package(self, group_id, package_name):
450         """
451         Get the specified package in the specified package group.
452
453         Arguments:
454
455         group_id - unique group id (e.g., 'plc')
456         package_name - unique package name (e.g., 'postgresql')
457
458         Returns:
459
460         package = { 'name': "package_name",
461                     'type': "mandatory|optional" }
462         """
463
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]
468             else:
469                 package = None
470         else:
471             group = None
472             package = None
473
474         return (group, package)
475
476
477     def delete_package(self, group_id, package_name):
478         """
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.
482
483         Arguments:
484
485         group_id - unique group id (e.g., 'plc')
486         package_name - unique package name (e.g., 'postgresql')
487         """
488
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]
498
499
500     def add_package(self, group, package):
501         """
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
506         specified group.
507
508         Arguments:
509
510         group = { 'id': "group_identifier",
511                   'name': "Group name",
512                   'default': "true|false",
513                   'description': "Group description",
514                   'uservisible': "true|false" }
515
516         package = { 'name': "package_name",
517                     'type': "mandatory|optional" }
518         """
519
520         if not group.has_key('id'):
521             return
522
523         group_id = group['id']
524
525         if self._packages.has_key(group_id):
526             # Existing group
527             (old_group, packages) = self._packages[group_id]
528
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])
534
535             group_element = old_group['element']
536         else:
537             # Merge into DOM
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])
542
543             if self._dom.documentElement.getElementsByTagName('comps'):
544                 comps_element = self._dom.documentElement.getElementsByTagName('comps')[0]
545             else:
546                 comps_element = self._dom.createElement('comps')
547                 self._dom.documentElement.appendChild(comps_element)
548             comps_element.appendChild(group_element)
549
550             # Cache it
551             group['element'] = group_element
552             packages = {}
553             self._packages[group_id] = (group, packages)
554
555         if package is None or not package.has_key('name'):
556             return
557
558         package_name = package['name']
559         if packages.has_key(package_name):
560             # Existing package
561             old_package = packages[package_name]
562
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])
568         else:
569             # Merge into DOM
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])
575                 
576             if group_element.getElementsByTagName('packagelist'):
577                 packagelist_element = group_element.getElementsByTagName('packagelist')[0]
578             else:
579                 packagelist_element = self._dom.createElement('packagelist')
580                 group_element.appendChild(packagelist_element)
581             packagelist_element.appendChild(packagereq_element)
582
583             # Cache it
584             package['element'] = packagereq_element
585             packages[package_name] = package
586
587
588     def variables(self):
589         """
590         Return all variables.
591
592         Returns:
593
594         variables = { 'category_id': (category, variablelist) }
595
596         category = { 'id': "category_identifier",
597                      'name': "Category name",
598                      'description': "Category description" }
599
600         variablelist = { 'variable_id': variable }
601
602         variable = { 'id': "variable_identifier",
603                      'type': "variable_type",
604                      'value': "variable_value",
605                      'name': "Variable name",
606                      'description': "Variable description" }
607         """
608
609         return self._variables
610
611
612     def packages(self):
613         """
614         Return all packages.
615
616         Returns:
617
618         packages = { 'group_id': (group, packagelist) }
619
620         group = { 'id': "group_identifier",
621                   'name': "Group name",
622                   'default': "true|false",
623                   'description': "Group description",
624                   'uservisible': "true|false" }
625
626         packagelist = { 'package_name': package }
627
628         package = { 'name': "package_name",
629                     'type': "mandatory|optional" }
630         """
631
632         return self._packages
633
634
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']
639         # And uppercase it
640         id = id.upper()
641
642         if variable.has_key('type'):
643             type = variable['type']
644         else:
645             type = None
646
647         if variable.has_key('name'):
648             name = variable['name']
649         else:
650             name = None
651
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
656                 pass
657             elif type == "boolean":
658                 # bash, Python, and PHP can all agree on 0 and 1
659                 if value == "true":
660                     value = "1"
661                 else:
662                     value = "0"
663             else:
664                 # bash, Python, and PHP all support strong single quoting
665                 value = "'" + value.replace("'", "\\'") + "'"
666         else:
667             value = None
668
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)
676         else:
677             comments = None
678
679         return (id, name, value, comments)
680
681
682     def _header(self):
683         header = """
684 DO NOT EDIT. This file was automatically generated at
685 %s from:
686
687 %s
688 """ % (time.asctime(), os.linesep.join(self._files))
689
690         # Get rid of the surrounding newlines
691         return header.strip().split(os.linesep)
692
693
694     def output_shell(self, show_comments = True, encoding = "utf-8"):
695         """
696         Return variables as a shell script.
697         """
698
699         buf = codecs.lookup(encoding)[3](StringIO())
700         buf.writelines(["# " + line + os.linesep for line in self._header()])
701
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)
705                 if show_comments:
706                     buf.write(os.linesep)
707                     if name is not None:
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)
714
715         return buf.getvalue()
716
717
718     def output_php(self, encoding = "utf-8"):
719         """
720         Return variables as a PHP script.
721         """
722
723         buf = codecs.lookup(encoding)[3](StringIO())
724         buf.write("<?php" + os.linesep)
725         buf.writelines(["// " + line + os.linesep for line in self._header()])
726
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)
731                 if name is not None:
732                     buf.write("// " + name + os.linesep)
733                 if comments is not None:
734                     buf.writelines(["// " + line + os.linesep for line in comments])
735                 if value is None:
736                     value = 'NULL'
737                 buf.write("define('%s', %s);" % (id, value) + os.linesep)
738
739         buf.write("?>" + os.linesep)
740
741         return buf.getvalue()
742
743
744     def output_xml(self, encoding = "utf-8"):
745         """
746         Return variables in original XML format.
747         """
748
749         buf = codecs.lookup(encoding)[3](StringIO())
750         self._dom.writexml(buf, addindent = "  ", indent = "", newl = "\n", encoding = encoding)
751
752         return buf.getvalue()
753
754
755     def output_variables(self, encoding = "utf-8"):
756         """
757         Return list of all variable names.
758         """
759
760         buf = codecs.lookup(encoding)[3](StringIO())
761
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)
766
767         return buf.getvalue()
768
769
770     def output_packages(self, encoding = "utf-8"):
771         """
772         Return list of all packages.
773         """
774
775         buf = codecs.lookup(encoding)[3](StringIO())
776
777         for (group, packages) in self._packages.values():
778             buf.write(os.linesep.join(packages.keys()))
779
780         if buf.tell():
781             buf.write(os.linesep)
782
783         return buf.getvalue()
784
785
786     def output_groups(self, encoding = "utf-8"):
787         """
788         Return list of all package group names.
789         """
790
791         buf = codecs.lookup(encoding)[3](StringIO())
792
793         for (group, packages) in self._packages.values():
794             buf.write(group['name'] + os.linesep)
795
796         return buf.getvalue()
797
798
799     def output_comps(self, encoding = "utf-8"):
800         """
801         Return <comps> section of configuration.
802         """
803
804         if self._dom is None or \
805            not self._dom.getElementsByTagName("comps"):
806             return
807         comps = self._dom.getElementsByTagName("comps")[0]
808
809         impl = xml.dom.minidom.getDOMImplementation()
810         doc = impl.createDocument(None, "comps", None)
811
812         buf = codecs.lookup(encoding)[3](StringIO())
813
814         # Pop it off the DOM temporarily
815         parent = comps.parentNode
816         parent.removeChild(comps)
817
818         doc.replaceChild(comps, doc.documentElement)
819         doc.writexml(buf, encoding = encoding)
820
821         # Put it back
822         parent.appendChild(comps)
823
824         return buf.getvalue()
825
826
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, "", "", "")
832
833
834 class TrimTextElement(xml.dom.minidom.Element):
835     def writexml(self, writer, indent="", addindent="", newl=""):
836         writer.write(indent)
837         xml.dom.minidom.Element.writexml(self, writer, "", "", "")
838         writer.write(newl)
839
840
841 if __name__ == '__main__':
842     import sys
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"])