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