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