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