diff --git a/tools/crm.in b/tools/crm.in index fe9796763c..7f4c8fbd68 100644 --- a/tools/crm.in +++ b/tools/crm.in @@ -1,3448 +1,3448 @@ #!/usr/bin/env python # # Copyright (C) 2008 Dejan Muhamedagic <dmuhamedagic@suse.de> # # 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 from popen2 import Popen3 import sys import readline import copy import xml.dom.minidom 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 '\n'.join(self.msg_list) if interactive: raw_input("Press enter to continue... ") self.msg_list = [] self.mode = "immediate" def writemsg(self,msg): if self.mode == "immediate": print msg else: self.msg_list.append(msg) def error(self,s): self.writemsg("ERROR: %s" % s) def warning(self,s): self.writemsg("WARNING: %s" % s) def info(self,s): self.writemsg(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): err_buf.error("required attribute %s not found in %s"%(attr,obj_type)) 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("object %s:%s missing (shouldn't have happened)"% \ (node.tagName,node.getAttribute("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 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 = ''): if type(s) == type(''): err_buf.error("syntax error near <%s>"%s) elif token: err_buf.error("syntax error near <%s>: %s"%(token,' '.join(s))) else: err_buf.error("syntax error: %s"%' '.join(s)) def bad_attr_usage(cmd,args): err_buf.error("bad usage: %s %s"%(cmd,args)) def info_msg(msg): err_buf.info(msg) def cib_parse_err(msg): err_buf.error("%s"%msg) def cib_ver_unsupported_err(cib_rel): err_buf.error("CIB release '%s' is not supported"% cib_rel) def update_err(obj_id,cibadm_opt,node): if cibadm_opt == '-C': task = "create" elif cibadm_opt == '-D': task = "delete" else: task = "update" err_buf.error("could not %s %s"%(task,obj_id)) err_buf.info("offending xml: %s" % node.toprettyxml()) def not_impl_info(s): err_buf.info("%s is not implemented yet" % s) def ask(msg): ans = raw_input(msg + ' ') if not ans: return False return ans[0].lower() == 'y' def cmd_end(cmd): "Go up one level." levels.droplevel() def cmd_exit(cmd): "Quit" global interactive if interactive: print "bye" try: readline.write_history_file(hist_file) except: pass sys.exit() def dump_short_desc(help_tab): for topic in help_tab: if topic == '.': continue print "\t",topic.ljust(16),help_tab[topic][0] def cmd_help(help_tab,topic = ''): "help!" # help_tab is a dict: topic: (short_desc,long_desc) # '.' is a special entry for the top level if not topic: print "" print help_tab['.'][1] print "" print "Available commands:" print "" dump_short_desc(help_tab) print "" return if topic not in help_tab: print "There is no help for topic %s" % topic return if not help_tab[topic][1]: print help_tab[topic][0] else: print help_tab[topic][1] def pipe_string(cmd,s): if user_prefs.crm_user: p = os.popen("sudo -E -u %s %s"%(user_prefs.crm_user,cmd),'w') else: p = os.popen(cmd,'w') p.write(s) return p.close() #def pipe_string(cmd,s): # 'Run a program, collect and return stdout.' # if user_prefs.crm_user: # p = Popen3("sudo -E -u %s %s"%(user_prefs.crm_user,cmd), None) # else: # p = Popen3(cmd, None) # p.fromchild.close() # p.tochild.write(s) # p.tochild.close() # p.wait() # Stolen from crm_utils.py def os_system(cmd, print_raw=False): 'Run a program, collect and return stdout.' if user_prefs.crm_user: p = Popen3("sudo -E -u %s %s"%(user_prefs.crm_user,cmd), None) else: p = Popen3(cmd, None) p.tochild.close() result = p.fromchild.readlines() p.fromchild.close() p.wait() if print_raw: for line in result: print line.rstrip() return result def ext_cmd(s): if user_prefs.crm_user: return os.system("sudo -E -u %s %s"%(user_prefs.crm_user,s)) else: return os.system(s) def is_program(prog): return os.system("which %s >/dev/null 2>&1"%prog) == 0 def find_program(envvar,*args): if os.getenv(envvar): return os.getenv(envvar) for prog in args: if is_program(prog): return prog 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") 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 = { ".": ("user preferences","Various user preferences may be set here."), "skill-level": ("set skill level", ""), "editor": ("set prefered editor program", ""), "pager": ("set prefered pager program", ""), "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": ("show help", ""), "end": ("go back one level", ""), } 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,0),0), } def set_skill_level(self,cmd,skill_level): """usage: skill-level <level> level: operator | administrator | expert""" global user_prefs 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) 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 <program>" global user_prefs if is_program(prog): user_prefs.editor = prog else: common_err("program %s does not exist"% prog) def set_pager(self,cmd,prog): "usage: pager <program>" global user_prefs if is_program(prog): user_prefs.pager = prog else: common_err("program %s does not exist"% prog) def set_crm_user(self,cmd,user = ''): "usage: user [<crm_user>]" global user_prefs 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" global rc_file global user_prefs 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 [<topic>]" global user_prefs cmd_help(self.help_table,topic) cib_dump = "cibadmin -Ql" cib_piped = "cibadmin -p" cib_verify = "crm_verify -V -p" class WCache(object): "Cache stuff. A naive implementation." def __init__(self): self.lists = {} def is_cached(self,name): return name in self.lists def cache(self,name,list): self.lists[name] = list return list def cached(self,name): if self.is_cached(name): return self.lists[name] else: return None def clear_cache(self): self.lists = {} class CibShadow(object): ''' CIB shadow management class ''' help_table = { ".": ("",""" CIB shadow management. See the crm_shadow program. """), "new": ("create a new shadow CIB", ""), "delete": ("delete a shadow CIB", ""), "reset": ("copy live cib to a shadow CIB", ""), "commit": ("copy a shadow CIB to the cluster", ""), "use": ("change working CIB", ''' Choose a shadow CIB for further changes. If the name provided is empty, then the live (cluster) CIB is used. '''), "diff": ("diff between the shadow CIB and the live CIB", ""), "list": ("list all shadow CIBs", ""), "quit": ("exit the program", ""), "help": ("show help", ""), "end": ("go back one level", ""), } envvar = "CIB_shadow" extcmd = ">/dev/null </dev/null crm_shadow" def __init__(self): self.cmd_table = { "new": (self.new,(1,1),1), "delete": (self.delete,(1,1),1), "reset": (self.reset,(1,1),1), "commit": (self.commit,(1,1),1), "use": (self.use,(0,1),1), "diff": (self.diff,(0,0),1), "list": (self.list,(0,0),1), "help": (self.help,(0,1),0), "quit": (cmd_exit,(0,0),0), "end": (cmd_end,(0,0),0), } self.chkcmd() def chkcmd(self): try: os.system("%s 2>&1" % self.extcmd) except os.error: no_prog_err(self.extcmd) return False return True def new(self,cmd,name): "usage: create <shadow_cib>" if ext_cmd("%s -c %s" % (self.extcmd,name)) == 0: info_msg("%s shadow CIB created"%name) else: common_err("failed to create %s shadow CIB"%name) def delete(self,cmd,name): "usage: delete <shadow_cib>" if ext_cmd("%s -D %s --force" % (self.extcmd,name)) == 0: info_msg("%s shadow CIB deleted"%name) else: common_err("failed to delete %s shadow CIB"%name) def reset(self,cmd,name): "usage: reset <shadow_cib>" if ext_cmd("%s -r %s" % (self.extcmd,name)) == 0: info_msg("copied live CIB to %s"%name) else: common_err("failed to copy live CIB to %s"%name) def commit(self,cmd,name): "usage: commit <shadow_cib>" if ext_cmd("%s -C %s --force" % (self.extcmd,name)) == 0: info_msg("commited %s shadow CIB to the cluster"%name) else: common_err("failed to commit the %s shadow CIB"%name) def diff(self,cmd): "usage: diff" ext_cmd("%s -d" % self.extcmd) def list(self,cmd): "usage: list" ext_cmd("ls @HA_VARLIBDIR@/heartbeat/crm | fgrep shadow.") def use(self,cmd,name = ''): "usage: use [<shadow_cib>]" # Choose a shadow cib for further changes. If the name # provided is empty, then choose the live (cluster) cib. global cib_in_use if name: if ext_cmd("test -r @HA_VARLIBDIR@/heartbeat/crm/shadow.%s"%name) != 0: common_err("%s: no such shadow CIB"%name) return os.putenv(self.envvar,name) else: os.unsetenv(self.envvar) cib_in_use = name def help(self,cmd,topic = ''): cmd_help(self.help_table,topic) def manage_attr(cmd,attr_ext_commands,*args): if len(args) < 3: bad_attr_usage(cmd,' '.join(args)) attr_cmd = attr_ext_commands[args[1]] if not attr_cmd: bad_attr_usage(cmd,' '.join(args)) if args[1] == 'set': if len(args) == 4: ext_cmd(attr_cmd%(args[0],args[2],args[3])) else: bad_attr_usage(cmd,' '.join(args)) elif args[1] in ('delete','show'): if len(args) == 3: ext_cmd(attr_cmd%(args[0],args[2])) else: bad_attr_usage(cmd,' '.join(args)) else: bad_attr_usage(cmd,' '.join(args)) class RscMgmt(object): ''' Resources management class ''' rsc_status_all = "crm_resource -L" rsc_status = "crm_resource -W -r %s" rsc_startstop = "crm_resource --meta -r %s -p target_role -v %s" rsc_manage = "crm_resource --meta -r %s -p is_managed -v %s" rsc_migrate = "crm_resource -M -r %s" rsc_migrateto = "crm_resource -M -r %s -H %s" rsc_unmigrate = "crm_resource -U -r %s" rsc_cleanup = "crm_resource -C -r %s -H %s" rsc_param = { 'set': "crm_resource -r %s -p %s -v %s", 'delete': "crm_resource -r %s -d %s", 'show': "crm_resource -r %s -g %s", } rsc_meta = { 'set': "crm_resource --meta -r %s -p %s -v %s", 'delete': "crm_resource --meta -r %s -d %s", 'show': "crm_resource --meta -r %s -g %s", } rsc_failcount = { 'set': "crm_failcount -r %s -U %s -v %s", 'delete': "crm_failcount -r %s -U %s -D", 'show': "crm_failcount -r %s -U %s -G", } rsc_refresh = "crm_resource -R" rsc_refresh_node = "crm_resource -R -H %s" rsc_reprobe = "crm_resource -P" rsc_reprobe_node = "crm_resource -P -H %s" help_table = { ".": ("","Resource management."), "status": ("show status of resources", ""), "start": ("start a resource", ""), "stop": ("stop a resource", ""), "manage": ("put a resource into managed mode", ""), "unmanage": ("put a resource into unmanaged mode", ""), "migrate": ("migrate a resource to another node", ""), "unmigrate": ("migrate a resource to its prefered node", ""), "param": ("manage a parameter of a resource",""" Manage or display a parameter of a resource (also known as an instance_attribute). Usage: param <rsc> set <param> <value> param <rsc> delete <param> param <rsc> show <param> Example: param ip_0 show ip """), "meta": ("manage a meta attribute",""" Show/edit/delete a meta attribute of a resource. Currently, all meta attributes of a resource may be managed with other commands such as 'resource stop'. Usage: meta <rsc> set <attr> <value> meta <rsc> delete <attr> meta <rsc> show <attr> Example: meta ip_0 set target_role stopped """), "failcount": ("manage failcounts", """ Show/edit/delete the failcount of a resource. Usage: failcount <rsc> set <node> <value> failcount <rsc> delete <node> failcount <rsc> show <node> Example: failcount fs_0 delete node2 """), "cleanup": ("cleanup resource status",""), "refresh": ("refresh CIB from the LRM status",""), "reprobe": ("probe for resources not started by the CRM",""), "quit": ("exit the program", ""), "help": ("show help", ""), "end": ("go back one level", ""), } def __init__(self): self.cmd_table = { "status": (self.status,(0,1),0,(rsc_list,)), "start": (self.start,(1,1),0,(rsc_list,)), "stop": (self.stop,(1,1),0,(rsc_list,)), "manage": (self.manage,(1,1),0,(rsc_list,)), "unmanage": (self.unmanage,(1,1),0,(rsc_list,)), "migrate": (self.migrate,(1,2),0,(rsc_list,listnodes)), "unmigrate": (self.unmigrate,(1,1),0,(rsc_list,)), "param": (self.param,(3,4),1,(rsc_list,attr_cmds)), "meta": (self.meta,(3,4),1,(rsc_list,attr_cmds)), "failcount": (self.failcount,(3,4),0,(rsc_list,attr_cmds,listnodes)), "cleanup": (self.cleanup,(1,2),1,(rsc_list,listnodes)), "refresh": (self.refresh,(0,1),0,(listnodes,)), "reprobe": (self.reprobe,(0,1),0,(listnodes,)), "help": (self.help,(0,1),0), "quit": (cmd_exit,(0,0),0), "end": (cmd_end,(0,0),0), } def status(self,cmd,rsc = None): "usage: status [<rsc>]" if rsc: ext_cmd(self.rsc_status % rsc) else: ext_cmd(self.rsc_status_all) def start(self,cmd,rsc): "usage: start <rsc>" ext_cmd(self.rsc_startstop%(rsc,"Started")) def stop(self,cmd,rsc): "usage: stop <rsc>" ext_cmd(self.rsc_startstop%(rsc,"Stopped")) def manage(self,cmd,rsc): "usage: manage <rsc>" ext_cmd(self.rsc_manage%(rsc,"true")) def unmanage(self,cmd,rsc): "usage: unmanage <rsc>" ext_cmd(self.rsc_manage%(rsc,"false")) def migrate(self,cmd,*args): """usage: migrate <rsc> [<node>]""" if len(args) == 1: ext_cmd(self.rsc_migrate%args[0]) else: ext_cmd(self.rsc_migrateto%(args[0],args[1])) def unmigrate(self,cmd,rsc): "usage: unmigrate <rsc>" ext_cmd(self.rsc_unmigrate%rsc) def cleanup(self,cmd,*args): "usage: cleanup <node>" # Cleanup a resource on a node. Omit node to cleanup on # all live nodes. if len(args) == 2: # remove ext_cmd(self.rsc_cleanup%(args[0],args[1])) else: for n in listnodes(): ext_cmd(self.rsc_cleanup%(args[0],n)) def failcount(self,cmd,*args): """usage: failcount <rsc> set <node> <value> failcount <rsc> delete <node> failcount <rsc> show <node>""" d = lambda: manage_attr(cmd,self.rsc_failcount,*args) d() def param(self,cmd,*args): """usage: param <rsc> set <param> <value> param <rsc> delete <param> param <rsc> show <param>""" d = lambda: manage_attr(cmd,self.rsc_param,*args) d() def meta(self,cmd,*args): """usage: meta <rsc> set <attr> <value> meta <rsc> delete <attr> meta <rsc> show <attr>""" d = lambda: manage_attr(cmd,self.rsc_meta,*args) d() def refresh(self,cmd,*args): 'usage: refresh [<node>]' if len(args) == 1: ext_cmd(self.rsc_refresh_node%args[0]) else: ext_cmd(self.rsc_refresh) def reprobe(self,cmd,*args): 'usage: reprobe [<node>]' if len(args) == 1: ext_cmd(self.rsc_reprobe_node%args[0]) else: ext_cmd(self.rsc_reprobe) def help(self,cmd,topic = ''): cmd_help(self.help_table,topic) all_nodes = "crmadmin -N" def listnodes(): nodes = [] for l in os_system(all_nodes): s = l.split() if s[0] == 'normal': nodes.append(s[2]) return nodes class NodeMgmt(object): ''' Nodes management class ''' node_standby = "crm_standby -U %s -v %s" cib_dump_nodes = "cibadmin -Q -o nodes" dc = "crmadmin -D" node_status = "crmadmin -S %s" node_attr = { 'set': "crm_attribute -t nodes -U %s -n %s -v %s", 'delete': "crm_attribute -D -t nodes -U %s -n %s", 'show': "crm_attribute -G -t nodes -U %s -n %s", } node_status = { 'set': "crm_attribute -t status -U %s -n %s -v %s", 'delete': "crm_attribute -D -t status -U %s -n %s", 'show': "crm_attribute -G -t status -U %s -n %s", } help_table = { ".": ("","Nodes management."), "status": ("show status", ""), "show": ("show node", ""), "standby": ("put node into standby", ""), "online": ("bring node online", ""), "attribute": ("manage attributes", """ Edit node attributes. This kind of attribute should refer to relatively static properties, such as memory size. Usage: attribute <node> set <attr> <value> attribute <node> delete <attr> attribute <node> show <attr> Example: attribute node_1 set memory_size 4096 """), "status-attr": ("manage status attributes", """ Edit node attributes which are in the CIB status section, i.e. attributes which hold properties of a more volatile nature. One typical example is attribute generated by the 'pingd' utility. Usage: ............... status-attr <node> set <attr> <value> status-attr <node> delete <attr> status-attr <node> show <attr> ............... Example: ............... status-attr node_1 show pingd """), "quit": ("exit the program", ""), "help": ("show help", ""), "end": ("go back one level", ""), } def __init__(self): self.cmd_table = { "status": (self.status,(0,1),0,(listnodes,)), "show": (self.show,(0,1),0,(listnodes,)), "standby": (self.standby,(1,1),0,(listnodes,)), "online": (self.online,(1,1),0,(listnodes,)), "attribute": (self.attribute,(3,4),0,(listnodes,attr_cmds)), "status-attr": (self.status_attr,(3,4),0,(listnodes,attr_cmds)), "help": (self.help,(0,1),0), "quit": (cmd_exit,(0,0),0), "end": (cmd_end,(0,0),0), } def status(self,cmd,node = None): 'usage: status [<node>]' ext_cmd(self.cib_dump_nodes) def show(self,cmd,node = None): 'usage: show [<node>]' ext_cmd(self.cib_dump_nodes) def standby(self,cmd,node): 'usage: standby <node>' ext_cmd(self.node_standby%(node,"on")) def online(self,cmd,node): 'usage: online <node>' ext_cmd(self.node_standby%(node,"off")) def attribute(self,cmd,*args): """usage: attribute <rsc> set <node> <value> attribute <rsc> delete <node> attribute <rsc> show <node>""" d = lambda: manage_attr(cmd,self.node_attr,*args) d() def status_attr(self,cmd,*args): """usage: status-attr <rsc> set <node> <value> status-attr <rsc> delete <node> status-attr <rsc> show <node>""" d = lambda: manage_attr(cmd,self.node_status,*args) d() def help(self,cmd,topic = ''): cmd_help(self.help_table,topic) def edit_file(fname): 'Edit a file.' global user_prefs if not fname: return if not user_prefs.editor: return return os.system("%s %s" % (user_prefs.editor,fname)) def page_string(s): 'Write string through a pager.' global user_prefs if not s: return if not user_prefs.pager or not interactive: print s else: pipe_string(user_prefs.pager,s) def lines2cli(s): ''' Convert a string into a list of lines. Replace continuation characters. Strip white space, left and right. Drop empty lines. ''' cl = [] l = s.split('\n') cum = [] for p in l: p = p.strip() if p.endswith('\\'): p = p.rstrip('\\') cum.append(p) else: cum.append(p) cl.append(''.join(cum).strip()) cum = [] if cum: # in case s ends with backslash cl.append(''.join(cum)) return [x for x in cl if x] class CibConfig(object): ''' The configuration class ''' help_table = { ".": ("","CIB configuration."), "verify": ("verify the CIB before commit", "Verify the CIB (before commit)"), "erase": ("erase the CIB", "Erase the CIB (careful!)."), "show": ("display CIB objects", """ The `show` command displays objects. It may display all objects or a set of objects. Specify 'changed' to see what changed. Usage: ............... show [xml] [<id> ...] show [xml] changed ............... """), "edit": ("edit CIB objects", """ This command invokes the editor with the object description. As with the `show` command, the user may choose to edit all objects or a set of objects. If the user insists, he or she may edit the XML edition of the object. Usage: ............... edit [xml] [<id> ...] edit [xml] changed ............... """), "delete": ("delete CIB objects", """ The user may delete one or more objects by specifying a list of ids. If the object to be deleted belongs to a container object, such as group, and it is the only resource in that container, then the container is deleted as well. Usage: ............... delete <id> [<id> ...] ............... """), "save": ("save the CIB to a file", """ Save the configuration to a file. Optionally, as XML. Usage: ............... save [xml] <file> ............... Example: ............... save myfirstcib.txt ............... """), "load": ("import the CIB from a file", """ Load a part of configuration (or all of it) from a local file or a network URL. The various methods of importing refer to the `cibadmin` and its `-C`, `-R`, and `-U` options. The file may be a CLI file or an XML file. Usage: ............... load [xml] method URL method :: replace | update ............... Example: ............... load xml replace myfirstcib.xml load xml replace http://storage.big.com/cibs/bigcib.xml ............... """), "template": ("edit and import a configuration from a template", """ The specified template is loaded into the editor. It's up to the user to make a good CRM configuration out of it. Usage: ............... template [xml] url ............... Example: ............... template two-apaches.txt ............... """), "enter": ("enter the interactive configuration mode", """ Those who prefer more typing, or in case the definition of a resource is rather long, the 'enter' command offers configuration in small steps. Usage: ............... enter <obj_type> <id> ............... Example: ............... enter primitive ip_1 ............... """), "commit": ("commit the changes to the CIB", """ The changes at the configure level are not immediately applied to the CIB, but by this command or on exiting the configure level. Sometimes, the program will refuse to apply the changes, usually for good reason. If you know what you're doing, you may say 'commit force' to force the changes. """), "primitive": ("define a resource", """ The primitive command describes a resource. Usage: ............... primitive <rsc> [<class>:[<provider>:]]<type> [params <param>=<value> [<param>=<value>...]] [meta <attribute>=<value> [<attribute>=<value>...]] [operations id_spec [op op_type [<attribute>=<value>...] ...]] id_spec :: $id=<id> | $id-ref=<id> op_type :: start | stop | monitor ............... Example: ............... primitive apcfence stonith:apcsmart \ params ttydev=/dev/ttyS0 hostlist="node1 node2" \ op start timeout=60s \ op monitor interval=30m timeout=60s primitive www8 apache \ params configfile=/etc/apache/www8.conf \ operations $id-ref=apache_ops ............... """), "group": ("define a group", """ The `group` command creates a group of resources. Usage: ............... group <name> <rsc> [<rsc>...] [params <param>=<value> [<param>=<value>...]] [meta <attribute>=<value> [<attribute>=<value>...]] ............... Example: ............... group internal_www disk0 fs0 internal_ip apache \ meta target_role=stopped ............... """), "clone": ("define a clone", """ The `clone` command creates a resource clone. It may contain a single primitive resource or one group of resources. Usage: ............... clone <name> <rsc> [params <param>=<value> [<param>=<value>...]] [meta <attribute>=<value> [<attribute>=<value>...]] ............... Example: ............... clone cl_fence apc_1 \ meta clone_node_max=1 globally_unique=false ............... """), "ms": ("define a master-slave resource", """ The `ms` command creates a master/slave resource type. It may contain a single primitive resource or one group of resources. Usage: ............... ms <name> <rsc> [params <param>=<value> [<param>=<value>...]] [meta <attribute>=<value> [<attribute>=<value>...]] ............... Example: ............... ms disk1 drbd1 \ meta notify=true globally_unique=false ............... """), "location": ("a location preference", """ `location` defines the preference of nodes for the given resource. The location constraints consist of one or more rules which specify a score to be awarded if the rule matches. Usage: ............... location <id> <rsc> rule [id_spec] [$role=<role>] <score> <expression> [rule [id_spec] [$role=<role>] <score> <expression> ...] id_spec :: $id=<id> | $id-ref=<id> score :: <number> | <attribute> | [-]inf expression :: <single_exp> [bool_op <simple_exp> ...] | <date_expr> bool_op :: or | and single_exp :: <attribute> [type:]<binary_op> <value> | <unary_op> <attribute> type :: string | version | number binary_op :: lt | gt | lte | gte | eq | ne unary_op :: defined | not_defined date_expr :: date_op <start> [<end>] (TBD) ............... Examples: ............... location conn_1 internal_www \ rule 50 uname eq node1 \ rule pingd defined pingd location conn_2 dummy_float \ rule -inf not_defined pingd or pingd lte 0 ............... """), "colocation": ("colocate resources", """ This constraint expresses the placement relation between two resources. Usage: ............... colocation <id> <score> <rsc>[:<role>] <rsc>[:<role>] [symmetrical=<bool>] ............... Example: ............... colocation dummy_and_apache -inf apache dummy ............... """), "order": ("order resources", """ This constraint expresses the order of actions on two resources. Usage: ............... order <id> score-type <first-rsc>[:<action>] <then-rsc>[:<action>] [symmetrical=<bool>] score-type :: advisory | mandatory | <score> ............... Example: ............... order c_apache_1 mandatory apache:start ip_1 ............... """), "property": ("set a cluster property", """ Set the cluster (`crm_config`) options. Usage: ............... property [$id=<set_id>] <option>=<value> [<option>=<value>...] ............... Example: ............... property stonith-enabled=true ............... """), "help": ("show help", ""), "end": ("go back one level", ""), "exit": ("exit program", ""), } def __init__(self): self.cmd_table = { "erase": (self.erase,(0,0),1), "verify": (self.verify,(0,0),1), "commit": (self.commit,(0,1),1), "show": (self.show,(0,),1), "edit": (self.edit,(0,),1), "delete": (self.delete,(1,),1), "save": (self.save,(1,2),1), "load": (self.load,(2,3),1), "template": (self.template,(1,2),1), "enter": (self.enter,(2,2),1), "primitive": (self.conf_primitive,(2,),1), "group": (self.conf_group,(2,),1), "clone": (self.conf_clone,(2,),1), "ms": (self.conf_ms,(2,),1), "location": (self.conf_location,(2,),1), "colocation": (self.conf_colocation,(2,),1), "order": (self.conf_order,(2,),1), "property": (self.conf_property,(1,),1,(property_list,)), "help": (self.help,(0,1),1), "end": (self.end,(0,0),1), "exit": (self.exit,(0,0),1), "test": (self.check_structure,(0,0),1), } if not build_completions: cib_factory.initialize() def check_structure(self,cmd): global cib_factory cib_factory.check_structure() def _mkset_obj(self,*args): if args and args[0] == "xml": obj = lambda: CibObjectSetRaw(*args[1:]) else: obj = lambda: CibObjectSetCli(*args) return obj() def show(self,cmd,*args): "usage: show [xml] [<id>...]" set_obj = self._mkset_obj(*args) set_obj.show() def edit(self,cmd,*args): "usage: edit [xml] [<id>...]" set_obj = self._mkset_obj(*args) set_obj.edit() def verify(self,cmd): "usage: verify" set_obj = self._mkset_obj("xml","NOOBJ") set_obj.verify() def save(self,cmd,*args): "usage: save [xml] <filename>" if args[0] == "xml": file = args[1] set_obj = self._mkset_obj("xml") else: file = args[0] set_obj = self._mkset_obj() set_obj.save_to_file(file) def load(self,cmd,*args): "usage: load [xml] {replace|update} {<url>|<path>}" if args[0] == "xml": url = args[2] method = args[1] set_obj = self._mkset_obj("xml","NOOBJ") else: url = args[1] method = args[0] set_obj = self._mkset_obj("NOOBJ") set_obj.import_file(method,url) def template(self,cmd,*args): "usage: template [xml] {<url>|<path>}" if batch: common_info("template not allowed in batch mode") return if args[0] == "xml": url = args[1] set_obj = self._mkset_obj("xml","NOOBJ") else: url = args[0] set_obj = self._mkset_obj("NOOBJ") set_obj.import_template(url) def delete(self,cmd,*args): "usage: delete <id> [<id>...]" global cib_factory cib_factory.delete(*args) def erase(self,cmd): "usage: erase" global cib_factory cib_factory.erase() def commit(self,cmd,force = None): "usage: commit [force]" global cib_factory if not force: cib_factory.commit() elif force == "force": cib_factory.commit(True) else: syntax_err((cmd,force)) return def enter(self,cmd,obj_type,obj_id): "usage: enter obj_type id" not_impl_info("enter") return global cib_factory if not obj_type in self.interactive: no_such_obj_err(obj_type) return False obj = cib_factory.find_object(obj_id) or \ cib_factory.new_object(obj_type,obj_id) if not obj: return if obj.nocli: obj_cli_err(obj_id) return self.int_inst = self.interactive[obj_type](obj) ptab = { "end": (self.int_inst.end,()) } ptab.update(self.int_inst.cmd_table) return ptab def conf_object(self,cmd,*args): "The configure object command." f = lambda: cib_factory.create_object(cmd,*args) f() def conf_primitive(self,cmd,*args): """usage: primitive <rsc> [<class>:[<provider>:]]<type> [params <param>=<value> [<param>=<value>...]] [meta <attribute>=<value> [<attribute>=<value>...]] [operations id_spec [op op_type [<attribute>=<value>...] ...]]""" self.conf_object(cmd,*args) def conf_group(self,cmd,*args): """usage: group <name> <rsc> [<rsc>...] [params <param>=<value> [<param>=<value>...]] [meta <attribute>=<value> [<attribute>=<value>...]]""" self.conf_object(cmd,*args) def conf_clone(self,cmd,*args): """usage: clone <name> <rsc> [params <param>=<value> [<param>=<value>...]] [meta <attribute>=<value> [<attribute>=<value>...]]""" self.conf_object(cmd,*args) def conf_ms(self,cmd,*args): """usage: ms <name> <rsc> [params <param>=<value> [<param>=<value>...]] [meta <attribute>=<value> [<attribute>=<value>...]]""" self.conf_object(cmd,*args) def conf_location(self,cmd,*args): """usage: location <id> <rsc> rule [id_spec] [$role=<role>] <score> <expression> [rule [id_spec] [$role=<role>] <score> <expression> ...]""" self.conf_object(cmd,*args) def conf_colocation(self,cmd,*args): """usage: colocation <id> <score> <rsc>[:<role>] <rsc>[:<role>] [symmetrical=<bool>]""" self.conf_object(cmd,*args) def conf_order(self,cmd,*args): """usage: order <id> score-type <first-rsc>[:<action>] <then-rsc>[:<action>] [symmetrical=<bool>]""" self.conf_object(cmd,*args) def conf_property(self,cmd,*args): "usage: property [$id=<set_id>] <option>=<value>" self.conf_object(cmd,*args) def end_game(self): global cib_factory global interactive if cib_factory.has_cib_changed(): if not interactive or \ ask("There are changes pending. Do you want to commit them?"): cib_factory.commit() cib_factory.reset() def end(self,cmd): "usage: end" self.end_game() cmd_end(cmd) def help(self,cmd,topic = ''): "usage: help [<topic>]" cmd_help(self.help_table,topic) def exit(self,cmd): "usage: exit" self.end_game() cmd_exit(cmd) def is_element(xmlnode): return xmlnode.nodeType == xmlnode.ELEMENT_NODE def nameandid(xmlnode,level): if xmlnode.nodeType == xmlnode.ELEMENT_NODE: print level*' ',xmlnode.tagName,xmlnode.getAttribute("id"),xmlnode.getAttribute("name") def xmltraverse(xmlnode,fun,ts=0): for c in xmlnode.childNodes: if is_element(c): fun(c,ts) xmltraverse(c,fun,ts+1) def xmltraverse_thin(xmlnode,fun,ts=0): ''' Skip elements which may be resources themselves. NB: Call this only on resource (or constraint) nodes, but never on cib or configuration! ''' for c in xmlnode.childNodes: if is_element(c) and not c.tagName in ('primitive','group'): xmltraverse_thin(c,fun,ts+1) fun(xmlnode,ts) def xml_processnodes(xmlnode,filter,proc): ''' Process with proc all nodes that match filter. ''' node_list = [] for child in xmlnode.childNodes: if filter(child): node_list.append(child) elif child.hasChildNodes(): xml_processnodes(child,filter,proc) if node_list: proc(node_list) # filter the cib def is_whitespace(node): return node.nodeType == node.TEXT_NODE and not node.data.strip() def is_status_node(node): return is_element(node) and node.tagName == "status" container_tags = ("group", "clone", "ms", "master") resource_tags = ("primitive","group","clone","ms","master") constraint_tags = ("rsc_location","rsc_colocation","rsc_order") constraint_rsc_refs = ("rsc","with-rsc","first-rsc","then-rsc") children_tags = ("group", "primitive") nvpairs_tags = ("rsc_location", "meta_attributes", "instance_attributes") def is_emptynvpairs(node): return is_element(node) \ and node.tagName in nvpairs_tags \ and not node.hasChildNodes() def is_container(node): return is_element(node) \ and node.tagName in container_tags def is_resource(node): return is_element(node) \ and node.tagName in resource_tags def is_constraint(node): return is_element(node) \ and node.tagName in constraint_tags def rsc_constraint(rsc_id,cons_node): if not is_element(cons_node): return False for attr in cons_node.attributes.keys(): if attr in constraint_rsc_refs \ and rsc_id == cons_node.getAttribute(attr): return True return False resource_cli_names = ("primitive","group","clone","ms") constraint_cli_names = ("location","colocation","order") def is_resource_cli(s): return s in resource_cli_names def is_constraint_cli(s): return s in constraint_cli_names def sort_container_children(node_list): ''' Make sure that attributes's nodes are first, followed by the elements (primitive/group). The order of elements is not disturbed, they are just shifted to end! ''' for node in node_list: children = [] for c in node.childNodes: if is_element(c) and c.tagName in children_tags: children.append(c) for c in children: node.removeChild(c) for c in children: node.appendChild(c) def rmnodes(node_list): for node in node_list: node.parentNode.removeChild(node) node.unlink() def sanitize_cib(doc): xml_processnodes(doc,is_status_node,rmnodes) xml_processnodes(doc,is_emptynvpairs,rmnodes) xml_processnodes(doc,is_whitespace,rmnodes) xml_processnodes(doc,is_container,sort_container_children) class IdMgmt(object): ''' Make sure that ids are unique. ''' def __init__(self): self._id_store = {} self.ok = True # error var def new(self,node,pfx): ''' Create a unique id for the xml node. ''' global hints_list name = node.getAttribute("name") if node.tagName == "nvpair": node_id = "%s-%s" % (pfx,name) elif node.tagName == "op": interval = node.getAttribute("interval") if interval: node_id = "%s-%s-%s" % (pfx,name,interval) else: node_id = "%s-%s" % (pfx,name) else: try: hint = hints_list[node.tagName] except: pass if hint: node_id = "%s-%s" % (pfx,hint) else: node_id = "%s" % pfx if self.is_used(node_id): for cnt in range(99): # shouldn't really get here try_id = "%s-%d" % (node_id,cnt) if not self.is_used(try_id): node_id = try_id break self.save(node_id) return node_id def check_node(self,node,lvl): node_id = node.getAttribute("id") if not node_id: return if self.is_used(node_id): id_used_err(node_id) self.ok = False return def _store_node(self,node,lvl): self.save(node.getAttribute("id")) def _drop_node(self,node,lvl): self.remove(node.getAttribute("id")) def check_xml(self,node): self.ok = True xmltraverse_thin(node,self.check_node) return self.ok def store_xml(self,node): if not self.check_xml(node): return False xmltraverse_thin(node,self._store_node) return True def remove_xml(self,node): xmltraverse_thin(node,self._drop_node) def replace_xml(self,oldnode,newnode): self.remove_xml(oldnode) if not self.store_xml(newnode): self.store_xml(oldnode) return False return True def is_used(self,node_id): return node_id in self._id_store def save(self,node_id): if not node_id: return self._id_store[node_id] = 1 def remove(self,node_id): if not node_id: return try: del self._id_store[node_id] except KeyError: pass def clear(self): self._id_store = {} def id_in_use(obj_id): global id_store if id_store.is_used(obj_id): id_used_err(obj_id) return True return False def find_cib_node(cib,tag): '''Find an element which is a child of configuration.''' for node in cib.childNodes: if is_element(node) and node.tagName == "configuration": for node2 in node.childNodes: if is_element(node2) and node2.tagName == tag: return node2 return None # # CLI parsing utilities # WARNING: ugly code ahead (to be replaced some day by a proper # yacc parser, if there's such a thing) # def cli_parse_rsctype(s, pl = []): ra_class = "ocf" provider = "heartbeat" list = s.split(':') if len(list) == 3: ra_class,provider,rsc_type = list elif len(list) == 2: ra_class,rsc_type = list elif len(list) == 1: rsc_type = list[0] else: return None pl.append(["class",ra_class]) pl.append(["provider",provider]) pl.append(["type",rsc_type]) def cli_parse_attr(s,pl=[]): if s and '=' in s[0]: n,v = s[0].split('=',1) pl.append([n,v]) cli_parse_attr(s[1:],pl) def parse_resource(s): el_type = s[0] attr_lists_keyw = ["params","meta"] cli_list = [] # the head head = [] head.append(["id",s[1]]) i = 3 if el_type == "primitive": cli_parse_rsctype(s[2],head) if not find_value(head,"type"): syntax_err(s[2:]) return None else: cl = [] cl.append(s[2]) if el_type == "group": while i < len(s): if s[i] in attr_lists_keyw: break else: cl.append(s[i]) i += 1 # skip to the next token head.append(["$children",cl]) cli_list.append([el_type,head]) # the rest state = 0 # 1: reading operations; 2: operations read while len(s) > i+1: pl = [] keyw = s[i] if keyw in attr_lists_keyw: if state == 1: state = 2 attr_lists_keyw.remove(keyw) elif el_type == "primitive" and state == 0 and keyw == "operations": state = 1 elif el_type == "primitive" and state <= 1 and keyw == "op": if state == 0: state = 1 pl.append(["name",s[i+1]]) else: syntax_err(s[i:]) return None if keyw == "op": if len(s) > i+2: cli_parse_attr(s[i+2:],pl) else: cli_parse_attr(s[i+1:],pl) if len(pl) == 0: syntax_err(s[i:]) return None i += len(pl)+1 # interval is obligary for ops, supply 0 if not there if keyw == "op" and not find_value(pl,"interval"): pl.append(["interval","0"]) cli_list.append([keyw,pl]) if len(s) > i: syntax_err(s[i:]) return None return cli_list score_type = {'advisory': '0','mandatory': 'inf'} def cli_parse_score(score,pl): import re if score in score_type: pl.append(["score",score_type[score]]) elif re.match("^[+-]?(inf|infinity|[0-9]+)$",score): pl.append(["score",score]) else: pl.append(["score_attribute",score]) return True binary_ops = ('lt','gt','lte','gte','eq','ne') binary_types = ('string' , 'version' , 'number') unary_ops = ('defined','not_defined') def is_binary_op(s): l = s.split(':') if len(l) == 2: return l[0] in binary_types and l[1] in binary_ops elif len(l) == 1: return l[0] in binary_ops else: return False def set_binary_op(s,pl): l = s.split(':') if len(l) == 2: pl.append(["type",l[0]]) pl.append(["operation",l[1]]) else: pl.append(["operation",l[0]]) def cli_get_expression(s,pl): if len(s) > 1 and s[0] in unary_ops: pl.append(["operation",s[0]]) pl.append(["attribute",s[1]]) elif len(s) > 2 and is_binary_op(s[1]): pl.append(["attribute",s[0]]) set_binary_op(s[1],pl) pl.append(["value",s[2]]) else: return False return True def parse_rule(s): if s[0] != "rule": return 0,None rule_list = [] head_pl = [] rule_list.append([s[0],head_pl]) i = 1 cli_parse_attr(s[i:],head_pl) i += len(head_pl) if find_value(head_pl,"$id-ref"): return i,rule_list if not cli_parse_score(s[i],head_pl): return i,None i += 1 bool_op = '' while len(s) > i+1: pl = [] if not cli_get_expression(s[i:],pl): syntax_err(s[i:]) return i,None rule_list.append(["expression",pl]) i += len(pl) if len(s) > i and s[i] in ('or','and'): if bool_op and bool_op != s[i]: common_err("rule contains different bool operations: %s" % ' '.join(s)) return i,None else: bool_op = s[i] i += 1 if len(s) > i and s[i] == "rule": break if bool_op: head_pl.append(["boolean_op",bool_op]) return i,rule_list def parse_location(s): cli_list = [] head_pl = [] head_pl.append(["id",s[1]]) head_pl.append(["rsc",s[2]]) cli_list.append([s[0],head_pl]) i = 3 while i < len(s): numtoks,l = parse_rule(s[i:]) if not l: return None cli_list += l i += numtoks if len(s) < i: syntax_err(s[i:]) return None return cli_list def cli_opt_symmetrical(s,pl): if not s: return True pl1 = [] cli_parse_attr(s,pl1) if len(pl1) != 1 or not find_value(pl1,"symmetrical"): syntax_err(s) return False pl += pl1 return True roles = ('Stopped', 'Started', 'Master', 'Slave') def cli_parse_rsc_role(s,pl,attr_pfx = ''): l = s.split(':') pl.append([attr_pfx+"rsc",l[0]]) if len(l) == 2: if l[1] not in roles: common_err("bad resource role definition: %s"%s) return False pl.append([attr_pfx+"rsc-role",l[1]]) elif len(l) > 2: common_err("bad resource role definition: %s"%s) return False return True def parse_colocation(s): cli_list = [] head_pl = [] cli_list.append([s[0],head_pl]) if len(s) < 5 or len(s) > 6: syntax_err(s) return None head_pl.append(["id",s[1]]) if not cli_parse_score(s[2],head_pl): return None if not cli_parse_rsc_role(s[3],head_pl): return None if not cli_parse_rsc_role(s[4],head_pl,'with-'): return None if not cli_opt_symmetrical(s[5:],head_pl): return None return cli_list actions = ( 'start', 'promote', 'demote', 'stop') def cli_parse_rsc_action(s,pl,rsc_pos): l = s.split(':') pl.append([rsc_pos,l[0]]) if len(l) == 2: if l[1] not in actions: common_err("bad resource action definition: %s"%s) return False pl.append([rsc_pos+"-action",l[1]]) elif len(l) > 1: common_err("bad resource action definition: %s"%s) return False return True def parse_order(s): cli_list = [] head_pl = [] cli_list.append([s[0],head_pl]) if len(s) < 5 or len(s) > 6: syntax_err(s) return None head_pl.append(["id",s[1]]) if not cli_parse_score(s[2],head_pl): return None if not cli_parse_rsc_action(s[3],head_pl,'first'): return None if not cli_parse_rsc_action(s[4],head_pl,'then'): return None if not cli_opt_symmetrical(s[5:],head_pl): return None return cli_list def parse_constraint(s): if s[0] == "location": return parse_location(s) elif s[0] == "colocation": return parse_colocation(s) elif s[0] == "order": return parse_order(s) def parse_property(s): cli_list = [] head_pl = [] cli_list.append([s[0],head_pl]) cli_parse_attr(s[1:],head_pl) if len(head_pl) < 0 or len(s) > len(head_pl)+1: syntax_err(s) return None return cli_list def parse_cli(s): ''' Input: a list of tokens (or a CLI format string). Return: a list of items; each item is a tuple with two members: a string (tag) and a nvpairs or attributes dict. ''' if type(s) == type(u''): s = s.encode('ascii') if type(s) == type(''): s = shlex.split(s) while '\n' in s: s.remove('\n') if s and s[0].startswith('#'): return None if len(s) > 1 and s[0] == "property": return parse_property(s) if len(s) < 3: # we want at least two tokens syntax_err(s) return None if is_resource_cli(s[0]): return parse_resource(s) elif is_constraint_cli(s[0]): return parse_constraint(s) else: syntax_err(s) return None # # XML generate utilities # hints_list = { "instance_attributes": "params", "meta_attributes": "meta-options", "operations": "ops", "rule": "rule", "expression": "expression", } match_list = { "crm_config": (), "cluster_property_set": (), "instance_attributes": (), "meta_attributes": (), "operations": (), "nvpair": ("name",), "op": ("name","interval"), "rule": ("score","score_attribute","role"), "expression": ("attribute","operation","value"), } def lookup_node(node,oldnode): ''' Find a child of oldnode which matches node. ''' global match_list #print "match:",node.tagName,oldnode.getAttribute("id") if not oldnode: return None try: attr_list = match_list[node.tagName] except KeyError: attr_list = [] for c in oldnode.childNodes: if is_element(c) and node.tagName == c.tagName: failed = False for a in attr_list: if node.getAttribute(a) != c.getAttribute(a): failed = True break if not failed: return c return None def set_id(node,oldnode,id_hint): global id_store id = node.getAttribute("id") or \ (oldnode and oldnode.getAttribute("id")) if not id: id = id_store.new(node,id_hint) else: id_store.save(id) node.setAttribute("id",id) def mkxmlsimple(e,oldnode,id_hint): ''' Create an xml node from the (name,dict) pair. The name is the name of the element. The dict contains a set of attributes. ''' global cib_factory node = cib_factory.createElement(e[0]) for n,v in e[1]: if n == "$children": # this one's skipped continue if n.startswith('$'): n = n.lstrip('$') if (type(v) != type('') and type(v) != type(u'')) \ or v: # skip empty strings node.setAttribute(n,v) set_id(node,lookup_node(node,oldnode),id_hint) return node def find_value(pl,name): for n,v in pl: if n == name: return v return None def mkxmlnvpairs(e,oldnode,id_hint): ''' Create xml from the (name,dict) pair. The name is the name of the element. The dict contains a set of nvpairs. Stuff such as instance_attributes. NB: Other tags not containing nvpairs are fine if the dict is empty. ''' global hints_list global cib_factory node = cib_factory.createElement(e[0]) match_node = lookup_node(node,oldnode) v = find_value(e[1],"$id") if v: node.setAttribute("id",v) e[1].remove(["$id",v]) else: set_id(node,match_node,id_hint) v = find_value(e[1],"$id-ref") if v: node.setAttribute("id-ref",v) e[1].remove(["$id-ref",v]) try: hint = hints_list[e[0]] except: hint = '' hint = hint and "%s_%s" % (id_hint,hint) or id_hint nvpair_pfx = node.getAttribute("id") or hint for n,v in e[1]: nvpair = cib_factory.createElement("nvpair") node.appendChild(nvpair) nvpair.setAttribute("name",n) nvpair.setAttribute("value",v) set_id(nvpair,lookup_node(nvpair,match_node),nvpair_pfx) return node def mkxmlop(e,oldnode,id_hint): ''' Create an operation xml from the (name,dict) pair. ''' global cib_factory node = cib_factory.createElement(e[0]) for n,v in e[1]: node.setAttribute(n,v) tmp = cib_factory.createElement("operations") oldops = lookup_node(tmp,oldnode) # first find old operations set_id(node,lookup_node(node,oldops),id_hint) return node conv_list = { "params": "instance_attributes", "meta": "meta_attributes", "property": "cluster_property_set" } def mkxmlnode(e,oldnode,id_hint): ''' Create xml from the (name,dict) pair. The name is the name of the element. The dict contains either a set of nvpairs or a set of attributes. The id is either generated or copied if found in the provided xml. Stuff such as instance_attributes. ''' if e[0] in conv_list: e[0] = conv_list[e[0]] if e[0] in ("instance_attributes","meta_attributes","operations","cluster_property_set"): return mkxmlnvpairs(e,oldnode,id_hint) elif e[0] == "op": return mkxmlop(e,oldnode,id_hint) else: return mkxmlsimple(e,oldnode,id_hint) def new_cib(): doc = xml.dom.minidom.Document() cib = doc.createElement("cib") doc.appendChild(cib) configuration = doc.createElement("configuration") cib.appendChild(configuration) crm_config = doc.createElement("crm_config") configuration.appendChild(crm_config) nodes = doc.createElement("nodes") configuration.appendChild(nodes) resources = doc.createElement("resources") configuration.appendChild(resources) constraints = doc.createElement("constraints") configuration.appendChild(constraints) return doc,cib,crm_config,nodes,resources,constraints def set_property(set_node,name,value): global cib_factory id = set_node.getAttribute("id") for c in set_node.childNodes: if is_element(c) and c.getAttribute("name") == name: c.setAttribute("value",value) return np = cib_factory.createElement("nvpair") np.setAttribute("id","%s-%s"%(id,name)) - np.setAttribute("name",value) + np.setAttribute("name",name) np.setAttribute("value",value) set_node.appendChild(np) def xml_cmp(n,m): #if hash(n.toxml()) != hash(m.toxml()): #print n.toprettyxml() #print m.toprettyxml() return hash(n.toxml()) == hash(m.toxml()) def get_interesting_nodes(node,nodes = []): global cib_object_map for c in node.childNodes: if is_element(c) and c.tagName in cib_object_map: nodes.append(c) get_interesting_nodes(c,nodes) return nodes def filter_on_tag(nl,tag): return [node for node in nl if node.tagName == tag] def primitives(node_list): return filter_on_tag(node_list,"primitive") def groups(node_list): return filter_on_tag(node_list,"group") def clones(node_list): return filter_on_tag(node_list,"clone") def mss(node_list): return filter_on_tag(node_list,"master") def constraints(node_list): return filter_on_tag(node_list,"rsc_location") \ + filter_on_tag(node_list,"rsc_colocation") \ + filter_on_tag(node_list,"rsc_order") def properties(node_list): return filter_on_tag(node_list,"cluster_property_set") def processing_sort(nl): ''' It's usually important to process cib objects in this order, i.e. simple objects first. ''' return primitives(nl) + groups(nl) + mss(nl) + clones(nl) \ + constraints(nl) + properties(nl) def filter_on_type(cl,obj_type): if type(cl[0]) == type([]): return [cli_list for cli_list in cl if cli_list[0][0] == obj_type] else: return [obj for obj in cl if obj.obj_type == obj_type] def primitives_cli(cl): return filter_on_type(cl,"primitive") def groups_cli(cl): return filter_on_type(cl,"group") def clones_cli(cl): return filter_on_type(cl,"clone") def mss_cli(cl): return filter_on_type(cl,"ms") def constraints_cli(node_list): return filter_on_type(node_list,"location") \ + filter_on_type(node_list,"colocation") \ + filter_on_type(node_list,"order") def properties_cli(cl): return filter_on_type(cl,"property") def processing_sort_cli(cl): return primitives_cli(cl) + groups_cli(cl) + mss_cli(cl) + clones_cli(cl) \ + constraints_cli(cl) + properties_cli(cl) # # CLI format generation utilities (from XML) # def cli_pairs(pl): 'Return a string of name="value" pairs (passed in a list of pairs).' l = [] for n,v in pl: l.append('%s="%s"'%(n,v)) return ' '.join(l) def nvpairs2list(node): "Convert nvpairs to a list of pairs." pl = [] for c in node.getElementsByTagName("nvpair"): name = c.getAttribute("name") value = c.getAttribute("value") pl.append([name,value]) return pl def op2cli(node): pl = [] for name in node.attributes.keys(): if name == "name": action = node.getAttribute(name) elif name != "id": # skip the id pl.append([name,node.getAttribute(name)]) return "op %s %s"%(action,cli_pairs(pl)) def cli_operations(node): s = [] node_id = node.getAttribute("id") if node_id: s.append("operations $id=%s" % node_id) else: idref = node.getAttribute("id-ref") if idref: s.append("operations $id-ref=%s" % idref) for c in node.childNodes: if is_element(c) and c.tagName == "op": s.append(op2cli(c)) return ' \\\n\t'.join(s) def exp2cli(node): operation = node.getAttribute("operation") attribute = node.getAttribute("attribute") value = node.getAttribute("value") if not value: return "%s %s" % (operation,attribute) else: return "%s %s %s" % (attribute,operation,value) def get_score(node): score = node.getAttribute("score") if not score: return node.getAttribute("score_attribute") if score.find("infinity") >= 0: score = score.replace("infinity","INFINITY") elif score.find("inf") >= 0: score = score.replace("inf","INFINITY") return score def cli_rule(node): s = [] node_id = node.getAttribute("id") if node_id: s.append("$id=%s" % node_id) else: idref = node.getAttribute("id-ref") if idref: return "$id-ref=%s" % idref s.append(get_score(node)) bool_op = node.getAttribute("boolean_op") if not bool_op: bool_op = "and" exp = [] for c in node.childNodes: if is_element(c) and c.tagName == "expression": exp.append(exp2cli(c)) expression = (" %s "%bool_op).join(exp) return "%s %s" % (' '.join(s),expression) def primitive_head(node): global cib_object_map obj_type = cib_object_map[node.tagName][0] node_id = node.getAttribute("id") ra_type = node.getAttribute("type") ra_class = node.getAttribute("class") ra_provider = node.getAttribute("provider") s1 = s2 = '' if ra_class: s1 = "%s:"%ra_class if ra_provider: s2 = "%s:"%ra_provider return "%s %s %s"%(obj_type,node_id,''.join((s1,s2,ra_type))) def cont_head(node): global cib_object_map obj_type = cib_object_map[node.tagName][0] node_id = node.getAttribute("id") children = [] for c in node.childNodes: if not is_element(c): continue if (obj_type == "group" and c.tagName == "primitive") or \ c.tagName in ("primitive","group"): children.append(c.getAttribute("id")) elif obj_type in ("clone","ms") and \ c.tagName in ("primitive","group"): children.append(c.getAttribute("id")) return "%s %s %s"%(obj_type,node_id,' '.join(children)) def location_head(node): global cib_object_map obj_type = cib_object_map[node.tagName][0] node_id = node.getAttribute("id") rsc = node.getAttribute("rsc") return "%s %s %s"%(obj_type,node_id,rsc) def mkrscrole(node,n): rsc = node.getAttribute(n) rsc_role = node.getAttribute(n + "-role") if rsc_role: return "%s:%s"%(rsc,rsc_role) else: return rsc def mkrscaction(node,n): rsc = node.getAttribute(n) rsc_action = node.getAttribute(n + "-action") if rsc_action: return "%s:%s"%(rsc_action,rsc) else: return rsc def simple_constraint_head(node): global cib_object_map obj_type = cib_object_map[node.tagName][0] node_id = node.getAttribute("id") score = get_score(node) col = [] if obj_type == "colocation": col.append(mkrscrole(node,"rsc")) col.append(mkrscrole(node,"with-rsc")) else: col.append(mkrscaction(node,"first-rsc")) col.append(mkrscaction(node,"then-rsc")) symm = node.getAttribute("symmetrical") if symm: col.append("symmetrical=%s"%symm) return "%s %s %s"%(obj_type,node_id,' '.join(col)) class CibObjectSet(object): ''' Edit or display a set of cib objects. repr() for objects representation and save() used to store objects into internal structures are defined in subclasses. ''' def __init__(self, *args): self.obj_list = [] def _write_tmp(self,s): fd,tmp = mkstemp() f = os.fdopen(fd,"w") f.write(s) f.close() return tmp def _open_url(self,src): import urllib try: return urllib.urlopen(src) except: pass try: return open(src) except: pass common_err("could not open %s" % src) return None def edit(self): if batch: common_info("edit not allowed in batch mode") return err_buf.buffer() # keep error messages tmp = self._write_tmp(self.repr()) filehash = hash(self.repr()) err_buf.release() # show them, but get an ack from the user while True: if edit_file(tmp) != 0: break f = open(tmp,'r') s = ''.join(f) f.close() if hash(s) == filehash: # file unchanged break if interactive and not self.save(s): if ask("There was a parsing problem. Do you want to edit again?"): continue break os.unlink(tmp) def import_template(self,template): global cib_factory f = self._open_url(template) if not f: return s = ''.join(f) f.close() tmp = self._write_tmp(s) if edit_file(tmp) != 0: os.unlink(tmp) return f = open(tmp,'r') s = ''.join(f) f.close() if interactive: if not ask("Do you want to commit the changes (the existing CIB will be erased)?"): return cib_factory.erase() self.save(s) os.unlink(tmp) def save_to_file(self,file): if interactive and os.access(file,os.F_OK): if not ask("File %s exists. Do you want to overwrite it?"%file): return f = open(file,"w") f.write(self.repr()) f.write('\n') f.close() def show(self): err_buf.buffer() # keep error messages s = self.repr() err_buf.release() # show them, but get an ack from the user page_string(s) def import_file(self,method,file): if method == "replace": if interactive and cib_factory.has_cib_changed(): if not ask("This operation will erase all changes. Do you want to proceed?"): return cib_factory.erase() f = self._open_url(file) if not f: return s = ''.join(f) f.close() self.save(s) def repr(self): pass def save(self,s): ''' For each object: - try to find a corresponding object in obj_list - if not found: create new - if found: replace the object in the obj_list with the new object See below for specific implementations. ''' pass def lookup_cli(self,cli_list): for obj in self.obj_list: if obj.matchcli(cli_list): return obj def lookup(self,obj_type,obj_id): for obj in self.obj_list: if obj.match(obj_type,obj_id): return obj def drop_remaining(self): 'Any remaining objects in obj_list are deleted.' global cib_factory for obj in self.obj_list: cib_factory.delete(obj.obj_id) class CibObjectSetCli(CibObjectSet): ''' Edit or display a set of cib objects (using cli notation). ''' def __init__(self, *args): global cib_factory CibObjectSet.__init__(self, *args) self.obj_list = cib_factory.mkobj_list("cli",*args) def repr(self): "Return a string containing cli format of all objects." if not self.obj_list: return '' return '\n'.join(obj.repr_cli() \ for obj in processing_sort_cli(self.obj_list)) def process(self,cli_list): global cib_factory obj = self.lookup_cli(cli_list) if obj: obj.update_from_cli(cli_list) self.obj_list.remove(obj) else: cib_factory.create_from_cli(cli_list) def save(self,s): l = [] for cli_text in lines2cli(s): cli_list = parse_cli(cli_text) if cli_list: l.append(cli_list) if l: for cli_list in processing_sort_cli(l): self.process(cli_list) self.drop_remaining() return True class CibObjectSetRaw(CibObjectSet): ''' Edit or display one or more CIB objects (XML). ''' def __init__(self, *args): global cib_factory CibObjectSet.__init__(self, *args) self.obj_list = cib_factory.mkobj_list("xml",*args) def repr(self): "Return a string containing xml of all objects." doc = cib_factory.objlist2doc(self.obj_list) s = doc.toprettyxml(user_prefs.xmlindent) doc.unlink() return s def process(self,node): global cib_factory obj = self.lookup(node.tagName,node.getAttribute("id")) if obj: obj.update_from_node(node) self.obj_list.remove(obj) else: cib_factory.create_from_node(node) def save(self,s): try: doc = xml.dom.minidom.parseString(s) except xml.parsers.expat.ExpatError,msg: cib_parse_err(msg) return False sanitize_cib(doc) newnodes = get_interesting_nodes(doc,[]) if newnodes: for node in processing_sort(newnodes): self.process(node) self.drop_remaining() doc.unlink() return True def verify(self): pipe_string(cib_verify,self.repr()) class CibObject(object): ''' The top level object of the CIB. Resources and constraints. ''' def __init__(self,xml_obj_type,obj_id = None): global cib_object_map self.backtrans = {} # generate a translation cli -> tag for key in cib_object_map: self.backtrans[cib_object_map[key][0]] = key if xml_obj_type in cib_object_map: self.obj_type = cib_object_map[xml_obj_type][0] self.parent_type = cib_object_map[xml_obj_type][2] self.xml_obj_type = xml_obj_type else: unsupported_err(xml_obj_type) return self.origin = "" # where did it originally come from? self.nocli = False # we don't support this one self.updated = False # was the object updated self.parent = None # object superior (group/clone/ms) self.children = [] # objects inferior if obj_id: self.new(obj_id) else: self.obj_id = None self.node = None def save_xml(self,node): self.obj_id = node.getAttribute("id") self.node = node return self.cli_use_validate() def new(self,obj_id): global cib_factory if id_in_use(obj_id): return tag = self.xml_obj_type self.node = cib_factory.createElement(tag) self.obj_id = obj_id self.node.setAttribute("id",self.obj_id) self.origin = "user" def attr_exists(self,attr): if not attr in self.node.attributes.keys(): no_attribute_err(attr,self.obj_id) return False return True def cli_use_validate(self): ''' Check validity of the object, as we know it. It may happen that we don't recognize a construct, but that the object is still valid for the CRM. In that case, the object is marked as "CLI read only", i.e. we will neither convert it to CLI nor try to edit it in that format. The validation procedure: we convert xml to cli and then back to xml. If the two xml representations match then we can understand the xml. ''' if not self.node: return True if not self.attr_exists("id"): return False cli_text = self.repr_cli() if not cli_text: return False xml2 = self.cli2node(cli_text) if not xml2: return False rc = xml_cmp(self.node,xml2) xml2.unlink() return rc def matchcli(self,cli_list): head = cli_list[0] return self.obj_type == head[0] \ and self.obj_id == find_value(head[1],"id") def match(self,obj_type,obj_id): return self.obj_type == obj_type and self.obj_id == obj_id def obj_string(self): return "%s:%s" % (self.obj_type,self.obj_id) def propagate_updated(self,v): p = self.parent while p: p.updated = v p = p.parent def update_from_cli(self,cli_list): 'Update ourselves from the cli intermediate.' global id_store global cib_factory oldnode = self.node id_store.remove_xml(oldnode) newnode = self.cli2node(cli_list) if not newnode: id_store.replace_xml(newnode,oldnode) return if xml_cmp(oldnode,newnode): newnode.unlink() return # the new and the old versions are equal oldnode.parentNode.replaceChild(newnode,oldnode) self.node = newnode head = cli_list[0] obj_type = head[0] obj_id = find_value(head[1],"id") v = find_value(head[1],"$children") if v: if not cib_factory.check_children(v,obj_type,obj_id): id_store.replace_xml(newnode,oldnode) newnode.unlink() return cib_factory.adjust_children(self,v) oldnode.unlink() self.updated = True self.propagate_updated(True) def update_from_node(self,node): 'Update ourselves from a doc node.' global id_store global cib_factory if not node: return oldxml = self.node newxml = node if xml_cmp(oldxml,newxml): return # the new and the old versions are equal if not id_store.replace_xml(oldxml,newxml): return oldxml.unlink() self.node = cib_factory.doc.importNode(newxml,1) cib_factory.topnode[self.parent_type].appendChild(self.node) cib_factory.update_links(self) self.updated = True self.propagate_updated(True) def find_child_in_node(self,child): for c in self.node.childNodes: if not is_element(c): continue if c.tagName == child.obj_type and \ c.getAttribute("id") == child.obj_id: return c return None class CibPrimitive(CibObject): ''' Primitives. ''' def repr_cli(self,node = None): if not node: node = self.node l = [] l.append(primitive_head(node)) for c in node.childNodes: if not is_element(c): continue if c.tagName == "instance_attributes": l.append("params %s" % cli_pairs(nvpairs2list(c))) elif c.tagName == "meta_attributes": l.append("meta %s" % cli_pairs(nvpairs2list(c))) elif c.tagName == "operations": l.append(cli_operations(c)) return ' \\\n\t'.join(l) def cli2node(self,cli,oldnode = None): ''' Convert a CLI description to DOM node. Try to preserve as many ids as possible in case there's an old XML version. ''' if type(cli) == type('') or type(cli) == type(u''): cli_list = parse_cli(cli) else: cli_list = cli if not cli_list: return None if not oldnode: oldnode = self.node head = copy.copy(cli_list[0]) head[0] = self.backtrans[head[0]] headnode = mkxmlsimple(head,oldnode,'rsc') id_hint = headnode.getAttribute("id") operations = None for e in cli_list[1:]: n = mkxmlnode(e,oldnode,id_hint) if e[0] == "operations": operations = n if e[0] != "op": headnode.appendChild(n) else: if not operations: operations = mkxmlnode(["operations",{}],oldnode,id_hint) headnode.appendChild(operations) operations.appendChild(n) return headnode class CibContainer(CibObject): ''' Groups and clones and ms. ''' def repr_cli(self,node = None): if not node: node = self.node l = [] l.append(cont_head(node)) for c in node.childNodes: if not is_element(c): continue if c.tagName == "instance_attributes": l.append("params %s" % cli_pairs(nvpairs2list(c))) elif c.tagName == "meta_attributes": l.append("meta %s" % cli_pairs(nvpairs2list(c))) return ' \\\n\t'.join(l) def cli2node(self,cli,oldnode = None): if type(cli) == type('') or type(cli) == type(u''): cli_list = parse_cli(cli) else: cli_list = cli if not cli_list: return None if not oldnode: oldnode = self.node head = copy.copy(cli_list[0]) head[0] = self.backtrans[head[0]] headnode = mkxmlsimple(head,oldnode,'grp') id_hint = headnode.getAttribute("id") for e in cli_list[1:]: n = mkxmlnode(e,oldnode,id_hint) headnode.appendChild(n) v = find_value(head[1],"$children") if v: for child_id in v: obj = cib_factory.find_object(child_id) if obj: n = obj.node.cloneNode(1) headnode.appendChild(n) else: no_object_err(child_id) return headnode class CibLocation(CibObject): ''' Location constraint. ''' def repr_cli(self,node = None): if not node: node = self.node l = [] l.append(location_head(node)) for c in node.childNodes: if not is_element(c): continue if c.tagName == "rule": l.append("rule %s" % cli_rule(c)) return ' \\\n\t'.join(l) def cli2node(self,cli,oldnode = None): if type(cli) == type('') or type(cli) == type(u''): cli_list = parse_cli(cli) else: cli_list = cli if not cli_list: return None if not oldnode: oldnode = self.node head = copy.copy(cli_list[0]) head[0] = self.backtrans[head[0]] headnode = mkxmlsimple(head,oldnode,'location') id_hint = headnode.getAttribute("id") for e in cli_list[1:]: if e[0] == "expression": n = mkxmlnode(e,oldrule,id_hint) else: n = mkxmlnode(e,oldnode,id_hint) if e[0] == "rule": rule = n headnode.appendChild(n) oldrule = lookup_node(rule,oldnode) else: rule.appendChild(n) return headnode class CibSimpleConstraint(CibObject): ''' Colocation and order constraints. ''' def repr_cli(self,node = None): if not node: node = self.node return simple_constraint_head(node) def cli2node(self,cli,oldnode = None): if type(cli) == type('') or type(cli) == type(u''): cli_list = parse_cli(cli) else: cli_list = cli if not cli_list: return None if not oldnode: oldnode = self.node head = copy.copy(cli_list[0]) head[0] = self.backtrans[head[0]] headnode = mkxmlsimple(head,oldnode,'') return headnode default_cluster_property_set = "cib-bootstrap-options" class CibProperty(CibObject): ''' Cluster properties. ''' def repr_cli(self,node = None): if not node: node = self.node l = [] l.append('property $id="%s"' % node.getAttribute("id")) properties = nvpairs2list(node) for n,v in properties: l.append('%s="%s"' % (n,v)) return ' \\\n\t'.join(l) def cli2node(self,cli,oldnode = None): if type(cli) == type('') or type(cli) == type(u''): cli_list = parse_cli(cli) else: cli_list = cli if not cli_list: return None if not oldnode: oldnode = cib_factory.topnode["crm_config"] head = copy.copy(cli_list[0]) head[0] = self.backtrans[head[0]] id = find_value(head[1],"$id") if not id: id = default_cluster_property_set headnode = mkxmlnode(head,oldnode,id) return headnode def matchcli(self,cli_list): head = cli_list[0] return self.obj_type == head[0] \ and self.obj_id == find_value(head[1],"$id") # xml -> cli translations (and classes) cib_object_map = { "primitive": ( "primitive", CibPrimitive, "resources" ), "group": ( "group", CibContainer, "resources" ), "clone": ( "clone", CibContainer, "resources" ), "master": ( "ms", CibContainer, "resources" ), "rsc_location": ( "location", CibLocation, "constraints" ), "rsc_colocation": ( "colocation", CibSimpleConstraint, "constraints" ), "rsc_order": ( "order", CibSimpleConstraint, "constraints" ), "cluster_property_set": ( "property", CibProperty, "crm_config" ), } class CibFactory(object): ''' Juggle with CIB objects. See check_structure below for details on the internal cib representation. ''' def __init__(self, cibfile=None): self.doc = None # the cib self.topnode = {} self.topnode["resources"] = None # the resources node self.topnode["constraints"] = None # the constraints node self.topnode["crm_config"] = None # the crm_config node self.cib_attributes = ('epoch', 'admin_epoch', 'num_updates', 'validate-with', 'crm_feature_set') self.cib_attrs = {} # cib version dictionary self.cib_objects = [] # a list of cib objects self.remove_queue = [] # a list of cib objects to be removed self.cibfile = cibfile # for testing self.overwrite = False # update cib unconditionally def check_topnode(self,obj): if not obj.node.parentNode.isSameNode(self.topnode[obj.parent_type]): common_err("object %s is not linked to %s"%(obj.obj_id,obj.parent_type)) def check_parent(self,obj,parent): if not obj in parent.children: common_err("object %s does not reference its child %s"%(parent.obj_id,obj.obj_id)) if not parent.node.isSameNode(obj.node.parentNode): common_err("object %s node is not a child of its parent %s, but %s:%s"%(obj.obj_id,parent.obj_id,obj.node.tagName,obj.node.getAttribute("id"))) def check_structure(self): #print "Checking structure..." for obj in self.cib_objects: #print "Checking %s... (%s)" % (obj.obj_id,obj.nocli) if obj.parent: self.check_parent(obj,obj.parent) else: self.check_topnode(obj) for child in obj.children: self.check_parent(child,child.parent) def createElement(self,tag): if self.doc: return self.doc.createElement(tag) def check_cib_release(self,cib): #req = cib.getAttribute("cib_feature_revision") req = cib.getAttribute("crm_feature_set") if not req or float(req) < 3.0: cib_ver_unsupported_err(req) return False return True def parse_cib(self): if self.cibfile: file = open(self.cibfile, "r") cib = ''.join(file) file.close() else: # use cibadmin cib = '\n'.join(os_system(cib_dump)) if not cib: common_err("invoking cibadmin failed") return False try: self.doc = xml.dom.minidom.parseString(cib) except xml.parsers.expat.ExpatError,msg: cib_parse_err(msg) return False cib = self.doc.childNodes[0] if not is_element(cib) or cib.tagName != "cib": common_err("CIB contains no 'cib' element!") self.reset() return False if not self.check_cib_release(cib): self.reset() return False for attr in self.cib_attributes: self.cib_attrs[attr] = cib.getAttribute(attr) for n in ("resources","constraints","crm_config"): self.topnode[n] = find_cib_node(cib,n) if not self.topnode[n]: common_err("CIB contains no '%s' element!"%n) self.reset() return False return True def chk_attribute(self,c,a): return self.cib_attrs[a] and \ c.getAttribute(a) == self.cib_attrs[a] def commit(self,force = False): 'Commit the configuration to the CIB.' # 0. ensure that cib didn't change in the meantime if not force and not self.overwrite: cib = '\n'.join(os_system(cib_dump)) if not cib: common_err("cibadmin failed") return doc = xml.dom.minidom.parseString(cib) if not doc: common_err("could not read current cib!") return cib = doc.childNodes[0] if not is_element(cib) or cib.tagName != "cib": common_err("current cib has no 'cib' element!") doc.unlink() return if not self.chk_attribute(cib,'epoch') or \ not self.chk_attribute(cib,'admin_epoch'): common_warn("cib changed in the meantime: won't touch it!") common_info("use 'commit force' to force the changes.") doc.unlink() return doc.unlink() cnt = 0 # 1. remove objects cnt += self.delete_objects() # 2. create objects cnt += self.commit_objects(lambda o: o.origin == 'user','-C') # 3. replace objects cnt += self.commit_objects(lambda o: o.origin != 'user' and o.updated,'-R') # reload the cib! if force: self.reset() self.initialize() elif cnt: epoch = self.cib_attrs['epoch'] self.cib_attrs['epoch'] = "%d"%(int(epoch)+cnt) def set_cib_attributes(self,cib): for attr in self.cib_attributes: cib.setAttribute(attr,self.cib_attrs[attr]) def objlist2doc(self,obj_list,filter = None): ''' Return document containing objects in obj_list. Must remove all children from the object list, because printing xml of parents will include them. Optional filter to sieve objects. ''' doc,cib,crm_config,nodes,resources,constraints = new_cib() for obj in [obj for obj in obj_list if not obj.parent]: if filter and not filter(obj): continue i_node = doc.importNode(obj.node,1) if obj.parent_type == "resources": resources.appendChild(i_node) elif obj.parent_type == "constraints": constraints.appendChild(i_node) elif obj.parent_type == "crm_config": crm_config.appendChild(i_node) self.set_cib_attributes(cib) return doc def delete_objects(self): cnt = 0 if not self.remove_queue: return cnt obj_list = processing_sort_cli(self.remove_queue) for obj in reversed(obj_list): node = self.createElement(obj.xml_obj_type) node.setAttribute("id",obj.obj_id) #print node.toprettyxml() rc = pipe_string("%s -D"%cib_piped,node.toxml()) if not rc: self.remove_queue.remove(obj) cnt += 1 else: update_err(obj.obj_id,'-D',node) node.unlink() return cnt def commit_objects(self,filter,cibadm_opt): upd_list = [] for obj in self.cib_objects: if not filter or filter(obj): if not obj.parent and not obj in upd_list: # only top children! upd_list.append(obj) cnt = 0 if not upd_list: return cnt for obj in processing_sort_cli(upd_list): #print obj.node.toprettyxml() rc = pipe_string("%s %s -o %s" % \ (cib_piped,cibadm_opt,obj.parent_type),obj.node.toxml()) if not rc: cnt += 1 self.obj_updated(obj) else: update_err(obj.obj_id,cibadm_opt,obj.node) return cnt def obj_updated(self,obj): obj.updated = False obj.origin = 'cib' for child in obj.children: self.obj_updated(child) def update_links(self,obj): ''' Update the structure links for the object (obj.children, obj.parent). Update also the dom nodes, if necessary. ''' obj.children = [] if obj.obj_type not in ("group","clone","ms"): return for c in obj.node.childNodes: if is_element(c) and c.tagName in ("primitive","group"): child = self.find_object_for_node(c) if not child: missing_obj_err(c) continue child.parent = obj obj.children.append(child) if not c.isSameNode(child.node): child.node.parentNode.removeChild(child.node) child.node.unlink() child.node = c def populate(self): "Walk the cib and collect cib objects." global cib_object_map all_nodes = get_interesting_nodes(self.doc,[]) if not all_nodes: return for node in processing_sort(all_nodes): obj = cib_object_map[node.tagName][1](node.tagName) obj.origin = "cib" self.cib_objects.append(obj) if not obj.save_xml(node): obj.nocli = True for obj in self.cib_objects: self.update_links(obj) def initialize(self): if self.doc: return True if not self.parse_cib(): common_err("bad cib") return False sanitize_cib(self.doc) self.populate() self.check_structure() return True def reset(self): if not self.doc: return self.doc.unlink() self.doc = None self.topnode = {} self.cib_attrs = {} # cib version dictionary self.cib_objects = [] # a list of cib objects self.remove_queue = [] # a list of cib objects to be removed self.overwrite = False # update cib unconditionally id_store.clear() # update cib unconditionally def find_object(self,obj_id): "Find an object." for obj in self.cib_objects: if obj.obj_id == obj_id: return obj return None def find_object_for_node(self,node): "Find an object which matches a dom node." for obj in self.cib_objects: if node.getAttribute("id") == obj.obj_id: return obj return None def new_object(self,obj_type,obj_id): "Create a new object of type obj_type." global cib_object_map if id_in_use(obj_id): return None for xml_obj_type,v in cib_object_map.items(): if v[0] == obj_type: obj = v[1](xml_obj_type,obj_id) if obj.obj_id: return obj else: return None return None def filter(self,obj,*args): "Filter objects." if not args: return True if args[0] == "NOOBJ": return False if args[0] == "changed": return obj.updated or obj.origin == "user" return obj.obj_id in args def mkobj_list(self,mode,*args): obj_list = [] for obj in self.cib_objects: f = lambda: self.filter(obj,*args) if not f(): continue if mode == "cli" and obj.nocli: obj_cli_err(obj.obj_id) continue obj_list.append(obj) return obj_list def has_cib_changed(self): return self.mkobj_list("xml","changed") or self.remove_queue def check_children(self,children_ids,obj_type,obj_id): # check prerequisites: # a) all children must exist # b) no child may have other parent than me # (or should we steal children?) for child_id in children_ids: if not self.check_child(child_id,obj_type,obj_id): return False return True def check_child(self,child_id,obj_type,obj_id): 'Check if child exists and obj_id is (or may be) its parent.' child = self.find_object(child_id) if not child: common_err("%s does not exist"%child_id) return False if child.parent and child.parent.obj_id != obj_id: common_err("%s already in use at %s"%(child_id,child.parent.obj_id)) return False if obj_type == "group" and child.obj_type != "primitive": common_err("a group may contain only primitives; %s is %s"%(child_id,child.obj_type)) return False if child.obj_type != "primitive" and child.obj_type != "group": common_err("%s may contain a primitive or a group; %s is %s"%(obj_type,child_id,child.obj_type)) return False return True def create_object(self,*args): self.create_from_cli(parse_cli(args)) def set_property_cli(self,cli_list): head_pl = cli_list[0] obj_type = head_pl[0] pset_id = find_value(head_pl[1],"$id") if pset_id: head_pl[1].remove(["$id",pset_id]) else: pset_id = default_cluster_property_set obj = self.find_object(pset_id) if not obj: obj = self.new_object(obj_type,pset_id) self.topnode[obj.parent_type].appendChild(obj.node) obj.origin = "user" for n,v in head_pl[1]: set_property(obj.node,n,v) obj.updated = True def create_from_cli(self,cli): 'Create a new cib object from the cli representation.' if type(cli) == type('') or type(cli) == type(u''): cli_list = parse_cli(cli) else: cli_list = cli if not cli_list: return head = cli_list[0] obj_type = head[0] if obj_type == "property": self.set_property_cli(cli_list) return obj_id = find_value(head[1],"id") c_ids = find_value(head[1],"$children") if c_ids: if not self.check_children(c_ids,obj_type,obj_id): return obj = self.new_object(obj_type,obj_id) if not obj: return obj.node = obj.cli2node(cli_list) self.topnode[obj.parent_type].appendChild(obj.node) if c_ids: self.adjust_children(obj,c_ids) obj.origin = "user" for child in obj.children: if child.origin == "cib": self.add_to_remove_queue(child) self.cib_objects.append(obj) def adjust_children(self,obj,children_ids): ''' All stuff children related: manage the nodes of children, update the list of children for the parent, update parents in the children. ''' new_children = [] for child_id in children_ids: new_children.append(self.find_object(child_id)) self._relink_orphans(obj,new_children) obj.children = new_children self._update_children(obj) def _relink_child(self,obj): 'Relink a child to the top node.' obj.node.parentNode.removeChild(obj.node) self.topnode[obj.parent_type].appendChild(obj.node) obj.parent = None def _update_children(self,obj): '''For composite objects: update all children nodes. ''' # unlink all and find them in the new node for child in obj.children: oldnode = child.node child.node = obj.find_child_in_node(child) if child.children: # and children of children self._update_children(child) oldnode.parentNode.removeChild(oldnode) oldnode.unlink() child.parent = obj def _relink_orphans(self,obj,new_children): "New orphans move to the top level for the object type." for child in obj.children: if child not in new_children: self._relink_child(child) def create_from_node(self,node): 'Create a new cib object from a document node.' if not node: return obj = self.new_object(node.tagName,node.getAttribute("id")) if not obj: return node = self.doc.importNode(node,1) self.topnode[obj.parent_type].appendChild(node) if not obj.save_xml(node): obj.nocli = True self.update_links(obj) obj.origin = "user" self.cib_objects.append(obj) def cib_objects_string(self): l = [] for obj in self.cib_objects: l.append(obj.obj_string()) return ' '.join(l) def _remove_obj(self,obj): "Remove a cib object and its children." # remove children first # can't remove them here from obj.children! global id_store for child in obj.children: self._remove_obj(child) id_store.remove_xml(obj.node) obj.node.parentNode.removeChild(obj.node) obj.node.unlink() self.cib_objects.remove(obj) def related_constraints(self,obj): if not is_resource(obj.node): return [] c_list = [] for obj2 in self.cib_objects: if not is_constraint(obj2.node): continue if rsc_constraint(obj.obj_id,obj2.node): c_list.append(obj2) return c_list def add_to_remove_queue(self,obj): self.remove_queue.append(obj) self.remove_queue += self.related_constraints(obj) #for obj in self.remove_queue: print obj.obj_string() def delete(self,*args): 'Delete a cib object.' l = [] for obj_id in args: obj = self.find_object(obj_id) if not obj: common_err("object %s not found"%obj_id) continue l.append(obj) for obj in l: # remove the parent for which this object is the # only child: empty containers are of no use and not # allowed while obj.parent: if len(obj.parent.children) > 1: break else: obj = obj.parent # recurse in another function; otherwise the previous # code would make an infinite loop self._remove_obj(obj) #print "remove",obj.obj_string() obj.children = [] # children are gone if obj.origin == "cib": self.add_to_remove_queue(obj) def erase(self): "Remove all cib objects." # find only topmost objects for obj in [obj for obj in self.cib_objects if not obj.parent]: self.delete(obj.obj_id) if self.cib_objects: common_err("strange, but these objects remained:") for obj in self.cib_objects: print obj.obj_string() self.cib_objects = [] class TopLevel(object): ''' The top level. ''' help_table = { ".": ("","""This is the CRM command line interface program."""), "cib": ("manage shadow CIBs", """ 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. """), "resource": ("resources management", """ Everything related to resources management is available at this level. Most commands are implemented using the crm_resource(8) program. """), "node": ("nodes management", """ A few node related tasks such as node standby are implemented here. """), "options": ("user preferences", """ Several user preferences are available. Note that it is possible to save the preferences to a startup file. """), "configure": ("CRM cluster configuration", """ The configuration level. Note that you can change the working CIB at the cib level. It is advisable to configure shadow CIBs and then commit them to the cluster. """), "quit": ("exit the program", ""), "help": ("show help", ""), "end": ("go back one level", ""), } def __init__(self): self.cmd_table = { 'cib': CibShadow, 'resource': RscMgmt, 'configure': CibConfig, 'node': NodeMgmt, 'options': CliOptions, 'status': (self.status,(0,0),0), 'help': (self.help,(0,1),0), 'end': (cmd_end,(0,0),0), 'exit': (cmd_exit,(0,0),0), 'quit': (cmd_exit,(0,0),0), } def help(self,cmd,topic = ''): cmd_help(self.help_table,topic) def status(self,cmd): ext_cmd("crm_mon -1") def attr_cmds(delimiter = False): if delimiter: return ' ' return ["delete","set","show","s-1","s-2"] def rsc_list(delimiter = False): if delimiter: return ' ' cmd = "%s -o %s" % (cib_dump,"resources") s = '\n'.join(os_system(cmd)) try: doc = xml.dom.minidom.parseString(s) except xml.parsers.expat.ExpatError,msg: cib_parse_err(msg) return [] nodes = get_interesting_nodes(doc,[]) return [x.getAttribute("id") for x in nodes if is_resource(x)] def skills_list(delimiter = False): if delimiter: return ' ' return CliOptions.skill_levels.keys() def property_list(delimiter = False): if delimiter: return '=' if wcache.is_cached("property_list"): return wcache.cached("property_list") f = os.popen("@hb_libdir@/pengine metadata") doc = xml.dom.minidom.parse(f) f.close() l = [] for node in doc.getElementsByTagName("parameter"): p = node.getAttribute("name") if p: l.append(p) doc.unlink() return wcache.cache("property_list",l) def mk_completion_tab(cmd_class,ctab = {}): obj = cmd_class() cmd_table = obj.cmd_table for key,value in cmd_table.items(): if type(value) == type(object): ctab[key] = {} mk_completion_tab(value,ctab[key]) else: ctab[key] = None try: ctab[key] = value[3] except: pass def lookup_dynamic(fun_list,words): if not fun_list: return [] f = fun_list[0] w = words[0] wordlist = f() delimiter = f(1) if len(words) == 1: return [x+delimiter for x in wordlist if x.startswith(w)] elif w in wordlist: return lookup_dynamic(fun_list[1:],words[1:]) return [] def lookup_words(ctab,words): if not ctab: return [] if type(ctab) == type(()): return lookup_dynamic(ctab,words) if len(words) == 1: return [x+' ' for x in ctab if x.startswith(words[0])] elif words[0] in ctab.keys(): return lookup_words(ctab[words[0]],words[1:]) return [] def completer(txt,state): global levels words = readline.get_line_buffer().split() if not words or readline.get_line_buffer()[-1] == ' ': words.append('') matched = lookup_words(levels.completion_tab,words) matched.append(None) return matched[state] wcache = WCache() err_buf = ErrorBuffer() user_prefs = UserPrefs() id_store = IdMgmt() cib_factory = CibFactory() 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 parse_line(levels,shlex.split(inp)) 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 stripped = text.strip() if stripped.endswith('\\'): stripped = stripped.rstrip('\\') line.append(stripped) prompt = '> ' else: line.append(stripped) break return ''.join(line) class Levels(object): ''' Keep track of levels and prompts. ''' def __init__(self,start_level): self._marker = 0 self.level_stack = [] self.comp_stack = [] self.current_level = start_level() self.parse_root = self.current_level.cmd_table self.prompts = [] self.completion_tab = {} mk_completion_tab(start_level,self.completion_tab) def getprompt(self): return ' '.join(self.prompts) def mark(self): self._marker = len(self.level_stack) def release(self): while len(self.level_stack) > self._marker: self.droplevel() def new_level(self,level_obj,token): self.level_stack.append(self.current_level) self.comp_stack.append(self.completion_tab) self.prompts.append(token) self.current_level = level_obj() self.parse_root = self.current_level.cmd_table self.completion_tab = self.completion_tab[token] def droplevel(self): if self.level_stack: try: self.current_level.end_game() except: pass self.current_level = self.level_stack.pop() self.completion_tab = self.comp_stack.pop() self.parse_root = self.current_level.cmd_table self.prompts.pop() def check_args(args,argsdim): if not argsdim: return True if len(argsdim) == 1: min = argsdim[0] return len(args) >= min else: min,max = argsdim return len(args) >= min and len(args) <= max # # 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) # def show_usage(cmd): p = None try: p = cmd.__doc__ except: pass if p: print p else: syntax_err(cmd.__name__) def parse_line(lvl,s): if not s: return if s[0].startswith('#'): return lvl.mark() pt = lvl.parse_root cmd = None for i in range(len(s)): token = s[i] if token in pt: if type(pt[token]) == type(object): 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 if cmd: # found a terminal symbol if not user_prefs.check_skill_level(cmd[2]): skill_err(s[i]) return args = s[i+1:] if not check_args(args,cmd[1]): show_usage(cmd[0]) return args = s[i:] d = lambda: cmd[0](*args) rv = d() # execute the command lvl.release() # three modes: interactive (no args supplied), batch (input from # a file), half-interactive (args supplied, but not batch) interactive = True batch = False inp_file = '' prompt = '' cib_in_use = os.getenv(CibShadow().envvar) def cib_prompt(): return cib_in_use or "live" def usage(): print """ usage: crm crm args crm [-f file] 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. Examples: # crm -f stopapp2.cli # crm -f - < stopapp2.cli # crm resource stop global_www # crm status """ sys.exit() def prereqs(): proglist = "cibadmin crm_resource crm_attribute crm_mon crm_standby crm_failcount" for prog in proglist.split(): if not is_program(prog): no_prog_err(prog) sys.exit(1) prereqs() hist_file = os.environ.get('HOME')+"/.crm_history" rc_file = os.environ.get('HOME')+"/.crm.rc" readline.set_history_length(100) readline.parse_and_bind("tab: complete") readline.set_completer(completer) readline.set_completer_delims(\ readline.get_completer_delims().replace('-','')) try: readline.read_history_file(hist_file) except: pass build_completions = True levels = Levels(TopLevel) build_completions = False load_rc(rc_file) argc = len(sys.argv) if argc > 1: interactive = False if sys.argv[1] == "-f": batch = True if argc != 3: usage() inp_file = sys.argv[2] elif sys.argv[1] == "-h": usage() else: args = sys.argv[1:] parse_line(levels,args) sys.exit() if inp_file == "-": pass elif inp_file: try: f = open(inp_file) except IOError, msg: common_err(msg) usage() sys.stdin = f while True: if interactive: prompt = "crm(%s)%s# " % (cib_prompt(),levels.getprompt()) inp = multi_input(prompt) if inp == None: cmd_exit("eof") parse_line(levels,shlex.split(inp))