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