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