check boolean input (entering "True" would mean false...)
[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         def email_validator(val):
844             return re.match('\A[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9._\-]+\.[a-zA-Z]+\Z', val)
845
846         def boolean_validator (val):
847             return val in ['true', 'false']
848
849         validators = {
850             'email' : email_validator,
851             'ip': ip_validator,
852             'boolean': boolean_validator,
853             }
854
855         # validate it if not a know type.
856         validator = validators.get(variable_type, lambda x: True)
857         return validator(value)
858
859
860
861 # xml.dom.minidom.Text.writexml adds surrounding whitespace to textual
862 # data when pretty-printing. Override this behavior.
863 class TrimText(xml.dom.minidom.Text):
864     def writexml(self, writer, indent="", addindent="", newl=""):
865         xml.dom.minidom.Text.writexml(self, writer, "", "", "")
866
867
868 class TrimTextElement(xml.dom.minidom.Element):
869     def writexml(self, writer, indent="", addindent="", newl=""):
870         writer.write(indent)
871         xml.dom.minidom.Element.writexml(self, writer, "", "", "")
872         writer.write(newl)
873
874
875 ####################
876 # GLOBAL VARIABLES
877 #
878 release_id = "$Id$"
879 release_rev = "$Revision$"
880 release_url = "$URL$"
881
882 g_configuration=None
883 usual_variables=None
884 config_dir=None
885 service=None
886
887 def noop_validator(validated_variables):
888     pass
889
890
891 # historically we could also configure the devel pkg....
892 def init_configuration ():
893     global g_configuration
894     global usual_variables, config_dir, service
895
896     usual_variables=g_configuration["usual_variables"]
897     config_dir=g_configuration["config_dir"]
898     service=g_configuration["service"]
899
900     global def_default_config, def_site_config, def_consolidated_config
901     def_default_config= "%s/default_config.xml" % config_dir
902     def_site_config = "%s/configs/site.xml" % config_dir
903     def_consolidated_config = "%s/%s_config.xml" % (config_dir, service)
904
905     global mainloop_usage
906     mainloop_usage= """Available commands:
907  Uppercase versions give variables comments, when available
908  u/U\t\t\tEdit usual variables
909  w\t\t\tWrite
910  r\t\t\tRestart %(service)s service
911  R\t\t\tReload %(service)s service (rebuild config files for sh, python....)
912  q\t\t\tQuit (without saving)
913  h/?\t\t\tThis help
914 ---
915  l/L [<cat>|<var>]\tShow Locally modified variables/values
916  s/S [<cat>|<var>]\tShow variables/values (all, in category, single)
917  e/E [<cat>|<var>]\tEdit variables (all, in category, single)
918 ---
919  c\t\t\tList categories
920  v/V [<cat>|<var>]\tList Variables (all, in category, single)
921 ---
922 Typical usage involves: u, [l,] w, r, q
923 """ % globals()
924
925 def usage ():
926     command_usage="%prog [options] [default-xml [site-xml [consolidated-xml]]]"
927     init_configuration ()
928     command_usage +="""
929 \t default-xml defaults to %s
930 \t site-xml defaults to %s
931 \t consolidated-xml defaults to %s""" % (def_default_config,def_site_config, def_consolidated_config)
932     return command_usage
933
934 ####################
935 variable_usage= """Edit Commands :
936 #\tShow variable comments
937 .\tStops prompting, return to mainloop
938 /\tCleans any site-defined value, reverts to default
939 =\tShows default value
940 >\tSkips to next category
941 ?\tThis help
942 """
943
944 ####################
945 def get_value (config,  category_id, variable_id):
946     (category, variable) = config.get (category_id, variable_id)
947     return variable['value']
948
949 def get_type (config, category_id, variable_id):
950     (category, variable) = config.get (category_id, variable_id)
951     return variable['type']
952
953 def get_current_value (cread, cwrite, category_id, variable_id):
954     # the value stored in cwrite, if present, is the one we want
955     try:
956         result=get_value (cwrite,category_id,variable_id)
957     except:
958         result=get_value (cread,category_id,variable_id)
959     return result
960
961 # refrain from using plc_config's _sanitize 
962 def get_varname (config,  category_id, variable_id):
963     (category, variable) = config.get (category_id, variable_id)
964     return (category_id+"_"+variable['id']).upper()
965
966 # could not avoid using _sanitize here..
967 def get_name_comments (config, cid, vid):
968     try:
969         (category, variable) = config.get (cid, vid)
970         (id, name, value, comments) = config._sanitize_variable (cid,variable)
971         return (name,comments)
972     except:
973         return (None,[])
974
975 def print_name_comments (config, cid, vid):
976     (name,comments)=get_name_comments(config,cid,vid)
977     if name:
978         print "### %s" % name
979     if comments:
980         for line in comments:
981             print "# %s" % line
982     else:
983         print "!!! No comment associated to %s_%s" % (cid,vid)
984
985 ####################
986 def list_categories (config):
987     result=[]
988     for (category_id, (category, variables)) in config.variables().iteritems():
989         result += [category_id]
990     return result
991
992 def print_categories (config):
993     print "Known categories"
994     for cid in list_categories(config):
995         print "%s" % (cid.upper())
996
997 ####################
998 def list_category (config, cid):
999     result=[]
1000     for (category_id, (category, variables)) in config.variables().iteritems():
1001         if (cid == category_id):
1002             for variable in variables.values():
1003                 result += ["%s_%s" %(cid,variable['id'])]
1004     return result
1005     
1006 def print_category (config, cid, show_comments=True):
1007     cid=cid.lower()
1008     CID=cid.upper()
1009     vids=list_category(config,cid)
1010     if (len(vids) == 0):
1011         print "%s : no such category"%CID
1012     else:
1013         print "Category %s contains" %(CID)
1014         for vid in vids:
1015             print vid.upper()
1016
1017 ####################
1018 def consolidate (default_config, site_config, consolidated_config):
1019     global service
1020     try:
1021         conso = PLCConfiguration (default_config)
1022         conso.load (site_config)
1023         conso.save (consolidated_config)
1024     except Exception, inst:
1025         print "Could not consolidate, %s" % (str(inst))
1026         return
1027     print ("Merged\n\t%s\nand\t%s\ninto\t%s"%(default_config,site_config,
1028                                               consolidated_config))
1029
1030 def reload_service ():
1031     global service
1032     os.system("set -x ; service %s reload" % service)
1033         
1034 ####################
1035 def restart_service ():
1036     global service
1037     print ("==================== Stopping %s" % service)
1038     os.system("service %s stop" % service)
1039     print ("==================== Starting %s" % service)
1040     os.system("service %s start" % service)
1041
1042 ####################
1043 def prompt_variable (cdef, cread, cwrite, category, variable,
1044                      show_comments, support_next=False):
1045
1046     assert category.has_key('id')
1047     assert variable.has_key('id')
1048
1049     category_id = category ['id']
1050     variable_id = variable['id']
1051
1052     while True:
1053         default_value = get_value(cdef,category_id,variable_id)
1054         variable_type = get_type(cdef,category_id,variable_id)
1055         current_value = get_current_value(cread,cwrite,category_id, variable_id)
1056         varname = get_varname (cread,category_id, variable_id)
1057         
1058         if show_comments :
1059             print_name_comments (cdef, category_id, variable_id)
1060         prompt = "== %s : [%s] " % (varname,current_value)
1061         try:
1062             answer = raw_input(prompt).strip()
1063         except EOFError :
1064             raise Exception ('BailOut')
1065         except KeyboardInterrupt:
1066             print "\n"
1067             raise Exception ('BailOut')
1068
1069         # no change
1070         if (answer == "") or (answer == current_value):
1071             return None
1072         elif (answer == "."):
1073             raise Exception ('BailOut')
1074         elif (answer == "#"):
1075             print_name_comments(cread,category_id,variable_id)
1076         elif (answer == "?"):
1077             print variable_usage.strip()
1078         elif (answer == "="):
1079             print ("%s defaults to %s" %(varname,default_value))
1080         # revert to default : remove from cwrite (i.e. site-config)
1081         elif (answer == "/"):
1082             cwrite.delete(category_id,variable_id)
1083             print ("%s reverted to %s" %(varname,default_value))
1084             return
1085         elif (answer == ">"):
1086             if support_next:
1087                 raise Exception ('NextCategory')
1088             else:
1089                 print "No support for next category"
1090         else:
1091             if cdef.validate_type(variable_type, answer):
1092                 variable['value'] = answer
1093                 cwrite.set(category,variable)
1094                 return
1095             else:
1096                 print "Not a valid value"
1097
1098 def prompt_variables_all (cdef, cread, cwrite, show_comments):
1099     try:
1100         for (category_id, (category, variables)) in cread.variables().iteritems():
1101             print ("========== Category = %s" % category_id.upper())
1102             for variable in variables.values():
1103                 try:
1104                     newvar = prompt_variable (cdef, cread, cwrite, category, variable,
1105                                               show_comments, True)
1106                 except Exception, inst:
1107                     if (str(inst) == 'NextCategory'): break
1108                     else: raise
1109                     
1110     except Exception, inst:
1111         if (str(inst) == 'BailOut'): return
1112         else: raise
1113
1114 def prompt_variables_category (cdef, cread, cwrite, cid, show_comments):
1115     cid=cid.lower()
1116     CID=cid.upper()
1117     try:
1118         print ("========== Category = %s" % CID)
1119         for vid in list_category(cdef,cid):
1120             (category,variable) = cdef.locate_varname(vid.upper())
1121             newvar = prompt_variable (cdef, cread, cwrite, category, variable,
1122                                       show_comments, False)
1123     except Exception, inst:
1124         if (str(inst) == 'BailOut'): return
1125         else: raise
1126
1127 ####################
1128 def show_variable (cdef, cread, cwrite,
1129                    category, variable,show_value,show_comments):
1130     assert category.has_key('id')
1131     assert variable.has_key('id')
1132
1133     category_id = category ['id']
1134     variable_id = variable['id']
1135
1136     default_value = get_value(cdef,category_id,variable_id)
1137     current_value = get_current_value(cread,cwrite,category_id,variable_id)
1138     varname = get_varname (cread,category_id, variable_id)
1139     if show_comments :
1140         print_name_comments (cdef, category_id, variable_id)
1141     if show_value:
1142         print "%s = %s" % (varname,current_value)
1143     else:
1144         print "%s" % (varname)
1145
1146 def show_variables_all (cdef, cread, cwrite, show_value, show_comments):
1147     for (category_id, (category, variables)) in cread.variables().iteritems():
1148         print ("========== Category = %s" % category_id.upper())
1149         for variable in variables.values():
1150             show_variable (cdef, cread, cwrite,
1151                            category, variable,show_value,show_comments)
1152
1153 def show_variables_category (cdef, cread, cwrite, cid, show_value,show_comments):
1154     cid=cid.lower()
1155     CID=cid.upper()
1156     print ("========== Category = %s" % CID)
1157     for vid in list_category(cdef,cid):
1158         (category,variable) = cdef.locate_varname(vid.upper())
1159         show_variable (cdef, cread, cwrite, category, variable,
1160                        show_value,show_comments)
1161
1162 ####################
1163 re_mainloop_0arg="^(?P<command>[uUwrRqlLsSeEcvVhH\?])[ \t]*$"
1164 re_mainloop_1arg="^(?P<command>[sSeEvV])[ \t]+(?P<arg>\w+)$"
1165 matcher_mainloop_0arg=re.compile(re_mainloop_0arg)
1166 matcher_mainloop_1arg=re.compile(re_mainloop_1arg)
1167
1168 def mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_config):
1169     global service
1170     while True:
1171         try:
1172             answer = raw_input("Enter command (u for usual changes, w to save, ? for help) ").strip()
1173         except EOFError:
1174             answer =""
1175         except KeyboardInterrupt:
1176             print "\nBye"
1177             sys.exit()
1178
1179         if (answer == "") or (answer in "?hH"):
1180             print mainloop_usage
1181             continue
1182         groups_parse = matcher_mainloop_0arg.match(answer)
1183         command=None
1184         if (groups_parse):
1185             command = groups_parse.group('command')
1186             arg=None
1187         else:
1188             groups_parse = matcher_mainloop_1arg.match(answer)
1189             if (groups_parse):
1190                 command = groups_parse.group('command')
1191                 arg=groups_parse.group('arg')
1192         if not command:
1193             print ("Unknown command >%s< -- use h for help" % answer)
1194             continue
1195
1196         show_comments=command.isupper()
1197
1198         mode='ALL'
1199         if arg:
1200             mode=None
1201             arg=arg.lower()
1202             variables=list_category (cdef,arg)
1203             if len(variables):
1204                 # category_id as the category name
1205                 # variables as the list of variable names
1206                 mode='CATEGORY'
1207                 category_id=arg
1208             arg=arg.upper()
1209             (category,variable)=cdef.locate_varname(arg)
1210             if variable:
1211                 # category/variable as output by locate_varname
1212                 mode='VARIABLE'
1213             if not mode:
1214                 print "%s: no such category or variable" % arg
1215                 continue
1216
1217         if command in "qQ":
1218             # todo check confirmation
1219             return
1220         elif command == "w":
1221             try:
1222                 # Confirm that various constraints are met before saving file.
1223                 validate_variables = g_configuration.get('validate_variables',{})
1224                 validated_variables = cwrite.verify(cdef, cread, validate_variables)
1225                 validator = g_configuration.get('validator',noop_validator)
1226                 validator(validated_variables)
1227                 cwrite.save(site_config)
1228             except ConfigurationException, e:
1229                 print "Save failed due to a configuration exception: %s" % e
1230                 break;
1231             except:
1232                 print traceback.print_exc()
1233                 print ("Could not save -- fix write access on %s" % site_config)
1234                 break
1235             print ("Wrote %s" % site_config)
1236             consolidate(default_config, site_config, consolidated_config)
1237             print ("You might want to type 'r' (restart %s), 'R' (reload %s) or 'q' (quit)" % \
1238                    (service,service))
1239         elif command in "uU":
1240             global usual_variables
1241             try:
1242                 for varname in usual_variables:
1243                     (category,variable) = cdef.locate_varname(varname)
1244                     if not (category is None and variable is None):
1245                         prompt_variable(cdef, cread, cwrite, category, variable, False)
1246             except Exception, inst:
1247                 if (str(inst) != 'BailOut'):
1248                     raise
1249         elif command == "r":
1250             restart_service()
1251         elif command == "R":
1252             reload_service()
1253         elif command == "c":
1254             print_categories(cread)
1255         elif command in "eE":
1256             if mode == 'ALL':
1257                 prompt_variables_all(cdef, cread, cwrite,show_comments)
1258             elif mode == 'CATEGORY':
1259                 prompt_variables_category(cdef,cread,cwrite,category_id,show_comments)
1260             elif mode == 'VARIABLE':
1261                 try:
1262                     prompt_variable (cdef,cread,cwrite,category,variable,
1263                                      show_comments,False)
1264                 except Exception, inst:
1265                     if str(inst) != 'BailOut':
1266                         raise
1267         elif command in "vVsSlL":
1268             show_value=(command in "sSlL")
1269             (c1,c2,c3) = (cdef, cread, cwrite)
1270             if command in "lL":
1271                 (c1,c2,c3) = (cwrite,cwrite,cwrite)
1272             if mode == 'ALL':
1273                 show_variables_all(c1,c2,c3,show_value,show_comments)
1274             elif mode == 'CATEGORY':
1275                 show_variables_category(c1,c2,c3,category_id,show_value,show_comments)
1276             elif mode == 'VARIABLE':
1277                 show_variable (c1,c2,c3,category,variable,show_value,show_comments)
1278         else:
1279             print ("Unknown command >%s< -- use h for help" % answer)
1280
1281 ####################
1282 # creates directory for file if not yet existing
1283 def check_dir (config_file):
1284     dirname = os.path.dirname (config_file)
1285     if (not os.path.exists (dirname)):
1286         try:
1287             os.makedirs(dirname,0755)
1288         except OSError, e:
1289             print "Cannot create dir %s due to %s - exiting" % (dirname,e)
1290             sys.exit(1)
1291             
1292         if (not os.path.exists (dirname)):
1293             print "Cannot create dir %s - exiting" % dirname
1294             sys.exit(1)
1295         else:
1296             print "Created directory %s" % dirname
1297                 
1298 ####################
1299 def optParserSetup(configuration):
1300     parser = OptionParser(usage=usage(), version="%prog " + release_rev + release_url )
1301     parser.set_defaults(config_dir=configuration['config_dir'],
1302                         service=configuration['service'],
1303                         usual_variables=configuration['usual_variables'])
1304     parser.add_option("","--configdir",dest="config_dir",help="specify configuration directory")
1305     parser.add_option("","--service",dest="service",help="specify /etc/init.d style service name")
1306     parser.add_option("","--usual_variable",dest="usual_variables",action="append", help="add a usual variable")
1307     return parser
1308
1309 def main(command,argv,configuration):
1310     global g_configuration
1311     g_configuration=configuration
1312
1313     parser = optParserSetup(configuration)
1314     (config,args) = parser.parse_args()
1315     if len(args)>3:
1316         parser.error("too many arguments")
1317
1318     configuration['service']=config.service
1319     configuration['usual_variables']=config.usual_variables
1320     configuration['config_dir']=config.config_dir
1321     # add in new usual_variables defined on the command line
1322     for usual_variable in config.usual_variables:
1323         if usual_variable not in configuration['usual_variables']:
1324             configuration['usual_variables'].append(usual_variable)
1325
1326     # intialize configuration
1327     init_configuration()
1328
1329     (default_config,site_config,consolidated_config) = (def_default_config, def_site_config, def_consolidated_config)
1330     if len(args) >= 1:
1331         default_config=args[0]
1332     if len(args) >= 2:
1333         site_config=args[1]
1334     if len(args) == 3:
1335         consolidated_config=args[2]
1336
1337     for c in (default_config,site_config,consolidated_config):
1338         check_dir (c)
1339
1340     try:
1341         # the default settings only - read only
1342         cdef = PLCConfiguration(default_config)
1343
1344         # in effect : default settings + local settings - read only
1345         cread = PLCConfiguration(default_config)
1346
1347     except ConfigurationException, e:
1348         print ("Error %s in default config file %s" %(e,default_config))
1349         return 1
1350     except:
1351         print traceback.print_exc()
1352         print ("default config files %s not found, is myplc installed ?" % default_config)
1353         return 1
1354
1355
1356     # local settings only, will be modified & saved
1357     cwrite=PLCConfiguration()
1358     
1359     try:
1360         cread.load(site_config)
1361         cwrite.load(site_config)
1362     except:
1363         cwrite = PLCConfiguration()
1364
1365     mainloop (cdef, cread, cwrite, default_config, site_config, consolidated_config)
1366     return 0
1367
1368 if __name__ == '__main__':
1369     import sys
1370     if len(sys.argv) > 1 and sys.argv[1] in ['build', 'install', 'uninstall']:
1371         from distutils.core import setup
1372         setup(py_modules=["plc_config"])