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