diff --git a/tools/crm.in b/tools/crm.in index 873c37846d..8696198681 100644 --- a/tools/crm.in +++ b/tools/crm.in @@ -1,5936 +1,6003 @@ #!/usr/bin/env python # # 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 shlex import os from tempfile import mkstemp import subprocess import sys import time import readline import copy import xml.dom.minidom import signal import re lineno = -1 class ErrorBuffer(object): ''' Show error messages either immediately or buffered. ''' def __init__(self): self.msg_list = [] self.mode = "immediate" def buffer(self): self.mode = "keep" def release(self): if self.msg_list: print >> sys.stderr, '\n'.join(self.msg_list) if not batch: raw_input("Press enter to continue... ") self.msg_list = [] self.mode = "immediate" def writemsg(self,msg): if self.mode == "immediate": print >> sys.stderr, msg else: self.msg_list.append(msg) def error(self,s): self.writemsg("ERROR: %s" % add_lineno(s)) def warning(self,s): self.writemsg("WARNING: %s" % add_lineno(s)) def info(self,s): self.writemsg("INFO: %s" % add_lineno(s)) def add_lineno(s): if lineno > 0: return "%d: %s" % (lineno,s) else: return s def common_err(s): err_buf.error(s) def common_warn(s): err_buf.warning(s) def common_info(s): err_buf.info(s) def no_prog_err(name): err_buf.error("%s not available, check your installation"%name) def missing_prog_warn(name): err_buf.warning("could not find any %s on the system"%name) def no_attribute_err(attr,obj_type): err_buf.error("required attribute %s not found in %s"%(attr,obj_type)) def bad_def_err(what,msg): err_buf.error("bad %s definition: %s"%(what,msg)) def unsupported_err(name): err_buf.error("%s is not supported"%name) def no_such_obj_err(name): err_buf.error("%s object is not supported"%name) def obj_cli_err(name): err_buf.error("object %s cannot be represented in the CLI notation"%name) def missing_obj_err(node): err_buf.error("object %s:%s missing (shouldn't have happened)"% \ (node.tagName,node.getAttribute("id"))) def constraint_norefobj_err(constraint_id,obj_id): err_buf.error("constraint %s references a resource %s which doesn't exist"% \ (constraint_id,obj_id)) def obj_exists_err(name): err_buf.error("object %s already exists"%name) def no_object_err(name): err_buf.error("object %s does not exist"%name) def invalid_id_err(obj_id): err_buf.error("%s: invalid object id"%obj_id) def id_used_err(node_id): err_buf.error("%s: id is already in use"%node_id) def skill_err(s): err_buf.error("%s: this command is not allowed at this skill level"%' '.join(s)) def syntax_err(s,token = '',context = ''): pfx = "syntax" if context: pfx = "%s in %s" %(pfx,context) if type(s) == type(''): err_buf.error("%s near <%s>"%(pfx,s)) elif token: err_buf.error("%s near <%s>: %s"%(pfx,token,' '.join(s))) else: err_buf.error("%s: %s"%(pfx,' '.join(s))) def bad_attr_usage(cmd,args): err_buf.error("bad usage: %s %s"%(cmd,args)) +def empty_cib_err(): + err_buf.error("No CIB!") def cib_parse_err(msg): err_buf.error("%s"%msg) def cib_no_elem_err(el_name): err_buf.error("CIB contains no '%s' element!"%el_name) def cib_ver_unsupported_err(validator,rel): err_buf.error("CIB not supported: validator '%s', release '%s'"% (validator,rel)) err_buf.error("You may try the upgrade command") def update_err(obj_id,cibadm_opt,xml): if cibadm_opt == '-U': task = "update" elif cibadm_opt == '-D': task = "delete" else: task = "replace" err_buf.error("could not %s %s"%(task,obj_id)) err_buf.info("offending xml: %s" % xml) def not_impl_info(s): err_buf.info("%s is not implemented yet" % s) def ask(msg): print_msg = True while True: ans = raw_input(msg + ' ') if not ans or ans[0].lower() not in ('n','y'): if print_msg: print "Please answer with y[es] or n[o]" print_msg = False else: return ans[0].lower() == 'y' from UserDict import DictMixin class odict(DictMixin): def __init__(self, data=None, **kwdata): self._keys = [] self._data = {} def __setitem__(self, key, value): if key not in self._data: self._keys.append(key) self._data[key] = value def __getitem__(self, key): return self._data[key] def __delitem__(self, key): del self._data[key] self._keys.remove(key) def keys(self): return list(self._keys) def copy(self): copyDict = odict() copyDict._data = self._data.copy() copyDict._keys = self._keys[:] return copyDict global_aliases = { "quit": ("bye","exit"), "end": ("cd","up"), } def setup_aliases(obj): for cmd in obj.cmd_aliases.keys(): for alias in obj.cmd_aliases[cmd]: obj.help_table[alias] = obj.help_table[cmd] obj.cmd_table[alias] = obj.cmd_table[cmd] # # Resource Agents interface (meta-data, parameters, etc) # def lrmadmin(opts, xml = False): ''' Get information directly from lrmd using lrmadmin. ''' lrmadmin_prog = "@sbindir@/lrmadmin" l = [] #print "invoke: lrmadmin",opts if is_program(lrmadmin_prog) and is_process("lrmd"): l = stdin2list("%s %s" % (lrmadmin_prog,opts)) if not xml: l = l[1:] # skip the first line return l def pengine_meta(): ''' Do pengine metadata. ''' pengine = "@CRM_DAEMON_DIR@/pengine" l = [] if is_program(pengine): l = stdin2list("%s metadata" % pengine) return l def get_nodes_text(n,tag): try: node = n.getElementsByTagName(tag)[0] for c in node.childNodes: if c.nodeType == c.TEXT_NODE: return c.data.strip() except: return '' def ra_classes(): ''' List of RA classes. ''' if wcache.is_cached("ra_classes"): return wcache.retrieve("ra_classes") l = lrmadmin("-C") return wcache.store("ra_classes",l) def ra_providers(ra_type,ra_class = "ocf"): 'List of providers for a class:type.' id = "ra_providers-%s-%s" % (ra_class,ra_type) if wcache.is_cached(id): return wcache.retrieve(id) l = lrmadmin("-P %s %s" % (ra_class,ra_type),True) return wcache.store(id,l) def ra_providers_all(ra_class = "ocf"): ''' List of providers for a class:type. ''' id = "ra_providers_all-%s" % ra_class if wcache.is_cached(id): return wcache.retrieve(id) ocf_root = os.getenv("@OCF_ROOT_DIR@") if not ocf_root: ocf_root = "/usr/lib/ocf" dir = ocf_root + "/resource.d" l = [] for s in os.listdir(dir): if os.path.isdir("%s/%s" % (dir,s)): l.append(s) return wcache.store(id,l) def ra_types(ra_class = "ocf", ra_provider = ""): ''' List of RA type for a class. ''' if not ra_class: ra_class = "ocf" id = "ra_types-%s-%s" % (ra_class,ra_provider) if wcache.is_cached(id): return wcache.retrieve(id) if ra_provider: list = [] for ra in lrmadmin("-T %s" % ra_class): if ra_provider in ra_providers(ra,ra_class): list.append(ra) else: list = lrmadmin("-T %s" % ra_class) list.sort() return wcache.store(id,list) class RAInfo(object): ''' A resource agent and whatever's useful about it. ''' ra_tab = " " # four horses skip_ops = ("meta-data", "validate-all") act_attr = ("timeout", "interval", "depth") def __init__(self,ra_class,ra_type,ra_provider = "heartbeat"): self.ra_class = ra_class self.ra_type = ra_type self.ra_provider = ra_provider if not self.ra_provider: self.ra_provider = "heartbeat" self.mk_ra_node() # indirectly caches meta-data and doc def mk_ra_node(self): ''' Return the resource_agent node. ''' meta = self.meta() try: self.doc = xml.dom.minidom.parseString('\n'.join(meta)) except: #common_err("could not parse meta-data for (%s,%s,%s)" \ # % (self.ra_class,self.ra_type,self.ra_provider)) self.ra_node = None return try: self.ra_node = self.doc.getElementsByTagName("resource-agent")[0] except: common_err("meta-data contains no resource-agent element") self.ra_node = None def params(self): ''' Construct a dict: parameters are keys and lists of flags (required and unique) are values. Cached too. ''' id = "ra_params-%s-%s-%s"%(self.ra_class,self.ra_type,self.ra_provider) if wcache.is_cached(id): return wcache.retrieve(id) if not self.ra_node: return None d = {} for pset in self.ra_node.getElementsByTagName("parameters"): for c in pset.getElementsByTagName("parameter"): name = c.getAttribute("name") if not name: continue required = c.getAttribute("required") unique = c.getAttribute("unique") d[name] = (required == '1',unique == '1') return wcache.store(id,d) def params_list(self): ''' List of parameters. ''' try: l = self.params().keys() l.sort() return l except: return [] def reqd_params_list(self): ''' List of required parameters. ''' d = self.params() if not d: return [] return [x for x in d if d[x][0]] def is_param_reqd(self,pname): ''' Is pname a required parameter? ''' return pname in self.reqd_params_list() def meta(self): ''' RA meta-data as raw xml. ''' id = "ra_meta-%s-%s-%s" % (self.ra_class,self.ra_type,self.ra_provider) if wcache.is_cached(id): return wcache.retrieve(id) if self.ra_class == "pengine": l = pengine_meta() else: l = lrmadmin("-M %s %s %s" % (self.ra_class,self.ra_type,self.ra_provider),True) return wcache.store(id, l) def meta_pretty(self): ''' Print the RA meta-data in a human readable form. ''' if not self.ra_node: return '' l = [] title = self.meta_title() l.append(title) longdesc = get_nodes_text(self.ra_node,"longdesc") if longdesc: l.append(longdesc) if self.ra_class != "heartbeat": params = self.meta_parameters() if params: l.append(params.rstrip()) actions = self.meta_actions() if actions: l.append(actions) return '\n\n'.join(l) def meta_title(self): if self.ra_class == "ocf": s = "%s:%s:%s" % (self.ra_class,self.ra_provider,self.ra_type) else: s = "%s:%s" % (self.ra_class,self.ra_type) shortdesc = get_nodes_text(self.ra_node,"shortdesc") if shortdesc and shortdesc != self.ra_type: s = "%s (%s)" % (shortdesc,s) return s def meta_param_head(self,n): type = default = None name = n.getAttribute("name") if not name: return None s = name if n.getAttribute("required") == "1": s = s + "*" try: content = n.getElementsByTagName("content")[0] type = content.getAttribute("type") default = content.getAttribute("default") except: pass if type and default: s = "%s (%s, [%s])" % (s,type,default) elif type: s = "%s (%s)" % (s,type) shortdesc = get_nodes_text(n,"shortdesc") if shortdesc and shortdesc != name: s = "%s: %s" % (s,shortdesc) return s def format_parameter(self,n): l = [] head = self.meta_param_head(n) if not head: common_err("no name attribute for parameter") return "" l.append(head) longdesc = get_nodes_text(n,"longdesc") if longdesc: longdesc = self.ra_tab + longdesc.replace("\n","\n"+self.ra_tab) + '\n' l.append(longdesc) return '\n'.join(l) def meta_parameter(self,param): if not self.ra_node: return '' l = [] for pset in self.ra_node.getElementsByTagName("parameters"): for c in pset.getElementsByTagName("parameter"): if c.getAttribute("name") == param: return self.format_parameter(c) def meta_parameters(self): if not self.ra_node: return '' l = [] for pset in self.ra_node.getElementsByTagName("parameters"): for c in pset.getElementsByTagName("parameter"): s = self.format_parameter(c) if s: l.append(s) if l: return "Parameters (* denotes required, [] the default):\n\n" + '\n'.join(l) def meta_action_head(self,n): name = n.getAttribute("name") if not name: return '' if name in self.skip_ops: return '' s = "%-8s" % name for a in self.act_attr: v = n.getAttribute(a) if v: s = "%s %s=%s" % (s,a,v) return s def meta_actions(self): l = [] for aset in self.ra_node.getElementsByTagName("actions"): for c in aset.getElementsByTagName("action"): s = self.meta_action_head(c) if s: l.append(self.ra_tab + s) if l: return "Operations' defaults (advisory minimum):\n\n" + '\n'.join(l) def cmd_end(cmd,dir = ".."): "Go up one level." levels.droplevel() def cmd_exit(cmd): "Exit the crm program" cmd_end(cmd) if interactive: print "bye" try: readline.write_history_file(hist_file) except: pass for f in tmpfiles: os.unlink(f) sys.exit() # # help or make users feel less lonely # def add_shorthelp(topic,shorthelp,topic_help): ''' Join topics ("%s,%s") if they share the same short description. ''' for i in range(len(topic_help)): if topic_help[i][1] == shorthelp: topic_help[i][0] = "%s,%s" % (topic_help[i][0], topic) return topic_help.append([topic, shorthelp]) def dump_short_help(help_tab): topic_help = [] for topic in help_tab: if topic == '.': continue # with odict, for whatever reason, python parses differently: # help_tab["..."] = ("...","...") and # help_tab["..."] = ("...",""" # ...""") # a parser bug? if type(help_tab[topic][0]) == type(()): shorthelp = help_tab[topic][0][0] else: shorthelp = help_tab[topic][0] add_shorthelp(topic,shorthelp,topic_help) for t,d in topic_help: print "\t%-16s %s" % (t,d) def overview(help_tab): print "" print help_tab['.'][1] print "" print "Available commands:" print "" dump_short_help(help_tab) print "" def topic_help(help_tab,topic): if topic not in help_tab: print "There is no help for topic %s" % topic return if type(help_tab[topic][0]) == type(()): shorthelp = help_tab[topic][0][0] longhelp = help_tab[topic][0][1] else: shorthelp = help_tab[topic][0] longhelp = help_tab[topic][1] print longhelp or shorthelp def cmd_help(help_tab,topic = ''): "help!" # help_tab is an odict (ordered dictionary): # help_tab[topic] = (short_help,long_help) # topic '.' is a special entry for the top level if not topic: overview(help_tab) else: topic_help(help_tab,topic) def add_sudo(cmd): if user_prefs.crm_user: return "sudo -E -u %s %s"%(user_prefs.crm_user,cmd) return cmd def pipe_string(cmd,s): rc = -1 # command failed cmd = add_sudo(cmd) p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE) try: p.communicate(s) p.wait() rc = p.returncode except IOError, msg: common_err(msg) return rc def xml2doc(cmd): cmd = add_sudo(cmd) p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) try: doc = xml.dom.minidom.parse(p.stdout) except xml.parsers.expat.ExpatError,msg: common_err("cannot parse output of %s: %s"%(cmd,msg)) p.wait() return None p.wait() return doc def str2tmp(s): ''' Write the given string to a temporary file. Return the name of the file. ''' fd,tmp = mkstemp() try: f = os.fdopen(fd,"w") except IOError, msg: common_err(msg) return f.write(s) f.close() return tmp def is_filename_sane(name): if re.match("['`/#*?$\[\]]",name): common_err("%s: bad name"%name) return False return True def is_name_sane(name): if re.match("[']",name): common_err("%s: bad name"%name) return False return True def is_value_sane(name): if re.match("[']",name): common_err("%s: bad name"%name) return False return True def ext_cmd(cmd): return subprocess.call(add_sudo(cmd), shell=True) def get_stdin(cmd, stderr_on = True): ''' Run a cmd, return stdin output. stderr_on controls whether to show output which comes on stderr. ''' proc = subprocess.Popen(cmd, shell=True, \ stdout=subprocess.PIPE, \ stderr=stderr_on and None or subprocess.PIPE) outp = proc.communicate()[0] proc.wait() outp = outp.strip() return outp def stdin2list(cmd, stderr_on = True): ''' Run a cmd, fetch output, return it as a list of lines. stderr_on controls whether to show output which comes on stderr. ''' s = get_stdin(add_sudo(cmd), stderr_on) return s.split('\n') def is_program(prog): return subprocess.call("which %s >/dev/null 2>&1"%prog, shell=True) == 0 def find_program(envvar,*args): if envvar and os.getenv(envvar): return os.getenv(envvar) for prog in args: if is_program(prog): return prog def is_id_valid(id): """ Verify that the id follows the definition: http://www.w3.org/TR/1999/REC-xml-names-19990114/#ns-qualnames """ if not id: return False id_re = "^[A-Za-z_][\w._-]*$" return re.match(id_re,id) def check_filename(fname): """ Verify that the string is a filename. """ fname_re = "^[^/]+$" return re.match(fname_re,id) class UserPrefs(object): ''' Keep user preferences here. ''' def __init__(self): self.skill_level = 2 #TODO: set back to 0? self.editor = find_program("EDITOR","vim","vi","emacs","nano") self.pager = find_program("PAGER","less","more","pg") self.dotty = find_program("","dotty") if not self.editor: missing_prog_warn("editor") if not self.pager: missing_prog_warn("pager") self.crm_user = "" self.xmlindent = " " # two spaces def check_skill_level(self,n): return self.skill_level >= n class CliOptions(object): ''' Manage user preferences ''' skill_levels = {"operator":0, "administrator":1, "expert":2} help_table = odict() help_table["."] = ("user preferences","Various user preferences may be set here.") help_table["skill-level"] = ("set skill level", "") help_table["editor"] = ("set prefered editor program", "") help_table["pager"] = ("set prefered pager program", "") help_table["user"] = ("set the cluster user", """ If you need extra privileges to talk to the cluster (i.e. the cib process), then set this to user. Typically, that is either "root" or "hacluster". Don't forget to setup the sudoers file as well. Example: user hacluster """) help_table["quit"] = ("exit the program", "") help_table["help"] = ("show help", "") help_table["end"] = ("go back one level", "") cmd_aliases = global_aliases def __init__(self): self.cmd_table = { "skill-level": (self.set_skill_level,(1,1),0,(skills_list,)), "editor": (self.set_editor,(1,1),0), "pager": (self.set_pager,(1,1),0), "user": (self.set_crm_user,(0,1),0), "save": (self.save_options,(0,0),0), "show": (self.show_options,(0,0),0), "help": (self.help,(0,1),0), "quit": (cmd_exit,(0,0),0), "end": (cmd_end,(0,1),0), } setup_aliases(self) def set_skill_level(self,cmd,skill_level): """usage: skill-level level: operator | administrator | expert""" if skill_level in self.skill_levels: user_prefs.skill_level = self.skill_levels[skill_level] else: common_err("no %s skill level"%skill_level) return False def get_skill_level(self): for s in self.skill_levels: if user_prefs.skill_level == self.skill_levels[s]: return s def set_editor(self,cmd,prog): "usage: editor " if is_program(prog): user_prefs.editor = prog else: common_err("program %s does not exist"% prog) return False def set_pager(self,cmd,prog): "usage: pager " if is_program(prog): user_prefs.pager = prog else: common_err("program %s does not exist"% prog) return False def set_crm_user(self,cmd,user = ''): "usage: user []" user_prefs.crm_user = user def write_rc(self,f): print >>f, '%s "%s"' % ("editor",user_prefs.editor) print >>f, '%s "%s"' % ("pager",user_prefs.pager) print >>f, '%s "%s"' % ("user",user_prefs.crm_user) print >>f, '%s "%s"' % ("skill-level",self.get_skill_level()) def show_options(self,cmd): "usage: show" self.write_rc(sys.stdout) def save_options(self,cmd): "usage: save" try: f = open(rc_file,"w") except os.error,msg: common_err("open: %s"%msg) return print >>f, 'options' self.write_rc(f) print >>f, 'end' f.close() def help(self,cmd,topic = ''): "usage: help []" cmd_help(self.help_table,topic) cib_dump = "cibadmin -Ql" cib_piped = "cibadmin -p" cib_upgrade = "cibadmin --upgrade --force" cib_verify = "crm_verify -V -p" class WCache(object): "Cache stuff. A naive implementation." def __init__(self): self.lists = {} self.stamp = time.time() self.max_cache_age = 600 # seconds def is_cached(self,name): if time.time() - self.stamp > self.max_cache_age: self.stamp = time.time() self.clear() return name in self.lists def store(self,name,lst): self.lists[name] = lst return lst def retrieve(self,name): if self.is_cached(name): return self.lists[name] else: return None def clear(self): self.lists = {} def listshadows(): return stdin2list("ls @CRM_CONFIG_DIR@ | fgrep shadow. | sed 's/^shadow\.//'") class CibShadow(object): ''' CIB shadow management class ''' help_table = odict() help_table["."] = ("",""" CIB shadow management. See the crm_shadow program. """) help_table["new"] = ("create a new shadow CIB", "") help_table["delete"] = ("delete a shadow CIB", "") help_table["reset"] = ("copy live cib to a shadow CIB", "") help_table["commit"] = ("copy a shadow CIB to the cluster", "") help_table["use"] = ("change working CIB", ''' Choose a shadow CIB for further changes. If the name provided is empty, then the live (cluster) CIB is used. ''') help_table["diff"] = ("diff between the shadow CIB and the live CIB", "") help_table["list"] = ("list all shadow CIBs", "") help_table["quit"] = ("exit the program", "") help_table["help"] = ("show help", "") help_table["end"] = ("go back one level", "") envvar = "CIB_shadow" extcmd = ">/dev/null &1" % self.extcmd) except os.error: no_prog_err(self.extcmd) return False return True def new(self,cmd,name,force = ''): "usage: new [force]" if not is_filename_sane(name): return False new_cmd = "%s -c '%s'" % (self.extcmd,name) if force: if force == "force" or force == "--force": new_cmd = "%s --force" % new_cmd else: syntax_err((new_cmd,force), context = 'new') return False if ext_cmd(new_cmd) == 0: common_info("%s shadow CIB created"%name) self.use("use",name) def delete(self,cmd,name): "usage: delete " if not is_filename_sane(name): return False if 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) wcache.clear() else: common_err("failed to commit the %s shadow CIB"%name) return False def diff(self,cmd): "usage: diff" return ext_cmd("%s -d" % self.extcmd) == 0 def list(self,cmd): "usage: list" multicolumn(listshadows()) def _use(self,name): # 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 global cib_in_use if not name or name == "live": os.unsetenv(self.envvar) cib_in_use = "" else: os.putenv(self.envvar,name) cib_in_use = name def use(self,cmd,name = ''): "usage: use []" # check the name argument if not is_filename_sane(name): return False if name and name != "live": if ext_cmd("test -r '@CRM_CONFIG_DIR@/shadow.%s'"%name) != 0: common_err("%s: no such shadow CIB"%name) 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) return True if not cib_factory.has_cib_changed(): self._use(name) # new CIB: refresh the CIB factory cib_factory.refresh() return True saved_cib = cib_in_use self._use(name) 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("you made changes and the requested CIB is different from the current one") common_info("either commit or refresh, then try again") self._use(saved_cib) # revert to the previous CIB return False else: # the requested CIB is equal return True def help(self,cmd,topic = ''): cmd_help(self.help_table,topic) def get_var(l,key): for s in l: a = s.split() if len(a) == 2 and a[0] == key: return a[1] return '' def chk_var(l,key): for s in l: a = s.split() if len(a) == 2 and a[0] == key and a[1]: return True return False def chk_key(l,key): for s in l: a = s.split() if len(a) >= 1 and a[0] == key: return True return False def validate(l): if not chk_var(l,'%name'): common_err("invalid template: missing '%name'") return False if not chk_key(l,'%generate'): common_err("invalid template: missing '%generate'") return False if not (chk_key(l,'%required') or chk_key(l[0:g],'%optional')): common_err("invalid template: missing '%required' or '%optional'") return False return True def fix_tmpl_refs(l,id,pfx): for i in range(len(l)): l[i] = l[i].replace(id,pfx) def fix_tmpl_refs_re(l,regex,repl): for i in range(len(l)): l[i] = re.sub(regex,repl,l[i]) class LoadTemplate(object): ''' Load a template and its dependencies, generate a configuration file which should be relatively easy and straightforward to parse. ''' edit_instructions = '''# Edit instructions: # # Add content only at the end of lines starting with '%%'. # Only add content, don't remove or replace anything. # The parameters following '%required' are not optional, # unlike those following '%optional'. # You may also add comments for future reference.''' no_more_edit = '''# Don't edit anything below this line.''' def __init__(self,name): self.name = name self.all_pre_gen = [] self.all_post_gen = [] self.all_pfx = [] def new_pfx(self,name): i = 1 pfx = name while pfx in self.all_pfx: pfx = "%s_%d" % (name,i) i += 1 self.all_pfx.append(pfx) return pfx def generate(self): return '\n'.join([ \ "# Configuration: %s" % self.name, \ '', \ self.edit_instructions, \ '', \ '\n'.join(self.all_pre_gen), \ self.no_more_edit, \ '', \ '%generate', \ '\n'.join(self.all_post_gen)]) def write_config(self,name): try: f = open("%s/%s" % (Template.conf_dir, name),"w") except os.error,msg: common_err("open: %s"%msg) return False print >>f, self.generate() f.close() return True def load_template(self,tmpl): try: f = open("%s/%s" % (Template.tmpl_dir, tmpl)) except os.error,msg: common_err("open: %s"%msg) return '' l = (''.join(f)).split('\n') if not validate(l): return '' common_info("pulling in template %s" % tmpl) g = l.index('%generate') pre_gen = l[0:g] post_gen = l[g+1:] name = get_var(pre_gen,'%name') for s in l[0:g]: if s.startswith('%depends_on'): a = s.split() if len(a) != 2: common_warn("%s: wrong usage" % s) continue tmpl_id = a[1] tmpl_pfx = self.load_template(a[1]) if tmpl_pfx: fix_tmpl_refs(post_gen,'%'+tmpl_id,'%'+tmpl_pfx) pfx = self.new_pfx(name) fix_tmpl_refs(post_gen, '%_:', '%'+pfx+':') # replace remaining %_, it may be useful at times fix_tmpl_refs(post_gen, '%_', pfx) v_idx = pre_gen.index('%required') or pre_gen.index('%optional') pre_gen.insert(v_idx,'%pfx ' + pfx) self.all_pre_gen += pre_gen self.all_post_gen += post_gen return pfx def post_process(self): pfx_re = '(%s)' % '|'.join(self.all_pfx) fix_tmpl_refs_re(self.all_post_gen, \ '%'+pfx_re+'([^:]|$)', r'\1\2') # process %if ... [%else] ... %fi rmidx_l = [] if_seq = False for i in range(len(self.all_post_gen)): s = self.all_post_gen[i] if if_seq: a = s.split() if len(a) >= 1 and a[0] == '%fi': if_seq = False rmidx_l.append(i) elif len(a) >= 1 and a[0] == '%else': outcome = not outcome rmidx_l.append(i) else: if not outcome: rmidx_l.append(i) continue if not s: continue a = s.split() if len(a) == 2 and a[0] == '%if': outcome = not a[1].startswith('%') # not replaced -> false if_seq = True rmidx_l.append(i) rmidx_l.reverse() for i in rmidx_l: del self.all_post_gen[i] def listtemplates(): l = [] for f in os.listdir(Template.tmpl_dir): if os.path.isfile("%s/%s" % (Template.tmpl_dir,f)): l.append(f) return l def listconfigs(): l = [] for f in os.listdir(Template.conf_dir): if os.path.isfile("%s/%s" % (Template.conf_dir,f)): l.append(f) return l 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(object): ''' Configuration templates. ''' help_table = odict() help_table["."] = ("",""" Configuration templates. """) help_table["new"] = ("create a new configuration from templates", "") help_table["load"] = ("load a configuration", "") help_table["delete"] = ("delete a configuration", "") help_table["list"] = ("list configurations/templates", "") help_table["apply"] = ("process and apply the current configuration to the current CIB", "") help_table["show"] = ("show the processed configuration", "") help_table["quit"] = ("exit the program", "") help_table["help"] = ("show help", "") help_table["end"] = ("go back one level", "") cmd_aliases = global_aliases conf_dir = "%s/%s" % (os.getenv("HOME"),".crmconf") tmpl_dir = "%s/%s" % (conf_dir,"templates") # TODO: autoconf the source templates_source = "/usr/share/doc/packages/pacemaker/templates" def __init__(self): self.cmd_table = { "new": (self.new,(2,),1,(null_list,templates_list,loop)), "load": (self.load,(0,1),1,(config_list,)), "edit": (self.edit,(0,1),1,(config_list,)), "delete": (self.delete,(1,2),1,(config_list,)), "show": (self.show,(0,1),0,(config_list,)), "apply": (self.apply,(0,1),1,(config_list,)), "list": (self.list,(0,1),0), "help": (self.help,(0,1),0), "quit": (cmd_exit,(0,0),0), "end": (cmd_end,(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(self.conf_dir): try: os.makedirs(self.conf_dir) except os.error,msg: common_err("makedirs: %s"%msg) return if not os.path.isdir(self.tmpl_dir): try: os.symlink(self.templates_source, self.tmpl_dir) except os.error,msg: common_err("symlink: %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" % (self.tmpl_dir, tmpl),"r") except os.error,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" % (self.conf_dir, config or self.curr_conf),'r') except os.error,msg: common_err("open: %s"%msg) return '' l = [] piece = [] user_data = {} # states START = 0; PFX = 1; DATA = 2; GENERATE = 3 state = START global lineno save_lineno = lineno lineno = 0 rc = True for inp in f: lineno += 1 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) lineno = save_lineno f.close() if not rc: return '' return self.generate(l,user_data) def new(self,cmd,name,*args): "usage: new