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