validate variable types. (for now only have email and ip (v4) validators)
[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-zA-Z0-9._%+-]+@[a-zA-Z0-9.-_]+\.[a-zA-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/W\t\t\tWrite / Write & reload
903  r\t\t\tRestart %s service
904  q\t\t\tQuit (without saving)
905  h/?\t\t\tThis help
906 ---
907  l/L [<cat>|<var>]\tShow Locally modified variables/values
908  s/S [<cat>|<var>]\tShow variables/values (all, in category, single)
909  e/E [<cat>|<var>]\tEdit variables (all, in category, single)
910 ---
911  c\t\t\tList categories
912  v/V [<cat>|<var>]List Variables (all, in category, single)
913 ---
914 Typical usage involves: u, [l,] w, r, q
915 """ % service
916
917 def usage ():
918     command_usage="%prog [options] [default-xml [site-xml [consolidated-xml]]]"
919     init_configuration ()
920     command_usage +="""
921 \t default-xml defaults to %s
922 \t site-xml defaults to %s
923 \t consolidated-xml defaults to %s""" % (def_default_config,def_site_config, def_consolidated_config)
924     return command_usage
925
926 ####################
927 variable_usage= """Edit Commands :
928 #\tShow variable comments
929 .\tStops prompting, return to mainloop
930 /\tCleans any site-defined value, reverts to default
931 =\tShows default value
932 >\tSkips to next category
933 ?\tThis help
934 """
935
936 ####################
937 def get_value (config,  category_id, variable_id):
938     (category, variable) = config.get (category_id, variable_id)
939     return variable['value']
940
941 def get_type (config, category_id, variable_id):
942     (category, variable) = config.get (category_id, variable_id)
943     return variable['type']
944
945 def get_current_value (cread, cwrite, category_id, variable_id):
946     # the value stored in cwrite, if present, is the one we want
947     try:
948         result=get_value (cwrite,category_id,variable_id)
949     except:
950         result=get_value (cread,category_id,variable_id)
951     return result
952
953 # refrain from using plc_config's _sanitize 
954 def get_varname (config,  category_id, variable_id):
955     (category, variable) = config.get (category_id, variable_id)
956     return (category_id+"_"+variable['id']).upper()
957
958 # could not avoid using _sanitize here..
959 def get_name_comments (config, cid, vid):
960     try:
961         (category, variable) = config.get (cid, vid)
962         (id, name, value, comments) = config._sanitize_variable (cid,variable)
963         return (name,comments)
964     except:
965         return (None,[])
966
967 def print_name_comments (config, cid, vid):
968     (name,comments)=get_name_comments(config,cid,vid)
969     if name:
970         print "### %s" % name
971     if comments:
972         for line in comments:
973             print "# %s" % line
974     else:
975         print "!!! No comment associated to %s_%s" % (cid,vid)
976
977 ####################
978 def list_categories (config):
979     result=[]
980     for (category_id, (category, variables)) in config.variables().iteritems():
981         result += [category_id]
982     return result
983
984 def print_categories (config):
985     print "Known categories"
986     for cid in list_categories(config):
987         print "%s" % (cid.upper())
988
989 ####################
990 def list_category (config, cid):
991     result=[]
992     for (category_id, (category, variables)) in config.variables().iteritems():
993         if (cid == category_id):
994             for variable in variables.values():
995                 result += ["%s_%s" %(cid,variable['id'])]
996     return result
997     
998 def print_category (config, cid, show_comments=True):
999     cid=cid.lower()
1000     CID=cid.upper()
1001     vids=list_category(config,cid)
1002     if (len(vids) == 0):
1003         print "%s : no such category"%CID
1004     else:
1005         print "Category %s contains" %(CID)
1006         for vid in vids:
1007             print vid.upper()
1008
1009 ####################
1010 def consolidate (default_config, site_config, consolidated_config):
1011     global service
1012     try:
1013         conso = PLCConfiguration (default_config)
1014         conso.load (site_config)
1015         conso.save (consolidated_config)
1016     except Exception, inst:
1017         print "Could not consolidate, %s" % (str(inst))
1018         return
1019     print ("Merged\n\t%s\nand\t%s\ninto\t%s"%(default_config,site_config,
1020                                               consolidated_config))
1021
1022 def reload_service ():
1023     global service
1024     os.system("set -x ; service %s reload" % service)
1025         
1026 ####################
1027 def restart_service ():
1028     global service
1029     print ("==================== Stopping %s" % service)
1030     os.system("service %s stop" % service)
1031     print ("==================== Starting %s" % service)
1032     os.system("service %s start" % service)
1033
1034 ####################
1035 def prompt_variable (cdef, cread, cwrite, category, variable,
1036                      show_comments, support_next=False):
1037
1038     assert category.has_key('id')
1039     assert variable.has_key('id')
1040
1041     category_id = category ['id']
1042     variable_id = variable['id']
1043
1044     while True:
1045         default_value = get_value(cdef,category_id,variable_id)
1046         variable_type = get_type(cdef,category_id,variable_id)
1047         current_value = get_current_value(cread,cwrite,category_id, variable_id)
1048         varname = get_varname (cread,category_id, variable_id)
1049         
1050         if show_comments :
1051             print_name_comments (cdef, category_id, variable_id)
1052         prompt = "== %s : [%s] " % (varname,current_value)
1053         try:
1054             answer = raw_input(prompt).strip()
1055         except EOFError :
1056             raise Exception ('BailOut')
1057         except KeyboardInterrupt:
1058             print "\n"
1059             raise Exception ('BailOut')
1060
1061         # no change
1062         if (answer == "") or (answer == current_value):
1063             return None
1064         elif (answer == "."):
1065             raise Exception ('BailOut')
1066         elif (answer == "#"):
1067             print_name_comments(cread,category_id,variable_id)
1068         elif (answer == "?"):
1069             print variable_usage.strip()
1070         elif (answer == "="):
1071             print ("%s defaults to %s" %(varname,default_value))
1072         # revert to default : remove from cwrite (i.e. site-config)
1073         elif (answer == "/"):
1074             cwrite.delete(category_id,variable_id)
1075             print ("%s reverted to %s" %(varname,default_value))
1076             return
1077         elif (answer == ">"):
1078             if support_next:
1079                 raise Exception ('NextCategory')
1080             else:
1081                 print "No support for next category"
1082         else:
1083             if cdef.validate_type(variable_type, answer):
1084                 variable['value'] = answer
1085                 cwrite.set(category,variable)
1086                 return
1087             else:
1088                 print "Not a valid value"
1089
1090 def prompt_variables_all (cdef, cread, cwrite, show_comments):
1091     try:
1092         for (category_id, (category, variables)) in cread.variables().iteritems():
1093             print ("========== Category = %s" % category_id.upper())
1094             for variable in variables.values():
1095                 try:
1096                     newvar = prompt_variable (cdef, cread, cwrite, category, variable,
1097                                               show_comments, True)
1098                 except Exception, inst:
1099                     if (str(inst) == 'NextCategory'): break
1100                     else: raise
1101                     
1102     except Exception, inst:
1103         if (str(inst) == 'BailOut'): return
1104         else: raise
1105
1106 def prompt_variables_category (cdef, cread, cwrite, cid, show_comments):
1107     cid=cid.lower()
1108     CID=cid.upper()
1109     try:
1110         print ("========== Category = %s" % CID)
1111         for vid in list_category(cdef,cid):
1112             (category,variable) = cdef.locate_varname(vid.upper())
1113             newvar = prompt_variable (cdef, cread, cwrite, category, variable,
1114                                       show_comments, False)
1115     except Exception, inst:
1116         if (str(inst) == 'BailOut'): return
1117         else: raise
1118
1119 ####################
1120 def show_variable (cdef, cread, cwrite,
1121                    category, variable,show_value,show_comments):
1122     assert category.has_key('id')
1123     assert variable.has_key('id')
1124
1125     category_id = category ['id']
1126     variable_id = variable['id']
1127
1128     default_value = get_value(cdef,category_id,variable_id)
1129     current_value = get_current_value(cread,cwrite,category_id,variable_id)
1130     varname = get_varname (cread,category_id, variable_id)
1131     if show_comments :
1132         print_name_comments (cdef, category_id, variable_id)
1133     if show_value:
1134         print "%s = %s" % (varname,current_value)
1135     else:
1136         print "%s" % (varname)
1137
1138 def show_variables_all (cdef, cread, cwrite, show_value, show_comments):
1139     for (category_id, (category, variables)) in cread.variables().iteritems():
1140         print ("========== Category = %s" % category_id.upper())
1141         for variable in variables.values():
1142             show_variable (cdef, cread, cwrite,
1143                            category, variable,show_value,show_comments)
1144
1145 def show_variables_category (cdef, cread, cwrite, cid, show_value,show_comments):
1146     cid=cid.lower()
1147     CID=cid.upper()
1148     print ("========== Category = %s" % CID)
1149     for vid in list_category(cdef,cid):
1150         (category,variable) = cdef.locate_varname(vid.upper())
1151         show_variable (cdef, cread, cwrite, category, variable,
1152                        show_value,show_comments)
1153
1154 ####################
1155 re_mainloop_0arg="^(?P<command>[uUwrRqlLsSeEcvVhH\?])[ \t]*$"
1156 re_mainloop_1arg="^(?P<command>[sSeEvV])[ \t]+(?P<arg>\w+)$"
1157 matcher_mainloop_0arg=re.compile(re_mainloop_0arg)
1158 matcher_mainloop_1arg=re.compile(re_mainloop_1arg)
1159
1160 def mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_config):
1161     global service
1162     while True:
1163         try:
1164             answer = raw_input("Enter command (u for usual changes, w to save, ? for help) ").strip()
1165         except EOFError:
1166             answer =""
1167         except KeyboardInterrupt:
1168             print "\nBye"
1169             sys.exit()
1170
1171         if (answer == "") or (answer in "?hH"):
1172             print mainloop_usage
1173             continue
1174         groups_parse = matcher_mainloop_0arg.match(answer)
1175         command=None
1176         if (groups_parse):
1177             command = groups_parse.group('command')
1178             arg=None
1179         else:
1180             groups_parse = matcher_mainloop_1arg.match(answer)
1181             if (groups_parse):
1182                 command = groups_parse.group('command')
1183                 arg=groups_parse.group('arg')
1184         if not command:
1185             print ("Unknown command >%s< -- use h for help" % answer)
1186             continue
1187
1188         show_comments=command.isupper()
1189         command=command.lower()
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 == "u"):
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"])