2214 lines
74 KiB
Diff
2214 lines
74 KiB
Diff
|
changeset: 10787:b694b75d2e33
|
||
|
user: Dejan Muhamedagic <dejan@hello-penguin.com>
|
||
|
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 <from_time> [<to_time>]
|
||
|
+...............
|
||
|
+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 [<dir>|<file>|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>
|
||
|
+
|
||
|
+ 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 <node> [<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 <rsc> [<rsc> ...]
|
||
|
+...............
|
||
|
+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 <node> [<node> ...]
|
||
|
+...............
|
||
|
+Example:
|
||
|
+...............
|
||
|
+ node node1
|
||
|
+...............
|
||
|
+
|
||
|
+[[cmdhelp_history_log,log content]]
|
||
|
+==== `log`
|
||
|
+
|
||
|
+Show logs for a node or combined logs of all nodes.
|
||
|
+
|
||
|
+Usage:
|
||
|
+...............
|
||
|
+ log [<node>]
|
||
|
+...............
|
||
|
+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 [{<range>|<number>} ...]
|
||
|
+ peinputs get [{<range>|<number>} ...]
|
||
|
+ peinputs show [<number>] [nograph] [v...] [scores] [actions] [utilization]
|
||
|
+ peinputs showdot [<number>]
|
||
|
+
|
||
|
+ range :: <n1>:<n2>
|
||
|
+...............
|
||
|
+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 <dmuhamedagic@suse.de>
|
||
|
+#
|
||
|
+# 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 <dmuhamedagic@suse.de>
|
||
|
+#
|
||
|
+# This program is free software; you can redistribute it and/or
|
||
|
+# modify it under the terms of the GNU General Public
|
||
|
+# License as published by the Free Software Foundation; either
|
||
|
+# version 2.1 of the License, or (at your option) any later version.
|
||
|
+#
|
||
|
+# This software is distributed in the hope that it will be useful,
|
||
|
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
+# General Public License for more details.
|
||
|
+#
|
||
|
+# You should have received a copy of the GNU General Public
|
||
|
+# License along with this library; if not, write to the Free Software
|
||
|
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||
|
+#
|
||
|
+
|
||
|
+import 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 <log>.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 <log>.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 {<dir>|<file>|live}"
|
||
|
+ if src != options.history:
|
||
|
+ return self._set_source(src)
|
||
|
+ def limit(self,cmd,from_time,to_time = ''):
|
||
|
+ "usage: limit <from_time> [<to_time>]"
|
||
|
+ 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_level>"
|
||
|
+ 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 <node> [<node> ...]"
|
||
|
+ 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 <rsc> [<rsc> ...]"
|
||
|
+ return crm_report.resource(*args)
|
||
|
+ def node(self,cmd,*args):
|
||
|
+ "usage: node <node> [<node> ...]"
|
||
|
+ return crm_report.node(*args)
|
||
|
+ def log(self,cmd,*args):
|
||
|
+ "usage: log [<node> ...]"
|
||
|
+ 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 [{<range>|<number>} ...]
|
||
|
+ peinputs get [{<range>|<number>} ...]
|
||
|
+ peinputs show [<number>] [nograph] [v...] [scores] [actions] [utilization]
|
||
|
+ peinputs showdot [<number>]"""
|
||
|
+ 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@"
|
||
|
|