diff --git a/acl_fix_d44ff2711662.patch b/acl_fix_d44ff2711662.patch new file mode 100644 index 0000000..30781d2 --- /dev/null +++ b/acl_fix_d44ff2711662.patch @@ -0,0 +1,163 @@ +# HG changeset patch +# User Andrew Beekhof +# Date 1314251944 -36000 +# Node ID d44ff2711662517d91b542b122218cffa2af3eb1 +# Parent 4cc8fdf2827a31d41b48b8c97d784c75c9418eda +Low: cib: Remove the remaining uses of the xml_child_iter() macro + +diff --git a/lib/cib/cib_acl.c b/lib/cib/cib_acl.c +--- a/lib/cib/cib_acl.c ++++ b/lib/cib/cib_acl.c +@@ -159,8 +159,7 @@ acl_check_diff(xmlNode *request, xmlNode + + orig_diff = diff_xml_object_orig(current_cib, result_cib, FALSE, diff); + +- xml_child_iter( +- orig_diff, diff_child, ++ for (diff_child = __xml_first_child(orig_diff); diff_child; diff_child = __xml_next(diff_child)) { + const char *tag = crm_element_name(diff_child); + GListPtr parsed_acl = NULL; + +@@ -176,8 +175,7 @@ acl_check_diff(xmlNode *request, xmlNode + continue; + } + +- xml_child_iter( +- diff_child, diff_cib, ++ for (diff_cib = __xml_first_child(diff_child); diff_cib; diff_child = __xml_next(diff_cib)) { + GHashTable *xml_perms = NULL; + + gen_xml_perms(diff_cib, parsed_acl, &xml_perms); +@@ -188,9 +186,9 @@ acl_check_diff(xmlNode *request, xmlNode + crm_warn("User '%s' doesn't have enough permission to modify the CIB objects", user); + goto done; + } +- ); ++ } + free_acl(parsed_acl); +- ); ++ } + + done: + free_xml(orig_diff); +@@ -264,8 +262,7 @@ unpack_user_acl(xmlNode *xml_acls, const + return FALSE; + } + +- xml_child_iter( +- xml_acls, xml_acl, ++ for (xml_acl = __xml_first_child(xml_acls); xml_acl; xml_acl = __xml_next(xml_acl)) { + const char *tag = crm_element_name(xml_acl); + const char *id = crm_element_value(xml_acl, XML_ATTR_ID); + +@@ -276,7 +273,7 @@ unpack_user_acl(xmlNode *xml_acls, const + return TRUE; + } + } +- ); ++ } + return FALSE; + } + +@@ -296,8 +293,7 @@ user_match(const char *user, const char + static gboolean + unpack_acl(xmlNode *xml_acls, xmlNode *xml_acl, GListPtr *acl) + { +- xml_child_iter( +- xml_acl, acl_child, ++ for (acl_child = __xml_first_child(xml_acl); acl_child; acl_child = __xml_next(acl_child)) { + const char *tag = crm_element_name(acl_child); + + if (crm_str_eq(XML_ACL_TAG_ROLE_REF, tag, TRUE)) { +@@ -316,8 +312,8 @@ unpack_acl(xmlNode *xml_acls, xmlNode *x + static gboolean + unpack_role_acl(xmlNode *xml_acls, const char *role, GListPtr *acl) + { +- xml_child_iter_filter( +- xml_acls, xml_acl, XML_ACL_TAG_ROLE, ++ for (xml_acl = __xml_first_child(xml_acls); xml_acl; xml_acl = __xml_next(xml_acl)) { ++ if(crm_str_eq(XML_ACL_TAG_ROLE, (const char *)child->name, TRUE)) { + const char *role_id = crm_element_value(xml_acl, XML_ATTR_ID); + + if (role_id && crm_str_eq(role, role_id, TRUE)) { +@@ -325,7 +321,8 @@ unpack_role_acl(xmlNode *xml_acls, const + unpack_acl(xml_acls, xml_acl, acl); + return TRUE; + } +- ); ++ } ++ } + return FALSE; + } + +@@ -495,12 +492,11 @@ search_xml_children(GListPtr *children, + } + + if(search_matches || match_found == 0) { +- xml_child_iter( +- root, child, ++ for (child = __xml_first_child(root); child; child = __xml_next(child)) { + match_found += search_xml_children( + children, child, tag, field, value, + search_matches); +- ); ++ } + } + + return match_found; +@@ -563,10 +559,9 @@ update_xml_perms(xmlNode *xml, acl_obj_t + crm_debug_3("Permission for element: element_mode=%s, tag=%s, id=%s", + perm->mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID)); + +- xml_child_iter( +- xml, child, ++ for (child = __xml_first_child(root); child; child = __xml_next(child)) { + update_xml_children_perms(child, perm->mode, xml_perms); +- ); ++ } + + } else { + if (perm->attribute_perms == NULL +@@ -610,10 +605,9 @@ update_xml_children_perms(xmlNode *xml, + crm_debug_4("Permission for child element: element_mode=%s, tag=%s, id=%s", + mode, crm_element_name(xml), crm_element_value(xml, XML_ATTR_ID)); + +- xml_child_iter( +- xml, child, ++ for (child = __xml_first_child(root); child; child = __xml_next(child)) { + update_xml_children_perms(child, mode, xml_perms); +- ); ++ } + + return TRUE; + } +@@ -647,12 +641,11 @@ acl_filter_xml(xmlNode *xml, GHashTable + xml_perm_t *perm = NULL; + int allow_counter = 0; + +- xml_child_iter( +- xml, child, ++ for (child = __xml_first_child(xml); child; child = __xml_next(child)) { + if (acl_filter_xml(child, xml_perms) == FALSE) { + children_counter++; + } +- ); ++ } + + g_hash_table_lookup_extended(xml_perms, xml, NULL, (gpointer)&perm); + +@@ -720,12 +713,11 @@ acl_check_diff_xml(xmlNode *xml, GHashTa + { + xml_perm_t *perm = NULL; + +- xml_child_iter( +- xml, child, ++ for (child = __xml_first_child(xml); child; child = __xml_next(child)) { + if (acl_check_diff_xml(child, xml_perms) == FALSE) { + return FALSE; + } +- ); ++ } + + g_hash_table_lookup_extended(xml_perms, xml, NULL, (gpointer)&perm); + diff --git a/crm_deleteunmanaged.patch b/crm_deleteunmanaged.patch new file mode 100644 index 0000000..62a7cee --- /dev/null +++ b/crm_deleteunmanaged.patch @@ -0,0 +1,100 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1313755383 -7200 +# Node ID e8ea8fb95f310997995576ee831693b0d3b2736a +# Parent 0abb257259ed722abaa32a237c3c284c08ec0737 +Medium: Shell: enable removal of unmanaged resources (bnc#696506) + +diff --git a/shell/modules/cibconfig.py b/shell/modules/cibconfig.py +--- a/shell/modules/cibconfig.py ++++ b/shell/modules/cibconfig.py +@@ -2303,7 +2303,7 @@ class CibFactory(Singleton): + no_object_err(obj_id) + rc = False + continue +- if is_rsc_running(obj_id): ++ if is_rsc_managed(obj_id) and is_rsc_running(obj_id): + common_err("resource %s is running, can't delete it" % obj_id) + rc = False + else: +diff --git a/shell/modules/xmlutil.py b/shell/modules/xmlutil.py +--- a/shell/modules/xmlutil.py ++++ b/shell/modules/xmlutil.py +@@ -178,6 +178,34 @@ def shadowfile(name): + def shadow2doc(name): + return file2doc(shadowfile(name)) + ++def is_xs_boolean_true(bool): ++ return bool.lower() in ("true","1") ++def is_rsc_managed(id): ++ if not is_live_cib(): ++ return False ++ rsc_node = rsc2node(id) ++ if not rsc_node: ++ return False ++ prop_node = get_properties_node(get_conf_elem(cibdump2doc("crm_config"), "crm_config")) ++ # maintenance-mode, if true, overrides all ++ attr = get_attr_value(prop_node, "maintenance-mode") ++ if attr and is_xs_boolean_true(attr): ++ return False ++ # then check the rsc is-managed meta attribute ++ rsc_meta_node = get_rsc_meta_node(rsc_node) ++ attr = get_attr_value(rsc_meta_node, "is-managed") ++ if attr: ++ return is_xs_boolean_true(attr) ++ # then rsc_defaults is-managed attribute ++ rsc_dflt_node = get_rscop_defaults_meta_node(get_conf_elem(cibdump2doc("rsc_defaults"), "rsc_defaults")) ++ attr = get_attr_value(rsc_dflt_node, "is-managed") ++ if attr: ++ return is_xs_boolean_true(attr) ++ # finally the is-managed-default property ++ attr = get_attr_value(prop_node, "is-managed-default") ++ if attr: ++ return is_xs_boolean_true(attr) ++ return True + def is_rsc_running(id): + if not is_live_cib(): + return False +@@ -691,12 +719,20 @@ def silly_constraint(c_node,rsc_id): + def get_rsc_children_ids(node): + return [x.getAttribute("id") \ + for x in node.childNodes if is_child_rsc(x)] +-def get_rscop_defaults_meta_node(node): ++def get_child_nvset_node(node, attr_set = "meta_attributes"): ++ if not node: ++ return None + for c in node.childNodes: +- if not is_element(c) or c.tagName != "meta_attributes": ++ if not is_element(c) or c.tagName != attr_set: + continue + return c + return None ++def get_rscop_defaults_meta_node(node): ++ return get_child_nvset_node(node) ++def get_rsc_meta_node(node): ++ return get_child_nvset_node(node) ++def get_properties_node(node): ++ return get_child_nvset_node(node, attr_set = "cluster_property_set") + + def new_cib(): + doc = xml.dom.minidom.Document() +@@ -727,12 +763,19 @@ def new_cib_element(node,tagname,id_pfx) + node.appendChild(newnode) + return newnode + def get_attr_in_set(node,attr): ++ if not node: ++ return None + for c in node.childNodes: + if not is_element(c): + continue + if c.tagName == "nvpair" and c.getAttribute("name") == attr: + return c + return None ++def get_attr_value(node,attr): ++ n = get_attr_in_set(node,attr) ++ if not n: ++ return None ++ return n.getAttribute("value") + def set_attr(node,attr,value): + ''' + Set an attribute in the attribute set. diff --git a/crm_history-fix-hb_report-limit.patch b/crm_history-fix-hb_report-limit.patch new file mode 100644 index 0000000..7d2b93a --- /dev/null +++ b/crm_history-fix-hb_report-limit.patch @@ -0,0 +1,13 @@ +Index: pacemaker/shell/modules/report.py +=================================================================== +--- pacemaker.orig/shell/modules/report.py ++++ pacemaker/shell/modules/report.py +@@ -643,6 +643,8 @@ class Report(Singleton): + def set_source(self,src): + 'Set our source.' + self.source = src ++ if self.source != "live": ++ self.reset_period() + def set_period(self,from_dt,to_dt): + ''' + Set from/to_dt. diff --git a/crm_history.patch b/crm_history.patch new file mode 100644 index 0000000..eb3684d --- /dev/null +++ b/crm_history.patch @@ -0,0 +1,2213 @@ +changeset: 10787:b694b75d2e33 +user: Dejan Muhamedagic +date: Mon Jul 18 12:35:57 2011 +0200 +summary: High: Shell: set of commands to examine logs, reports, etc + +diff -r 9609937061d7 -r b694b75d2e33 Makefile.am +--- a/Makefile.am Fri Jul 15 10:51:00 2011 +1000 ++++ b/Makefile.am Mon Jul 18 12:35:57 2011 +0200 +@@ -39,8 +39,10 @@ install-exec-local: + $(INSTALL) -d $(DESTDIR)/$(LCRSODIR) + $(INSTALL) -d -m 750 $(DESTDIR)/$(CRM_CONFIG_DIR) + $(INSTALL) -d -m 750 $(DESTDIR)/$(CRM_STATE_DIR) ++ $(INSTALL) -d -m 750 $(DESTDIR)/$(CRM_CACHE_DIR) + -chown $(CRM_DAEMON_USER):$(CRM_DAEMON_GROUP) $(DESTDIR)/$(CRM_CONFIG_DIR) + -chown $(CRM_DAEMON_USER):$(CRM_DAEMON_GROUP) $(DESTDIR)/$(CRM_STATE_DIR) ++ -chown $(CRM_DAEMON_USER):$(CRM_DAEMON_GROUP) $(DESTDIR)/$(CRM_CACHE_DIR) + if BUILD_CS_SUPPORT + rm -f $(DESTDIR)$(LCRSODIR)/pacemaker.lcrso $(DESTDIR)$(LCRSODIR)/service_crm.so + cp $(DESTDIR)$(libdir)/service_crm.so $(DESTDIR)$(LCRSODIR)/pacemaker.lcrso +diff -r 9609937061d7 -r b694b75d2e33 configure.ac +--- a/configure.ac Fri Jul 15 10:51:00 2011 +1000 ++++ b/configure.ac Mon Jul 18 12:35:57 2011 +0200 +@@ -460,6 +460,10 @@ CRM_STATE_DIR=${localstatedir}/run/crm + AC_DEFINE_UNQUOTED(CRM_STATE_DIR,"$CRM_STATE_DIR", Where to keep state files and sockets) + AC_SUBST(CRM_STATE_DIR) + ++CRM_CACHE_DIR=${localstatedir}/cache/crm ++AC_DEFINE_UNQUOTED(CRM_CACHE_DIR,"$CRM_CACHE_DIR", Where crm shell keeps the cache) ++AC_SUBST(CRM_CACHE_DIR) ++ + PE_STATE_DIR="${localstatedir}/lib/pengine" + AC_DEFINE_UNQUOTED(PE_STATE_DIR,"$PE_STATE_DIR", Where to keep PEngine outputs) + AC_SUBST(PE_STATE_DIR) +diff -r 9609937061d7 -r b694b75d2e33 doc/crm.8.txt +--- a/doc/crm.8.txt Fri Jul 15 10:51:00 2011 +1000 ++++ b/doc/crm.8.txt Mon Jul 18 12:35:57 2011 +0200 +@@ -13,7 +13,7 @@ crm - Pacemaker command line interface f + + SYNOPSIS + -------- +-*crm* [-D output_type] [-f file] [-hFRDw] [--version] [args] ++*crm* [-D output_type] [-f file] [-H hist_src] [-hFRDw] [--version] [args] + + + DESCRIPTION +@@ -67,6 +67,11 @@ OPTIONS + Make `crm` wait for the transition to finish. Applicable only + for commands such as "resource start." + ++*-H, --history*='DIR|FILE':: ++ The `history` commands can examine either live cluster ++ (default) or a report generated by `hb_report`. Use this ++ option to specify a directory or file containing the report. ++ + *-h, --help*:: + Print help page. + +@@ -2346,6 +2351,254 @@ Example: + simulate + ............... + ++[[cmdhelp_history,cluster history]] ++=== `history` ++ ++Examining Pacemaker's history is a particularly involved task. ++The number of subsystems to be considered, the complexity of the ++configuration, and the set of various information sources, most ++of which are not exactly human readable, keep analyzing resource ++or node problems accessible to only the most knowledgeable. Or, ++depending on the point of view, to the most persistent. The ++following set of commands has been devised in hope to make ++cluster history more accessible. ++ ++Of course, looking at _all_ history could be time consuming ++regardless of how good tools at hand are. Therefore, one should ++first say which period he or she wants to analyze. If not ++otherwise specified, the last hour is considered. Logs and other ++relevant information is collected using `hb_report`. Since this ++process takes some time and we always need fresh logs, ++information is refreshed in a much faster way using `pssh(1)`. If ++`python-pssh` is not found on the system, examining live cluster ++is still possible though not as comfortable. ++ ++Apart from examining live cluster, events may be retrieved from a ++report generated by `hb_report` (see also the `-H` option). In ++that case we assume that the period stretching the whole report ++needs to be investigated. Of course, it is still possible to ++further reduce the time range. ++ ++==== `info` ++ ++The `info` command shows most important information about the ++cluster. ++ ++Usage: ++............... ++ info ++............... ++Example: ++............... ++ info ++............... ++ ++[[cmdhelp_history_latest,show latest news from the cluster]] ++==== `latest` ++ ++The `latest` command shows a bit of recent history, more ++precisely whatever happened since the last cluster change (the ++latest transition). ++ ++Usage: ++............... ++ latest ++............... ++Example: ++............... ++ latest ++............... ++ ++[[cmdhelp_history_limit,limit timeframe to be examined]] ++==== `limit` ++ ++All history commands look at events within certain period. It ++defaults to the last hour for the live cluster source. There is ++no limit for the `hb_report` source. Use this command to set the ++timeframe. ++ ++The time period is parsed by the dateutil python module. It ++covers wide range of date formats. For instance: ++ ++- 3:00 (today at 3am) ++- 15:00 (today at 3pm) ++- 2010/9/1 2pm (September 1st 2010 at 2pm) ++ ++We won't bother to give definition of the time specification in ++usage below. Either use common sense or read the ++http://labix.org/python-dateutil[dateutil] documentation. ++ ++If dateutil is not available, then the time is parsed using ++strptime and only the kind as printed by `date(1)` is allowed: ++ ++- Tue Sep 15 20:46:27 CEST 2010 ++ ++Usage: ++............... ++ limit [] ++............... ++Examples: ++............... ++ limit 10:15 ++ limit 15h22m 16h ++ limit "Sun 5 20:46" "Sun 5 22:00" ++............... ++ ++[[cmdhelp_history_source,set source to be examined]] ++==== `source` ++ ++Events to be examined can come from the current cluster or from a ++`hb_report` report. This command sets the source. `source live` ++sets source to the running cluster and system logs. If no source ++is specified, the current source information is printed. ++ ++In case a report source is specified as a file reference, the file ++is going to be unpacked in place where it resides. This directory ++is not removed on exit. ++ ++Usage: ++............... ++ source [||live] ++............... ++Examples: ++............... ++ source live ++ source /tmp/customer_case_22.tar.bz2 ++ source /tmp/customer_case_22 ++ source ++............... ++ ++[[cmdhelp_history_refresh,refresh live report]] ++==== `refresh` ++ ++This command makes sense only for the `live` source and makes ++`crm` collect the latest logs and other relevant information from ++the logs. If you want to make a completely new report, specify ++`force`. ++ ++Usage: ++............... ++ refresh [force] ++............... ++ ++[[cmdhelp_history_detail,set the level of detail shown]] ++==== `detail` ++ ++How much detail to show from the logs. ++ ++Usage: ++............... ++ detail ++ ++ detail_level :: small integer (defaults to 0) ++............... ++Example: ++............... ++ detail 1 ++............... ++ ++[[cmdhelp_history_setnodes,set the list of cluster nodes]] ++==== `setnodes` ++ ++In case the host this program runs on is not part of the cluster, ++it is necessary to set the list of nodes. ++ ++Usage: ++............... ++ setnodes node [ ...] ++............... ++Example: ++............... ++ setnodes node_a node_b ++............... ++ ++[[cmdhelp_history_resource,resource failed actions]] ++==== `resource` ++ ++Show status changes and any failures that happened on a resource. ++ ++Usage: ++............... ++ resource [ ...] ++............... ++Example: ++............... ++ resource mydb ++............... ++ ++[[cmdhelp_history_node,node events]] ++==== `node` ++ ++Show important events that happened on a node. Important events ++are node lost and join, standby and online, and fence. ++ ++Usage: ++............... ++ node [ ...] ++............... ++Example: ++............... ++ node node1 ++............... ++ ++[[cmdhelp_history_log,log content]] ++==== `log` ++ ++Show logs for a node or combined logs of all nodes. ++ ++Usage: ++............... ++ log [] ++............... ++Example: ++............... ++ log node-a ++............... ++ ++[[cmdhelp_history_peinputs,list or get PE input files]] ++==== `peinputs` ++ ++Every event in the cluster results in generating one or more ++Policy Engine (PE) files. These files describe future motions of ++resources. The files are listed along with the node where they ++were created (the DC at the time). The `get` subcommand will copy ++all PE input files to the current working directory (and use ssh ++if necessary). ++ ++The `show` subcommand will print actions planned by the PE and ++run graphviz (`dotty`) to display a graphical representation. Of ++course, for the latter an X11 session is required. This command ++invokes `ptest(8)` in background. ++ ++The `showdot` subcommand runs graphviz (`dotty`) to display a ++graphical representation of the `.dot` file which has been ++included in the report. Essentially, it shows the calculation ++produced by `pengine` which is installed on the node where the ++report was produced. ++ ++If the PE input file number is not provided, it defaults to the ++last one, i.e. the last transition. If the number is negative, ++then the corresponding transition relative to the last one is ++chosen. ++ ++Usage: ++............... ++ peinputs list [{|} ...] ++ peinputs get [{|} ...] ++ peinputs show [] [nograph] [v...] [scores] [actions] [utilization] ++ peinputs showdot [] ++ ++ range :: : ++............... ++Example: ++............... ++ peinputs get 440:444 446 ++ peinputs show ++ peinputs show 444 ++ peinputs show -1 ++ peinputs showdot 444 ++............... ++ + === `end` (`cd`, `up`) + + The `end` command ends the current level and the user moves to +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/Makefile.am +--- a/shell/modules/Makefile.am Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/Makefile.am Mon Jul 18 12:35:57 2011 +0200 +@@ -33,6 +33,8 @@ modules = __init__.py \ + msg.py \ + parse.py \ + ra.py \ ++ report.py \ ++ log_patterns.py \ + singletonmixin.py \ + template.py \ + term.py \ +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/cibconfig.py +--- a/shell/modules/cibconfig.py Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/cibconfig.py Mon Jul 18 12:35:57 2011 +0200 +@@ -20,6 +20,7 @@ import subprocess + import copy + import xml.dom.minidom + import re ++import time + + from singletonmixin import Singleton + from userprefs import Options, UserPrefs +@@ -404,7 +405,6 @@ class CibObjectSetRaw(CibObjectSet): + ''' + Edit or display one or more CIB objects (XML). + ''' +- actions_filter = "grep LogActions: | grep -vw Leave" + def __init__(self, *args): + CibObjectSet.__init__(self, *args) + self.obj_list = cib_factory.mkobj_list("xml",*args) +@@ -470,19 +470,6 @@ class CibObjectSetRaw(CibObjectSet): + def ptest(self, nograph, scores, utilization, actions, verbosity): + if not cib_factory.is_cib_sane(): + return False +- if verbosity: +- if actions: +- verbosity = 'v' * max(3,len(verbosity)) +- ptest = "ptest -X -%s" % verbosity.upper() +- if scores: +- ptest = "%s -s" % ptest +- if utilization: +- ptest = "%s -U" % ptest +- if user_prefs.dotty and not nograph: +- fd,dotfile = mkstemp() +- ptest = "%s -D %s" % (ptest,dotfile) +- else: +- dotfile = None + doc = cib_factory.objlist2doc(self.obj_list) + cib = doc.childNodes[0] + status = cib_status.get_status() +@@ -490,21 +477,9 @@ class CibObjectSetRaw(CibObjectSet): + common_err("no status section found") + return False + cib.appendChild(doc.importNode(status,1)) +- # ptest prints to stderr +- if actions: +- ptest = "%s 2>&1 | %s | %s" % \ +- (ptest, self.actions_filter, user_prefs.pager) +- else: +- ptest = "%s 2>&1 | %s" % (ptest, user_prefs.pager) +- pipe_string(ptest,doc.toprettyxml()) ++ graph_s = doc.toprettyxml() + doc.unlink() +- if dotfile: +- show_dot_graph(dotfile) +- vars.tmpfiles.append(dotfile) +- else: +- if not nograph: +- common_info("install graphviz to see a transition graph") +- return True ++ return run_ptest(graph_s, nograph, scores, utilization, actions, verbosity) + + # + # XML generate utilities +@@ -1426,6 +1401,7 @@ class CibFactory(Singleton): + def __init__(self): + self.init_vars() + self.regtest = options.regression_tests ++ self.last_commit_time = 0 + self.all_committed = True # has commit produced error + self._no_constraint_rm_msg = False # internal (just not to produce silly messages) + self.supported_cib_re = "^pacemaker-1[.][012]$" +@@ -1598,6 +1574,8 @@ class CibFactory(Singleton): + print "Remove queue:" + for obj in self.remove_queue: + obj.dump_state() ++ def last_commit_at(self): ++ return self.last_commit_time + def commit(self,force = False): + 'Commit the configuration to the CIB.' + if not self.doc: +@@ -1608,6 +1586,8 @@ class CibFactory(Singleton): + cnt = self.commit_doc(force) + if cnt: + # reload the cib! ++ if is_live_cib(): ++ self.last_commit_time = time.time() + self.reset() + self.initialize() + return self.all_committed +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/completion.py +--- a/shell/modules/completion.py Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/completion.py Mon Jul 18 12:35:57 2011 +0200 +@@ -22,6 +22,7 @@ import readline + + from cibconfig import CibFactory + from cibstatus import CibStatus ++from report import Report + from levels import Levels + from ra import * + from vars import Vars +@@ -156,6 +157,22 @@ def ra_classes_list(idx,delimiter = Fals + if delimiter: + return ':' + return ra_classes() ++def report_rsc_list(idx,delimiter = False): ++ if delimiter: ++ return ' ' ++ return crm_report.rsc_list() ++def report_node_list(idx,delimiter = False): ++ if delimiter: ++ return ' ' ++ return crm_report.node_list() ++def report_pe_cmd_list(idx,delimiter = False): ++ if delimiter: ++ return ' ' ++ return ["list","get","show","showdot"] ++def report_pe_list(idx,delimiter = False): ++ if delimiter: ++ return ' ' ++ return crm_report.peinputs_list() + + # + # completion for primitives including help for parameters +@@ -463,6 +480,12 @@ completer_lists = { + "_regtest" : None, + "_objects" : None, + }, ++ "history" : { ++ "resource" : (report_rsc_list,loop), ++ "node" : (report_node_list,loop), ++ "log" : (report_node_list,loop), ++ "peinputs" : (report_pe_cmd_list,report_pe_list,loop), ++ }, + } + def get_completer_list(level,cmd): + 'Return a list of completer functions.' +@@ -474,5 +497,6 @@ user_prefs = UserPrefs.getInstance() + vars = Vars.getInstance() + cib_status = CibStatus.getInstance() + cib_factory = CibFactory.getInstance() ++crm_report = Report.getInstance() + + # vim:ts=4:sw=4:et: +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/crm_pssh.py +--- /dev/null Thu Jan 01 00:00:00 1970 +0000 ++++ b/shell/modules/crm_pssh.py Mon Jul 18 12:35:57 2011 +0200 +@@ -0,0 +1,160 @@ ++# Modified pssh ++# Copyright (c) 2011, Dejan Muhamedagic ++# Copyright (c) 2009, Andrew McNabb ++# Copyright (c) 2003-2008, Brent N. Chun ++ ++"""Parallel ssh to the set of nodes in hosts.txt. ++ ++For each node, this essentially does an "ssh host -l user prog [arg0] [arg1] ++...". The -o option can be used to store stdout from each remote node in a ++directory. Each output file in that directory will be named by the ++corresponding remote node's hostname or IP address. ++""" ++ ++import fcntl ++import os ++import sys ++import glob ++import re ++ ++parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0]))) ++if os.path.exists(os.path.join(parent, 'psshlib')): ++ sys.path.insert(0, parent) ++ ++from psshlib import psshutil ++from psshlib.manager import Manager, FatalError ++from psshlib.task import Task ++from psshlib.cli import common_parser, common_defaults ++ ++from msg import * ++ ++_DEFAULT_TIMEOUT = 60 ++_EC_LOGROT = 120 ++ ++def option_parser(): ++ parser = common_parser() ++ parser.usage = "%prog [OPTIONS] command [...]" ++ parser.epilog = "Example: pssh -h hosts.txt -l irb2 -o /tmp/foo uptime" ++ ++ parser.add_option('-i', '--inline', dest='inline', action='store_true', ++ help='inline aggregated output for each server') ++ parser.add_option('-I', '--send-input', dest='send_input', ++ action='store_true', ++ help='read from standard input and send as input to ssh') ++ parser.add_option('-P', '--print', dest='print_out', action='store_true', ++ help='print output as we get it') ++ ++ return parser ++ ++def parse_args(myargs): ++ parser = option_parser() ++ defaults = common_defaults(timeout=_DEFAULT_TIMEOUT) ++ parser.set_defaults(**defaults) ++ opts, args = parser.parse_args(myargs) ++ return opts, args ++ ++def show_errors(errdir, hosts): ++ for host in hosts: ++ fl = glob.glob("%s/*%s*" % (errdir,host)) ++ if not fl: ++ continue ++ for fname in fl: ++ try: ++ if os.stat(fname).st_size == 0: ++ continue ++ f = open(fname) ++ except: ++ continue ++ print "%s stderr:" % host ++ print ''.join(f) ++ f.close() ++ ++def do_pssh(l, opts): ++ if opts.outdir and not os.path.exists(opts.outdir): ++ os.makedirs(opts.outdir) ++ if opts.errdir and not os.path.exists(opts.errdir): ++ os.makedirs(opts.errdir) ++ if opts.send_input: ++ stdin = sys.stdin.read() ++ else: ++ stdin = None ++ manager = Manager(opts) ++ user = "" ++ port = "" ++ hosts = [] ++ for host, cmdline in l: ++ cmd = ['ssh', host, '-o', 'PasswordAuthentication=no', ++ '-o', 'SendEnv=PSSH_NODENUM'] ++ if opts.options: ++ for opt in opts.options: ++ cmd += ['-o', opt] ++ if user: ++ cmd += ['-l', user] ++ if port: ++ cmd += ['-p', port] ++ if opts.extra: ++ cmd.extend(opts.extra) ++ if cmdline: ++ cmd.append(cmdline) ++ hosts.append(host) ++ t = Task(host, port, user, cmd, opts, stdin) ++ manager.add_task(t) ++ try: ++ statuses = manager.run() ++ except FatalError: ++ common_err("pssh to nodes failed") ++ show_errors(opts.errdir, hosts) ++ return False ++ ++ if min(statuses) < 0: ++ # At least one process was killed. ++ common_err("ssh process was killed") ++ show_errors(opts.errdir, hosts) ++ return False ++ # The any builtin was introduced in Python 2.5 (so we can't use it yet): ++ #elif any(x==255 for x in statuses): ++ for status in statuses: ++ if status == 255: ++ common_warn("ssh processes failed") ++ show_errors(opts.errdir, hosts) ++ return False ++ for status in statuses: ++ if status not in (0, _EC_LOGROT): ++ common_warn("some ssh processes failed") ++ show_errors(opts.errdir, hosts) ++ return False ++ return True ++ ++def next_loglines(a, outdir, errdir): ++ ''' ++ pssh to nodes to collect new logs. ++ ''' ++ l = [] ++ for node,rptlog,logfile,nextpos in a: ++ common_debug("updating %s from %s (pos %d)" % (logfile, node, nextpos)) ++ cmdline = "perl -e 'exit(%d) if (stat(\"%s\"))[7]<%d' && tail -c +%d %s" % (_EC_LOGROT, logfile, nextpos-1, nextpos, logfile) ++ myopts = ["-q", "-o", outdir, "-e", errdir] ++ opts, args = parse_args(myopts) ++ l.append([node, cmdline]) ++ return do_pssh(l, opts) ++ ++def next_peinputs(node_pe_l, outdir, errdir): ++ ''' ++ pssh to nodes to collect new logs. ++ ''' ++ l = [] ++ for node,pe_l in node_pe_l: ++ r = re.search("(.*)/pengine/", pe_l[0]) ++ if not r: ++ common_err("strange, %s doesn't contain string pengine" % pe_l[0]) ++ continue ++ dir = "/%s" % r.group(1) ++ red_pe_l = [x.replace("%s/" % r.group(1),"") for x in pe_l] ++ common_debug("getting new PE inputs %s from %s" % (red_pe_l, node)) ++ cmdline = "tar -C %s -cf - %s" % (dir, ' '.join(red_pe_l)) ++ myopts = ["-q", "-o", outdir, "-e", errdir] ++ opts, args = parse_args(myopts) ++ l.append([node, cmdline]) ++ return do_pssh(l, opts) ++ ++# vim:ts=4:sw=4:et: +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/log_patterns.py +--- /dev/null Thu Jan 01 00:00:00 1970 +0000 ++++ b/shell/modules/log_patterns.py Mon Jul 18 12:35:57 2011 +0200 +@@ -0,0 +1,69 @@ ++# Copyright (C) 2011 Dejan Muhamedagic ++# ++# log pattern specification ++# ++# patterns are grouped one of several classes: ++# - resources: pertaining to a resource ++# - node: pertaining to a node ++# - quorum: quorum changes ++# - events: other interesting events (core dumps, etc) ++# ++# paterns are grouped based on a detail level ++# detail level 0 is the lowest, i.e. should match the least ++# number of relevant messages ++ ++# NB: If you modify this file, you must follow python syntax! ++ ++log_patterns = { ++ "resource": ( ++ ( # detail 0 ++ "lrmd:.*rsc:%%.*(start|stop)", ++ "lrmd:.*RA output:.*%%.*stderr", ++ "lrmd:.*WARN:.*Managed.*%%.*exited", ++ ), ++ ( # detail 1 ++ "lrmd:.*rsc:%%.*probe", ++ "lrmd:.*info:.*Managed.*%%.*exited", ++ ), ++ ), ++ "node": ( ++ ( # detail 0 ++ "%%.*Corosync.Cluster.Engine", ++ "%%.*Executive.Service.RELEASE", ++ "%%.*crm_shutdown:.Requesting.shutdown", ++ "%%.*pcmk_shutdown:.Shutdown.complete", ++ "%%.*Configuration.validated..Starting.heartbeat", ++ "pengine.*Scheduling Node %%", ++ "te_fence_node.*Exec.*%%", ++ "stonith-ng.*log_oper.*reboot.*%%", ++ "stonithd.*to STONITH.*%%", ++ "stonithd.*fenced node %%", ++ "pcmk_peer_update.*(lost|memb): %%", ++ "crmd.*ccm_event.*(NEW|LOST) %%", ++ ), ++ ( # detail 1 ++ ), ++ ), ++ "quorum": ( ++ ( # detail 0 ++ "crmd.*crm_update_quorum:.Updating.quorum.status", ++ "crmd.*ais.disp.*quorum.(lost|ac?quir)", ++ ), ++ ( # detail 1 ++ ), ++ ), ++ "events": ( ++ ( # detail 0 ++ "CRIT:", ++ "ERROR:", ++ ), ++ ( # detail 1 ++ "WARN:", ++ ), ++ ), ++} ++ ++transition_patt = ( ++ "crmd: .* Processing graph.*derived from (.*bz2)", # transition start ++ "crmd: .* Transition.*Source=(.*bz2): (Stopped|Complete|Terminated)", # and stop ++) +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/main.py +--- a/shell/modules/main.py Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/main.py Mon Jul 18 12:35:57 2011 +0200 +@@ -172,7 +172,7 @@ def usage(rc): + f = sys.stdout + print >> f, """ + usage: +- crm [-D display_type] [-f file] [-hF] [args] ++ crm [-D display_type] [-f file] [-H hist_src] [-hFRDw] [--version] [args] + + Use crm without arguments for an interactive session. + Supply one or more arguments for a "single-shot" use. +@@ -226,8 +226,8 @@ def run(): + + try: + opts, args = getopt.getopt(sys.argv[1:], \ +- 'whdf:FRD:', ("wait","version","help","debug","file=",\ +- "force","regression-tests","display=")) ++ 'whdf:FRD:H:', ("wait","version","help","debug","file=",\ ++ "force","regression-tests","display=","history=")) + for o,p in opts: + if o in ("-h","--help"): + usage(0) +@@ -250,6 +250,8 @@ Written by Dejan Muhamedagic + options.interactive = False + err_buf.reset_lineno() + inp_file = p ++ elif o in ("-H","--history"): ++ options.history = p + elif o in ("-w","--wait"): + user_prefs.wait = "yes" + except getopt.GetoptError,msg: +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/msg.py +--- a/shell/modules/msg.py Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/msg.py Mon Jul 18 12:35:57 2011 +0200 +@@ -27,6 +27,7 @@ class ErrorBuffer(Singleton): + self.msg_list = [] + self.mode = "immediate" + self.lineno = -1 ++ self.written = {} + def buffer(self): + self.mode = "keep" + def release(self): +@@ -65,6 +66,10 @@ class ErrorBuffer(Singleton): + self.writemsg("ERROR: %s" % self.add_lineno(s)) + def warning(self,s): + self.writemsg("WARNING: %s" % self.add_lineno(s)) ++ def one_warning(self,s): ++ if not s in self.written: ++ self.written[s] = 1 ++ self.writemsg("WARNING: %s" % self.add_lineno(s)) + def info(self,s): + self.writemsg("INFO: %s" % self.add_lineno(s)) + def debug(self,s): +@@ -79,12 +84,16 @@ def common_warning(s): + err_buf.warning(s) + def common_warn(s): + err_buf.warning(s) ++def warn_once(s): ++ err_buf.one_warning(s) + def common_info(s): + err_buf.info(s) + def common_debug(s): + err_buf.debug(s) + def no_prog_err(name): + err_buf.error("%s not available, check your installation"%name) ++def no_file_err(name): ++ err_buf.error("%s does not exist"%name) + def missing_prog_warn(name): + err_buf.warning("could not find any %s on the system"%name) + def node_err(msg, node): +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/report.py +--- /dev/null Thu Jan 01 00:00:00 1970 +0000 ++++ b/shell/modules/report.py Mon Jul 18 12:35:57 2011 +0200 +@@ -0,0 +1,887 @@ ++# Copyright (C) 2011 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 os ++import sys ++import time ++import datetime ++import copy ++import re ++import glob ++ ++from singletonmixin import Singleton ++from userprefs import Options, UserPrefs ++from cibconfig import CibFactory ++from vars import Vars, getuser ++from term import TerminalController ++from xmlutil import * ++from utils import * ++from msg import * ++from log_patterns import log_patterns, transition_patt ++_NO_PSSH = False ++try: ++ from crm_pssh import next_loglines, next_peinputs ++except: ++ _NO_PSSH = True ++ ++# ++# hb_report interface ++# ++# read hb_report generated report, show interesting stuff, search ++# through logs, get PE input files, get log slices (perhaps even ++# coloured nicely!) ++# ++ ++def mk_re_list(patt_l,repl): ++ 'Build a list of regular expressions, replace "%%" with repl' ++ l = [] ++ for re_l in patt_l: ++ l += [ x.replace("%%",repl) for x in re_l ] ++ if not repl: ++ l = [ x.replace(".*.*",".*") for x in l ] ++ return l ++ ++YEAR = time.strftime("%Y") ++def syslog_ts(s): ++ try: ++ # strptime defaults year to 1900 (sigh) ++ tm = time.strptime(' '.join([YEAR] + s.split()[0:3]),"%Y %b %d %H:%M:%S") ++ return time.mktime(tm) ++ except: ++ common_warn("malformed line: %s" % s) ++ return None ++ ++def log_seek(f, ts, endpos = False): ++ ''' ++ f is an open log. Do binary search for the timestamp. ++ Return the position of the (more or less) first line with a ++ newer time. ++ ''' ++ first = 0 ++ f.seek(0,2) ++ last = f.tell() ++ if not ts: ++ return endpos and last or first ++ badline = 0 ++ maxbadline = 10 ++ common_debug("seek ts %s" % time.ctime(ts)) ++ while first <= last: ++ # we can skip some iterations if it's about few lines ++ if abs(first-last) < 120: ++ break ++ mid = (first+last)/2 ++ f.seek(mid) ++ log_ts = get_timestamp(f) ++ if not log_ts: ++ badline += 1 ++ if badline > maxbadline: ++ common_warn("giving up on log %s" % f.name) ++ return -1 ++ first += 120 # move forward a bit ++ continue ++ if log_ts > ts: ++ last = mid-1 ++ elif log_ts < ts: ++ first = mid+1 ++ else: ++ break ++ common_debug("sought to %s" % time.ctime(log_ts)) ++ return f.tell() ++ ++def get_timestamp(f): ++ ''' ++ Get the whole line from f. The current file position is ++ usually in the middle of the line. ++ Then get timestamp and return it. ++ ''' ++ step = 30 # no line should be longer than 30 ++ cnt = 1 ++ current_pos = f.tell() ++ s = f.readline() ++ if not s: # EOF? ++ f.seek(-step, 1) # backup a bit ++ current_pos = f.tell() ++ s = f.readline() ++ while s and current_pos < f.tell(): ++ if cnt*step >= f.tell(): # at 0? ++ f.seek(0) ++ break ++ f.seek(-cnt*step, 1) ++ s = f.readline() ++ cnt += 1 ++ pos = f.tell() # save the position ... ++ s = f.readline() # get the line ++ f.seek(pos) # ... and move the cursor back there ++ if not s: # definitely EOF (probably cannot happen) ++ return None ++ return syslog_ts(s) ++ ++def is_our_log(s, node_l): ++ try: return s.split()[3] in node_l ++ except: return False ++def log2node(log): ++ return os.path.basename(os.path.dirname(log)) ++def filter(sl, log_l): ++ ''' ++ Filter list of messages to get only those from the given log ++ files list. ++ ''' ++ node_l = [log2node(x) for x in log_l if x] ++ return [x for x in sl if is_our_log(x, node_l)] ++def convert_dt(dt): ++ try: return time.mktime(dt.timetuple()) ++ except: return None ++ ++class LogSyslog(object): ++ ''' ++ Slice log, search log. ++ self.fp is an array of dicts. ++ ''' ++ def __init__(self, central_log, log_l, from_dt, to_dt): ++ self.log_l = log_l ++ self.central_log = central_log ++ self.f = {} ++ self.startpos = {} ++ self.endpos = {} ++ self.cache = {} ++ self.open_logs() ++ self.set_log_timeframe(from_dt, to_dt) ++ def open_log(self, log): ++ try: ++ self.f[log] = open(log) ++ except IOError,msg: ++ common_err("open %s: %s"%(log,msg)) ++ def open_logs(self): ++ if self.central_log: ++ self.open_log(self.central_log) ++ else: ++ for log in self.log_l: ++ self.open_log(log) ++ def set_log_timeframe(self, from_dt, to_dt): ++ ''' ++ Convert datetime to timestamps (i.e. seconds), then ++ find out start/end file positions. Logs need to be ++ already open. ++ ''' ++ self.from_ts = convert_dt(from_dt) ++ self.to_ts = convert_dt(to_dt) ++ bad_logs = [] ++ for log in self.f: ++ f = self.f[log] ++ start = log_seek(f, self.from_ts) ++ end = log_seek(f, self.to_ts, endpos = True) ++ if start == -1 or end == -1: ++ bad_logs.append(log) ++ else: ++ self.startpos[f] = start ++ self.endpos[f] = end ++ for log in bad_logs: ++ del self.f[log] ++ self.log_l.remove(log) ++ def get_match_line(self, f, patt): ++ ''' ++ Get first line from f that matches re_s, but is not ++ behind endpos[f]. ++ ''' ++ while f.tell() < self.endpos[f]: ++ fpos = f.tell() ++ s = f.readline().rstrip() ++ if not patt or patt.search(s): ++ return s,fpos ++ return '',-1 ++ def single_log_list(self, f, patt): ++ l = [] ++ while True: ++ s = self.get_match_line(f, patt)[0] ++ if not s: ++ return l ++ l.append(s) ++ return l ++ def search_logs(self, log_l, re_s = ''): ++ ''' ++ Search logs for re_s and sort by time. ++ ''' ++ patt = None ++ if re_s: ++ patt = re.compile(re_s) ++ # if there's central log, there won't be merge ++ if self.central_log: ++ fl = [ self.f[f] for f in self.f ] ++ else: ++ fl = [ self.f[f] for f in self.f if self.f[f].name in log_l ] ++ for f in fl: ++ f.seek(self.startpos[f]) ++ # get head lines of all nodes ++ top_line = [ self.get_match_line(x, patt)[0] for x in fl ] ++ top_line_ts = [] ++ rm_idx_l = [] ++ # calculate time stamps for head lines ++ for i in range(len(top_line)): ++ if not top_line[i]: ++ rm_idx_l.append(i) ++ else: ++ top_line_ts.append(syslog_ts(top_line[i])) ++ # remove files with no matches found ++ rm_idx_l.reverse() ++ for i in rm_idx_l: ++ del fl[i],top_line[i] ++ common_debug("search <%s> in %s" % (re_s, [ f.name for f in fl ])) ++ if len(fl) == 0: # nothing matched ? ++ return [] ++ if len(fl) == 1: ++ # no need to merge if there's only one log ++ return [top_line[0]] + self.single_log_list(fl[0],patt) ++ # search through multiple logs, merge sorted by time ++ l = [] ++ first = 0 ++ while True: ++ for i in range(len(fl)): ++ try: ++ if i == first: ++ continue ++ if top_line_ts[i] and top_line_ts[i] < top_line_ts[first]: ++ first = i ++ except: pass ++ if not top_line[first]: ++ break ++ l.append(top_line[first]) ++ top_line[first] = self.get_match_line(fl[first], patt)[0] ++ if not top_line[first]: ++ top_line_ts[first] = time.time() ++ else: ++ top_line_ts[first] = syslog_ts(top_line[first]) ++ return l ++ def get_matches(self, re_l, log_l = []): ++ ''' ++ Return a list of log messages which ++ match one of the regexes in re_l. ++ ''' ++ if not log_l: ++ log_l = self.log_l ++ re_s = '|'.join(re_l) ++ return filter(self.search_logs(log_l, re_s), log_l) ++ # caching is not ready! ++ # gets complicated because of different time frames ++ # (TODO) ++ #if not re_s: # just list logs ++ # return filter(self.search_logs(log_l), log_l) ++ #if re_s not in self.cache: # cache regex search ++ # self.cache[re_s] = self.search_logs(log_l, re_s) ++ #return filter(self.cache[re_s], log_l) ++ ++def human_date(dt): ++ 'Some human date representation. Date defaults to now.' ++ if not dt: ++ dt = datetime.datetime.now() ++ # drop microseconds ++ return re.sub("[.].*","","%s %s" % (dt.date(),dt.time())) ++ ++def is_log(p): ++ return os.path.isfile(p) and os.path.getsize(p) > 0 ++ ++def pe_file_in_range(pe_f, a, ext): ++ r = re.search("pe-[^-]+-([0-9]+)[.]%s$" % ext, pe_f) ++ if not r: ++ return None ++ if not a or (a[0] <= int(r.group(1)) <= a[1]): ++ return pe_f ++ return None ++ ++def read_log_info(log): ++ 'Read .info and return logfile and next pos' ++ s = file2str("%s.info" % log) ++ try: ++ logf,pos = s.split() ++ return logf, int(pos) ++ except: ++ warn_once("hb_report too old, you need to update cluster-glue") ++ return '',-1 ++ ++def update_loginfo(rptlog, logfile, oldpos, appended_file): ++ 'Update .info with new next pos' ++ newpos = oldpos + os.stat(appended_file).st_size ++ try: ++ f = open("%s.info" % rptlog, "w") ++ f.write("%s %d\n" % (logfile, newpos)) ++ f.close() ++ except IOError, msg: ++ common_err("couldn't the update %s.info: %s" % (rptlog, msg)) ++ ++class Report(Singleton): ++ ''' ++ A hb_report class. ++ ''' ++ live_recent = 6*60*60 # recreate live hb_report once every 6 hours ++ short_live_recent = 60 # update once a minute ++ nodecolors = ( ++ "NORMAL", "GREEN", "CYAN", "MAGENTA", "YELLOW", "WHITE", "BLUE", "RED" ++ ) ++ def __init__(self): ++ self.source = None ++ self.loc = None ++ self.ready = False ++ self.from_dt = None ++ self.to_dt = None ++ self.detail = 0 ++ self.nodecolor = {} ++ self.logobj = None ++ self.desc = None ++ self.log_l = [] ++ self.central_log = None ++ self.cibgrp_d = {} ++ self.cibrsc_l = [] ++ self.cibnode_l = [] ++ self.setnodes = [] ++ self.outdir = os.path.join(vars.report_cache,"psshout") ++ self.errdir = os.path.join(vars.report_cache,"pssherr") ++ self.last_live_update = 0 ++ def error(self, s): ++ common_err("%s: %s" % (self.source, s)) ++ def warn(self, s): ++ common_warn("%s: %s" % (self.source, s)) ++ def rsc_list(self): ++ return self.cibgrp_d.keys() + self.cibrsc_l ++ def node_list(self): ++ return self.cibnode_l ++ def peinputs_list(self): ++ return [re.search("pe-[^-]+-([0-9]+)[.]bz2$", x).group(1) ++ for x in self._file_list("bz2")] ++ def unpack_report(self, tarball): ++ ''' ++ Unpack hb_report tarball. ++ Don't unpack if the directory already exists! ++ ''' ++ bfname = os.path.basename(tarball) ++ parentdir = os.path.dirname(tarball) ++ common_debug("tarball: %s, in dir: %s" % (bfname,parentdir)) ++ if bfname.endswith(".tar.bz2"): ++ loc = tarball.replace(".tar.bz2","") ++ tar_unpack_option = "j" ++ elif bfname.endswith(".tar.gz"): # hmm, must be ancient ++ loc = tarball.replace(".tar.gz","") ++ tar_unpack_option = "z" ++ else: ++ self.error("this doesn't look like a report tarball") ++ return None ++ if os.path.isdir(loc): ++ return loc ++ cwd = os.getcwd() ++ try: ++ os.chdir(parentdir) ++ except OSError,msg: ++ self.error(msg) ++ return None ++ rc = ext_cmd_nosudo("tar -x%s < %s" % (tar_unpack_option,bfname)) ++ if self.source == "live": ++ os.remove(bfname) ++ os.chdir(cwd) ++ if rc != 0: ++ return None ++ return loc ++ def get_nodes(self): ++ return [ os.path.basename(p) ++ for p in os.listdir(self.loc) ++ if os.path.isdir(os.path.join(self.loc, p)) and ++ os.path.isfile(os.path.join(self.loc, p, "cib.txt")) ++ ] ++ def check_nodes(self): ++ 'Verify if the nodes in cib match the nodes in the report.' ++ nl = self.get_nodes() ++ if not nl: ++ self.error("no nodes in report") ++ return False ++ for n in self.cibnode_l: ++ if not (n in nl): ++ self.warn("node %s not in report" % n) ++ else: ++ nl.remove(n) ++ if nl: ++ self.warn("strange, extra node(s) %s in report" % ','.join(nl)) ++ return True ++ def check_report(self): ++ ''' ++ Check some basic properties of the report. ++ ''' ++ if not self.loc: ++ return False ++ if not os.access(self.desc, os.F_OK): ++ self.error("no description file in the report") ++ return False ++ if not self.check_nodes(): ++ return False ++ return True ++ def _live_loc(self): ++ return os.path.join(vars.report_cache,"live") ++ def is_last_live_recent(self): ++ ''' ++ Look at the last live hb_report. If it's recent enough, ++ return True. Return True also if self.to_dt is not empty ++ (not an open end report). ++ ''' ++ try: ++ last_ts = os.stat(self.desc).st_mtime ++ return (time.time() - last_ts <= self.live_recent) ++ except Exception, msg: ++ self.warn(msg) ++ self.warn("strange, couldn't stat %s" % self.desc) ++ return False ++ def find_node_log(self, node): ++ p = os.path.join(self.loc, node) ++ if is_log(os.path.join(p, "ha-log.txt")): ++ return os.path.join(p, "ha-log.txt") ++ elif is_log(os.path.join(p, "messages")): ++ return os.path.join(p, "messages") ++ else: ++ return None ++ def find_central_log(self): ++ 'Return common log, if found.' ++ central_log = os.path.join(self.loc, "ha-log.txt") ++ if is_log(central_log): ++ logf, pos = read_log_info(central_log) ++ if logf.startswith("synthetic"): ++ # not really central log ++ return ++ common_debug("found central log %s" % logf) ++ self.central_log = central_log ++ def find_logs(self): ++ 'Return a list of logs found (one per node).' ++ l = [] ++ for node in self.get_nodes(): ++ log = self.find_node_log(node) ++ if log: ++ l.append(log) ++ else: ++ self.warn("no log found for node %s" % node) ++ self.log_l = l ++ def append_newlogs(self, a): ++ ''' ++ Append new logs fetched from nodes. ++ ''' ++ if not os.path.isdir(self.outdir): ++ return ++ for node,rptlog,logfile,nextpos in a: ++ fl = glob.glob("%s/*%s*" % (self.outdir,node)) ++ if not fl: ++ continue ++ append_file(rptlog,fl[0]) ++ update_loginfo(rptlog, logfile, nextpos, fl[0]) ++ def unpack_new_peinputs(self, a): ++ ''' ++ Untar PE inputs fetched from nodes. ++ ''' ++ if not os.path.isdir(self.outdir): ++ return ++ for node,pe_l in a: ++ fl = glob.glob("%s/*%s*" % (self.outdir,node)) ++ if not fl: ++ continue ++ u_dir = os.path.join(self.loc, node) ++ rc = ext_cmd_nosudo("tar -C %s -x < %s" % (u_dir,fl[0])) ++ def find_new_peinputs(self, a): ++ ''' ++ Get a list of pe inputs appearing in logs. ++ ''' ++ if not os.path.isdir(self.outdir): ++ return [] ++ l = [] ++ for node,rptlog,logfile,nextpos in a: ++ node_l = [] ++ fl = glob.glob("%s/*%s*" % (self.outdir,node)) ++ if not fl: ++ continue ++ for s in file2list(fl[0]): ++ r = re.search(transition_patt[0], s) ++ if not r: ++ continue ++ node_l.append(r.group(1)) ++ if node_l: ++ common_debug("found new PE inputs %s at %s" % ++ ([os.path.basename(x) for x in node_l], node)) ++ l.append([node,node_l]) ++ return l ++ def update_live(self): ++ ''' ++ Update the existing live report, if it's older than ++ self.short_live_recent: ++ - append newer logs ++ - get new PE inputs ++ ''' ++ if (time.time() - self.last_live_update) <= self.short_live_recent: ++ return True ++ if _NO_PSSH: ++ warn_once("pssh not installed, slow live updates ahead") ++ return False ++ a = [] ++ common_info("fetching new logs, please wait ...") ++ for rptlog in self.log_l: ++ node = log2node(rptlog) ++ logf, pos = read_log_info(rptlog) ++ if logf: ++ a.append([node, rptlog, logf, pos]) ++ if not a: ++ common_info("no elligible logs found :(") ++ return False ++ rmdir_r(self.outdir) ++ rmdir_r(self.errdir) ++ rc1 = next_loglines(a, self.outdir, self.errdir) ++ self.append_newlogs(a) ++ pe_l = self.find_new_peinputs(a) ++ rmdir_r(self.outdir) ++ rmdir_r(self.errdir) ++ rc2 = True ++ if pe_l: ++ rc2 = next_peinputs(pe_l, self.outdir, self.errdir) ++ self.unpack_new_peinputs(pe_l) ++ self.logobj = None ++ rmdir_r(self.outdir) ++ rmdir_r(self.errdir) ++ self.last_live_update = time.time() ++ return (rc1 and rc2) ++ def get_live_report(self): ++ acquire_lock(vars.report_cache) ++ loc = self.new_live_hb_report() ++ release_lock(vars.report_cache) ++ return loc ++ def manage_live_report(self, force = False): ++ ''' ++ Update or create live report. ++ ''' ++ d = self._live_loc() ++ if not d or not os.path.isdir(d): ++ return self.get_live_report() ++ if not self.loc: ++ # the live report is there, but we were just invoked ++ self.loc = d ++ self.report_setup() ++ if not force and self.is_last_live_recent(): ++ acquire_lock(vars.report_cache) ++ rc = self.update_live() ++ release_lock(vars.report_cache) ++ if rc: ++ return self._live_loc() ++ return self.get_live_report() ++ def new_live_hb_report(self): ++ ''' ++ Run hb_report to get logs now. ++ ''' ++ d = self._live_loc() ++ rmdir_r(d) ++ tarball = "%s.tar.bz2" % d ++ to_option = "" ++ if self.to_dt: ++ to_option = "-t '%s'" % human_date(self.to_dt) ++ nodes_option = "" ++ if self.setnodes: ++ nodes_option = "'-n %s'" % ' '.join(self.setnodes) ++ if ext_cmd_nosudo("mkdir -p %s" % os.path.dirname(d)) != 0: ++ return None ++ common_info("retrieving information from cluster nodes, please wait ...") ++ rc = ext_cmd_nosudo("hb_report -f '%s' %s %s %s" % ++ (self.from_dt.ctime(), to_option, nodes_option, d)) ++ if rc != 0: ++ if os.path.isfile(tarball): ++ self.warn("hb_report thinks it failed, proceeding anyway") ++ else: ++ self.error("hb_report failed") ++ return None ++ self.last_live_update = time.time() ++ return self.unpack_report(tarball) ++ def reset_period(self): ++ self.from_dt = None ++ self.to_dt = None ++ def set_source(self,src): ++ 'Set our source.' ++ self.source = src ++ def set_period(self,from_dt,to_dt): ++ ''' ++ Set from/to_dt. ++ ''' ++ common_debug("setting report times: <%s> - <%s>" % (from_dt,to_dt)) ++ if not self.from_dt: ++ self.from_dt = from_dt ++ self.to_dt = to_dt ++ elif self.source != "live": ++ if self.from_dt > from_dt: ++ self.error("from time %s not within report" % from_dt) ++ return False ++ if to_dt and self.to_dt < to_dt: ++ self.error("end time %s not within report" % to_dt) ++ return False ++ self.from_dt = from_dt ++ self.to_dt = to_dt ++ else: ++ need_ref = (self.from_dt > from_dt or \ ++ (to_dt and self.to_dt < to_dt)) ++ self.from_dt = from_dt ++ self.to_dt = to_dt ++ if need_ref: ++ self.refresh_source(force = True) ++ if self.logobj: ++ self.logobj.set_log_timeframe(self.from_dt, self.to_dt) ++ def set_detail(self,detail_lvl): ++ ''' ++ Set the detail level. ++ ''' ++ self.detail = int(detail_lvl) ++ def set_nodes(self,*args): ++ ''' ++ Allow user to set the node list (necessary if the host is ++ not part of the cluster). ++ ''' ++ self.setnodes = args ++ def read_cib(self): ++ ''' ++ Get some information from the report's CIB (node list, ++ resource list, groups). If "live" and not central log, ++ then use cibadmin. ++ ''' ++ nl = self.get_nodes() ++ if not nl: ++ return ++ if self.source == "live" and not self.central_log: ++ doc = cibdump2doc() ++ else: ++ doc = file2doc(os.path.join(self.loc,nl[0],"cib.xml")) ++ if not doc: ++ return # no cib? ++ try: conf = doc.getElementsByTagName("configuration")[0] ++ except: # bad cib? ++ return ++ self.cibrsc_l = [ x.getAttribute("id") ++ for x in conf.getElementsByTagName("primitive") ] ++ self.cibnode_l = [ x.getAttribute("uname") ++ for x in conf.getElementsByTagName("node") ] ++ for grp in conf.getElementsByTagName("group"): ++ self.cibgrp_d[grp.getAttribute("id")] = get_rsc_children_ids(grp) ++ def set_node_colors(self): ++ i = 0 ++ for n in self.cibnode_l: ++ self.nodecolor[n] = self.nodecolors[i] ++ i = (i+1) % len(self.nodecolors) ++ def report_setup(self): ++ if not self.loc: ++ return ++ self.desc = os.path.join(self.loc,"description.txt") ++ self.find_logs() ++ self.find_central_log() ++ self.read_cib() ++ self.set_node_colors() ++ self.logobj = None ++ def prepare_source(self): ++ ''' ++ Unpack a hb_report tarball. ++ For "live", create an ad-hoc hb_report and unpack it ++ somewhere in the cache area. ++ Parse the period. ++ ''' ++ if self.ready and self.source != "live": ++ return True ++ if self.source == "live": ++ self.loc = self.manage_live_report() ++ elif os.path.isfile(self.source): ++ self.loc = self.unpack_report(self.source) ++ elif os.path.isdir(self.source): ++ self.loc = self.source ++ if not self.loc: ++ return False ++ self.report_setup() ++ self.ready = self.check_report() ++ return self.ready ++ def refresh_source(self, force = False): ++ ''' ++ Refresh report from live. ++ ''' ++ if self.source != "live": ++ self.error("refresh not supported") ++ return False ++ self.loc = self.manage_live_report(force) ++ self.report_setup() ++ self.ready = self.check_report() ++ return self.ready ++ def get_patt_l(self,type): ++ ''' ++ get the list of patterns for this type, up to and ++ including current detail level ++ ''' ++ if not type in log_patterns: ++ common_error("%s not featured in log patterns" % type) ++ return None ++ return log_patterns[type][0:self.detail+1] ++ def build_re(self,type,args): ++ ''' ++ Prepare a regex string for the type and args. ++ For instance, "resource" and rsc1, rsc2, ... ++ ''' ++ patt_l = self.get_patt_l(type) ++ if not patt_l: ++ return None ++ if not args: ++ re_l = mk_re_list(patt_l,"") ++ else: ++ re_l = mk_re_list(patt_l,r'(%s)\W' % "|".join(args)) ++ return re_l ++ def disp(self, s): ++ 'color output' ++ a = s.split() ++ try: clr = self.nodecolor[a[3]] ++ except: return s ++ return termctrl.render("${%s}%s${NORMAL}" % (clr,s)) ++ def show_logs(self, log_l = [], re_l = []): ++ ''' ++ Print log lines, either matched by re_l or all. ++ ''' ++ if not log_l: ++ log_l = self.log_l ++ if not self.central_log and not log_l: ++ self.error("no logs found") ++ return ++ if not self.logobj: ++ self.logobj = LogSyslog(self.central_log, log_l, \ ++ self.from_dt, self.to_dt) ++ l = self.logobj.get_matches(re_l, log_l) ++ if not options.batch and sys.stdout.isatty(): ++ page_string('\n'.join([ self.disp(x) for x in l ])) ++ else: # raw output ++ try: # in case user quits the next prog in pipe ++ for s in l: print s ++ except IOError, msg: ++ if not ("Broken pipe" in msg): ++ common_err(msg) ++ def match_args(self, cib_l, args): ++ for a in args: ++ a_clone = re.sub(r':.*', '', a) ++ if not (a in cib_l) and not (a_clone in cib_l): ++ self.warn("%s not found in report, proceeding anyway" % a) ++ def get_desc_line(self,fld): ++ try: ++ f = open(self.desc) ++ except IOError,msg: ++ common_err("open %s: %s"%(self.desc,msg)) ++ return ++ for s in f: ++ if s.startswith("%s: " % fld): ++ f.close() ++ s = s.replace("%s: " % fld,"").rstrip() ++ return s ++ f.close() ++ def info(self): ++ ''' ++ Print information about the source. ++ ''' ++ if not self.prepare_source(): ++ return False ++ print "Source: %s" % self.source ++ if self.source != "live": ++ print "Created:", self.get_desc_line("Date") ++ print "By:", self.get_desc_line("By") ++ print "Period: %s - %s" % \ ++ ((self.from_dt and human_date(self.from_dt) or "start"), ++ (self.to_dt and human_date(self.to_dt) or "end")) ++ print "Nodes:",' '.join(self.cibnode_l) ++ print "Groups:",' '.join(self.cibgrp_d.keys()) ++ print "Resources:",' '.join(self.cibrsc_l) ++ def latest(self): ++ ''' ++ Get the very latest cluster events, basically from the ++ latest transition. ++ Some transitions may be empty though. ++ ''' ++ def events(self): ++ ''' ++ Show all events. ++ ''' ++ if not self.prepare_source(): ++ return False ++ all_re_l = self.build_re("resource",self.cibrsc_l) + \ ++ self.build_re("node",self.cibnode_l) ++ if not all_re_l: ++ self.error("no resources or nodes found") ++ return False ++ self.show_logs(re_l = all_re_l) ++ def resource(self,*args): ++ ''' ++ Show resource relevant logs. ++ ''' ++ if not self.prepare_source(): ++ return False ++ # expand groups (if any) ++ expanded_l = [] ++ for a in args: ++ if a in self.cibgrp_d: ++ expanded_l += self.cibgrp_d[a] ++ else: ++ expanded_l.append(a) ++ self.match_args(self.cibrsc_l,expanded_l) ++ rsc_re_l = self.build_re("resource",expanded_l) ++ if not rsc_re_l: ++ return False ++ self.show_logs(re_l = rsc_re_l) ++ def node(self,*args): ++ ''' ++ Show node relevant logs. ++ ''' ++ if not self.prepare_source(): ++ return False ++ self.match_args(self.cibnode_l,args) ++ node_re_l = self.build_re("node",args) ++ if not node_re_l: ++ return False ++ self.show_logs(re_l = node_re_l) ++ def log(self,*args): ++ ''' ++ Show logs for a node or all nodes. ++ ''' ++ if not self.prepare_source(): ++ return False ++ if not args: ++ self.show_logs() ++ else: ++ l = [] ++ for n in args: ++ if n not in self.cibnode_l: ++ self.warn("%s: no such node" % n) ++ continue ++ l.append(self.find_node_log(n)) ++ if not l: ++ return False ++ self.show_logs(log_l = l) ++ def _file_list(self, ext, a = []): ++ ''' ++ Return list of PE (or dot) files (abs paths) sorted by ++ mtime. ++ Input is a number or a pair of numbers representing ++ range. Otherwise, all matching files are returned. ++ ''' ++ if not self.prepare_source(): ++ return [] ++ if not isinstance(a,(tuple,list)) and a is not None: ++ a = [a,a] ++ return sort_by_mtime([x for x in dirwalk(self.loc) \ ++ if pe_file_in_range(x,a,ext)]) ++ def pelist(self, a = []): ++ return self._file_list("bz2", a) ++ def dotlist(self, a = []): ++ return self._file_list("dot", a) ++ def find_file(self, f): ++ return file_find_by_name(self.loc, f) ++ ++vars = Vars.getInstance() ++options = Options.getInstance() ++termctrl = TerminalController.getInstance() ++cib_factory = CibFactory.getInstance() ++crm_report = Report.getInstance() ++# vim:ts=4:sw=4:et: +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/ui.py.in +--- a/shell/modules/ui.py.in Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/ui.py.in Mon Jul 18 12:35:57 2011 +0200 +@@ -27,6 +27,7 @@ from vars import Vars + from levels import Levels + from cibconfig import mkset_obj, CibFactory + from cibstatus import CibStatus ++from report import Report + from template import LoadTemplate + from cliformat import nvpairs2list + from ra import * +@@ -1390,6 +1391,7 @@ cluster. + self.cmd_table["cib"] = CibShadow + self.cmd_table["cibstatus"] = StatusMgmt + self.cmd_table["template"] = Template ++ self.cmd_table["history"] = History + self.cmd_table["_test"] = (self.check_structure,(0,0),1,0) + self.cmd_table["_regtest"] = (self.regression_testing,(1,1),1,0) + self.cmd_table["_objects"] = (self.showobjects,(0,0),1,0) +@@ -1661,6 +1663,227 @@ cluster. + self.commit("commit") + cib_factory.reset() + ++class History(UserInterface): ++ ''' ++ The history class ++ ''' ++ lvl_name = "history" ++ desc_short = "CRM cluster history" ++ desc_long = """ ++The history level. ++ ++Examine Pacemaker's history: node and resource events, logs. ++""" ++ def __init__(self): ++ UserInterface.__init__(self) ++ self.cmd_table["source"] = (self.source,(1,1),1,0) ++ self.cmd_table["limit"] = (self.limit,(1,2),1,0) ++ self.cmd_table["refresh"] = (self.refresh,(0,1),1,0) ++ self.cmd_table["detail"] = (self.detail,(1,1),1,0) ++ self.cmd_table["setnodes"] = (self.setnodes,(1,),1,0) ++ self.cmd_table["info"] = (self.info,(0,0),1,0) ++ self.cmd_table["latest"] = (self.latest,(0,0),1,0) ++ self.cmd_table["resource"] = (self.resource,(1,),1,0) ++ self.cmd_table["node"] = (self.node,(1,),1,1) ++ self.cmd_table["log"] = (self.log,(0,),1,0) ++ self.cmd_table["peinputs"] = (self.peinputs,(0,),1,0) ++ self._set_source(options.history) ++ def _no_source(self): ++ common_error("we have no source set yet! please use the source command") ++ def parse_time(self, t): ++ ''' ++ Try to make sense of the user provided time spec. ++ Use dateutil if available, otherwise strptime. ++ Return the datetime value. ++ ''' ++ try: ++ import dateutil.parser ++ dt = dateutil.parser.parse(t) ++ except ValueError,msg: ++ common_err("%s: %s" % (t,msg)) ++ return None ++ except ImportError,msg: ++ import datetime ++ try: ++ tm = time.strptime(t) ++ dt = datetime.datetime(*tm[0:7]) ++ except ValueError,msg: ++ common_err("no dateutil, please provide times as printed by date(1)") ++ return None ++ return dt ++ def _set_period(self,from_time,to_time = ''): ++ ''' ++ parse time specs and set period ++ ''' ++ from_dt = self.parse_time(from_time) ++ if not from_dt: ++ return False ++ to_dt = None ++ if to_time: ++ to_dt = self.parse_time(to_time) ++ if not to_dt: ++ return False ++ if to_dt and to_dt <= from_dt: ++ common_err("%s - %s: bad period" % from_time, to_time) ++ return False ++ common_debug("setting period to <%s>:<%s>" % (from_dt,to_dt)) ++ return crm_report.set_period(from_dt,to_dt) ++ def _check_source(self,src): ++ 'a (very) quick source check' ++ if src == "live" or os.path.isfile(src) or os.path.isdir(src): ++ return True ++ else: ++ common_error("source %s doesn't exist" % src) ++ return False ++ def _set_source(self,src,live_from_time = None): ++ ''' ++ Have the last history source survive the History ++ and Report instances ++ ''' ++ common_debug("setting source to %s" % src) ++ if not self._check_source(src): ++ return False ++ crm_report.set_source(src) ++ options.history = src ++ options.report_to_time = '' ++ rc = True ++ if src == "live": ++ options.report_from_time = time.ctime(live_from_time and \ ++ live_from_time or (time.time() - 60*60)) ++ rc = self._set_period(\ ++ options.report_from_time, options.report_to_time) ++ else: ++ options.report_from_time = '' ++ return rc ++ def source(self,cmd,src = None): ++ "usage: source {||live}" ++ if src != options.history: ++ return self._set_source(src) ++ def limit(self,cmd,from_time,to_time = ''): ++ "usage: limit []" ++ if options.report_from_time != from_time or \ ++ options.report_to_time != to_time: ++ if not self._set_period(from_time, to_time): ++ return False ++ options.report_from_time = from_time ++ options.report_to_time = to_time ++ def refresh(self, cmd, force = ''): ++ "usage: refresh" ++ if options.history != "live": ++ common_info("nothing to refresh if source isn't live") ++ return False ++ if force: ++ if force != "force" and force != "--force": ++ syntax_err((cmd,force), context = 'refresh') ++ return False ++ force = True ++ return crm_report.refresh_source(force) ++ def detail(self,cmd,detail_lvl): ++ "usage: detail " ++ detail_num = convert2ints(detail_lvl) ++ if not (isinstance(detail_num,int) and int(detail_num) >= 0): ++ bad_usage(cmd,detail_lvl) ++ return False ++ return crm_report.set_detail(detail_lvl) ++ def setnodes(self,cmd,*args): ++ "usage: setnodes [ ...]" ++ if options.history != "live": ++ common_info("setting nodes not necessary for existing reports, proceeding anyway") ++ return crm_report.set_nodes(*args) ++ def info(self,cmd): ++ "usage: info" ++ return crm_report.info() ++ def latest(self,cmd): ++ "usage: latest" ++ try: ++ prev_level = levels.previous().myname() ++ except: ++ prev_level = '' ++ if prev_level != "cibconfig": ++ common_err("%s is available only when invoked from configure" % cmd) ++ return False ++ ts = cib_factory.last_commit_at() ++ if not ts: ++ common_err("no last commit time found") ++ return False ++ if not wait4dc("transition", not options.batch): ++ return False ++ self._set_source("live", ts) ++ crm_report.refresh_source() ++ return crm_report.events() ++ def resource(self,cmd,*args): ++ "usage: resource [ ...]" ++ return crm_report.resource(*args) ++ def node(self,cmd,*args): ++ "usage: node [ ...]" ++ return crm_report.node(*args) ++ def log(self,cmd,*args): ++ "usage: log [ ...]" ++ return crm_report.log(*args) ++ def ptest(self, nograph, scores, utilization, actions, verbosity): ++ 'Send a decompressed self.pe_file to ptest' ++ try: ++ f = open(self.pe_file) ++ except IOError,msg: ++ common_err("open: %s"%msg) ++ return False ++ s = bz2.decompress(''.join(f)) ++ f.close() ++ return run_ptest(s, nograph, scores, utilization, actions, verbosity) ++ def peinputs(self,cmd,subcmd,*args): ++ """usage: peinputs list [{|} ...] ++ peinputs get [{|} ...] ++ peinputs show [] [nograph] [v...] [scores] [actions] [utilization] ++ peinputs showdot []""" ++ if subcmd in ("get","list"): ++ if args: ++ l = [] ++ for s in args: ++ a = convert2ints(s.split(':')) ++ if len(a) == 2 and not check_range(a): ++ common_err("%s: invalid peinputs range" % a) ++ return False ++ l += crm_report.pelist(a) ++ else: ++ l = crm_report.pelist() ++ if not l: return False ++ if subcmd == "list": ++ s = get_stdout("ls -lrt %s" % ' '.join(l)) ++ page_string(s) ++ else: ++ print '\n'.join(l) ++ elif subcmd in ("show","showdot"): ++ try: n = convert2ints(args[0]) ++ except: n = None ++ startarg = 1 ++ if n is None: ++ idx = -1 ++ startarg = 0 # peinput number missing ++ elif n <= 0: ++ idx = n - 1 ++ n = [] # to get all peinputs ++ else: ++ idx = 0 ++ if subcmd == "showdot": ++ if not user_prefs.dotty: ++ common_err("install graphviz to draw transition graphs") ++ return False ++ l = crm_report.dotlist(n) ++ else: ++ l = crm_report.pelist(n) ++ if len(l) < abs(idx): ++ common_err("pe input or dot file not found") ++ return False ++ if subcmd == "show": ++ self.pe_file = l[idx] ++ return ptestlike(self.ptest,'vv',"%s %s" % \ ++ (cmd, subcmd), *args[startarg:]) ++ else: ++ show_dot_graph(l[idx]) ++ else: ++ bad_usage(cmd,' '.join(subcmd,args)) ++ return False ++ + class TopLevel(UserInterface): + ''' + The top level. +@@ -1681,6 +1904,7 @@ class TopLevel(UserInterface): + self.cmd_table['configure'] = CibConfig + self.cmd_table['node'] = NodeMgmt + self.cmd_table['options'] = CliOptions ++ self.cmd_table['history'] = History + self.cmd_table['status'] = (self.status,(0,5),0,0) + self.cmd_table['ra'] = RA + setup_aliases(self) +@@ -1726,5 +1950,5 @@ vars = Vars.getInstance() + levels = Levels.getInstance(TopLevel) + cib_status = CibStatus.getInstance() + cib_factory = CibFactory.getInstance() +- ++crm_report = Report.getInstance() + # vim:ts=4:sw=4:et: +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/userprefs.py +--- a/shell/modules/userprefs.py Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/userprefs.py Mon Jul 18 12:35:57 2011 +0200 +@@ -18,6 +18,7 @@ + from os import getenv + import subprocess + import sys ++import datetime, time + + from singletonmixin import Singleton + from term import TerminalController +@@ -26,6 +27,10 @@ class Options(Singleton): + interactive = False + batch = False + regression_tests = False ++ history = "live" ++ # now minus one hour ++ report_from_time = time.ctime(time.time()-60*60) ++ report_to_time = "" + + options = Options.getInstance() + termctrl = TerminalController.getInstance() +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/utils.py +--- a/shell/modules/utils.py Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/utils.py Mon Jul 18 12:35:57 2011 +0200 +@@ -22,6 +22,7 @@ import subprocess + import re + import glob + import time ++import shutil + + from userprefs import Options, UserPrefs + from vars import Vars +@@ -177,6 +178,29 @@ def str2file(s,fname): + f.write(s) + f.close() + return True ++def file2str(fname, noerr = True): ++ ''' ++ Read a one line file into a string, strip whitespace around. ++ ''' ++ try: f = open(fname,"r") ++ except IOError, msg: ++ if not noerr: ++ common_err(msg) ++ return None ++ s = f.readline() ++ f.close() ++ return s.strip() ++def file2list(fname): ++ ''' ++ Read a file into a list (newlines dropped). ++ ''' ++ try: f = open(fname,"r") ++ except IOError, msg: ++ common_err(msg) ++ return None ++ l = ''.join(f).split('\n') ++ f.close() ++ return l + + def is_filename_sane(name): + if re.search("['`/#*?$\[\]]",name): +@@ -203,9 +227,70 @@ def ext_cmd(cmd): + print ".EXT", cmd + return subprocess.call(add_sudo(cmd), shell=True) + +-def get_stdout(cmd, stderr_on = True): ++def rmdir_r(d): ++ if d and os.path.isdir(d): ++ shutil.rmtree(d) ++ ++_LOCKDIR = ".lockdir" ++_PIDF = "pid" ++def check_locker(dir): ++ if not os.path.isdir(os.path.join(dir,_LOCKDIR)): ++ return ++ s = file2str(os.path.join(dir,_LOCKDIR,_PIDF)) ++ pid = convert2ints(s) ++ if not isinstance(pid,int): ++ common_warn("history: removing malformed lock") ++ rmdir_r(os.path.join(dir,_LOCKDIR)) ++ return ++ try: ++ os.kill(pid, 0) ++ except OSError, err: ++ if err.errno == os.errno.ESRCH: ++ common_info("history: removing stale lock") ++ rmdir_r(os.path.join(dir,_LOCKDIR)) ++ else: ++ common_err("%s: %s" % (_LOCKDIR,err.message)) ++def acquire_lock(dir): ++ check_locker(dir) ++ while True: ++ try: ++ os.mkdir(os.path.join(dir,_LOCKDIR)) ++ str2file("%d" % os.getpid(),os.path.join(dir,_LOCKDIR,_PIDF)) ++ return True ++ except OSError, e: ++ if e.errno != os.errno.EEXIST: ++ common_err("%s" % e.message) ++ return False ++ time.sleep(0.1) ++ continue ++ else: ++ return False ++def release_lock(dir): ++ rmdir_r(os.path.join(dir,_LOCKDIR)) ++ ++#def ext_cmd_nosudo(cmd): ++# if options.regression_tests: ++# print ".EXT", cmd ++# return subprocess.call(cmd, shell=True) ++ ++def ext_cmd_nosudo(cmd): ++ if options.regression_tests: ++ print ".EXT", cmd ++ proc = subprocess.Popen(cmd, shell = True, ++ stdout = subprocess.PIPE, ++ stderr = subprocess.PIPE) ++ (outp,err_outp) = proc.communicate() ++ proc.wait() ++ rc = proc.returncode ++ if rc != 0: ++ print outp ++ print err_outp ++ return rc ++ ++def get_stdout(cmd, input_s = None, stderr_on = True): + ''' +- Run a cmd, return stdin output. ++ Run a cmd, return stdout output. ++ Optional input string "input_s". + stderr_on controls whether to show output which comes on stderr. + ''' + if stderr_on: +@@ -213,8 +298,9 @@ def get_stdout(cmd, stderr_on = True): + else: + stderr = subprocess.PIPE + proc = subprocess.Popen(cmd, shell = True, \ ++ stdin = subprocess.PIPE, \ + stdout = subprocess.PIPE, stderr = stderr) +- outp = proc.communicate()[0] ++ outp = proc.communicate(input_s)[0] + proc.wait() + outp = outp.strip() + return outp +@@ -223,9 +309,27 @@ def stdout2list(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_stdout(add_sudo(cmd), stderr_on) ++ s = get_stdout(add_sudo(cmd), stderr_on = stderr_on) + return s.split('\n') + ++def append_file(dest,src): ++ 'Append src to dest' ++ try: ++ dest_f = open(dest,"a") ++ except IOError,msg: ++ common_err("open %s: %s" % (dest, msg)) ++ return False ++ try: ++ f = open(src) ++ except IOError,msg: ++ common_err("open %s: %s" % (src, msg)) ++ dest_f.close() ++ return False ++ dest_f.write(''.join(f)) ++ f.close() ++ dest_f.close() ++ return True ++ + def wait4dc(what = "", show_progress = True): + ''' + Wait for the DC to get into the S_IDLE state. This should be +@@ -283,6 +387,38 @@ def wait4dc(what = "", show_progress = T + if cnt % 5 == 0: + sys.stderr.write(".") + ++def run_ptest(graph_s, nograph, scores, utilization, actions, verbosity): ++ ''' ++ Pipe graph_s thru ptest(8). Show graph using dotty if requested. ++ ''' ++ actions_filter = "grep LogActions: | grep -vw Leave" ++ ptest = "ptest -X" ++ if verbosity: ++ if actions: ++ verbosity = 'v' * max(3,len(verbosity)) ++ ptest = "%s -%s" % (ptest,verbosity.upper()) ++ if scores: ++ ptest = "%s -s" % ptest ++ if utilization: ++ ptest = "%s -U" % ptest ++ if user_prefs.dotty and not nograph: ++ fd,dotfile = mkstemp() ++ ptest = "%s -D %s" % (ptest,dotfile) ++ else: ++ dotfile = None ++ # ptest prints to stderr ++ if actions: ++ ptest = "%s 2>&1 | %s" % (ptest, actions_filter) ++ print get_stdout(ptest, input_s = graph_s) ++ #page_string(get_stdout(ptest, input_s = graph_s)) ++ if dotfile: ++ show_dot_graph(dotfile) ++ vars.tmpfiles.append(dotfile) ++ else: ++ if not nograph: ++ common_info("install graphviz to see a transition graph") ++ return True ++ + def is_id_valid(id): + """ + Verify that the id follows the definition: +@@ -299,6 +435,57 @@ def check_filename(fname): + fname_re = "^[^/]+$" + return re.match(fname_re,id) + ++def check_range(a): ++ """ ++ Verify that the integer range in list a is valid. ++ """ ++ if len(a) != 2: ++ return False ++ if not isinstance(a[0],int) or not isinstance(a[1],int): ++ return False ++ return (int(a[0]) < int(a[1])) ++ ++def sort_by_mtime(l): ++ 'Sort a (small) list of files by time mod.' ++ l2 = [(os.stat(x).st_mtime, x) for x in l] ++ l2.sort() ++ return [x[1] for x in l2] ++def dirwalk(dir): ++ "walk a directory tree, using a generator" ++ # http://code.activestate.com/recipes/105873/ ++ for f in os.listdir(dir): ++ fullpath = os.path.join(dir,f) ++ if os.path.isdir(fullpath) and not os.path.islink(fullpath): ++ for x in dirwalk(fullpath): # recurse into subdir ++ yield x ++ else: ++ yield fullpath ++def file_find_by_name(dir, fname): ++ 'Find a file within a tree matching fname.' ++ if not dir: ++ common_err("cannot dirwalk nothing!") ++ return None ++ if not fname: ++ common_err("file to find not provided") ++ return None ++ for f in dirwalk(dir): ++ if os.path.basename(f) == fname: ++ return f ++ return None ++ ++def convert2ints(l): ++ """ ++ Convert a list of strings (or a string) to a list of ints. ++ All strings must be ints, otherwise conversion fails and None ++ is returned! ++ """ ++ try: ++ if isinstance(l,(tuple,list)): ++ return [int(x) for x in l] ++ else: # it's a string then ++ return int(l) ++ except: return None ++ + def is_process(s): + proc = subprocess.Popen("ps -e -o pid,command | grep -qs '%s'" % s, \ + shell=True, stdout=subprocess.PIPE) +diff -r 9609937061d7 -r b694b75d2e33 shell/modules/vars.py.in +--- a/shell/modules/vars.py.in Fri Jul 15 10:51:00 2011 +1000 ++++ b/shell/modules/vars.py.in Mon Jul 18 12:35:57 2011 +0200 +@@ -186,6 +186,7 @@ class Vars(Singleton): + hist_file = os.path.join(homedir,".crm_history") + rc_file = os.path.join(homedir,".crm.rc") + tmpl_conf_dir = os.path.join(homedir,".crmconf") ++ report_cache = os.path.join("@CRM_CACHE_DIR@","history") + tmpl_dir = "@datadir@/@PACKAGE@/templates" + pe_dir = "@PE_STATE_DIR@" + crm_conf_dir = "@CRM_CONFIG_DIR@" + diff --git a/crm_history_10_d21f988a419c.patch b/crm_history_10_d21f988a419c.patch new file mode 100644 index 0000000..1920025 --- /dev/null +++ b/crm_history_10_d21f988a419c.patch @@ -0,0 +1,38 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1314279513 -7200 +# Node ID d21f988a419c0c7fa349c4e26f6b500944d91370 +# Parent 709ef91cfada2822aca53dcef085ddb6952393c5 +Low: Shell: look for log segments with more care and don't throw exception on seek (bnc#713939) + +diff --git a/shell/modules/report.py b/shell/modules/report.py +--- a/shell/modules/report.py ++++ b/shell/modules/report.py +@@ -72,8 +72,15 @@ def seek_to_edge(f, ts, to_end): + Linear search, but should be short. + ''' + if not to_end: ++ beg = 0 + while ts == get_timestamp(f): +- f.seek(-1000, 1) # go back 10 or so lines ++ if f.tell() < 1000: ++ f.seek(0) # otherwise, the seek below throws an exception ++ if beg > 0: # avoid infinite loop ++ return # goes all the way to the top ++ beg += 1 ++ else: ++ f.seek(-1000, 1) # go back 10 or so lines + while True: + pos = f.tell() + s = f.readline() +@@ -86,8 +93,8 @@ def seek_to_edge(f, ts, to_end): + def log_seek(f, ts, to_end = False): + ''' + f is an open log. Do binary search for the timestamp. +- Return the position of the (more or less) first line with a +- newer time. ++ Return the position of the (more or less) first line with an ++ earlier (or later) time. + ''' + first = 0 + f.seek(0,2) diff --git a/crm_history_11_ccd0c1e1edf9.patch b/crm_history_11_ccd0c1e1edf9.patch new file mode 100644 index 0000000..6a5ef7b --- /dev/null +++ b/crm_history_11_ccd0c1e1edf9.patch @@ -0,0 +1,298 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1314632951 -7200 +# Node ID ccd0c1e1edf9f23cafb4363014acba755f1b4e25 +# Parent d21f988a419c0c7fa349c4e26f6b500944d91370 +Medium: Shell: several history improvements + +- add more patterns for fencing +- handle better PE files number reaching limit + +diff --git a/doc/crm.8.txt b/doc/crm.8.txt +--- a/doc/crm.8.txt ++++ b/doc/crm.8.txt +@@ -2426,7 +2426,8 @@ Example: + + The `latest` command shows a bit of recent history, more + precisely whatever happened since the last cluster change (the +-latest transition). ++latest transition). If the transition is running, the shell will ++first wait until it finishes. + + Usage: + ............... +@@ -2540,10 +2541,13 @@ Example: + setnodes node_a node_b + ............... + +-[[cmdhelp_history_resource,resource failed actions]] ++[[cmdhelp_history_resource,resource events]] + ==== `resource` + +-Show status changes and any failures that happened on a resource. ++Show actions and any failures that happened on all specified ++resources on all nodes. Normally, one gives resource names as ++arguments, but it is also possible to use extended regular ++expressions. + + Usage: + ............... +@@ -2551,14 +2555,17 @@ Usage: + ............... + Example: + ............... +- resource mydb ++ resource bigdb public_ip ++ resource bigdb:0 ++ resource bigdb:. + ............... + + [[cmdhelp_history_node,node events]] + ==== `node` + + Show important events that happened on a node. Important events +-are node lost and join, standby and online, and fence. ++are node lost and join, standby and online, and fence. Use either ++node names or extended regular expressions. + + Usage: + ............... +@@ -2572,7 +2579,17 @@ Example: + [[cmdhelp_history_log,log content]] + ==== `log` + +-Show logs for a node or combined logs of all nodes. ++Show messages logged on one or more nodes. Leaving out a node ++name produces combined logs of all nodes. Messages are sorted by ++time and, if the terminal emulations supports it, displayed in ++different colours depending on the node to allow for easier ++reading. ++ ++The sorting key is the timestamp as written by syslog which ++normally has the maximum resolution of one second. Obviously, ++messages generated by events which share the same timestamp may ++not be sorted in the same way as they happened. Such close events ++may actually happen fairly often. + + Usage: + ............... +@@ -2634,8 +2651,8 @@ the transition are printed. + + Usage: + ............... +- transition [|] [nograph] [v...] [scores] [actions] [utilization] +- transition showdot [|] ++ transition [||] [nograph] [v...] [scores] [actions] [utilization] ++ transition showdot [||] + ............... + Examples: + ............... +diff --git a/shell/modules/log_patterns.py b/shell/modules/log_patterns.py +--- a/shell/modules/log_patterns.py ++++ b/shell/modules/log_patterns.py +@@ -12,34 +12,41 @@ + # detail level 0 is the lowest, i.e. should match the least + # number of relevant messages + +-# NB: If you modify this file, you must follow python syntax! ++# NB: ++# %% stands for whatever user input we get, for instance a ++# resource name or node name or just some regular expression ++# in optimal case, it should be surrounded by literals ++# ++# [Note that resources may contain clone numbers!] + + log_patterns = { + "resource": ( + ( # detail 0 +- "lrmd:.*rsc:%%.*(start|stop|promote|demote|migrate)", +- "lrmd:.*RA output:.*%%.*stderr", +- "lrmd:.*WARN:.*Managed.*%%.*exited", ++ "lrmd:.*rsc:%% (start|stop|promote|demote|migrate)", ++ "lrmd:.*RA output: .%%:.*:stderr", ++ "lrmd:.*WARN: Managed %%:.*exited", + ), + ( # detail 1 +- "lrmd:.*rsc:%%.*(probe|notify)", +- "lrmd:.*info:.*Managed.*%%.*exited", ++ "lrmd:.*rsc:%%:.*(probe|notify)", ++ "lrmd:.*info: Managed %%:.*exited", + ), + ), + "node": ( + ( # detail 0 +- "%%.*Corosync.Cluster.Engine", +- "%%.*Executive.Service.RELEASE", +- "%%.*crm_shutdown:.Requesting.shutdown", +- "%%.*pcmk_shutdown:.Shutdown.complete", +- "%%.*Configuration.validated..Starting.heartbeat", +- "pengine.*Scheduling Node %%", +- "te_fence_node.*Exec.*%%", +- "stonith-ng.*log_oper.*reboot.*%%", +- "stonithd.*to STONITH.*%%", +- "stonithd.*fenced node %%", +- "pcmk_peer_update.*(lost|memb): %%", +- "crmd.*ccm_event.*(NEW|LOST) %%", ++ " %% .*Corosync.Cluster.Engine", ++ " %% .*Executive.Service.RELEASE", ++ " %% .*crm_shutdown:.Requesting.shutdown", ++ " %% .*pcmk_shutdown:.Shutdown.complete", ++ " %% .*Configuration.validated..Starting.heartbeat", ++ "pengine.*Scheduling Node %% for STONITH", ++ "crmd.* tengine_stonith_callback: .* of %% failed", ++ "stonith-ng.*log_operation:.*host '%%'", ++ "te_fence_node: Exec.*on %% ", ++ "pe_fence_node: Node %% will be fenced", ++ "stonith-ng.*remote_op_timeout:.*for %% timed", ++ "stonithd.*Succeeded.*node %%:", ++ "pcmk_peer_update.*(lost|memb): %% ", ++ "crmd.*ccm_event.*(NEW|LOST):.* %% ", + ), + ( # detail 1 + ), +diff --git a/shell/modules/report.py b/shell/modules/report.py +--- a/shell/modules/report.py ++++ b/shell/modules/report.py +@@ -589,7 +589,7 @@ class Report(Singleton): + except IOError,msg: + common_err("open %s: %s"%(fl[0],msg)) + continue +- pe_l = self.get_transitions([x for x in f], keep_pe_path = True) ++ pe_l = self.list_transitions([x for x in f], future_pe = True) + if pe_l: + l.append([node,pe_l]) + return l +@@ -752,12 +752,13 @@ class Report(Singleton): + for n in self.cibnode_l: + self.nodecolor[n] = self.nodecolors[i] + i = (i+1) % len(self.nodecolors) +- def get_transitions(self, msg_l = None, keep_pe_path = False): ++ def list_transitions(self, msg_l = None, future_pe = False): + ''' +- Get a list of transitions. ++ List transitions by reading logs. + Empty transitions are skipped. +- Some callers need original PE file path (keep_pe_path), +- otherwise we produce the path within the report. ++ Some callers need original PE file path (future_pe), ++ otherwise we produce the path within the report and check ++ if the transition files exist. + If the caller doesn't provide the message list, then we + build it from the collected log files (self.logobj). + Otherwise, we get matches for transition patterns. +@@ -786,11 +787,18 @@ class Report(Singleton): + continue + elif num_actions == -1: # couldn't find messages + common_warn("could not find number of actions for transition (%s)" % pe_base) +- common_debug("found PE input at %s: %s" % (node, pe_file)) +- if keep_pe_path: +- pe_l.append(pe_file) ++ if not future_pe: ++ pe_l_file = os.path.join(self.loc, node, "pengine", pe_base) ++ if not os.path.isfile(pe_l_file): ++ warn_once("%s in the logs, but not in the report" % pe_l_file) ++ continue + else: +- pe_l.append(os.path.join(self.loc, node, "pengine", pe_base)) ++ pe_l_file = "%s:%s" % (node, pe_file) ++ if pe_l_file in pe_l: ++ common_warn("duplicate %s, replacing older PE file" % pe_l_file) ++ pe_l.remove(pe_l_file) ++ common_debug("found PE input: %s" % pe_l_file) ++ pe_l.append(pe_l_file) + return pe_l + def report_setup(self): + if not self.loc: +@@ -802,11 +810,7 @@ class Report(Singleton): + self.set_node_colors() + self.logobj = LogSyslog(self.central_log, self.log_l, \ + self.from_dt, self.to_dt) +- self.peinputs_l = self.get_transitions() +- for pe_input in self.peinputs_l: +- if not os.path.isfile(pe_input): +- warn_once("%s in the logs, but not in the report" % pe_input) +- self.peinputs_l.remove(pe_input) ++ self.peinputs_l = self.list_transitions() + def prepare_source(self): + ''' + Unpack a hb_report tarball. +@@ -859,7 +863,7 @@ class Report(Singleton): + if not args: + re_l = mk_re_list(patt_l,"") + else: +- re_l = mk_re_list(patt_l,r'(%s)\W' % "|".join(args)) ++ re_l = mk_re_list(patt_l,r'(%s)' % "|".join(args)) + return re_l + def disp(self, s): + 'color output' +@@ -886,11 +890,6 @@ class Report(Singleton): + self.error("no logs found") + return + self.display_logs(self.logobj.get_matches(re_l, log_l)) +- def match_args(self, cib_l, args): +- for a in args: +- a_clone = re.sub(r':.*', '', a) +- if not (a in cib_l) and not (a_clone in cib_l): +- self.warn("%s not found in report, proceeding anyway" % a) + def get_desc_line(self,fld): + try: + f = open(self.desc) +@@ -923,8 +922,9 @@ class Report(Singleton): + ''' + Show all events. + ''' +- all_re_l = self.build_re("resource",self.cibrsc_l) + \ +- self.build_re("node",self.cibnode_l) ++ all_re_l = self.build_re("resource", self.cibrsc_l) + \ ++ self.build_re("node", self.cibnode_l) + \ ++ self.build_re("events", []) + if not all_re_l: + self.error("no resources or nodes found") + return False +@@ -940,6 +940,7 @@ class Report(Singleton): + te_invoke_patt = transition_patt[0].replace("%%", pe_num) + run_patt = transition_patt[1].replace("%%", pe_num) + r = None ++ msg_l.reverse() + for msg in msg_l: + r = re.search(te_invoke_patt, msg) + if r: +@@ -1009,7 +1010,6 @@ class Report(Singleton): + expanded_l += self.cibgrp_d[a] + else: + expanded_l.append(a) +- self.match_args(self.cibrsc_l,expanded_l) + rsc_re_l = self.build_re("resource",expanded_l) + if not rsc_re_l: + return False +@@ -1020,7 +1020,6 @@ class Report(Singleton): + ''' + if not self.prepare_source(): + return False +- self.match_args(self.cibnode_l,args) + node_re_l = self.build_re("node",args) + if not node_re_l: + return False +diff --git a/shell/modules/ui.py.in b/shell/modules/ui.py.in +--- a/shell/modules/ui.py.in ++++ b/shell/modules/ui.py.in +@@ -1877,16 +1877,16 @@ Examine Pacemaker's history: node and re + def _get_pe_byidx(self, idx): + l = crm_report.pelist() + if len(l) < abs(idx): +- common_err("pe input file for index %d not found" % (idx+1)) ++ common_err("PE input file for index %d not found" % (idx+1)) + return None + return l[idx] + def _get_pe_bynum(self, n): + l = crm_report.pelist([n]) + if len(l) == 0: +- common_err("%s: PE file %d not found" % n) ++ common_err("PE file %d not found" % n) + return None + elif len(l) > 1: +- common_err("%s: PE file %d ambiguous" % n) ++ common_err("PE file %d ambiguous" % n) + return None + return l[0] + def transition(self,cmd,*args): diff --git a/crm_history_1_d0359dca5dba.patch b/crm_history_1_d0359dca5dba.patch new file mode 100644 index 0000000..c90072a --- /dev/null +++ b/crm_history_1_d0359dca5dba.patch @@ -0,0 +1,20 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1312579593 -7200 +# Node ID d0359dca5dba3fd6fee856d51cca5ee7ac752ee6 +# Parent a7acb683b3568ca81d90472f770b0270270d5dfd +Low: Shell: relax host key checking in pssh + +diff -r a7acb683b356 -r d0359dca5dba shell/modules/crm_pssh.py +--- a/shell/modules/crm_pssh.py Fri Aug 05 23:13:37 2011 +0200 ++++ b/shell/modules/crm_pssh.py Fri Aug 05 23:26:33 2011 +0200 +@@ -84,7 +84,8 @@ def do_pssh(l, opts): + hosts = [] + for host, cmdline in l: + cmd = ['ssh', host, '-o', 'PasswordAuthentication=no', +- '-o', 'SendEnv=PSSH_NODENUM'] ++ '-o', 'SendEnv=PSSH_NODENUM', ++ '-o', 'StrictHostKeyChecking=no'] + if opts.options: + for opt in opts.options: + cmd += ['-o', opt] diff --git a/crm_history_2_29fd4f04c01f.patch b/crm_history_2_29fd4f04c01f.patch new file mode 100644 index 0000000..3438020 --- /dev/null +++ b/crm_history_2_29fd4f04c01f.patch @@ -0,0 +1,19 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1312580731 -7200 +# Node ID 29fd4f04c01f92e54026d9d6bb54d617d8b1fdcd +# Parent d0359dca5dba3fd6fee856d51cca5ee7ac752ee6 +Low: Shell: enforce remote report directory removal for history + +diff -r d0359dca5dba -r 29fd4f04c01f shell/modules/report.py +--- a/shell/modules/report.py Fri Aug 05 23:26:33 2011 +0200 ++++ b/shell/modules/report.py Fri Aug 05 23:45:31 2011 +0200 +@@ -595,7 +595,7 @@ class Report(Singleton): + if ext_cmd_nosudo("mkdir -p %s" % os.path.dirname(d)) != 0: + return None + common_info("retrieving information from cluster nodes, please wait ...") +- rc = ext_cmd_nosudo("hb_report -f '%s' %s %s %s" % ++ rc = ext_cmd_nosudo("hb_report -Z -f '%s' %s %s %s" % + (self.from_dt.ctime(), to_option, nodes_option, d)) + if rc != 0: + if os.path.isfile(tarball): diff --git a/crm_history_3_b3a014c0f85b.patch b/crm_history_3_b3a014c0f85b.patch new file mode 100644 index 0000000..afcc4f3 --- /dev/null +++ b/crm_history_3_b3a014c0f85b.patch @@ -0,0 +1,558 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1312993121 -7200 +# Node ID b3a014c0f85b2bbe1e6a2360c44fbbfc7ac27b73 +# Parent a09974a06cdf6a3d73c3cdfa6e4d89d41e2ca9f0 +Medium: Shell: improve peinputs and transition interface (bnc#710655,711060) + +- allow specifying PE files as relative paths in order to + disambiguate between PE inputs with the same number +- remove peinputs "get" and "list" subcommands, just use 'v' for the + long listing +- remove transition "show" subcommand, if there is no subcommand + it is assumed that the user wants to do "show" +- detect (and ignore) empty transitions +- update completion tables + +diff --git a/doc/crm.8.txt b/doc/crm.8.txt +--- a/doc/crm.8.txt ++++ b/doc/crm.8.txt +@@ -2560,55 +2560,62 @@ Example: + + Every event in the cluster results in generating one or more + Policy Engine (PE) files. These files describe future motions of +-resources. The files are listed along with the node where they +-were created (the DC at the time). The `get` subcommand will copy +-all PE input files to the current working directory (and use ssh +-if necessary). ++resources. The files are listed as full paths in the current ++report directory. Add `v` to also see the creation time stamps. + + Usage: + ............... +- peinputs list [{|} ...] +- peinputs get [{|} ...] ++ peinputs [{|} ...] [v] + + range :: : + ............... + Example: + ............... +- peinputs get 440:444 446 ++ peinputs ++ peinputs 440:444 446 ++ peinputs v + ............... + + [[cmdhelp_history_transition,show transition]] + ==== `transition` + +-The `show` subcommand will print actions planned by the PE and +-run graphviz (`dotty`) to display a graphical representation. Of +-course, for the latter an X11 session is required. This command +-invokes `ptest(8)` in background. ++This command will print actions planned by the PE and run ++graphviz (`dotty`) to display a graphical representation of the ++transition. Of course, for the latter an X11 session is required. ++This command invokes `ptest(8)` in background. + + The `showdot` subcommand runs graphviz (`dotty`) to display a + graphical representation of the `.dot` file which has been + included in the report. Essentially, it shows the calculation + produced by `pengine` which is installed on the node where the +-report was produced. ++report was produced. In optimal case this output should not ++differ from the one produced by the locally installed `pengine`. + + If the PE input file number is not provided, it defaults to the + last one, i.e. the last transition. If the number is negative, + then the corresponding transition relative to the last one is + chosen. + ++If there are warning and error PE input files or different nodes ++were the DC in the observed timeframe, it may happen that PE ++input file numbers collide. In that case provide some unique part ++of the path to the file. ++ + After the `ptest` output, logs about events that happened during + the transition are printed. + + Usage: + ............... +- transition show [] [nograph] [v...] [scores] [actions] [utilization] +- transition showdot [] ++ transition [|] [nograph] [v...] [scores] [actions] [utilization] ++ transition showdot [|] + ............... + Examples: + ............... +- transition show +- transition show 444 +- transition show -1 ++ transition ++ transition 444 ++ transition -1 ++ transition pe-error-3.bz2 ++ transition node-a/pengine/pe-input-2.bz2 + transition showdot 444 + ............... + +diff --git a/shell/modules/completion.py b/shell/modules/completion.py +--- a/shell/modules/completion.py ++++ b/shell/modules/completion.py +@@ -165,14 +165,14 @@ def report_node_list(idx,delimiter = Fal + if delimiter: + return ' ' + return crm_report.node_list() +-def report_pe_cmd_list(idx,delimiter = False): ++def report_pe_list_transition(idx,delimiter = False): + if delimiter: + return ' ' +- return ["list","get","show","showdot"] +-def report_pe_list(idx,delimiter = False): ++ return crm_report.peinputs_list() + ["showdot"] ++def report_pe_list_peinputs(idx,delimiter = False): + if delimiter: + return ' ' +- return crm_report.peinputs_list() ++ return crm_report.peinputs_list() + ["v"] + + # + # completion for primitives including help for parameters +@@ -484,7 +484,8 @@ completer_lists = { + "resource" : (report_rsc_list,loop), + "node" : (report_node_list,loop), + "log" : (report_node_list,loop), +- "peinputs" : (report_pe_cmd_list,report_pe_list,loop), ++ "peinputs" : (report_pe_list_peinputs,loop), ++ "transition" : (report_pe_list_transition,), + }, + } + def get_completer_list(level,cmd): +diff --git a/shell/modules/crm_pssh.py b/shell/modules/crm_pssh.py +--- a/shell/modules/crm_pssh.py ++++ b/shell/modules/crm_pssh.py +@@ -156,6 +156,9 @@ def next_peinputs(node_pe_l, outdir, err + myopts = ["-q", "-o", outdir, "-e", errdir] + opts, args = parse_args(myopts) + l.append([node, cmdline]) ++ if not l: ++ # is this a failure? ++ return True + return do_pssh(l, opts) + + # vim:ts=4:sw=4:et: +diff --git a/shell/modules/log_patterns.py b/shell/modules/log_patterns.py +--- a/shell/modules/log_patterns.py ++++ b/shell/modules/log_patterns.py +@@ -62,8 +62,3 @@ log_patterns = { + ), + ), + } +- +-transition_patt = ( +- "crmd: .* Processing graph.*derived from .*/pe-[^-]+-(%%)[.]bz2", # transition start +- "crmd: .* Transition.*Source=.*/pe-[^-]+-(%%)[.]bz2.: (Stopped|Complete|Terminated)", # and stop +-) +diff --git a/shell/modules/report.py b/shell/modules/report.py +--- a/shell/modules/report.py ++++ b/shell/modules/report.py +@@ -31,7 +31,7 @@ from term import TerminalController + from xmlutil import * + from utils import * + from msg import * +-from log_patterns import log_patterns, transition_patt ++from log_patterns import log_patterns + _NO_PSSH = False + try: + from crm_pssh import next_loglines, next_peinputs +@@ -297,8 +297,8 @@ def human_date(dt): + def is_log(p): + return os.path.isfile(p) and os.path.getsize(p) > 0 + +-def pe_file_in_range(pe_f, a, ext): +- r = re.search("pe-[^-]+-([0-9]+)[.]%s$" % ext, pe_f) ++def pe_file_in_range(pe_f, a): ++ r = re.search("pe-[^-]+-([0-9]+)[.]bz2$", pe_f) + if not r: + return None + if not a or (a[0] <= int(r.group(1)) <= a[1]): +@@ -325,6 +325,17 @@ def update_loginfo(rptlog, logfile, oldp + except IOError, msg: + common_err("couldn't the update %s.info: %s" % (rptlog, msg)) + ++# r.group(1) transition number (a different thing from file number) ++# r.group(2) contains full path ++# r.group(3) file number ++transition_patt = ( ++ "crmd: .* do_te_invoke: Processing graph ([0-9]+) .*derived from (.*/pe-[^-]+-(%%)[.]bz2)", # transition start ++ "crmd: .* run_graph: Transition ([0-9]+).*Source=(.*/pe-[^-]+-(%%)[.]bz2).: (Stopped|Complete|Terminated)", # and stop ++# r.group(1) transition number ++# r.group(2) number of actions ++ "crmd: .* unpack_graph: Unpacked transition (%%): ([0-9]+) actions", # number of actions ++) ++ + class Report(Singleton): + ''' + A hb_report class. +@@ -346,6 +357,7 @@ class Report(Singleton): + self.desc = None + self.log_l = [] + self.central_log = None ++ self.peinputs_l = [] + self.cibgrp_d = {} + self.cibrsc_l = [] + self.cibnode_l = [] +@@ -363,7 +375,7 @@ class Report(Singleton): + return self.cibnode_l + def peinputs_list(self): + return [re.search("pe-[^-]+-([0-9]+)[.]bz2$", x).group(1) +- for x in self._file_list("bz2")] ++ for x in self.peinputs_l] + def unpack_report(self, tarball): + ''' + Unpack hb_report tarball. +@@ -495,28 +507,26 @@ class Report(Singleton): + continue + u_dir = os.path.join(self.loc, node) + rc = ext_cmd_nosudo("tar -C %s -x < %s" % (u_dir,fl[0])) +- def find_new_peinputs(self, a): ++ def find_new_peinputs(self, node_l): + ''' +- Get a list of pe inputs appearing in logs. ++ Get a list of pe inputs appearing in new logs. ++ The log is put in self.outdir/node by pssh. + ''' + if not os.path.isdir(self.outdir): + return [] + l = [] +- trans_re_l = [x.replace("%%","") for x in transition_patt] +- for node,rptlog,logfile,nextpos in a: +- node_l = [] ++ for node in node_l: + fl = glob.glob("%s/*%s*" % (self.outdir,node)) + if not fl: + continue +- for s in file2list(fl[0]): +- r = re.search(trans_re_l[0], s) +- if not r: +- continue +- node_l.append(r.group(1)) +- if node_l: +- common_debug("found new PE inputs %s at %s" % +- ([os.path.basename(x) for x in node_l], node)) +- l.append([node,node_l]) ++ try: ++ f = open(fl[0]) ++ except IOError,msg: ++ common_err("open %s: %s"%(fl[0],msg)) ++ continue ++ pe_l = self.get_transitions([x for x in f], keep_pe_path = True) ++ if pe_l: ++ l.append([node,pe_l]) + return l + def update_live(self): + ''' +@@ -544,7 +554,7 @@ class Report(Singleton): + rmdir_r(self.errdir) + rc1 = next_loglines(a, self.outdir, self.errdir) + self.append_newlogs(a) +- pe_l = self.find_new_peinputs(a) ++ pe_l = self.find_new_peinputs([x[0] for x in a]) + rmdir_r(self.outdir) + rmdir_r(self.errdir) + rc2 = True +@@ -677,6 +687,55 @@ class Report(Singleton): + for n in self.cibnode_l: + self.nodecolor[n] = self.nodecolors[i] + i = (i+1) % len(self.nodecolors) ++ def get_transitions(self, msg_l = None, keep_pe_path = False): ++ ''' ++ Get a list of transitions. ++ Empty transitions are skipped. ++ We use the unpack_graph message to see the number of ++ actions. ++ Some callers need original PE file path (keep_pe_path), ++ otherwise we produce the path within the report. ++ If the caller doesn't provide the message list, then we ++ build it from the collected log files (self.logobj). ++ Otherwise, we get matches for transition patterns. ++ ''' ++ trans_re_l = [x.replace("%%", "[0-9]+") for x in transition_patt] ++ if not msg_l: ++ msg_l = self.logobj.get_matches(trans_re_l) ++ else: ++ re_s = '|'.join(trans_re_l) ++ msg_l = [x for x in msg_l if re.search(re_s, x)] ++ pe_l = [] ++ for msg in msg_l: ++ msg_a = msg.split() ++ if len(msg_a) < 8: ++ # this looks too short ++ common_warn("log message <%s> unexpected format, please report a bug" % msg) ++ continue ++ if msg_a[7] in ("unpack_graph:","run_graph:"): ++ continue # we want another message ++ node = msg_a[3] ++ pe_file = msg_a[-1] ++ pe_base = os.path.basename(pe_file) ++ # check if there were any actions in this transition ++ r = re.search(trans_re_l[0], msg) ++ trans_num = r.group(1) ++ unpack_patt = transition_patt[2].replace("%%", trans_num) ++ num_actions = 0 ++ for t in msg_l: ++ try: ++ num_actions = int(re.search(unpack_patt, t).group(2)) ++ break ++ except: pass ++ if num_actions == 0: # empty transition ++ common_debug("skipping empty transition %s (%s)" % (trans_num, pe_base)) ++ continue ++ common_debug("found PE input at %s: %s" % (node, pe_file)) ++ if keep_pe_path: ++ pe_l.append(pe_file) ++ else: ++ pe_l.append(os.path.join(self.loc, node, "pengine", pe_base)) ++ return pe_l + def report_setup(self): + if not self.loc: + return +@@ -687,6 +746,11 @@ class Report(Singleton): + self.set_node_colors() + self.logobj = LogSyslog(self.central_log, self.log_l, \ + self.from_dt, self.to_dt) ++ self.peinputs_l = self.get_transitions() ++ for pe_input in self.peinputs_l: ++ if not os.path.isfile(pe_input): ++ warn_once("%s in the logs, but not in the report" % pe_input) ++ self.peinputs_l.remove(pe_input) + def prepare_source(self): + ''' + Unpack a hb_report tarball. +@@ -821,16 +885,16 @@ class Report(Singleton): + Search for events within the given transition. + ''' + pe_base = os.path.basename(pe_file) +- r = re.search("pe-[^-]+-([0-9]+)[.]bz2", pe_base) ++ r = re.search("pe-[^-]+-([0-9]+)[.]", pe_base) + pe_num = r.group(1) + trans_re_l = [x.replace("%%",pe_num) for x in transition_patt] + trans_start = self.logobj.search_logs(self.log_l, trans_re_l[0]) + trans_end = self.logobj.search_logs(self.log_l, trans_re_l[1]) + if not trans_start: +- common_warn("transition %s start not found in logs" % pe_base) ++ common_warn("start of transition %s not found in logs" % pe_base) + return False + if not trans_end: +- common_warn("transition %s end not found in logs" % pe_base) ++ common_warn("end of transition %s not found in logs (transition not complete yet?)" % pe_base) + return False + common_debug("transition start: %s" % trans_start[0]) + common_debug("transition end: %s" % trans_end[0]) +@@ -891,23 +955,23 @@ class Report(Singleton): + if not l: + return False + self.show_logs(log_l = l) +- def _file_list(self, ext, a = []): +- ''' +- Return list of PE (or dot) files (abs paths) sorted by +- mtime. +- Input is a number or a pair of numbers representing +- range. Otherwise, all matching files are returned. +- ''' ++ def pelist(self, a = []): + if not self.prepare_source(): + return [] +- if not isinstance(a,(tuple,list)) and a is not None: ++ if isinstance(a,(tuple,list)): ++ if len(a) == 1: ++ a.append(a[0]) ++ elif a is not None: + a = [a,a] +- return sort_by_mtime([x for x in dirwalk(self.loc) \ +- if pe_file_in_range(x,a,ext)]) +- def pelist(self, a = []): +- return self._file_list("bz2", a) ++ return [x for x in self.peinputs_l \ ++ if pe_file_in_range(x, a)] + def dotlist(self, a = []): +- return self._file_list("dot", a) ++ l = [x.replace("bz2","dot") for x in self.pelist(a)] ++ return [x for x in l if os.path.isfile(x)] ++ def find_pe_files(self, path): ++ 'Find a PE or dot file matching part of the path.' ++ pe_l = path.endswith(".dot") and self.dotlist() or self.pelist() ++ return [x for x in pe_l if x.endswith(path)] + def find_file(self, f): + return file_find_by_name(self.loc, f) + +diff --git a/shell/modules/ui.py.in b/shell/modules/ui.py.in +--- a/shell/modules/ui.py.in ++++ b/shell/modules/ui.py.in +@@ -1686,8 +1686,8 @@ Examine Pacemaker's history: node and re + self.cmd_table["resource"] = (self.resource,(1,),1,0) + self.cmd_table["node"] = (self.node,(1,),1,1) + self.cmd_table["log"] = (self.log,(0,),1,0) +- self.cmd_table["peinputs"] = (self.peinputs,(1,),1,0) +- self.cmd_table["transition"] = (self.transition,(1,),1,0) ++ self.cmd_table["peinputs"] = (self.peinputs,(0,),1,0) ++ self.cmd_table["transition"] = (self.transition,(0,),1,0) + self._set_source(options.history) + def _no_source(self): + common_error("we have no source set yet! please use the source command") +@@ -1831,64 +1831,83 @@ Examine Pacemaker's history: node and re + s = bz2.decompress(''.join(f)) + f.close() + return run_ptest(s, nograph, scores, utilization, actions, verbosity) +- def peinputs(self,cmd,subcmd,*args): +- """usage: peinputs list [{|} ...] +- peinputs get [{|} ...]""" +- if subcmd not in ("get","list"): +- bad_usage(cmd,subcmd) +- return False +- if args: ++ def peinputs(self,cmd,*args): ++ """usage: peinputs [{|} ...] [v]""" ++ argl = list(args) ++ long = "v" in argl ++ if long: ++ argl.remove("v") ++ if argl: + l = [] +- for s in args: ++ for s in argl: + a = convert2ints(s.split(':')) +- if len(a) == 2 and not check_range(a): ++ if a and len(a) == 2 and not check_range(a): + common_err("%s: invalid peinputs range" % a) + return False + l += crm_report.pelist(a) + else: + l = crm_report.pelist() + if not l: return False +- if subcmd == "list": +- s = get_stdout("ls -lrt %s" % ' '.join(l)) +- page_string(s) ++ if long: ++ s = get_stdout("for f in %s; do ls -l $f; done" % ' '.join(l)) + else: +- print '\n'.join(l) +- def transition(self,cmd,subcmd,*args): +- """usage: transition show [] [nograph] [v...] [scores] [actions] [utilization] +- transition showdot []""" +- if subcmd not in ("show", "showdot"): +- bad_usage(cmd,subcmd) +- return False +- try: n = convert2ints(args[0]) +- except: n = None +- startarg = 1 +- if n is None: +- idx = -1 +- startarg = 0 # peinput number missing +- elif n <= 0: +- idx = n - 1 +- n = [] # to get all peinputs +- else: +- idx = 0 +- if subcmd == "showdot": ++ s = '\n'.join(l) ++ page_string(s) ++ def transition(self,cmd,*args): ++ """usage: transition [|] [nograph] [v...] [scores] [actions] [utilization] ++ transition showdot [|]""" ++ argl = list(args) ++ subcmd = "show" ++ if argl and argl[0] == "showdot": + if not user_prefs.dotty: + common_err("install graphviz to draw transition graphs") + return False +- l = crm_report.dotlist(n) ++ subcmd = "showdot" ++ argl.remove(subcmd) ++ f = None ++ startarg = 1 ++ if argl and re.search('pe-', argl[0]): ++ l = crm_report.find_pe_files(argl[0]) ++ if len(l) == 0: ++ common_err("%s: path not found" % argl[0]) ++ return False ++ elif len(l) > 1: ++ common_err("%s: path ambiguous" % argl[0]) ++ return False ++ f = l[0] + else: +- l = crm_report.pelist(n) +- if len(l) < abs(idx): +- common_err("pe input or dot file not found") ++ try: n = convert2ints(argl[0]) ++ except: n = None ++ if n is None: ++ idx = -1 ++ startarg = 0 # peinput number missing ++ elif n <= 0: ++ idx = n - 1 ++ n = [] # to get all peinputs ++ else: ++ idx = 0 ++ if subcmd == "showdot": ++ l = crm_report.dotlist(n) ++ else: ++ l = crm_report.pelist(n) ++ if len(l) < abs(idx): ++ if subcmd == "show": ++ common_err("pe input file not found") ++ else: ++ common_err("dot file not found") ++ return False ++ f = l[idx] ++ if not f: + return False + rc = True + if subcmd == "show": +- self.pe_file = l[idx] ++ self.pe_file = f # self.pe_file needed by self.ptest + rc = ptestlike(self.ptest,'vv',"%s %s" % \ +- (cmd, subcmd), *args[startarg:]) +- if rc: +- crm_report.show_transition_log(self.pe_file) ++ (cmd, subcmd), *argl[startarg:]) + else: +- show_dot_graph(l[idx]) ++ show_dot_graph(f.replace("bz2","dot")) ++ if rc: ++ crm_report.show_transition_log(f) + return rc + + class TopLevel(UserInterface): +diff --git a/shell/modules/utils.py b/shell/modules/utils.py +--- a/shell/modules/utils.py ++++ b/shell/modules/utils.py +@@ -392,7 +392,7 @@ def run_ptest(graph_s, nograph, scores, + Pipe graph_s thru ptest(8). Show graph using dotty if requested. + ''' + actions_filter = "grep LogActions: | grep -vw Leave" +- ptest = "ptest -X" ++ ptest = "2>&1 ptest -X" + if verbosity: + if actions: + verbosity = 'v' * max(3,len(verbosity)) +@@ -408,7 +408,8 @@ def run_ptest(graph_s, nograph, scores, + dotfile = None + # ptest prints to stderr + if actions: +- ptest = "%s 2>&1 | %s" % (ptest, actions_filter) ++ ptest = "%s | %s" % (ptest, actions_filter) ++ common_debug("invoke: %s" % ptest) + print get_stdout(ptest, input_s = graph_s) + #page_string(get_stdout(ptest, input_s = graph_s)) + if dotfile: +@@ -443,7 +444,7 @@ def check_range(a): + return False + if not isinstance(a[0],int) or not isinstance(a[1],int): + return False +- return (int(a[0]) < int(a[1])) ++ return (int(a[0]) <= int(a[1])) + + def sort_by_mtime(l): + 'Sort a (small) list of files by time mod.' diff --git a/crm_history_4_a09974a06cdf.patch b/crm_history_4_a09974a06cdf.patch new file mode 100644 index 0000000..679cad5 --- /dev/null +++ b/crm_history_4_a09974a06cdf.patch @@ -0,0 +1,23 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1312838871 -7200 +# Node ID a09974a06cdf6a3d73c3cdfa6e4d89d41e2ca9f0 +# Parent 29fd4f04c01f92e54026d9d6bb54d617d8b1fdcd +Low: Shell: avoid DeprecationWarning for BaseException.message + +diff --git a/shell/modules/utils.py b/shell/modules/utils.py +--- a/shell/modules/utils.py ++++ b/shell/modules/utils.py +@@ -257,9 +257,9 @@ def acquire_lock(dir): + os.mkdir(os.path.join(dir,_LOCKDIR)) + str2file("%d" % os.getpid(),os.path.join(dir,_LOCKDIR,_PIDF)) + return True +- except OSError, e: +- if e.errno != os.errno.EEXIST: +- common_err("%s" % e.message) ++ except OSError as (errno, strerror): ++ if errno != os.errno.EEXIST: ++ common_err(strerror) + return False + time.sleep(0.1) + continue diff --git a/crm_history_5_c3068d22de72.patch b/crm_history_5_c3068d22de72.patch new file mode 100644 index 0000000..2d42591 --- /dev/null +++ b/crm_history_5_c3068d22de72.patch @@ -0,0 +1,90 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1313019300 -7200 +# Node ID c3068d22de72d1ba616d43c808091bef830eb9f6 +# Parent b3a014c0f85b2bbe1e6a2360c44fbbfc7ac27b73 +Medium: Shell: improve capture log slices for transitions (bnc#710907) + +diff --git a/shell/modules/report.py b/shell/modules/report.py +--- a/shell/modules/report.py ++++ b/shell/modules/report.py +@@ -65,7 +65,25 @@ def syslog_ts(s): + common_warn("malformed line: %s" % s) + return None + +-def log_seek(f, ts, endpos = False): ++def seek_to_edge(f, ts, to_end): ++ ''' ++ f contains lines with exactly the timestamp ts. ++ Read forward (or backward) till we find the edge. ++ Linear search, but should be short. ++ ''' ++ if not to_end: ++ while ts == get_timestamp(f): ++ f.seek(-1000, 1) # go back 10 or so lines ++ while True: ++ pos = f.tell() ++ s = f.readline() ++ curr_ts = syslog_ts(s) ++ if (to_end and curr_ts > ts) or \ ++ (not to_end and curr_ts >= ts): ++ break ++ f.seek(pos) ++ ++def log_seek(f, ts, to_end = False): + ''' + f is an open log. Do binary search for the timestamp. + Return the position of the (more or less) first line with a +@@ -75,10 +93,11 @@ def log_seek(f, ts, endpos = False): + f.seek(0,2) + last = f.tell() + if not ts: +- return endpos and last or first ++ return to_end and last or first + badline = 0 + maxbadline = 10 +- common_debug("seek ts %s" % time.ctime(ts)) ++ common_debug("seek %s:%s in %s" % ++ (time.ctime(ts), to_end and "end" or "start", f.name)) + while first <= last: + # we can skip some iterations if it's about few lines + if abs(first-last) < 120: +@@ -98,9 +117,12 @@ def log_seek(f, ts, endpos = False): + elif log_ts < ts: + first = mid+1 + else: ++ seek_to_edge(f, log_ts, to_end) + break +- common_debug("sought to %s" % time.ctime(log_ts)) +- return f.tell() ++ fpos = f.tell() ++ common_debug("sought to %s (%d)" % (f.readline(), fpos)) ++ f.seek(fpos) ++ return fpos + + def get_timestamp(f): + ''' +@@ -187,7 +209,7 @@ class LogSyslog(object): + for log in self.f: + f = self.f[log] + start = log_seek(f, self.from_ts) +- end = log_seek(f, self.to_ts, endpos = True) ++ end = log_seek(f, self.to_ts, to_end = True) + if start == -1 or end == -1: + bad_logs.append(log) + else: +diff --git a/shell/modules/utils.py b/shell/modules/utils.py +--- a/shell/modules/utils.py ++++ b/shell/modules/utils.py +@@ -413,7 +413,10 @@ def run_ptest(graph_s, nograph, scores, + print get_stdout(ptest, input_s = graph_s) + #page_string(get_stdout(ptest, input_s = graph_s)) + if dotfile: +- show_dot_graph(dotfile) ++ if os.path.getsize(dotfile) > 0: ++ show_dot_graph(dotfile) ++ else: ++ common_warn("ptest produced empty dot file") + vars.tmpfiles.append(dotfile) + else: + if not nograph: diff --git a/crm_history_6_441f4448eba6.patch b/crm_history_6_441f4448eba6.patch new file mode 100644 index 0000000..64ca5c8 --- /dev/null +++ b/crm_history_6_441f4448eba6.patch @@ -0,0 +1,245 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1313081065 -7200 +# Node ID 441f4448eba6eda1a2cf44d3d63a0db9f8d56a20 +# Parent c3068d22de72d1ba616d43c808091bef830eb9f6 +Medium: Shell: reimplement the history latest command (bnc#710958) + +This command is going to show logs for the latest transition. +Basically, it's the same as "history transition", but it will +wait for the current (if any) transition to finish. + +(Also, the horrible transition command arg parsing has been +improved.) + +diff --git a/shell/modules/report.py b/shell/modules/report.py +--- a/shell/modules/report.py ++++ b/shell/modules/report.py +@@ -467,8 +467,7 @@ class Report(Singleton): + def is_last_live_recent(self): + ''' + Look at the last live hb_report. If it's recent enough, +- return True. Return True also if self.to_dt is not empty +- (not an open end report). ++ return True. + ''' + try: + last_ts = os.stat(self.desc).st_mtime +@@ -800,6 +799,7 @@ class Report(Singleton): + if self.source != "live": + self.error("refresh not supported") + return False ++ self.last_live_update = 0 + self.loc = self.manage_live_report(force) + self.report_setup() + self.ready = self.check_report() +@@ -884,18 +884,10 @@ class Report(Singleton): + print "Nodes:",' '.join(self.cibnode_l) + print "Groups:",' '.join(self.cibgrp_d.keys()) + print "Resources:",' '.join(self.cibrsc_l) +- def latest(self): +- ''' +- Get the very latest cluster events, basically from the +- latest transition. +- Some transitions may be empty though. +- ''' + def events(self): + ''' + Show all events. + ''' +- if not self.prepare_source(): +- return False + all_re_l = self.build_re("resource",self.cibrsc_l) + \ + self.build_re("node",self.cibnode_l) + if not all_re_l: +@@ -906,6 +898,8 @@ class Report(Singleton): + ''' + Search for events within the given transition. + ''' ++ if not self.prepare_source(): ++ return False + pe_base = os.path.basename(pe_file) + r = re.search("pe-[^-]+-([0-9]+)[.]", pe_base) + pe_num = r.group(1) +@@ -926,6 +920,9 @@ class Report(Singleton): + self.warn("strange, no timestamps found") + return False + # limit the log scope temporarily ++ common_info("logs for transition %s (%s-%s)" % ++ (pe_file.replace(self.loc+"/",""), \ ++ shorttime(start_ts), shorttime(end_ts))) + self.logobj.set_log_timeframe(start_ts, end_ts) + self.events() + self.logobj.set_log_timeframe(self.from_dt, self.to_dt) +@@ -994,6 +991,11 @@ class Report(Singleton): + 'Find a PE or dot file matching part of the path.' + pe_l = path.endswith(".dot") and self.dotlist() or self.pelist() + return [x for x in pe_l if x.endswith(path)] ++ def pe2dot(self, f): ++ f = f.replace("bz2","dot") ++ if os.path.isfile(f): ++ return f ++ return None + def find_file(self, f): + return file_find_by_name(self.loc, f) + +diff --git a/shell/modules/ui.py.in b/shell/modules/ui.py.in +--- a/shell/modules/ui.py.in ++++ b/shell/modules/ui.py.in +@@ -1796,22 +1796,15 @@ Examine Pacemaker's history: node and re + return crm_report.info() + def latest(self,cmd): + "usage: latest" +- try: +- prev_level = levels.previous().myname() +- except: +- prev_level = '' +- if prev_level != "cibconfig": +- common_err("%s is available only when invoked from configure" % cmd) +- return False +- ts = cib_factory.last_commit_at() +- if not ts: +- common_err("no last commit time found") +- return False + if not wait4dc("transition", not options.batch): + return False +- self._set_source("live", ts) ++ self._set_source("live") + crm_report.refresh_source() +- return crm_report.events() ++ f = self._get_pe_byidx(-1) ++ if not f: ++ common_err("no transitions found") ++ return False ++ crm_report.show_transition_log(f) + def resource(self,cmd,*args): + "usage: resource [ ...]" + return crm_report.resource(*args) +@@ -1853,6 +1846,30 @@ Examine Pacemaker's history: node and re + else: + s = '\n'.join(l) + page_string(s) ++ def _get_pe_byname(self, s): ++ l = crm_report.find_pe_files(s) ++ if len(l) == 0: ++ common_err("%s: path not found" % s) ++ return None ++ elif len(l) > 1: ++ common_err("%s: path ambiguous" % s) ++ return None ++ return l[0] ++ def _get_pe_byidx(self, idx): ++ l = crm_report.pelist() ++ if len(l) < abs(idx): ++ common_err("pe input file not found") ++ return None ++ return l[idx] ++ def _get_pe_bynum(self, n): ++ l = crm_report.pelist([n]) ++ if len(l) == 0: ++ common_err("%s: PE file %d not found" % n) ++ return None ++ elif len(l) > 1: ++ common_err("%s: PE file %d ambiguous" % n) ++ return None ++ return l[0] + def transition(self,cmd,*args): + """usage: transition [|] [nograph] [v...] [scores] [actions] [utilization] + transition showdot [|]""" +@@ -1864,48 +1881,35 @@ Examine Pacemaker's history: node and re + return False + subcmd = "showdot" + argl.remove(subcmd) +- f = None +- startarg = 1 +- if argl and re.search('pe-', argl[0]): +- l = crm_report.find_pe_files(argl[0]) +- if len(l) == 0: +- common_err("%s: path not found" % argl[0]) +- return False +- elif len(l) > 1: +- common_err("%s: path ambiguous" % argl[0]) +- return False +- f = l[0] ++ if argl: ++ if re.search('pe-', argl[0]): ++ f = self._get_pe_byname(argl[0]) ++ argl.pop(0) ++ elif is_int(argl[0]): ++ n = int(argl[0]) ++ if n <= 0: ++ f = self._get_pe_byidx(n-1) ++ else: ++ f = self._get_pe_bynum(n) ++ argl.pop(0) ++ else: ++ f = self._get_pe_byidx(-1) + else: +- try: n = convert2ints(argl[0]) +- except: n = None +- if n is None: +- idx = -1 +- startarg = 0 # peinput number missing +- elif n <= 0: +- idx = n - 1 +- n = [] # to get all peinputs +- else: +- idx = 0 +- if subcmd == "showdot": +- l = crm_report.dotlist(n) +- else: +- l = crm_report.pelist(n) +- if len(l) < abs(idx): +- if subcmd == "show": +- common_err("pe input file not found") +- else: +- common_err("dot file not found") +- return False +- f = l[idx] ++ f = self._get_pe_byidx(-1) + if not f: + return False + rc = True + if subcmd == "show": + self.pe_file = f # self.pe_file needed by self.ptest ++ common_info("running ptest with %s" % f) + rc = ptestlike(self.ptest,'vv',"%s %s" % \ +- (cmd, subcmd), *argl[startarg:]) ++ (cmd, subcmd), *argl) + else: +- show_dot_graph(f.replace("bz2","dot")) ++ f = crm_report.pe2dot(f) ++ if not f: ++ common_err("dot file not found in the report") ++ return False ++ show_dot_graph(f) + if rc: + crm_report.show_transition_log(f) + return rc +diff --git a/shell/modules/utils.py b/shell/modules/utils.py +--- a/shell/modules/utils.py ++++ b/shell/modules/utils.py +@@ -449,6 +449,9 @@ def check_range(a): + return False + return (int(a[0]) <= int(a[1])) + ++def shorttime(ts): ++ return time.strftime("%X",time.localtime(ts)) ++ + def sort_by_mtime(l): + 'Sort a (small) list of files by time mod.' + l2 = [(os.stat(x).st_mtime, x) for x in l] +@@ -489,6 +492,13 @@ def convert2ints(l): + else: # it's a string then + return int(l) + except: return None ++def is_int(s): ++ 'Check if the string can be converted to an integer.' ++ try: ++ i = int(s) ++ return True ++ except: ++ return False + + def is_process(s): + proc = subprocess.Popen("ps -e -o pid,command | grep -qs '%s'" % s, \ diff --git a/crm_history_7_3f3c348aaaed.patch b/crm_history_7_3f3c348aaaed.patch new file mode 100644 index 0000000..5c5aca2 --- /dev/null +++ b/crm_history_7_3f3c348aaaed.patch @@ -0,0 +1,207 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1313413824 -7200 +# Node ID 3f3c348aaaed52383f6646b08899943aec8911f4 +# Parent 441f4448eba6eda1a2cf44d3d63a0db9f8d56a20 +Medium: Shell: relax transition acceptance + +Sometimes logs are missing one or another transition related +message. Try to be more forgiving then. +Also, print information about number of actions which were +completed, skipped, etc. + +diff --git a/shell/modules/report.py b/shell/modules/report.py +--- a/shell/modules/report.py ++++ b/shell/modules/report.py +@@ -320,10 +320,8 @@ def is_log(p): + return os.path.isfile(p) and os.path.getsize(p) > 0 + + def pe_file_in_range(pe_f, a): +- r = re.search("pe-[^-]+-([0-9]+)[.]bz2$", pe_f) +- if not r: +- return None +- if not a or (a[0] <= int(r.group(1)) <= a[1]): ++ pe_num = get_pe_num(pe_f) ++ if not a or (a[0] <= int(pe_num) <= a[1]): + return pe_f + return None + +@@ -347,6 +345,12 @@ def update_loginfo(rptlog, logfile, oldp + except IOError, msg: + common_err("couldn't the update %s.info: %s" % (rptlog, msg)) + ++def get_pe_num(pe_file): ++ try: ++ return re.search("pe-[^-]+-([0-9]+)[.]", pe_file).group(1) ++ except: ++ return "-1" ++ + # r.group(1) transition number (a different thing from file number) + # r.group(2) contains full path + # r.group(3) file number +@@ -358,6 +362,40 @@ transition_patt = ( + "crmd: .* unpack_graph: Unpacked transition (%%): ([0-9]+) actions", # number of actions + ) + ++def run_graph_msg_actions(msg): ++ ''' ++ crmd: [13667]: info: run_graph: Transition 399 (Complete=5, ++ Pending=1, Fired=1, Skipped=0, Incomplete=3, ++ Source=... ++ ''' ++ d = {} ++ s = msg ++ while True: ++ r = re.search("([A-Z][a-z]+)=([0-9]+)", s) ++ if not r: ++ return d ++ d[r.group(1)] = int(r.group(2)) ++ s = s[r.end():] ++def transition_actions(msg_l, te_invoke_msg, pe_file): ++ ''' ++ Get the number of actions for the transition. ++ ''' ++ # check if there were any actions in this transition ++ pe_num = get_pe_num(pe_file) ++ te_invoke_patt = transition_patt[0].replace("%%", pe_num) ++ run_patt = transition_patt[1].replace("%%", pe_num) ++ r = re.search(te_invoke_patt, te_invoke_msg) ++ trans_num = r.group(1) ++ unpack_patt = transition_patt[2].replace("%%", trans_num) ++ for msg in msg_l: ++ try: ++ return int(re.search(unpack_patt, msg).group(2)) ++ except: ++ if re.search(run_patt, msg): ++ act_d = run_graph_msg_actions(msg) ++ return sum(act_d.values()) ++ return -1 ++ + class Report(Singleton): + ''' + A hb_report class. +@@ -396,8 +434,7 @@ class Report(Singleton): + def node_list(self): + return self.cibnode_l + def peinputs_list(self): +- return [re.search("pe-[^-]+-([0-9]+)[.]bz2$", x).group(1) +- for x in self.peinputs_l] ++ return [get_pe_num(x) for x in self.peinputs_l] + def unpack_report(self, tarball): + ''' + Unpack hb_report tarball. +@@ -712,8 +749,6 @@ class Report(Singleton): + ''' + Get a list of transitions. + Empty transitions are skipped. +- We use the unpack_graph message to see the number of +- actions. + Some callers need original PE file path (keep_pe_path), + otherwise we produce the path within the report. + If the caller doesn't provide the message list, then we +@@ -738,19 +773,12 @@ class Report(Singleton): + node = msg_a[3] + pe_file = msg_a[-1] + pe_base = os.path.basename(pe_file) +- # check if there were any actions in this transition +- r = re.search(trans_re_l[0], msg) +- trans_num = r.group(1) +- unpack_patt = transition_patt[2].replace("%%", trans_num) +- num_actions = 0 +- for t in msg_l: +- try: +- num_actions = int(re.search(unpack_patt, t).group(2)) +- break +- except: pass ++ num_actions = transition_actions(msg_l, msg, pe_file) + if num_actions == 0: # empty transition +- common_debug("skipping empty transition %s (%s)" % (trans_num, pe_base)) ++ common_debug("skipping empty transition (%s)" % pe_base) + continue ++ elif num_actions == -1: # couldn't find messages ++ common_warn("could not find number of actions for transition (%s)" % pe_base) + common_debug("found PE input at %s: %s" % (node, pe_file)) + if keep_pe_path: + pe_l.append(pe_file) +@@ -894,6 +922,34 @@ class Report(Singleton): + self.error("no resources or nodes found") + return False + self.show_logs(re_l = all_re_l) ++ def get_transition_msgs(self, pe_file, msg_l = []): ++ if not msg_l: ++ trans_re_l = [x.replace("%%", "[0-9]+") for x in transition_patt] ++ msg_l = self.logobj.get_matches(trans_re_l) ++ te_invoke_msg = "" ++ run_msg = "" ++ unpack_msg = "" ++ pe_num = get_pe_num(pe_file) ++ te_invoke_patt = transition_patt[0].replace("%%", pe_num) ++ run_patt = transition_patt[1].replace("%%", pe_num) ++ r = None ++ for msg in msg_l: ++ r = re.search(te_invoke_patt, msg) ++ if r: ++ te_invoke_msg = msg ++ break ++ if not r: ++ return ["", "", ""] ++ trans_num = r.group(1) ++ unpack_patt = transition_patt[2].replace("%%", trans_num) ++ for msg in msg_l: ++ if re.search(run_patt, msg): ++ run_msg = msg ++ elif re.search(unpack_patt, msg): ++ unpack_msg = msg ++ if run_msg and unpack_msg: ++ break ++ return [unpack_msg, te_invoke_msg, run_msg] + def show_transition_log(self, pe_file): + ''' + Search for events within the given transition. +@@ -901,28 +957,34 @@ class Report(Singleton): + if not self.prepare_source(): + return False + pe_base = os.path.basename(pe_file) +- r = re.search("pe-[^-]+-([0-9]+)[.]", pe_base) +- pe_num = r.group(1) +- trans_re_l = [x.replace("%%",pe_num) for x in transition_patt] +- trans_start = self.logobj.search_logs(self.log_l, trans_re_l[0]) +- trans_end = self.logobj.search_logs(self.log_l, trans_re_l[1]) +- if not trans_start: ++ pe_num = get_pe_num(pe_base) ++ unpack_msg, te_invoke_msg, run_msg = self.get_transition_msgs(pe_file) ++ if not te_invoke_msg: + common_warn("start of transition %s not found in logs" % pe_base) + return False +- if not trans_end: ++ if not run_msg: + common_warn("end of transition %s not found in logs (transition not complete yet?)" % pe_base) + return False +- common_debug("transition start: %s" % trans_start[0]) +- common_debug("transition end: %s" % trans_end[0]) +- start_ts = syslog_ts(trans_start[0]) +- end_ts = syslog_ts(trans_end[0]) ++ common_debug("transition start: %s" % te_invoke_msg) ++ common_debug("transition end: %s" % run_msg) ++ start_ts = syslog_ts(te_invoke_msg) ++ end_ts = syslog_ts(run_msg) + if not start_ts or not end_ts: + self.warn("strange, no timestamps found") + return False +- # limit the log scope temporarily ++ act_d = run_graph_msg_actions(run_msg) ++ total = sum(act_d.values()) ++ s = "" ++ for a in act_d: ++ if not act_d[a]: ++ continue ++ s = "%s %s=%d" % (s, a, act_d[a]) ++ common_info("transition %s %d actions: %s" % ++ (pe_file.replace(self.loc+"/",""), total, s)) + common_info("logs for transition %s (%s-%s)" % + (pe_file.replace(self.loc+"/",""), \ + shorttime(start_ts), shorttime(end_ts))) ++ # limit the log scope temporarily + self.logobj.set_log_timeframe(start_ts, end_ts) + self.events() + self.logobj.set_log_timeframe(self.from_dt, self.to_dt) diff --git a/crm_history_8_3681d3471fde.patch b/crm_history_8_3681d3471fde.patch new file mode 100644 index 0000000..997d6f6 --- /dev/null +++ b/crm_history_8_3681d3471fde.patch @@ -0,0 +1,34 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1313416746 -7200 +# Node ID 3681d3471fdecde109ea7c25ab2ceb31e1e8646f +# Parent 3f3c348aaaed52383f6646b08899943aec8911f4 +Low: Shell: update log patterns for history + +diff --git a/shell/modules/log_patterns.py b/shell/modules/log_patterns.py +--- a/shell/modules/log_patterns.py ++++ b/shell/modules/log_patterns.py +@@ -3,7 +3,7 @@ + # log pattern specification + # + # patterns are grouped one of several classes: +-# - resources: pertaining to a resource ++# - resource: pertaining to a resource + # - node: pertaining to a node + # - quorum: quorum changes + # - events: other interesting events (core dumps, etc) +@@ -17,12 +17,12 @@ + log_patterns = { + "resource": ( + ( # detail 0 +- "lrmd:.*rsc:%%.*(start|stop)", ++ "lrmd:.*rsc:%%.*(start|stop|promote|demote|migrate)", + "lrmd:.*RA output:.*%%.*stderr", + "lrmd:.*WARN:.*Managed.*%%.*exited", + ), + ( # detail 1 +- "lrmd:.*rsc:%%.*probe", ++ "lrmd:.*rsc:%%.*(probe|notify)", + "lrmd:.*info:.*Managed.*%%.*exited", + ), + ), diff --git a/crm_history_9_709ef91cfada.patch b/crm_history_9_709ef91cfada.patch new file mode 100644 index 0000000..5c44438 --- /dev/null +++ b/crm_history_9_709ef91cfada.patch @@ -0,0 +1,47 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1314196090 -7200 +# Node ID 709ef91cfada2822aca53dcef085ddb6952393c5 +# Parent 3a81b7eae66672dd9873fe6b53ee3c0da6fc87d7 +Low: Shell: update pe not found message + +diff --git a/shell/modules/ui.py.in b/shell/modules/ui.py.in +--- a/shell/modules/ui.py.in ++++ b/shell/modules/ui.py.in +@@ -1822,7 +1822,6 @@ Examine Pacemaker's history: node and re + crm_report.refresh_source() + f = self._get_pe_byidx(-1) + if not f: +- common_err("no transitions found") + return False + crm_report.show_transition_log(f) + def resource(self,cmd,*args): +@@ -1878,7 +1877,7 @@ Examine Pacemaker's history: node and re + def _get_pe_byidx(self, idx): + l = crm_report.pelist() + if len(l) < abs(idx): +- common_err("pe input file not found") ++ common_err("pe input file for index %d not found" % (idx+1)) + return None + return l[idx] + def _get_pe_bynum(self, n): +@@ -1913,7 +1912,8 @@ Examine Pacemaker's history: node and re + f = self._get_pe_bynum(n) + argl.pop(0) + else: +- f = self._get_pe_byidx(-1) ++ common_err("<%s> doesn't sound like a PE input" % argl[0]) ++ return False + else: + f = self._get_pe_byidx(-1) + if not f: +@@ -1922,8 +1922,7 @@ Examine Pacemaker's history: node and re + if subcmd == "show": + self.pe_file = f # self.pe_file needed by self.ptest + common_info("running ptest with %s" % f) +- rc = ptestlike(self.ptest,'vv',"%s %s" % \ +- (cmd, subcmd), *argl) ++ rc = ptestlike(self.ptest,'vv', cmd, *argl) + else: + f = crm_report.pe2dot(f) + if not f: diff --git a/crm_history_peinputs.patch b/crm_history_peinputs.patch new file mode 100644 index 0000000..91034f0 --- /dev/null +++ b/crm_history_peinputs.patch @@ -0,0 +1,315 @@ +changeset: 10788:6f9cc20dba0d +user: Dejan Muhamedagic +date: Mon Jul 18 12:35:57 2011 +0200 +summary: Dev: Shell: spawn transition command from peinputs + +diff -r b694b75d2e33 -r 6f9cc20dba0d doc/crm.8.txt +--- a/doc/crm.8.txt Mon Jul 18 12:35:57 2011 +0200 ++++ b/doc/crm.8.txt Mon Jul 18 12:35:57 2011 +0200 +@@ -2565,6 +2565,21 @@ were created (the DC at the time). The ` + all PE input files to the current working directory (and use ssh + if necessary). + ++Usage: ++............... ++ peinputs list [{|} ...] ++ peinputs get [{|} ...] ++ ++ range :: : ++............... ++Example: ++............... ++ peinputs get 440:444 446 ++............... ++ ++[[cmdhelp_history_transition,show transition]] ++==== `transition` ++ + The `show` subcommand will print actions planned by the PE and + run graphviz (`dotty`) to display a graphical representation. Of + course, for the latter an X11 session is required. This command +@@ -2581,22 +2596,20 @@ last one, i.e. the last transition. If t + then the corresponding transition relative to the last one is + chosen. + ++After the `ptest` output, logs about events that happened during ++the transition are printed. ++ + Usage: + ............... +- peinputs list [{|} ...] +- peinputs get [{|} ...] +- peinputs show [] [nograph] [v...] [scores] [actions] [utilization] +- peinputs showdot [] +- +- range :: : ++ transition show [] [nograph] [v...] [scores] [actions] [utilization] ++ transition showdot [] + ............... +-Example: ++Examples: + ............... +- peinputs get 440:444 446 +- peinputs show +- peinputs show 444 +- peinputs show -1 +- peinputs showdot 444 ++ transition show ++ transition show 444 ++ transition show -1 ++ transition showdot 444 + ............... + + === `end` (`cd`, `up`) +diff -r b694b75d2e33 -r 6f9cc20dba0d shell/modules/log_patterns.py +--- a/shell/modules/log_patterns.py Mon Jul 18 12:35:57 2011 +0200 ++++ b/shell/modules/log_patterns.py Mon Jul 18 12:35:57 2011 +0200 +@@ -64,6 +64,6 @@ log_patterns = { + } + + transition_patt = ( +- "crmd: .* Processing graph.*derived from (.*bz2)", # transition start +- "crmd: .* Transition.*Source=(.*bz2): (Stopped|Complete|Terminated)", # and stop ++ "crmd: .* Processing graph.*derived from .*/pe-[^-]+-(%%)[.]bz2", # transition start ++ "crmd: .* Transition.*Source=.*/pe-[^-]+-(%%)[.]bz2.: (Stopped|Complete|Terminated)", # and stop + ) +diff -r b694b75d2e33 -r 6f9cc20dba0d shell/modules/report.py +--- a/shell/modules/report.py Mon Jul 18 12:35:57 2011 +0200 ++++ b/shell/modules/report.py Mon Jul 18 12:35:57 2011 +0200 +@@ -177,8 +177,12 @@ class LogSyslog(object): + find out start/end file positions. Logs need to be + already open. + ''' +- self.from_ts = convert_dt(from_dt) +- self.to_ts = convert_dt(to_dt) ++ if isinstance(from_dt, datetime.datetime): ++ self.from_ts = convert_dt(from_dt) ++ self.to_ts = convert_dt(to_dt) ++ else: ++ self.from_ts = from_dt ++ self.to_ts = to_dt + bad_logs = [] + for log in self.f: + f = self.f[log] +@@ -498,13 +502,14 @@ class Report(Singleton): + if not os.path.isdir(self.outdir): + return [] + l = [] ++ trans_re_l = [x.replace("%%","") for x in transition_patt] + for node,rptlog,logfile,nextpos in a: + node_l = [] + fl = glob.glob("%s/*%s*" % (self.outdir,node)) + if not fl: + continue + for s in file2list(fl[0]): +- r = re.search(transition_patt[0], s) ++ r = re.search(trans_re_l[0], s) + if not r: + continue + node_l.append(r.group(1)) +@@ -680,7 +685,8 @@ class Report(Singleton): + self.find_central_log() + self.read_cib() + self.set_node_colors() +- self.logobj = None ++ self.logobj = LogSyslog(self.central_log, self.log_l, \ ++ self.from_dt, self.to_dt) + def prepare_source(self): + ''' + Unpack a hb_report tarball. +@@ -740,6 +746,15 @@ class Report(Singleton): + try: clr = self.nodecolor[a[3]] + except: return s + return termctrl.render("${%s}%s${NORMAL}" % (clr,s)) ++ def display_logs(self, l): ++ if not options.batch and sys.stdout.isatty(): ++ page_string('\n'.join([ self.disp(x) for x in l ])) ++ else: # raw output ++ try: # in case user quits the next prog in pipe ++ for s in l: print s ++ except IOError, msg: ++ if not ("Broken pipe" in msg): ++ common_err(msg) + def show_logs(self, log_l = [], re_l = []): + ''' + Print log lines, either matched by re_l or all. +@@ -749,18 +764,7 @@ class Report(Singleton): + if not self.central_log and not log_l: + self.error("no logs found") + return +- if not self.logobj: +- self.logobj = LogSyslog(self.central_log, log_l, \ +- self.from_dt, self.to_dt) +- l = self.logobj.get_matches(re_l, log_l) +- if not options.batch and sys.stdout.isatty(): +- page_string('\n'.join([ self.disp(x) for x in l ])) +- else: # raw output +- try: # in case user quits the next prog in pipe +- for s in l: print s +- except IOError, msg: +- if not ("Broken pipe" in msg): +- common_err(msg) ++ self.display_logs(self.logobj.get_matches(re_l, log_l)) + def match_args(self, cib_l, args): + for a in args: + a_clone = re.sub(r':.*', '', a) +@@ -812,6 +816,34 @@ class Report(Singleton): + self.error("no resources or nodes found") + return False + self.show_logs(re_l = all_re_l) ++ def show_transition_log(self, pe_file): ++ ''' ++ Search for events within the given transition. ++ ''' ++ pe_base = os.path.basename(pe_file) ++ r = re.search("pe-[^-]+-([0-9]+)[.]bz2", pe_base) ++ pe_num = r.group(1) ++ trans_re_l = [x.replace("%%",pe_num) for x in transition_patt] ++ trans_start = self.logobj.search_logs(self.log_l, trans_re_l[0]) ++ trans_end = self.logobj.search_logs(self.log_l, trans_re_l[1]) ++ if not trans_start: ++ common_warn("transition %s start not found in logs" % pe_base) ++ return False ++ if not trans_end: ++ common_warn("transition %s end not found in logs" % pe_base) ++ return False ++ common_debug("transition start: %s" % trans_start[0]) ++ common_debug("transition end: %s" % trans_end[0]) ++ start_ts = syslog_ts(trans_start[0]) ++ end_ts = syslog_ts(trans_end[0]) ++ if not start_ts or not end_ts: ++ self.warn("strange, no timestamps found") ++ return False ++ # limit the log scope temporarily ++ self.logobj.set_log_timeframe(start_ts, end_ts) ++ self.events() ++ self.logobj.set_log_timeframe(self.from_dt, self.to_dt) ++ return True + def resource(self,*args): + ''' + Show resource relevant logs. +diff -r b694b75d2e33 -r 6f9cc20dba0d shell/modules/ui.py.in +--- a/shell/modules/ui.py.in Mon Jul 18 12:35:57 2011 +0200 ++++ b/shell/modules/ui.py.in Mon Jul 18 12:35:57 2011 +0200 +@@ -1686,7 +1686,8 @@ Examine Pacemaker's history: node and re + self.cmd_table["resource"] = (self.resource,(1,),1,0) + self.cmd_table["node"] = (self.node,(1,),1,1) + self.cmd_table["log"] = (self.log,(0,),1,0) +- self.cmd_table["peinputs"] = (self.peinputs,(0,),1,0) ++ self.cmd_table["peinputs"] = (self.peinputs,(1,),1,0) ++ self.cmd_table["transition"] = (self.transition,(1,),1,0) + self._set_source(options.history) + def _no_source(self): + common_error("we have no source set yet! please use the source command") +@@ -1832,57 +1833,63 @@ Examine Pacemaker's history: node and re + return run_ptest(s, nograph, scores, utilization, actions, verbosity) + def peinputs(self,cmd,subcmd,*args): + """usage: peinputs list [{|} ...] +- peinputs get [{|} ...] +- peinputs show [] [nograph] [v...] [scores] [actions] [utilization] +- peinputs showdot []""" +- if subcmd in ("get","list"): +- if args: +- l = [] +- for s in args: +- a = convert2ints(s.split(':')) +- if len(a) == 2 and not check_range(a): +- common_err("%s: invalid peinputs range" % a) +- return False +- l += crm_report.pelist(a) +- else: +- l = crm_report.pelist() +- if not l: return False +- if subcmd == "list": +- s = get_stdout("ls -lrt %s" % ' '.join(l)) +- page_string(s) +- else: +- print '\n'.join(l) +- elif subcmd in ("show","showdot"): +- try: n = convert2ints(args[0]) +- except: n = None +- startarg = 1 +- if n is None: +- idx = -1 +- startarg = 0 # peinput number missing +- elif n <= 0: +- idx = n - 1 +- n = [] # to get all peinputs +- else: +- idx = 0 +- if subcmd == "showdot": +- if not user_prefs.dotty: +- common_err("install graphviz to draw transition graphs") ++ peinputs get [{|} ...]""" ++ if subcmd not in ("get","list"): ++ bad_usage(cmd,subcmd) ++ return False ++ if args: ++ l = [] ++ for s in args: ++ a = convert2ints(s.split(':')) ++ if len(a) == 2 and not check_range(a): ++ common_err("%s: invalid peinputs range" % a) + return False +- l = crm_report.dotlist(n) +- else: +- l = crm_report.pelist(n) +- if len(l) < abs(idx): +- common_err("pe input or dot file not found") ++ l += crm_report.pelist(a) ++ else: ++ l = crm_report.pelist() ++ if not l: return False ++ if subcmd == "list": ++ s = get_stdout("ls -lrt %s" % ' '.join(l)) ++ page_string(s) ++ else: ++ print '\n'.join(l) ++ def transition(self,cmd,subcmd,*args): ++ """usage: transition show [] [nograph] [v...] [scores] [actions] [utilization] ++ transition showdot []""" ++ if subcmd not in ("show", "showdot"): ++ bad_usage(cmd,subcmd) ++ return False ++ try: n = convert2ints(args[0]) ++ except: n = None ++ startarg = 1 ++ if n is None: ++ idx = -1 ++ startarg = 0 # peinput number missing ++ elif n <= 0: ++ idx = n - 1 ++ n = [] # to get all peinputs ++ else: ++ idx = 0 ++ if subcmd == "showdot": ++ if not user_prefs.dotty: ++ common_err("install graphviz to draw transition graphs") + return False +- if subcmd == "show": +- self.pe_file = l[idx] +- return ptestlike(self.ptest,'vv',"%s %s" % \ +- (cmd, subcmd), *args[startarg:]) +- else: +- show_dot_graph(l[idx]) ++ l = crm_report.dotlist(n) + else: +- bad_usage(cmd,' '.join(subcmd,args)) ++ l = crm_report.pelist(n) ++ if len(l) < abs(idx): ++ common_err("pe input or dot file not found") + return False ++ rc = True ++ if subcmd == "show": ++ self.pe_file = l[idx] ++ rc = ptestlike(self.ptest,'vv',"%s %s" % \ ++ (cmd, subcmd), *args[startarg:]) ++ if rc: ++ crm_report.show_transition_log(self.pe_file) ++ else: ++ show_dot_graph(l[idx]) ++ return rc + + class TopLevel(UserInterface): + ''' + diff --git a/crm_history_pssh.patch b/crm_history_pssh.patch new file mode 100644 index 0000000..f98f681 --- /dev/null +++ b/crm_history_pssh.patch @@ -0,0 +1,12 @@ +Index: pacemaker/shell/modules/Makefile.am +=================================================================== +--- pacemaker.orig/shell/modules/Makefile.am ++++ pacemaker/shell/modules/Makefile.am +@@ -35,6 +35,7 @@ modules = __init__.py \ + ra.py \ + report.py \ + log_patterns.py \ ++ crm_pssh.py \ + singletonmixin.py \ + template.py \ + term.py \ diff --git a/crm_lrmsecrets_3a81b7eae666.patch b/crm_lrmsecrets_3a81b7eae666.patch new file mode 100644 index 0000000..84ffafd --- /dev/null +++ b/crm_lrmsecrets_3a81b7eae666.patch @@ -0,0 +1,98 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1313760016 -7200 +# Node ID 3a81b7eae66672dd9873fe6b53ee3c0da6fc87d7 +# Parent e8ea8fb95f310997995576ee831693b0d3b2736a +Medium: Shell: support for LRM secrets in resource level + +diff --git a/doc/crm.8.txt b/doc/crm.8.txt +--- a/doc/crm.8.txt ++++ b/doc/crm.8.txt +@@ -869,6 +869,34 @@ Example: + param ip_0 show ip + ............... + ++[[cmdhelp_resource_secret,manage sensitive parameters]] ++==== `secret` ++ ++Sensitive parameters can be kept in local files rather than CIB ++in order to prevent accidental data exposure. Use the `secret` ++command to manage such parameters. `stash` and `unstash` move the ++value from the CIB and back to the CIB respectively. The `set` ++subcommand sets the parameter to the provided value. `delete` ++removes the parameter completely. `show` displays the value of ++the parameter from the local file. Use `check` to verify if the ++local file content is valid. ++ ++Usage: ++............... ++ secret set ++ secret stash ++ secret unstash ++ secret delete ++ secret show ++ secret check ++............... ++Example: ++............... ++ secret fence_1 show password ++ secret fence_1 stash password ++ secret fence_1 set password secret_value ++............... ++ + [[cmdhelp_resource_meta,manage a meta attribute]] + ==== `meta` + +diff --git a/shell/modules/ui.py.in b/shell/modules/ui.py.in +--- a/shell/modules/ui.py.in ++++ b/shell/modules/ui.py.in +@@ -661,7 +661,8 @@ def manage_attr(cmd,attr_ext_commands,*a + else: + bad_usage(cmd,' '.join(args)) + return False +- elif args[1] in ('delete','show'): ++ elif args[1] in ('delete','show') or \ ++ (cmd == "secret" and args[1] in ('stash','unstash','check')): + if len(args) == 3: + if not is_name_sane(args[0]) \ + or not is_name_sane(args[2]): +@@ -770,6 +771,14 @@ program. + 'delete': "crm_resource -z -r '%s' -d '%s'", + 'show': "crm_resource -z -r '%s' -g '%s'", + } ++ rsc_secret = { ++ 'set': "cibsecret set '%s' '%s' '%s'", ++ 'stash': "cibsecret stash '%s' '%s'", ++ 'unstash': "cibsecret unstash '%s' '%s'", ++ 'delete': "cibsecret delete '%s' '%s'", ++ 'show': "cibsecret get '%s' '%s'", ++ 'check': "cibsecret check '%s' '%s'", ++ } + rsc_refresh = "crm_resource -R" + rsc_refresh_node = "crm_resource -R -H '%s'" + rsc_reprobe = "crm_resource -P" +@@ -787,6 +796,7 @@ program. + self.cmd_table["migrate"] = (self.migrate,(1,4),0,1) + self.cmd_table["unmigrate"] = (self.unmigrate,(1,1),0,1) + self.cmd_table["param"] = (self.param,(3,4),1,1) ++ self.cmd_table["secret"] = (self.secret,(3,4),1,1) + self.cmd_table["meta"] = (self.meta,(3,4),1,1) + self.cmd_table["utilization"] = (self.utilization,(3,4),1,1) + self.cmd_table["failcount"] = (self.failcount,(3,4),0,0) +@@ -924,6 +934,16 @@ program. + param show """ + d = lambda: manage_attr(cmd,self.rsc_param,*args) + return d() ++ def secret(self,cmd,*args): ++ """usage: ++ secret set ++ secret stash ++ secret unstash ++ secret delete ++ secret show ++ secret check """ ++ d = lambda: manage_attr(cmd,self.rsc_secret,*args) ++ return d() + def meta(self,cmd,*args): + """usage: + meta set diff --git a/crm_pager_f77e52725f2d.patch b/crm_pager_f77e52725f2d.patch new file mode 100644 index 0000000..be59314 --- /dev/null +++ b/crm_pager_f77e52725f2d.patch @@ -0,0 +1,28 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1314633641 -7200 +# Node ID f77e52725f2d98c219d8b22208da0b89b3d42112 +# Parent ccd0c1e1edf9f23cafb4363014acba755f1b4e25 +Low: Shell: let the pager decide how to handle output smaller than terminal + +Instead of trying to calculate the size of the output, which may +not be trivial, better let the pager deal with it. For instance, +less(1) can be configured to exit immediately on a +less-than-screenful of input (-F). IIRC, more(1) does that +automatically. + +diff --git a/shell/modules/utils.py b/shell/modules/utils.py +--- a/shell/modules/utils.py ++++ b/shell/modules/utils.py +@@ -524,10 +524,7 @@ def page_string(s): + 'Write string through a pager.' + if not s: + return +- w,h = get_winsize() +- if s.count('\n') < h: +- print s +- elif not user_prefs.pager or not sys.stdout.isatty() or options.batch: ++ if not user_prefs.pager or not sys.stdout.isatty() or options.batch: + print s + else: + opts = "" diff --git a/crm_path_bnc712605.patch b/crm_path_bnc712605.patch new file mode 100644 index 0000000..8e614a2 --- /dev/null +++ b/crm_path_bnc712605.patch @@ -0,0 +1,34 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1313589488 -7200 +# Node ID 0abb257259ed722abaa32a237c3c284c08ec0737 +# Parent 3681d3471fdecde109ea7c25ab2ceb31e1e8646f +Low: Shell: add crm execute directory to the PATH if not already present (bnc#712605) + +Important if crm is run as non-root user. We use sys.argv[0], +but perhaps it'd be better to use autoconf @sbindir@ (or however +it's named) and set it in vars.sbindir. + +diff --git a/shell/modules/main.py b/shell/modules/main.py +--- a/shell/modules/main.py ++++ b/shell/modules/main.py +@@ -16,6 +16,7 @@ + # + + import sys ++import os + import shlex + import getopt + +@@ -205,7 +206,10 @@ vars = Vars.getInstance() + levels = Levels.getInstance() + + # prefer the user set PATH +-os.putenv("PATH", "%s:%s" % (os.getenv("PATH"),vars.crm_daemon_dir)) ++mybinpath = os.path.dirname(sys.argv[0]) ++for p in mybinpath, vars.crm_daemon_dir: ++ if p not in os.environ["PATH"].split(':'): ++ os.environ['PATH'] = "%s:%s" % (os.environ['PATH'], p) + + def set_interactive(): + '''Set the interactive option only if we're on a tty.''' diff --git a/crm_site_9b07d41c73b4.patch b/crm_site_9b07d41c73b4.patch new file mode 100644 index 0000000..c41d332 --- /dev/null +++ b/crm_site_9b07d41c73b4.patch @@ -0,0 +1,148 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1314872213 -7200 +# Node ID 9b07d41c73b456e8189fea757a5c3d9e5b32512d +# Parent 825cb3e79d7bc1c4ac30468f8c028c9129d00541 +High: Shell: geo-cluster support commands + +diff --git a/doc/crm.8.txt b/doc/crm.8.txt +--- a/doc/crm.8.txt ++++ b/doc/crm.8.txt +@@ -1133,6 +1133,31 @@ Example: + status-attr node_1 show pingd + ............... + ++[[cmdhelp_site,site support]] ++=== `site` ++ ++A cluster may consist of two or more subclusters in different and ++distant locations. This set of commands supports such setups. ++ ++[[cmdhelp_site_ticket,manage site tickets]] ++==== `ticket` ++ ++Tickets are cluster-wide attributes. They can be managed at the ++site where this command is executed. ++ ++It is then possible to constrain resources depending on the ++ticket availability (see the <> command ++for more details). ++ ++Usage: ++............... ++ ticket {grant|revoke|show|time|delete} ++............... ++Example: ++............... ++ ticket grant ticket1 ++............... ++ + [[cmdhelp_options,user preferences]] + === `options` + +@@ -1652,6 +1677,8 @@ resource (or resources) if the ticket is + either `stop` or `demote` depending on whether a resource is + multi-state. + ++See also the <> set of commands. ++ + Usage: + ............... + rsc_ticket : [:] [[:] ...] +diff --git a/shell/modules/completion.py b/shell/modules/completion.py +--- a/shell/modules/completion.py ++++ b/shell/modules/completion.py +@@ -173,6 +173,10 @@ def report_pe_list_peinputs(idx,delimite + if delimiter: + return ' ' + return crm_report.peinputs_list() + ["v"] ++def ticket_cmd_list(idx,delimiter = False): ++ if delimiter: ++ return ' ' ++ return ["grant","revoke","show","time","delete"] + + # + # completion for primitives including help for parameters +@@ -488,6 +492,9 @@ completer_lists = { + "peinputs" : (report_pe_list_peinputs,loop), + "transition" : (report_pe_list_transition,), + }, ++ "site" : { ++ "ticket" : (ticket_cmd_list,), ++ }, + } + def get_completer_list(level,cmd): + 'Return a list of completer functions.' +diff --git a/shell/modules/ui.py.in b/shell/modules/ui.py.in +--- a/shell/modules/ui.py.in ++++ b/shell/modules/ui.py.in +@@ -1938,6 +1938,61 @@ Examine Pacemaker's history: node and re + crm_report.show_transition_log(f) + return rc + ++class Site(UserInterface): ++ ''' ++ The site class ++ ''' ++ lvl_name = "site" ++ desc_short = "Geo-cluster support" ++ desc_long = """ ++The site level. ++ ++Geo-cluster related management. ++""" ++ crm_ticket = { ++ 'grant': "crm_ticket -t '%s' -v true", ++ 'revoke': "crm_ticket -t '%s' -v false", ++ 'delete': "crm_ticket -t '%s' -D", ++ 'show': "crm_ticket -t '%s' -G", ++ 'time': "crm_ticket -t '%s' -T", ++ } ++ def __init__(self): ++ UserInterface.__init__(self) ++ self.cmd_table["ticket"] = (self.ticket,(2,2),1,0) ++ def ticket(self, cmd, subcmd, ticket): ++ "usage: ticket {grant|revoke|show|time|delete} " ++ try: ++ attr_cmd = self.crm_ticket[subcmd] ++ except: ++ bad_usage(cmd,'%s %s' % (subcmd, ticket)) ++ return False ++ if not is_name_sane(ticket): ++ return False ++ if subcmd not in ("show", "time"): ++ return ext_cmd(attr_cmd % ticket) == 0 ++ l = stdout2list(attr_cmd % ticket) ++ try: ++ val = l[0].split('=')[3] ++ except: ++ common_warn("apparently nothing to show for ticket %s" % ticket) ++ return False ++ if subcmd == "show": ++ if val == "false": ++ print "ticket %s is revoked" % ticket ++ elif val == "true": ++ print "ticket %s is granted" % ticket ++ else: ++ common_warn("unexpected value for ticket %s: %s" % (ticket, val)) ++ return False ++ else: # time ++ if not is_int(val): ++ common_warn("unexpected value for ticket %s: %s" % (ticket, val)) ++ return False ++ if val == "-1": ++ print "%s: no such ticket" % ticket ++ return False ++ print "ticket %s last time granted on %s" % (ticket, time.ctime(int(val))) ++ + class TopLevel(UserInterface): + ''' + The top level. +@@ -1959,6 +2014,7 @@ class TopLevel(UserInterface): + self.cmd_table['node'] = NodeMgmt + self.cmd_table['options'] = CliOptions + self.cmd_table['history'] = History ++ self.cmd_table['site'] = Site + self.cmd_table['status'] = (self.status,(0,5),0,0) + self.cmd_table['ra'] = RA + setup_aliases(self) diff --git a/crm_tickets_825cb3e79d7b.patch b/crm_tickets_825cb3e79d7b.patch new file mode 100644 index 0000000..fe01811 --- /dev/null +++ b/crm_tickets_825cb3e79d7b.patch @@ -0,0 +1,326 @@ +# HG changeset patch +# User Dejan Muhamedagic +# Date 1314783705 -7200 +# Node ID 825cb3e79d7bc1c4ac30468f8c028c9129d00541 +# Parent f77e52725f2d98c219d8b22208da0b89b3d42112 +High: Shell: support for rsc_ticket + +diff --git a/doc/crm.8.txt b/doc/crm.8.txt +--- a/doc/crm.8.txt ++++ b/doc/crm.8.txt +@@ -1639,6 +1639,34 @@ Example: + order o1 inf: A ( B C ) + ............... + ++[[cmdhelp_configure_rsc_ticket,resources ticket dependency]] ++==== `rsc_ticket` ++ ++This constraint expresses dependency of resources on cluster-wide ++attributes, also known as tickets. Tickets are mainly used in ++geo-clusters, which consist of multiple sites. A ticket may be ++granted to a site, thus allowing resources to run there. ++ ++The `loss-policy` attribute specifies what happens to the ++resource (or resources) if the ticket is revoked. The default is ++either `stop` or `demote` depending on whether a resource is ++multi-state. ++ ++Usage: ++............... ++ rsc_ticket : [:] [[:] ...] ++ [loss-policy=] ++ ++ loss_policy_action :: stop | demote | fence | freeze ++............... ++Example: ++............... ++ rsc_ticket ticket-A_public-ip ticket-A: public-ip ++ rsc_ticket ticket-A_bigdb ticket-A: bigdb loss-policy=fence ++ rsc_ticket ticket-B_storage ticket-B: drbd-a:Master drbd-b:Master ++............... ++ ++ + [[cmdhelp_configure_property,set a cluster property]] + ==== `property` + +diff --git a/shell/modules/cibconfig.py b/shell/modules/cibconfig.py +--- a/shell/modules/cibconfig.py ++++ b/shell/modules/cibconfig.py +@@ -1243,7 +1243,7 @@ class CibSimpleConstraint(CibObject): + if node.getElementsByTagName("resource_set"): + col = rsc_set_constraint(node,obj_type) + else: +- col = two_rsc_constraint(node,obj_type) ++ col = simple_rsc_constraint(node,obj_type) + if not col: + return None + symm = node.getAttribute("symmetrical") +@@ -1264,6 +1264,27 @@ class CibSimpleConstraint(CibObject): + remove_id_used_attributes(oldnode) + return headnode + ++class CibRscTicket(CibSimpleConstraint): ++ ''' ++ rsc_ticket constraint. ++ ''' ++ def repr_cli_head(self,node): ++ obj_type = vars.cib_cli_map[node.tagName] ++ node_id = node.getAttribute("id") ++ s = cli_display.keyword(obj_type) ++ id = cli_display.id(node_id) ++ ticket = cli_display.ticket(node.getAttribute("ticket")) ++ if node.getElementsByTagName("resource_set"): ++ col = rsc_set_constraint(node,obj_type) ++ else: ++ col = simple_rsc_constraint(node,obj_type) ++ if not col: ++ return None ++ a = node.getAttribute("loss-policy") ++ if a: ++ col.append("loss-policy=%s" % a) ++ return "%s %s %s: %s" % (s,id,ticket,' '.join(col)) ++ + class CibProperty(CibObject): + ''' + Cluster properties. +@@ -1371,6 +1392,7 @@ cib_object_map = { + "rsc_location": ( "location", CibLocation, "constraints" ), + "rsc_colocation": ( "colocation", CibSimpleConstraint, "constraints" ), + "rsc_order": ( "order", CibSimpleConstraint, "constraints" ), ++ "rsc_ticket": ( "rsc_ticket", CibRscTicket, "constraints" ), + "cluster_property_set": ( "property", CibProperty, "crm_config", "cib-bootstrap-options" ), + "rsc_defaults": ( "rsc_defaults", CibProperty, "rsc_defaults", "rsc-options" ), + "op_defaults": ( "op_defaults", CibProperty, "op_defaults", "op-options" ), +diff --git a/shell/modules/clidisplay.py b/shell/modules/clidisplay.py +--- a/shell/modules/clidisplay.py ++++ b/shell/modules/clidisplay.py +@@ -62,6 +62,8 @@ class CliDisplay(Singleton): + return self.otherword(4, s) + def score(self, s): + return self.otherword(5, s) ++ def ticket(self, s): ++ return self.otherword(5, s) + + user_prefs = UserPrefs.getInstance() + vars = Vars.getInstance() +diff --git a/shell/modules/cliformat.py b/shell/modules/cliformat.py +--- a/shell/modules/cliformat.py ++++ b/shell/modules/cliformat.py +@@ -226,22 +226,25 @@ def rsc_set_constraint(node,obj_type): + action = n.getAttribute("action") + for r in n.getElementsByTagName("resource_ref"): + rsc = cli_display.rscref(r.getAttribute("id")) +- q = (obj_type == "colocation") and role or action ++ q = (obj_type == "order") and action or role + col.append(q and "%s:%s"%(rsc,q) or rsc) + cnt += 1 + if not sequential: + col.append(")") +- if cnt <= 2: # a degenerate thingie ++ if (obj_type != "rsc_ticket" and cnt <= 2) or \ ++ (obj_type == "rsc_ticket" and cnt <= 1): # a degenerate thingie + col.insert(0,"_rsc_set_") + return col +-def two_rsc_constraint(node,obj_type): ++def simple_rsc_constraint(node,obj_type): + col = [] + if obj_type == "colocation": + col.append(mkrscrole(node,"rsc")) + col.append(mkrscrole(node,"with-rsc")) +- else: ++ elif obj_type == "order": + col.append(mkrscaction(node,"first")) + col.append(mkrscaction(node,"then")) ++ else: # rsc_ticket ++ col.append(mkrscrole(node,"rsc")) + return col + + # this pre (or post)-processing is oversimplified +diff --git a/shell/modules/completion.py b/shell/modules/completion.py +--- a/shell/modules/completion.py ++++ b/shell/modules/completion.py +@@ -467,6 +467,7 @@ completer_lists = { + "location" : (null_list,rsc_id_list), + "colocation" : (null_list,null_list,rsc_id_list,loop), + "order" : (null_list,null_list,rsc_id_list,loop), ++ "rsc_ticket" : (null_list,null_list,rsc_id_list,loop), + "property" : (property_complete,loop), + "rsc_defaults" : (prim_complete_meta,loop), + "op_defaults" : (op_attr_list,loop), +diff --git a/shell/modules/parse.py b/shell/modules/parse.py +--- a/shell/modules/parse.py ++++ b/shell/modules/parse.py +@@ -178,6 +178,15 @@ def parse_op(s): + head_pl.append(["name",s[0]]) + return cli_list + ++def cli_parse_ticket(ticket,pl): ++ if ticket.endswith(':'): ++ ticket = ticket.rstrip(':') ++ else: ++ syntax_err(ticket, context = 'rsc_ticket') ++ return False ++ pl.append(["ticket",ticket]) ++ return True ++ + def cli_parse_score(score,pl,noattr = False): + if score.endswith(':'): + score = score.rstrip(':') +@@ -197,6 +206,7 @@ def cli_parse_score(score,pl,noattr = Fa + else: + pl.append(["score-attribute",score]) + return True ++ + def is_binary_op(s): + l = s.split(':') + if len(l) == 2: +@@ -302,13 +312,13 @@ def parse_location(s): + return False + return cli_list + +-def cli_opt_symmetrical(p,pl): ++def cli_opt_attribute(type, p, pl, attr): + if not p: + return True + pl1 = [] + cli_parse_attr([p],pl1) +- if len(pl1) != 1 or not find_value(pl1,"symmetrical"): +- syntax_err(p,context = "order") ++ if len(pl1) != 1 or not find_value(pl1, attr): ++ syntax_err(p,context = type) + return False + pl += pl1 + return True +@@ -490,7 +500,33 @@ def parse_order(s): + resource_set_obj = ResourceSet(type,s[3:],cli_list) + if not resource_set_obj.parse(): + return False +- if not cli_opt_symmetrical(symm,head_pl): ++ if not cli_opt_attribute(type, symm, head_pl, "symmetrical"): ++ return False ++ return cli_list ++ ++def parse_rsc_ticket(s): ++ cli_list = [] ++ head_pl = [] ++ type = "rsc_ticket" ++ cli_list.append([s[0],head_pl]) ++ if len(s) < 4: ++ syntax_err(s,context = "rsc_ticket") ++ return False ++ head_pl.append(["id",s[1]]) ++ if not cli_parse_ticket(s[2],head_pl): ++ return False ++ # save loss-policy for later (if it exists) ++ loss_policy = "" ++ if is_attribute(s[len(s)-1],"loss-policy"): ++ loss_policy = s.pop() ++ if len(s) == 4: ++ if not cli_parse_rsc_role(s[3], head_pl): ++ return False ++ else: ++ resource_set_obj = ResourceSet(type, s[3:], cli_list) ++ if not resource_set_obj.parse(): ++ return False ++ if not cli_opt_attribute(type, loss_policy, head_pl, attr = "loss-policy"): + return False + return cli_list + +@@ -501,6 +537,8 @@ def parse_constraint(s): + return parse_colocation(s) + elif keyword_cmp(s[0], "order"): + return parse_order(s) ++ elif keyword_cmp(s[0], "rsc_ticket"): ++ return parse_rsc_ticket(s) + def parse_property(s): + cli_list = [] + head_pl = [] +@@ -708,6 +746,7 @@ class CliParser(object): + "colocation": (3,parse_constraint), + "collocation": (3,parse_constraint), + "order": (3,parse_constraint), ++ "rsc_ticket": (3,parse_constraint), + "monitor": (3,parse_op), + "node": (2,parse_node), + "property": (2,parse_property), +diff --git a/shell/modules/ui.py.in b/shell/modules/ui.py.in +--- a/shell/modules/ui.py.in ++++ b/shell/modules/ui.py.in +@@ -1400,6 +1400,7 @@ cluster. + self.cmd_table["location"] = (self.conf_location,(2,),1,0) + self.cmd_table["colocation"] = (self.conf_colocation,(2,),1,0) + self.cmd_table["order"] = (self.conf_order,(2,),1,0) ++ self.cmd_table["rsc_ticket"] = (self.conf_rsc_ticket,(2,),1,0) + self.cmd_table["property"] = (self.conf_property,(1,),1,0) + self.cmd_table["rsc_defaults"] = (self.conf_rsc_defaults,(1,),1,0) + self.cmd_table["op_defaults"] = (self.conf_op_defaults,(1,),1,0) +@@ -1632,6 +1633,10 @@ cluster. + """usage: order score-type: [:] [:] + [symmetrical=]""" + return self.__conf_object(cmd,*args) ++ def conf_rsc_ticket(self,cmd,*args): ++ """usage: rsc_ticket : [:] [[:] ...] ++ [loss-policy=]""" ++ return self.__conf_object(cmd,*args) + def conf_property(self,cmd,*args): + "usage: property [$id=]