a little nicer wrt pep8
[sfa.git] / config / sfa-config-tty
1 #!/usr/bin/env python3
2
3 import os
4 import sys
5 import re
6 import traceback
7 import readline
8 from optparse import OptionParser
9
10 from sfa.util.version import version_tag
11 from sfa.util.config import Config
12
13
14 def validator(validated_variables):
15     pass
16 #    maint_user = validated_variables["PLC_API_MAINTENANCE_USER"]
17 #    root_user = validated_variables["PLC_ROOT_USER"]
18 #    if maint_user == root_user:
19 #        errStr="PLC_API_MAINTENANCE_USER=%s cannot be the same as PLC_ROOT_USER=%s"%(maint_user, root_user)
20 #        raise plc_config.ConfigurationException(errStr)
21
22 usual_variables = [
23     "SFA_GENERIC_FLAVOUR",
24     "SFA_INTERFACE_HRN",
25     "SFA_REGISTRY_ROOT_AUTH",
26     "SFA_REGISTRY_HOST",
27     "SFA_AGGREGATE_HOST",
28     "SFA_DB_HOST",
29 ]
30
31 flavour_xml_section_hash = {
32     'pl': 'sfa_plc',
33     'dummy': 'sfa_dummy',
34 }
35 configuration = {
36     'name': 'sfa',
37     'service': "sfa",
38     'usual_variables': usual_variables,
39     'config_dir': "/etc/sfa",
40     'validate_variables': {},
41     'validator': validator,
42 }
43
44
45 # GLOBAL VARIABLES
46 #
47 g_configuration = None
48 usual_variables = None
49 config_dir = None
50 service = None
51
52
53 def noop_validator(validated_variables):
54     pass
55
56 # historically we could also configure the devel pkg....
57
58
59 def init_configuration():
60     global g_configuration
61     global usual_variables, config_dir, service
62
63     usual_variables = g_configuration["usual_variables"]
64     config_dir = g_configuration["config_dir"]
65     service = g_configuration["service"]
66
67     global def_default_config, def_site_config, def_consolidated_config
68     def_default_config = "%s/default_config.xml" % config_dir
69     def_site_config = "%s/configs/site_config" % config_dir
70     def_consolidated_config = "%s/%s_config" % (config_dir, service)
71
72     global mainloop_usage
73     mainloop_usage = """Available commands:
74  Uppercase versions give variables comments, when available
75  u/U\t\t\tEdit usual variables
76  w\t\t\tWrite
77  r\t\t\tRestart %(service)s service
78  R\t\t\tReload %(service)s service (rebuild config files for sh, python....)
79  q\t\t\tQuit (without saving)
80  h/?\t\t\tThis help
81 ---
82  l/L [<cat>|<var>]\tShow Locally modified variables/values
83  s/S [<cat>|<var>]\tShow variables/values (all, in category, single)
84  e/E [<cat>|<var>]\tEdit variables (all, in category, single)
85 ---
86  c\t\t\tList categories
87  v/V [<cat>|<var>]\tList Variables (all, in category, single)
88 ---
89 Typical usage involves: u, [l,] w, r, q
90 """ % globals()
91
92
93 def usage():
94     command_usage = "%prog [options] [default-xml [site-xml [consolidated-xml]]]"
95     init_configuration()
96     command_usage += """
97 \t default-xml defaults to %s
98 \t site-xml defaults to %s
99 \t consolidated-xml defaults to %s""" % (def_default_config, def_site_config, def_consolidated_config)
100     return command_usage
101
102 ####################
103 variable_usage = """Edit Commands :
104 #\tShow variable comments
105 .\tStops prompting, return to mainloop
106 /\tCleans any site-defined value, reverts to default
107 =\tShows default value
108 >\tSkips to next category
109 ?\tThis help
110 """
111
112 ####################
113
114
115 def get_value(config,  category_id, variable_id):
116     value = config.get(category_id, variable_id)
117     return value
118
119
120 def get_type(config, category_id, variable_id):
121     value = config.get(category_id, variable_id)
122     # return variable['type']
123     return str
124
125
126 def get_current_value(cread, cwrite, category_id, variable_id):
127     # the value stored in cwrite, if present, is the one we want
128     try:
129         result = get_value(cwrite, category_id, variable_id)
130     except:
131         result = get_value(cread, category_id, variable_id)
132     return result
133
134 # refrain from using plc_config's _sanitize
135
136
137 def get_varname(config,  category_id, variable_id):
138     varname = category_id + "_" + variable_id
139     config.locate_varname(varname)
140     return varname
141
142 # could not avoid using _sanitize here..
143
144
145 def get_name_comments(config, cid, vid):
146     try:
147         (category, variable) = config.get(cid, vid)
148         (id, name, value, comments) = config._sanitize_variable(cid, variable)
149         return (name, comments)
150     except:
151         return (None, [])
152
153
154 def print_name_comments(config, cid, vid):
155     name, comments = get_name_comments(config, cid, vid)
156     if name:
157         print("### %s" % name)
158     if comments:
159         for line in comments:
160             print("# %s" % line)
161     else:
162         print("!!! No comment associated to %s_%s" % (cid, vid))
163
164 ####################
165
166
167 def list_categories(config):
168     result = []
169     for section in config.sections():
170         result += [section]
171     return result
172
173
174 def print_categories(config):
175     print("Known categories")
176     for cid in list_categories(config):
177         print("%s" % (cid.upper()))
178
179 ####################
180
181
182 def list_category(config, cid):
183     result = []
184     for section in config.sections():
185         if section == cid.lower():
186             for (name, value) in config.items(section):
187                 result += ["%s_%s" % (cid, name)]
188     return result
189
190
191 def print_category(config, cid, show_comments=True):
192     cid = cid.lower()
193     CID = cid.upper()
194     vids = list_category(config, cid)
195     if (len(vids) == 0):
196         print("%s : no such category" % CID)
197     else:
198         print("Category %s contains" % (CID))
199         for vid in vids:
200             print(vid.upper())
201
202 ####################
203
204
205 def consolidate(default_config, site_config, consolidated_config):
206     global service
207     try:
208         conso = Config(default_config)
209         conso.load(site_config)
210         conso.save(consolidated_config)
211     except Exception as inst:
212         print("Could not consolidate, %s" % (str(inst)))
213         return
214     print(("Merged\n\t%s\nand\t%s\ninto\t%s" % (default_config, site_config,
215                                                consolidated_config)))
216
217
218 def reload_service():
219     reload = "sfa-setup.sh reload"
220     print(("Running: {}".format(reload)))
221     os.system(reload)
222
223 ####################
224
225
226 def restart_service():
227     services = ('sfa-db', 'sfa-aggregate', 'sfa-registry')
228     for service in services:
229         restart = ("systemctl -q is-active {s} && "
230                    "{{ echo restarting {s} ; systemctl restart {s}; }}"
231                    .format(s=service))
232         os.system(restart)
233
234 ####################
235
236
237 def prompt_variable(cdef, cread, cwrite, category, variable,
238                     show_comments, support_next=False):
239
240     category_id = category
241     variable_id = variable
242
243     while True:
244         default_value = get_value(cdef, category_id, variable_id)
245         variable_type = get_type(cdef, category_id, variable_id)
246         current_value = get_current_value(
247             cread, cwrite, category_id, variable_id)
248         varname = get_varname(cread, category_id, variable_id)
249
250         if show_comments:
251             print_name_comments(cdef, category_id, variable_id)
252         prompt = "== %s : [%s] " % (varname, current_value)
253         try:
254             answer = input(prompt).strip()
255         except EOFError:
256             raise Exception('BailOut')
257         except KeyboardInterrupt:
258             print("\n")
259             raise Exception('BailOut')
260
261         # no change
262         if (answer == "") or (answer == current_value):
263             return None
264         elif (answer == "."):
265             raise Exception('BailOut')
266         elif (answer == "#"):
267             print_name_comments(cread, category_id, variable_id)
268         elif (answer == "?"):
269             print(variable_usage.strip())
270         elif (answer == "="):
271             print(("%s defaults to %s" % (varname, default_value)))
272         # revert to default : remove from cwrite (i.e. site-config)
273         elif (answer == "/"):
274             cwrite.delete(category_id, variable_id)
275             print(("%s reverted to %s" % (varname, default_value)))
276             return
277         elif (answer == ">"):
278             if support_next:
279                 raise Exception('NextCategory')
280             else:
281                 print("No support for next category")
282         else:
283             if cdef.validate_type(variable_type, answer):
284                 cwrite.set(category_id, variable_id, answer)
285                 return
286             else:
287                 print("Not a valid value")
288
289
290 def prompt_variables_all(cdef, cread, cwrite, show_comments):
291     try:
292         for (category_id, (category, variables)) in cread.variables().items():
293             print(("========== Category = %s" % category_id.upper()))
294             for variable in list(variables.values()):
295                 try:
296                     newvar = prompt_variable(cdef, cread, cwrite, category, variable,
297                                              show_comments, True)
298                 except Exception as inst:
299                     if (str(inst) == 'NextCategory'):
300                         break
301                     else:
302                         raise
303
304     except Exception as inst:
305         if (str(inst) == 'BailOut'):
306             return
307         else:
308             raise
309
310
311 def prompt_variables_category(cdef, cread, cwrite, cid, show_comments):
312     cid = cid.lower()
313     CID = cid.upper()
314     try:
315         print(("========== Category = %s" % CID))
316         for vid in list_category(cdef, cid):
317             (category, variable) = cdef.locate_varname(vid.upper())
318             newvar = prompt_variable(cdef, cread, cwrite, category, variable,
319                                      show_comments, False)
320     except Exception as inst:
321         if (str(inst) == 'BailOut'):
322             return
323         else:
324             raise
325
326 ####################
327
328
329 def show_variable(cdef, cread, cwrite,
330                   category, variable, show_value, show_comments):
331     assert 'id' in category
332     assert 'id' in variable
333
334     category_id = category['id']
335     variable_id = variable['id']
336
337     default_value = get_value(cdef, category_id, variable_id)
338     current_value = get_current_value(cread, cwrite, category_id, variable_id)
339     varname = get_varname(cread, category_id, variable_id)
340     if show_comments:
341         print_name_comments(cdef, category_id, variable_id)
342     if show_value:
343         print("%s = %s" % (varname, current_value))
344     else:
345         print("%s" % (varname))
346
347
348 def show_variables_all(cdef, cread, cwrite, show_value, show_comments):
349     for (category_id, (category, variables)) in cread.variables().items():
350         print(("========== Category = %s" % category_id.upper()))
351         for variable in list(variables.values()):
352             show_variable(cdef, cread, cwrite,
353                           category, variable, show_value, show_comments)
354
355
356 def show_variables_category(cdef, cread, cwrite, cid, show_value, show_comments):
357     cid = cid.lower()
358     CID = cid.upper()
359     print(("========== Category = %s" % CID))
360     for vid in list_category(cdef, cid):
361         (category, variable) = cdef.locate_varname(vid.upper())
362         show_variable(cdef, cread, cwrite, category, variable,
363                       show_value, show_comments)
364
365 ####################
366 re_mainloop_0arg = "^(?P<command>[uUwrRqlLsSeEcvVhH\?])[ \t]*$"
367 re_mainloop_1arg = "^(?P<command>[sSeEvV])[ \t]+(?P<arg>\w+)$"
368 matcher_mainloop_0arg = re.compile(re_mainloop_0arg)
369 matcher_mainloop_1arg = re.compile(re_mainloop_1arg)
370
371
372 def mainloop(cdef, cread, cwrite, default_config, site_config, consolidated_config):
373     global service
374     while True:
375         try:
376             answer = input(
377                 "Enter command (u for usual changes, w to save, ? for help) ").strip()
378         except EOFError:
379             answer = ""
380         except KeyboardInterrupt:
381             print("\nBye")
382             sys.exit()
383
384         if (answer == "") or (answer in "?hH"):
385             print(mainloop_usage)
386             continue
387         groups_parse = matcher_mainloop_0arg.match(answer)
388         command = None
389         if (groups_parse):
390             command = groups_parse.group('command')
391             arg = None
392         else:
393             groups_parse = matcher_mainloop_1arg.match(answer)
394             if (groups_parse):
395                 command = groups_parse.group('command')
396                 arg = groups_parse.group('arg')
397         if not command:
398             print(("Unknown command >%s< -- use h for help" % answer))
399             continue
400
401         show_comments = command.isupper()
402
403         mode = 'ALL'
404         if arg:
405             mode = None
406             arg = arg.lower()
407             variables = list_category(cdef, arg)
408             if len(variables):
409                 # category_id as the category name
410                 # variables as the list of variable names
411                 mode = 'CATEGORY'
412                 category_id = arg
413             arg = arg.upper()
414             (category, variable) = cdef.locate_varname(arg)
415             if variable:
416                 # category/variable as output by locate_varname
417                 mode = 'VARIABLE'
418             if not mode:
419                 print("%s: no such category or variable" % arg)
420                 continue
421
422         if command in "qQ":
423             # todo check confirmation
424             return
425         elif command == "w":
426             try:
427                 # Confirm that various constraints are met before saving file.
428                 validate_variables = g_configuration.get(
429                     'validate_variables', {})
430                 validated_variables = cwrite.verify(
431                     cdef, cread, validate_variables)
432                 validator = g_configuration.get('validator', noop_validator)
433                 validator(validated_variables)
434                 cwrite.save(site_config)
435             except:
436                 print("Save failed due to a configuration exception:")
437                 print(traceback.print_exc())
438                 print(("Could not save -- fix write access on %s" % site_config))
439                 break
440             print(("Wrote %s" % site_config))
441             consolidate(default_config, site_config, consolidated_config)
442             print(("You might want to type 'r' (restart %s), 'R' (reload %s) or 'q' (quit)" %
443                   (service, service)))
444         elif command in "uU":
445             global usual_variables
446             global flavour_xml_section_hash
447             try:
448                 for varname in usual_variables:
449                     (category, variable) = cdef.locate_varname(varname)
450                     if not (category is None and variable is None):
451                         prompt_variable(cdef, cread, cwrite,
452                                         category, variable, False)
453
454                 # set the driver variable according to the already set flavour
455                 generic_flavour = cwrite.items('sfa')[0][1]
456                 for section in cdef.sections():
457                     if generic_flavour in flavour_xml_section_hash and flavour_xml_section_hash[generic_flavour] == section:
458                         for item in cdef.items(section):
459                             category = section
460                             variable = item[0]
461                             prompt_variable(cdef, cread, cwrite,
462                                             category, variable, False)
463                         break
464
465             except Exception as inst:
466                 if (str(inst) != 'BailOut'):
467                     raise
468         elif command == "r":
469             restart_service()
470         elif command == "R":
471             reload_service()
472         elif command == "c":
473             print_categories(cread)
474         elif command in "eE":
475             if mode == 'ALL':
476                 prompt_variables_all(cdef, cread, cwrite, show_comments)
477             elif mode == 'CATEGORY':
478                 prompt_variables_category(
479                     cdef, cread, cwrite, category_id, show_comments)
480             elif mode == 'VARIABLE':
481                 try:
482                     prompt_variable(cdef, cread, cwrite, category, variable,
483                                     show_comments, False)
484                 except Exception as inst:
485                     if str(inst) != 'BailOut':
486                         raise
487         elif command in "vVsSlL":
488             show_value = (command in "sSlL")
489             (c1, c2, c3) = (cdef, cread, cwrite)
490             if command in "lL":
491                 (c1, c2, c3) = (cwrite, cwrite, cwrite)
492             if mode == 'ALL':
493                 show_variables_all(c1, c2, c3, show_value, show_comments)
494             elif mode == 'CATEGORY':
495                 show_variables_category(
496                     c1, c2, c3, category_id, show_value, show_comments)
497             elif mode == 'VARIABLE':
498                 show_variable(c1, c2, c3, category, variable,
499                               show_value, show_comments)
500         else:
501             print(("Unknown command >%s< -- use h for help" % answer))
502
503
504 ####################
505 # creates directory for file if not yet existing
506 def check_dir(config_file):
507     dirname = os.path.dirname(config_file)
508     if (not os.path.exists(dirname)):
509         try:
510             os.makedirs(dirname, 0o755)
511         except OSError as e:
512             print("Cannot create dir %s due to %s - exiting" % (dirname, e))
513             sys.exit(1)
514
515         if (not os.path.exists(dirname)):
516             print("Cannot create dir %s - exiting" % dirname)
517             sys.exit(1)
518         else:
519             print("Created directory %s" % dirname)
520
521 ####################
522
523
524 def optParserSetup(configuration):
525     parser = OptionParser(usage=usage(), version="%prog " + version_tag)
526     parser.set_defaults(config_dir=configuration['config_dir'],
527                         service=configuration['service'],
528                         usual_variables=configuration['usual_variables'])
529     parser.add_option("", "--configdir", dest="config_dir",
530                       help="specify configuration directory")
531     parser.add_option("", "--service", dest="service",
532                       help="specify /etc/init.d style service name")
533     parser.add_option("", "--usual_variable", dest="usual_variables",
534                       action="append", help="add a usual variable")
535     return parser
536
537
538 def main(command, argv, configuration):
539     global g_configuration
540     g_configuration = configuration
541
542     parser = optParserSetup(configuration)
543     (config, args) = parser.parse_args()
544     if len(args) > 3:
545         parser.error("too many arguments")
546
547     configuration['service'] = config.service
548     configuration['usual_variables'] = config.usual_variables
549     configuration['config_dir'] = config.config_dir
550     # add in new usual_variables defined on the command line
551     for usual_variable in config.usual_variables:
552         if usual_variable not in configuration['usual_variables']:
553             configuration['usual_variables'].append(usual_variable)
554
555     # intialize configuration
556     init_configuration()
557
558     default_config, site_config, consolidated_config = \
559         def_default_config, def_site_config, def_consolidated_config
560     if len(args) >= 1:
561         default_config = args[0]
562     if len(args) >= 2:
563         site_config = args[1]
564     if len(args) == 3:
565         consolidated_config = args[2]
566
567     for c in (default_config, site_config, consolidated_config):
568         check_dir(c)
569
570     try:
571         # the default settings only - read only
572         cdef = Config(default_config)
573
574         # in effect : default settings + local settings - read only
575         cread = Config(default_config)
576     except:
577         print(traceback.print_exc())
578         print(("default config files %s not found, is myplc installed ?" %
579               default_config))
580         return 1
581
582     # local settings only, will be modified & saved
583     config_filename = "%s/sfa_config" % config.config_dir
584     cwrite = Config(config_filename)
585     try:
586         cread.load(site_config)
587         cwrite.load(default_config)
588         cwrite.load(site_config)
589     except:
590         cwrite = Config()
591
592     mainloop(cdef, cread, cwrite, default_config,
593              site_config, consolidated_config)
594     return 0
595
596 if __name__ == '__main__':
597     command = sys.argv[0]
598     argv = sys.argv[1:]
599     main(command, argv, configuration)