diff --git a/shell/modules/main.py b/shell/modules/main.py index cd6f001e1b..3f4bed727c 100644 --- a/shell/modules/main.py +++ b/shell/modules/main.py @@ -1,292 +1,299 @@ # Copyright (C) 2008 Dejan Muhamedagic # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This software is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # import sys import shlex import getopt from utils import * from userprefs import Options, UserPrefs from vars import Vars from ui import cmd_exit from msg import * from levels import Levels def load_rc(rcfile): try: f = open(rcfile) except: return save_stdin = sys.stdin sys.stdin = f while True: inp = multi_input() if inp == None: break try: parse_line(levels,shlex.split(inp)) except ValueError, msg: common_err(msg) f.close() sys.stdin = save_stdin def multi_input(prompt = ''): """ Get input from user Allow multiple lines using a continuation character """ line = [] while True: try: text = raw_input(prompt) except EOFError: return None err_buf.incr_lineno() if options.regression_tests: print ".INP:",text sys.stdout.flush() sys.stderr.flush() stripped = text.strip() if stripped.endswith('\\'): stripped = stripped.rstrip('\\') line.append(stripped) if prompt: prompt = '> ' else: line.append(stripped) break return ''.join(line) def check_args(args,argsdim): if not argsdim: return True if len(argsdim) == 1: minargs = argsdim[0] return len(args) >= minargs else: minargs,maxargs = argsdim return len(args) >= minargs and len(args) <= maxargs # # Note on parsing # # Parsing tables are python dictionaries. # # Keywords are used as keys and the corresponding values are # lists (actually tuples, since they should be read-only) or # classes. In the former case, the keyword is a terminal and # in the latter, a new object for the class is created. The class # must have the cmd_table variable. # # The list has the following content: # # function: a function to handle this command # numargs_list: number of minimum/maximum arguments; for example, # (0,1) means one optional argument, (1,1) one required; if the # list is empty then the function will parse arguments itself # required minimum skill level: operator, administrator, expert # (encoded as a small integer from 0 to 2) # list of completer functions (optional) # def show_usage(cmd): p = None try: p = cmd.__doc__ except: pass if p: print >> sys.stderr, p else: syntax_err(cmd.__name__) def parse_line(lvl,s): if not s: return True if s[0].startswith('#'): return True lvl.mark() pt = lvl.parse_root cmd = None i = 0 for i in range(len(s)): token = s[i] if token in pt: if type(pt[token]) == type(object): + # on entering new level we need to set the + # interactive option _before_ creating the level + if not options.interactive and i == len(s)-1: + set_interactive() lvl.new_level(pt[token],token) pt = lvl.parse_root # move to the next level else: cmd = pt[token] # terminal symbol break # and stop parsing else: syntax_err(s[i:]) lvl.release() return False if cmd: # found a terminal symbol if not user_prefs.check_skill_level(cmd[2]): lvl.release() skill_err(s[i]) return False args = s[i+1:] if not check_args(args,cmd[1]): lvl.release() show_usage(cmd[0]) return False args = s[i:] d = lambda: cmd[0](*args) rv = d() # execute the command lvl.release() return rv != False return True def prereqs(): proglist = "which cibadmin crm_resource crm_attribute crm_mon" for prog in proglist.split(): if not is_program(prog): print >> sys.stderr, "%s not available, check your installation"%prog sys.exit(1) # three modes: interactive (no args supplied), batch (input from # a file), half-interactive (args supplied, but not batch) def cib_prompt(): return vars.cib_in_use or "live" def usage(rc): f = sys.stderr if rc == 0: f = sys.stdout print >> f, """ usage: crm [-D display_type] [-f file] [-hF] [args] Use crm without arguments for an interactive session. Supply one or more arguments for a "single-shot" use. Specify with -f a file which contains a script. Use '-' for standard input or use pipe/redirection. crm displays cli format configurations using a color scheme and/or in uppercase. Pick one of "color" or "uppercase", or use "-D color,uppercase" if you want colorful uppercase. Get plain output by "-D plain". The default may be set in user preferences (options). -F stands for force, if set all operations will behave as if force was specified on the line (e.g. configure commit). Examples: # crm -f stopapp2.cli # crm < stopapp2.cli # crm resource stop global_www # crm status """ sys.exit(rc) user_prefs = UserPrefs.getInstance() options = Options.getInstance() err_buf = ErrorBuffer.getInstance() vars = Vars.getInstance() levels = Levels.getInstance() # prefer the user set PATH os.putenv("PATH", "%s:%s" % (os.getenv("PATH"),vars.crm_daemon_dir)) +def set_interactive(): + '''Set the interactive option only if we're on a tty.''' + if sys.stdin.isatty(): + options.interactive = True + def run(): prereqs() inp_file = '' load_rc(vars.rc_file) if not sys.stdin.isatty(): err_buf.reset_lineno() options.batch = True else: options.interactive = True try: opts, args = getopt.getopt(sys.argv[1:], \ 'hdf:FRD:', ("version","help","debug","file=",\ "force","regression-tests","display=")) for o,p in opts: if o in ("-h","--help"): usage(0) elif o in ("--version"): print >> sys.stdout,("""%s Written by Dejan Muhamedagic """ % vars.crm_version) sys.exit(0) elif o == "-d": user_prefs.set_debug() elif o == "-R": options.regression_tests = True elif o in ("-D","--display"): user_prefs.set_output(p) elif o in ("-F","--force"): user_prefs.set_force() elif o in ("-f","--file"): options.batch = True + options.interactive = False err_buf.reset_lineno() inp_file = p except getopt.GetoptError,msg: print msg usage(1) # this special case is silly, but we have to keep it to # preserve the backward compatibility if len(args) == 1 and args[0].startswith("conf"): parse_line(levels,["configure"]) - if not inp_file and sys.stdin.isatty(): - options.interactive = True elif len(args) > 0: err_buf.reset_lineno() + # we're not sure yet whether it's an interactive session or not + # (single-shot commands aren't) options.interactive = False if parse_line(levels,shlex.split(' '.join(args))): # if the user entered a level, then just continue - if levels.previous(): - if not inp_file and sys.stdin.isatty(): - options.interactive = True - else: + if not levels.previous(): sys.exit(0) else: sys.exit(1) if inp_file == "-": pass elif inp_file: try: f = open(inp_file) except IOError, msg: common_err(msg) usage(2) sys.stdin = f if options.interactive and not options.batch: from completion import setup_readline setup_readline() rc = 0 while True: if options.interactive and not options.batch: vars.prompt = "crm(%s)%s# " % (cib_prompt(),levels.getprompt()) inp = multi_input(vars.prompt) if inp == None: if options.interactive: cmd_exit("eof") else: cmd_exit("eof", rc) try: if not parse_line(levels,shlex.split(inp)): rc = 1 except ValueError, msg: rc = 1 common_err(msg) # vim:ts=4:sw=4:et: diff --git a/shell/modules/ui.py.in b/shell/modules/ui.py.in index 9e1d27ff57..4c83ecb452 100644 --- a/shell/modules/ui.py.in +++ b/shell/modules/ui.py.in @@ -1,1715 +1,1723 @@ # Copyright (C) 2008 Dejan Muhamedagic # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This software is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # import sys import re import os import shlex import time import bz2 from help import HelpSystem, cmd_help from vars import Vars from levels import Levels from cibconfig import mkset_obj, CibFactory from cibstatus import CibStatus from template import LoadTemplate from cliformat import nvpairs2list from ra import * from msg import * from utils import * from xmlutil import * def cmd_end(cmd,dir = ".."): "Go up one level." levels.droplevel() def cmd_exit(cmd,rc = 0): "Exit the crm program" cmd_end(cmd) if options.interactive and not options.batch: print "bye" try: from readline import write_history_file write_history_file(vars.hist_file) except: pass for f in vars.tmpfiles: os.unlink(f) sys.exit(rc) class UserInterface(object): ''' Stuff common to all user interface classes. ''' global_cmd_aliases = { "quit": ("bye","exit"), "end": ("cd","up"), } def __init__(self): + self.help_table = odict() self.cmd_table = odict() self.cmd_table["help"] = (self.help,(0,1),0) self.cmd_table["quit"] = (self.exit,(0,0),0) self.cmd_table["end"] = (self.end,(0,1),0) self.cmd_aliases = self.global_cmd_aliases.copy() + if options.interactive: + self.help_table = help_sys.load_level(self.lvl_name) def end_game(self, no_questions_asked = False): pass def help(self,cmd,topic = ''): "usage: help []" + if not self.help_table: + self.help_table = help_sys.load_level(self.lvl_name) + setup_help_aliases(self) cmd_help(self.help_table,topic) def end(self,cmd,dir = ".."): "usage: end" self.end_game() cmd_end(cmd,dir) def exit(self,cmd): "usage: exit" self.end_game() cmd_exit(cmd) class CliOptions(UserInterface): ''' Manage user preferences ''' lvl_name = "options" + desc_short = "user preferences" + desc_long = """ +Several user preferences are available. Note that it is possible +to save the preferences to a startup file. +""" def __init__(self): UserInterface.__init__(self) - self.help_table = help_sys.load_level("options") self.cmd_table["skill-level"] = (self.set_skill_level,(1,1),0) self.cmd_table["editor"] = (self.set_editor,(1,1),0) self.cmd_table["pager"] = (self.set_pager,(1,1),0) self.cmd_table["user"] = (self.set_crm_user,(0,1),0) self.cmd_table["output"] = (self.set_output,(1,1),0) self.cmd_table["colorscheme"] = (self.set_colors,(1,1),0) self.cmd_table["check-frequency"] = (self.set_check_frequency,(1,1),0) self.cmd_table["check-mode"] = (self.set_check_mode,(1,1),0) self.cmd_table["sort-elements"] = (self.set_sort_elements,(1,1),0) self.cmd_table["save"] = (self.save_options,(0,0),0) self.cmd_table["show"] = (self.show_options,(0,0),0) setup_aliases(self) def set_skill_level(self,cmd,skill_level): """usage: skill-level level: operator | administrator | expert""" return user_prefs.set_skill_level(skill_level) def set_editor(self,cmd,prog): "usage: editor " return user_prefs.set_editor(prog) def set_pager(self,cmd,prog): "usage: pager " return user_prefs.set_pager(prog) def set_crm_user(self,cmd,user = ''): "usage: user []" return user_prefs.set_crm_user(user) def set_output(self,cmd,otypes): "usage: output " return user_prefs.set_output(otypes) def set_colors(self,cmd,scheme): "usage: colorscheme " return user_prefs.set_colors(scheme) def set_check_frequency(self,cmd,freq): "usage: check-frequence " return user_prefs.set_check_freq(freq) def set_check_mode(self,cmd,mode): "usage: check-mode " return user_prefs.set_check_mode(mode) def set_sort_elements(self,cmd,opt): "usage: sort-elements {yes|no}" if not verify_boolean(opt): common_err("%s: bad boolean option"%opt) return True return user_prefs.set_sort_elems(opt) def show_options(self,cmd): "usage: show" return user_prefs.write_rc(sys.stdout) def save_options(self,cmd): "usage: save" return user_prefs.save_options(vars.rc_file) def end_game(self, no_questions_asked = False): if no_questions_asked and not options.interactive: self.save_options("save") class CibShadow(UserInterface): ''' CIB shadow management class ''' lvl_name = "cib" + desc_short = "manage shadow CIBs" + desc_long = """ +A shadow CIB is a regular cluster configuration which is kept in +a file. The CRM and the CRM tools may manage a shadow CIB in the +same way as the live CIB (i.e. the current cluster configuration). +A shadow CIB may be applied to the cluster in one step. +""" extcmd = ">/dev/null &1" % self.extcmd) except os.error: no_prog_err(self.extcmd) return False return True def new(self,cmd,name,*args): "usage: new [withstatus] [force] [empty]" if not is_filename_sane(name): return False for par in args: if not par in ("force","--force","withstatus","empty"): syntax_err((cmd,name,par), context = 'new') return False if "empty" in args: new_cmd = "%s -e '%s'" % (self.extcmd,name) else: new_cmd = "%s -c '%s'" % (self.extcmd,name) if user_prefs.get_force() or "force" in args or "--force" in args: new_cmd = "%s --force" % new_cmd if ext_cmd(new_cmd) == 0: common_info("%s shadow CIB created"%name) self.use("use",name) if "withstatus" in args: cib_status.load("shadow:%s" % name) def _find_pe(self,infile): 'Find a pe input' for p in ("%s/%s", "%s/%s.bz2", "%s/pe-*-%s.bz2"): fl = glob.glob(p % (vars.pe_dir,infile)) if fl: break if not fl: common_err("no %s pe input file"%infile) return '' if len(fl) > 1: common_err("more than one %s pe input file: %s" % \ (infile,' '.join(fl))) return '' return fl[0] def pe_import(self,cmd,infile,name = None): "usage: import {|} []" if name and not is_filename_sane(name): return False # where's the input? if not os.access(infile,os.F_OK): if "/" in infile: common_err("%s: no such file"%infile) return False infile = self._find_pe(infile) if not infile: return False if not name: name = os.path.basename(infile) # read input try: f = open(infile) except IOError,msg: common_err("open: %s"%msg) return s = ''.join(f) f.close() # decompresed and rename shadow if it ends with .bz2 if infile.endswith(".bz2"): name = name.replace(".bz2","") s = bz2.decompress(s) # copy input to the shadow try: f = open(shadowfile(name), "w") except IOError,msg: common_err("open: %s"%msg) return f.write(s) f.close() # use the shadow and load the status from there return self.use("use",name,"withstatus") def delete(self,cmd,name): "usage: delete " if not is_filename_sane(name): return False if vars.cib_in_use == name: common_err("%s shadow CIB is in use"%name) return False if ext_cmd("%s -D '%s' --force" % (self.extcmd,name)) == 0: common_info("%s shadow CIB deleted"%name) else: common_err("failed to delete %s shadow CIB"%name) return False def reset(self,cmd,name): "usage: reset " if not is_filename_sane(name): return False if ext_cmd("%s -r '%s'" % (self.extcmd,name)) == 0: common_info("copied live CIB to %s"%name) else: common_err("failed to copy live CIB to %s"%name) return False def commit(self,cmd,name): "usage: commit " if not is_filename_sane(name): return False if ext_cmd("%s -C '%s' --force" % (self.extcmd,name)) == 0: common_info("commited '%s' shadow CIB to the cluster"%name) else: common_err("failed to commit the %s shadow CIB"%name) return False def diff(self,cmd): "usage: diff" s = get_stdout(add_sudo("%s -d" % self.extcmd_stdout)) page_string(s) def list(self,cmd): "usage: list" if options.regression_tests: for t in listshadows(): print t else: multicolumn(listshadows()) def _use(self,name,withstatus): # Choose a shadow cib for further changes. If the name # provided is empty, then choose the live (cluster) cib. # Don't allow ' in shadow names if not name or name == "live": os.unsetenv(vars.shadow_envvar) vars.cib_in_use = "" if withstatus: cib_status.load("live") else: os.putenv(vars.shadow_envvar,name) vars.cib_in_use = name if withstatus: cib_status.load("shadow:%s" % name) def use(self,cmd,name = '', withstatus = ''): "usage: use [] [withstatus]" # check the name argument if name and not is_filename_sane(name): return False if name and name != "live": if not os.access(shadowfile(name),os.F_OK): common_err("%s: no such shadow CIB"%name) return False if withstatus and withstatus != "withstatus": syntax_err((cmd,withstatus), context = 'use') return False # If invoked from configure # take special precautions try: prev_level = levels.previous().myname() except: prev_level = '' if prev_level != "cibconfig": self._use(name,withstatus) return True if not cib_factory.has_cib_changed(): self._use(name,withstatus) # new CIB: refresh the CIB factory cib_factory.refresh() return True saved_cib = vars.cib_in_use self._use(name,'') # don't load the status yet if not cib_factory.is_current_cib_equal(silent = True): # user made changes and now wants to switch to a # different and unequal CIB; we refuse to cooperate common_err("the requested CIB is different from the current one") if user_prefs.get_force(): common_info("CIB overwrite forced") elif not ask("All changes will be dropped. Do you want to proceed?"): self._use(saved_cib,'') # revert to the previous CIB return False self._use(name,withstatus) # now load the status too return True def check_transition(inp,state,possible_l): if not state in possible_l: common_err("input (%s) in wrong state %s" % (inp,state)) return False return True class Template(UserInterface): ''' Configuration templates. ''' lvl_name = "template" def __init__(self): UserInterface.__init__(self) - self.help_table = help_sys.load_level("template") self.cmd_table["new"] = (self.new,(2,),1) self.cmd_table["load"] = (self.load,(0,1),1) self.cmd_table["edit"] = (self.edit,(0,1),1) self.cmd_table["delete"] = (self.delete,(1,2),1) self.cmd_table["show"] = (self.show,(0,1),0) self.cmd_table["apply"] = (self.apply,(0,2),1) self.cmd_table["list"] = (self.list,(0,1),0) setup_aliases(self) self.init_dir() self.curr_conf = '' def init_dir(self): '''Create the conf directory, link to templates''' if not os.path.isdir(vars.tmpl_conf_dir): try: os.makedirs(vars.tmpl_conf_dir) except os.error,msg: common_err("makedirs: %s"%msg) return def get_depends(self,tmpl): '''return a list of required templates''' # Not used. May need it later. try: tf = open("%s/%s" % (vars.tmpl_dir, tmpl),"r") except IOError,msg: common_err("open: %s"%msg) return l = [] for s in tf: a = s.split() if len(a) >= 2 and a[0] == '%depends_on': l += a[1:] tf.close() return l def replace_params(self,s,user_data): change = False for i in range(len(s)): word = s[i] for p in user_data: # is parameter in the word? pos = word.find('%' + p) if pos < 0: continue endpos = pos + len('%' + p) # and it isn't part of another word? if re.match("[A-Za-z0-9]", word[endpos:endpos+1]): continue # if the value contains a space or # it is a value of an attribute # put quotes around it if user_data[p].find(' ') >= 0 or word[pos-1:pos] == '=': v = '"' + user_data[p] + '"' else: v = user_data[p] word = word.replace('%' + p, v) change = True # we did replace something if change: s[i] = word if 'opt' in s: if not change: s = [] else: s.remove('opt') return s def generate(self,l,user_data): '''replace parameters (user_data) and generate output ''' l2 = [] for piece in l: piece2 = [] for s in piece: s = self.replace_params(s,user_data) if s: piece2.append(' '.join(s)) if piece2: l2.append(' \\\n\t'.join(piece2)) return '\n'.join(l2) def process(self,config = ''): '''Create a cli configuration from the current config''' try: f = open("%s/%s" % (vars.tmpl_conf_dir, config or self.curr_conf),'r') except IOError,msg: common_err("open: %s"%msg) return '' l = [] piece = [] user_data = {} # states START = 0; PFX = 1; DATA = 2; GENERATE = 3 state = START err_buf.start_tmp_lineno() rc = True for inp in f: err_buf.incr_lineno() if inp.startswith('#'): continue if type(inp) == type(u''): inp = inp.encode('ascii') inp = inp.strip() try: s = shlex.split(inp) except ValueError, msg: common_err(msg) continue while '\n' in s: s.remove('\n') if not s: if state == GENERATE and piece: l.append(piece) piece = [] elif s[0] in ("%name","%depends_on","%suggests"): continue elif s[0] == "%pfx": if check_transition(inp,state,(START,DATA)) and len(s) == 2: pfx = s[1] state = PFX elif s[0] == "%required": if check_transition(inp,state,(PFX,)): state = DATA data_reqd = True elif s[0] == "%optional": if check_transition(inp,state,(PFX,DATA)): state = DATA data_reqd = False elif s[0] == "%%": if state != DATA: common_warn("user data in wrong state %s" % state) if len(s) < 2: common_warn("parameter name missing") elif len(s) == 2: if data_reqd: common_err("required parameter %s not set" % s[1]) rc = False elif len(s) == 3: user_data["%s:%s" % (pfx,s[1])] = s[2] else: common_err("%s: syntax error" % inp) elif s[0] == "%generate": if check_transition(inp,state,(DATA,)): state = GENERATE piece = [] elif state == GENERATE: if s: piece.append(s) else: common_err("<%s> unexpected" % inp) if piece: l.append(piece) err_buf.stop_tmp_lineno() f.close() if not rc: return '' return self.generate(l,user_data) def new(self,cmd,name,*args): "usage: new