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