R really does service <> reload as advertised
[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 codecs
14 import os
15 import re
16 import sys
17 import textwrap
18 import time
19 import traceback
20 import types
21 import xml.dom.minidom
22 from xml.parsers.expat import ExpatError
23 from StringIO import StringIO
24 from optparse import OptionParser
25
26 class ConfigurationException(Exception): pass
27
28 class PLCConfiguration:
29     """
30     Configuration file store. Optionally instantiate with a file path
31     or object:
32
33     plc = PLCConfiguration()
34     plc = PLCConfiguration(fileobj)
35     plc = PLCConfiguration("/etc/planetlab/plc_config.xml")
36
37     You may load() additional files later, which will be merged into
38     the current configuration:
39
40     plc.load("/etc/planetlab/local.xml")
41
42     You may also save() the configuration. If a file path or object is
43     not specified, the configuration will be written to the file path
44     or object that was first loaded.
45     
46     plc.save()
47     plc.save("/etc/planetlab/plc_config.xml")
48     """
49
50     def __init__(self, file = None):
51         impl = xml.dom.minidom.getDOMImplementation()
52         self._dom = impl.createDocument(None, "configuration", None)
53         self._variables = {}
54         self._packages = {}
55         self._files = []
56
57         if file is not None:
58             self.load(file)
59
60
61     def _get_text(self, node):
62         """
63         Get the text of a text node.
64         """
65
66         if node.firstChild and \
67            node.firstChild.nodeType == node.TEXT_NODE:
68             if node.firstChild.data is None:
69                 # Interpret simple presence of node as "", not NULL
70                 return ""
71             else:
72                 return node.firstChild.data
73
74         return None
75
76
77     def _get_text_of_child(self, parent, name):
78         """
79         Get the text of a (direct) child text node.
80         """
81
82         for node in parent.childNodes:
83             if node.nodeType == node.ELEMENT_NODE and \
84                node.tagName == name:
85                 return self._get_text(node)
86
87         return None
88
89
90     def _set_text(self, node, data):
91         """
92         Set the text of a text node.
93         """
94
95         if node.firstChild and \
96            node.firstChild.nodeType == node.TEXT_NODE:
97             if data is None:
98                 node.removeChild(node.firstChild)
99             else:
100                 node.firstChild.data = data
101         elif data is not None:
102             text = TrimText()
103             text.data = data
104             node.appendChild(text)
105
106
107     def _set_text_of_child(self, parent, name, data):
108         """
109         Set the text of a (direct) child text node.
110         """
111
112         for node in parent.childNodes:
113             if node.nodeType == node.ELEMENT_NODE and \
114                node.tagName == name:
115                 self._set_text(node, data)
116                 return
117
118         child = TrimTextElement(name)
119         self._set_text(child, data)
120         parent.appendChild(child)
121
122
123     def _category_element_to_dict(self, category_element):
124         """
125         Turn a <category> element into a dictionary of its attributes
126         and child text nodes.
127         """
128
129         category = {}
130         category['id'] = category_element.getAttribute('id').lower()
131         for node in category_element.childNodes:
132             if node.nodeType == node.ELEMENT_NODE and \
133                node.tagName in ['name', 'description']:
134                 category[node.tagName] = self._get_text_of_child(category_element, node.tagName)
135         category['element'] = category_element
136
137         return category
138
139
140     def _variable_element_to_dict(self, variable_element):
141         """
142         Turn a <variable> element into a dictionary of its attributes
143         and child text nodes.
144         """
145
146         variable = {}
147         variable['id'] = variable_element.getAttribute('id').lower()
148         if variable_element.hasAttribute('type'):
149             variable['type'] = variable_element.getAttribute('type')
150         for node in variable_element.childNodes:
151             if node.nodeType == node.ELEMENT_NODE and \
152                node.tagName in ['name', 'value', 'description']:
153                 variable[node.tagName] = self._get_text_of_child(variable_element, node.tagName)
154         variable['element'] = variable_element
155
156         return variable
157
158
159     def _group_element_to_dict(self, group_element):
160         """
161         Turn a <group> element into a dictionary of its attributes
162         and child text nodes.
163         """
164
165         group = {}
166         for node in group_element.childNodes:
167             if node.nodeType == node.ELEMENT_NODE and \
168                node.tagName in ['id', 'name', 'default', 'description', 'uservisible']:
169                 group[node.tagName] = self._get_text_of_child(group_element, node.tagName)
170         group['element'] = group_element
171
172         return group
173
174
175     def _packagereq_element_to_dict(self, packagereq_element):
176         """
177         Turns a <packagereq> element into a dictionary of its attributes
178         and child text nodes.
179         """
180
181         package = {}
182         if packagereq_element.hasAttribute('type'):
183             package['type'] = packagereq_element.getAttribute('type')
184         package['name'] = self._get_text(packagereq_element)
185         package['element'] = packagereq_element
186
187         return package
188
189
190     def load(self, file = "/etc/planetlab/plc_config.xml"):
191         """
192         Merge file into configuration store.
193         """
194
195         try:
196             dom = xml.dom.minidom.parse(file)
197         except ExpatError, e:
198             raise ConfigurationException, e
199
200         if type(file) in types.StringTypes:
201             self._files.append(os.path.abspath(file))
202
203         # Parse <variables> section
204         for variables_element in dom.getElementsByTagName('variables'):
205             for category_element in variables_element.getElementsByTagName('category'):
206                 category = self._category_element_to_dict(category_element)
207                 self.set(category, None)
208
209                 for variablelist_element in category_element.getElementsByTagName('variablelist'):
210                     for variable_element in variablelist_element.getElementsByTagName('variable'):
211                         variable = self._variable_element_to_dict(variable_element)
212                         self.set(category, variable)
213
214         # Parse <comps> section
215         for comps_element in dom.getElementsByTagName('comps'):
216             for group_element in comps_element.getElementsByTagName('group'):
217                 group = self._group_element_to_dict(group_element)
218                 self.add_package(group, None)
219
220                 for packagereq_element in group_element.getElementsByTagName('packagereq'):
221                     package = self._packagereq_element_to_dict(packagereq_element)
222                     self.add_package(group, package)
223
224
225     def save(self, file = None):
226         """
227         Write configuration store to file.
228         """
229
230         if file is None:
231             if self._files:
232                 file = self._files[0]
233             else:
234                 file = "/etc/planetlab/plc_config.xml"
235
236         if type(file) in types.StringTypes:
237             fileobj = open(file, 'w')
238         else:
239             fileobj = file
240
241         fileobj.seek(0)
242         fileobj.write(self.output_xml())
243         fileobj.truncate()
244
245         fileobj.close()
246
247     def verify(self, default, read, verify_variables={}):
248         """ Confirm that the existing configuration is consistent
249             according to the checks below.
250
251             It looks for filled-in values in the order of, local object (self),
252             followed by cread (read values), and finally default values.
253
254         Arguments: 
255
256             default configuration
257             site configuration
258             list of category/variable tuples to validate in these configurations
259
260         Returns:
261
262             dict of values for the category/variables passed in
263             If an exception is found, ConfigurationException is raised.
264
265         """
266
267         validated_variables = {}
268         for category_id, variable_id in verify_variables.iteritems():
269             category_id = category_id.lower()
270             variable_id = variable_id.lower()
271             variable_value = None
272             sources = (self, read, default)
273             for source in sources:
274                 (category_value, variable_value) = source.get(category_id,variable_id)
275                 if variable_value <> None:
276                     entry = validated_variables.get(category_id,[])
277                     entry.append(variable_value['value'])
278                     validated_variables["%s_%s"%(category_id.upper(),variable_id.upper())]=entry
279                     break
280             if variable_value == None:
281                 raise ConfigurationException("Cannot find %s_%s)" % \
282                                              (category_id.upper(),
283                                               variable_id.upper()))
284         return validated_variables
285
286     def get(self, category_id, variable_id):
287         """
288         Get the specified variable in the specified category.
289
290         Arguments:
291
292         category_id = unique category identifier (e.g., 'plc_www')
293         variable_id = unique variable identifier (e.g., 'port')
294
295         Returns:
296
297         variable = { 'id': "variable_identifier",
298                      'type': "variable_type",
299                      'value': "variable_value",
300                      'name': "Variable name",
301                      'description': "Variable description" }
302         """
303
304         if self._variables.has_key(category_id.lower()):
305             (category, variables) = self._variables[category_id]
306             if variables.has_key(variable_id.lower()):
307                 variable = variables[variable_id]
308             else:
309                 variable = None
310         else:
311             category = None
312             variable = None
313
314         return (category, variable)
315
316
317     def delete(self, category_id, variable_id):
318         """
319         Delete the specified variable from the specified category. If
320         variable_id is None, deletes all variables from the specified
321         category as well as the category itself.
322
323         Arguments:
324
325         category_id = unique category identifier (e.g., 'plc_www')
326         variable_id = unique variable identifier (e.g., 'port')
327         """
328
329         if self._variables.has_key(category_id.lower()):
330             (category, variables) = self._variables[category_id]
331             if variable_id is None:
332                 category['element'].parentNode.removeChild(category['element'])
333                 del self._variables[category_id]
334             elif variables.has_key(variable_id.lower()):
335                 variable = variables[variable_id]
336                 variable['element'].parentNode.removeChild(variable['element'])
337                 del variables[variable_id]
338
339
340     def set(self, category, variable):
341         """
342         Add and/or update the specified variable. The 'id' fields are
343         mandatory. If a field is not specified and the category and/or
344         variable already exists, the field will not be updated. If
345         'variable' is None, only adds and/or updates the specified
346         category.
347
348         Arguments:
349
350         category = { 'id': "category_identifier",
351                      'name': "Category name",
352                      'description': "Category description" }
353
354         variable = { 'id': "variable_identifier",
355                      'type': "variable_type",
356                      'value': "variable_value",
357                      'name': "Variable name",
358                      'description': "Variable description" }
359         """
360
361         if not category.has_key('id') or type(category['id']) not in types.StringTypes:
362             return
363         
364         category_id = category['id'].lower()
365
366         if self._variables.has_key(category_id):
367             # Existing category
368             (old_category, variables) = self._variables[category_id]
369
370             # Merge category attributes
371             for tag in ['name', 'description']:
372                 if category.has_key(tag):
373                     old_category[tag] = category[tag]
374                     self._set_text_of_child(old_category['element'], tag, category[tag])
375
376             category_element = old_category['element']
377         else:
378             # Merge into DOM
379             category_element = self._dom.createElement('category')
380             category_element.setAttribute('id', category_id)
381             for tag in ['name', 'description']:
382                 if category.has_key(tag):
383                     self._set_text_of_child(category_element, tag, category[tag])
384
385             if self._dom.documentElement.getElementsByTagName('variables'):
386                 variables_element = self._dom.documentElement.getElementsByTagName('variables')[0]
387             else:
388                 variables_element = self._dom.createElement('variables')
389                 self._dom.documentElement.appendChild(variables_element)
390             variables_element.appendChild(category_element)
391
392             # Cache it
393             category['element'] = category_element
394             variables = {}
395             self._variables[category_id] = (category, variables)
396
397         if variable is None or not variable.has_key('id') or type(variable['id']) not in types.StringTypes:
398             return
399
400         variable_id = variable['id'].lower()
401
402         if variables.has_key(variable_id):
403             # Existing variable
404             old_variable = variables[variable_id]
405
406             # Merge variable attributes
407             for attribute in ['type']:
408                 if variable.has_key(attribute):
409                     old_variable[attribute] = variable[attribute]
410                     old_variable['element'].setAttribute(attribute, variable[attribute])
411             for tag in ['name', 'value', 'description']:
412                 if variable.has_key(tag):
413                     old_variable[tag] = variable[tag]
414                     self._set_text_of_child(old_variable['element'], tag, variable[tag])
415         else:
416             # Merge into DOM
417             variable_element = self._dom.createElement('variable')
418             variable_element.setAttribute('id', variable_id)
419             for attribute in ['type']:
420                 if variable.has_key(attribute):
421                     variable_element.setAttribute(attribute, variable[attribute])
422             for tag in ['name', 'value', 'description']:
423                 if variable.has_key(tag):
424                     self._set_text_of_child(variable_element, tag, variable[tag])
425                 
426             if category_element.getElementsByTagName('variablelist'):
427                 variablelist_element = category_element.getElementsByTagName('variablelist')[0]
428             else:
429                 variablelist_element = self._dom.createElement('variablelist')
430                 category_element.appendChild(variablelist_element)
431             variablelist_element.appendChild(variable_element)
432
433             # Cache it
434             variable['element'] = variable_element
435             variables[variable_id] = variable
436
437
438     def locate_varname (self, varname):
439         """
440         Locates category and variable from a variable's (shell) name
441
442         Returns:
443         (variable, category) when found
444         (None, None) otherwise
445         """
446         
447         for (category_id, (category, variables)) in self._variables.iteritems():
448             for variable in variables.values():
449                 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
450                 if (id == varname):
451                     return (category,variable)
452         return (None,None)
453
454     def get_package(self, group_id, package_name):
455         """
456         Get the specified package in the specified package group.
457
458         Arguments:
459
460         group_id - unique group id (e.g., 'plc')
461         package_name - unique package name (e.g., 'postgresql')
462
463         Returns:
464
465         package = { 'name': "package_name",
466                     'type': "mandatory|optional" }
467         """
468
469         if self._packages.has_key(group_id.lower()):
470             (group, packages) = self._packages[group_id]
471             if packages.has_key(package_name):
472                 package = packages[package_name]
473             else:
474                 package = None
475         else:
476             group = None
477             package = None
478
479         return (group, package)
480
481
482     def delete_package(self, group_id, package_name):
483         """
484         Deletes the specified variable from the specified category. If
485         variable_id is None, deletes all variables from the specified
486         category as well as the category itself.
487
488         Arguments:
489
490         group_id - unique group id (e.g., 'plc')
491         package_name - unique package name (e.g., 'postgresql')
492         """
493
494         if self._packages.has_key(group_id):
495             (group, packages) = self._packages[group_id]
496             if package_name is None:
497                 group['element'].parentNode.removeChild(group['element'])
498                 del self._packages[group_id]
499             elif packages.has_key(package_name.lower()):
500                 package = packages[package_name]
501                 package['element'].parentNode.removeChild(package['element'])
502                 del packages[package_name]
503
504
505     def add_package(self, group, package):
506         """
507         Add and/or update the specified package. The 'id' and 'name'
508         fields are mandatory. If a field is not specified and the
509         package or group already exists, the field will not be
510         updated. If package is None, only adds/or updates the
511         specified group.
512
513         Arguments:
514
515         group = { 'id': "group_identifier",
516                   'name': "Group name",
517                   'default': "true|false",
518                   'description': "Group description",
519                   'uservisible': "true|false" }
520
521         package = { 'name': "package_name",
522                     'type': "mandatory|optional" }
523         """
524
525         if not group.has_key('id'):
526             return
527
528         group_id = group['id']
529
530         if self._packages.has_key(group_id):
531             # Existing group
532             (old_group, packages) = self._packages[group_id]
533
534             # Merge group attributes
535             for tag in ['id', 'name', 'default', 'description', 'uservisible']:
536                 if group.has_key(tag):
537                     old_group[tag] = group[tag]
538                     self._set_text_of_child(old_group['element'], tag, group[tag])
539
540             group_element = old_group['element']
541         else:
542             # Merge into DOM
543             group_element = self._dom.createElement('group')
544             for tag in ['id', 'name', 'default', 'description', 'uservisible']:
545                 if group.has_key(tag):
546                     self._set_text_of_child(group_element, tag, group[tag])
547
548             if self._dom.documentElement.getElementsByTagName('comps'):
549                 comps_element = self._dom.documentElement.getElementsByTagName('comps')[0]
550             else:
551                 comps_element = self._dom.createElement('comps')
552                 self._dom.documentElement.appendChild(comps_element)
553             comps_element.appendChild(group_element)
554
555             # Cache it
556             group['element'] = group_element
557             packages = {}
558             self._packages[group_id] = (group, packages)
559
560         if package is None or not package.has_key('name'):
561             return
562
563         package_name = package['name']
564         if packages.has_key(package_name):
565             # Existing package
566             old_package = packages[package_name]
567
568             # Merge variable attributes
569             for attribute in ['type']:
570                 if package.has_key(attribute):
571                     old_package[attribute] = package[attribute]
572                     old_package['element'].setAttribute(attribute, package[attribute])
573         else:
574             # Merge into DOM
575             packagereq_element = TrimTextElement('packagereq')
576             self._set_text(packagereq_element, package_name)
577             for attribute in ['type']:
578                 if package.has_key(attribute):
579                     packagereq_element.setAttribute(attribute, package[attribute])
580                 
581             if group_element.getElementsByTagName('packagelist'):
582                 packagelist_element = group_element.getElementsByTagName('packagelist')[0]
583             else:
584                 packagelist_element = self._dom.createElement('packagelist')
585                 group_element.appendChild(packagelist_element)
586             packagelist_element.appendChild(packagereq_element)
587
588             # Cache it
589             package['element'] = packagereq_element
590             packages[package_name] = package
591
592
593     def variables(self):
594         """
595         Return all variables.
596
597         Returns:
598
599         variables = { 'category_id': (category, variablelist) }
600
601         category = { 'id': "category_identifier",
602                      'name': "Category name",
603                      'description': "Category description" }
604
605         variablelist = { 'variable_id': variable }
606
607         variable = { 'id': "variable_identifier",
608                      'type': "variable_type",
609                      'value': "variable_value",
610                      'name': "Variable name",
611                      'description': "Variable description" }
612         """
613
614         return self._variables
615
616
617     def packages(self):
618         """
619         Return all packages.
620
621         Returns:
622
623         packages = { 'group_id': (group, packagelist) }
624
625         group = { 'id': "group_identifier",
626                   'name': "Group name",
627                   'default': "true|false",
628                   'description': "Group description",
629                   'uservisible': "true|false" }
630
631         packagelist = { 'package_name': package }
632
633         package = { 'name': "package_name",
634                     'type': "mandatory|optional" }
635         """
636
637         return self._packages
638
639
640     def _sanitize_variable(self, category_id, variable):
641         assert variable.has_key('id')
642         # Prepend variable name with category label
643         id = category_id + "_" + variable['id']
644         # And uppercase it
645         id = id.upper()
646
647         if variable.has_key('type'):
648             type = variable['type']
649         else:
650             type = None
651
652         if variable.has_key('name'):
653             name = variable['name']
654         else:
655             name = None
656
657         if variable.has_key('value') and variable['value'] is not None:
658             value = variable['value']
659             if type == "int" or type == "double":
660                 # bash, Python, and PHP do not require that numbers be quoted
661                 pass
662             elif type == "boolean":
663                 # bash, Python, and PHP can all agree on 0 and 1
664                 if value == "true":
665                     value = "1"
666                 else:
667                     value = "0"
668             else:
669                 # bash, Python, and PHP all support strong single quoting
670                 value = "'" + value.replace("'", "\\'") + "'"
671         else:
672             value = None
673
674         if variable.has_key('description') and variable['description'] is not None:
675             description = variable['description']
676             # Collapse consecutive whitespace
677             description = re.sub(r'\s+', ' ', description)
678             # Wrap comments at 70 columns
679             wrapper = textwrap.TextWrapper()
680             comments = wrapper.wrap(description)
681         else:
682             comments = None
683
684         return (id, name, value, comments)
685
686
687     def _header(self):
688         header = """
689 DO NOT EDIT. This file was automatically generated at
690 %s from:
691
692 %s
693 """ % (time.asctime(), os.linesep.join(self._files))
694
695         # Get rid of the surrounding newlines
696         return header.strip().split(os.linesep)
697
698
699     def output_shell(self, show_comments = True, encoding = "utf-8"):
700         """
701         Return variables as a shell script.
702         """
703
704         buf = codecs.lookup(encoding)[3](StringIO())
705         buf.writelines(["# " + line + os.linesep for line in self._header()])
706
707         for (category_id, (category, variables)) in self._variables.iteritems():
708             for variable in variables.values():
709                 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
710                 if show_comments:
711                     buf.write(os.linesep)
712                     if name is not None:
713                         buf.write("# " + name + os.linesep)
714                     if comments is not None:
715                         buf.writelines(["# " + line + os.linesep for line in comments])
716                 # bash does not have the concept of NULL
717                 if value is not None:
718                     buf.write(id + "=" + value + os.linesep)
719
720         return buf.getvalue()
721
722
723     def output_php(self, encoding = "utf-8"):
724         """
725         Return variables as a PHP script.
726         """
727
728         buf = codecs.lookup(encoding)[3](StringIO())
729         buf.write("<?php" + os.linesep)
730         buf.writelines(["// " + line + os.linesep for line in self._header()])
731
732         for (category_id, (category, variables)) in self._variables.iteritems():
733             for variable in variables.values():
734                 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
735                 buf.write(os.linesep)
736                 if name is not None:
737                     buf.write("// " + name + os.linesep)
738                 if comments is not None:
739                     buf.writelines(["// " + line + os.linesep for line in comments])
740                 if value is None:
741                     value = 'NULL'
742                 buf.write("define('%s', %s);" % (id, value) + os.linesep)
743
744         buf.write("?>" + os.linesep)
745
746         return buf.getvalue()
747
748
749     def output_xml(self, encoding = "utf-8"):
750         """
751         Return variables in original XML format.
752         """
753
754         buf = codecs.lookup(encoding)[3](StringIO())
755         self._dom.writexml(buf, addindent = "  ", indent = "", newl = "\n", encoding = encoding)
756
757         return buf.getvalue()
758
759
760     def output_variables(self, encoding = "utf-8"):
761         """
762         Return list of all variable names.
763         """
764
765         buf = codecs.lookup(encoding)[3](StringIO())
766
767         for (category_id, (category, variables)) in self._variables.iteritems():
768             for variable in variables.values():
769                 (id, name, value, comments) = self._sanitize_variable(category_id, variable)
770                 buf.write(id + os.linesep)
771
772         return buf.getvalue()
773
774
775     def output_packages(self, encoding = "utf-8"):
776         """
777         Return list of all packages.
778         """
779
780         buf = codecs.lookup(encoding)[3](StringIO())
781
782         for (group, packages) in self._packages.values():
783             buf.write(os.linesep.join(packages.keys()))
784
785         if buf.tell():
786             buf.write(os.linesep)
787
788         return buf.getvalue()
789
790
791     def output_groups(self, encoding = "utf-8"):
792         """
793         Return list of all package group names.
794         """
795
796         buf = codecs.lookup(encoding)[3](StringIO())
797
798         for (group, packages) in self._packages.values():
799             buf.write(group['name'] + os.linesep)
800
801         return buf.getvalue()
802
803
804     def output_comps(self, encoding = "utf-8"):
805         """
806         Return <comps> section of configuration.
807         """
808
809         if self._dom is None or \
810            not self._dom.getElementsByTagName("comps"):
811             return
812         comps = self._dom.getElementsByTagName("comps")[0]
813
814         impl = xml.dom.minidom.getDOMImplementation()
815         doc = impl.createDocument(None, "comps", None)
816
817         buf = codecs.lookup(encoding)[3](StringIO())
818
819         # Pop it off the DOM temporarily
820         parent = comps.parentNode
821         parent.removeChild(comps)
822
823         doc.replaceChild(comps, doc.documentElement)
824         doc.writexml(buf, encoding = encoding)
825
826         # Put it back
827         parent.appendChild(comps)
828
829         return buf.getvalue()
830
831     def validate_type(self, variable_type, value):
832
833         # ideally we should use the "validate_*" methods in PLCAPI or
834         # even declare some checks along with the default
835         # configuration (using RELAX NG?) but this shall work for now.
836         def ip_validator(val):
837             import socket
838             try:
839                 socket.inet_aton(val)
840                 return True
841             except: return False
842
843         validators = {
844             'email' : lambda val: re.match('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z', val),
845             'ip': ip_validator
846             }
847
848         # validate it if not a know type.
849         validator = validators.get(variable_type, lambda x: True)
850         return validator(value)
851
852
853
854 # xml.dom.minidom.Text.writexml adds surrounding whitespace to textual
855 # data when pretty-printing. Override this behavior.
856 class TrimText(xml.dom.minidom.Text):
857     def writexml(self, writer, indent="", addindent="", newl=""):
858         xml.dom.minidom.Text.writexml(self, writer, "", "", "")
859
860
861 class TrimTextElement(xml.dom.minidom.Element):
862     def writexml(self, writer, indent="", addindent="", newl=""):
863         writer.write(indent)
864         xml.dom.minidom.Element.writexml(self, writer, "", "", "")
865         writer.write(newl)
866
867
868 ####################
869 # GLOBAL VARIABLES
870 #
871 release_id = "$Id$"
872 release_rev = "$Revision$"
873 release_url = "$URL$"
874
875 g_configuration=None
876 usual_variables=None
877 config_dir=None
878 service=None
879
880 def noop_validator(validated_variables):
881     pass
882
883
884 # historically we could also configure the devel pkg....
885 def init_configuration ():
886     global g_configuration
887     global usual_variables, config_dir, service
888
889     usual_variables=g_configuration["usual_variables"]
890     config_dir=g_configuration["config_dir"]
891     service=g_configuration["service"]
892
893     global def_default_config, def_site_config, def_consolidated_config
894     def_default_config= "%s/default_config.xml" % config_dir
895     def_site_config = "%s/configs/site.xml" % config_dir
896     def_consolidated_config = "%s/%s_config.xml" % (config_dir, service)
897
898     global mainloop_usage
899     mainloop_usage= """Available commands:
900  Uppercase versions give variables comments, when available
901  u/U\t\t\tEdit usual variables
902  w\t\t\tWrite
903  r\t\t\tRestart %(service)s service
904  R\t\t\tReload %(service)s service (rebuild config files for sh, python....)
905  q\t\t\tQuit (without saving)
906  h/?\t\t\tThis help
907 ---
908  l/L [<cat>|<var>]\tShow Locally modified variables/values
909  s/S [<cat>|<var>]\tShow variables/values (all, in category, single)
910  e/E [<cat>|<var>]\tEdit variables (all, in category, single)
911 ---
912  c\t\t\tList categories
913  v/V [<cat>|<var>]\tList Variables (all, in category, single)
914 ---
915 Typical usage involves: u, [l,] w, r, q
916 """ % globals()
917
918 def usage ():
919     command_usage="%prog [options] [default-xml [site-xml [consolidated-xml]]]"
920     init_configuration ()
921     command_usage +="""
922 \t default-xml defaults to %s
923 \t site-xml defaults to %s
924 \t consolidated-xml defaults to %s""" % (def_default_config,def_site_config, def_consolidated_config)
925     return command_usage
926
927 ####################
928 variable_usage= """Edit Commands :
929 #\tShow variable comments
930 .\tStops prompting, return to mainloop
931 /\tCleans any site-defined value, reverts to default
932 =\tShows default value
933 >\tSkips to next category
934 ?\tThis help
935 """
936
937 ####################
938 def get_value (config,  category_id, variable_id):
939     (category, variable) = config.get (category_id, variable_id)
940     return variable['value']
941
942 def get_type (config, category_id, variable_id):
943     (category, variable) = config.get (category_id, variable_id)
944     return variable['type']
945
946 def get_current_value (cread, cwrite, category_id, variable_id):
947     # the value stored in cwrite, if present, is the one we want
948     try:
949         result=get_value (cwrite,category_id,variable_id)
950     except:
951         result=get_value (cread,category_id,variable_id)
952     return result
953
954 # refrain from using plc_config's _sanitize 
955 def get_varname (config,  category_id, variable_id):
956     (category, variable) = config.get (category_id, variable_id)
957     return (category_id+"_"+variable['id']).upper()
958
959 # could not avoid using _sanitize here..
960 def get_name_comments (config, cid, vid):
961     try:
962         (category, variable) = config.get (cid, vid)
963         (id, name, value, comments) = config._sanitize_variable (cid,variable)
964         return (name,comments)
965     except:
966         return (None,[])
967
968 def print_name_comments (config, cid, vid):
969     (name,comments)=get_name_comments(config,cid,vid)
970     if name:
971         print "### %s" % name
972     if comments:
973         for line in comments:
974             print "# %s" % line
975     else:
976         print "!!! No comment associated to %s_%s" % (cid,vid)
977
978 ####################
979 def list_categories (config):
980     result=[]
981     for (category_id, (category, variables)) in config.variables().iteritems():
982         result += [category_id]
983     return result
984
985 def print_categories (config):
986     print "Known categories"
987     for cid in list_categories(config):
988         print "%s" % (cid.upper())
989
990 ####################
991 def list_category (config, cid):
992     result=[]
993     for (category_id, (category, variables)) in config.variables().iteritems():
994         if (cid == category_id):
995             for variable in variables.values():
996                 result += ["%s_%s" %(cid,variable['id'])]
997     return result
998     
999 def print_category (config, cid, show_comments=True):
1000     cid=cid.lower()
1001     CID=cid.upper()
1002     vids=list_category(config,cid)
1003     if (len(vids) == 0):
1004         print "%s : no such category"%CID
1005     else:
1006         print "Category %s contains" %(CID)
1007         for vid in vids:
1008             print vid.upper()
1009
1010 ####################
1011 def consolidate (default_config, site_config, consolidated_config):
1012     global service
1013     try:
1014         conso = PLCConfiguration (default_config)
1015         conso.load (site_config)
1016         conso.save (consolidated_config)
1017     except Exception, inst:
1018         print "Could not consolidate, %s" % (str(inst))
1019         return
1020     print ("Merged\n\t%s\nand\t%s\ninto\t%s"%(default_config,site_config,
1021                                               consolidated_config))
1022
1023 def reload_service ():
1024     global service
1025     os.system("set -x ; service %s reload" % service)
1026         
1027 ####################
1028 def restart_service ():
1029     global service
1030     print ("==================== Stopping %s" % service)
1031     os.system("service %s stop" % service)
1032     print ("==================== Starting %s" % service)
1033     os.system("service %s start" % service)
1034
1035 ####################
1036 def prompt_variable (cdef, cread, cwrite, category, variable,
1037                      show_comments, support_next=False):
1038
1039     assert category.has_key('id')
1040     assert variable.has_key('id')
1041
1042     category_id = category ['id']
1043     variable_id = variable['id']
1044
1045     while True:
1046         default_value = get_value(cdef,category_id,variable_id)
1047         variable_type = get_type(cdef,category_id,variable_id)
1048         current_value = get_current_value(cread,cwrite,category_id, variable_id)
1049         varname = get_varname (cread,category_id, variable_id)
1050         
1051         if show_comments :
1052             print_name_comments (cdef, category_id, variable_id)
1053         prompt = "== %s : [%s] " % (varname,current_value)
1054         try:
1055             answer = raw_input(prompt).strip()
1056         except EOFError :
1057             raise Exception ('BailOut')
1058         except KeyboardInterrupt:
1059             print "\n"
1060             raise Exception ('BailOut')
1061
1062         # no change
1063         if (answer == "") or (answer == current_value):
1064             return None
1065         elif (answer == "."):
1066             raise Exception ('BailOut')
1067         elif (answer == "#"):
1068             print_name_comments(cread,category_id,variable_id)
1069         elif (answer == "?"):
1070             print variable_usage.strip()
1071         elif (answer == "="):
1072             print ("%s defaults to %s" %(varname,default_value))
1073         # revert to default : remove from cwrite (i.e. site-config)
1074         elif (answer == "/"):
1075             cwrite.delete(category_id,variable_id)
1076             print ("%s reverted to %s" %(varname,default_value))
1077             return
1078         elif (answer == ">"):
1079             if support_next:
1080                 raise Exception ('NextCategory')
1081             else:
1082                 print "No support for next category"
1083         else:
1084             if cdef.validate_type(variable_type, answer):
1085                 variable['value'] = answer
1086                 cwrite.set(category,variable)
1087                 return
1088             else:
1089                 print "Not a valid value"
1090
1091 def prompt_variables_all (cdef, cread, cwrite, show_comments):
1092     try:
1093         for (category_id, (category, variables)) in cread.variables().iteritems():
1094             print ("========== Category = %s" % category_id.upper())
1095             for variable in variables.values():
1096                 try:
1097                     newvar = prompt_variable (cdef, cread, cwrite, category, variable,
1098                                               show_comments, True)
1099                 except Exception, inst:
1100                     if (str(inst) == 'NextCategory'): break
1101                     else: raise
1102                     
1103     except Exception, inst:
1104         if (str(inst) == 'BailOut'): return
1105         else: raise
1106
1107 def prompt_variables_category (cdef, cread, cwrite, cid, show_comments):
1108     cid=cid.lower()
1109     CID=cid.upper()
1110     try:
1111         print ("========== Category = %s" % CID)
1112         for vid in list_category(cdef,cid):
1113             (category,variable) = cdef.locate_varname(vid.upper())
1114             newvar = prompt_variable (cdef, cread, cwrite, category, variable,
1115                                       show_comments, False)
1116     except Exception, inst:
1117         if (str(inst) == 'BailOut'): return
1118         else: raise
1119
1120 ####################
1121 def show_variable (cdef, cread, cwrite,
1122                    category, variable,show_value,show_comments):
1123     assert category.has_key('id')
1124     assert variable.has_key('id')
1125
1126     category_id = category ['id']
1127     variable_id = variable['id']
1128
1129     default_value = get_value(cdef,category_id,variable_id)
1130     current_value = get_current_value(cread,cwrite,category_id,variable_id)
1131     varname = get_varname (cread,category_id, variable_id)
1132     if show_comments :
1133         print_name_comments (cdef, category_id, variable_id)
1134     if show_value:
1135         print "%s = %s" % (varname,current_value)
1136     else:
1137         print "%s" % (varname)
1138
1139 def show_variables_all (cdef, cread, cwrite, show_value, show_comments):
1140     for (category_id, (category, variables)) in cread.variables().iteritems():
1141         print ("========== Category = %s" % category_id.upper())
1142         for variable in variables.values():
1143             show_variable (cdef, cread, cwrite,
1144                            category, variable,show_value,show_comments)
1145
1146 def show_variables_category (cdef, cread, cwrite, cid, show_value,show_comments):
1147     cid=cid.lower()
1148     CID=cid.upper()
1149     print ("========== Category = %s" % CID)
1150     for vid in list_category(cdef,cid):
1151         (category,variable) = cdef.locate_varname(vid.upper())
1152         show_variable (cdef, cread, cwrite, category, variable,
1153                        show_value,show_comments)
1154
1155 ####################
1156 re_mainloop_0arg="^(?P<command>[uUwrRqlLsSeEcvVhH\?])[ \t]*$"
1157 re_mainloop_1arg="^(?P<command>[sSeEvV])[ \t]+(?P<arg>\w+)$"
1158 matcher_mainloop_0arg=re.compile(re_mainloop_0arg)
1159 matcher_mainloop_1arg=re.compile(re_mainloop_1arg)
1160
1161 def mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_config):
1162     global service
1163     while True:
1164         try:
1165             answer = raw_input("Enter command (u for usual changes, w to save, ? for help) ").strip()
1166         except EOFError:
1167             answer =""
1168         except KeyboardInterrupt:
1169             print "\nBye"
1170             sys.exit()
1171
1172         if (answer == "") or (answer in "?hH"):
1173             print mainloop_usage
1174             continue
1175         groups_parse = matcher_mainloop_0arg.match(answer)
1176         command=None
1177         if (groups_parse):
1178             command = groups_parse.group('command')
1179             arg=None
1180         else:
1181             groups_parse = matcher_mainloop_1arg.match(answer)
1182             if (groups_parse):
1183                 command = groups_parse.group('command')
1184                 arg=groups_parse.group('arg')
1185         if not command:
1186             print ("Unknown command >%s< -- use h for help" % answer)
1187             continue
1188
1189         show_comments=command.isupper()
1190
1191         mode='ALL'
1192         if arg:
1193             mode=None
1194             arg=arg.lower()
1195             variables=list_category (cdef,arg)
1196             if len(variables):
1197                 # category_id as the category name
1198                 # variables as the list of variable names
1199                 mode='CATEGORY'
1200                 category_id=arg
1201             arg=arg.upper()
1202             (category,variable)=cdef.locate_varname(arg)
1203             if variable:
1204                 # category/variable as output by locate_varname
1205                 mode='VARIABLE'
1206             if not mode:
1207                 print "%s: no such category or variable" % arg
1208                 continue
1209
1210         if command in "qQ":
1211             # todo check confirmation
1212             return
1213         elif command == "w":
1214             try:
1215                 # Confirm that various constraints are met before saving file.
1216                 validate_variables = g_configuration.get('validate_variables',{})
1217                 validated_variables = cwrite.verify(cdef, cread, validate_variables)
1218                 validator = g_configuration.get('validator',noop_validator)
1219                 validator(validated_variables)
1220                 cwrite.save(site_config)
1221             except ConfigurationException, e:
1222                 print "Save failed due to a configuration exception: %s" % e
1223                 break;
1224             except:
1225                 print traceback.print_exc()
1226                 print ("Could not save -- fix write access on %s" % site_config)
1227                 break
1228             print ("Wrote %s" % site_config)
1229             consolidate(default_config, site_config, consolidated_config)
1230             print ("You might want to type 'r' (restart %s), 'R' (reload %s) or 'q' (quit)" % \
1231                    (service,service))
1232         elif command in "uU":
1233             global usual_variables
1234             try:
1235                 for varname in usual_variables:
1236                     (category,variable) = cdef.locate_varname(varname)
1237                     if not (category is None and variable is None):
1238                         prompt_variable(cdef, cread, cwrite, category, variable, False)
1239             except Exception, inst:
1240                 if (str(inst) != 'BailOut'):
1241                     raise
1242         elif command == "r":
1243             restart_service()
1244         elif command == "R":
1245             reload_service()
1246         elif command == "c":
1247             print_categories(cread)
1248         elif command in "eE":
1249             if mode == 'ALL':
1250                 prompt_variables_all(cdef, cread, cwrite,show_comments)
1251             elif mode == 'CATEGORY':
1252                 prompt_variables_category(cdef,cread,cwrite,category_id,show_comments)
1253             elif mode == 'VARIABLE':
1254                 try:
1255                     prompt_variable (cdef,cread,cwrite,category,variable,
1256                                      show_comments,False)
1257                 except Exception, inst:
1258                     if str(inst) != 'BailOut':
1259                         raise
1260         elif command in "vVsSlL":
1261             show_value=(command in "sSlL")
1262             (c1,c2,c3) = (cdef, cread, cwrite)
1263             if command in "lL":
1264                 (c1,c2,c3) = (cwrite,cwrite,cwrite)
1265             if mode == 'ALL':
1266                 show_variables_all(c1,c2,c3,show_value,show_comments)
1267             elif mode == 'CATEGORY':
1268                 show_variables_category(c1,c2,c3,category_id,show_value,show_comments)
1269             elif mode == 'VARIABLE':
1270                 show_variable (c1,c2,c3,category,variable,show_value,show_comments)
1271         else:
1272             print ("Unknown command >%s< -- use h for help" % answer)
1273
1274 ####################
1275 # creates directory for file if not yet existing
1276 def check_dir (config_file):
1277     dirname = os.path.dirname (config_file)
1278     if (not os.path.exists (dirname)):
1279         try:
1280             os.makedirs(dirname,0755)
1281         except OSError, e:
1282             print "Cannot create dir %s due to %s - exiting" % (dirname,e)
1283             sys.exit(1)
1284             
1285         if (not os.path.exists (dirname)):
1286             print "Cannot create dir %s - exiting" % dirname
1287             sys.exit(1)
1288         else:
1289             print "Created directory %s" % dirname
1290                 
1291 ####################
1292 def optParserSetup(configuration):
1293     parser = OptionParser(usage=usage(), version="%prog " + release_rev + release_url )
1294     parser.set_defaults(config_dir=configuration['config_dir'],
1295                         service=configuration['service'],
1296                         usual_variables=configuration['usual_variables'])
1297     parser.add_option("","--configdir",dest="config_dir",help="specify configuration directory")
1298     parser.add_option("","--service",dest="service",help="specify /etc/init.d style service name")
1299     parser.add_option("","--usual_variable",dest="usual_variables",action="append", help="add a usual variable")
1300     return parser
1301
1302 def main(command,argv,configuration):
1303     global g_configuration
1304     g_configuration=configuration
1305
1306     parser = optParserSetup(configuration)
1307     (config,args) = parser.parse_args()
1308     if len(args)>3:
1309         parser.error("too many arguments")
1310
1311     configuration['service']=config.service
1312     configuration['usual_variables']=config.usual_variables
1313     configuration['config_dir']=config.config_dir
1314     # add in new usual_variables defined on the command line
1315     for usual_variable in config.usual_variables:
1316         if usual_variable not in configuration['usual_variables']:
1317             configuration['usual_variables'].append(usual_variable)
1318
1319     # intialize configuration
1320     init_configuration()
1321
1322     (default_config,site_config,consolidated_config) = (def_default_config, def_site_config, def_consolidated_config)
1323     if len(args) >= 1:
1324         default_config=args[0]
1325     if len(args) >= 2:
1326         site_config=args[1]
1327     if len(args) == 3:
1328         consolidated_config=args[2]
1329
1330     for c in (default_config,site_config,consolidated_config):
1331         check_dir (c)
1332
1333     try:
1334         # the default settings only - read only
1335         cdef = PLCConfiguration(default_config)
1336
1337         # in effect : default settings + local settings - read only
1338         cread = PLCConfiguration(default_config)
1339
1340     except ConfigurationException, e:
1341         print ("Error %s in default config file %s" %(e,default_config))
1342         return 1
1343     except:
1344         print traceback.print_exc()
1345         print ("default config files %s not found, is myplc installed ?" % default_config)
1346         return 1
1347
1348
1349     # local settings only, will be modified & saved
1350     cwrite=PLCConfiguration()
1351     
1352     try:
1353         cread.load(site_config)
1354         cwrite.load(site_config)
1355     except:
1356         cwrite = PLCConfiguration()
1357
1358     mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_config)
1359     return 0
1360
1361 if __name__ == '__main__':
1362     import sys
1363     if len(sys.argv) > 1 and sys.argv[1] in ['build', 'install', 'uninstall']:
1364         from distutils.core import setup
1365         setup(py_modules=["plc_config"])