diff --git a/python/pacemaker/_cts/cib.py b/python/pacemaker/_cts/cib.py
index 21b6c56139..ada231899f 100644
--- a/python/pacemaker/_cts/cib.py
+++ b/python/pacemaker/_cts/cib.py
@@ -1,408 +1,408 @@
"""CIB generator for Pacemaker's Cluster Test Suite (CTS)."""
__all__ = ["ConfigFactory"]
__copyright__ = "Copyright 2008-2024 the Pacemaker project contributors"
__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"
import warnings
import tempfile
from pacemaker.buildoptions import BuildOptions
from pacemaker._cts.cibxml import Alerts, Clone, Expression, FencingTopology, Group, Nodes, OpDefaults, Option, Resource, Rule
from pacemaker._cts.network import next_ip
class CIB:
"""A class for generating, representing, and installing a CIB file onto cluster nodes."""
def __init__(self, cm, version, factory, tmpfile=None):
"""
Create a new CIB instance.
Arguments:
cm -- A ClusterManager instance
version -- The schema syntax version
factory -- A ConfigFactory instance
tmpfile -- Where to store the CIB, or None to use a new tempfile
"""
# pylint: disable=invalid-name
self._cib = None
self._cm = cm
self._counter = 1
self._factory = factory
self._num_nodes = 0
self.version = version
if not tmpfile:
warnings.filterwarnings("ignore")
# pylint: disable=consider-using-with
f = tempfile.NamedTemporaryFile(delete=True)
f.close()
tmpfile = f.name
warnings.resetwarnings()
self._factory.tmpfile = tmpfile
def _show(self):
"""Query a cluster node for its generated CIB; log and return the result."""
output = ""
- (_, result) = self._factory.rsh(self._factory.target, "HOME=/root CIB_file=%s cibadmin -Ql" % self._factory.tmpfile, verbose=1)
+ (_, result) = self._factory.rsh(self._factory.target, "HOME=/root CIB_file=%s cibadmin -Q" % self._factory.tmpfile, verbose=1)
for line in result:
output += line
self._factory.debug("Generated Config: %s" % line)
return output
def new_ip(self, name=None):
"""Generate an IP resource for the next available IP address, optionally specifying the resource's name."""
if self._cm.env["IPagent"] == "IPaddr2":
ip = next_ip(self._cm.env["IPBase"])
if not name:
if ":" in ip:
(_, _, suffix) = ip.rpartition(":")
name = "r%s" % suffix
else:
name = "r%s" % ip
r = Resource(self._factory, name, self._cm.env["IPagent"], "ocf")
r["ip"] = ip
if ":" in ip:
r["cidr_netmask"] = "64"
r["nic"] = "eth0"
else:
r["cidr_netmask"] = "32"
else:
if not name:
name = "r%s%d" % (self._cm.env["IPagent"], self._counter)
self._counter += 1
r = Resource(self._factory, name, self._cm.env["IPagent"], "ocf")
r.add_op("monitor", "5s")
return r
def get_node_id(self, node_name):
"""Check the cluster configuration for the node ID for the given node_name."""
# We can't account for every possible configuration,
# so we only return a node ID if:
# * The node is specified in /etc/corosync/corosync.conf
# with "ring0_addr:" equal to node_name and "nodeid:"
# explicitly specified.
# In all other cases, we return 0.
node_id = 0
# awkward command: use } as record separator
# so each corosync.conf "object" is one record;
# match the "node {" record that has "ring0_addr: node_name";
# then print the substring of that record after "nodeid:"
awk = r"""awk -v RS="}" """ \
r"""'/^(\s*nodelist\s*{)?\s*node\s*{.*(ring0_addr|name):\s*%s(\s+|$)/""" \
r"""{gsub(/.*nodeid:\s*/,"");gsub(/\s+.*$/,"");print}' %s""" \
% (node_name, BuildOptions.COROSYNC_CONFIG_FILE)
(rc, output) = self._factory.rsh(self._factory.target, awk, verbose=1)
if rc == 0 and len(output) == 1:
try:
node_id = int(output[0])
except ValueError:
node_id = 0
return node_id
def install(self, target):
"""Generate a CIB file and install it to the given cluster node."""
old = self._factory.tmpfile
# Force a rebuild
self._cib = None
self._factory.tmpfile = "%s/cib.xml" % BuildOptions.CIB_DIR
self.contents(target)
self._factory.rsh(self._factory.target, "chown %s %s" % (BuildOptions.DAEMON_USER, self._factory.tmpfile))
self._factory.tmpfile = old
def contents(self, target):
"""Generate a complete CIB file."""
# fencing resource
if self._cib:
return self._cib
if target:
self._factory.target = target
self._factory.rsh(self._factory.target, "HOME=/root cibadmin --empty %s > %s" % (self.version, self._factory.tmpfile))
self._num_nodes = len(self._cm.env["nodes"])
no_quorum = "stop"
if self._num_nodes < 3:
no_quorum = "ignore"
self._factory.log("Cluster only has %d nodes, configuring: no-quorum-policy=ignore" % self._num_nodes)
# We don't need a nodes section unless we add attributes
stn = None
# Fencing resource
# Define first so that the shell doesn't reject every update
if self._cm.env["DoFencing"]:
# Define the "real" fencing device
st = Resource(self._factory, "Fencing", self._cm.env["stonith-type"], "stonith")
# Set a threshold for unreliable stonith devices such as the vmware one
st.add_meta("migration-threshold", "5")
st.add_op("monitor", "120s", timeout="120s")
st.add_op("stop", "0", timeout="60s")
st.add_op("start", "0", timeout="60s")
# For remote node tests, a cluster node is stopped and brought back up
# as a remote node with the name "remote-OLDNAME". To allow fencing
# devices to fence these nodes, create a list of all possible node names.
all_node_names = [prefix + n for n in self._cm.env["nodes"] for prefix in ('', 'remote-')]
# Add all parameters specified by user
entries = self._cm.env["stonith-params"].split(',')
for entry in entries:
try:
(name, value) = entry.split('=', 1)
except ValueError:
print("Warning: skipping invalid fencing parameter: %s" % entry)
continue
# Allow user to specify "all" as the node list, and expand it here
if name in ["hostlist", "pcmk_host_list"] and value == "all":
value = ' '.join(all_node_names)
st[name] = value
st.commit()
# Test advanced fencing logic
stf_nodes = []
stt_nodes = []
attr_nodes = {}
# Create the levels
stl = FencingTopology(self._factory)
for node in self._cm.env["nodes"]:
# Remote node tests will rename the node
remote_node = "remote-%s" % node
# Randomly assign node to a fencing method
ftype = self._cm.env.random_gen.choice(["levels-and", "levels-or ", "broadcast "])
# For levels-and, randomly choose targeting by node name or attribute
by = ""
if ftype == "levels-and":
node_id = self.get_node_id(node)
if node_id == 0 or self._cm.env.random_gen.choice([True, False]):
by = " (by name)"
else:
attr_nodes[node] = node_id
by = " (by attribute)"
self._cm.log(" - Using %s fencing for node: %s%s" % (ftype, node, by))
if ftype == "levels-and":
# If targeting by name, add a topology level for this node
if node not in attr_nodes:
stl.level(1, node, "FencingPass,Fencing")
# Always target remote nodes by name, otherwise we would need to add
# an attribute to the remote node only during remote tests (we don't
# want nonexistent remote nodes showing up in the non-remote tests).
# That complexity is not worth the effort.
stl.level(1, remote_node, "FencingPass,Fencing")
# Add the node (and its remote equivalent) to the list of levels-and nodes.
stt_nodes.extend([node, remote_node])
elif ftype == "levels-or ":
for n in [node, remote_node]:
stl.level(1, n, "FencingFail")
stl.level(2, n, "Fencing")
stf_nodes.extend([node, remote_node])
# If any levels-and nodes were targeted by attribute,
# create the attributes and a level for the attribute.
if attr_nodes:
stn = Nodes(self._factory)
for (node_name, node_id) in attr_nodes.items():
stn.add_node(node_name, node_id, {"cts-fencing": "levels-and"})
stl.level(1, None, "FencingPass,Fencing", "cts-fencing", "levels-and")
# Create a Dummy agent that always passes for levels-and
if stt_nodes:
stt = Resource(self._factory, "FencingPass", "fence_dummy", "stonith")
stt["pcmk_host_list"] = " ".join(stt_nodes)
# Wait this many seconds before doing anything, handy for letting disks get flushed too
stt["random_sleep_range"] = "30"
stt["mode"] = "pass"
stt.commit()
# Create a Dummy agent that always fails for levels-or
if stf_nodes:
stf = Resource(self._factory, "FencingFail", "fence_dummy", "stonith")
stf["pcmk_host_list"] = " ".join(stf_nodes)
# Wait this many seconds before doing anything, handy for letting disks get flushed too
stf["random_sleep_range"] = "30"
stf["mode"] = "fail"
stf.commit()
# Now commit the levels themselves
stl.commit()
o = Option(self._factory)
o["stonith-enabled"] = self._cm.env["DoFencing"]
o["start-failure-is-fatal"] = "false"
o["pe-input-series-max"] = "5000"
o["shutdown-escalation"] = "5min"
o["batch-limit"] = "10"
o["dc-deadtime"] = "5s"
o["no-quorum-policy"] = no_quorum
o.commit()
o = OpDefaults(self._factory)
o["timeout"] = "90s"
o.commit()
# Commit the nodes section if we defined one
if stn is not None:
stn.commit()
# Add an alerts section if possible
if self._factory.rsh.exists_on_all(self._cm.env["notification-agent"], self._cm.env["nodes"]):
alerts = Alerts(self._factory)
alerts.add_alert(self._cm.env["notification-agent"],
self._cm.env["notification-recipient"])
alerts.commit()
# Add resources?
if self._cm.env["CIBResource"]:
self.add_resources()
# generate cib
self._cib = self._show()
if self._factory.tmpfile != "%s/cib.xml" % BuildOptions.CIB_DIR:
self._factory.rsh(self._factory.target, "rm -f %s" % self._factory.tmpfile)
return self._cib
def add_resources(self):
"""Add various resources and their constraints to the CIB."""
# Per-node resources
for node in self._cm.env["nodes"]:
name = "rsc_%s" % node
r = self.new_ip(name)
r.prefer(node, "100")
r.commit()
# Migrator
# Make this slightly sticky (since we have no other location constraints) to avoid relocation during Reattach
m = Resource(self._factory, "migrator", "Dummy", "ocf", "pacemaker")
m["passwd"] = "whatever"
m.add_meta("resource-stickiness", "1")
m.add_meta("allow-migrate", "1")
m.add_op("monitor", "P10S")
m.commit()
# Ping the test exerciser
p = Resource(self._factory, "ping-1", "ping", "ocf", "pacemaker")
p.add_op("monitor", "60s")
p["host_list"] = self._cm.env["cts-exerciser"]
p["name"] = "connected"
p["debug"] = "true"
c = Clone(self._factory, "Connectivity", p)
c["globally-unique"] = "false"
c.commit()
# promotable clone resource
s = Resource(self._factory, "stateful-1", "Stateful", "ocf", "pacemaker")
s.add_op("monitor", "15s", timeout="60s")
s.add_op("monitor", "16s", timeout="60s", role="Promoted")
ms = Clone(self._factory, "promotable-1", s)
ms["promotable"] = "true"
ms["clone-max"] = self._num_nodes
ms["clone-node-max"] = 1
ms["promoted-max"] = 1
ms["promoted-node-max"] = 1
# Require connectivity to run the promotable clone
r = Rule(self._factory, "connected", "-INFINITY", op="or")
r.add_child(Expression(self._factory, "m1-connected-1", "connected", "lt", "1"))
r.add_child(Expression(self._factory, "m1-connected-2", "connected", "not_defined", None))
ms.prefer("connected", rule=r)
ms.commit()
# Group Resource
g = Group(self._factory, "group-1")
g.add_child(self.new_ip())
if self._cm.env["have_systemd"]:
sysd = Resource(self._factory, "petulant", "pacemaker-cts-dummyd@10", "service")
sysd.add_op("monitor", "P10S")
g.add_child(sysd)
else:
g.add_child(self.new_ip())
g.add_child(self.new_ip())
# Make group depend on the promotable clone
g.after("promotable-1", first="promote", then="start")
g.colocate("promotable-1", "INFINITY", withrole="Promoted")
g.commit()
# LSB resource dependent on group-1
if BuildOptions.INIT_DIR is not None:
lsb = Resource(self._factory, "lsb-dummy", "LSBDummy", "lsb")
lsb.add_op("monitor", "5s")
lsb.after("group-1")
lsb.colocate("group-1")
lsb.commit()
class ConfigFactory:
"""Singleton to generate a CIB file for the environment's schema version."""
def __init__(self, cm):
"""
Create a new ConfigFactory instance.
Arguments:
cm -- A ClusterManager instance
"""
# pylint: disable=invalid-name
self._cm = cm
self.rsh = self._cm.rsh
if not self._cm.env["ListTests"]:
self.target = self._cm.env["nodes"][0]
self.tmpfile = None
def log(self, args):
"""Log a message."""
self._cm.log("cib: %s" % args)
def debug(self, args):
"""Log a debug message."""
self._cm.debug("cib: %s" % args)
def create_config(self, name="pacemaker-%s" % BuildOptions.CIB_SCHEMA_VERSION):
"""Return a CIB object for the given schema version."""
return CIB(self._cm, name, self)
diff --git a/python/pacemaker/_cts/patterns.py b/python/pacemaker/_cts/patterns.py
index 46051b6668..083a2af6ac 100644
--- a/python/pacemaker/_cts/patterns.py
+++ b/python/pacemaker/_cts/patterns.py
@@ -1,399 +1,399 @@
"""Pattern-holding classes for Pacemaker's Cluster Test Suite (CTS)."""
__all__ = ["PatternSelector"]
__copyright__ = "Copyright 2008-2024 the Pacemaker project contributors"
__license__ = "GNU General Public License version 2 or later (GPLv2+)"
import argparse
from pacemaker.buildoptions import BuildOptions
class BasePatterns:
"""
The base class for holding a stack-specific set of command and log file/stdout patterns.
Stack-specific classes need to be built on top of this one.
"""
def __init__(self):
"""Create a new BasePatterns instance which holds a very minimal set of basic patterns."""
self._bad_news = []
self._components = {}
self._name = "crm-base"
self._ignore = [
"avoid confusing Valgrind",
# Logging bug in some versions of libvirtd
r"libvirtd.*: internal error: Failed to parse PCI config address",
# pcs can log this when node is fenced, but fencing is OK in some
# tests (and we will catch it in pacemaker logs when not OK)
r"pcs.daemon:No response from: .* request: get_configs, error:",
# This is overbroad, but there's no way to say that only certain
# transition errors are acceptable. We have to rely on causes of a
# transition error logging their own error message, which should
# always be the case.
r"pacemaker-schedulerd.* Calculated transition .*/pe-error",
]
self._commands = {
"StatusCmd": "crmadmin -t 60 -S %s 2>/dev/null",
- "CibQuery": "cibadmin -Ql",
+ "CibQuery": "cibadmin -Q",
"CibAddXml": "cibadmin --modify -c --xml-text %s",
"CibDelXpath": "cibadmin --delete --xpath %s",
"RscRunning": BuildOptions.DAEMON_DIR + "/cts-exec-helper -R -r %s",
"CIBfile": "%s:" + BuildOptions.CIB_DIR + "/cib.xml",
"TmpDir": "/tmp",
"BreakCommCmd": "iptables -A INPUT -s %s -j DROP >/dev/null 2>&1",
"FixCommCmd": "iptables -D INPUT -s %s -j DROP >/dev/null 2>&1",
"MaintenanceModeOn": "cibadmin --modify -c --xml-text ''",
"MaintenanceModeOff": "cibadmin --delete --xpath \"//nvpair[@name='maintenance-mode']\"",
"StandbyCmd": "crm_attribute -Vq -U %s -n standby -l forever -v %s 2>/dev/null",
"StandbyQueryCmd": "crm_attribute -qG -U %s -n standby -l forever -d off 2>/dev/null",
}
self._search = {
"Pat:DC_IDLE": r"pacemaker-controld.*State transition.*-> S_IDLE",
# This won't work if we have multiple partitions
"Pat:Local_started": r"%s\W.*controller successfully started",
"Pat:NonDC_started": r"%s\W.*State transition.*-> S_NOT_DC",
"Pat:DC_started": r"%s\W.*State transition.*-> S_IDLE",
"Pat:We_stopped": r"%s\W.*OVERRIDE THIS PATTERN",
"Pat:They_stopped": r"%s\W.*LOST:.* %s ",
"Pat:They_dead": r"node %s.*: is dead",
"Pat:They_up": r"%s %s\W.*OVERRIDE THIS PATTERN",
"Pat:TransitionComplete": "Transition status: Complete: complete",
"Pat:Fencing_start": r"Requesting peer fencing .* targeting %s",
"Pat:Fencing_ok": r"pacemaker-fenced.*:\s*Operation .* targeting %s by .* for .*@.*: OK",
"Pat:Fencing_recover": r"pacemaker-schedulerd.*: Recover\s+%s",
"Pat:Fencing_active": r"stonith resource .* is active on 2 nodes (attempting recovery)",
"Pat:Fencing_probe": r"pacemaker-controld.* Result of probe operation for %s on .*: Error",
"Pat:RscOpOK": r"pacemaker-controld.*:\s+Result of %s operation for %s.*: (0 \()?OK",
"Pat:RscOpFail": r"pacemaker-schedulerd.*:.*Unexpected result .* recorded for %s of %s ",
"Pat:CloneOpFail": r"pacemaker-schedulerd.*:.*Unexpected result .* recorded for %s of (%s|%s) ",
"Pat:RscRemoteOpOK": r"pacemaker-controld.*:\s+Result of %s operation for %s on %s: (0 \()?OK",
"Pat:NodeFenced": r"pacemaker-controld.*:\s* Peer %s was terminated \(.*\) by .* on behalf of .*: OK",
}
def get_component(self, key):
"""
Return the patterns for a single component as a list, given by key.
This is typically the name of some subprogram (pacemaker-based,
pacemaker-fenced, etc.) or various special purpose keys. If key is
unknown, return an empty list.
"""
if key in self._components:
return self._components[key]
print("Unknown component '%s' for %s" % (key, self._name))
return []
def get_patterns(self, key):
"""
Return various patterns supported by this object, given by key.
Depending on the key, this could either be a list or a hash. If key is
unknown, return None.
"""
if key == "BadNews":
return self._bad_news
if key == "BadNewsIgnore":
return self._ignore
if key == "Commands":
return self._commands
if key == "Search":
return self._search
if key == "Components":
return self._components
print("Unknown pattern '%s' for %s" % (key, self._name))
return None
def __getitem__(self, key):
if key == "Name":
return self._name
if key in self._commands:
return self._commands[key]
if key in self._search:
return self._search[key]
print("Unknown template '%s' for %s" % (key, self._name))
return None
class Corosync2Patterns(BasePatterns):
"""Patterns for Corosync version 2 cluster manager class."""
def __init__(self):
BasePatterns.__init__(self)
self._name = "crm-corosync"
self._commands.update({
"StartCmd": "service corosync start && service pacemaker start",
"StopCmd": "service pacemaker stop; [ ! -e /usr/sbin/pacemaker-remoted ] || service pacemaker_remote stop; service corosync stop",
"EpochCmd": "crm_node -e",
"QuorumCmd": "crm_node -q",
"PartitionCmd": "crm_node -p",
})
self._search.update({
# Close enough ... "Corosync Cluster Engine exiting normally" isn't
# printed reliably.
"Pat:We_stopped": r"%s\W.*Unloading all Corosync service engines",
"Pat:They_stopped": r"%s\W.*pacemaker-controld.*Node %s(\[|\s).*state is now lost",
"Pat:They_dead": r"pacemaker-controld.*Node %s(\[|\s).*state is now lost",
"Pat:They_up": r"\W%s\W.*pacemaker-controld.*Node %s state is now member",
"Pat:ChildExit": r"\[[0-9]+\] exited with status [0-9]+ \(",
# "with signal 9" == pcmk_child_exit(), "$" == check_active_before_startup_processes()
"Pat:ChildKilled": r"%s\W.*pacemakerd.*%s\[[0-9]+\] terminated( with signal 9|$)",
"Pat:ChildRespawn": r"%s\W.*pacemakerd.*Respawning subdaemon %s after unexpected exit",
"Pat:InfraUp": r"%s\W.*corosync.*Initializing transport",
"Pat:PacemakerUp": r"%s\W.*pacemakerd.*Starting Pacemaker",
})
self._ignore += [
r"crm_mon:",
r"crmadmin:",
r"update_trace_data",
r"async_notify:.*strange, client not found",
r"Parse error: Ignoring unknown option .*nodename",
r"error.*: Operation 'reboot' .* using FencingFail returned ",
r"getinfo response error: 1$",
r"sbd.* error: inquisitor_child: DEBUG MODE IS ACTIVE",
r"sbd.* pcmk:\s*error:.*Connection to cib_ro.* (failed|closed)",
]
self._bad_news = [
r"[^(]error:",
r"crit:",
r"ERROR:",
r"CRIT:",
r"Shutting down...NOW",
r"Timer I_TERMINATE just popped",
r"input=I_ERROR",
r"input=I_FAIL",
r"input=I_INTEGRATED cause=C_TIMER_POPPED",
r"input=I_FINALIZED cause=C_TIMER_POPPED",
r"input=I_ERROR",
r"(pacemakerd|pacemaker-execd|pacemaker-controld):.*, exiting",
r"schedulerd.*Attempting recovery of resource",
r"is taking more than 2x its timeout",
r"Confirm not received from",
r"Welcome reply not received from",
r"Attempting to schedule .* after a stop",
r"Resource .* was active at shutdown",
r"duplicate entries for call_id",
r"Search terminated:",
r":global_timer_callback",
r"Faking parameter digest creation",
r"Parameters to .* action changed:",
r"Parameters to .* changed",
r"pacemakerd.*\[[0-9]+\] terminated( with signal|$)",
r"pacemakerd.*\[[0-9]+\] .* will now be killed",
r"pacemaker-schedulerd.*Recover\s+.*\(.* -\> .*\)",
r"rsyslogd.* lost .* due to rate-limiting",
r"Peer is not part of our cluster",
r"We appear to be in an election loop",
r"Unknown node -> we will not deliver message",
r"(Blackbox dump requested|Problem detected)",
r"pacemakerd.*Could not connect to Cluster Configuration Database API",
r"Receiving messages from a node we think is dead",
r"share the same cluster nodeid",
r"share the same name",
r"pacemaker-controld:.*Transition failed: terminated",
r"Local CIB .* differs from .*:",
r"warn.*:\s*Continuing but .* will NOT be used",
r"warn.*:\s*Cluster configuration file .* is corrupt",
r"Election storm",
r"stalled the FSA with pending inputs",
]
self._components["common-ignore"] = [
r"Pending action:",
r"resource( was|s were) active at shutdown",
r"pending LRM operations at shutdown",
r"Lost connection to the CIB manager",
r"pacemaker-controld.*:\s*Action A_RECOVER .* not supported",
r"pacemaker-controld.*:\s*Exiting now due to errors",
r".*:\s*Requesting fencing \([^)]+\) targeting node ",
r"(Blackbox dump requested|Problem detected)",
]
self._components["corosync-ignore"] = [
r"Could not connect to Corosync CFG: CS_ERR_LIBRARY",
r"error:.*Connection to the CPG API failed: Library error",
r"\[[0-9]+\] exited with status [0-9]+ \(",
r"\[[0-9]+\] terminated with signal 15",
r"pacemaker-based.*error:.*Corosync connection lost",
r"pacemaker-fenced.*error:.*Corosync connection terminated",
r"pacemaker-controld.*State transition .* S_RECOVERY",
r"pacemaker-controld.*error:.*Input (I_ERROR|I_TERMINATE ) .*received in state",
r"pacemaker-controld.*error:.*Could not recover from internal error",
r"error:.*Connection to cib_(shm|rw).* (failed|closed)",
r"error:.*cib_(shm|rw) IPC provider disconnected while waiting",
r"error:.*Connection to (fencer|stonith-ng).* (closed|failed|lost)",
r"error: Lost fencer connection",
]
self._components["corosync"] = [
# We expect each daemon to lose its cluster connection.
# However, if the CIB manager loses its connection first,
# it's possible for another daemon to lose that connection and
# exit before losing the cluster connection.
r"pacemakerd.*:\s*warning:.*Lost connection to cluster layer",
r"pacemaker-attrd.*:\s*(crit|error):.*Lost connection to (Corosync process group|the CIB manager)",
r"pacemaker-based.*:\s*(crit|error):.*Lost connection to cluster layer",
r"pacemaker-controld.*:\s*(crit|error):.*Lost connection to (cluster layer|the CIB manager)",
r"pacemaker-fenced.*:\s*(crit|error):.*Lost connection to (cluster layer|the CIB manager)",
r"schedulerd.*Scheduling node .* for fencing",
r"pacemaker-controld.*:\s*Peer .* was terminated \(.*\) by .* on behalf of .*:\s*OK",
]
self._components["pacemaker-based"] = [
r"pacemakerd.* pacemaker-attrd\[[0-9]+\] exited with status 102",
r"pacemakerd.* pacemaker-controld\[[0-9]+\] exited with status 1",
r"pacemakerd.* Respawning subdaemon pacemaker-attrd after unexpected exit",
r"pacemakerd.* Respawning subdaemon pacemaker-based after unexpected exit",
r"pacemakerd.* Respawning subdaemon pacemaker-controld after unexpected exit",
r"pacemakerd.* Respawning subdaemon pacemaker-fenced after unexpected exit",
r"pacemaker-.* Connection to cib_.* (failed|closed)",
r"pacemaker-attrd.*:.*Lost connection to the CIB manager",
r"pacemaker-controld.*:.*Lost connection to the CIB manager",
r"pacemaker-controld.*I_ERROR.*handle_cib_disconnect",
r"pacemaker-controld.* State transition .* S_RECOVERY",
r"pacemaker-controld.*: Input I_TERMINATE .*from do_recover",
r"pacemaker-controld.*Could not recover from internal error",
]
self._components["pacemaker-based-ignore"] = [
r"pacemaker-execd.*Connection to (fencer|stonith-ng).* (closed|failed|lost)",
r"pacemaker-controld.*:\s+Result of .* operation for Fencing.*Error \(Lost connection to fencer\)",
r"pacemaker-controld.*:Could not connect to attrd: Connection refused",
]
self._components["pacemaker-execd"] = [
r"pacemaker-controld.*Lost connection to local executor",
r"pacemaker-controld.*I_ERROR.*lrm_connection_destroy",
r"pacemaker-controld.*State transition .* S_RECOVERY",
r"pacemaker-controld.*: Input I_TERMINATE .*from do_recover",
r"pacemaker-controld.*Could not recover from internal error",
r"pacemakerd.*pacemaker-controld\[[0-9]+\] exited with status 1",
r"pacemakerd.* Respawning subdaemon pacemaker-execd after unexpected exit",
r"pacemakerd.* Respawning subdaemon pacemaker-controld after unexpected exit",
]
self._components["pacemaker-execd-ignore"] = [
r"pacemaker-(attrd|controld).*Connection to lrmd.* (failed|closed)",
r"pacemaker-(attrd|controld).*Could not execute alert",
]
self._components["pacemaker-controld"] = [
r"State transition .* -> S_IDLE",
]
self._components["pacemaker-controld-ignore"] = []
self._components["pacemaker-attrd"] = []
self._components["pacemaker-attrd-ignore"] = [
r"pacemaker-controld.*Connection to attrd (IPC failed|closed)",
]
self._components["pacemaker-schedulerd"] = [
r"State transition .* S_RECOVERY",
r"pacemakerd.* Respawning subdaemon pacemaker-controld after unexpected exit",
r"pacemaker-controld\[[0-9]+\] exited with status 1 \(",
r"pacemaker-controld.*Lost connection to the scheduler",
r"pacemaker-controld.*I_ERROR.*save_cib_contents",
r"pacemaker-controld.*: Input I_TERMINATE .*from do_recover",
r"pacemaker-controld.*Could not recover from internal error",
]
self._components["pacemaker-schedulerd-ignore"] = [
r"Connection to pengine.* (failed|closed)",
]
self._components["pacemaker-fenced"] = [
r"error:.*Connection to (fencer|stonith-ng).* (closed|failed|lost)",
r"Lost fencer connection",
r"pacemaker-controld.*Fencer successfully connected",
]
self._components["pacemaker-fenced-ignore"] = [
r"(error|warning):.*Connection to (fencer|stonith-ng).* (closed|failed|lost)",
r"error:.*Lost fencer connection",
r"error:.*Fencer connection failed \(will retry\)",
r"pacemaker-controld.*:\s+Result of .* operation for Fencing.*Error \(Lost connection to fencer\)",
]
self._components["pacemaker-fenced-ignore"].extend(self._components["common-ignore"])
patternVariants = {
"crm-base": BasePatterns,
"crm-corosync": Corosync2Patterns
}
class PatternSelector:
"""Choose from among several Pattern objects and return the information from that object."""
def __init__(self, name="crm-corosync"):
"""
Create a new PatternSelector object.
Instantiate whatever class is given by name. Defaults to Corosync2Patterns
for "crm-corosync" or None. While other objects could be supported in the
future, only this and the base object are supported at this time.
"""
self._name = name
# If no name was given, use the default. Otherwise, look up the appropriate
# class in patternVariants, instantiate it, and use that.
if not name:
self._base = Corosync2Patterns()
else:
self._base = patternVariants[name]()
def get_patterns(self, kind):
"""Call get_patterns on the previously instantiated pattern object."""
return self._base.get_patterns(kind)
def get_template(self, key):
"""
Return a single pattern from the previously instantiated pattern object.
If no pattern exists for the given key, return None.
"""
return self._base[key]
def get_component(self, kind):
"""Call get_component on the previously instantiated pattern object."""
return self._base.get_component(kind)
def __getitem__(self, key):
"""Return the pattern for the given key, or None if it does not exist."""
return self.get_template(key)
# PYTHONPATH=python python python/pacemaker/_cts/patterns.py -k crm-corosync -t StartCmd
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("-k", "--kind", metavar="KIND")
parser.add_argument("-t", "--template", metavar="TEMPLATE")
args = parser.parse_args()
print(PatternSelector(args.kind)[args.template])
diff --git a/tools/crm_failcount.in b/tools/crm_failcount.in
index f70fe78a91..23ef8cdfd7 100755
--- a/tools/crm_failcount.in
+++ b/tools/crm_failcount.in
@@ -1,294 +1,294 @@
#!@BASH_PATH@
#
# Copyright 2009-2018 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#
USAGE_TEXT="Usage: crm_failcount []
Common options:
--help Display this text, then exit
--version Display version information, then exit
-V, --verbose Specify multiple times to increase debug output
-q, --quiet Print only the value (if querying)
Commands:
-G, --query Query the current value of the resource's fail count
-D, --delete Delete resource's recorded failures
Additional Options:
-r, --resource=value Name of the resource to use (required)
-n, --operation=value Name of operation to use (instead of all operations)
-I, --interval=value If operation is specified, its interval
-N, --node=value Use failcount on named node (instead of local node)"
HELP_TEXT="crm_failcount - Query or delete resource fail counts
$USAGE_TEXT"
# These constants must track crm_exit_t values
CRM_EX_OK=0
CRM_EX_USAGE=64
CRM_EX_NOSUCH=105
exit_usage() {
if [ $# -gt 0 ]; then
echo "error:" "$@" >&2
fi
echo
echo "$USAGE_TEXT"
exit $CRM_EX_USAGE
}
warn() {
echo "warning:" "$@" >&2
}
interval_re() {
echo "^[[:blank:]]*([0-9]+)[[:blank:]]*(${1})[[:blank:]]*$"
}
# This function should follow crm_get_interval() as closely as possible
parse_interval() {
INT_S="$1"
INT_8601RE="^P(([0-9]+)Y)?(([0-9]+)M)?(([0-9]+)D)?T?(([0-9]+)H)?(([0-9]+)M)?(([0-9]+)S)?$"
if [[ $INT_S =~ $(interval_re "") ]]; then
echo $(( ${BASH_REMATCH[1]} * 1000 ))
elif [[ $INT_S =~ $(interval_re "s|sec") ]]; then
echo $(( ${BASH_REMATCH[1]} * 1000 ))
elif [[ $INT_S =~ $(interval_re "ms|msec") ]]; then
echo "${BASH_REMATCH[1]}"
elif [[ $INT_S =~ $(interval_re "m|min") ]]; then
echo $(( ${BASH_REMATCH[1]} * 60000 ))
elif [[ $INT_S =~ $(interval_re "h|hr") ]]; then
echo $(( ${BASH_REMATCH[1]} * 3600000 ))
elif [[ $INT_S =~ $(interval_re "us|usec") ]]; then
echo $(( ${BASH_REMATCH[1]} / 1000 ))
elif [[ $INT_S =~ ^P([0-9]+)W$ ]]; then
echo $(( ${BASH_REMATCH[1]} * 604800000 ))
elif [[ $INT_S =~ $INT_8601RE ]]; then
echo $(( ( ${BASH_REMATCH[2]:-0} * 31536000000 ) \
+ ( ${BASH_REMATCH[4]:-0} * 2592000000 ) \
+ ( ${BASH_REMATCH[6]:-0} * 86400000 ) \
+ ( ${BASH_REMATCH[8]:-0} * 3600000 ) \
+ ( ${BASH_REMATCH[10]:-0} * 60000 ) \
+ ( ${BASH_REMATCH[12]:-0} * 1000 ) ))
else
warn "Unrecognized interval, using 0"
echo "0"
fi
}
query_single_attr() {
QSR_TARGET="$1"
QSR_ATTR="$2"
crm_attribute $VERBOSE --quiet --query -t status -d 0 \
-N "$QSR_TARGET" -n "$QSR_ATTR"
}
query_attr_sum() {
QAS_TARGET="$1"
QAS_PREFIX="$2"
# Build xpath to match all transient node attributes with prefix
QAS_XPATH="/cib/status/node_state[@uname='${QAS_TARGET}']"
QAS_XPATH="${QAS_XPATH}/transient_attributes/instance_attributes"
QAS_XPATH="${QAS_XPATH}/nvpair[starts-with(@name,'$QAS_PREFIX')]"
# Query attributes that match xpath
# @TODO We ignore stderr because we don't want "no results" to look
# like an error, but that also makes $VERBOSE pointless.
- QAS_ALL=$(cibadmin --query --sync-call --local \
+ QAS_ALL=$(cibadmin --query --sync-call \
--xpath="$QAS_XPATH" 2>/dev/null)
QAS_EX=$?
# "No results" is not an error
if [ $QAS_EX -ne $CRM_EX_OK ] && [ $QAS_EX -ne $CRM_EX_NOSUCH ]; then
echo "error: could not query CIB for fail counts" >&2
exit $QAS_EX
fi
# Extract the attribute values (one per line) from the output
QAS_VALUE=$(echo "$QAS_ALL" | sed -n -e \
's/.*.*/\1/p')
# Sum the values
QAS_SUM=0
for i in 0 $QAS_VALUE; do
if [ "$i" = "INFINITY" ]; then
QAS_SUM="INFINITY"
break
else
QAS_SUM=$(($QAS_SUM + $i))
fi
done
if [ "$QAS_SUM" = "INFINITY" ]; then
echo $QAS_SUM
elif [ "$QAS_SUM" -ge 1000000 ]; then
echo "INFINITY"
else
echo $QAS_SUM
fi
}
query_failcount() {
QF_TARGET="$1"
QF_RESOURCE="$2"
QF_OPERATION="$3"
QF_INTERVAL="$4"
QF_ATTR_RSC="fail-count-${QF_RESOURCE}"
if [ -n "$QF_OPERATION" ]; then
QF_ATTR_DISPLAY="${QF_ATTR_RSC}#${QF_OPERATION}_${QF_INTERVAL}"
QF_COUNT=$(query_single_attr "$QF_TARGET" "$QF_ATTR_DISPLAY")
else
QF_ATTR_DISPLAY="$QF_ATTR_RSC"
QF_COUNT=$(query_attr_sum "$QF_TARGET" "${QF_ATTR_RSC}#")
fi
# @COMPAT attributes set < 1.1.17:
# If we didn't find any per-operation failcount,
# check whether there is a legacy per-resource failcount.
if [ "$QF_COUNT" = "0" ]; then
QF_COUNT=$(query_single_attr "$QF_TARGET" "$QF_ATTR_RSC")
if [ "$QF_COUNT" != "0" ]; then
QF_ATTR_DISPLAY="$QF_ATTR_RSC"
fi
fi
# Echo result (comparable to crm_attribute, for backward compatibility)
if [ -n "$QUIET" ]; then
echo $QF_COUNT
else
echo "scope=status name=$QF_ATTR_DISPLAY value=$QF_COUNT"
fi
}
clear_failcount() {
CF_TARGET="$1"
CF_RESOURCE="$2"
CF_OPERATION="$3"
CF_INTERVAL="$4"
if [ -n "$CF_OPERATION" ]; then
CF_OPERATION="-n $CF_OPERATION -I ${CF_INTERVAL}ms"
fi
crm_resource $QUIET $VERBOSE --cleanup \
-N "$CF_TARGET" -r "$CF_RESOURCE" $CF_OPERATION
}
QUIET=""
VERBOSE=""
command=""
resource=""
operation=""
interval="0"
target=$(crm_node -n 2>/dev/null)
SHORTOPTS="qDGQVN:U:v:i:l:r:n:I:"
LONGOPTS_COMMON="help,version,verbose,quiet"
LONGOPTS_COMMANDS="query,delete"
LONGOPTS_OTHER="resource:,node:,operation:,interval:"
LONGOPTS_COMPAT="delete-attr,get-value,resource-id:,uname:,lifetime:,attr-value:,attr-id:"
LONGOPTS="$LONGOPTS_COMMON,$LONGOPTS_COMMANDS,$LONGOPTS_OTHER,$LONGOPTS_COMPAT"
TEMP=$(@GETOPT_PATH@ -o $SHORTOPTS --long $LONGOPTS -n crm_failcount -- "$@")
if [ $? -ne 0 ]; then
exit_usage
fi
eval set -- "$TEMP" # Quotes around $TEMP are essential
while true ; do
case "$1" in
--help)
echo "$HELP_TEXT"
exit $CRM_EX_OK
;;
--version)
crm_attribute --version
exit $?
;;
-q|-Q|--quiet)
QUIET="--quiet"
shift
;;
-V|--verbose)
VERBOSE="$VERBOSE $1"
shift
;;
-G|--query|--get-value)
command="--query"
shift
;;
-D|--delete|--delete-attr)
command="--delete"
shift
;;
-r|--resource|--resource-id)
resource="$2"
shift 2
;;
-n|--operation)
operation="$2"
shift 2
;;
-I|--interval)
interval="$2"
shift 2
;;
-N|--node|-U|--uname)
target="$2"
shift 2
;;
-v|--attr-value)
if [ "$2" = "0" ]; then
command="--delete"
else
warn "ignoring deprecated option '$1' with nonzero value"
fi
shift 2
;;
-i|--attr-id|-l|--lifetime)
warn "ignoring deprecated option '$1'"
shift 2
;;
--)
shift
break
;;
*)
exit_usage "unknown option '$1'"
;;
esac
done
[ -n "$command" ] || exit_usage "must specify a command"
[ -n "$resource" ] || exit_usage "resource name required"
[ -n "$target" ] || exit_usage "node name required"
interval=$(parse_interval $interval)
if [ "$command" = "--query" ]; then
query_failcount "$target" "$resource" "$operation" "$interval"
else
clear_failcount "$target" "$resource" "$operation" "$interval"
fi
diff --git a/tools/crm_report.in b/tools/crm_report.in
index 181887969e..91a8d70726 100644
--- a/tools/crm_report.in
+++ b/tools/crm_report.in
@@ -1,481 +1,481 @@
#!/bin/sh
#
# Copyright 2010-2019 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#
TEMP=`@GETOPT_PATH@ \
-o hv?xl:f:t:n:T:L:p:c:dSCu:D:MVse: \
--long help,corosync,cts:,cts-log:,dest:,node:,nodes:,from:,to:,sos-mode,logfile:,as-directory,single-node,cluster:,user:,max-depth:,version,features,rsh: \
-n 'crm_report' -- "$@"`
# The quotes around $TEMP are essential
eval set -- "$TEMP"
progname=$(basename "$0")
rsh="ssh -T"
tests=""
nodes=""
compress=1
cluster="any"
ssh_user="root"
search_logs=1
sos_mode=0
report_data=`dirname $0`
maxdepth=5
extra_logs=""
sanitize_patterns="passw.*"
log_patterns="CRIT: ERROR:"
usage() {
cat< "$l_base/$HALOG_F"
fi
for node in $nodes; do
cat <$l_base/.env
LABEL="$label"
REPORT_HOME="$r_base"
REPORT_MASTER="$host"
REPORT_TARGET="$node"
LOG_START=$start
LOG_END=$end
REMOVE=1
SANITIZE="$sanitize_patterns"
CLUSTER=$cluster
LOG_PATTERNS="$log_patterns"
EXTRA_LOGS="$extra_logs"
SEARCH_LOGS=$search_logs
SOS_MODE=$sos_mode
verbose=$verbose
maxdepth=$maxdepth
EOF
if [ $host = $node ]; then
cat <>$l_base/.env
REPORT_HOME="$l_base"
EOF
cat $l_base/.env $report_data/report.common $report_data/report.collector > $l_base/collector
bash $l_base/collector
else
cat $l_base/.env $report_data/report.common $report_data/report.collector \
| $rsh -l $ssh_user $node -- "mkdir -p $r_base; cat > $r_base/collector; bash $r_base/collector" | (cd $l_base && tar mxf -)
fi
done
analyze $l_base > $l_base/$ANALYSIS_F
if [ -f $l_base/$HALOG_F ]; then
node_events $l_base/$HALOG_F > $l_base/$EVENTS_F
fi
for node in $nodes; do
cat $l_base/$node/$ANALYSIS_F >> $l_base/$ANALYSIS_F
if [ -s $l_base/$node/$EVENTS_F ]; then
cat $l_base/$node/$EVENTS_F >> $l_base/$EVENTS_F
elif [ -s $l_base/$HALOG_F ]; then
awk "\$4==\"$nodes\"" $l_base/$EVENTS_F >> $l_base/$n/$EVENTS_F
fi
done
log " "
if [ $compress = 1 ]; then
fname=`shrink $l_base`
rm -rf $l_base
log "Collected results are available in $fname"
log " "
log "Please create a bug entry at"
log " @BUG_URL@"
log "Include a description of your problem and attach this tarball"
log " "
log "Thank you for taking time to create this report."
else
log "Collected results are available in $l_base"
fi
log " "
}
#
# check if files have same content in the cluster
#
cibdiff() {
d1=$(dirname $1)
d2=$(dirname $2)
if [ -f "$d1/RUNNING" ] && [ ! -f "$d2/RUNNING" ]; then
DIFF_OK=0
elif [ -f "$d1/STOPPED" ] && [ ! -f "$d2/STOPPED" ]; then
DIFF_OK=0
else
DIFF_OK=1
fi
if [ $DIFF_OK -eq 1 ]; then
if which crm_diff > /dev/null 2>&1; then
crm_diff -c -n $1 -o $2
else
info "crm_diff(8) not found, cannot diff CIBs"
fi
else
echo "can't compare cibs from running and stopped systems"
fi
}
diffcheck() {
[ -f "$1" ] || {
echo "$1 does not exist"
return 1
}
[ -f "$2" ] || {
echo "$2 does not exist"
return 1
}
case $(basename "$1") in
$CIB_F) cibdiff $1 $2 ;;
*) diff -u $1 $2 ;;
esac
}
#
# remove duplicates if files are same, make links instead
#
consolidate() {
for n in $nodes; do
if [ -f $1/$2 ]; then
rm $1/$n/$2
else
mv $1/$n/$2 $1
fi
ln -s ../$2 $1/$n
done
}
analyze_one() {
rc=0
node0=""
for n in $nodes; do
if [ "$node0" ]; then
diffcheck $1/$node0/$2 $1/$n/$2
rc=$(($rc+$?))
else
node0=$n
fi
done
return $rc
}
analyze() {
flist="$MEMBERSHIP_F $CIB_F $CRM_MON_F $SYSINFO_F"
for f in $flist; do
printf "Diff $f... "
ls $1/*/$f >/dev/null 2>&1 || {
echo "no $1/*/$f :/"
continue
}
if analyze_one $1 $f; then
echo "OK"
[ "$f" != $CIB_F ] && consolidate $1 $f
else
echo ""
fi
done
}
do_cts() {
test_sets=`echo $tests | tr ',' ' '`
for test_set in $test_sets; do
start_time=0
start_test=`echo $test_set | tr '-' ' ' | awk '{print $1}'`
end_time=0
end_test=`echo $test_set | tr '-' ' ' | awk '{print $2}'`
if [ x$end_test = x ]; then
msg="Extracting test $start_test"
label="CTS-$start_test-`date +"%b-%d-%Y"`"
end_test=`expr $start_test + 1`
else
msg="Extracting tests $start_test to $end_test"
label="CTS-$start_test-$end_test-`date +"%b-%d-%Y"`"
end_test=`expr $end_test + 1`
fi
if [ $start_test = 0 ]; then
start_pat="BEGINNING [0-9].* TESTS"
else
start_pat="Running test.*\[ *$start_test\]"
fi
if [ x$ctslog = x ]; then
ctslog=`findmsg 1 "$start_pat"`
if [ x$ctslog = x ]; then
fatal "No CTS control file detected"
else
log "Using CTS control file: $ctslog"
fi
fi
line=`grep -n "$start_pat" $ctslog | tail -1 | sed 's/:.*//'`
if [ ! -z "$line" ]; then
start_time=`linetime $ctslog $line`
fi
line=`grep -n "Running test.*\[ *$end_test\]" $ctslog | tail -1 | sed 's/:.*//'`
if [ ! -z "$line" ]; then
end_time=`linetime $ctslog $line`
fi
if [ -z "$nodes" ]; then
nodes=`grep CTS: $ctslog | grep -v debug: | grep " \* " | sed s:.*\\\*::g | sort -u | tr '\\n' ' '`
log "Calculated node list: $nodes"
fi
if [ $end_time -lt $start_time ]; then
debug "Test didn't complete, grabbing everything up to now"
end_time=`date +%s`
fi
if [ $start_time != 0 ];then
log "$msg (`time2str $start_time` to `time2str $end_time`)"
collect_data $label $start_time $end_time $ctslog
else
fatal "$msg failed: not found"
fi
done
}
node_names_from_xml() {
awk '
/uname/ {
for( i=1; i<=NF; i++ )
if( $i~/^uname=/ ) {
sub("uname=.","",$i);
sub("\".*","",$i);
print $i;
next;
}
}
' | tr '\n' ' '
}
getnodes() {
cluster="$1"
# 1. Live (cluster nodes or Pacemaker Remote nodes)
# TODO: This will not detect Pacemaker Remote nodes unless they
# have ever had a permanent node attribute set, because it only
# searches the nodes section. It should also search the config
# for resources that create Pacemaker Remote nodes.
- cib_nodes=$(cibadmin -Ql -o nodes 2>/dev/null)
+ cib_nodes=$(cibadmin -Q -o nodes 2>/dev/null)
if [ $? -eq 0 ]; then
debug "Querying CIB for nodes"
echo "$cib_nodes" | node_names_from_xml
return
fi
# 2. Saved
if [ -f "@CRM_CONFIG_DIR@/cib.xml" ]; then
debug "Querying on-disk CIB for nodes"
grep "node " "@CRM_CONFIG_DIR@/cib.xml" | node_names_from_xml
return
fi
# 3. logs
# TODO: Look for something like crm_update_peer
}
if [ $compress -eq 1 ]; then
require_tar
fi
if [ "x$tests" != "x" ]; then
do_cts
elif [ "x$start_time" != "x" ]; then
masterlog=""
if [ -z "$sanitize_patterns" ]; then
log "WARNING: The tarball produced by this program may contain"
log " sensitive information such as passwords."
log ""
log "We will attempt to remove such information if you use the"
log "-p option. For example: -p \"pass.*\" -p \"user.*\""
log ""
log "However, doing this may reduce the ability for the recipients"
log "to diagnose issues and generally provide assistance."
log ""
log "IT IS YOUR RESPONSIBILITY TO PROTECT SENSITIVE DATA FROM EXPOSURE"
log ""
fi
# If user didn't specify a cluster stack, make a best guess if possible.
if [ -z "$cluster" ] || [ "$cluster" = "any" ]; then
cluster=$(get_cluster_type)
fi
# If user didn't specify node(s), make a best guess if possible.
if [ -z "$nodes" ]; then
nodes=`getnodes $cluster`
if [ -n "$nodes" ]; then
log "Calculated node list: $nodes"
else
fatal "Cannot determine nodes; specify --nodes or --single-node"
fi
fi
if
echo $nodes | grep -qs $host
then
debug "We are a cluster node"
else
debug "We are a log master"
masterlog=`findmsg 1 "pacemaker-controld\\|CTS"`
fi
if [ -z $end_time ]; then
end_time=`perl -e 'print time()'`
fi
label="pcmk-`date +"%a-%d-%b-%Y"`"
log "Collecting data from $nodes (`time2str $start_time` to `time2str $end_time`)"
collect_data $label $start_time $end_time $masterlog
else
fatal "Not sure what to do, no tests or time ranges to extract"
fi
# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 textwidth=80:
diff --git a/tools/report.collector.in b/tools/report.collector.in
index 394d43f7cd..dd7acf5d06 100644
--- a/tools/report.collector.in
+++ b/tools/report.collector.in
@@ -1,885 +1,885 @@
#
# Originally based on hb_report
# Copyright 2007 Dejan Muhamedagic
# Later changes copyright 2010-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#
if
echo $REPORT_HOME | grep -qs '^/'
then
debug "Using full path to working directory: $REPORT_HOME"
else
REPORT_HOME="$HOME/$REPORT_HOME"
debug "Canonicalizing working directory path: $REPORT_HOME"
fi
detect_host
#
# find files newer than a and older than b
#
isnumber() {
echo "$*" | grep -qs '^[0-9][0-9]*$'
}
touchfile() {
t=`mktemp` &&
perl -e "\$file=\"$t\"; \$tm=$1;" -e 'utime $tm, $tm, $file;' &&
echo $t
}
find_files_clean() {
[ -z "$from_stamp" ] || rm -f "$from_stamp"
[ -z "$to_stamp" ] || rm -f "$to_stamp"
from_stamp=""
to_stamp=""
}
find_files() {
dirs=
from_time=$2
to_time=$3
for d in $1; do
if [ -d $d ]; then
dirs="$dirs $d"
fi
done
if [ x"$dirs" = x ]; then
return
fi
isnumber "$from_time" && [ "$from_time" -gt 0 ] || {
warning "sorry, can't find files in [ $1 ] based on time if you don't supply time"
return
}
trap find_files_clean 0
if ! from_stamp=`touchfile $from_time`; then
warning "sorry, can't create temporary file for find_files"
return
fi
findexp="-newer $from_stamp"
if isnumber "$to_time" && [ "$to_time" -gt 0 ]; then
if ! to_stamp=`touchfile $to_time`; then
warning "sorry, can't create temporary file for find_files"
find_files_clean
return
fi
findexp="$findexp ! -newer $to_stamp"
fi
find $dirs -type f $findexp
find_files_clean
trap "" 0
}
#
# check permissions of files/dirs
#
pl_checkperms() {
perl -e '
# check permissions and ownership
# uid and gid are numeric
# everything must match exactly
# no error checking! (file should exist, etc)
($filename, $perms, $in_uid, $in_gid) = @ARGV;
($mode,$uid,$gid) = (stat($filename))[2,4,5];
$p=sprintf("%04o", $mode & 07777);
$p ne $perms and exit(1);
$uid ne $in_uid and exit(1);
$gid ne $in_gid and exit(1);
' $*
}
num_id() {
getent $1 $2 | awk -F: '{print $3}'
}
chk_id() {
[ "$2" ] && return 0
echo "$1: id not found"
return 1
}
check_perms() {
while read type f p uid gid; do
if [ ! -e "$f" ]; then
echo "$f doesn't exist"
continue
elif [ ! -$type "$f" ]; then
echo "$f has wrong type"
continue
fi
n_uid=`num_id passwd $uid`
chk_id "$uid" "$n_uid" || continue
n_gid=`num_id group $gid`
chk_id "$gid" "$n_gid" || continue
pl_checkperms $f $p $n_uid $n_gid || {
echo "wrong permissions or ownership for $f:"
ls -ld $f
}
done
}
#
# coredumps
#
findbinary() {
random_binary=`which cat 2>/dev/null` # suppose we are lucky
binary=`gdb $random_binary $1 < /dev/null 2>/dev/null |
grep 'Core was generated' | awk '{print $5}' |
sed "s/^.//;s/[.':]*$//"`
if [ x = x"$binary" ]; then
debug "Could not detect the program name for core $1 from the gdb output; will try with file(1)"
binary=$(file $1 | awk '/from/{
for( i=1; i<=NF; i++ )
if( $i == "from" ) {
print $(i+1)
break
}
}')
binary=`echo $binary | tr -d "'"`
binary=$(echo $binary | tr -d '`')
if [ "$binary" ]; then
binary=`which $binary 2>/dev/null`
fi
fi
if [ x = x"$binary" ]; then
warning "Could not find the program path for core $1"
return
fi
fullpath=`which $binary 2>/dev/null`
if [ x = x"$fullpath" ]; then
if [ -x $CRM_DAEMON_DIR/$binary ]; then
echo $CRM_DAEMON_DIR/$binary
debug "Found the program at $CRM_DAEMON_DIR/$binary for core $1"
else
warning "Could not find the program path for core $1"
fi
else
echo $fullpath
debug "Found the program at $fullpath for core $1"
fi
}
getbt() {
which gdb > /dev/null 2>&1 || {
warning "Please install gdb to get backtraces"
return
}
for corefile; do
absbinpath=`findbinary $corefile`
[ x = x"$absbinpath" ] && continue
echo "====================== start backtrace ======================"
ls -l $corefile
# Summary first...
gdb -batch -n -quiet -ex ${BT_OPTS:-"thread apply all bt"} -ex quit \
$absbinpath $corefile 2>/dev/null
echo "====================== start detail ======================"
# Now the unreadable details...
gdb -batch -n -quiet -ex ${BT_OPTS:-"thread apply all bt full"} -ex quit \
$absbinpath $corefile 2>/dev/null
echo "======================= end backtrace ======================="
done
}
dump_status_and_config() {
crm_mon -1 2>&1 | grep -v '^Last upd' > $target/$CRM_MON_F
- cibadmin -Ql 2>/dev/null > $target/${CIB_F}.live
+ cibadmin -Q 2>/dev/null > $target/${CIB_F}.live
}
getconfig() {
cluster=$1; shift;
target=$1; shift;
for cf in $*; do
if [ -e "$cf" ]; then
cp -a "$cf" $target/
fi
done
if is_running pacemaker-controld; then
dump_status_and_config
crm_node -p > "$target/$MEMBERSHIP_F" 2>&1
echo "$host" > $target/RUNNING
elif is_running pacemaker-remoted; then
dump_status_and_config
echo "$host" > $target/RUNNING
# Pre-2.0.0 daemon name in case we're collecting on a mixed-version cluster
elif is_running pacemaker_remoted; then
dump_status_and_config
echo "$host" > $target/RUNNING
else
echo "$host" > $target/STOPPED
fi
}
get_readable_cib() {
target="$1"; shift;
if [ -f "$target/$CIB_F" ]; then
crm_verify -V -x "$target/$CIB_F" >"$target/$CRM_VERIFY_F" 2>&1
if which crm >/dev/null 2>&1 ; then
CIB_file="$target/$CIB_F" crm configure show >"$target/$CIB_TXT_F" 2>&1
elif which pcs >/dev/null 2>&1 ; then
pcs config -f "$target/$CIB_F" >"$target/$CIB_TXT_F" 2>&1
fi
fi
}
#
# remove values of sensitive attributes
#
# this is not proper xml parsing, but it will work under the
# circumstances
sanitize_xml_attrs() {
sed $(
for patt in $SANITIZE; do
echo "-e /name=\"$patt\"/s/value=\"[^\"]*\"/value=\"****\"/"
done
)
}
sanitize_hacf() {
awk '
$1=="stonith_host"{ for( i=5; i<=NF; i++ ) $i="****"; }
{print}
'
}
sanitize_one_clean() {
[ -z "$tmp" ] || rm -f "$tmp"
tmp=""
[ -z "$ref" ] || rm -f "$ref"
ref=""
}
sanitize() {
file=$1
compress=""
if [ -z "$SANITIZE" ]; then
return
fi
echo $file | grep -qs 'gz$' && compress=gzip
echo $file | grep -qs 'bz2$' && compress=bzip2
if [ "$compress" ]; then
decompress="$compress -dc"
else
compress=cat
decompress=cat
fi
trap sanitize_one_clean 0
tmp=`mktemp`
ref=`mktemp`
if [ -z "$tmp" -o -z "$ref" ]; then
sanitize_one_clean
fatal "cannot create temporary files"
fi
touch -r $file $ref # save the mtime
if [ "`basename $file`" = ha.cf ]; then
sanitize_hacf
else
$decompress | sanitize_xml_attrs | $compress
fi < $file > $tmp
mv $tmp $file
# note: cleaning $tmp up is still needed even after it's renamed
# because its temp directory is still there.
touch -r $ref $file
sanitize_one_clean
trap "" 0
}
#
# get some system info
#
distro() {
if
which lsb_release >/dev/null 2>&1
then
lsb_release -d | sed -e 's/^Description:\s*//'
debug "Using lsb_release for distribution info"
return
fi
relf=`ls /etc/debian_version 2>/dev/null` ||
relf=`ls /etc/slackware-version 2>/dev/null` ||
relf=`ls -d /etc/*-release 2>/dev/null` && {
for f in $relf; do
test -f $f && {
echo "`ls $f` `cat $f`"
debug "Found `echo $relf | tr '\n' ' '` distribution release file(s)"
return
}
done
}
warning "No lsb_release, no /etc/*-release, no /etc/debian_version: no distro information"
}
pkg_ver() {
if which dpkg >/dev/null 2>&1 ; then
pkg_mgr="deb"
elif which rpm >/dev/null 2>&1 ; then
pkg_mgr="rpm"
elif which pkg_info >/dev/null 2>&1 ; then
pkg_mgr="pkg_info"
elif which pkginfo >/dev/null 2>&1 ; then
pkg_mgr="pkginfo"
else
warning "Unknown package manager"
return
fi
debug "The package manager is: $pkg_mgr"
echo "The package manager is: $pkg_mgr"
echo "Installed packages:"
case $pkg_mgr in
deb)
dpkg-query -f '${Package} ${Version} ${Architecture}\n' -W | sort
echo
for pkg in $*; do
if dpkg-query -W $pkg 2>/dev/null ; then
debug "Verifying installation of: $pkg"
echo "Verifying installation of: $pkg"
debsums -s $pkg 2>/dev/null
fi
done
;;
rpm)
rpm -qa --qf '%{name} %{version}-%{release} - %{distribution} %{arch}\n' | sort
echo
for pkg in $*; do
if rpm -q $pkg >/dev/null 2>&1 ; then
debug "Verifying installation of: $pkg"
echo "Verifying installation of: $pkg"
rpm --verify $pkg 2>&1
fi
done
;;
pkg_info)
pkg_info
;;
pkginfo)
pkginfo | awk '{print $3}' # format?
;;
esac
}
getbacktraces() {
debug "Looking for backtraces: $*"
flist=$(
for f in `find_files "$CRM_CORE_DIRS" $1 $2`; do
bf=`basename $f`
test `expr match $bf core` -gt 0 &&
echo $f
done)
if [ "$flist" ]; then
for core in $flist; do
log "Found core file: `ls -al $core`"
done
# Make a copy of them in case we need more data later
# Luckily they compress well
mkdir cores >/dev/null 2>&1
cp -a $flist cores/
shrink cores
rm -rf cores
# Now get as much as we can from them automagically
for f in $flist; do
getbt $f
done
fi
}
getpeinputs() {
if [ -n "$PCMK_SCHEDULER_INPUT_DIR" ]; then
flist=$(
find_files "$PCMK_SCHEDULER_INPUT_DIR" "$1" "$2" | sed "s,`dirname $PCMK_SCHEDULER_INPUT_DIR`/,,g"
)
if [ "$flist" ]; then
(cd $(dirname "$PCMK_SCHEDULER_INPUT_DIR") && tar cf - $flist) | (cd "$3" && tar xf -)
debug "found `echo $flist | wc -w` scheduler input files in $PCMK_SCHEDULER_INPUT_DIR"
fi
fi
}
getblackboxes() {
flist=$(
find_files $BLACKBOX_DIR $1 $2
)
for bb in $flist; do
bb_short=`basename $bb`
qb-blackbox $bb > $3/${bb_short}.blackbox 2>&1
info "Extracting contents of blackbox: $bb_short"
done
}
#
# some basic system info and stats
#
sys_info() {
cluster=$1; shift
echo "Platform: `uname`"
echo "Kernel release: `uname -r`"
echo "Architecture: `uname -m`"
if [ `uname` = Linux ]; then
echo "Distribution: `distro`"
fi
echo
cibadmin --version 2>&1 | head -1
cibadmin -! 2>&1
case $cluster in
corosync)
/usr/sbin/corosync -v 2>&1 | head -1
;;
esac
# Cluster glue version hash (if available)
stonith -V 2>/dev/null
# Resource agents version hash
echo "resource-agents: `grep 'Build version:' /usr/lib/ocf/resource.d/heartbeat/.ocf-shellfuncs`"
echo
pkg_ver $*
}
sys_stats() {
set -x
uname -n
uptime
ps axf
ps auxw
top -b -n 1
ifconfig -a
ip addr list
netstat -i
arp -an
test -d /proc && {
cat /proc/cpuinfo
}
lsscsi
lspci
lsblk
mount
df
set +x
}
dlm_dump() {
if which dlm_tool >/dev/null 2>&1 ; then
if is_running dlm_controld; then
echo "--- Lockspace overview:"
dlm_tool ls -n
echo "---Lockspace history:"
dlm_tool dump
echo "---Lockspace status:"
dlm_tool status
dlm_tool status -v
echo "---Lockspace config:"
dlm_tool dump_config
dlm_tool log_plock
dlm_tool ls | grep name |
while read X N ; do
echo "--- Lockspace $N:"
dlm_tool lockdump "$N"
dlm_tool lockdebug -svw "$N"
done
fi
fi
}
drbd_info() {
test -f /proc/drbd && {
echo "--- /proc/drbd:"
cat /proc/drbd 2>&1
echo
}
if which drbdadm >/dev/null 2>&1; then
echo "--- drbdadm dump:"
if [ -z "$SANITIZE"]; then
drbdadm dump 2>&1
else
drbdadm dump 2>&1 | sed "s/\(shared-secret[ ]*\"\)[^\"]*\";/\1****\";/"
fi
echo
echo "--- drbdadm status:"
drbdadm status 2>&1
echo
echo "--- drbdadm show-gi:"
for res in $(drbdsetup status | grep -e ^\\S | awk '{ print $1 }'); do
echo "$res:"
drbdadm show-gi $res 2>&1
echo
done
fi
if which drbd-overview >/dev/null 2>&1; then
echo "--- drbd-overview:"
drbd-overview 2>&1
echo
fi
if which drbdsetup >/dev/null 2>&1; then
echo "--- drbdsetup status:"
drbdsetup status --verbose --statistics 2>&1
echo
echo "--- drbdsetup events2:"
drbdsetup events2 --timestamps --statistics --now 2>&1
echo
fi
}
iscfvarset() {
test "`getcfvar $1 $2`"
}
iscfvartrue() {
getcfvar $1 $2 $3 | grep -E -qsi "^(true|y|yes|on|1)"
}
iscfvarfalse() {
getcfvar $1 $2 $3 | grep -E -qsi "^(false|n|no|off|0)"
}
find_syslog() {
priority="$1"
# Always include system logs (if we can find them)
msg="Mark:pcmk:`perl -e 'print time()'`"
logger -p "$priority" "$msg" >/dev/null 2>&1
# Force buffer flush
killall -HUP rsyslogd >/dev/null 2>&1
sleep 2 # Give syslog time to catch up in case it's busy
findmsg 1 "$msg"
}
get_logfiles_cs() {
if [ ! -f "$cf_file" ]; then
return
fi
debug "Reading $cf_type log settings from $cf_file"
# The default value of to_syslog is yes.
if ! iscfvarfalse $cf_type to_syslog "$cf_file"; then
facility_cs=$(getcfvar $cf_type syslog_facility "$cf_file")
if [ -z "$facility_cs" ]; then
facility_cs="daemon"
fi
find_syslog "$facility_cs.info"
fi
if [ "$SOS_MODE" = "1" ]; then
return
fi
if iscfvartrue $cf_type to_logfile "$cf_file"; then
logfile=$(getcfvar $cf_type logfile "$cf_file")
if [ -f "$logfile" ]; then
debug "Log settings found for cluster type $cf_type: $logfile"
echo "$logfile"
fi
fi
}
get_logfiles() {
cf_type=$1
cf_file="$2"
case $cf_type in
corosync) get_logfiles_cs;;
esac
. @CONFIGDIR@/pacemaker
facility="$PCMK_logfacility"
if [ -z "$facility" ]; then
facility="daemon"
fi
if [ "$facility" != "$facility_cs" ]&&[ "$facility" != none ]; then
find_syslog "$facility.notice"
fi
if [ "$SOS_MODE" = "1" ]; then
return
fi
logfile="$PCMK_logfile"
if [ "$logfile" != none ]; then
if [ -z "$logfile" ]; then
for logfile in "@CRM_LOG_DIR@/pacemaker.log" "/var/log/pacemaker.log"; do
if [ -f "$logfile" ]; then
debug "Log settings not found for Pacemaker, assuming $logfile"
echo "$logfile"
break
fi
done
elif [ -f "$logfile" ]; then
debug "Log settings found for Pacemaker: $logfile"
echo "$logfile"
fi
fi
# Look for detail logs:
# - initial pacemakerd logs and tracing might go to a different file
pattern="Starting Pacemaker"
# - make sure we get something from the scheduler
pattern="$pattern\\|Calculated transition"
# - cib and pacemaker-execd updates
# (helpful on non-DC nodes and when cluster has been up for a long time)
pattern="$pattern\\|cib_perform_op\\|process_lrm_event"
# - pacemaker_remote might use a different file
pattern="$pattern\\|pacemaker[-_]remoted:"
findmsg 3 "$pattern"
}
essential_files() {
cat< /dev/null 2>&1
if [ $? -eq 0 ]; then
cl_have_journald=1
else
cl_have_journald=0
fi
cl_lognames="$CL_LOGFILES"
if [ $cl_have_journald -eq 1 ]; then
cl_lognames="$cl_lognames journalctl"
fi
cl_lognames=$(trim "$cl_lognames")
if [ -z "$cl_lognames" ]; then
return
fi
# YYYY-MM-DD HH:MM:SS
cl_start_ymd=$(date -d @${CL_START} +"%F %T")
cl_end_ymd=$(date -d @${CL_END} +"%F %T")
debug "Gathering logs from $cl_start_ymd to $cl_end_ymd:"
debug " $cl_lognames"
# Remove our temporary file if we get interrupted here
trap '[ -z "$cl_pattfile" ] || rm -f "$cl_pattfile"' 0
# Create a temporary file with patterns to grep for
cl_pattfile=$(mktemp) || fatal "cannot create temporary files"
for cl_pattern in $LOG_PATTERNS; do
echo "$cl_pattern"
done > $cl_pattfile
echo "Log pattern matches from $REPORT_TARGET:" > $ANALYSIS_F
if [ -n "$CL_LOGFILES" ]; then
for cl_logfile in $CL_LOGFILES; do
cl_extract="$(basename $cl_logfile).extract.txt"
if [ ! -f "$cl_logfile" ]; then
# Not a file
continue
elif [ -f "$cl_extract" ]; then
# We already have it
continue
fi
dumplogset "$cl_logfile" $LOG_START $LOG_END > "$cl_extract"
sanitize "$cl_extract"
grep -f "$cl_pattfile" "$cl_extract" >> $ANALYSIS_F
done
fi
# Collect systemd logs if present
if [ $cl_have_journald -eq 1 ]; then
journalctl --since "$cl_start_ymd" --until "$cl_end_ymd" > journal.log
grep -f "$cl_pattfile" journal.log >> $ANALYSIS_F
fi
rm -f $cl_pattfile
trap "" 0
}
require_tar
debug "Initializing $REPORT_TARGET subdir"
if [ "$REPORT_MASTER" != "$REPORT_TARGET" ]; then
if [ -e $REPORT_HOME/$REPORT_TARGET ]; then
warning "Directory $REPORT_HOME/$REPORT_TARGET already exists, using /tmp/$$/$REPORT_TARGET instead"
REPORT_HOME=/tmp/$$
fi
fi
mkdir -p $REPORT_HOME/$REPORT_TARGET
cd $REPORT_HOME/$REPORT_TARGET
case $CLUSTER in
any) cluster=`get_cluster_type`;;
*) cluster=$CLUSTER;;
esac
cluster_cf=`find_cluster_cf $cluster`
# If cluster stack is still "any", this might be a Pacemaker Remote node,
# so don't complain in that case.
if [ -z "$cluster_cf" ] && [ $cluster != "any" ]; then
warning "Could not determine the location of your cluster configuration"
fi
if [ "$SEARCH_LOGS" = "1" ]; then
logfiles=$(get_logfiles "$cluster" "$cluster_cf" | sort -u)
fi
logfiles="$(trim "$logfiles $EXTRA_LOGS")"
if [ -z "$logfiles" ]; then
which journalctl > /dev/null 2>&1
if [ $? -eq 0 ]; then
info "Systemd journal will be only log collected"
else
info "No logs will be collected"
fi
info "No log files found or specified with --logfile /some/path"
fi
debug "Config: $cluster ($cluster_cf) $logfiles"
sys_info $cluster $PACKAGES > $SYSINFO_F
essential_files $cluster | check_perms > $PERMISSIONS_F 2>&1
getconfig $cluster "$REPORT_HOME/$REPORT_TARGET" "$cluster_cf" "$CRM_CONFIG_DIR/$CIB_F" "/etc/drbd.conf" "/etc/drbd.d" "/etc/booth"
getpeinputs $LOG_START $LOG_END $REPORT_HOME/$REPORT_TARGET
getbacktraces $LOG_START $LOG_END > $REPORT_HOME/$REPORT_TARGET/$BT_F
getblackboxes $LOG_START $LOG_END $REPORT_HOME/$REPORT_TARGET
case $cluster in
corosync)
if is_running corosync; then
corosync-blackbox >corosync-blackbox-live.txt 2>&1
# corosync-fplay > corosync-blackbox.txt
tool=`pickfirst corosync-objctl corosync-cmapctl`
case $tool in
*objctl) $tool -a > corosync.dump 2>/dev/null;;
*cmapctl) $tool > corosync.dump 2>/dev/null;;
esac
corosync-quorumtool -s -i > corosync.quorum 2>&1
fi
;;
esac
dc=`crm_mon -1 2>/dev/null | awk '/Current DC/ {print $3}'`
if [ "$REPORT_TARGET" = "$dc" ]; then
echo "$REPORT_TARGET" > DC
fi
dlm_dump > $DLM_DUMP_F 2>&1
sys_stats > $SYSSTATS_F 2>&1
drbd_info > $DRBD_INFO_F 2>&1
debug "Sanitizing files: $SANITIZE"
#
# replace sensitive info with '****'
#
cf=""
if [ ! -z "$cluster_cf" ]; then
cf=`basename $cluster_cf`
fi
for f in "$cf" "$CIB_F" "$CIB_F.live" pengine/*; do
if [ -f "$f" ]; then
sanitize "$f"
fi
done
# For convenience, generate human-readable version of CIB and any XML errors
# in it (AFTER sanitizing, so we don't need to sanitize this output).
# sosreport does this itself, so we do not need to when run by sosreport.
if [ "$SOS_MODE" != "1" ]; then
get_readable_cib "$REPORT_HOME/$REPORT_TARGET"
fi
collect_logs "$LOG_START" "$LOG_END" $logfiles
# Purge files containing no information
for f in `ls -1`; do
if [ -d "$f" ]; then
continue
elif [ ! -s "$f" ]; then
case $f in
*core*) log "Detected empty core file: $f";;
*) debug "Removing empty file: `ls -al $f`"
rm -f $f
;;
esac
fi
done
# Parse for events
for l in $logfiles; do
b="$(basename $l).extract.txt"
node_events "$b" > $EVENTS_F
# Link the first logfile to a standard name if it doesn't yet exist
if [ -e "$b" -a ! -e "$HALOG_F" ]; then
ln -s "$b" "$HALOG_F"
fi
done
if [ -e $REPORT_HOME/.env ]; then
debug "Localhost: $REPORT_MASTER $REPORT_TARGET"
elif [ "$REPORT_MASTER" != "$REPORT_TARGET" ]; then
debug "Streaming report back to $REPORT_MASTER"
(cd $REPORT_HOME && tar cf - $REPORT_TARGET)
if [ "$REMOVE" = "1" ]; then
cd
rm -rf $REPORT_HOME
fi
fi
# vim: set expandtab tabstop=8 softtabstop=4 shiftwidth=4 textwidth=80: